├── .gitignore ├── APPLICATIONS.md ├── DISCORD.md ├── LICENSE ├── Makefile ├── README.md ├── screenshot.png ├── setup.sh └── src ├── logging.hpp ├── main.cpp ├── rpcpp.hpp └── wm.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | .vscode/* 4 | src/discord 5 | lib/ 6 | rpcpp 7 | tmp/ -------------------------------------------------------------------------------- /APPLICATIONS.md: -------------------------------------------------------------------------------- 1 | # List of supported applications 2 | 3 | 4 | - Blender 5 | 6 | 7 | 8 | - Chrome 9 | 10 | 11 | 12 | - Chromium 13 | 14 | 15 | 16 | - Discord 17 | 18 | 19 | 20 | - Dolphin (Plasma 5) 21 | 22 | 23 | 24 | - Firefox 25 | 26 | 27 | 28 | - GIMP 29 | 30 | 31 | 32 | - Half Life 2 / Garry's Mod 33 | 34 | 35 | 36 | - Hearts Of Iron IV 37 | 38 | 39 | 40 | - Konsole (Plasma 5) 41 | 42 | 43 | 44 | - Lutris 45 | 46 | 47 | 48 | - Minecraft 49 | 50 | 51 | 52 | - st (simple terminal) 53 | 54 | 55 | 56 | - Stardew Valley 57 | 58 | 59 | 60 | - Steam 61 | 62 | 63 | 64 | - surf 65 | 66 | 67 | 68 | - Telegram 69 | 70 | 71 | 72 | - Terraria 73 | 74 | 75 | 76 | - Vivaldi 77 | 78 | 79 | 80 | - VSCode\* 81 | 82 | 83 | 84 | - WorldBox 85 | 86 | 87 | 88 | - XTerm 89 | 90 | VSCode\* : all versions of VSCode are supported (VSCodium, VSCode, VSCode OSS, VSCode Insiders) -------------------------------------------------------------------------------- /DISCORD.md: -------------------------------------------------------------------------------- 1 | # Setting up Discord libs and headers 2 | You need Discord's Game SDK to compile and run RPC++. 3 | ## Automated way 4 | There's a script called `setup.sh`, which will download an unzip Discord Game SDK properly. The script need unzip to be installed! 5 | ## Manual way 6 | Alternatively you can do it yourself if you don't want to use the script or it does not work. 7 | ### Steps 8 | 1. Download [Discord Game SDK](https://dl-game-sdk.discordapp.net/2.5.6/discord_game_sdk.zip) 9 | 1. Extract the downloaded zip file 10 | 1. Copy the files from the extracted `cpp` folder to the project's `src/discord` folder 11 | 2. Copy the files from the extracted `lib/x86_64` folder to the project's lib folder 12 | 13 | --- 14 | Since I don't know what license the SDK has, I did not include it in this repository. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 grialion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=/usr/bin/g++ 2 | 3 | CPPFILES=$(wildcard src/*.cpp) 4 | HPPFILES=$(wildcard src/*.hpp) 5 | LIBFILES=$(wildcard src/discord/*.cpp) 6 | CFLAGS=-Llib/ -l:discord_game_sdk.so -lpthread -lX11 7 | 8 | build/rpcpp: $(CPPFILES) $(HPPFILES) 9 | mkdir -p build 10 | $(CC) $(CPPFILES) $(LIBFILES) $(CFLAGS) -o $@ 11 | 12 | clean: 13 | rm -rf tmp build 14 | 15 | install: build/rpcpp 16 | mkdir -p ${DESTDIR}${PREFIX}/bin 17 | mkdir -p ${DESTDIR}${PREFIX}/lib 18 | cp -f build/rpcpp ${DESTDIR}${PREFIX}/bin 19 | cp -f lib/discord_game_sdk.so ${DESTDIR}${PREFIX}/lib 20 | chmod 755 ${DESTDIR}${PREFIX}/bin/rpcpp 21 | 22 | uninstall: 23 | rm -f ${DESTDIR}${PREFIX}/bin/rpcpp 24 | 25 | .PHONY: clean install uninstall -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RPC++ 2 | RPC++ is a tool for Discord RPC (Rich Presence) to let your friends know about your Linux system 3 | 4 | 5 | ## Installing requirements 6 | ### Arch based systems 7 | ```sh 8 | pacman -S unzip 9 | ``` 10 | ### Debian based systems 11 | ```sh 12 | apt install unzip -y 13 | ``` 14 | 15 | ## Building 16 | **GNU Make**, and **Discord Game SDK** are **required**. To see more information about setting up Discord Game SDK, see [DISCORD.md](./DISCORD.md) 17 | 18 | If you have Arch Linux, please read the AUR section. 19 | 20 | To build RPC++, use the command: 21 | ```sh 22 | make 23 | ``` 24 | 25 | ## Installing & Running 26 | To install RPC++, run the this command: 27 | ```sh 28 | sudo make install 29 | ``` 30 | You can run the app from any directory with 31 | ```sh 32 | rpcpp 33 | ``` 34 | 35 | To run manually (without installing) you need to start `./build/rpcpp` with the variables `LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$(pwd)/lib"` 36 | 37 | ## AUR 38 | RPC++ is available in the Arch User Repository. 39 | 40 | To install, it run the commands: 41 | ```sh 42 | pacman -S --needed base-devel 43 | pacman -S git 44 | git clone https://aur.archlinux.org/rpcpp-git.git 45 | cd rpcpp-git 46 | makepkg -si 47 | ``` 48 | 49 | You can use an AUR helper (for example yay): 50 | ```sh 51 | yay -S rpcpp-git 52 | ``` 53 | 54 | ## Features 55 | - Displays your distro with an icon (supported: Arch, Gentoo, Mint, Ubuntu, Manjaro) 56 | - Displays the focused window's class name with an icon (see supported apps [here](./APPLICATIONS.md)) 57 | - Displays CPU and RAM usage % 58 | - Displays your window manager (WM) 59 | - Displays your uptime 60 | - Refreshes every second 61 | 62 | ![Preview of the rich presence](./screenshot.png) 63 | 64 | ## Will you add more application/distro support? 65 | Sure, let me know on my [discord server](https://grial.tech/discord)! Though I'm pretty sure Discord has a limit of images that can be uploaded per application. 66 | 67 | ## Contributing 68 | You can make pull requests, to improve the code or if you have new ideas, but I don't think I will update the code very often. 69 | 70 | ## Supporting 71 | Want to support me? That's great! Joining my [discord server](https://grial.tech/discord) and subscribing to my [YouTube channel](https://www.youtube.com/channel/UCi-C-JNMVZNpX9kOs2ZLwxw) would help a lot! 72 | 73 | Are you a rich boi? You can send me XMR through this address: 74 | ``` 75 | 48DM6VYH72tRfsBHpLctkNN9KKPCwPM2gU5J4moraS1JHYwLQnS1heA4FHasqYMA66SVnusFFPb3GAyW5yBPBwLRAKJuvT1 76 | ``` -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grialion/rpcpp/008b548917a1b8f463f2293f98e57575dd5e3393/screenshot.png -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v unzip &> /dev/null 4 | then 5 | echo "unzip is required to unzip the downloaded file" 6 | exit 7 | fi 8 | 9 | rm -rf tmp 10 | rm -rf lib 11 | rm -rf src/discord 12 | mkdir tmp 13 | mkdir lib 14 | mkdir -p src/discord 15 | cd tmp 16 | wget "https://dl-game-sdk.discordapp.net/3.2.1/discord_game_sdk.zip" 17 | unzip discord*.zip 18 | cp lib/x86_64/* ../lib/ 19 | cp cpp/* ../src/discord/ 20 | cd ../src/discord/ 21 | 22 | # for some stupid reason you can't compile discord unless the std:: integer types are removed lol 23 | sed s/std::int/int/g -i *.* 24 | sed s/std::uint/uint/g -i *.* 25 | 26 | echo "Successfully set up Discord Game SDK" -------------------------------------------------------------------------------- /src/logging.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | enum LogType 4 | { 5 | INFO, 6 | DEBUG, 7 | WARN, 8 | ERROR 9 | }; 10 | 11 | inline const char *convertLogType(LogType type) 12 | { 13 | switch (type) 14 | { 15 | case DEBUG: 16 | return "DEBUG"; 17 | case WARN: 18 | return "WARN"; 19 | case ERROR: 20 | return "ERROR"; 21 | default: 22 | return "INFO"; 23 | } 24 | } 25 | 26 | void log(string msg, LogType type) 27 | { 28 | if (config.debug) 29 | { 30 | time_t now; 31 | time(&now); 32 | char buf[sizeof "0000-00-00T00:00:00Z"]; 33 | strftime(buf, sizeof buf, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now)); 34 | // build a string to avoid multi threaded mess 35 | string out = string(buf) + " " + convertLogType(type) + ": " + msg + "\n"; 36 | cout << out; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "rpcpp.hpp" 2 | 3 | void *updateRPC(void *ptr) 4 | { 5 | string windowName, lastWindow; 6 | WindowAsset windowAsset; 7 | DistroAsset distroAsset; 8 | DiscordState *state = (struct DiscordState *)ptr; 9 | 10 | log("Waiting for usages to load...", LogType::DEBUG); 11 | 12 | // wait for usages to load 13 | while (cpu == -1 || mem == -1) 14 | { 15 | usleep(1000); 16 | } 17 | 18 | log("Starting RPC loop.", LogType::DEBUG); 19 | distroAsset = getDistroAsset(distro); 20 | 21 | while (true) 22 | { 23 | string cpupercent = to_string((long)cpu); 24 | string rampercent = to_string((long)mem); 25 | 26 | usleep(config.updateSleep * 1000); 27 | 28 | if (!config.noSmallImage) 29 | { 30 | try 31 | { 32 | windowName = getActiveWindowClassName(disp); 33 | } 34 | catch (exception ex) 35 | { 36 | log(ex.what(), LogType::ERROR); 37 | continue; 38 | } 39 | 40 | if (windowName != lastWindow) 41 | { 42 | windowAsset = getWindowAsset(windowName); 43 | lastWindow = windowName; 44 | } 45 | } 46 | 47 | setActivity(*state, string("CPU: " + cpupercent + "% | RAM: " + rampercent + "%"), "WM: " + wm, windowAsset.image, windowAsset.text, distroAsset.image, distroAsset.text, startTime, discord::ActivityType::Playing); 48 | } 49 | } 50 | 51 | void *updateUsage(void *ptr) 52 | { 53 | distro = getDistro(); 54 | log("Distro: " + distro, LogType::DEBUG); 55 | 56 | startTime = time(0) - ms_uptime(); 57 | wm = string(wm_info(disp)); 58 | log("WM: " + wm, LogType::DEBUG); 59 | 60 | while (true) 61 | { 62 | mem = getRAM(); 63 | cpu = getCPU(); 64 | sleep(config.usageSleep / 1000.0); 65 | } 66 | } 67 | 68 | int main(int argc, char **argv) 69 | { 70 | parseConfigs(); 71 | parseArgs(argc, argv); 72 | 73 | if (config.printHelp) 74 | { 75 | cout << helpMsg << endl; 76 | exit(0); 77 | } 78 | if (config.printVersion) 79 | { 80 | cout << "RPC++ version " << VERSION << endl; 81 | exit(0); 82 | } 83 | 84 | int waitedTime = 0; 85 | while (!processRunning("discord") && !config.ignoreDiscord) 86 | { 87 | if (waitedTime > 60) 88 | { 89 | log(string("Discord is not running for ") + to_string(waitedTime) + " seconds. Maybe ignore Discord check with --ignore-discord or -f?", LogType::INFO); 90 | } 91 | log("Waiting for Discord...", LogType::INFO); 92 | waitedTime += 5; 93 | sleep(5); 94 | } 95 | 96 | disp = XOpenDisplay(NULL); 97 | 98 | if (!disp) 99 | { 100 | cout << "Can't open display" << endl; 101 | return -1; 102 | } 103 | 104 | static int (*old_error_handler)(Display *, XErrorEvent *); 105 | trapped_error_code = 0; 106 | old_error_handler = XSetErrorHandler(error_handler); 107 | 108 | // Compile all regexes 109 | compileAllRegexes(); 110 | 111 | pthread_t updateThread; 112 | pthread_t usageThread; 113 | pthread_create(&usageThread, 0, updateUsage, 0); 114 | log("Created usage thread", LogType::DEBUG); 115 | 116 | DiscordState state{}; 117 | 118 | discord::Core *core{}; 119 | auto result = discord::Core::Create(934099338374824007, DiscordCreateFlags_Default, &core); // change with your own app's id if you made one 120 | state.core.reset(core); 121 | if (!state.core) 122 | { 123 | cout << "Failed to instantiate discord core! (err " << static_cast(result) 124 | << ")\n"; 125 | exit(-1); 126 | } 127 | 128 | if (config.debug) 129 | { 130 | state.core->SetLogHook( 131 | discord::LogLevel::Debug, [](discord::LogLevel level, const char *message) 132 | { cerr << "Log(" << static_cast(level) << "): " << message << "\n"; }); 133 | } 134 | 135 | pthread_create(&updateThread, 0, updateRPC, ((void *)&state)); 136 | log("Threads started.", LogType::DEBUG); 137 | log("Xorg version " + to_string(XProtocolVersion(disp)), LogType::DEBUG); // this is kinda dumb to do since it shouldn't be anything else other than 11, but whatever 138 | log("Connected to Discord.", LogType::INFO); 139 | 140 | signal(SIGINT, [](int) 141 | { interrupted = true; }); 142 | 143 | do 144 | { 145 | state.core->RunCallbacks(); 146 | 147 | this_thread::sleep_for(chrono::milliseconds(16)); 148 | } while (!interrupted); 149 | 150 | cout << "Exiting..." << endl; 151 | 152 | XCloseDisplay(disp); 153 | 154 | pthread_kill(updateThread, 9); 155 | pthread_kill(usageThread, 9); 156 | 157 | return 0; 158 | } 159 | -------------------------------------------------------------------------------- /src/rpcpp.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Discord RPC 11 | #include "discord/discord.h" 12 | 13 | // X11 libs 14 | #include 15 | #include 16 | #include 17 | 18 | // variables 19 | #define VERSION "2.2.0" 20 | 21 | namespace 22 | { 23 | volatile bool interrupted{false}; 24 | } 25 | namespace fs = std::filesystem; 26 | using namespace std; 27 | 28 | int startTime; 29 | Display *disp; 30 | float mem = -1, cpu = -1; 31 | string distro; 32 | static int trapped_error_code = 0; 33 | string wm; 34 | 35 | vector apps = {"blender", "chrome", "chromium", "discord", "dolphin", "firefox", "gimp", "hl2_linux", "hoi4", "konsole", "lutris", "st", "steam", "surf", "vscode", "worldbox", "xterm"}; // currently supported app icons on discord rpc (replace if you made your own discord application) 36 | map aliases = { 37 | {"vscodium", "vscode"}, {"code", "vscode"}, {"code - [a-z]+", "vscode"}, {"stardew valley", "stardewvalley"}, {"minecraft [a-z0-9.]+", "minecraft"}, {"lunar client [a-z0-9\\(\\)\\.\\-\\/]+", "minecraft"}, {"telegram(desktop)?", "telegram"}, {"terraria\\.bin\\.x86_64", "terraria"}, {"u?xterm", "xterm"}, {"vivaldi(-stable)?", "vivaldi"}}; // for apps with different names 38 | map distros_lsb = {{"Arch|Artix", "archlinux"}, {"LinuxMint", "lmint"}, {"Gentoo", "gentoo"}, {"Ubuntu", "ubuntu"}, {"ManjaroLinux", "manjaro"}}; // distro names in /etc/lsb_release 39 | map distros_os = {{"Arch Linux", "archlinux"}, {"Linux Mint", "lmint"}, {"Gentoo", "gentoo"}, {"Ubuntu", "ubuntu"}, {"Manjaro Linux", "manjaro"}}; // same but in /etc/os-release (fallback) 40 | string helpMsg = string( 41 | "Usage:\n") + 42 | " rpcpp [options]\n\n" + 43 | "Options:\n" + 44 | " -f, --ignore-discord don't check for discord on start\n" + 45 | " --debug print debug messages\n" + 46 | " --usage-sleep=5000 sleep time in milliseconds between updating cpu and ram usages\n" + 47 | " --update-sleep=100 sleep time in milliseconds between updating the rich presence and focused application\n" + 48 | " --no-small-image disable small image in the rich presence (focused application)\n\n" + 49 | " -h, --help display this help and exit\n" + 50 | " -v, --version output version number and exit"; 51 | 52 | // regular expressions 53 | 54 | regex memavailr("MemAvailable: +(\\d+) kB"); 55 | regex memtotalr("MemTotal: +(\\d+) kB"); 56 | regex processRegex("\\/proc\\/\\d+\\/cmdline"); 57 | regex usageRegex("^usage-sleep=(\\d+)$"); 58 | regex updateRegex("^update-sleep=(\\d+)$"); 59 | 60 | vector> aliases_regex = {}; 61 | vector> distros_lsb_regex = {}; 62 | vector> distros_os_regex = {}; 63 | 64 | struct DiscordState 65 | { 66 | discord::User currentUser; 67 | 68 | unique_ptr core; 69 | }; 70 | 71 | struct DistroAsset 72 | { 73 | string image; 74 | string text; 75 | }; 76 | 77 | struct WindowAsset 78 | { 79 | string image; 80 | string text; 81 | }; 82 | 83 | struct Config 84 | { 85 | bool ignoreDiscord = false; 86 | bool debug = false; 87 | int usageSleep = 5000; 88 | int updateSleep = 300; 89 | bool noSmallImage = false; 90 | bool printHelp = false; 91 | bool printVersion = false; 92 | }; 93 | 94 | Config config; 95 | 96 | // local imports 97 | 98 | #include "logging.hpp" 99 | #include "wm.hpp" 100 | 101 | // methods 102 | 103 | static int error_handler(Display *display, XErrorEvent *error) 104 | { 105 | trapped_error_code = error->error_code; 106 | return 0; 107 | } 108 | 109 | string lower(string s) 110 | { 111 | transform(s.begin(), s.end(), s.begin(), 112 | [](unsigned char c) 113 | { return tolower(c); }); 114 | return s; 115 | } 116 | 117 | double ms_uptime(void) 118 | { 119 | FILE *in = fopen("/proc/uptime", "r"); 120 | double retval = 0; 121 | char tmp[256] = {0x0}; 122 | if (in != NULL) 123 | { 124 | fgets(tmp, sizeof(tmp), in); 125 | retval = atof(tmp); 126 | fclose(in); 127 | } 128 | return retval; 129 | } 130 | 131 | float getRAM() 132 | { 133 | ifstream meminfo; 134 | meminfo.open("/proc/meminfo"); 135 | 136 | long total = 0; 137 | long available = 0; 138 | 139 | smatch matcher; 140 | string line; 141 | 142 | while (getline(meminfo, line)) 143 | { 144 | if (regex_search(line, matcher, memavailr)) 145 | { 146 | available = stoi(matcher[1]); 147 | } 148 | else if (regex_search(line, matcher, memtotalr)) 149 | { 150 | total = stoi(matcher[1]); 151 | } 152 | } 153 | 154 | meminfo.close(); 155 | 156 | if (total == 0) 157 | { 158 | return 0; 159 | } 160 | return (float)(total - available) / total * 100; 161 | } 162 | 163 | void setActivity(DiscordState &state, string details, string sstate, string smallimage, string smallimagetext, string largeimage, string largeimagetext, long uptime, discord::ActivityType type) 164 | { 165 | time_t now = time(nullptr); 166 | discord::Activity activity{}; 167 | activity.SetDetails(details.c_str()); 168 | activity.SetState(sstate.c_str()); 169 | activity.GetAssets().SetSmallImage(smallimage.c_str()); 170 | activity.GetAssets().SetSmallText(smallimagetext.c_str()); 171 | activity.GetAssets().SetLargeImage(largeimage.c_str()); 172 | activity.GetAssets().SetLargeText(largeimagetext.c_str()); 173 | activity.GetTimestamps().SetStart(uptime); 174 | activity.SetType(type); 175 | 176 | state.core->ActivityManager().UpdateActivity(activity, [](discord::Result result) 177 | { if(config.debug) log(string((result == discord::Result::Ok) ? "Succeeded" : "Failed") + " updating activity!", LogType::DEBUG); }); 178 | } 179 | 180 | string getActiveWindowClassName(Display *disp) 181 | { 182 | Window root = XDefaultRootWindow(disp); 183 | 184 | char prop[256]; 185 | get_property(disp, root, XA_WINDOW, "_NET_ACTIVE_WINDOW", prop, sizeof(prop)); 186 | 187 | if (prop[0] == '\0') 188 | { 189 | return ""; 190 | } 191 | 192 | XClassHint hint; 193 | int hintStatus = XGetClassHint(disp, *((Window *)prop), &hint); 194 | 195 | if (hintStatus == 0) 196 | { 197 | return ""; 198 | } 199 | 200 | XFree(hint.res_name); 201 | string s(hint.res_class); 202 | XFree(hint.res_class); 203 | 204 | return s; 205 | } 206 | 207 | static unsigned long long lastTotalUser, lastTotalUserLow, lastTotalSys, lastTotalIdle; 208 | 209 | void getLast() 210 | { 211 | FILE *file = fopen("/proc/stat", "r"); 212 | fscanf(file, "cpu %llu %llu %llu %llu", &lastTotalUser, &lastTotalUserLow, 213 | &lastTotalSys, &lastTotalIdle); 214 | fclose(file); 215 | } 216 | 217 | double getCPU() 218 | { 219 | getLast(); 220 | sleep(1); 221 | double percent; 222 | FILE *file; 223 | unsigned long long totalUser, totalUserLow, totalSys, totalIdle, total; 224 | 225 | file = fopen("/proc/stat", "r"); 226 | fscanf(file, "cpu %llu %llu %llu %llu", &totalUser, &totalUserLow, 227 | &totalSys, &totalIdle); 228 | fclose(file); 229 | 230 | if (totalUser < lastTotalUser || totalUserLow < lastTotalUserLow || 231 | totalSys < lastTotalSys || totalIdle < lastTotalIdle) 232 | { 233 | // Overflow detection. Just skip this value. 234 | percent = -1.0; 235 | } 236 | else 237 | { 238 | total = (totalUser - lastTotalUser) + (totalUserLow - lastTotalUserLow) + 239 | (totalSys - lastTotalSys); 240 | percent = total; 241 | total += (totalIdle - lastTotalIdle); 242 | percent /= total; 243 | percent *= 100; 244 | } 245 | 246 | lastTotalUser = totalUser; 247 | lastTotalUserLow = totalUserLow; 248 | lastTotalSys = totalSys; 249 | lastTotalIdle = totalIdle; 250 | 251 | return percent; 252 | } 253 | 254 | bool processRunning(string name, bool ignoreCase = true) 255 | { 256 | 257 | string strReg = "\\/" + name + " ?"; 258 | regex nameRegex; 259 | smatch progmatcher; 260 | 261 | if (ignoreCase) 262 | nameRegex = regex(strReg, regex::icase); 263 | 264 | else 265 | nameRegex = regex(strReg); 266 | 267 | string procs; 268 | smatch isProcessMatcher; 269 | 270 | std::string path = "/proc"; 271 | for (const auto &entry : fs::directory_iterator(path)) 272 | { 273 | if (fs::is_directory(entry.path())) 274 | { 275 | for (const auto &entry2 : fs::directory_iterator(entry.path())) 276 | { 277 | string path = entry2.path(); 278 | if (regex_search(path, isProcessMatcher, processRegex)) 279 | { 280 | ifstream s; 281 | s.open(entry2.path()); 282 | string line; 283 | while (getline(s, line)) 284 | { 285 | if (regex_search(line, progmatcher, nameRegex)) 286 | { 287 | return true; 288 | } 289 | } 290 | } 291 | } 292 | } 293 | } 294 | 295 | return false; 296 | } 297 | 298 | bool in_array(const string &value, const vector &array) 299 | { 300 | return find(array.begin(), array.end(), value) != array.end(); 301 | } 302 | 303 | void parseConfigOption(Config *config, char *option, bool arg) 304 | { 305 | smatch matcher; 306 | string s = option; 307 | 308 | if (arg) 309 | { 310 | if (s == "-h" || s == "--help") 311 | { 312 | config->printHelp = true; 313 | return; 314 | } 315 | 316 | if (s == "-v" || s == "--version") 317 | { 318 | config->printVersion = true; 319 | return; 320 | } 321 | 322 | if (s == "--debug") 323 | { 324 | config->debug = true; 325 | return; 326 | } 327 | 328 | if (!strncmp(option, "--", 2)) 329 | { 330 | s = s.substr(2, s.size() - 2); 331 | } 332 | } 333 | 334 | if (s == "ignore-discord") 335 | { 336 | config->ignoreDiscord = true; 337 | return; 338 | } 339 | 340 | if (s == "no-small-image") 341 | { 342 | config->noSmallImage = true; 343 | return; 344 | } 345 | 346 | if (regex_search(s, matcher, usageRegex)) 347 | { 348 | config->usageSleep = stoi(matcher[1]); 349 | return; 350 | } 351 | 352 | if (regex_search(s, matcher, updateRegex)) 353 | { 354 | config->updateSleep = stoi(matcher[1]); 355 | return; 356 | } 357 | } 358 | 359 | void parseConfig(string configFile, Config *config) 360 | { 361 | ifstream file(configFile); 362 | if (file.is_open()) 363 | { 364 | string line; 365 | while (getline(file, line)) 366 | { 367 | parseConfigOption(config, (char *)line.c_str(), false); 368 | } 369 | file.close(); 370 | } 371 | } 372 | 373 | /** 374 | * @brief Parse default configs 375 | * /etc/rpcpp/config < ~/.config/rpcpp/config 376 | */ 377 | void parseConfigs() 378 | { 379 | char *home = getenv("HOME"); 380 | if (!home) 381 | { 382 | parseConfig("/etc/rpcpp/config", &config); 383 | return; 384 | } 385 | 386 | string configFile = string(home) + "/.config/rpcpp/config"; 387 | parseConfig(configFile, &config); 388 | if (ifstream(configFile).fail()) 389 | { 390 | parseConfig("/etc/rpcpp/config", &config); 391 | } 392 | } 393 | 394 | void parseArgs(int argc, char **argv) 395 | { 396 | for (int i = 1; i < argc; i++) 397 | { 398 | parseConfigOption(&config, argv[i], true); 399 | } 400 | } 401 | 402 | string getDistro() 403 | { 404 | string distro = ""; 405 | string line; 406 | ifstream release; 407 | regex distroreg; 408 | smatch distromatcher; 409 | if (fs::exists("/etc/lsb-release")) 410 | { 411 | distroreg = regex("DISTRIB_ID=\"?([a-zA-Z0-9 ]+)\"?"); 412 | release.open("/etc/lsb-release"); 413 | } 414 | else if (fs::exists("/etc/os-release")) 415 | { 416 | distroreg = regex("NAME=\"?([a-zA-Z0-9 ]+)\"?"); 417 | release.open("/etc/os-release"); 418 | } 419 | else 420 | { 421 | log("Warning: Neither /etc/lsb-release nor /etc/os-release was found. Please install lsb_release or ask your distribution's developer to support os-release.", LogType::DEBUG); 422 | return distro; 423 | } 424 | while (getline(release, line)) 425 | { 426 | if (regex_search(line, distromatcher, distroreg)) 427 | { 428 | distro = distromatcher[1]; 429 | break; 430 | } 431 | } 432 | return distro; 433 | } 434 | 435 | WindowAsset getWindowAsset(string w) 436 | { 437 | WindowAsset window{}; 438 | window.text = w; 439 | if (w == "") 440 | { 441 | window.image = ""; 442 | return window; 443 | } 444 | window.image = "file"; 445 | w = lower(w); 446 | 447 | if (in_array(w, apps)) 448 | { 449 | window.image = w; 450 | } 451 | else 452 | { 453 | for (const auto &kv : aliases_regex) 454 | { 455 | regex r = kv.first; 456 | smatch m; 457 | if (regex_match(w, m, r)) 458 | { 459 | window.image = kv.second; 460 | break; 461 | } 462 | } 463 | } 464 | 465 | return window; 466 | } 467 | 468 | DistroAsset getDistroAsset(string d) 469 | { 470 | DistroAsset dist{}; 471 | dist.text = d + " / RPC++ " + VERSION; 472 | dist.image = "tux"; 473 | 474 | for (const auto &kv : distros_lsb_regex) 475 | { 476 | regex r = kv.first; 477 | smatch m; 478 | if (regex_match(d, m, r)) 479 | { 480 | dist.image = kv.second; 481 | break; 482 | } 483 | } 484 | if (dist.image == "tux") 485 | { 486 | for (const auto &kv : distros_os_regex) 487 | { 488 | regex r = kv.first; 489 | smatch m; 490 | if (regex_match(d, m, r)) 491 | { 492 | dist.image = kv.second; 493 | break; 494 | } 495 | } 496 | } 497 | 498 | return dist; 499 | } 500 | 501 | /** 502 | * @brief Compile strings to regular expressions 503 | */ 504 | void compileRegexes(map *from, vector> *to, bool ignoreCase) 505 | { 506 | 507 | for (const auto &kv : *from) 508 | { 509 | const regex r = regex(kv.first); 510 | to->push_back({r, kv.second}); 511 | } 512 | } 513 | 514 | /** 515 | * @brief Compile all strings to regular expressions 516 | */ 517 | void compileAllRegexes() 518 | { 519 | compileRegexes(&aliases, &aliases_regex, false); 520 | compileRegexes(&distros_lsb, &distros_lsb_regex, true); 521 | compileRegexes(&distros_os, &distros_os_regex, true); 522 | } 523 | -------------------------------------------------------------------------------- /src/wm.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * @brief Get X window property simplified. 5 | * Should be freed after usage. 6 | * 7 | * @param disp Current display 8 | * @param win Current window 9 | * @param xa_prop_type Prop type, equal to the return prop type Atom, otherwise NULL will be returned 10 | * @param prop_name Name of the property that should be queried. Will be converted to a new Atom 11 | * @return 1 on success, 0 on error 12 | */ 13 | static int get_property(Display *disp, Window win, 14 | Atom xa_prop_type, string prop_name, char *ret, size_t ret_length) 15 | { 16 | Atom xa_prop_name; 17 | Atom xa_ret_type; 18 | int ret_format; 19 | unsigned long ret_nitems; 20 | unsigned long ret_bytes_after; 21 | unsigned long tmp_size; 22 | unsigned char *ret_prop; 23 | 24 | xa_prop_name = XInternAtom(disp, prop_name.c_str(), False); 25 | 26 | if (XGetWindowProperty(disp, win, xa_prop_name, 0, (~0L), False, 27 | xa_prop_type, &xa_ret_type, &ret_format, 28 | &ret_nitems, &ret_bytes_after, &ret_prop) != Success) 29 | { 30 | return 0; 31 | } 32 | 33 | if (xa_ret_type != xa_prop_type) 34 | { 35 | log("Invalid return type received: " + to_string(xa_ret_type), LogType::WARN); 36 | XFree(ret_prop); 37 | 38 | return 0; 39 | } 40 | 41 | tmp_size = (ret_format / (16 / sizeof(long))) * ret_nitems; 42 | tmp_size = ret_length < tmp_size ? ret_length : tmp_size; 43 | 44 | memcpy(ret, ret_prop, tmp_size - 1); 45 | ret[tmp_size - 1] = '\0'; 46 | 47 | XFree(ret_prop); 48 | return 1; 49 | } 50 | 51 | string wm_info(Display *disp) 52 | { 53 | Window sup_window[256]; 54 | char wm_name[256]; 55 | 56 | if (!get_property(disp, DefaultRootWindow(disp), 57 | XA_WINDOW, "_NET_SUPPORTING_WM_CHECK", 58 | (char *)sup_window, sizeof(sup_window))) 59 | { 60 | if (!get_property(disp, DefaultRootWindow(disp), 61 | XA_CARDINAL, "_WIN_SUPPORTING_WM_CHECK", 62 | (char *)sup_window, sizeof(sup_window))) 63 | { 64 | cout << "could not get window manager\n"; 65 | } 66 | } 67 | 68 | /* WM_NAME */ 69 | if (!get_property(disp, *sup_window, 70 | XInternAtom(disp, "UTF8_STRING", False), "_NET_WM_NAME", 71 | wm_name, sizeof(wm_name))) 72 | { 73 | if (!get_property(disp, *sup_window, 74 | XA_STRING, "_NET_WM_NAME", 75 | wm_name, sizeof(wm_name))) 76 | { 77 | cout << "could not get window manager name\n"; 78 | } 79 | } 80 | 81 | return wm_name; 82 | } 83 | --------------------------------------------------------------------------------