├── .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 | 
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 |
--------------------------------------------------------------------------------