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