From b51ff80d4e8058081f6a73fcfefa1b4f731d651f Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Wed, 21 Oct 2026 04:39:20 +0800 Subject: [PATCH 01/12] fix(linux): preserve 64-bit task and list IDs in GTK row data --- linux/src/main.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 0e562d5..0401c45 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -959,7 +959,7 @@ std::vector selected_task_ids() { auto* row = GTK_LIST_BOX_ROW(node->data); auto const pointer = g_object_get_data(G_OBJECT(row), "task-id"); if (pointer != nullptr) { - ids.push_back(static_cast(GPOINTER_TO_INT(pointer))); + ids.push_back(static_cast(GPOINTER_TO_SIZE(pointer))); } } g_list_free(selected); @@ -1934,7 +1934,7 @@ void open_list_editor( void on_task_check_toggled(GtkCheckButton* button, gpointer) { if (g_state.api == nullptr) return; auto const id = static_cast( - GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "task-id"))); + GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(button), "task-id"))); toggle_task(id, gtk_check_button_get_active(button)); } @@ -2108,7 +2108,7 @@ void open_task_editor(std::int64_t id, bool focus_date) { child != nullptr; child = gtk_widget_get_next_sibling(child)) { if (!GTK_IS_LIST_BOX_ROW(child)) continue; - auto const row_id = static_cast(GPOINTER_TO_INT( + auto const row_id = static_cast(GPOINTER_TO_SIZE( g_object_get_data(G_OBJECT(child), "task-id"))); if (row_id == id) { set_task_editor_expanded(GTK_LIST_BOX_ROW(child), id, true, @@ -2125,7 +2125,7 @@ void collapse_open_editor() { child != nullptr; child = gtk_widget_get_next_sibling(child)) { if (!GTK_IS_LIST_BOX_ROW(child)) continue; - auto const row_id = static_cast(GPOINTER_TO_INT( + auto const row_id = static_cast(GPOINTER_TO_SIZE( g_object_get_data(G_OBJECT(child), "task-id"))); if (row_id == g_state.open_editor_id) { set_task_editor_expanded(GTK_LIST_BOX_ROW(child), row_id, false, @@ -2139,7 +2139,7 @@ void collapse_open_editor() { void show_task_menu(GtkWidget* row, gdouble x, gdouble y, bool user_list) { auto* menu = g_menu_new(); if (user_list) { - auto const list_id = static_cast(GPOINTER_TO_INT( + auto const list_id = static_cast(GPOINTER_TO_SIZE( g_object_get_data(G_OBJECT(row), "list-id"))); auto* section = g_menu_new(); g_menu_append(section, tr("dialogEditList").c_str(), @@ -2149,7 +2149,7 @@ void show_task_menu(GtkWidget* row, gdouble x, gdouble y, bool user_list) { g_menu_append_section(menu, nullptr, G_MENU_MODEL(section)); g_object_unref(section); } else { - auto const task_id = static_cast(GPOINTER_TO_INT( + auto const task_id = static_cast(GPOINTER_TO_SIZE( g_object_get_data(G_OBJECT(row), "task-id"))); auto* due_section = g_menu_new(); g_menu_append(due_section, tr("navToday").c_str(), @@ -2201,7 +2201,7 @@ void on_row_gesture_pressed(GtkGestureClick* gesture, gint n_press, } else if (button == 1 && n_press == 2 && is_user_list) { // Task rows use GtkListBox's native row-activated signal instead: // handling both double-click gestures would toggle the editor twice. - auto const id = static_cast(GPOINTER_TO_INT( + auto const id = static_cast(GPOINTER_TO_SIZE( g_object_get_data(G_OBJECT(row), "list-id"))); auto* parameter = g_variant_new_string(std::to_string(id).c_str()); g_action_group_activate_action( @@ -2248,7 +2248,7 @@ GtkWidget* make_task_row(rivet_app::Task const& task) { gtk_check_button_set_active(GTK_CHECK_BUTTON(check), task.completed); gtk_widget_set_valign(check, GTK_ALIGN_CENTER); g_object_set_data(G_OBJECT(check), "task-id", - GINT_TO_POINTER(static_cast(task.id))); + GSIZE_TO_POINTER(static_cast(task.id))); g_signal_connect(check, "toggled", G_CALLBACK(on_task_check_toggled), nullptr); gtk_box_append(GTK_BOX(box), check); @@ -2302,7 +2302,7 @@ GtkWidget* make_task_row(rivet_app::Task const& task) { gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box); g_object_set_data(G_OBJECT(row), "task-id", - GINT_TO_POINTER(static_cast(task.id))); + GSIZE_TO_POINTER(static_cast(task.id))); attach_row_gestures(row, false); return row; } @@ -2466,7 +2466,7 @@ GtkWidget* make_user_list_row(rivet_app::TodoList const& list) { gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box); g_object_set_data(G_OBJECT(row), "list-id", - GINT_TO_POINTER(static_cast(list.id))); + GSIZE_TO_POINTER(static_cast(list.id))); attach_row_gestures(row, true); return row; } @@ -2522,7 +2522,7 @@ void rebuild_task_list() { child != nullptr; child = gtk_widget_get_next_sibling(child)) { if (!GTK_IS_LIST_BOX_ROW(child)) continue; - auto const row_id = static_cast(GPOINTER_TO_INT( + auto const row_id = static_cast(GPOINTER_TO_SIZE( g_object_get_data(G_OBJECT(child), "task-id"))); if (row_id != g_state.open_editor_id) continue; auto* editor = static_cast( @@ -2558,7 +2558,7 @@ void rebuild_task_list() { auto const value = g_object_get_data(G_OBJECT(selected), "task-id"); if (value != nullptr) { selected_task_ids.insert( - static_cast(GPOINTER_TO_INT(value))); + static_cast(GPOINTER_TO_SIZE(value))); } } g_list_free(selected_rows); @@ -3055,7 +3055,7 @@ void on_user_row_selected(GtkListBox*, GtkListBoxRow* row, gpointer) { if (!g_state.current_search.empty()) { gtk_editable_set_text(GTK_EDITABLE(g_state.search_box), ""); } - auto const id = static_cast(GPOINTER_TO_INT( + auto const id = static_cast(GPOINTER_TO_SIZE( g_object_get_data(G_OBJECT(row), "list-id"))); g_state.view = ViewKind::List; g_state.view_list_id = id; @@ -3769,7 +3769,7 @@ void on_activate(GtkApplication* app, gpointer) { G_CALLBACK(+[](GtkListBox*, GtkListBoxRow* row, gpointer) { auto const data = g_object_get_data(G_OBJECT(row), "task-id"); if (data == nullptr) return; // completed-section header - auto const id = static_cast(GPOINTER_TO_INT(data)); + auto const id = static_cast(GPOINTER_TO_SIZE(data)); toggle_task_editor(row, id, false); }), nullptr); gtk_widget_add_css_class(task_list, "task-row-list"); From d26030b2850203b79d270aa954cd8a19bc67d408 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 16:55:16 +0800 Subject: [PATCH 02/12] test(linux): exercise 64-bit IDs and undo end to end --- scripts/e2e/e2e-linux.sh | 73 ++++++++++++++++++++ scripts/e2e/linux-atspi.py | 136 ++++++++++++++++++++++++++++++++++++- 2 files changed, 206 insertions(+), 3 deletions(-) diff --git a/scripts/e2e/e2e-linux.sh b/scripts/e2e/e2e-linux.sh index e2b9ac2..8a3a9a6 100644 --- a/scripts/e2e/e2e-linux.sh +++ b/scripts/e2e/e2e-linux.sh @@ -17,6 +17,56 @@ last-selected-list-id = 0 last-update-check = $epoch EOF +# Seed IDs beyond the 32-bit boundary so the GTK acceptance flow exercises +# the exact row-data conversions used by list selection and batch actions. +/usr/bin/python3 - "$home/.taskly/tasks.db" <<'PY' +import sqlite3 +import sys + +database = sqlite3.connect(sys.argv[1]) +database.executescript( + """ + PRAGMA journal_mode = WAL; + PRAGMA user_version = 4; + CREATE TABLE lists ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + icon TEXT, + color INTEGER, + created_at TEXT NOT NULL + ); + CREATE TABLE tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + list_id INTEGER, + text TEXT NOT NULL, + due_date TEXT, + due_time TEXT, + completed INTEGER DEFAULT 0, + created_at TEXT NOT NULL, + notes TEXT, + FOREIGN KEY (list_id) REFERENCES lists (id) + ); + CREATE INDEX idx_tasks_list_id ON tasks(list_id); + CREATE INDEX idx_tasks_completed ON tasks(completed); + CREATE INDEX idx_tasks_due_date ON tasks(due_date); + INSERT INTO lists (id, name, icon, color, created_at) VALUES + (1, 'Default list', 'D', -16745729, '2026-10-21T00:00:00Z'), + (4294967301, '64-bit identity list', 'H', -13318311, '2026-10-21T00:00:00Z'); + INSERT INTO tasks + (id, list_id, text, due_date, due_time, completed, created_at, notes) + VALUES + (1, 1, 'Default-list sentinel', NULL, NULL, 0, + '2026-10-21T00:00:00Z', NULL), + (4294967302, 4294967301, '64-bit task A', NULL, '09:30', 0, + '2026-10-21T00:00:00Z', 'metadata A'), + (4294967303, 4294967301, '64-bit task B', NULL, '10:45', 0, + '2026-10-21T00:00:00Z', 'metadata B'); + """ +) +database.commit() +database.close() +PY + export HOME="$home" export XDG_RUNTIME_DIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/taskly-acceptance-runtime-$$" export WAYLAND_DISPLAY=wayland-taskly-acceptance @@ -88,6 +138,29 @@ set -e app_pid="" [[ "$app_status" -eq 0 || "$app_status" -eq 143 ]] +# UI actions must have targeted the full-width IDs and kept undo metadata. +/usr/bin/python3 - "$home/.taskly/tasks.db" <<'PY' +import datetime +import sqlite3 +import sys + +database = sqlite3.connect(sys.argv[1]) +tomorrow = (datetime.date.today() + datetime.timedelta(days=1)).isoformat() +rows = database.execute( + "SELECT id, list_id, text, due_date, due_time, completed, notes " + "FROM tasks WHERE id IN (4294967302, 4294967303) ORDER BY id" +).fetchall() +assert rows == [ + (4294967302, 4294967301, '64-bit task A', tomorrow, None, 0, 'metadata A'), + (4294967303, 4294967301, '64-bit task B', tomorrow, None, 0, 'metadata B'), +], rows +created = database.execute( + "SELECT list_id, completed FROM tasks WHERE text = 'Acceptance task'" +).fetchone() +assert created == (4294967301, 0), created +database.close() +PY + trap - EXIT kill -TERM "$atspi_pid" "$weston_pid" 2>/dev/null || true wait "$atspi_pid" "$weston_pid" 2>/dev/null || true diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 9a88748..8ab169f 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -36,7 +36,7 @@ def text(node): return "" -def wait_for(description, probe, timeout=30.0): +def wait_for(description, probe, timeout=60.0): deadline = time.monotonic() + timeout while time.monotonic() < deadline: value = probe() @@ -78,6 +78,67 @@ def contains_text(root, expected): return any((node.name or "") == expected or text(node) == expected for node in walk(root)) +def count_text_fragment(root, expected): + return sum( + 1 + for node in walk(root) + if expected in (node.name or "") or expected in text(node) + ) + + +def matching_child_indexes(container, expected): + result = [] + for index in range(container.childCount): + try: + child = container.getChildAtIndex(index) + except Exception: + continue + if contains_text(child, expected): + result.append(index) + return result + + +def select_single_named_child(root, expected): + """Select the one direct row containing expected in a selection container.""" + for container in walk(root): + try: + selection = container.querySelection() + except Exception: + continue + indexes = matching_child_indexes(container, expected) + if len(indexes) != 1: + continue + selection.clearSelection() + if not selection.selectChild(indexes[0]): + raise AssertionError(f"AT-SPI failed to select row containing {expected!r}") + return container + return None + + +def select_named_children(root, expected_names): + """Select the direct rows that contain every expected task title.""" + for container in walk(root): + try: + selection = container.querySelection() + except Exception: + continue + indexes = [] + for expected in expected_names: + matches = matching_child_indexes(container, expected) + if len(matches) != 1: + indexes = [] + break + indexes.append(matches[0]) + if not indexes or len(set(indexes)) != len(expected_names): + continue + selection.clearSelection() + for index in indexes: + if not selection.selectChild(index): + raise AssertionError("AT-SPI failed to select all requested task rows") + return container + return None + + def snapshot(node, depth=0): result = {"name": node.name or "", "role": role(node)} value = text(node) @@ -107,8 +168,64 @@ def main(): # stable behavioral contract we actually need. entry = wait_for("quick-add entry", lambda: find_editable_named(window, "+ Add Task")) submit = wait_for("quick-add submit", lambda: find_named(window, "+ Add Task", "push button")) + + wait_for("default-list sentinel", lambda: contains_text(window, "Default-list sentinel")) + wait_for("first 64-bit task row", lambda: contains_text(window, "64-bit task A")) + wait_for("second 64-bit task row", lambda: contains_text(window, "64-bit task B")) + + wait_for( + "64-bit list row selection", + lambda: select_single_named_child(window, "64-bit identity list"), + ) + wait_for( + "64-bit list filter", + lambda: not contains_text(window, "Default-list sentinel") + and contains_text(window, "64-bit task A") + and contains_text(window, "64-bit task B"), + ) + + wait_for( + "two 64-bit task rows selected for scheduling", + lambda: select_named_children(window, ["64-bit task A", "64-bit task B"]), + ) + schedule = wait_for( + "batch schedule button", + lambda: find_named(window, "Schedule Selected", "push button"), + ) + press(schedule) + tomorrow = wait_for( + "Tomorrow schedule option", lambda: find_named(window, "Tomorrow", "push button") + ) + press(tomorrow) + wait_for( + "scheduled task rows refreshed", + lambda: count_text_fragment(window, "Tomorrow") >= 2, + ) + + wait_for( + "two 64-bit task rows selected for deletion", + lambda: select_named_children(window, ["64-bit task A", "64-bit task B"]), + ) + delete = wait_for( + "batch delete button", lambda: find_named(window, "Delete List", "push button") + ) + press(delete) + wait_for( + "staged tasks hidden", + lambda: not contains_text(window, "64-bit task A") + and not contains_text(window, "64-bit task B"), + ) + undo = wait_for("undo banner", lambda: find_named(window, "Undo", "push button")) + press(undo) + wait_for( + "identity-preserving undo", + lambda: contains_text(window, "64-bit task A") + and contains_text(window, "64-bit task B"), + ) + entry.queryEditableText().setTextContents("Acceptance task") press(submit) + wait_for("quick-add field cleared", lambda: text(entry) == "") wait_for("RPC-created task row", lambda: contains_text(window, "Acceptance task")) (args.output / "accessibility.json").write_text( @@ -118,11 +235,24 @@ def main(): json.dumps( { "platform": "linux", - "actions": ["set:quick-add", "press:quick-add-submit"], + "actions": [ + "select:64-bit-list-row", + "select:two-64-bit-task-rows", + "press:batch-schedule-tomorrow", + "press:batch-delete", + "press:undo", + "set:quick-add", + "press:quick-add-submit", + ], "assertions": [ "packaged GTK4 frame activated on native Wayland", "AT-SPI roles and names exposed", - "RPC-created task appeared through the changed Event", + "list and task IDs above 2^32 survive GTK row data", + "batch scheduling clears hidden due times for both tasks", + "six-second staged deletion hides both tasks without deleting them", + "undo restores the original task IDs and metadata", + "RPC-created task appeared after the quick-add field cleared", + "quick-add preserved the selected 64-bit list ownership", ], }, indent=2, From 0e8d6fe6ef7ac30f2ce9f01997aecf49d4a416f0 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 17:02:03 +0800 Subject: [PATCH 03/12] test(linux): tolerate AT-SPI row refresh races --- scripts/e2e/linux-atspi.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 8ab169f..95e95a6 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -10,7 +10,11 @@ def children(node): for index in range(node.childCount): try: - yield node.getChildAtIndex(index) + child = node.getChildAtIndex(index) + # GTK can remove and rebuild a row between childCount and the + # D-Bus lookup while async task snapshots arrive. + if child is not None: + yield child except Exception: continue From bde9a17e2990398997516dd35fb35bc283b9a6c3 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 17:02:42 +0800 Subject: [PATCH 04/12] test(linux): skip transient null accessibility roots --- scripts/e2e/linux-atspi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 95e95a6..2da55bb 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -20,6 +20,8 @@ def children(node): def walk(node): + if node is None: + return yield node for child in children(node): yield from walk(child) From 4c6d968bdc44372fceb3b376c5ec26d9721d89b3 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 17:11:31 +0800 Subject: [PATCH 05/12] fix(linux): make batch scheduling date-only --- linux/src/main.cpp | 7 ++++--- scripts/e2e/linux-atspi.py | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 0401c45..a010986 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -1024,9 +1024,10 @@ void batch_schedule_selected(BulkScheduleCommand const& command) { if (auto const* task = find_selected_task(id)) { auto updated = *task; updated.due_date = command.due_date; - // Clearing the date also clears the time. Keeping an invisible - // due_time is inconsistent with the single-task Clear action. - if (!command.due_date.has_value()) updated.due_time = std::nullopt; + // Batch scheduling is deliberately date-only. Replace the complete + // schedule so an old or inconsistent hidden time cannot leak into the + // newly selected Today/Tomorrow date. + updated.due_time = std::nullopt; save_task_edits(std::move(updated)); } } diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 2da55bb..6b54b0f 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -208,12 +208,13 @@ def main(): lambda: count_text_fragment(window, "Tomorrow") >= 2, ) - wait_for( - "two 64-bit task rows selected for deletion", - lambda: select_named_children(window, ["64-bit task A", "64-bit task B"]), - ) delete = wait_for( - "batch delete button", lambda: find_named(window, "Delete List", "push button") + "two selected 64-bit task rows and batch delete button", + lambda: ( + find_named(window, "Delete List", "push button") + if select_named_children(window, ["64-bit task A", "64-bit task B"]) + else None + ), ) press(delete) wait_for( From 4b82e2db9005dc3400282628f7b8ee700c1b8169 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 17:19:06 +0800 Subject: [PATCH 06/12] test(linux): retain stable bulk action references --- scripts/e2e/linux-atspi.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 6b54b0f..b6a7378 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -84,14 +84,6 @@ def contains_text(root, expected): return any((node.name or "") == expected or text(node) == expected for node in walk(root)) -def count_text_fragment(root, expected): - return sum( - 1 - for node in walk(root) - if expected in (node.name or "") or expected in text(node) - ) - - def matching_child_indexes(container, expected): result = [] for index in range(container.childCount): @@ -198,23 +190,20 @@ def main(): "batch schedule button", lambda: find_named(window, "Schedule Selected", "push button"), ) + # The bulk toolbar is stable for the lifetime of the window. Keep the + # delete action now, while the toolbar is known to be exposed, because an + # asynchronous row rebuild can briefly hide the toolbar after scheduling. + delete = wait_for( + "batch delete button", lambda: find_named(window, "Delete List", "push button") + ) press(schedule) tomorrow = wait_for( "Tomorrow schedule option", lambda: find_named(window, "Tomorrow", "push button") ) press(tomorrow) wait_for( - "scheduled task rows refreshed", - lambda: count_text_fragment(window, "Tomorrow") >= 2, - ) - - delete = wait_for( - "two selected 64-bit task rows and batch delete button", - lambda: ( - find_named(window, "Delete List", "push button") - if select_named_children(window, ["64-bit task A", "64-bit task B"]) - else None - ), + "two 64-bit task rows selected for deletion", + lambda: select_named_children(window, ["64-bit task A", "64-bit task B"]), ) press(delete) wait_for( From 847f0a64a7b504cdfd2048f6df5c6fd405698152 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 17:27:16 +0800 Subject: [PATCH 07/12] fix(linux): label the batch task delete action --- linux/src/main.cpp | 2 +- scripts/e2e/linux-atspi.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index a010986..72e5d9c 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -3697,7 +3697,7 @@ void on_activate(GtkApplication* app, gpointer) { })), nullptr); gtk_box_append(GTK_BOX(bulk_bar), bulk_move); - auto* bulk_delete = gtk_button_new_with_label(tr("listDelete").c_str()); + auto* bulk_delete = gtk_button_new_with_label(tr("taskDelete").c_str()); g_signal_connect( bulk_delete, "clicked", G_CALLBACK(+[](GtkButton*, gpointer) { diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index b6a7378..5f93842 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -68,6 +68,28 @@ def find_named(root, name, expected_role=None): return None +def find_action_named(root, name): + """Find a named action, accepting GTK's label-child accessibility shape.""" + for node in walk(root): + if (node.name or "") != name and text(node) != name: + continue + candidate = node + for _ in range(3): + if candidate is None: + break + try: + actions = candidate.queryAction() + if actions.nActions > 0: + return candidate + except Exception: + pass + try: + candidate = candidate.parent + except Exception: + candidate = None + return None + + def find_editable_named(root, name): for node in walk(root): if (node.name or "") != name: @@ -194,7 +216,7 @@ def main(): # delete action now, while the toolbar is known to be exposed, because an # asynchronous row rebuild can briefly hide the toolbar after scheduling. delete = wait_for( - "batch delete button", lambda: find_named(window, "Delete List", "push button") + "batch delete button", lambda: find_action_named(window, "Delete") ) press(schedule) tomorrow = wait_for( From 7ed1e2d593e6313b72f3ea525543ebeb5c74aba1 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 17:35:10 +0800 Subject: [PATCH 08/12] test(linux): locate adjacent bulk delete action --- scripts/e2e/linux-atspi.py | 44 +++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 5f93842..e63f541 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -68,25 +68,21 @@ def find_named(root, name, expected_role=None): return None -def find_action_named(root, name): - """Find a named action, accepting GTK's label-child accessibility shape.""" - for node in walk(root): - if (node.name or "") != name and text(node) != name: +def preceding_action_sibling(node): + """Return the nearest actionable sibling before a stable toolbar action.""" + try: + parent = node.parent + index = node.getIndexInParent() + except Exception: + return None + for sibling_index in range(index - 1, -1, -1): + try: + sibling = parent.getChildAtIndex(sibling_index) + actions = sibling.queryAction() + if actions.nActions > 0: + return sibling + except Exception: continue - candidate = node - for _ in range(3): - if candidate is None: - break - try: - actions = candidate.queryAction() - if actions.nActions > 0: - return candidate - except Exception: - pass - try: - candidate = candidate.parent - except Exception: - candidate = None return None @@ -212,12 +208,12 @@ def main(): "batch schedule button", lambda: find_named(window, "Schedule Selected", "push button"), ) - # The bulk toolbar is stable for the lifetime of the window. Keep the - # delete action now, while the toolbar is known to be exposed, because an - # asynchronous row rebuild can briefly hide the toolbar after scheduling. - delete = wait_for( - "batch delete button", lambda: find_action_named(window, "Delete") - ) + # GTK exposes the adjacent Delete button without a reliably searchable + # accessible name on the headless runner. Its structural position is + # stable: it is the nearest actionable sibling before Schedule Selected. + delete = preceding_action_sibling(schedule) + if delete is None: + raise AssertionError("batch delete action is missing before schedule action") press(schedule) tomorrow = wait_for( "Tomorrow schedule option", lambda: find_named(window, "Tomorrow", "push button") From 111a475d573140bfae699b9ed25503cef87d1b4a Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 22:12:54 +0800 Subject: [PATCH 09/12] fix: expose stable batch and task automation peers --- linux/src/main.cpp | 3 +++ scripts/e2e/e2e-windows.ps1 | 2 +- scripts/e2e/linux-atspi.py | 33 ++++++++------------------------- windows/MainWindow.xaml.cpp | 4 +++- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 72e5d9c..4032d84 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -3698,6 +3698,9 @@ void on_activate(GtkApplication* app, gpointer) { nullptr); gtk_box_append(GTK_BOX(bulk_bar), bulk_move); auto* bulk_delete = gtk_button_new_with_label(tr("taskDelete").c_str()); + gtk_accessible_update_property( + GTK_ACCESSIBLE(bulk_delete), GTK_ACCESSIBLE_PROPERTY_LABEL, + tr("taskDelete").c_str(), -1); g_signal_connect( bulk_delete, "clicked", G_CALLBACK(+[](GtkButton*, gpointer) { diff --git a/scripts/e2e/e2e-windows.ps1 b/scripts/e2e/e2e-windows.ps1 index 5577333..eadb471 100644 --- a/scripts/e2e/e2e-windows.ps1 +++ b/scripts/e2e/e2e-windows.ps1 @@ -94,7 +94,7 @@ try { [System.Windows.Automation.ValuePattern]::Pattern).SetValue('Acceptance task') $submit.GetCurrentPattern( [System.Windows.Automation.InvokePattern]::Pattern).Invoke() - [void](Wait-Until 'RPC-created task row' { Find-ByName $window 'Acceptance task' }) + [void](Wait-Until 'RPC-created task row' { Find-ById $window 'task-title-1' }) $checkbox = Wait-Until 'task completion control' { Find-ById $window 'task-complete-1' diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index e63f541..78146c0 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -8,7 +8,11 @@ def children(node): - for index in range(node.childCount): + try: + count = node.childCount + except Exception: + return + for index in range(count): try: child = node.getChildAtIndex(index) # GTK can remove and rebuild a row between childCount and the @@ -68,24 +72,6 @@ def find_named(root, name, expected_role=None): return None -def preceding_action_sibling(node): - """Return the nearest actionable sibling before a stable toolbar action.""" - try: - parent = node.parent - index = node.getIndexInParent() - except Exception: - return None - for sibling_index in range(index - 1, -1, -1): - try: - sibling = parent.getChildAtIndex(sibling_index) - actions = sibling.queryAction() - if actions.nActions > 0: - return sibling - except Exception: - continue - return None - - def find_editable_named(root, name): for node in walk(root): if (node.name or "") != name: @@ -208,12 +194,9 @@ def main(): "batch schedule button", lambda: find_named(window, "Schedule Selected", "push button"), ) - # GTK exposes the adjacent Delete button without a reliably searchable - # accessible name on the headless runner. Its structural position is - # stable: it is the nearest actionable sibling before Schedule Selected. - delete = preceding_action_sibling(schedule) - if delete is None: - raise AssertionError("batch delete action is missing before schedule action") + delete = wait_for( + "batch delete button", lambda: find_named(window, "Delete", "push button") + ) press(schedule) tomorrow = wait_for( "Tomorrow schedule option", lambda: find_named(window, "Tomorrow", "push button") diff --git a/windows/MainWindow.xaml.cpp b/windows/MainWindow.xaml.cpp index ddcf584..70e0858 100644 --- a/windows/MainWindow.xaml.cpp +++ b/windows/MainWindow.xaml.cpp @@ -1014,7 +1014,7 @@ void MainWindow::RenderTasks() { : mx::Visibility::Visible); SearchPane().Visibility(searching ? mx::Visibility::Visible : mx::Visibility::Collapsed); - auto const empty = !searching && tasks_.empty(); + auto const empty = !searching && TaskList().Items().Size() == 0; EmptyState().Visibility(empty ? mx::Visibility::Visible : mx::Visibility::Collapsed); if (empty) { @@ -1094,6 +1094,8 @@ mxc::Grid MainWindow::MakeTaskRow(rivet_app::Task const& task, auto text = mxc::TextBlock{}; text.Text(wide(task.text)); + winrt::Microsoft::UI::Xaml::Automation::AutomationProperties::SetAutomationId( + text, L"task-title-" + std::to_wstring(task.id)); text.FontSize(14); text.TextTrimming(mx::TextTrimming::CharacterEllipsis); text.Margin(mx::ThicknessHelper::FromLengths(8, 0, 0, 0)); From 38a92934e25d7e8961bfb5325b1b6c5119cbff83 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 22:13:15 +0800 Subject: [PATCH 10/12] chore: refresh generated backend clients --- linux/GeneratedBackend.hpp | 24 ++++++++++++++++++- .../Sources/RivetHost/GeneratedBackend.swift | 2 +- windows/GeneratedBackend.hpp | 24 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/linux/GeneratedBackend.hpp b/linux/GeneratedBackend.hpp index 8158a6c..8f9cf93 100644 --- a/linux/GeneratedBackend.hpp +++ b/linux/GeneratedBackend.hpp @@ -1,6 +1,7 @@ // Generated by Rivet. Do not edit by hand. #pragma once +#include #include #include #include @@ -17,7 +18,7 @@ namespace rivet_app { inline constexpr char kModuleName[] = "backend"; inline constexpr char kEntryName[] = "start"; -inline constexpr char kDisplayName[] = "taskly"; +inline constexpr char kDisplayName[] = "Taskly"; inline constexpr char kVersion[] = "0.2.3"; inline constexpr std::int64_t kBuild = 1; inline constexpr char kIdentifier[] = "app.taskly.Taskly"; @@ -155,46 +156,67 @@ class API { std::future add_task(std::string text, std::optional list_id, std::optional due_date, std::optional due_time, std::optional notes) { auto rivet_call = backend_.call("add_task", rivet::Value::List{detail::encode_String(text), detail::encode__Optional_Int64_(list_id), detail::encode__Optional_String_(due_date), detail::encode__Optional_String_(due_time), detail::encode__Optional_String_(notes)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t add_task_async(std::string text, std::optional list_id, std::optional due_date, std::optional due_time, std::optional notes, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("add_task", rivet::Value::List{detail::encode_String(text), detail::encode__Optional_Int64_(list_id), detail::encode__Optional_String_(due_date), detail::encode__Optional_String_(due_time), detail::encode__Optional_String_(notes)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional add_task_within(std::string text, std::optional list_id, std::optional due_date, std::optional due_time, std::optional notes, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("add_task", rivet::Value::List{detail::encode_String(text), detail::encode__Optional_Int64_(list_id), detail::encode__Optional_String_(due_date), detail::encode__Optional_String_(due_time), detail::encode__Optional_String_(notes)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future check_updates() { auto rivet_call = backend_.call("check_updates", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateCheck { return detail::decode_UpdateCheck(rivet_call.get()); }); } [[nodiscard]] std::uint64_t check_updates_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("check_updates", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateCheck(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional check_updates_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("check_updates", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateCheck(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future close_database() { auto rivet_call = backend_.call("close_database", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> void { detail::decode_Void(rivet_call.get()); }); } [[nodiscard]] std::uint64_t close_database_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("close_database", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + bool close_database_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("close_database", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); rivet_done->set_value(); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return false; } rivet_future.get(); return true; } std::future create_list(std::string name, std::optional icon, std::optional color) { auto rivet_call = backend_.call("create_list", rivet::Value::List{detail::encode_String(name), detail::encode__Optional_String_(icon), detail::encode__Optional_Int64_(color)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> TodoList { return detail::decode_TodoList(rivet_call.get()); }); } [[nodiscard]] std::uint64_t create_list_async(std::string name, std::optional icon, std::optional color, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("create_list", rivet::Value::List{detail::encode_String(name), detail::encode__Optional_String_(icon), detail::encode__Optional_Int64_(color)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_TodoList(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional create_list_within(std::string name, std::optional icon, std::optional color, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("create_list", rivet::Value::List{detail::encode_String(name), detail::encode__Optional_String_(icon), detail::encode__Optional_Int64_(color)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_TodoList(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future default_database() { auto rivet_call = backend_.call("default_database", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::string { return detail::decode_String(rivet_call.get()); }); } [[nodiscard]] std::uint64_t default_database_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("default_database", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_String(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional default_database_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("default_database", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_String(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future delete_list(std::int64_t id) { auto rivet_call = backend_.call("delete_list", rivet::Value::List{detail::encode_Int64(id)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> bool { return detail::decode_Bool(rivet_call.get()); }); } [[nodiscard]] std::uint64_t delete_list_async(std::int64_t id, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("delete_list", rivet::Value::List{detail::encode_Int64(id)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Bool(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional delete_list_within(std::int64_t id, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("delete_list", rivet::Value::List{detail::encode_Int64(id)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Bool(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future delete_task(std::int64_t id) { auto rivet_call = backend_.call("delete_task", rivet::Value::List{detail::encode_Int64(id)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> bool { return detail::decode_Bool(rivet_call.get()); }); } [[nodiscard]] std::uint64_t delete_task_async(std::int64_t id, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("delete_task", rivet::Value::List{detail::encode_Int64(id)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Bool(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional delete_task_within(std::int64_t id, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("delete_task", rivet::Value::List{detail::encode_Int64(id)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Bool(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future get_setting(std::string key) { auto rivet_call = backend_.call("get_setting", rivet::Value::List{detail::encode_String(key)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::string { return detail::decode_String(rivet_call.get()); }); } [[nodiscard]] std::uint64_t get_setting_async(std::string key, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("get_setting", rivet::Value::List{detail::encode_String(key)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_String(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional get_setting_within(std::string key, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("get_setting", rivet::Value::List{detail::encode_String(key)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_String(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future get_settings() { auto rivet_call = backend_.call("get_settings", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Settings { return detail::decode_Settings(rivet_call.get()); }); } [[nodiscard]] std::uint64_t get_settings_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("get_settings", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Settings(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional get_settings_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("get_settings", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Settings(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future load_snapshot(std::string view, std::optional list_id, bool show_completed) { auto rivet_call = backend_.call("load_snapshot", rivet::Value::List{detail::encode_String(view), detail::encode__Optional_Int64_(list_id), detail::encode_Bool(show_completed)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Snapshot { return detail::decode_Snapshot(rivet_call.get()); }); } [[nodiscard]] std::uint64_t load_snapshot_async(std::string view, std::optional list_id, bool show_completed, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("load_snapshot", rivet::Value::List{detail::encode_String(view), detail::encode__Optional_Int64_(list_id), detail::encode_Bool(show_completed)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Snapshot(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional load_snapshot_within(std::string view, std::optional list_id, bool show_completed, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("load_snapshot", rivet::Value::List{detail::encode_String(view), detail::encode__Optional_Int64_(list_id), detail::encode_Bool(show_completed)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Snapshot(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future open_database(std::string path) { auto rivet_call = backend_.call("open_database", rivet::Value::List{detail::encode_String(path)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Snapshot { return detail::decode_Snapshot(rivet_call.get()); }); } [[nodiscard]] std::uint64_t open_database_async(std::string path, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("open_database", rivet::Value::List{detail::encode_String(path)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Snapshot(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional open_database_within(std::string path, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("open_database", rivet::Value::List{detail::encode_String(path)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Snapshot(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future parse_due(std::string expression) { auto rivet_call = backend_.call("parse_due", rivet::Value::List{detail::encode_String(expression)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::string { return detail::decode_String(rivet_call.get()); }); } [[nodiscard]] std::uint64_t parse_due_async(std::string expression, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("parse_due", rivet::Value::List{detail::encode_String(expression)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_String(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional parse_due_within(std::string expression, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("parse_due", rivet::Value::List{detail::encode_String(expression)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_String(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future parse_quick_add(std::string text) { auto rivet_call = backend_.call("parse_quick_add", rivet::Value::List{detail::encode_String(text)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> QuickAddParse { return detail::decode_QuickAddParse(rivet_call.get()); }); } [[nodiscard]] std::uint64_t parse_quick_add_async(std::string text, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("parse_quick_add", rivet::Value::List{detail::encode_String(text)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_QuickAddParse(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional parse_quick_add_within(std::string text, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("parse_quick_add", rivet::Value::List{detail::encode_String(text)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_QuickAddParse(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> search_tasks(std::string keyword) { auto rivet_call = backend_.call("search_tasks", rivet::Value::List{detail::encode_String(keyword)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_Task_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t search_tasks_async(std::string keyword, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("search_tasks", rivet::Value::List{detail::encode_String(keyword)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_Task_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> search_tasks_within(std::string keyword, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("search_tasks", rivet::Value::List{detail::encode_String(keyword)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_Task_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future set_completed(std::int64_t id, bool completed) { auto rivet_call = backend_.call("set_completed", rivet::Value::List{detail::encode_Int64(id), detail::encode_Bool(completed)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t set_completed_async(std::int64_t id, bool completed, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("set_completed", rivet::Value::List{detail::encode_Int64(id), detail::encode_Bool(completed)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional set_completed_within(std::int64_t id, bool completed, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("set_completed", rivet::Value::List{detail::encode_Int64(id), detail::encode_Bool(completed)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future set_setting(std::string key, std::string value) { auto rivet_call = backend_.call("set_setting", rivet::Value::List{detail::encode_String(key), detail::encode_String(value)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Settings { return detail::decode_Settings(rivet_call.get()); }); } [[nodiscard]] std::uint64_t set_setting_async(std::string key, std::string value, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("set_setting", rivet::Value::List{detail::encode_String(key), detail::encode_String(value)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Settings(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional set_setting_within(std::string key, std::string value, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("set_setting", rivet::Value::List{detail::encode_String(key), detail::encode_String(value)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Settings(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future start_download() { auto rivet_call = backend_.call("start_download", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> void { detail::decode_Void(rivet_call.get()); }); } [[nodiscard]] std::uint64_t start_download_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("start_download", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + bool start_download_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("start_download", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); rivet_done->set_value(); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return false; } rivet_future.get(); return true; } std::future update_list(TodoList item) { auto rivet_call = backend_.call("update_list", rivet::Value::List{detail::encode_TodoList(item)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> TodoList { return detail::decode_TodoList(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_list_async(TodoList item, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_list", rivet::Value::List{detail::encode_TodoList(item)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_TodoList(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_list_within(TodoList item, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_list", rivet::Value::List{detail::encode_TodoList(item)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_TodoList(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_state() { auto rivet_call = backend_.call("update_state", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateState { return detail::decode_UpdateState(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_state_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_state", rivet::Value::List{}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateState(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_state_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_state", rivet::Value::List{}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateState(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_task(Task task) { auto rivet_call = backend_.call("update_task", rivet::Value::List{detail::encode_Task(task)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_task_async(Task task, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_task", rivet::Value::List{detail::encode_Task(task)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_task_within(Task task, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_task", rivet::Value::List{detail::encode_Task(task)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_task_text(std::int64_t id, std::string text) { auto rivet_call = backend_.call("update_task_text", rivet::Value::List{detail::encode_Int64(id), detail::encode_String(text)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_task_text_async(std::int64_t id, std::string text, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_task_text", rivet::Value::List{detail::encode_Int64(id), detail::encode_String(text)}, [completion = std::move(completion)](rivet::linux_runtime::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_task_text_within(std::int64_t id, std::string text, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_task_text", rivet::Value::List{detail::encode_Int64(id), detail::encode_String(text)}, [rivet_done](rivet::linux_runtime::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } private: rivet::linux_runtime::Backend& backend_; diff --git a/macos-host/Sources/RivetHost/GeneratedBackend.swift b/macos-host/Sources/RivetHost/GeneratedBackend.swift index 96fb0c4..f7a8067 100644 --- a/macos-host/Sources/RivetHost/GeneratedBackend.swift +++ b/macos-host/Sources/RivetHost/GeneratedBackend.swift @@ -6,7 +6,7 @@ public enum RivetGeneratedError: Error { case typeMismatch(String); case unknown public enum RivetGeneratedConfig { public static let moduleName = "backend" public static let entryName = "start" - public static let displayName = "taskly" + public static let displayName = "Taskly" public static let version = "0.2.3" public static let build: Int64 = 1 public static let identifier = "app.taskly.Taskly" diff --git a/windows/GeneratedBackend.hpp b/windows/GeneratedBackend.hpp index f24b7ee..1204d66 100644 --- a/windows/GeneratedBackend.hpp +++ b/windows/GeneratedBackend.hpp @@ -1,6 +1,7 @@ // Generated by Rivet. Do not edit by hand. #pragma once +#include #include #include #include @@ -17,7 +18,7 @@ namespace rivet_app { inline constexpr char kModuleName[] = "backend"; inline constexpr char kEntryName[] = "start"; -inline constexpr char kDisplayName[] = "taskly"; +inline constexpr char kDisplayName[] = "Taskly"; inline constexpr char kVersion[] = "0.2.3"; inline constexpr std::int64_t kBuild = 1; inline constexpr char kIdentifier[] = "app.taskly.Taskly"; @@ -155,46 +156,67 @@ class API { std::future add_task(std::string text, std::optional list_id, std::optional due_date, std::optional due_time, std::optional notes) { auto rivet_call = backend_.call("add_task", rivet::Value::List{detail::encode_String(text), detail::encode__Optional_Int64_(list_id), detail::encode__Optional_String_(due_date), detail::encode__Optional_String_(due_time), detail::encode__Optional_String_(notes)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t add_task_async(std::string text, std::optional list_id, std::optional due_date, std::optional due_time, std::optional notes, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("add_task", rivet::Value::List{detail::encode_String(text), detail::encode__Optional_Int64_(list_id), detail::encode__Optional_String_(due_date), detail::encode__Optional_String_(due_time), detail::encode__Optional_String_(notes)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional add_task_within(std::string text, std::optional list_id, std::optional due_date, std::optional due_time, std::optional notes, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("add_task", rivet::Value::List{detail::encode_String(text), detail::encode__Optional_Int64_(list_id), detail::encode__Optional_String_(due_date), detail::encode__Optional_String_(due_time), detail::encode__Optional_String_(notes)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future check_updates() { auto rivet_call = backend_.call("check_updates", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateCheck { return detail::decode_UpdateCheck(rivet_call.get()); }); } [[nodiscard]] std::uint64_t check_updates_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("check_updates", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateCheck(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional check_updates_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("check_updates", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateCheck(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future close_database() { auto rivet_call = backend_.call("close_database", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> void { detail::decode_Void(rivet_call.get()); }); } [[nodiscard]] std::uint64_t close_database_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("close_database", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + bool close_database_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("close_database", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); rivet_done->set_value(); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return false; } rivet_future.get(); return true; } std::future create_list(std::string name, std::optional icon, std::optional color) { auto rivet_call = backend_.call("create_list", rivet::Value::List{detail::encode_String(name), detail::encode__Optional_String_(icon), detail::encode__Optional_Int64_(color)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> TodoList { return detail::decode_TodoList(rivet_call.get()); }); } [[nodiscard]] std::uint64_t create_list_async(std::string name, std::optional icon, std::optional color, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("create_list", rivet::Value::List{detail::encode_String(name), detail::encode__Optional_String_(icon), detail::encode__Optional_Int64_(color)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_TodoList(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional create_list_within(std::string name, std::optional icon, std::optional color, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("create_list", rivet::Value::List{detail::encode_String(name), detail::encode__Optional_String_(icon), detail::encode__Optional_Int64_(color)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_TodoList(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future default_database() { auto rivet_call = backend_.call("default_database", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::string { return detail::decode_String(rivet_call.get()); }); } [[nodiscard]] std::uint64_t default_database_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("default_database", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_String(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional default_database_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("default_database", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_String(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future delete_list(std::int64_t id) { auto rivet_call = backend_.call("delete_list", rivet::Value::List{detail::encode_Int64(id)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> bool { return detail::decode_Bool(rivet_call.get()); }); } [[nodiscard]] std::uint64_t delete_list_async(std::int64_t id, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("delete_list", rivet::Value::List{detail::encode_Int64(id)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Bool(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional delete_list_within(std::int64_t id, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("delete_list", rivet::Value::List{detail::encode_Int64(id)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Bool(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future delete_task(std::int64_t id) { auto rivet_call = backend_.call("delete_task", rivet::Value::List{detail::encode_Int64(id)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> bool { return detail::decode_Bool(rivet_call.get()); }); } [[nodiscard]] std::uint64_t delete_task_async(std::int64_t id, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("delete_task", rivet::Value::List{detail::encode_Int64(id)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Bool(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional delete_task_within(std::int64_t id, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("delete_task", rivet::Value::List{detail::encode_Int64(id)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Bool(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future get_setting(std::string key) { auto rivet_call = backend_.call("get_setting", rivet::Value::List{detail::encode_String(key)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::string { return detail::decode_String(rivet_call.get()); }); } [[nodiscard]] std::uint64_t get_setting_async(std::string key, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("get_setting", rivet::Value::List{detail::encode_String(key)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_String(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional get_setting_within(std::string key, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("get_setting", rivet::Value::List{detail::encode_String(key)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_String(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future get_settings() { auto rivet_call = backend_.call("get_settings", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Settings { return detail::decode_Settings(rivet_call.get()); }); } [[nodiscard]] std::uint64_t get_settings_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("get_settings", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Settings(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional get_settings_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("get_settings", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Settings(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future load_snapshot(std::string view, std::optional list_id, bool show_completed) { auto rivet_call = backend_.call("load_snapshot", rivet::Value::List{detail::encode_String(view), detail::encode__Optional_Int64_(list_id), detail::encode_Bool(show_completed)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Snapshot { return detail::decode_Snapshot(rivet_call.get()); }); } [[nodiscard]] std::uint64_t load_snapshot_async(std::string view, std::optional list_id, bool show_completed, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("load_snapshot", rivet::Value::List{detail::encode_String(view), detail::encode__Optional_Int64_(list_id), detail::encode_Bool(show_completed)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Snapshot(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional load_snapshot_within(std::string view, std::optional list_id, bool show_completed, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("load_snapshot", rivet::Value::List{detail::encode_String(view), detail::encode__Optional_Int64_(list_id), detail::encode_Bool(show_completed)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Snapshot(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future open_database(std::string path) { auto rivet_call = backend_.call("open_database", rivet::Value::List{detail::encode_String(path)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Snapshot { return detail::decode_Snapshot(rivet_call.get()); }); } [[nodiscard]] std::uint64_t open_database_async(std::string path, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("open_database", rivet::Value::List{detail::encode_String(path)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Snapshot(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional open_database_within(std::string path, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("open_database", rivet::Value::List{detail::encode_String(path)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Snapshot(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future parse_due(std::string expression) { auto rivet_call = backend_.call("parse_due", rivet::Value::List{detail::encode_String(expression)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::string { return detail::decode_String(rivet_call.get()); }); } [[nodiscard]] std::uint64_t parse_due_async(std::string expression, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("parse_due", rivet::Value::List{detail::encode_String(expression)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_String(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional parse_due_within(std::string expression, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("parse_due", rivet::Value::List{detail::encode_String(expression)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_String(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future parse_quick_add(std::string text) { auto rivet_call = backend_.call("parse_quick_add", rivet::Value::List{detail::encode_String(text)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> QuickAddParse { return detail::decode_QuickAddParse(rivet_call.get()); }); } [[nodiscard]] std::uint64_t parse_quick_add_async(std::string text, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("parse_quick_add", rivet::Value::List{detail::encode_String(text)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_QuickAddParse(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional parse_quick_add_within(std::string text, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("parse_quick_add", rivet::Value::List{detail::encode_String(text)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_QuickAddParse(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future> search_tasks(std::string keyword) { auto rivet_call = backend_.call("search_tasks", rivet::Value::List{detail::encode_String(keyword)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> std::vector { return detail::decode__List_Task_(rivet_call.get()); }); } [[nodiscard]] std::uint64_t search_tasks_async(std::string keyword, std::function>)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("search_tasks", rivet::Value::List{detail::encode_String(keyword)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode__List_Task_(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional> search_tasks_within(std::string keyword, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("search_tasks", rivet::Value::List{detail::encode_String(keyword)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode__List_Task_(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future set_completed(std::int64_t id, bool completed) { auto rivet_call = backend_.call("set_completed", rivet::Value::List{detail::encode_Int64(id), detail::encode_Bool(completed)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t set_completed_async(std::int64_t id, bool completed, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("set_completed", rivet::Value::List{detail::encode_Int64(id), detail::encode_Bool(completed)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional set_completed_within(std::int64_t id, bool completed, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("set_completed", rivet::Value::List{detail::encode_Int64(id), detail::encode_Bool(completed)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future set_setting(std::string key, std::string value) { auto rivet_call = backend_.call("set_setting", rivet::Value::List{detail::encode_String(key), detail::encode_String(value)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Settings { return detail::decode_Settings(rivet_call.get()); }); } [[nodiscard]] std::uint64_t set_setting_async(std::string key, std::string value, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("set_setting", rivet::Value::List{detail::encode_String(key), detail::encode_String(value)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Settings(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional set_setting_within(std::string key, std::string value, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("set_setting", rivet::Value::List{detail::encode_String(key), detail::encode_String(value)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Settings(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future start_download() { auto rivet_call = backend_.call("start_download", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> void { detail::decode_Void(rivet_call.get()); }); } [[nodiscard]] std::uint64_t start_download_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("start_download", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + bool start_download_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("start_download", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); detail::decode_Void(*rivet_result.value); rivet_done->set_value(); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return false; } rivet_future.get(); return true; } std::future update_list(TodoList item) { auto rivet_call = backend_.call("update_list", rivet::Value::List{detail::encode_TodoList(item)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> TodoList { return detail::decode_TodoList(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_list_async(TodoList item, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_list", rivet::Value::List{detail::encode_TodoList(item)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_TodoList(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_list_within(TodoList item, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_list", rivet::Value::List{detail::encode_TodoList(item)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_TodoList(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_state() { auto rivet_call = backend_.call("update_state", rivet::Value::List{}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> UpdateState { return detail::decode_UpdateState(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_state_async(std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_state", rivet::Value::List{}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_UpdateState(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_state_within(std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_state", rivet::Value::List{}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_UpdateState(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_task(Task task) { auto rivet_call = backend_.call("update_task", rivet::Value::List{detail::encode_Task(task)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_task_async(Task task, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_task", rivet::Value::List{detail::encode_Task(task)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_task_within(Task task, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_task", rivet::Value::List{detail::encode_Task(task)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } std::future update_task_text(std::int64_t id, std::string text) { auto rivet_call = backend_.call("update_task_text", rivet::Value::List{detail::encode_Int64(id), detail::encode_String(text)}); return std::async(std::launch::deferred, [rivet_call = std::move(rivet_call)]() mutable -> Task { return detail::decode_Task(rivet_call.get()); }); } [[nodiscard]] std::uint64_t update_task_text_async(std::int64_t id, std::string text, std::function)> completion) { if (!completion) throw std::invalid_argument("Rivet async completion handler is empty"); return backend_.request_async("update_task_text", rivet::Value::List{detail::encode_Int64(id), detail::encode_String(text)}, [completion = std::move(completion)](rivet::windows::CallResult rivet_result) mutable { Result result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); result.value = detail::decode_Task(*rivet_result.value); } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }); } + std::optional update_task_text_within(std::int64_t id, std::string text, std::chrono::milliseconds rivet_timeout) { auto rivet_done = std::make_shared>(); auto rivet_future = rivet_done->get_future(); auto rivet_id = backend_.request_async("update_task_text", rivet::Value::List{detail::encode_Int64(id), detail::encode_String(text)}, [rivet_done](rivet::windows::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error("Rivet async call completed without a value"); rivet_done->set_value(detail::decode_Task(*rivet_result.value)); } catch (...) { rivet_done->set_exception(std::current_exception()); } } }); if (rivet_future.wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return std::nullopt; } return rivet_future.get(); } private: rivet::windows::Backend& backend_; From 81f989719bcbc259d496cf3469266b2b4940446b Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 22:22:14 +0800 Subject: [PATCH 11/12] fix(linux): retain batch delete accessibility label --- linux/src/main.cpp | 6 ++++++ scripts/e2e/linux-atspi.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 4032d84..c864335 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -398,6 +398,7 @@ struct AppState { GtkLabel* bulk_count{nullptr}; GtkButton* bulk_complete{nullptr}; GtkButton* bulk_move{nullptr}; + GtkButton* bulk_delete{nullptr}; GtkButton* bulk_schedule{nullptr}; GtkStack* list_stack{nullptr}; GtkLabel* empty_icon{nullptr}; @@ -3024,6 +3025,10 @@ void apply_language() { if (g_state.bulk_complete != nullptr) { gtk_button_set_label(g_state.bulk_complete, tr("navCompleted").c_str()); gtk_button_set_label(g_state.bulk_move, tr("menuMoveToList").c_str()); + gtk_button_set_label(g_state.bulk_delete, tr("taskDelete").c_str()); + gtk_accessible_update_property( + GTK_ACCESSIBLE(g_state.bulk_delete), GTK_ACCESSIBLE_PROPERTY_LABEL, + tr("taskDelete").c_str(), -1); gtk_button_set_label(g_state.bulk_schedule, tr("bulkSchedule").c_str()); } gtk_button_set_label( @@ -3849,6 +3854,7 @@ void on_activate(GtkApplication* app, gpointer) { g_state.bulk_count = GTK_LABEL(bulk_count); g_state.bulk_complete = GTK_BUTTON(bulk_complete); g_state.bulk_move = GTK_BUTTON(bulk_move); + g_state.bulk_delete = GTK_BUTTON(bulk_delete); g_state.bulk_schedule = GTK_BUTTON(bulk_schedule); g_state.list_stack = GTK_STACK(list_stack); g_state.empty_icon = GTK_LABEL(empty_icon); diff --git a/scripts/e2e/linux-atspi.py b/scripts/e2e/linux-atspi.py index 78146c0..fd0b4f6 100644 --- a/scripts/e2e/linux-atspi.py +++ b/scripts/e2e/linux-atspi.py @@ -194,6 +194,9 @@ def main(): "batch schedule button", lambda: find_named(window, "Schedule Selected", "push button"), ) + (args.output / "bulk-toolbar-debug.json").write_text( + json.dumps(snapshot(window), ensure_ascii=False, indent=2), encoding="utf-8" + ) delete = wait_for( "batch delete button", lambda: find_named(window, "Delete", "push button") ) From a696b23c2a2a857dec4b1dbf57fecb01437af67a Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Wed, 21 Oct 2026 22:32:37 +0800 Subject: [PATCH 12/12] test(windows): wait for backend readiness --- scripts/e2e/e2e-windows.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/e2e/e2e-windows.ps1 b/scripts/e2e/e2e-windows.ps1 index eadb471..f59160d 100644 --- a/scripts/e2e/e2e-windows.ps1 +++ b/scripts/e2e/e2e-windows.ps1 @@ -90,12 +90,18 @@ try { } $quickAdd = Wait-Until 'quick-add field' { Find-ById $window 'quick-add' } $submit = Wait-Until 'quick-add submit button' { Find-ById $window 'quick-add-submit' } + # The native window becomes automatable before the embedded backend has + # opened its database. Clicking during that interval intentionally keeps + # the draft unsent, so wait for the product's real readiness signal. + [void](Wait-Until 'database connection' { Find-ByName $window 'Database Connected' }) $quickAdd.GetCurrentPattern( [System.Windows.Automation.ValuePattern]::Pattern).SetValue('Acceptance task') $submit.GetCurrentPattern( [System.Windows.Automation.InvokePattern]::Pattern).Invoke() - [void](Wait-Until 'RPC-created task row' { Find-ById $window 'task-title-1' }) - + [void](Wait-Until 'quick-add submission' { + $quickAdd.GetCurrentPattern( + [System.Windows.Automation.ValuePattern]::Pattern).Current.Value -eq '' + }) $checkbox = Wait-Until 'task completion control' { Find-ById $window 'task-complete-1' }