/* stask - simple task manager * See LICENSE file for copyright and license details. */ #include #include #include #include #include "stask.h" #include "config.h" typedef struct { TaskDB *tdb; int dark; const Theme *theme; GtkWidget *window; GtkWidget *theme_btn; GtkWidget *list_box; GtkWidget *task_box; GtkWidget *list_label; GtkWidget *entry; GtkWidget *status; GtkCssProvider *theme_css; int cur_list; /* selected list id, -1 = none */ char cur_name[128]; /* selected list name */ } Gui; static Gui gui; /* theme CSS */ static void apply_theme_css(Gui *g) { char css[4096]; const Theme *t; int bg[3], fg[3], bd[3], mt[3], cd[3], mfg[3]; int pr[3], pfg[3], ac[3], afg[3]; t = g->theme; bg[0] = (int)(t->bg[0] * 255); bg[1] = (int)(t->bg[1] * 255); bg[2] = (int)(t->bg[2] * 255); fg[0] = (int)(t->fg[0] * 255); fg[1] = (int)(t->fg[1] * 255); fg[2] = (int)(t->fg[2] * 255); bd[0] = (int)(t->border[0] * 255); bd[1] = (int)(t->border[1] * 255); bd[2] = (int)(t->border[2] * 255); mt[0] = (int)(t->muted[0] * 255); mt[1] = (int)(t->muted[1] * 255); mt[2] = (int)(t->muted[2] * 255); cd[0] = (int)(t->card[0] * 255); cd[1] = (int)(t->card[1] * 255); cd[2] = (int)(t->card[2] * 255); mfg[0] = (int)(t->muted_fg[0] * 255); mfg[1] = (int)(t->muted_fg[1] * 255); mfg[2] = (int)(t->muted_fg[2] * 255); pr[0] = (int)(t->primary[0] * 255); pr[1] = (int)(t->primary[1] * 255); pr[2] = (int)(t->primary[2] * 255); pfg[0] = (int)(t->primary_fg[0] * 255); pfg[1] = (int)(t->primary_fg[1] * 255); pfg[2] = (int)(t->primary_fg[2] * 255); ac[0] = (int)(t->accent[0] * 255); ac[1] = (int)(t->accent[1] * 255); ac[2] = (int)(t->accent[2] * 255); afg[0] = (int)(t->accent_fg[0] * 255); afg[1] = (int)(t->accent_fg[1] * 255); afg[2] = (int)(t->accent_fg[2] * 255); snprintf(css, sizeof(css), "window, box, paned {" " background: rgb(%d,%d,%d);" " color: rgb(%d,%d,%d);" "}" "headerbar {" " background: rgb(%d,%d,%d);" " border-bottom: 1px solid rgb(%d,%d,%d);" " color: rgb(%d,%d,%d);" "}" "headerbar button {" " background: rgb(%d,%d,%d);" " color: rgb(%d,%d,%d);" " border: 1px solid rgb(%d,%d,%d);" "}" "headerbar button:hover {" " background: rgb(%d,%d,%d);" "}" "label {" " color: rgb(%d,%d,%d);" "}" "entry {" " background: rgb(%d,%d,%d);" " color: rgb(%d,%d,%d);" " border: 1px solid rgb(%d,%d,%d);" " caret-color: rgb(%d,%d,%d);" "}" "listboxrow {" " background: rgb(%d,%d,%d);" " color: rgb(%d,%d,%d);" " padding: 6px 10px;" " border-bottom: 1px solid rgb(%d,%d,%d);" "}" "listboxrow:selected {" " background: rgb(%d,%d,%d);" " color: rgb(%d,%d,%d);" "}" "separator {" " background: rgb(%d,%d,%d);" " min-width: 1px; min-height: 1px;" "}" "checkbutton {" " color: rgb(%d,%d,%d);" "}" "checkbutton indicator {" " border: 1px solid rgb(%d,%d,%d);" " background: rgb(%d,%d,%d);" "}" ".task-done label {" " color: rgb(%d,%d,%d);" "}" ".list-header {" " font-weight: bold;" " font-size: 14px;" " padding: 8px 12px;" "}" ".task-date {" " color: rgb(%d,%d,%d);" " font-size: 11px;" "}" "popover > contents {" " background: rgb(%d,%d,%d);" " border: 1px solid rgb(%d,%d,%d);" "}" "popover button {" " background: transparent;" " color: rgb(%d,%d,%d);" " padding: 6px 12px;" "}" "popover button:hover {" " background: rgb(%d,%d,%d);" "}", /* window bg */ bg[0], bg[1], bg[2], /* window fg */ fg[0], fg[1], fg[2], /* headerbar bg */ bg[0], bg[1], bg[2], /* headerbar border */ bd[0], bd[1], bd[2], /* headerbar fg */ fg[0], fg[1], fg[2], /* btn bg */ mt[0], mt[1], mt[2], /* btn fg */ fg[0], fg[1], fg[2], /* btn border */ bd[0], bd[1], bd[2], /* btn hover */ bd[0], bd[1], bd[2], /* label */ fg[0], fg[1], fg[2], /* entry bg */ cd[0], cd[1], cd[2], /* entry fg */ fg[0], fg[1], fg[2], /* entry border */ bd[0], bd[1], bd[2], /* entry caret */ pr[0], pr[1], pr[2], /* listrow bg */ cd[0], cd[1], cd[2], /* listrow fg */ fg[0], fg[1], fg[2], /* listrow border */ bd[0], bd[1], bd[2], /* listrow sel bg */ ac[0], ac[1], ac[2], /* listrow sel fg */ afg[0], afg[1], afg[2], /* separator */ bd[0], bd[1], bd[2], /* checkbutton fg */ fg[0], fg[1], fg[2], /* check border */ bd[0], bd[1], bd[2], /* check bg */ cd[0], cd[1], cd[2], /* done label */ mfg[0], mfg[1], mfg[2], /* date label */ mfg[0], mfg[1], mfg[2], /* popover bg */ cd[0], cd[1], cd[2], /* popover border */ bd[0], bd[1], bd[2], /* popover btn fg */ fg[0], fg[1], fg[2], /* popover hover */ mt[0], mt[1], mt[2] ); if (!g->theme_css) { g->theme_css = gtk_css_provider_new(); gtk_style_context_add_provider_for_display( gdk_display_get_default(), GTK_STYLE_PROVIDER(g->theme_css), GTK_STYLE_PROVIDER_PRIORITY_USER); } gtk_css_provider_load_from_string(g->theme_css, css); } static void toggle_theme(Gui *g) { g->dark = !g->dark; g->theme = g->dark ? &theme_dark : &theme_light; apply_theme_css(g); if (g->theme_btn) gtk_button_set_label( GTK_BUTTON(g->theme_btn), g->dark ? "Light" : "Dark"); } /* refresh task list — forward declaration */ static void refresh_tasks(Gui *g); static void on_check_toggled(GtkCheckButton *cb, gpointer data) { Gui *g; int task_id; gboolean active; g = &gui; task_id = GPOINTER_TO_INT(data); active = gtk_check_button_get_active(cb); if (active) task_done(g->tdb, task_id); else task_undo(g->tdb, task_id); refresh_tasks(g); } /* refresh list sidebar */ static void on_list_selected(GtkListBox *box, GtkListBoxRow *row, gpointer data) { Gui *g; const char *name; GtkWidget *label; List list; (void)box; g = (Gui *)data; if (!row) { g->cur_list = -1; g->cur_name[0] = '\0'; refresh_tasks(g); return; } label = gtk_widget_get_first_child( GTK_WIDGET(row)); if (!label) return; name = gtk_label_get_text(GTK_LABEL(label)); if (list_by_name(g->tdb, name, &list) < 0) return; g->cur_list = list.id; snprintf(g->cur_name, sizeof(g->cur_name), "%s", list.name); refresh_tasks(g); } static void refresh_lists(Gui *g) { List *lists; GtkWidget *row, *label; int i, n; while ((row = gtk_widget_get_first_child( GTK_WIDGET(g->list_box)))) gtk_list_box_remove( GTK_LIST_BOX(g->list_box), row); if (list_all(g->tdb, &lists, &n) < 0) return; for (i = 0; i < n; i++) { label = gtk_label_new(lists[i].name); gtk_label_set_xalign( GTK_LABEL(label), 0.0); gtk_list_box_append( GTK_LIST_BOX(g->list_box), label); /* re-select current list */ if (lists[i].id == g->cur_list) { row = gtk_widget_get_parent(label); gtk_list_box_select_row( GTK_LIST_BOX(g->list_box), GTK_LIST_BOX_ROW(row)); } } free(lists); } /* new list dialog */ static void on_new_list_response(GtkDialog *dlg, int response, gpointer data) { Gui *g; GtkWidget *entry; const char *name; g = (Gui *)data; if (response == GTK_RESPONSE_OK) { entry = g_object_get_data( G_OBJECT(dlg), "entry"); name = gtk_editable_get_text( GTK_EDITABLE(entry)); if (name && name[0]) { list_create(g->tdb, name); refresh_lists(g); } } gtk_window_destroy(GTK_WINDOW(dlg)); } static void on_new_list(GtkButton *btn, gpointer data) { Gui *g; GtkWidget *dlg, *box, *entry, *label; (void)btn; g = (Gui *)data; dlg = gtk_dialog_new_with_buttons("New List", GTK_WINDOW(g->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "Create", GTK_RESPONSE_OK, "Cancel", GTK_RESPONSE_CANCEL, NULL); box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16); gtk_widget_set_margin_top(box, 12); gtk_widget_set_margin_bottom(box, 12); label = gtk_label_new("List name:"); gtk_label_set_xalign(GTK_LABEL(label), 0.0); gtk_box_append(GTK_BOX(box), label); entry = gtk_entry_new(); gtk_box_append(GTK_BOX(box), entry); g_object_set_data(G_OBJECT(dlg), "entry", entry); gtk_box_append( GTK_BOX(gtk_dialog_get_content_area( GTK_DIALOG(dlg))), box); g_signal_connect(dlg, "response", G_CALLBACK(on_new_list_response), g); gtk_window_present(GTK_WINDOW(dlg)); } /* delete list */ static void on_del_list(GtkButton *btn, gpointer data) { Gui *g; (void)btn; g = (Gui *)data; if (g->cur_list < 0) return; list_delete(g->tdb, g->cur_name); g->cur_list = -1; g->cur_name[0] = '\0'; refresh_lists(g); refresh_tasks(g); } /* add task via entry */ static void on_entry_activate(GtkEntry *entry, gpointer data) { Gui *g; GtkEntryBuffer *buf; const char *text; g = (Gui *)data; if (g->cur_list < 0) return; buf = gtk_entry_get_buffer(entry); text = gtk_entry_buffer_get_text(buf); if (!text || !text[0]) return; task_add(g->tdb, g->cur_list, text); gtk_entry_buffer_set_text(buf, "", 0); refresh_tasks(g); } /* delete selected task */ static void on_del_task(GtkButton *btn, gpointer data) { Gui *g; GtkListBoxRow *row; GtkWidget *hbox, *check; int task_id; (void)btn; g = (Gui *)data; row = gtk_list_box_get_selected_row( GTK_LIST_BOX(g->task_box)); if (!row) return; hbox = gtk_widget_get_first_child( GTK_WIDGET(row)); if (!hbox) return; /* first child of hbox is the checkbutton */ check = gtk_widget_get_first_child(hbox); if (!check) return; /* get task id from the signal data — we need * another approach: store id on the row */ task_id = GPOINTER_TO_INT( g_object_get_data( G_OBJECT(check), "task-id")); if (task_id > 0) { task_del(g->tdb, task_id); refresh_tasks(g); } } static void refresh_tasks(Gui *g) { Task *tasks; GtkWidget *row, *hbox, *check, *label, *date; char buf[64]; int i, n, total, done; while ((row = gtk_widget_get_first_child( GTK_WIDGET(g->task_box)))) gtk_list_box_remove( GTK_LIST_BOX(g->task_box), row); if (g->cur_list < 0) { gtk_label_set_text( GTK_LABEL(g->list_label), "Select a list"); gtk_label_set_text( GTK_LABEL(g->status), ""); return; } gtk_label_set_text( GTK_LABEL(g->list_label), g->cur_name); if (task_all(g->tdb, g->cur_list, &tasks, &n) < 0) return; for (i = 0; i < n; i++) { hbox = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 8); check = gtk_check_button_new(); gtk_check_button_set_active( GTK_CHECK_BUTTON(check), tasks[i].done); g_object_set_data(G_OBJECT(check), "task-id", GINT_TO_POINTER(tasks[i].id)); g_signal_connect(check, "toggled", G_CALLBACK(on_check_toggled), GINT_TO_POINTER(tasks[i].id)); gtk_box_append(GTK_BOX(hbox), check); label = gtk_label_new(tasks[i].text); gtk_label_set_xalign( GTK_LABEL(label), 0.0); gtk_widget_set_hexpand(label, TRUE); gtk_label_set_ellipsize( GTK_LABEL(label), PANGO_ELLIPSIZE_END); gtk_box_append(GTK_BOX(hbox), label); date = gtk_label_new(tasks[i].added); gtk_widget_add_css_class(date, "task-date"); gtk_box_append(GTK_BOX(hbox), date); gtk_list_box_append( GTK_LIST_BOX(g->task_box), hbox); if (tasks[i].done) { row = gtk_widget_get_parent(hbox); gtk_widget_add_css_class( row, "task-done"); } } task_count(g->tdb, g->cur_list, &total, &done); snprintf(buf, sizeof(buf), " %s | %d tasks, %d done", g->cur_name, total, done); gtk_label_set_text(GTK_LABEL(g->status), buf); free(tasks); } /* toolbar button helper */ static GtkWidget * make_btn(const char *label, GCallback cb, gpointer data) { GtkWidget *btn; btn = gtk_button_new_with_label(label); g_signal_connect(btn, "clicked", cb, data); return btn; } static void on_theme_toggle(GtkButton *btn, gpointer data) { (void)btn; toggle_theme((Gui *)data); } static void activate(GtkApplication *app, gpointer data) { GtkWidget *window, *header, *paned; GtkWidget *left_box, *right_box; GtkWidget *scroll_lists, *scroll_tasks; GtkWidget *entry, *status, *list_label; GtkWidget *entry_box; GtkCssProvider *css; Gui *g; (void)data; g = &gui; /* squared corners CSS */ css = gtk_css_provider_new(); gtk_css_provider_load_from_string(css, app_css); gtk_style_context_add_provider_for_display( gdk_display_get_default(), GTK_STYLE_PROVIDER(css), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); apply_theme_css(g); /* window */ window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "stask"); gtk_window_set_default_size(GTK_WINDOW(window), 800, 600); g->window = window; /* header bar */ header = gtk_header_bar_new(); g->theme_btn = make_btn( g->dark ? "Light" : "Dark", G_CALLBACK(on_theme_toggle), g); gtk_header_bar_pack_start( GTK_HEADER_BAR(header), g->theme_btn); gtk_header_bar_pack_start( GTK_HEADER_BAR(header), gtk_separator_new( GTK_ORIENTATION_VERTICAL)); gtk_header_bar_pack_start( GTK_HEADER_BAR(header), make_btn("New List", G_CALLBACK(on_new_list), g)); gtk_header_bar_pack_start( GTK_HEADER_BAR(header), make_btn("Delete List", G_CALLBACK(on_del_list), g)); gtk_header_bar_pack_end( GTK_HEADER_BAR(header), make_btn("Delete Task", G_CALLBACK(on_del_task), g)); gtk_window_set_titlebar( GTK_WINDOW(window), header); /* main layout: paned */ paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); gtk_paned_set_position(GTK_PANED(paned), 200); gtk_window_set_child(GTK_WINDOW(window), paned); /* left: list sidebar */ left_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_size_request(left_box, 150, -1); scroll_lists = gtk_scrolled_window_new(); gtk_widget_set_vexpand(scroll_lists, TRUE); g->list_box = gtk_list_box_new(); gtk_list_box_set_selection_mode( GTK_LIST_BOX(g->list_box), GTK_SELECTION_SINGLE); g_signal_connect(g->list_box, "row-selected", G_CALLBACK(on_list_selected), g); gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW(scroll_lists), g->list_box); gtk_box_append(GTK_BOX(left_box), scroll_lists); gtk_paned_set_start_child( GTK_PANED(paned), left_box); gtk_paned_set_resize_start_child( GTK_PANED(paned), FALSE); gtk_paned_set_shrink_start_child( GTK_PANED(paned), FALSE); /* right: task panel */ right_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0); /* list name header */ list_label = gtk_label_new("Select a list"); gtk_widget_add_css_class( list_label, "list-header"); gtk_label_set_xalign( GTK_LABEL(list_label), 0.0); gtk_box_append(GTK_BOX(right_box), list_label); gtk_box_append(GTK_BOX(right_box), gtk_separator_new( GTK_ORIENTATION_HORIZONTAL)); g->list_label = list_label; /* task list */ scroll_tasks = gtk_scrolled_window_new(); gtk_widget_set_vexpand(scroll_tasks, TRUE); g->task_box = gtk_list_box_new(); gtk_list_box_set_selection_mode( GTK_LIST_BOX(g->task_box), GTK_SELECTION_SINGLE); gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW(scroll_tasks), g->task_box); gtk_box_append(GTK_BOX(right_box), scroll_tasks); /* add task entry */ gtk_box_append(GTK_BOX(right_box), gtk_separator_new( GTK_ORIENTATION_HORIZONTAL)); entry_box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_set_margin_start(entry_box, 8); gtk_widget_set_margin_end(entry_box, 8); gtk_widget_set_margin_top(entry_box, 6); gtk_widget_set_margin_bottom(entry_box, 6); entry = gtk_entry_new(); gtk_entry_set_placeholder_text( GTK_ENTRY(entry), "Add task (press Enter)"); gtk_widget_set_hexpand(entry, TRUE); g_signal_connect(entry, "activate", G_CALLBACK(on_entry_activate), g); gtk_box_append(GTK_BOX(entry_box), entry); g->entry = entry; gtk_box_append(GTK_BOX(right_box), entry_box); gtk_paned_set_end_child( GTK_PANED(paned), right_box); gtk_paned_set_resize_end_child( GTK_PANED(paned), TRUE); gtk_paned_set_shrink_end_child( GTK_PANED(paned), FALSE); /* status bar */ status = gtk_label_new(""); gtk_widget_set_halign(status, GTK_ALIGN_START); gtk_widget_set_margin_start(status, 6); gtk_widget_set_margin_end(status, 6); gtk_widget_set_margin_top(status, 3); gtk_widget_set_margin_bottom(status, 3); g->status = status; /* status goes outside paned, at window bottom */ /* wrap paned + status in a vbox */ { GtkWidget *vbox; vbox = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0); gtk_box_append(GTK_BOX(vbox), paned); gtk_box_append(GTK_BOX(vbox), gtk_separator_new( GTK_ORIENTATION_HORIZONTAL)); gtk_box_append(GTK_BOX(vbox), status); gtk_window_set_child( GTK_WINDOW(window), vbox); } /* load lists */ refresh_lists(g); gtk_window_present(GTK_WINDOW(window)); } int gui_run(TaskDB *tdb, int argc, char *argv[]) { GtkApplication *app; int ret; memset(&gui, 0, sizeof(gui)); gui.tdb = tdb; gui.dark = default_dark; gui.theme = gui.dark ? &theme_dark : &theme_light; gui.cur_list = -1; app = gtk_application_new("org.stask.app", G_APPLICATION_NON_UNIQUE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); ret = g_application_run( G_APPLICATION(app), 1, argv); g_object_unref(app); return ret; }