/*
* Trivial Tea Timer
*
- * (c) 2002 Martin Mares <mj@ucw.cz>
+ * (c) 2002, 2010, 2013 Martin Mares <mj@ucw.cz>
+ * (c) 2021 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
*
* GPL'ed
*/
#include <stdio.h>
#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
#include <time.h>
+#include <getopt.h>
+#include <glib.h>
#include <gtk/gtk.h>
#define UNUSED __attribute__((unused))
static guint second_timer;
-static unsigned char old_text[8];
-static GtkWidget *win, *hbox1, *timebox, *togglebutton1;
+static char old_text[16];
+static GtkWidget *win, *hbox1, *vbox1, *timebox, *namebox, *togglebutton1;
+static PangoFontDescription *timebox_font;
+static bool autoresize_timebox_font = false;
+static int timebox_font_size;
static time_t alarm_time;
+static char *run_notify;
+static char *default_name = "Tea";
+static int expired;
+static int notify_pid = 0;
+static gint notify_watch = 0;
+static int kill_notify_by = 0;
+
+static void
+kill_notify(void)
+{
+ if (kill_notify_by && notify_pid)
+ {
+ kill(notify_pid, kill_notify_by);
+ notify_pid = 0;
+ }
+ if (notify_watch)
+ {
+ g_source_remove(notify_watch);
+ notify_watch = 0;
+ }
+}
+
+static void
+quit(void)
+{
+ kill_notify();
+ gtk_main_quit();
+}
+
+static int // return pid of new process or 0 if failed
+expand_and_exec(char *cmd)
+{
+ GError *err = NULL;
+ gint argc;
+ gchar ** argv = NULL;
+ int pid;
+
+ g_shell_parse_argv(cmd, &argc, &argv, &err);
+ GString ** expanded_argv = malloc(sizeof(expanded_argv[0])*argc);
+ if (!expanded_argv)
+ return 0;
+ gchar ** expanded_argv_gchar = malloc(sizeof(expanded_argv_gchar[0])*(argc+1));
+ if (err)
+ {
+ fprintf(stderr, "teatimer: Unable to parse command: %s\n", err->message);
+ g_error_free(err);
+ return 0;
+ }
+ if(!expanded_argv_gchar) return 0;
+ for (int i=0; i<argc; i++)
+ {
+ expanded_argv[i] = g_string_new("");
+ for (int j=0; argv[i][j]; j++)
+ {
+ if (argv[i][j]=='%' && argv[i][j+1]=='%')
+ {
+ j++;
+ g_string_append_c(expanded_argv[i], '%');
+ }
+ else
+ if (argv[i][j]=='%' && argv[i][j+1]=='n')
+ {
+ j++;
+ const gchar * name = gtk_entry_get_text(GTK_ENTRY(namebox));
+ g_string_append(expanded_argv[i], name);
+ }
+ else
+ g_string_append_c(expanded_argv[i], argv[i][j]);
+ }
+ expanded_argv_gchar[i] = expanded_argv[i]->str;
+ }
+ expanded_argv_gchar[argc]=NULL;
+ g_spawn_async(NULL, expanded_argv_gchar, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &pid, &err);
+ g_strfreev(argv);
+ for (int i=0; i<argc; i++)
+ g_string_free(expanded_argv[i], 1);
+ if (err)
+ {
+ fprintf(stderr, "teatimer: Unable to run command: %s\n", err->message);
+ g_error_free(err);
+ return 0;
+ }
+ return pid;
+}
+
+static void
+notify_exit(GPid pid, gint status UNUSED, gpointer data UNUSED)
+{
+ if (notify_pid == pid)
+ notify_pid = 0;
+}
static void
it_tolls_for_thee(void)
{
- gdk_beep();
+ if (run_notify)
+ {
+ if (!expired)
+ {
+ if(notify_watch)
+ g_source_remove(notify_watch);
+ notify_pid = expand_and_exec(run_notify);
+ notify_watch = g_child_watch_add(notify_pid, notify_exit, NULL);
+ expired = 1;
+ }
+ }
+ else
+ gdk_beep();
}
static gint
on_second_timeout(gpointer data UNUSED)
{
- char buf[8];
+ char buf[16];
time_t now = time(NULL);
int delta = alarm_time - now;
char *sign = "";
sign = "-";
delta = -delta;
}
- if (delta >= 6000)
- delta = 5999;
- sprintf(buf, "%s%02d:%02d", sign, delta/60, delta%60);
+ if (delta >= 100*60*60)
+ delta = 100*60*60 - 1;
+ if (delta < 60*60)
+ sprintf(buf, "%s%02d:%02d", sign, delta/60, delta%60);
+ else
+ sprintf(buf, "%s%02d:%02d:%02d", sign, delta/3600, (delta%3600)/60, delta%60);
gtk_entry_set_text(GTK_ENTRY(timebox), buf);
if (now >= alarm_time)
it_tolls_for_thee();
+ else
+ expired = 0;
return 1;
}
static gint
-on_timebox_key(GtkWidget *widget UNUSED, GdkEventKey *ev, gpointer user_data UNUSED)
+on_box_key(GtkWidget *widget UNUSED, GdkEventKey *ev, gpointer user_data UNUSED)
{
if (!strcmp(ev->string, "\r"))
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(togglebutton1), !GTK_TOGGLE_BUTTON(togglebutton1)->active);
else if (!strcmp(ev->string, "\033"))
- gtk_main_quit();
+ quit();
return FALSE;
}
parse_time(char *c)
{
int t = 0;
+ int parts = 0;
- while (*c && *c != ':')
+ while (*c)
{
- if (*c >= '0' && *c <= '9')
- t = 10*t + *c++ - '0';
- else
+ parts++;
+ if (parts > 3)
return -1;
- }
- if (*c)
- {
int m = 0;
- c++;
- while (*c)
+ while (*c >= '0' && *c <= '9')
+ m = 10*m + *c++ - '0';
+ t += m;
+ if (*c == ':')
{
- if (*c >= '0' && *c <= '9')
- m = 10*m + *c++ - '0';
- else
- return -1;
+ c++;
+ t = 60*t;
}
- t = 60*t + m;
+ else if (*c)
+ return -1;
}
- if (t >= 6000)
+
+ if (!parts)
+ return -1;
+ if (t >= 100*60*60)
return -1;
return t;
}
gtk_timeout_remove(second_timer);
second_timer = 0;
}
+ kill_notify();
gtk_entry_set_text(GTK_ENTRY(timebox), old_text);
gtk_entry_set_editable(GTK_ENTRY(timebox), 1);
- gtk_widget_grab_focus (timebox);
+ gtk_widget_grab_focus(timebox);
}
}
static void
on_window_remove(GtkContainer *container UNUSED, GtkWidget *widget UNUSED, gpointer user_data UNUSED)
{
- gtk_main_quit();
+ quit();
+}
+
+static void
+set_timebox_font_size(int new_size)
+{
+ if (new_size == timebox_font_size)
+ return;
+ timebox_font_size = new_size;
+
+ pango_font_description_set_size(timebox_font, timebox_font_size*PANGO_SCALE);
+ gtk_widget_modify_font(timebox, timebox_font);
+}
+
+static void
+on_window_resized(GtkWidget *widget UNUSED, GdkRectangle *rect, gpointer user_data UNUSED)
+{
+ int window_width = rect->width;
+ int window_height = rect->height;
+
+ if (autoresize_timebox_font)
+ {
+ // Binary search for optimal font size
+ int beg = 12, end = 1024;
+ while (beg < end)
+ {
+ int mid = (beg + end) / 2;
+ pango_font_description_set_size(timebox_font, mid*PANGO_SCALE);
+ PangoFontMetrics *metric = pango_context_get_metrics(gtk_widget_get_pango_context(timebox), timebox_font, NULL);
+ int width = pango_font_metrics_get_approximate_digit_width(metric) * 8.3 / PANGO_SCALE;
+ int height = pango_font_metrics_get_height(metric) / PANGO_SCALE;
+ if (width > window_width - 50 || height > window_height - 45)
+ end = mid;
+ else
+ beg = mid + 1;
+ }
+
+ set_timebox_font_size(beg);
+ }
}
static void
open_window(void)
{
- win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
- gtk_window_set_title (GTK_WINDOW (win), "Tea Timer");
- gtk_window_set_policy (GTK_WINDOW (win), TRUE, TRUE, TRUE);
+ win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW (win), "Tea Timer");
+ gtk_window_set_policy(GTK_WINDOW (win), TRUE, TRUE, TRUE);
+
+ hbox1 = gtk_hbox_new(FALSE, 0);
+ gtk_widget_show(hbox1);
+ gtk_container_add(GTK_CONTAINER (win), hbox1);
+
+ vbox1 = gtk_vbox_new(FALSE, 0);
+ gtk_widget_show(vbox1);
+ gtk_box_pack_start(GTK_BOX(hbox1), vbox1, TRUE, TRUE, 0);
- hbox1 = gtk_hbox_new (FALSE, 0);
- gtk_widget_show (hbox1);
- gtk_container_add (GTK_CONTAINER (win), hbox1);
+ namebox = gtk_entry_new_with_max_length(30);
+ gtk_widget_show(namebox);
+ gtk_box_pack_start(GTK_BOX(vbox1), namebox, TRUE, TRUE, 0);
+ gtk_entry_set_text(GTK_ENTRY(namebox), default_name);
- timebox = gtk_entry_new_with_max_length (6);
- gtk_widget_show (timebox);
- gtk_box_pack_start (GTK_BOX (hbox1), timebox, TRUE, TRUE, 0);
- gtk_entry_set_text (GTK_ENTRY (timebox), "00:00");
+ timebox = gtk_entry_new_with_max_length(9);
+ gtk_widget_show(timebox);
+ gtk_box_pack_start(GTK_BOX(vbox1), timebox, TRUE, TRUE, 0);
+ gtk_entry_set_text(GTK_ENTRY(timebox), "00:00");
+ timebox_font = pango_font_description_from_string("Monospace");
+ set_timebox_font_size(12);
- togglebutton1 = gtk_toggle_button_new_with_label ("Run");
- gtk_widget_show (togglebutton1);
- gtk_box_pack_start (GTK_BOX (hbox1), togglebutton1, FALSE, FALSE, 0);
+ togglebutton1 = gtk_toggle_button_new_with_label("Run");
+ gtk_widget_show(togglebutton1);
+ gtk_box_pack_start(GTK_BOX(hbox1), togglebutton1, FALSE, FALSE, 0);
- gtk_signal_connect(GTK_OBJECT (win), "remove", GTK_SIGNAL_FUNC (on_window_remove), NULL);
- gtk_signal_connect(GTK_OBJECT (timebox), "key_press_event", GTK_SIGNAL_FUNC (on_timebox_key), NULL);
- gtk_signal_connect(GTK_OBJECT (togglebutton1), "toggled", GTK_SIGNAL_FUNC (on_togglebutton1_toggled), NULL);
+ gtk_signal_connect(GTK_OBJECT(win), "remove", GTK_SIGNAL_FUNC(on_window_remove), NULL);
+ gtk_signal_connect(GTK_OBJECT(win), "size-allocate", GTK_SIGNAL_FUNC(on_window_resized), NULL);
+ gtk_signal_connect(GTK_OBJECT(namebox), "key_press_event", GTK_SIGNAL_FUNC(on_box_key), NULL);
+ gtk_signal_connect(GTK_OBJECT(timebox), "key_press_event", GTK_SIGNAL_FUNC(on_box_key), NULL);
+ gtk_signal_connect(GTK_OBJECT(togglebutton1), "toggled", GTK_SIGNAL_FUNC(on_togglebutton1_toggled), NULL);
- gtk_widget_grab_focus (timebox);
+ gtk_widget_grab_focus(timebox);
+
+ // Do not focus button
+ GList *focus_chain = NULL;
+ focus_chain = g_list_append(focus_chain, vbox1);
+ gtk_container_set_focus_chain(GTK_CONTAINER (hbox1), focus_chain);
gtk_widget_show(win);
}
+static const char short_opts[] = "r:n:k:a";
+
+static const struct option long_opts[] = {
+ { "run", required_argument, NULL, 'r' },
+ { "kill", required_argument, NULL, 'k' },
+ { "timer-name", required_argument, NULL, 'n' },
+ { "auto-resize", no_argument, NULL, 'a' },
+ { NULL, 0, NULL, 0 },
+};
+
+static void
+usage(void)
+{
+ fprintf(stderr, "Usage: teatimer [<options>] [<mm:ss>]\n\n\
+Options:\n\
+-r, --run=<cmd>\t\tRun a given program when the tea is ready\n\
+\t\t\t\t%%d will be expanded to timer name\n\
+\t\t\t\t%%%% will be expanded to %%\n\
+-k, --kill=<int>\tKill run program by a given signal when the timer is stopped\n\
+-n, --timer-name=<str>\tFill name box with <str>\n\
+-a, --auto-resize\tAutomatically resize font to fit the box\n\
+");
+ exit(1);
+}
+
+void sig_handler(int signo)
+{
+ if (signo == SIGINT || signo == SIGTERM)
+ {
+ kill_notify();
+ exit(0);
+ }
+}
+
int
main(int argc, char **argv)
{
gtk_set_locale();
gtk_init(&argc, &argv);
+
+ signal(SIGINT, sig_handler);
+ signal(SIGTERM, sig_handler);
+
+ int opt;
+ while ((opt = getopt_long(argc, argv, short_opts, long_opts, NULL)) >= 0)
+ switch (opt)
+ {
+ case 'r':
+ run_notify = optarg;
+ break;
+ case 'k':
+ kill_notify_by = atoi(optarg);
+ break;
+ case 'n':
+ default_name = optarg;
+ break;
+ case 'a':
+ autoresize_timebox_font = true;
+ break;
+ default:
+ usage();
+ }
+ if (optind != argc && optind+1 != argc)
+ usage();
+
open_window();
- if (argc > 1)
+ if (optind < argc)
{
- gtk_entry_set_text(GTK_ENTRY(timebox), argv[1]);
+ gtk_entry_set_text(GTK_ENTRY(timebox), argv[optind]);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(togglebutton1), 1);
}
gtk_main();