A => .gitignore +3 -0
@@ 1,3 @@
+*.o
+stask
+config.h
A => LICENSE +21 -0
@@ 1,21 @@
+MIT License
+
+Copyright (c) 2026 Kris Yotam
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
A => Makefile +33 -0
@@ 1,33 @@
+# stask - simple task manager
+# See LICENSE file for copyright and license details.
+
+include config.mk
+
+SRC = stask.c data.c gui.c
+OBJ = ${SRC:.c=.o}
+
+all: stask
+
+.c.o:
+ ${CC} -c ${CFLAGS} $<
+
+${OBJ}: stask.h config.h
+
+config.h:
+ cp config.def.h config.h
+
+stask: ${OBJ}
+ ${CC} -o $@ ${OBJ} ${LDFLAGS}
+
+clean:
+ rm -f stask ${OBJ}
+
+install: stask
+ mkdir -p ${DESTDIR}${PREFIX}/bin
+ cp -f stask ${DESTDIR}${PREFIX}/bin
+ chmod 755 ${DESTDIR}${PREFIX}/bin/stask
+
+uninstall:
+ rm -f ${DESTDIR}${PREFIX}/bin/stask
+
+.PHONY: all clean install uninstall
A => config.def.h +69 -0
@@ 1,69 @@
+/* stask - simple task manager
+ * See LICENSE file for copyright and license details. */
+
+/* krisyotam.com monochromatic palette */
+/* all colors: {r, g, b, a} - pure grayscale (0 saturation) */
+
+static const Theme theme_light = {
+ /* bg: hsl(0 0% 98%) */
+ .bg = {0.98, 0.98, 0.98, 1.0},
+ /* card: hsl(0 0% 99%) */
+ .card = {0.99, 0.99, 0.99, 1.0},
+ /* fg: hsl(0 0% 20%) */
+ .fg = {0.20, 0.20, 0.20, 1.0},
+ /* border: hsl(0 0% 88%) */
+ .border = {0.88, 0.88, 0.88, 1.0},
+ /* muted: hsl(0 0% 92%) */
+ .muted = {0.92, 0.92, 0.92, 1.0},
+ /* muted-fg: hsl(0 0% 50%) */
+ .muted_fg = {0.50, 0.50, 0.50, 1.0},
+ /* primary: hsl(0 0% 10%) */
+ .primary = {0.10, 0.10, 0.10, 1.0},
+ /* primary-fg: hsl(0 0% 95%) */
+ .primary_fg = {0.95, 0.95, 0.95, 1.0},
+ /* selection: hsl(0 0% 40%) */
+ .sel = {0.40, 0.40, 0.40, 1.0},
+ /* accent: hsl(0 0% 10%) - dark in light mode */
+ .accent = {0.10, 0.10, 0.10, 1.0},
+ /* accent-fg: hsl(0 0% 95%) */
+ .accent_fg = {0.95, 0.95, 0.95, 1.0},
+};
+
+static const Theme theme_dark = {
+ /* bg: hsl(0 0% 7%) */
+ .bg = {0.07, 0.07, 0.07, 1.0},
+ /* card: hsl(0 0% 9%) */
+ .card = {0.09, 0.09, 0.09, 1.0},
+ /* fg: hsl(0 0% 98%) */
+ .fg = {0.98, 0.98, 0.98, 1.0},
+ /* border: hsl(0 0% 15%) */
+ .border = {0.15, 0.15, 0.15, 1.0},
+ /* muted: hsl(0 0% 12%) */
+ .muted = {0.12, 0.12, 0.12, 1.0},
+ /* muted-fg: hsl(0 0% 70%) */
+ .muted_fg = {0.70, 0.70, 0.70, 1.0},
+ /* primary: hsl(0 0% 98%) */
+ .primary = {0.98, 0.98, 0.98, 1.0},
+ /* primary-fg: hsl(0 0% 9%) */
+ .primary_fg = {0.09, 0.09, 0.09, 1.0},
+ /* selection: hsl(0 0% 83%) */
+ .sel = {0.83, 0.83, 0.83, 1.0},
+ /* accent: hsl(0 0% 88%) - light in dark mode */
+ .accent = {0.88, 0.88, 0.88, 1.0},
+ /* accent-fg: hsl(0 0% 9%) */
+ .accent_fg = {0.09, 0.09, 0.09, 1.0},
+};
+
+/* default: 0=light, 1=dark */
+static const int default_dark = 0;
+
+/* GTK CSS for squared corners (krisyotam.com aesthetic) */
+static const char *app_css =
+ "* { border-radius: 0; }\n"
+ "button { border-radius: 0; padding: 4px 10px; }\n"
+ "popover > contents { border-radius: 0; padding: 0; }\n"
+ "entry { border-radius: 0; }\n"
+ "headerbar { border-radius: 0; }\n"
+ "window { border-radius: 0; }\n"
+ "separator { margin: 0 2px; }\n"
+ "checkbutton indicator { border-radius: 0; }\n";
A => config.mk +24 -0
@@ 1,24 @@
+# stask - simple task manager
+# See LICENSE file for copyright and license details.
+
+VERSION = 0.1
+
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/share/man
+
+PKG_CONFIG = pkg-config
+
+# GTK4 and SQLite3
+GTKINC = `$(PKG_CONFIG) --cflags gtk4`
+GTKLIB = `$(PKG_CONFIG) --libs gtk4`
+SQLITEINC = `$(PKG_CONFIG) --cflags sqlite3`
+SQLITELIB = `$(PKG_CONFIG) --libs sqlite3`
+
+INCS = ${GTKINC} ${SQLITEINC}
+LIBS = ${GTKLIB} ${SQLITELIB}
+
+CPPFLAGS = -D_POSIX_C_SOURCE=200809L -DVERSION=\"${VERSION}\"
+CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os ${INCS} ${CPPFLAGS}
+LDFLAGS = ${LIBS}
+
+CC = cc
A => data.c +387 -0
@@ 1,387 @@
+/* stask - simple task manager
+ * See LICENSE file for copyright and license details. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "stask.h"
+
+static void
+now_str(char *buf, int len)
+{
+ time_t t;
+ struct tm *tm;
+
+ t = time(NULL);
+ tm = localtime(&t);
+ strftime(buf, len, "%Y-%m-%d", tm);
+}
+
+static int
+ensure_dir(const char *path)
+{
+ char tmp[1024];
+ char *p;
+
+ snprintf(tmp, sizeof(tmp), "%s", path);
+ for (p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ mkdir(tmp, 0755);
+ *p = '/';
+ }
+ }
+ return mkdir(tmp, 0755);
+}
+
+int
+db_open(TaskDB *tdb)
+{
+ const char *home;
+ char dir[1024];
+ int rc;
+ const char *sql;
+
+ home = getenv("HOME");
+ if (!home)
+ home = "/tmp";
+
+ snprintf(dir, sizeof(dir),
+ "%s/.local/share/stask", home);
+ ensure_dir(dir);
+
+ snprintf(tdb->dbpath, sizeof(tdb->dbpath),
+ "%s/stask.db", dir);
+
+ rc = sqlite3_open(tdb->dbpath, &tdb->db);
+ if (rc != SQLITE_OK) {
+ fprintf(stderr, "stask: cannot open db: %s\n",
+ sqlite3_errmsg(tdb->db));
+ return -1;
+ }
+
+ sql =
+ "CREATE TABLE IF NOT EXISTS lists ("
+ " id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ " name TEXT UNIQUE NOT NULL,"
+ " created TEXT NOT NULL"
+ ");"
+ "CREATE TABLE IF NOT EXISTS tasks ("
+ " id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ " list_id INTEGER NOT NULL,"
+ " text TEXT NOT NULL,"
+ " done INTEGER NOT NULL DEFAULT 0,"
+ " added TEXT NOT NULL,"
+ " FOREIGN KEY(list_id) REFERENCES lists(id)"
+ " ON DELETE CASCADE"
+ ");";
+
+ rc = sqlite3_exec(tdb->db, sql, NULL, NULL, NULL);
+ if (rc != SQLITE_OK) {
+ fprintf(stderr, "stask: schema error: %s\n",
+ sqlite3_errmsg(tdb->db));
+ return -1;
+ }
+
+ /* enable foreign keys */
+ sqlite3_exec(tdb->db,
+ "PRAGMA foreign_keys = ON;",
+ NULL, NULL, NULL);
+
+ return 0;
+}
+
+void
+db_close(TaskDB *tdb)
+{
+ if (tdb->db)
+ sqlite3_close(tdb->db);
+ tdb->db = NULL;
+}
+
+int
+list_create(TaskDB *tdb, const char *name)
+{
+ sqlite3_stmt *stmt;
+ char date[32];
+ int rc;
+
+ now_str(date, sizeof(date));
+ rc = sqlite3_prepare_v2(tdb->db,
+ "INSERT INTO lists (name, created) VALUES (?, ?);",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 2, date, -1, SQLITE_STATIC);
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+int
+list_delete(TaskDB *tdb, const char *name)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ /* delete tasks first (in case FK not enforced) */
+ rc = sqlite3_prepare_v2(tdb->db,
+ "DELETE FROM tasks WHERE list_id = "
+ "(SELECT id FROM lists WHERE name = ?);",
+ -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(stmt, 1, name, -1,
+ SQLITE_STATIC);
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ }
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "DELETE FROM lists WHERE name = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+int
+list_all(TaskDB *tdb, List **out, int *count)
+{
+ sqlite3_stmt *stmt;
+ int rc, n, cap;
+ List *arr;
+
+ *out = NULL;
+ *count = 0;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "SELECT id, name, created FROM lists "
+ "ORDER BY name;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ n = 0;
+ cap = 16;
+ arr = malloc(cap * sizeof(List));
+ if (!arr) {
+ sqlite3_finalize(stmt);
+ return -1;
+ }
+
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
+ if (n >= cap) {
+ cap *= 2;
+ arr = realloc(arr, cap * sizeof(List));
+ if (!arr) {
+ sqlite3_finalize(stmt);
+ return -1;
+ }
+ }
+ arr[n].id = sqlite3_column_int(stmt, 0);
+ snprintf(arr[n].name, sizeof(arr[n].name),
+ "%s", sqlite3_column_text(stmt, 1));
+ snprintf(arr[n].created,
+ sizeof(arr[n].created),
+ "%s", sqlite3_column_text(stmt, 2));
+ n++;
+ }
+
+ sqlite3_finalize(stmt);
+ *out = arr;
+ *count = n;
+ return 0;
+}
+
+int
+list_by_name(TaskDB *tdb, const char *name, List *out)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "SELECT id, name, created FROM lists "
+ "WHERE name = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
+ rc = sqlite3_step(stmt);
+ if (rc != SQLITE_ROW) {
+ sqlite3_finalize(stmt);
+ return -1;
+ }
+
+ out->id = sqlite3_column_int(stmt, 0);
+ snprintf(out->name, sizeof(out->name),
+ "%s", sqlite3_column_text(stmt, 1));
+ snprintf(out->created, sizeof(out->created),
+ "%s", sqlite3_column_text(stmt, 2));
+ sqlite3_finalize(stmt);
+ return 0;
+}
+
+int
+task_add(TaskDB *tdb, int list_id, const char *text)
+{
+ sqlite3_stmt *stmt;
+ char date[32];
+ int rc;
+
+ now_str(date, sizeof(date));
+ rc = sqlite3_prepare_v2(tdb->db,
+ "INSERT INTO tasks (list_id, text, done, added)"
+ " VALUES (?, ?, 0, ?);",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_int(stmt, 1, list_id);
+ sqlite3_bind_text(stmt, 2, text, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 3, date, -1, SQLITE_STATIC);
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+int
+task_del(TaskDB *tdb, int task_id)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "DELETE FROM tasks WHERE id = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_int(stmt, 1, task_id);
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+int
+task_done(TaskDB *tdb, int task_id)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "UPDATE tasks SET done = 1 WHERE id = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_int(stmt, 1, task_id);
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+int
+task_undo(TaskDB *tdb, int task_id)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "UPDATE tasks SET done = 0 WHERE id = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_int(stmt, 1, task_id);
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+int
+task_all(TaskDB *tdb, int list_id, Task **out, int *count)
+{
+ sqlite3_stmt *stmt;
+ int rc, n, cap;
+ Task *arr;
+
+ *out = NULL;
+ *count = 0;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "SELECT id, list_id, text, done, added "
+ "FROM tasks WHERE list_id = ? "
+ "ORDER BY done ASC, id ASC;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_int(stmt, 1, list_id);
+
+ n = 0;
+ cap = 32;
+ arr = malloc(cap * sizeof(Task));
+ if (!arr) {
+ sqlite3_finalize(stmt);
+ return -1;
+ }
+
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
+ if (n >= cap) {
+ cap *= 2;
+ arr = realloc(arr, cap * sizeof(Task));
+ if (!arr) {
+ sqlite3_finalize(stmt);
+ return -1;
+ }
+ }
+ arr[n].id = sqlite3_column_int(stmt, 0);
+ arr[n].list_id = sqlite3_column_int(stmt, 1);
+ snprintf(arr[n].text, sizeof(arr[n].text),
+ "%s", sqlite3_column_text(stmt, 2));
+ arr[n].done = sqlite3_column_int(stmt, 3);
+ snprintf(arr[n].added, sizeof(arr[n].added),
+ "%s", sqlite3_column_text(stmt, 4));
+ n++;
+ }
+
+ sqlite3_finalize(stmt);
+ *out = arr;
+ *count = n;
+ return 0;
+}
+
+int
+task_count(TaskDB *tdb, int list_id, int *total, int *done)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ *total = 0;
+ *done = 0;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "SELECT COUNT(*), SUM(done) FROM tasks "
+ "WHERE list_id = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_int(stmt, 1, list_id);
+ if (sqlite3_step(stmt) == SQLITE_ROW) {
+ *total = sqlite3_column_int(stmt, 0);
+ *done = sqlite3_column_int(stmt, 1);
+ }
+ sqlite3_finalize(stmt);
+ return 0;
+}
A => gui.c +731 -0
@@ 1,731 @@
+/* stask - simple task manager
+ * See LICENSE file for copyright and license details. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <gtk/gtk.h>
+#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;
+}
A => stask.c +209 -0
@@ 1,209 @@
+/* stask - simple task manager
+ * See LICENSE file for copyright and license details. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "stask.h"
+#include "config.h"
+
+static void
+usage(void)
+{
+ fprintf(stderr,
+ "usage: stask [-g]\n"
+ " stask [ls]\n"
+ " stask new <list>\n"
+ " stask rm <list>\n"
+ " stask <list>\n"
+ " stask <list> add <text>\n"
+ " stask <list> done <id>\n"
+ " stask <list> undo <id>\n"
+ " stask <list> del <id>\n");
+ exit(1);
+}
+
+static void
+cmd_ls(TaskDB *tdb)
+{
+ List *lists;
+ int i, n, total, done;
+
+ if (list_all(tdb, &lists, &n) < 0) {
+ fprintf(stderr, "stask: cannot list\n");
+ return;
+ }
+
+ if (n == 0) {
+ printf("No lists. Create one with: stask new <name>\n");
+ free(lists);
+ return;
+ }
+
+ printf("%-20s %10s %10s %s\n",
+ "LIST", "TASKS", "DONE", "CREATED");
+ for (i = 0; i < n; i++) {
+ task_count(tdb, lists[i].id, &total, &done);
+ printf("%-20s %10d %10d %s\n",
+ lists[i].name, total, done,
+ lists[i].created);
+ }
+ free(lists);
+}
+
+static void
+cmd_show(TaskDB *tdb, const char *name)
+{
+ List list;
+ Task *tasks;
+ int i, n;
+
+ if (list_by_name(tdb, name, &list) < 0) {
+ fprintf(stderr,
+ "stask: list '%s' not found\n", name);
+ return;
+ }
+
+ if (task_all(tdb, list.id, &tasks, &n) < 0) {
+ fprintf(stderr, "stask: cannot read tasks\n");
+ return;
+ }
+
+ if (n == 0) {
+ printf("%s: empty\n", name);
+ free(tasks);
+ return;
+ }
+
+ printf("%s:\n", name);
+ for (i = 0; i < n; i++) {
+ printf(" [%c] %-4d %-40s %s\n",
+ tasks[i].done ? 'x' : ' ',
+ tasks[i].id,
+ tasks[i].text,
+ tasks[i].added);
+ }
+ free(tasks);
+}
+
+static void
+cmd_new(TaskDB *tdb, const char *name)
+{
+ if (list_create(tdb, name) < 0)
+ fprintf(stderr,
+ "stask: cannot create '%s' "
+ "(already exists?)\n", name);
+ else
+ printf("Created list '%s'\n", name);
+}
+
+static void
+cmd_rm(TaskDB *tdb, const char *name)
+{
+ if (list_delete(tdb, name) < 0)
+ fprintf(stderr,
+ "stask: cannot remove '%s'\n", name);
+ else
+ printf("Removed list '%s'\n", name);
+}
+
+static void
+cmd_add(TaskDB *tdb, const char *listname, const char *text)
+{
+ List list;
+
+ if (list_by_name(tdb, listname, &list) < 0) {
+ fprintf(stderr,
+ "stask: list '%s' not found\n",
+ listname);
+ return;
+ }
+ if (task_add(tdb, list.id, text) < 0)
+ fprintf(stderr, "stask: cannot add task\n");
+ else
+ printf("Added to '%s'\n", listname);
+}
+
+static void
+cmd_done(TaskDB *tdb, int id)
+{
+ if (task_done(tdb, id) < 0)
+ fprintf(stderr,
+ "stask: cannot mark done: %d\n", id);
+}
+
+static void
+cmd_undo(TaskDB *tdb, int id)
+{
+ if (task_undo(tdb, id) < 0)
+ fprintf(stderr,
+ "stask: cannot undo: %d\n", id);
+}
+
+static void
+cmd_del(TaskDB *tdb, int id)
+{
+ if (task_del(tdb, id) < 0)
+ fprintf(stderr,
+ "stask: cannot delete: %d\n", id);
+}
+
+int
+main(int argc, char *argv[])
+{
+ TaskDB tdb;
+ int id;
+
+ memset(&tdb, 0, sizeof(tdb));
+
+ /* gui mode */
+ if (argc >= 2 && strcmp(argv[1], "-g") == 0) {
+ if (db_open(&tdb) < 0)
+ return 1;
+ id = gui_run(&tdb, argc, argv);
+ db_close(&tdb);
+ return id;
+ }
+
+ if (db_open(&tdb) < 0)
+ return 1;
+
+ if (argc < 2 || strcmp(argv[1], "ls") == 0) {
+ cmd_ls(&tdb);
+ } else if (strcmp(argv[1], "new") == 0) {
+ if (argc < 3) usage();
+ cmd_new(&tdb, argv[2]);
+ } else if (strcmp(argv[1], "rm") == 0) {
+ if (argc < 3) usage();
+ cmd_rm(&tdb, argv[2]);
+ } else if (argc == 2) {
+ /* stask <list> - show tasks */
+ cmd_show(&tdb, argv[1]);
+ } else if (argc >= 3) {
+ /* stask <list> <cmd> [args] */
+ if (strcmp(argv[2], "add") == 0) {
+ if (argc < 4) usage();
+ cmd_add(&tdb, argv[1], argv[3]);
+ } else if (strcmp(argv[2], "done") == 0) {
+ if (argc < 4) usage();
+ id = atoi(argv[3]);
+ cmd_done(&tdb, id);
+ } else if (strcmp(argv[2], "undo") == 0) {
+ if (argc < 4) usage();
+ id = atoi(argv[3]);
+ cmd_undo(&tdb, id);
+ } else if (strcmp(argv[2], "del") == 0) {
+ if (argc < 4) usage();
+ id = atoi(argv[3]);
+ cmd_del(&tdb, id);
+ } else {
+ usage();
+ }
+ } else {
+ usage();
+ }
+
+ db_close(&tdb);
+ return 0;
+}
A => stask.h +63 -0
@@ 1,63 @@
+/* stask - simple task manager
+ * See LICENSE file for copyright and license details. */
+
+#ifndef STASK_H
+#define STASK_H
+
+#include <sqlite3.h>
+
+enum { TASK_TODO, TASK_DONE };
+
+typedef struct {
+ double bg[4];
+ double card[4];
+ double fg[4];
+ double border[4];
+ double muted[4];
+ double muted_fg[4];
+ double primary[4];
+ double primary_fg[4];
+ double sel[4];
+ double accent[4];
+ double accent_fg[4];
+} Theme;
+
+typedef struct {
+ int id;
+ char name[128];
+ char created[32];
+} List;
+
+typedef struct {
+ int id;
+ int list_id;
+ char text[512];
+ int done;
+ char added[32];
+} Task;
+
+typedef struct {
+ sqlite3 *db;
+ char dbpath[1024];
+} TaskDB;
+
+/* data.c */
+int db_open(TaskDB *tdb);
+void db_close(TaskDB *tdb);
+
+int list_create(TaskDB *tdb, const char *name);
+int list_delete(TaskDB *tdb, const char *name);
+int list_all(TaskDB *tdb, List **out, int *count);
+int list_by_name(TaskDB *tdb, const char *name, List *out);
+
+int task_add(TaskDB *tdb, int list_id, const char *text);
+int task_del(TaskDB *tdb, int task_id);
+int task_done(TaskDB *tdb, int task_id);
+int task_undo(TaskDB *tdb, int task_id);
+int task_all(TaskDB *tdb, int list_id, Task **out, int *count);
+int task_count(TaskDB *tdb, int list_id, int *total, int *done);
+
+/* gui.c */
+int gui_run(TaskDB *tdb, int argc, char *argv[]);
+
+#endif /* STASK_H */