]> mj.ucw.cz Git - teatimer.git/blob - teatimer.c
Fix expansion in cmd
[teatimer.git] / teatimer.c
1 /*
2  *      Trivial Tea Timer
3  *
4  *      (c) 2002, 2010, 2013 Martin Mares <mj@ucw.cz>
5  *      (c) 2021 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
6  *
7  *      GPL'ed
8  */
9
10 #include <stdio.h>
11 #include <string.h>
12 #include <stdlib.h>
13 #include <stdbool.h>
14 #include <time.h>
15 #include <getopt.h>
16
17 #include <glib.h>
18 #include <gtk/gtk.h>
19
20 #define UNUSED __attribute__((unused))
21
22 static guint second_timer;
23 static char old_text[16];
24 static GtkWidget *win, *hbox1, *vbox1, *timebox, *namebox, *togglebutton1;
25 static PangoFontDescription *timebox_font;
26 static bool autoresize_timebox_font = false;
27 static int timebox_font_size;
28 static time_t alarm_time;
29 static char *run_notify;
30 static char *default_name = "Tea";
31 static int expired;
32 static int notify_pid = 0;
33 static gint notify_watch = 0;
34 static int kill_notify_by = 0;
35
36 static void
37 kill_notify(void)
38 {
39   if (kill_notify_by && notify_pid)
40     {
41       kill(notify_pid, kill_notify_by);
42       notify_pid = 0;
43     }
44   if (notify_watch)
45     {
46       g_source_remove(notify_watch);
47       notify_watch = 0;
48     }
49 }
50
51 static void
52 quit(void)
53 {
54   kill_notify();
55   gtk_main_quit();
56 }
57
58 static int // return pid of new process or 0 if failed
59 expand_and_exec(char *cmd)
60 {
61   GError *err = NULL;
62   gint argc;
63   gchar ** argv = NULL;
64   int pid;
65
66   g_shell_parse_argv(cmd, &argc, &argv, &err);
67   GString ** expanded_argv = malloc(sizeof(expanded_argv[0])*argc);
68   if (!expanded_argv)
69     return 0;
70   gchar ** expanded_argv_gchar = malloc(sizeof(expanded_argv_gchar[0])*(argc+1));
71   if (err)
72     {
73       fprintf(stderr, "teatimer: Unable to parse command: %s\n", err->message);
74       g_error_free(err);
75       return 0;
76     }
77   if(!expanded_argv_gchar) return 0;
78   for (int i=0; i<argc; i++)
79     {
80       expanded_argv[i] = g_string_new("");
81       for (int j=0; argv[i][j]; j++)
82         {
83           if (argv[i][j]=='%' && argv[i][j+1]=='%')
84             {
85               j++;
86               g_string_append_c(expanded_argv[i], '%');
87             }
88           else
89           if (argv[i][j]=='%' && argv[i][j+1]=='n')
90             {
91               j++;
92               const gchar * name = gtk_entry_get_text(GTK_ENTRY(namebox));
93               g_string_append(expanded_argv[i], name);
94             }
95           else
96             g_string_append_c(expanded_argv[i], argv[i][j]);
97         }
98       expanded_argv_gchar[i] = expanded_argv[i]->str;
99     }
100   expanded_argv_gchar[argc]=NULL;
101   g_spawn_async(NULL, expanded_argv_gchar, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &pid, &err);
102   g_strfreev(argv);
103   for (int i=0; i<argc; i++)
104     g_string_free(expanded_argv[i], 1);
105   if (err)
106     {
107       fprintf(stderr, "teatimer: Unable to run command: %s\n", err->message);
108       g_error_free(err);
109       return 0;
110     }
111   return pid;
112 }
113
114 static void
115 notify_exit(GPid pid, gint status UNUSED, gpointer data UNUSED)
116 {
117   if (notify_pid == pid)
118     notify_pid = 0;
119 }
120
121 static void
122 it_tolls_for_thee(void)
123 {
124   if (run_notify)
125     {
126       if (!expired)
127         {
128           if(notify_watch)
129             g_source_remove(notify_watch);
130           notify_pid = expand_and_exec(run_notify);
131           notify_watch = g_child_watch_add(notify_pid, notify_exit, NULL);
132           expired = 1;
133         }
134     }
135   else
136     gdk_beep();
137 }
138
139 static gint
140 on_second_timeout(gpointer data UNUSED)
141 {
142   char buf[16];
143   time_t now = time(NULL);
144   int delta = alarm_time - now;
145   char *sign = "";
146
147   if (delta < 0)
148     {
149       sign = "-";
150       delta = -delta;
151     }
152   if (delta >= 100*60*60)
153     delta = 100*60*60 - 1;
154   if (delta < 60*60)
155     sprintf(buf, "%s%02d:%02d", sign, delta/60, delta%60);
156   else
157     sprintf(buf, "%s%02d:%02d:%02d", sign, delta/3600, (delta%3600)/60, delta%60);
158   gtk_entry_set_text(GTK_ENTRY(timebox), buf);
159   if (now >= alarm_time)
160     it_tolls_for_thee();
161   else
162     expired = 0;
163   return 1;
164 }
165
166 static gint
167 on_box_key(GtkWidget *widget UNUSED, GdkEventKey *ev, gpointer user_data UNUSED)
168 {
169   if (!strcmp(ev->string, "\r"))
170     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(togglebutton1), !GTK_TOGGLE_BUTTON(togglebutton1)->active);
171   else if (!strcmp(ev->string, "\033"))
172     quit();
173   return FALSE;
174 }
175
176 static int
177 parse_time(char *c)
178 {
179   int t = 0;
180   int parts = 0;
181
182   while (*c)
183     {
184       parts++;
185       if (parts > 3)
186         return -1;
187       int m = 0;
188       while (*c >= '0' && *c <= '9')
189         m = 10*m + *c++ - '0';
190       t += m;
191       if (*c == ':')
192         {
193           c++;
194           t = 60*t;
195         }
196       else if (*c)
197         return -1;
198     }
199
200   if (!parts)
201     return -1;
202   if (t >= 100*60*60)
203     return -1;
204   return t;
205 }
206
207 static void
208 on_togglebutton1_toggled(GtkToggleButton *togglebutton, gpointer user_data UNUSED)
209 {
210   if (togglebutton->active)
211     {
212       int t;
213       strcpy(old_text, gtk_entry_get_text(GTK_ENTRY(timebox)));
214       t = parse_time(old_text);
215       if (t < 0)
216         {
217           gtk_toggle_button_set_active(togglebutton, 0);
218           return;
219         }
220       alarm_time = time(NULL) + t;
221       gtk_entry_set_editable(GTK_ENTRY(timebox), 0);
222       on_second_timeout(NULL);
223       second_timer = gtk_timeout_add(1000, on_second_timeout, NULL);
224     }
225   else
226     {
227       if (second_timer)
228         {
229           gtk_timeout_remove(second_timer);
230           second_timer = 0;
231         }
232       kill_notify();
233       gtk_entry_set_text(GTK_ENTRY(timebox), old_text);
234       gtk_entry_set_editable(GTK_ENTRY(timebox), 1);
235       gtk_widget_grab_focus(timebox);
236     }
237 }
238
239 static void
240 on_window_remove(GtkContainer *container UNUSED, GtkWidget *widget UNUSED, gpointer user_data UNUSED)
241 {
242   quit();
243 }
244
245 static void
246 set_timebox_font_size(int new_size)
247 {
248   if (new_size == timebox_font_size)
249     return;
250   timebox_font_size = new_size;
251
252   pango_font_description_set_size(timebox_font, timebox_font_size*PANGO_SCALE);
253   gtk_widget_modify_font(timebox, timebox_font);
254 }
255
256 static void
257 on_window_resized(GtkWidget *widget UNUSED, GdkRectangle *rect, gpointer user_data UNUSED)
258 {
259   int window_width = rect->width;
260   int window_height = rect->height;
261
262   if (autoresize_timebox_font)
263   {
264     // Binary search for optimal font size
265     int beg = 12, end = 1024;
266     while (beg < end)
267       {
268         int mid = (beg + end) / 2;
269         pango_font_description_set_size(timebox_font, mid*PANGO_SCALE);
270         PangoFontMetrics *metric = pango_context_get_metrics(gtk_widget_get_pango_context(timebox), timebox_font, NULL);
271         int width = pango_font_metrics_get_approximate_digit_width(metric) * 8.3 / PANGO_SCALE;
272         int height = pango_font_metrics_get_height(metric) / PANGO_SCALE;
273         if (width > window_width - 50 || height > window_height - 45)
274           end = mid;
275         else
276           beg = mid + 1;
277       }
278
279     set_timebox_font_size(beg);
280   }
281 }
282
283 static void
284 open_window(void)
285 {
286   win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
287   gtk_window_set_title(GTK_WINDOW (win), "Tea Timer");
288   gtk_window_set_policy(GTK_WINDOW (win), TRUE, TRUE, TRUE);
289
290   hbox1 = gtk_hbox_new(FALSE, 0);
291   gtk_widget_show(hbox1);
292   gtk_container_add(GTK_CONTAINER (win), hbox1);
293
294   vbox1 = gtk_vbox_new(FALSE, 0);
295   gtk_widget_show(vbox1);
296   gtk_box_pack_start(GTK_BOX(hbox1), vbox1, TRUE, TRUE, 0);
297
298   namebox = gtk_entry_new_with_max_length(30);
299   gtk_widget_show(namebox);
300   gtk_box_pack_start(GTK_BOX(vbox1), namebox, TRUE, TRUE, 0);
301   gtk_entry_set_text(GTK_ENTRY(namebox), default_name);
302
303   timebox = gtk_entry_new_with_max_length(9);
304   gtk_widget_show(timebox);
305   gtk_box_pack_start(GTK_BOX(vbox1), timebox, TRUE, TRUE, 0);
306   gtk_entry_set_text(GTK_ENTRY(timebox), "00:00");
307   timebox_font = pango_font_description_from_string("Monospace");
308   set_timebox_font_size(12);
309
310   togglebutton1 = gtk_toggle_button_new_with_label("Run");
311   gtk_widget_show(togglebutton1);
312   gtk_box_pack_start(GTK_BOX(hbox1), togglebutton1, FALSE, FALSE, 0);
313
314   gtk_signal_connect(GTK_OBJECT(win), "remove", GTK_SIGNAL_FUNC(on_window_remove), NULL);
315   gtk_signal_connect(GTK_OBJECT(win), "size-allocate", GTK_SIGNAL_FUNC(on_window_resized), NULL);
316   gtk_signal_connect(GTK_OBJECT(namebox), "key_press_event", GTK_SIGNAL_FUNC(on_box_key), NULL);
317   gtk_signal_connect(GTK_OBJECT(timebox), "key_press_event", GTK_SIGNAL_FUNC(on_box_key), NULL);
318   gtk_signal_connect(GTK_OBJECT(togglebutton1), "toggled", GTK_SIGNAL_FUNC(on_togglebutton1_toggled), NULL);
319
320   gtk_widget_grab_focus(timebox);
321
322   // Do not focus button
323   GList *focus_chain = NULL;
324   focus_chain = g_list_append(focus_chain, vbox1);
325   gtk_container_set_focus_chain(GTK_CONTAINER (hbox1), focus_chain);
326
327   gtk_widget_show(win);
328 }
329
330 static const char short_opts[] = "r:n:k:a";
331
332 static const struct option long_opts[] = {
333   { "run",              required_argument,      NULL,   'r' },
334   { "kill",             required_argument,      NULL,   'k' },
335   { "timer-name",       required_argument,      NULL,   'n' },
336   { "auto-resize",      no_argument,    NULL,   'a' },
337   { NULL,               0,                      NULL,   0   },
338 };
339
340 static void
341 usage(void)
342 {
343   fprintf(stderr, "Usage: teatimer [<options>] [<mm:ss>]\n\n\
344 Options:\n\
345 -r, --run=<cmd>\t\tRun a given program when the tea is ready\n\
346 \t\t\t\t%%d will be expanded to timer name\n\
347 \t\t\t\t%%%% will be expanded to %%\n\
348 -k, --kill=<int>\tKill run program by a given signal when the timer is stopped\n\
349 -n, --timer-name=<str>\tFill name box with <str>\n\
350 -a, --auto-resize\tAutomatically resize font to fit the box\n\
351 ");
352   exit(1);
353 }
354
355 void sig_handler(int signo)
356 {
357   if (signo == SIGINT || signo == SIGTERM)
358     {
359       kill_notify();
360       exit(0);
361     }
362 }
363
364 int
365 main(int argc, char **argv)
366 {
367   gtk_set_locale();
368   gtk_init(&argc, &argv);
369
370   signal(SIGINT, sig_handler);
371   signal(SIGTERM, sig_handler);
372
373   int opt;
374   while ((opt = getopt_long(argc, argv, short_opts, long_opts, NULL)) >= 0)
375     switch (opt)
376       {
377       case 'r':
378         run_notify = optarg;
379         break;
380       case 'k':
381         kill_notify_by = atoi(optarg);
382         break;
383       case 'n':
384         default_name = optarg;
385         break;
386       case 'a':
387         autoresize_timebox_font = true;
388         break;
389       default:
390         usage();
391       }
392   if (optind != argc && optind+1 != argc)
393     usage();
394
395   open_window();
396   if (optind < argc)
397     {
398       gtk_entry_set_text(GTK_ENTRY(timebox), argv[optind]);
399       gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(togglebutton1), 1);
400     }
401   gtk_main();
402   return 0;
403 }