1 /*
2 * Classes for nwg-launchers
3 * Copyright (c) 2021 Érico Nogueira
4 * e-mail: ericonr@disroot.org
5 * Copyright (c) 2021 Piotr Miller
6 * e-mail: nwg.piotr@gmail.com
7 * Website: http://nwg.pl
8 * Project: https://github.com/nwg-piotr/nwg-launchers
9 * License: GPL3
10 * */
11
12 #include <unistd.h>
13 #include <glib-unix.h>
14
15 #include <algorithm>
16 #include <array>
17 #include <fstream>
18
19 #include "charconv-compat.h"
20 #include "nwgconfig.h"
21 #include "nwg_classes.h"
22 #include "nwg_exceptions.h"
23 #include "nwg_tools.h"
24
InputParser(int argc,char ** argv)25 InputParser::InputParser (int argc, char **argv) {
26 tokens.reserve(argc - 1);
27 for (int i = 1; i < argc; ++i) {
28 tokens.emplace_back(argv[i]);
29 }
30 }
31
getCmdOption(std::string_view option) const32 std::string_view InputParser::getCmdOption(std::string_view option) const {
33 auto itr = std::find(this->tokens.begin(), this->tokens.end(), option);
34 if (itr != this->tokens.end() && ++itr != this->tokens.end()){
35 return *itr;
36 }
37 return {};
38 }
39
cmdOptionExists(std::string_view option) const40 bool InputParser::cmdOptionExists(std::string_view option) const {
41 return std::find(this->tokens.begin(), this->tokens.end(), option)
42 != this->tokens.end();
43 }
44
get_background_color(double default_opacity) const45 RGBA InputParser::get_background_color(double default_opacity) const {
46 RGBA color{ 0.0, 0.0, 0.0, default_opacity };
47 if (auto opacity_str = getCmdOption("-o"); !opacity_str.empty()) {
48 auto opacity = std::stod(std::string{opacity_str});
49 if (opacity >= 0.0 && opacity <= 1.0) {
50 color.alpha = opacity;
51 } else {
52 Log::error("Opacity must be in range 0.0 to 1.0");
53 }
54 }
55 if (auto color_str = getCmdOption("-b"); !color_str.empty()) {
56 decode_color(color_str, color);
57 }
58 return color;
59 }
60
Config(const InputParser & parser,std::string_view title,std::string_view role,const Glib::RefPtr<Gdk::Screen> & screen)61 Config::Config(const InputParser& parser, std::string_view title, std::string_view role, const Glib::RefPtr<Gdk::Screen>& screen):
62 parser{parser},
63 title{title},
64 role{role}
65 #ifdef HAVE_GTK_LAYER_SHELL
66 ,layer_shell_args{parser}
67 #endif
68 {
69 if (auto wm_name = parser.getCmdOption("-wm"); !wm_name.empty()){
70 this->wm = wm_name;
71 } else {
72 this->wm = detect_wm(screen->get_display(), screen);
73 }
74 Log::info("wm: ", this->wm);
75
76 auto halign_ = parser.getCmdOption("-ha");
77 if (halign_ == "l" || halign_ == "left") { halign = HAlign::Left; }
78 if (halign_ == "r" || halign_ == "right") { halign = HAlign::Right; }
79 auto valign_ = parser.getCmdOption("-va");
80 if (valign_ == "t" || valign_ == "top") { valign = VAlign::Top; }
81 if (valign_ == "b" || valign_ == "bottom") { valign = VAlign::Bottom; }
82
83 if (auto css_name = parser.getCmdOption("-c"); !css_name.empty()) {
84 css_filename = css_name;
85 }
86 }
87
CommonWindow(Config & config)88 CommonWindow::CommonWindow(Config& config): title{config.title} {
89 set_title({config.title.data(), config.title.size()});
90 set_role({config.role.data(), config.role.size()});
91 set_skip_pager_hint(true);
92 add_events(Gdk::KEY_PRESS_MASK | Gdk::KEY_RELEASE_MASK);
93 set_app_paintable(true);
94 check_screen();
95 }
96
title_view()97 std::string_view CommonWindow::title_view() { return title; }
98
on_draw(const Cairo::RefPtr<Cairo::Context> & cr)99 bool CommonWindow::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
100 cr->save();
101 auto [r, g, b, a] = this->background_color;
102 if (_SUPPORTS_ALPHA) {
103 cr->set_source_rgba(r, g, b, a);
104 } else {
105 cr->set_source_rgb(r, g, b);
106 }
107 cr->set_operator(Cairo::OPERATOR_SOURCE);
108 cr->paint();
109 cr->restore();
110 return Gtk::Window::on_draw(cr);
111 }
112
on_screen_changed(const Glib::RefPtr<Gdk::Screen> & previous_screen)113 void CommonWindow::on_screen_changed(const Glib::RefPtr<Gdk::Screen>& previous_screen) {
114 (void) previous_screen; // suppress warning
115 this->check_screen();
116 }
117
check_screen()118 void CommonWindow::check_screen() {
119 auto screen = get_screen();
120 auto visual = screen -> get_rgba_visual();
121
122 if (!visual) {
123 Log::warn("Your screen does not support alpha channels!");
124 }
125 _SUPPORTS_ALPHA = (bool)visual;
126 gtk_widget_set_visual(GTK_WIDGET(gobj()), visual->gobj());
127 }
128
set_background_color(RGBA color)129 void CommonWindow::set_background_color(RGBA color) {
130 this->background_color = color;
131 }
132
get_height()133 int CommonWindow::get_height() { return Gtk::Window::get_height(); }
134
AppBox()135 AppBox::AppBox() {
136 this -> set_always_show_image(true);
137 }
138
AppBox(Glib::ustring name,Glib::ustring exec,Glib::ustring comment)139 AppBox::AppBox(Glib::ustring name, Glib::ustring exec, Glib::ustring comment):
140 Gtk::Button(name, true),
141 name{std::move(name)},
142 exec{std::move(exec)},
143 comment{std::move(comment)}
144 {
145 if (this->name.length() > 25) {
146 this->name.resize(22);
147 this->name.append("...");
148 }
149 this -> set_always_show_image(true);
150 }
151
Instance(Gtk::Application & app,std::string_view name)152 Instance::Instance(Gtk::Application& app, std::string_view name): app{ app } {
153 // TODO: maybe use dbus if it is present?
154 pid_file = get_pid_file(name);
155 pid_file += ".pid";
156 auto lock_file = pid_file;
157 lock_file += ".lock";
158
159 // we'll need this lock file to synchronize us & running instance
160 // note: it doesn't get unlinked when the program exits
161 // so the other instance can safely wait on this file
162 pid_lock_fd = open(lock_file.c_str(), O_CLOEXEC | O_CREAT | O_WRONLY, S_IWUSR | S_IRUSR);
163 if (pid_lock_fd < 0) {
164 int err = errno;
165 throw ErrnoException{ "failed to open pid lock: ", err };
166 }
167
168 // let's try to read pid file
169 if (auto pid = get_instance_pid(pid_file.c_str())) {
170 Log::info("Another instance is running, trying to terminate it...");
171 if (kill(*pid, SIGTERM) != 0) {
172 throw std::runtime_error{ "failed to send SIGTERM to pid" };
173 }
174 Log::plain("Success");
175 }
176
177 // acquire lock
178 // we'll hold this lock until the very exit
179 if (lockf(pid_lock_fd, F_LOCK, 0)) {
180 int err = errno;
181 throw ErrnoException{ "failed to lock the pid lock: ", err };
182 }
183
184 // write instance pid
185 write_instance_pid(pid_file.c_str(), getpid());
186
187 // using glib unix extensions instead of plain signals allows for arbitrary functions to be used
188 // when handling signals
189 g_unix_signal_add(SIGHUP, instance_on_sighup, this);
190 g_unix_signal_add(SIGINT, instance_on_sigint, this);
191 g_unix_signal_add(SIGUSR1, instance_on_sigusr1, this);
192 g_unix_signal_add(SIGTERM, instance_on_sigterm, this);
193 }
194
on_sighup()195 void Instance::on_sighup(){}
on_sigint()196 void Instance::on_sigint(){ app.quit(); }
on_sigusr1()197 void Instance::on_sigusr1() {}
on_sigterm()198 void Instance::on_sigterm(){ app.quit(); }
199
~Instance()200 Instance::~Instance() {
201 // it is important to delete pid file BEFORE releasing the lock
202 // otherwise other instance may overwrite it just before we delete it
203 if (std::error_code err; !fs::remove(pid_file, err) && err) {
204 Log::error("Failed to remove pid file '", pid_file, "': ", err.message());
205 }
206 if (lockf(pid_lock_fd, F_ULOCK, 0)) {
207 int err = errno;
208 Log::error("Failed to unlock pid lock: ", error_description(err));
209 }
210 close(pid_lock_fd);
211 }
212
IconProvider(const Glib::RefPtr<Gtk::IconTheme> & theme,int icon_size)213 IconProvider::IconProvider(const Glib::RefPtr<Gtk::IconTheme>& theme, int icon_size):
214 icon_theme{ theme },
215 icon_size{ icon_size }
216 {
217 constexpr std::array fallback_icons {
218 DATA_DIR_STR "/icon-missing.svg",
219 DATA_DIR_STR "/icon-missing.png"
220 };
221 for (auto && icon: fallback_icons) {
222 try {
223 fallback = Gdk::Pixbuf::create_from_file(
224 icon,
225 icon_size,
226 icon_size,
227 true
228 );
229 break;
230 } catch (const Glib::Error& e) {
231 Log::error("Failed to load fallback icon '", icon, "'");
232 }
233 }
234 if (!fallback) {
235 throw std::runtime_error{ "No fallback icon available" };
236 }
237 }
238
load_icon(const std::string & icon) const239 Gtk::Image IconProvider::load_icon(const std::string& icon) const {
240 if (icon.empty()) {
241 return Gtk::Image{ fallback };
242 }
243 try {
244 if (icon.find_first_of("/") == icon.npos) {
245 return Gtk::Image{ icon_theme->load_icon(icon, icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE) };
246 } else {
247 return Gtk::Image{ Gdk::Pixbuf::create_from_file(icon, icon_size, icon_size, true) };
248 }
249 } catch (const Glib::Error& error) {
250 Log::error("Failed to load icon '", icon, "': ", error.what());
251 }
252 try {
253 return Gtk::Image{ Gdk::Pixbuf::create_from_file("/usr/local/share/pixmaps/" + icon, icon_size, icon_size, true) };
254 } catch (const Glib::Error& error) {
255 Log::error("Failed to load icon '", icon, "': ", error.what());
256 Log::plain("falling back to placeholder");
257 }
258 return Gtk::Image{ fallback };
259 }
260
GenericShell(Config & config)261 GenericShell::GenericShell(Config& config) {
262 // respects_fullscreen is default initialized to true
263 using namespace std::string_view_literals;
264 constexpr std::array wms { "openbox"sv, "i3"sv, "sway"sv };
265 for (auto && wm: wms) {
266 if (config.wm == wm) {
267 respects_fullscreen = false;
268 break;
269 }
270 }
271 }
272
geometry(CommonWindow & window)273 Geometry GenericShell::geometry(CommonWindow& window) {
274 Geometry geo;
275 auto get_geo = [&](auto && monitor) {
276 Gdk::Rectangle rect;
277 monitor->get_geometry(rect);
278 geo.x = rect.get_x();
279 geo.y = rect.get_y();
280 geo.width = rect.get_width();
281 geo.height = rect.get_height();
282 };
283 auto display = window.get_display();
284
285 #ifdef GDK_WINDOWING_X11
286 // only works on X11, reports 0,0 on wayland
287 auto device_mgr = display->get_device_manager();
288 auto device = device_mgr->get_client_pointer();
289 int x, y;
290 device->get_position(x, y);
291 if (auto monitor = display->get_monitor_at_point(x, y)) {
292 get_geo(monitor);
293 } else
294 #endif
295 if (auto monitor = display->get_monitor_at_window(window.get_window())) {
296 get_geo(monitor);
297 } else {
298 throw std::logic_error{ "No monitor at window" };
299 }
300 return geo;
301 }
302
SwayShell(CommonWindow & window,Config & config)303 SwayShell::SwayShell(CommonWindow& window, Config& config):
304 GenericShell{config}
305 {
306 window.set_type_hint(Gdk::WINDOW_TYPE_HINT_SPLASHSCREEN);
307 window.set_decorated(false);
308 using namespace std::string_view_literals;
309 sock_.run("for_window [title="sv, window.title_view(), "*] floating enable"sv);
310 sock_.run("for_window [title="sv, window.title_view(), "*] border none"sv);
311 }
312
show(CommonWindow & window,hint::Fullscreen_)313 void SwayShell::show(CommonWindow& window, hint::Fullscreen_) {
314 // We can not go fullscreen() here:
315 // On sway the window would become opaque - we don't want it
316 // On i3 all windows below will be hidden - we don't want it as well
317 window.show();
318 // works just fine on Sway/i3 as far as I could test
319 // thus, no need to use ipc (I hope)
320 auto [x, y, w, h] = geometry(window);
321 window.resize(w, h);
322 window.move(x, y);
323 }
324
325 #ifdef HAVE_GTK_LAYER_SHELL
LayerShellArgs(const InputParser & parser)326 LayerShellArgs::LayerShellArgs(const InputParser& parser) {
327 using namespace std::string_view_literals;
328 if (auto layer = parser.getCmdOption("-layer-shell-layer"); !layer.empty()) {
329 constexpr std::array map {
330 std::pair{ "BACKGROUND"sv, GTK_LAYER_SHELL_LAYER_BACKGROUND },
331 std::pair{ "BOTTOM"sv, GTK_LAYER_SHELL_LAYER_BOTTOM },
332 std::pair{ "TOP"sv, GTK_LAYER_SHELL_LAYER_TOP },
333 std::pair{ "OVERLAY"sv, GTK_LAYER_SHELL_LAYER_OVERLAY }
334 };
335 bool found = false;
336 for (auto && [s, l]: map) {
337 if (layer == s) {
338 this->layer = l;
339 found = true;
340 break;
341 }
342 }
343 if (!found) {
344 Log::error("Incorrect layer-shell-layer value");
345 std::exit(EXIT_FAILURE);
346 }
347 }
348 if (auto zone = parser.getCmdOption("-layer-shell-exclusive-zone"); !zone.empty()) {
349 this->exclusive_zone_is_auto = zone == "auto"sv;
350 if (!this->exclusive_zone_is_auto) {
351 if (!parse_number(zone, this->exclusive_zone)) {
352 Log::error("Unable to decode layer-shell-exclusive-zone value");
353 std::exit(EXIT_FAILURE);
354 }
355 }
356 }
357 }
358
LayerShell(CommonWindow & window,LayerShellArgs args)359 LayerShell::LayerShell(CommonWindow& window, LayerShellArgs args): args{args} {
360 // this has to be called before the window is realized
361 gtk_layer_init_for_window(window.gobj());
362 }
363 #endif
364
PlatformWindow(Config & config)365 PlatformWindow::PlatformWindow(Config& config):
366 CommonWindow{config},
367 shell{std::in_place_type<GenericShell>, config}
368 {
369 #ifdef HAVE_GTK_LAYER_SHELL
370 if (gtk_layer_is_supported()) {
371 shell.emplace<LayerShell>(*this, config.layer_shell_args);
372 return;
373 }
374 #endif
375 if (config.wm == "sway" || config.wm == "i3") {
376 shell.emplace<SwayShell>(*this, config);
377 }
378 }
379