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 #pragma once
13 
14 #include <array>
15 #include <string>
16 #include <vector>
17 #include <variant>
18 
19 #include <gtkmm.h>
20 #include <glibmm/ustring.h>
21 
22 #ifdef HAVE_GTK_LAYER_SHELL
23 #include <gtk-layer-shell.h>
24 #endif
25 
26 #include "filesystem-compat.h"
27 
28 template <typename ... Os>
29 struct Overloaded: Os... { using Os::operator()...; };
30 template <typename ... Os> Overloaded(Os ...) -> Overloaded<Os...>;
31 
32 struct RGBA {
33     double red;
34     double green;
35     double blue;
36     double alpha;
37 };
38 
39 /*
40  * Argument parser
41  * Credits for this cool class go to iain at https://stackoverflow.com/a/868894
42  * */
43 class InputParser{
44     public:
45         InputParser (int, char **);
46         /// @author iain
47         std::string_view getCmdOption(std::string_view) const;
48         /// @author iain
49         bool cmdOptionExists(std::string_view) const;
50         RGBA get_background_color(double default_opacity) const;
51     private:
52         std::vector <std::string_view> tokens;
53 };
54 
55 #ifdef HAVE_GTK_LAYER_SHELL
56 struct LayerShellArgs {
57     GtkLayerShellLayer layer                  = GTK_LAYER_SHELL_LAYER_OVERLAY;
58     int                exclusive_zone         = -1;
59     bool               exclusive_zone_is_auto = true;
60 
61     LayerShellArgs(const InputParser& parser);
62 };
63 #endif
64 
65 enum class HAlign: unsigned int { NotSpecified = 0, Left, Right };
66 enum class VAlign: unsigned int { NotSpecified = 0, Top, Bottom };
67 
68 /*
69  * Stores configuration data
70  */
71 struct Config {
72     const InputParser& parser;
73     std::string        wm;
74     std::string_view   title;
75     std::string_view   role;
76     HAlign             halign{ HAlign::NotSpecified };
77     VAlign             valign{ VAlign::NotSpecified };
78     fs::path           css_filename{ "style.css" };   // filename relative to config dir
79 
80 #ifdef HAVE_GTK_LAYER_SHELL
81     LayerShellArgs layer_shell_args;
82 #endif
83 
84     Config(const InputParser&, std::string_view, std::string_view, const Glib::RefPtr<Gdk::Screen>&);
85 };
86 
87 class CommonWindow : public Gtk::Window {
88     public:
89         CommonWindow(Config&);
90         virtual ~CommonWindow() = default;
91 
92         void check_screen();
93         void set_background_color(RGBA color);
94 
95         virtual int get_height(); // we need to override get_height for dmenu to work
96 
97         std::string_view title_view();
98     protected:
99         bool on_draw(const ::Cairo::RefPtr< ::Cairo::Context>& cr) override;
100         void on_screen_changed(const Glib::RefPtr<Gdk::Screen>& previous_screen) override;
101     private:
102         std::string_view title;
103         RGBA background_color;
104         bool _SUPPORTS_ALPHA;
105 };
106 
107 class AppBox : public Gtk::Button {
108     public:
109         AppBox();
110         AppBox(Glib::ustring, Glib::ustring, Glib::ustring);
111         AppBox(AppBox&&) = default;
112         AppBox(const AppBox&) = delete;
113 
114         Glib::ustring name;
115         Glib::ustring exec;
116         Glib::ustring comment;
117 
118         virtual ~AppBox() = default;
119 };
120 
121 /*
122  * Stores x, y, width, height
123  * */
124 struct Geometry {
125     int x;
126     int y;
127     int width;
128     int height;
129 };
130 
131 struct DesktopEntry {
132     std::string name;
133     std::string exec;
134     std::string icon;
135     std::string comment;
136     std::string mime_type;
137     bool terminal;
138 };
139 
140 struct Instance {
141     Gtk::Application& app;
142     fs::path pid_file;
143     int      pid_lock_fd;
144 
145     Instance(Gtk::Application& app, std::string_view name);
146     virtual ~Instance();
147     // note: the provided implementation of on_{sigterm,sigint} handlers
148     // calls Gtk::Application::quit, which does NOT call any destructors
149     virtual void on_sigterm();
150     virtual void on_sigusr1();
151     virtual void on_sighup();
152     virtual void on_sigint();
153 };
154 
155 struct IconProvider {
156     Glib::RefPtr<Gtk::IconTheme> icon_theme;
157     Glib::RefPtr<Gdk::Pixbuf>    fallback;
158     int                          icon_size;
159 
160     IconProvider(const Glib::RefPtr<Gtk::IconTheme>& theme, int icon_size);
161     // Returns Gtk::Image out of the icon name of file path
162     // the returned image is scaled to icon_size x icon_size
163     Gtk::Image load_icon(const std::string& icon) const;
164 };
165 
166 enum class SwayError {
167     ConnectFailed,
168     EnvNotSet,
169     OpenFailed,
170     RecvHeaderFailed,
171     RecvBodyFailed,
172     SendHeaderFailed,
173     SendBodyFailed
174 };
175 
176 struct SwaySock {
177     SwaySock();
178     SwaySock(const SwaySock&) = delete;
179     ~SwaySock();
180     // pass the command to sway via socket
181     template <typename ... Ts>
runSwaySock182     void run(Ts ... ts) {
183         auto body_size = (ts.size() + ...);
184         send_header_(body_size, Commands::Run);
185         // should we send it as one message? 1 write() is better than N
186         (send_body_(ts), ...);
187         // should we recv the response?
188         // suppress warning
189         (void)recv_response_();
190     }
191     // swaymsg -t get_outputs
192     std::string get_outputs();
193     std::string get_workspaces();
194 
195     // see sway-ipc (7)
196     enum class Commands: std::uint32_t {
197         Run = 0,
198         GetWorkspaces = 1,
199         GetOutputs = 3
200     };
201     static constexpr std::array MAGIC { 'i', '3', '-', 'i', 'p', 'c' };
202     static constexpr auto MAGIC_SIZE = MAGIC.size();
203     // magic + body length (u32) + type (u32)
204     static constexpr auto HEADER_SIZE = MAGIC_SIZE + 2 * sizeof(std::uint32_t);
205 
206     int                           sock_;
207     std::array<char, HEADER_SIZE> header;
208 
209     void send_header_(std::uint32_t, Commands);
210     void send_body_(std::string_view);
211     std::string recv_response_();
212 };
213 
214 /*
215  * This namespace defines types that can be passed to PlatformWindow::show
216  * The rationale behind this design is as follows:
217  * 1) Logic implementing all the positioning is collected in one place,
218  *      not scattered across classes / spagetti functions
219  * 2) It is resolved at compile time, allowing compiler to optimize it better
220  * 3) It can be tweaked and expanded with ease and safety (everything is checked at compile-time)
221  */
222 namespace hint {
223     constexpr struct Fullscreen_ {} Fullscreen;
224     constexpr struct Center_     {} Center;
225     struct Horizontal{};
226     struct Vertical{};
227     template <typename S>
228     struct Side { bool side; int margin; constexpr static S side_type{}; };
229     struct Sides { Side<Horizontal> h; Side<Vertical> v; };
230 }
231 
232 /*
233  * Each shell defined by which means window is positioned. They do not share common base class,
234  *  but instead wrapped in a variant to avoid unecessary dynamic allocations
235  * Shell::show method receives a window reference and a templated parameter,
236  *   it's job is to position window according to the parameter type
237  * GenericShell only uses common Gtk functions and is best used on X11 or as a fallback
238  * SwayShell uses IPC connection to Sway/i3
239  * LayerShell uses wlr-layer-shell (or rather gtk-layer-shell library built on top of it)
240  */
241 struct GenericShell {
242     GenericShell(Config& config);
243     Geometry geometry(CommonWindow& window);
244     template <typename S> void show(CommonWindow&, S);
245     // some window managers (openbox, notably) do not open window in fullscreen
246     // when requested
247     bool respects_fullscreen = true;
248 };
249 
250 struct SwayShell: GenericShell {
251     SwayShell(CommonWindow& window, Config& config);
252     // use GenericShell::show unless called with Fullscreen
253     using GenericShell::show;
254     void show(CommonWindow& window, hint::Fullscreen_);
255 
256     SwaySock sock_;
257 };
258 
259 #ifdef HAVE_GTK_LAYER_SHELL
260 struct LayerShell {
261     LayerShell(CommonWindow& window, LayerShellArgs args);
262     template <typename S> void show(CommonWindow& window, S);
263 
264     LayerShellArgs args;
265 };
266 #endif
267 
268 struct PlatformWindow: public CommonWindow {
269 public:
270     PlatformWindow(Config& config);
271     void fullscreen();
272     template <typename S> void show(S);
273 private:
274     std::variant<
275 #ifdef HAVE_GTK_LAYER_SHELL
276                  LayerShell,
277 #endif
278                  SwayShell, GenericShell> shell;
279 };
280 
281 template <typename Hint>
show(CommonWindow & window,Hint hint)282 void GenericShell::show(CommonWindow& window, Hint hint) {
283     window.show();
284     window.set_type_hint(Gdk::WINDOW_TYPE_HINT_SPLASHSCREEN);
285     window.set_decorated(false);
286     auto display = geometry(window);
287     auto window_coord_at_side = [](auto d_size, auto w_size, auto side, auto margin) {
288         std::array map { margin, d_size - w_size - margin };
289         return map[side];
290     };
291     Overloaded place_window {
292         [&](hint::Center_) {
293             auto x = display.x + (display.width - window.get_width()) / 2;
294             auto y = display.y + (display.height - window.get_height()) / 2;
295             window.move(x, y);
296         },
297         [&](hint::Fullscreen_) {
298             if (this->respects_fullscreen) {
299                 window.fullscreen();
300             } else {
301                 window.resize(display.width, display.height);
302                 window.move(display.x, display.y);
303             }
304         },
305         [&](hint::Side<hint::Horizontal> hint) {
306             auto w_x = window_coord_at_side(display.width, window.get_width(), hint.side, hint.margin);
307             window.move(display.x + w_x, display.y + (display.height - window.get_height()) / 2);
308         },
309         [&](hint::Side<hint::Vertical> hint) {
310             auto w_y = window_coord_at_side(display.height, window.get_height(), hint.side, hint.margin);
311             window.move(display.x + (display.width - window.get_width()) / 2, display.y + w_y);
312         },
313         [&,display](hint::Sides hint) {
314             auto w_x = window_coord_at_side(display.width, window.get_width(), hint.h.side, hint.h.margin);
315             auto w_y = window_coord_at_side(display.height, window.get_height(), hint.v.side, hint.v.margin);
316             window.move(display.x + w_x, display.y + w_y);
317         }
318     };
319     place_window(hint);
320     window.present();   // grab focus
321 }
322 
323 #ifdef HAVE_GTK_LAYER_SHELL
324 template <typename Hint>
show(CommonWindow & window,Hint hint)325 void LayerShell::show(CommonWindow& window, Hint hint) {
326     std::array<bool, 4> edges{ 0, 0, 0, 0 };
327     std::array<int,  4> margins{ 0, 0, 0, 0 };
328     constexpr Overloaded index { [](hint::Horizontal){ return 0; }, [](hint::Vertical) { return 2; } };
329     auto account_side = [&](auto side) {
330         constexpr auto i = index(side.side_type);
331         edges[i + side.side] = true;
332         margins[i + side.side] = side.margin;
333     };
334     Overloaded set_edges_margins {
335         [&](hint::Center_) { /* nothing to do */ },
336         [&](hint::Fullscreen_) { edges = { 1, 1, 1, 1 }; },
337         [&](hint::Sides hint) { account_side(hint.h); account_side(hint.v); },
338         account_side
339     };
340     set_edges_margins(hint);
341     window.show();
342     auto gtk_win = window.gobj();
343     std::array edges_ {
344         GTK_LAYER_SHELL_EDGE_LEFT,
345         GTK_LAYER_SHELL_EDGE_RIGHT,
346         GTK_LAYER_SHELL_EDGE_TOP,
347         GTK_LAYER_SHELL_EDGE_BOTTOM
348     };
349     for (size_t i = 0; i < 4; i++) {
350         gtk_layer_set_anchor(gtk_win, edges_[i], edges[i]);
351         gtk_layer_set_margin(gtk_win, edges_[i], margins[i]);
352     }
353     gtk_layer_set_layer(gtk_win, args.layer);
354     gtk_layer_set_keyboard_interactivity(gtk_win, true);
355     gtk_layer_set_namespace(gtk_win, window.title_view().data());
356     if (args.exclusive_zone_is_auto) {
357         gtk_layer_auto_exclusive_zone_enable (gtk_win);
358     } else {
359         gtk_layer_set_exclusive_zone(gtk_win, args.exclusive_zone);
360     }
361 }
362 #endif
363 
364 template <typename Hint>
show(Hint h)365 void PlatformWindow::show(Hint h) {
366     std::visit([&](auto& shell){ shell.show(*this, h); }, shell);
367 }
368 
369 
370