1 /*
2  * This file is part of YAD.
3  *
4  * YAD is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * YAD is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with YAD. If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Copyright (C) 2008-2019, Victor Ananjevsky <ananasik@gmail.com>
18  */
19 
20 #include <sys/stat.h>
21 #include <fcntl.h>
22 #include <time.h>
23 #include <string.h>
24 #include <unistd.h>
25 
26 #include "yad.h"
27 
28 typedef struct {
29   gchar *name;
30   gchar *action;
31   gchar *icon;
32 } MenuData;
33 
34 static GtkStatusIcon *status_icon;
35 
36 static gchar *icon = NULL;
37 static gchar *action = NULL;
38 
39 static GSList *menu_data = NULL;
40 
41 static gint exit_code;
42 
43 static void popup_menu_cb (GtkStatusIcon *, guint, guint, gpointer);
44 
45 static void
free_menu_data(gpointer data)46 free_menu_data (gpointer data)
47 {
48   MenuData *m = (MenuData *) data;
49 
50   if (m) {
51     g_free (m->name);
52     g_free (m->action);
53     g_free (m->icon);
54     g_free (m);
55   }
56 }
57 
58 static void
parse_menu_str(gchar * str)59 parse_menu_str (gchar * str)
60 {
61   gchar **menu_vals;
62   gint i = 0;
63 
64   if (menu_data)
65     {
66       g_slist_free_full (menu_data, free_menu_data);
67       menu_data = NULL;
68     }
69 
70   menu_vals = g_strsplit (str, options.common_data.separator, -1);
71 
72   while (menu_vals[i] != NULL)
73     {
74       MenuData *mdata = g_new0 (MenuData, 1);
75       gchar **s = g_strsplit (menu_vals[i], options.common_data.item_separator, 3);
76 
77       if (s[0])
78         {
79           YadStock sit;
80           if (stock_lookup (s[0], &sit))
81             {
82               mdata->name = g_strdup (sit.label);
83               mdata->icon = g_strdup (sit.icon);
84             }
85           else
86               mdata->name = g_strdup (s[0]);
87           if (s[1])
88             {
89               mdata->action = g_strdup (s[1]);
90               if (s[2])
91                 mdata->icon = g_strdup (s[2]);
92             }
93         }
94       menu_data = g_slist_append (menu_data, mdata);
95       g_strfreev (s);
96       i++;
97     }
98 
99   g_strfreev (menu_vals);
100 }
101 
102 static void
timeout_cb(gpointer data)103 timeout_cb (gpointer data)
104 {
105   exit_code = YAD_RESPONSE_TIMEOUT;
106   gtk_main_quit ();
107 }
108 
109 static void
set_icon(void)110 set_icon (void)
111 {
112   GdkPixbuf *pixbuf;
113   GError *err = NULL;
114 
115   if (icon == NULL)
116     {
117       G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
118       gtk_status_icon_set_from_icon_name (status_icon, "yad");
119       G_GNUC_END_IGNORE_DEPRECATIONS;
120       return;
121     }
122 
123   if (g_file_test (icon, G_FILE_TEST_EXISTS))
124     {
125       gint isize = (options.common_data.icon_size > 0) ? options.common_data.icon_size : 16;
126 
127       pixbuf = gdk_pixbuf_new_from_file_at_scale (icon, isize, isize, TRUE, &err);
128       if (err)
129         {
130           g_printerr (_("Could not load notification icon '%s': %s\n"), icon, err->message);
131           g_clear_error (&err);
132         }
133       if (pixbuf)
134         {
135           G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
136           gtk_status_icon_set_from_pixbuf (status_icon, pixbuf);
137           G_GNUC_END_IGNORE_DEPRECATIONS;
138           g_object_unref (pixbuf);
139         }
140       else
141         {
142           G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
143           gtk_status_icon_set_from_icon_name (status_icon, "yad");
144           G_GNUC_END_IGNORE_DEPRECATIONS;
145         }
146     }
147   else
148     {
149       G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
150       gtk_status_icon_set_from_icon_name (status_icon, icon);
151       G_GNUC_END_IGNORE_DEPRECATIONS;
152     }
153 }
154 
155 static gboolean
activate_cb(GtkWidget * widget,YadData * data)156 activate_cb (GtkWidget * widget, YadData * data)
157 {
158   if ((action == NULL && !options.common_data.listen) || (action && g_ascii_strcasecmp (action, "quit") == 0))
159     {
160       exit_code = YAD_RESPONSE_OK;
161       gtk_main_quit ();
162     }
163   else if (action)
164     {
165       if (g_ascii_strcasecmp (action, "menu") == 0)
166         popup_menu_cb (GTK_STATUS_ICON (widget), 1, GDK_CURRENT_TIME, data);
167       else
168         run_command_async (action);
169     }
170 
171   return TRUE;
172 }
173 
174 static gboolean
middle_quit_cb(GtkStatusIcon * icon,GdkEventButton * ev,gpointer data)175 middle_quit_cb (GtkStatusIcon * icon, GdkEventButton * ev, gpointer data)
176 {
177   if (ev->button == 2)
178     {
179       if (options.data.escape_ok)
180         exit_code = YAD_RESPONSE_OK;
181       else
182         exit_code = YAD_RESPONSE_ESC;
183       gtk_main_quit ();
184     }
185 
186   return FALSE;
187 }
188 
189 static void
popup_menu_item_activate_cb(GtkWidget * w,gpointer data)190 popup_menu_item_activate_cb (GtkWidget * w, gpointer data)
191 {
192   gchar *cmd = (gchar *) data;
193 
194   if (cmd)
195     {
196       if (g_ascii_strcasecmp (g_strstrip (cmd), "quit") == 0)
197         {
198           exit_code = YAD_RESPONSE_OK;
199           gtk_main_quit ();
200         }
201       else
202         run_command_async (cmd);
203     }
204 }
205 
206 static void
popup_menu_cb(GtkStatusIcon * icon,guint button,guint activate_time,gpointer data)207 popup_menu_cb (GtkStatusIcon *icon, guint button, guint activate_time, gpointer data)
208 {
209   GtkWidget *menu;
210   GtkWidget *item;
211   GSList *m;
212 
213   if (!menu_data)
214     return;
215 
216   menu = gtk_menu_new ();
217   gtk_menu_set_reserve_toggle_size (GTK_MENU (menu), FALSE);
218 
219   for (m = menu_data; m; m = m->next)
220     {
221       MenuData *d = (MenuData *) m->data;
222 
223       if (d->name)
224         {
225           GtkWidget *b, *i, *l;
226 
227           item = gtk_menu_item_new ();
228           b = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
229 
230           if (d->icon)
231             {
232               GdkPixbuf *pb = get_pixbuf (d->icon, YAD_SMALL_ICON, TRUE);
233               if (pb)
234                 {
235                   i = gtk_image_new_from_pixbuf (pb);
236                   gtk_container_add (GTK_CONTAINER (b), i);
237                   g_object_unref (pb);
238                 }
239             }
240           l = gtk_label_new_with_mnemonic (d->name);
241           gtk_label_set_xalign (GTK_LABEL (l), 0.0);
242           gtk_label_set_mnemonic_widget (GTK_LABEL (l), item);
243           gtk_box_pack_end (GTK_BOX (b), l, TRUE, TRUE, 0);
244 
245           gtk_container_add (GTK_CONTAINER (item), b);
246 
247           g_signal_connect (GTK_MENU_ITEM (item), "activate",
248                             G_CALLBACK (popup_menu_item_activate_cb), (gpointer) d->action);
249         }
250       else
251         item = gtk_separator_menu_item_new ();
252 
253       gtk_widget_show_all (item);
254       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
255     }
256 
257   gtk_menu_popup_at_pointer (GTK_MENU (menu), NULL);
258 }
259 
260 static gboolean
handle_stdin(GIOChannel * channel,GIOCondition condition,gpointer data)261 handle_stdin (GIOChannel * channel, GIOCondition condition, gpointer data)
262 {
263   if ((condition & G_IO_IN) != 0)
264     {
265       GString *string;
266       GError *err = NULL;
267 
268       string = g_string_new (NULL);
269       while (channel->is_readable == FALSE)
270         usleep (100);
271 
272       do
273         {
274           gint status;
275           gchar *command = NULL, *value = NULL, **args;
276 
277           do
278             {
279               status = g_io_channel_read_line_string (channel, string, NULL, &err);
280 
281               while (gdk_events_pending ())
282                 gtk_main_iteration ();
283             }
284           while (status == G_IO_STATUS_AGAIN);
285 
286           if (status != G_IO_STATUS_NORMAL)
287             {
288               if (err)
289                 {
290                   g_printerr ("yad_notification_handle_stdin(): %s\n", err->message);
291                   g_error_free (err);
292                   err = NULL;
293                 }
294               /* stop handling but not exit */
295               g_io_channel_shutdown (channel, TRUE, NULL);
296               return FALSE;
297             }
298 
299           strip_new_line (string->str);
300           if (!string->str[0])
301             continue;
302 
303           args = g_strsplit (string->str, ":", 2);
304           command = g_strdup (args[0]);
305           if (args[1])
306             value = g_strdup (args[1]);
307           g_strfreev (args);
308           if (value)
309             g_strstrip (value);
310 
311           if (!g_ascii_strcasecmp (command, "icon") && value)
312             {
313               g_free (icon);
314               icon = g_strdup (value);
315 
316               G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
317               if (gtk_status_icon_get_visible (status_icon) && gtk_status_icon_is_embedded (status_icon))
318                 set_icon ();
319               G_GNUC_END_IGNORE_DEPRECATIONS;
320             }
321           else if (!g_ascii_strcasecmp (command, "tooltip"))
322             {
323               if (g_utf8_validate (value, -1, NULL))
324                 {
325                   gchar *message = g_strcompress (value);
326                   G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
327                   if (!options.data.no_markup)
328                     gtk_status_icon_set_tooltip_markup (status_icon, message);
329                   else
330                     gtk_status_icon_set_tooltip_text (status_icon, message);
331                   G_GNUC_END_IGNORE_DEPRECATIONS;
332                   g_free (message);
333                 }
334               else
335                 g_printerr (_("Invalid UTF-8 in tooltip!\n"));
336             }
337           else if (!g_ascii_strcasecmp (command, "visible"))
338             {
339               G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
340               if (!g_ascii_strcasecmp (value, "false"))
341                 gtk_status_icon_set_visible (status_icon, FALSE);
342               else
343                 gtk_status_icon_set_visible (status_icon, TRUE);
344               G_GNUC_END_IGNORE_DEPRECATIONS;
345             }
346           else if (!g_ascii_strcasecmp (command, "action"))
347             {
348               g_free (action);
349               if (value)
350                 action = g_strdup (value);
351             }
352           else if (!g_ascii_strcasecmp (command, "quit"))
353             {
354               exit_code = YAD_RESPONSE_OK;
355               gtk_main_quit ();
356             }
357           else if (!g_ascii_strcasecmp (command, "menu"))
358             {
359               if (value)
360                 parse_menu_str (value);
361             }
362           else
363             g_printerr (_("Unknown command '%s'\n"), command);
364 
365           g_free (command);
366           g_free (value);
367         }
368       while (g_io_channel_get_buffer_condition (channel) == G_IO_IN);
369       g_string_free (string, TRUE);
370     }
371 
372   if ((condition & G_IO_HUP) != 0)
373     {
374       g_io_channel_shutdown (channel, TRUE, NULL);
375       gtk_main_quit ();
376       return FALSE;
377     }
378 
379   return TRUE;
380 }
381 
382 gint
yad_notification_run()383 yad_notification_run ()
384 {
385   GIOChannel *channel = NULL;
386 
387   G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
388   status_icon = gtk_status_icon_new ();
389   G_GNUC_END_IGNORE_DEPRECATIONS;
390 
391   G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
392   if (options.data.dialog_text)
393     {
394       if (!options.data.no_markup)
395         gtk_status_icon_set_tooltip_markup (status_icon, options.data.dialog_text);
396       else
397         gtk_status_icon_set_tooltip_text (status_icon, options.data.dialog_text);
398     }
399   else
400     gtk_status_icon_set_tooltip_text (status_icon, _("Yad notification"));
401   G_GNUC_END_IGNORE_DEPRECATIONS;
402 
403   if (options.data.dialog_image)
404     icon = g_strdup (options.data.dialog_image);
405   if (options.common_data.command)
406     action = g_strdup (options.common_data.command);
407 
408   set_icon ();
409 
410   g_signal_connect (status_icon, "activate", G_CALLBACK (activate_cb), NULL);
411   g_signal_connect (status_icon, "popup_menu", G_CALLBACK (popup_menu_cb), NULL);
412 
413   if (options.notification_data.menu)
414     parse_menu_str (options.notification_data.menu);
415 
416   /* quit on middle click (like press Esc) */
417   if (options.notification_data.middle)
418     g_signal_connect (status_icon, "button-press-event", G_CALLBACK (middle_quit_cb), NULL);
419 
420   if (options.common_data.listen)
421     {
422       channel = g_io_channel_unix_new (0);
423       if (channel)
424         {
425           g_io_channel_set_encoding (channel, NULL, NULL);
426           g_io_channel_set_flags (channel, G_IO_FLAG_NONBLOCK, NULL);
427           g_io_add_watch (channel, G_IO_IN | G_IO_HUP, handle_stdin, NULL);
428         }
429     }
430 
431   /* Show icon and wait */
432   G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
433   gtk_status_icon_set_visible (status_icon, !options.notification_data.hidden);
434   G_GNUC_END_IGNORE_DEPRECATIONS;
435 
436   if (options.data.timeout > 0)
437     g_timeout_add_seconds (options.data.timeout, (GSourceFunc) timeout_cb, NULL);
438 
439   gtk_main ();
440 
441   return exit_code;
442 }
443