diff --git a/CMakeLists.txt b/CMakeLists.txt index 58d988e..3925257 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ if (WIN32) target_compile_options(soundux PRIVATE /W4) else() add_executable(soundux ${src}) - target_compile_options(soundux PRIVATE -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture) + target_compile_options(soundux PRIVATE -Wall -Wextra -Werror -pedantic -Wno-unused-lambda-capture -DWNCK_I_KNOW_THIS_IS_UNSTABLE) endif() target_include_directories(soundux SYSTEM PRIVATE "lib/miniaudio") @@ -31,7 +31,13 @@ target_link_libraries(soundux PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) if (UNIX) find_package(X11 REQUIRED) include_directories(${X11_INCLUDE_DIR}) - target_link_libraries(soundux PRIVATE ${X11_LIBRARIES} ${X11_Xinput_LIB}) + + find_package(PkgConfig REQUIRED) + pkg_check_modules(WNCK REQUIRED libwnck-3.0) + + add_definitions(${WNCK_CFLAGS}) + include_directories(${WNCK_LIBRARY_DIRS}) + target_link_libraries(soundux PRIVATE ${X11_LIBRARIES} ${X11_Xinput_LIB} ${WNCK_LIBRARIES}) endif() if (WIN32) target_compile_definitions(soundux PRIVATE _CRT_SECURE_NO_WARNINGS=1 _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS=1 _UNICODE=1) diff --git a/src/core/global/globals.hpp b/src/core/global/globals.hpp index 73839e6..fe61320 100644 --- a/src/core/global/globals.hpp +++ b/src/core/global/globals.hpp @@ -3,6 +3,7 @@ #if defined(__linux__) #include "../../helper/audio/linux/pulse.hpp" #endif +#include "../../helper/icons/icons.hpp" #include "../../helper/threads/processing.hpp" #include "../../ui/ui.hpp" #include "../config/config.hpp" @@ -18,6 +19,7 @@ namespace Soundux inline Objects::Audio gAudio; #if defined(__linux__) inline Objects::Pulse gPulse; + inline Objects::IconFetcher gIcons; #endif inline Objects::Config gConfig; inline Objects::Hotkeys gHotKeys; diff --git a/src/helper/audio/linux/pulse.cpp b/src/helper/audio/linux/pulse.cpp index e9d13a2..6fa18d7 100644 --- a/src/helper/audio/linux/pulse.cpp +++ b/src/helper/audio/linux/pulse.cpp @@ -367,7 +367,7 @@ namespace Soundux::Objects { std::vector fetchedStreams; static const auto recordingStreamRegex = std::regex( - R"rgx((^.*#(\d+)$)|(Driver: (.+))|(Source: (\d+))|(.*process.*binary.* = "(.+)")|(Resample method: (.+)|(.*application.name.* = "(.+)")))rgx"); + R"rgx((^.*#(\d+)$)|(Driver: (.+))|(Source: (\d+))|(.*process.*binary.* = "(.+)")|(Resample method: (.+)|(.*application.name.* = "(.+)"))|(.*application\.process.\id\.* = "(\d+)"))rgx"); PulseRecordingStream stream; std::smatch match; @@ -406,6 +406,10 @@ namespace Soundux::Objects { stream.application = match[12]; } + else if (match[14].matched) + { + stream.pid = std::stoi(match[14]); + } } } if (stream) @@ -424,7 +428,7 @@ namespace Soundux::Objects { std::vector fetchedStreams; static const auto playbackStreamRegex = std::regex( - R"rgx((^.*#(\d+)$)|(Driver: (.+))|(Sink: (\d+))|(.*application\.name.* = "(.+)")|(.*process.*binary.* = "(.+)"))rgx"); + R"rgx((^.*#(\d+)$)|(Driver: (.+))|(Sink: (\d+))|(.*application\.name.* = "(.+)")|(.*process\.binary.* = "(.+)")|(.*application\.process.\id\.* = "(\d+)"))rgx"); PulsePlaybackStream stream; std::smatch match; @@ -459,6 +463,10 @@ namespace Soundux::Objects { stream.name = match[10]; } + else if (match[12].matched) + { + stream.pid = std::stoi(match[12]); + } } } if (stream && stream.name != "soundux") diff --git a/src/helper/audio/linux/pulse.hpp b/src/helper/audio/linux/pulse.hpp index a6762e2..a46581d 100644 --- a/src/helper/audio/linux/pulse.hpp +++ b/src/helper/audio/linux/pulse.hpp @@ -15,8 +15,10 @@ namespace Soundux { std::uint32_t id; std::string name; + std::int32_t pid; std::string driver; std::string source; + std::string appIcon; std::string application; std::string resampleMethod; @@ -30,7 +32,9 @@ namespace Soundux std::uint32_t id; std::string sink; std::string name; + std::int32_t pid; std::string driver; + std::string appIcon; std::string application; operator bool() const diff --git a/src/helper/base64/base64.cpp b/src/helper/base64/base64.cpp new file mode 100644 index 0000000..5bb1994 --- /dev/null +++ b/src/helper/base64/base64.cpp @@ -0,0 +1,309 @@ +/* + base64.cpp and base64.h + + base64 encoding and decoding with C++. + More information at + https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp + + Version: 2.rc.08 (release candidate) + + Copyright (C) 2004-2017, 2020, 2021 René Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +*/ + +#include "base64.hpp" + +#include +#include + +// +// Depending on the url parameter in base64_chars, one of +// two sets of base64 characters needs to be chosen. +// They differ in their last two characters. +// +static const char *base64_chars[2] = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "+/", + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-_"}; + +static unsigned int pos_of_char(const unsigned char chr) +{ + // + // Return the position of chr within base64_encode() + // + + if (chr >= 'A' && chr <= 'Z') + return chr - 'A'; + else if (chr >= 'a' && chr <= 'z') + return chr - 'a' + ('Z' - 'A') + 1; + else if (chr >= '0' && chr <= '9') + return chr - '0' + ('Z' - 'A') + ('z' - 'a') + 2; + else if (chr == '+' || chr == '-') + return 62; // Be liberal with input and accept both url ('-') and non-url ('+') base 64 characters ( + else if (chr == '/' || chr == '_') + return 63; // Ditto for '/' and '_' + else + // + // 2020-10-23: Throw std::exception rather than const char* + //(Pablo Martin-Gomez, https://github.com/Bouska) + // + throw std::runtime_error("Input is not valid base64-encoded data."); +} + +static std::string insert_linebreaks(std::string str, size_t distance) +{ + // + // Provided by https://github.com/JomaCorpFX, adapted by me. + // + if (!str.length()) + { + return ""; + } + + size_t pos = distance; + + while (pos < str.size()) + { + str.insert(pos, "\n"); + pos += distance + 1; + } + + return str; +} + +template static std::string encode_with_line_breaks(String s) +{ + return insert_linebreaks(base64_encode(s, false), line_length); +} + +template static std::string encode_pem(String s) +{ + return encode_with_line_breaks(s); +} + +template static std::string encode_mime(String s) +{ + return encode_with_line_breaks(s); +} + +template static std::string encode(String s, bool url) +{ + return base64_encode(reinterpret_cast(s.data()), s.length(), url); +} + +std::string base64_encode(unsigned char const *bytes_to_encode, size_t in_len, bool url) +{ + + size_t len_encoded = (in_len + 2) / 3 * 4; + + unsigned char trailing_char = url ? '.' : '='; + + // + // Choose set of base64 characters. They differ + // for the last two positions, depending on the url + // parameter. + // A bool (as is the parameter url) is guaranteed + // to evaluate to either 0 or 1 in C++ therefore, + // the correct character set is chosen by subscripting + // base64_chars with url. + // + const char *base64_chars_ = base64_chars[url]; + + std::string ret; + ret.reserve(len_encoded); + + unsigned int pos = 0; + + while (pos < in_len) + { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0xfc) >> 2]); + + if (pos + 1 < in_len) + { + ret.push_back( + base64_chars_[((bytes_to_encode[pos + 0] & 0x03) << 4) + ((bytes_to_encode[pos + 1] & 0xf0) >> 4)]); + + if (pos + 2 < in_len) + { + ret.push_back( + base64_chars_[((bytes_to_encode[pos + 1] & 0x0f) << 2) + ((bytes_to_encode[pos + 2] & 0xc0) >> 6)]); + ret.push_back(base64_chars_[bytes_to_encode[pos + 2] & 0x3f]); + } + else + { + ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]); + ret.push_back(trailing_char); + } + } + else + { + + ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]); + ret.push_back(trailing_char); + ret.push_back(trailing_char); + } + + pos += 3; + } + + return ret; +} + +template static std::string decode(String encoded_string, bool remove_linebreaks) +{ + // + // decode(…) is templated so that it can be used with String = const std::string& + // or std::string_view (requires at least C++17) + // + + if (encoded_string.empty()) + return std::string(); + + if (remove_linebreaks) + { + + std::string copy(encoded_string); + + copy.erase(std::remove(copy.begin(), copy.end(), '\n'), copy.end()); + + return base64_decode(copy, false); + } + + size_t length_of_string = encoded_string.length(); + size_t pos = 0; + + // + // The approximate length (bytes) of the decoded string might be one or + // two bytes smaller, depending on the amount of trailing equal signs + // in the encoded string. This approximation is needed to reserve + // enough space in the string to be returned. + // + size_t approx_length_of_decoded_string = length_of_string / 4 * 3; + std::string ret; + ret.reserve(approx_length_of_decoded_string); + + while (pos < length_of_string) + { + // + // Iterate over encoded input string in chunks. The size of all + // chunks except the last one is 4 bytes. + // + // The last chunk might be padded with equal signs or dots + // in order to make it 4 bytes in size as well, but this + // is not required as per RFC 2045. + // + // All chunks except the last one produce three output bytes. + // + // The last chunk produces at least one and up to three bytes. + // + + size_t pos_of_char_1 = pos_of_char(encoded_string[pos + 1]); + + // + // Emit the first output byte that is produced in each chunk: + // + ret.push_back(static_cast(((pos_of_char(encoded_string[pos + 0])) << 2) + + ((pos_of_char_1 & 0x30) >> 4))); + + if ((pos + 2 < + length_of_string) && // Check for data that is not padded with equal signs (which is allowed by RFC 2045) + encoded_string[pos + 2] != '=' && + encoded_string[pos + 2] != '.' // accept URL-safe base 64 strings, too, so check for '.' also. + ) + { + // + // Emit a chunk's second byte (which might not be produced in the last chunk). + // + unsigned int pos_of_char_2 = pos_of_char(encoded_string[pos + 2]); + ret.push_back( + static_cast(((pos_of_char_1 & 0x0f) << 4) + ((pos_of_char_2 & 0x3c) >> 2))); + + if ((pos + 3 < length_of_string) && encoded_string[pos + 3] != '=' && encoded_string[pos + 3] != '.') + { + // + // Emit a chunk's third byte (which might not be produced in the last chunk). + // + ret.push_back(static_cast(((pos_of_char_2 & 0x03) << 6) + + pos_of_char(encoded_string[pos + 3]))); + } + } + + pos += 4; + } + + return ret; +} + +std::string base64_decode(std::string const &s, bool remove_linebreaks) +{ + return decode(s, remove_linebreaks); +} + +std::string base64_encode(std::string const &s, bool url) +{ + return encode(s, url); +} + +std::string base64_encode_pem(std::string const &s) +{ + return encode_pem(s); +} + +std::string base64_encode_mime(std::string const &s) +{ + return encode_mime(s); +} + +#if __cplusplus >= 201703L +// +// Interface with std::string_view rather than const std::string& +// Requires C++17 +// Provided by Yannic Bonenberger (https://github.com/Yannic) +// + +std::string base64_encode(std::string_view s, bool url) +{ + return encode(s, url); +} + +std::string base64_encode_pem(std::string_view s) +{ + return encode_pem(s); +} + +std::string base64_encode_mime(std::string_view s) +{ + return encode_mime(s); +} + +std::string base64_decode(std::string_view s, bool remove_linebreaks) +{ + return decode(s, remove_linebreaks); +} + +#endif // __cplusplus >= 201703L \ No newline at end of file diff --git a/src/helper/base64/base64.hpp b/src/helper/base64/base64.hpp new file mode 100644 index 0000000..7d77fe7 --- /dev/null +++ b/src/helper/base64/base64.hpp @@ -0,0 +1,35 @@ +// +// base64 encoding and decoding with C++. +// Version: 2.rc.08 (release candidate) +// + +#ifndef BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A +#define BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A + +#include + +#if __cplusplus >= 201703L +#include +#endif // __cplusplus >= 201703L + +std::string base64_encode(std::string const &s, bool url = false); +std::string base64_encode_pem(std::string const &s); +std::string base64_encode_mime(std::string const &s); + +std::string base64_decode(std::string const &s, bool remove_linebreaks = false); +std::string base64_encode(unsigned char const *, size_t len, bool url = false); + +#if __cplusplus >= 201703L +// +// Interface with std::string_view rather than const std::string& +// Requires C++17 +// Provided by Yannic Bonenberger (https://github.com/Yannic) +// +std::string base64_encode(std::string_view s, bool url = false); +std::string base64_encode_pem(std::string_view s); +std::string base64_encode_mime(std::string_view s); + +std::string base64_decode(std::string_view s, bool remove_linebreaks = false); +#endif // __cplusplus >= 201703L + +#endif /* BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A */ \ No newline at end of file diff --git a/src/helper/icons/icons.cpp b/src/helper/icons/icons.cpp new file mode 100644 index 0000000..c0b6aed --- /dev/null +++ b/src/helper/icons/icons.cpp @@ -0,0 +1,77 @@ +#if defined(__linux__) +#include "icons.hpp" +#include "../base64/base64.hpp" +#include "../misc/misc.hpp" +#include +#include + +namespace Soundux::Objects +{ + void IconFetcher::setup() + { + gdk_init(nullptr, nullptr); + screen = wnck_screen_get_default(); + if (!screen) + { + Fancy::fancy.logTime().warning() << "Failed to get default screen!" << std::endl; + } + } + std::optional IconFetcher::getIcon(int pid, bool recursive) + { + if (recursive) + { + auto parentProcess = Helpers::getPpid(pid); + if (parentProcess) + { + auto recursiveResult = getIcon(*parentProcess, false); + if (recursiveResult) + { + return recursiveResult; + } + } + } + if (cache.find(pid) != cache.end()) + { + return cache.at(pid); + } + + wnck_screen_force_update(screen); + auto *windows = wnck_screen_get_windows(screen); + + for (auto *item = windows; item != nullptr; item = item->next) + { + auto *window = reinterpret_cast(item->data); + auto _pid = wnck_window_get_pid(window); + + if (pid == _pid) + { + auto *icon = wnck_window_get_icon(window); + + gsize size = 2048; + auto *iconBuff = new gchar[2048]; + + GError *error = nullptr; + if (gdk_pixbuf_save_to_buffer(icon, &iconBuff, &size, "png", &error, NULL) != TRUE) + { + Fancy::fancy.logTime().warning() << "Failed to save icon to buffer, error: " << error->message + << "(" << error->code << ")" << std::endl; + return std::nullopt; + } + + auto base64 = base64_encode(reinterpret_cast(iconBuff), 2048, false); + delete[] iconBuff; + + if (cache.find(pid) == cache.end()) + { + cache.insert({pid, base64}); + } + + return base64; + } + } + + Fancy::fancy.logTime().warning() << "Could not find proccess with id " >> pid << std::endl; + return std::nullopt; + } +} // namespace Soundux::Objects +#endif \ No newline at end of file diff --git a/src/helper/icons/icons.hpp b/src/helper/icons/icons.hpp new file mode 100644 index 0000000..0688f1b --- /dev/null +++ b/src/helper/icons/icons.hpp @@ -0,0 +1,23 @@ +#if defined(__linux__) +#pragma once +#include +#include +#include +#include + +namespace Soundux +{ + namespace Objects + { + class IconFetcher + { + WnckScreen *screen; + std::map cache; + + public: + void setup(); + std::optional getIcon(int pid, bool recursive = true); + }; + } // namespace Objects +} // namespace Soundux +#endif \ No newline at end of file diff --git a/src/helper/json/bindings.hpp b/src/helper/json/bindings.hpp index 4b79b3c..1b53d2c 100644 --- a/src/helper/json/bindings.hpp +++ b/src/helper/json/bindings.hpp @@ -153,18 +153,22 @@ namespace nlohmann static void to_json(json &j, const Soundux::Objects::PulseRecordingStream &obj) { j = {{"id", obj.id}, + {"pid", obj.pid}, {"name", obj.name}, {"driver", obj.driver}, {"source", obj.source}, + {"appIcon", obj.appIcon}, {"application", obj.application}, {"resampleMethod", obj.resampleMethod}}; } static void from_json(const json &j, Soundux::Objects::PulseRecordingStream &obj) { j.at("id").get_to(obj.id); + j.at("pid").get_to(obj.pid); j.at("name").get_to(obj.name); j.at("driver").get_to(obj.driver); j.at("source").get_to(obj.source); + j.at("appIcon").get_to(obj.appIcon); j.at("application").get_to(obj.application); j.at("resampleMethod").get_to(obj.resampleMethod); } @@ -174,17 +178,21 @@ namespace nlohmann static void to_json(json &j, const Soundux::Objects::PulsePlaybackStream &obj) { j = {{"id", obj.id}, + {"pid", obj.pid}, {"name", obj.name}, {"sink", obj.sink}, {"driver", obj.driver}, + {"appIcon", obj.appIcon}, {"application", obj.application}}; } static void from_json(const json &j, Soundux::Objects::PulsePlaybackStream &obj) { j.at("id").get_to(obj.id); + j.at("pid").get_to(obj.pid); j.at("name").get_to(obj.name); j.at("sink").get_to(obj.sink); j.at("driver").get_to(obj.driver); + j.at("appIcon").get_to(obj.appIcon); j.at("application").get_to(obj.application); } }; diff --git a/src/helper/misc/misc.cpp b/src/helper/misc/misc.cpp index 069a4da..4257d24 100644 --- a/src/helper/misc/misc.cpp +++ b/src/helper/misc/misc.cpp @@ -1,5 +1,10 @@ #include "misc.hpp" #include +#include +#include +#include +#include +#include #include #if defined(_WIN32) @@ -50,5 +55,37 @@ namespace Soundux::Helpers return pclose(pipe) == 0; } + std::optional getPpid(int pid) + { + std::filesystem::path path("/proc/" + std::to_string(pid)); + if (std::filesystem::exists(path)) + { + auto statusFile = path / "status"; + if (std::filesystem::exists(statusFile) && std::filesystem::is_regular_file(statusFile)) + { + static const std::regex pidRegex(R"(PPid:(\ +|\t)(\d+))"); + std::ifstream statusStream(statusFile); + + std::string line; + std::smatch match; + while (std::getline(statusStream, line)) + { + if (std::regex_search(line, match, pidRegex)) + { + if (match[2].matched) + { + return std::stoi(match[2]); + } + } + } + + Fancy::fancy.logTime().warning() << "Failed to find ppid of " >> pid << std::endl; + return std::nullopt; + } + } + + Fancy::fancy.logTime().warning() << "Failed to find ppid of " >> pid << ", process does not exist" << std::endl; + return std::nullopt; + } #endif } // namespace Soundux::Helpers \ No newline at end of file diff --git a/src/helper/misc/misc.hpp b/src/helper/misc/misc.hpp index ac12b75..65110f2 100644 --- a/src/helper/misc/misc.hpp +++ b/src/helper/misc/misc.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -10,6 +11,7 @@ namespace Soundux std::wstring widen(const std::string &s); #endif #if defined(__linux__) + std::optional getPpid(int pid); bool exec(const std::string &command, std::string &result); #endif std::vector splitByNewLine(const std::string &str); diff --git a/src/main.cpp b/src/main.cpp index a53b3f7..0d69bb2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -40,6 +40,8 @@ int main() } #if defined(__linux__) + Soundux::Globals::gIcons.setup(); + if (!Soundux::Globals::gPulse.isSwitchOnConnectLoaded()) { Soundux::Globals::gPulse.setup(); diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 24e7172..29c129e 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -551,12 +551,17 @@ namespace Soundux::Objects //* duplicates here. auto streams = Globals::gPulse.getRecordingStreams(); std::vector uniqueStreams; - for (const auto &stream : streams) + for (auto &stream : streams) { auto item = std::find_if(std::begin(uniqueStreams), std::end(uniqueStreams), [&](const auto &_stream) { return stream.name == _stream.name; }); if (item == std::end(uniqueStreams)) { + auto icon = Soundux::Globals::gIcons.getIcon(stream.pid); + if (icon) + { + stream.appIcon = *icon; + } uniqueStreams.emplace_back(stream); } } @@ -567,12 +572,17 @@ namespace Soundux::Objects { auto streams = Globals::gPulse.getPlaybackStreams(); std::vector uniqueStreams; - for (const auto &stream : streams) + for (auto &stream : streams) { auto item = std::find_if(std::begin(uniqueStreams), std::end(uniqueStreams), [&](const auto &_stream) { return stream.name == _stream.name; }); if (item == std::end(uniqueStreams)) { + auto icon = Soundux::Globals::gIcons.getIcon(stream.pid); + if (icon) + { + stream.appIcon = *icon; + } uniqueStreams.emplace_back(stream); } }