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