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