4 * (c) 2002, 2010, 2013 Martin Mares <mj@ucw.cz>
5 * (c) 2021 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
20 #define UNUSED __attribute__((unused))
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";
32 static int notify_pid = 0;
33 static gint notify_watch = 0;
34 static int kill_notify_by = 0;
39 if (kill_notify_by && notify_pid)
41 kill(notify_pid, kill_notify_by);
46 g_source_remove(notify_watch);
58 static int // return pid of new process or 0 if failed
59 expand_and_exec(char *cmd)
66 g_shell_parse_argv(cmd, &argc, &argv, &err);
67 GString ** expanded_argv = malloc(sizeof(expanded_argv[0])*argc);
70 gchar ** expanded_argv_gchar = malloc(sizeof(expanded_argv_gchar[0])*(argc+1));
73 fprintf(stderr, "teatimer: Unable to parse command: %s\n", err->message);
77 if(!expanded_argv_gchar) return 0;
78 for (int i=0; i<argc; i++)
80 expanded_argv[i] = g_string_new("");
81 for (int j=0; argv[i][j]; j++)
83 if (argv[i][j]=='%' && argv[i][j+1]=='%')
86 g_string_append_c(expanded_argv[i], '%');
89 if (argv[i][j]=='%' && argv[i][j+1]=='n')
92 const gchar * name = gtk_entry_get_text(GTK_ENTRY(namebox));
93 g_string_append(expanded_argv[i], name);
96 g_string_append_c(expanded_argv[i], argv[i][j]);
98 expanded_argv_gchar[i] = expanded_argv[i]->str;
100 expanded_argv_gchar[argc]=NULL;
101 g_spawn_async(NULL, expanded_argv_gchar, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &pid, &err);
103 for (int i=0; i<argc; i++)
104 g_string_free(expanded_argv[i], 1);
107 fprintf(stderr, "teatimer: Unable to run command: %s\n", err->message);
115 notify_exit(GPid pid, gint status UNUSED, gpointer data UNUSED)
117 if (notify_pid == pid)
122 it_tolls_for_thee(void)
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);
140 on_second_timeout(gpointer data UNUSED)
143 time_t now = time(NULL);
144 int delta = alarm_time - now;
152 if (delta >= 100*60*60)
153 delta = 100*60*60 - 1;
155 sprintf(buf, "%s%02d:%02d", sign, delta/60, delta%60);
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)
167 on_box_key(GtkWidget *widget UNUSED, GdkEventKey *ev, gpointer user_data UNUSED)
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"))
188 while (*c >= '0' && *c <= '9')
189 m = 10*m + *c++ - '0';
208 on_togglebutton1_toggled(GtkToggleButton *togglebutton, gpointer user_data UNUSED)
210 if (togglebutton->active)
213 strcpy(old_text, gtk_entry_get_text(GTK_ENTRY(timebox)));
214 t = parse_time(old_text);
217 gtk_toggle_button_set_active(togglebutton, 0);
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);
229 gtk_timeout_remove(second_timer);
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);
240 on_window_remove(GtkContainer *container UNUSED, GtkWidget *widget UNUSED, gpointer user_data UNUSED)
246 set_timebox_font_size(int new_size)
248 if (new_size == timebox_font_size)
250 timebox_font_size = new_size;
252 pango_font_description_set_size(timebox_font, timebox_font_size*PANGO_SCALE);
253 gtk_widget_modify_font(timebox, timebox_font);
257 on_window_resized(GtkWidget *widget UNUSED, GdkRectangle *rect, gpointer user_data UNUSED)
259 int window_width = rect->width;
260 int window_height = rect->height;
262 if (autoresize_timebox_font)
264 // Binary search for optimal font size
265 int beg = 12, end = 1024;
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)
279 set_timebox_font_size(beg);
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);
290 hbox1 = gtk_hbox_new(FALSE, 0);
291 gtk_widget_show(hbox1);
292 gtk_container_add(GTK_CONTAINER (win), hbox1);
294 vbox1 = gtk_vbox_new(FALSE, 0);
295 gtk_widget_show(vbox1);
296 gtk_box_pack_start(GTK_BOX(hbox1), vbox1, TRUE, TRUE, 0);
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);
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);
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);
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);
320 gtk_widget_grab_focus(timebox);
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);
327 gtk_widget_show(win);
330 static const char short_opts[] = "r:n:k:a";
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 },
343 fprintf(stderr, "Usage: teatimer [<options>] [<mm:ss>]\n\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\
355 void sig_handler(int signo)
357 if (signo == SIGINT || signo == SIGTERM)
365 main(int argc, char **argv)
368 gtk_init(&argc, &argv);
370 signal(SIGINT, sig_handler);
371 signal(SIGTERM, sig_handler);
374 while ((opt = getopt_long(argc, argv, short_opts, long_opts, NULL)) >= 0)
381 kill_notify_by = atoi(optarg);
384 default_name = optarg;
387 autoresize_timebox_font = true;
392 if (optind != argc && optind+1 != argc)
398 gtk_entry_set_text(GTK_ENTRY(timebox), argv[optind]);
399 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(togglebutton1), 1);