Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion linux/GeneratedBackend.hpp

Large diffs are not rendered by default.

46 changes: 28 additions & 18 deletions linux/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -959,7 +960,7 @@ std::vector<std::int64_t> 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<std::int64_t>(GPOINTER_TO_INT(pointer)));
ids.push_back(static_cast<std::int64_t>(GPOINTER_TO_SIZE(pointer)));
}
}
g_list_free(selected);
Expand Down Expand Up @@ -1024,9 +1025,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));
}
}
Expand Down Expand Up @@ -1934,7 +1936,7 @@ void open_list_editor(
void on_task_check_toggled(GtkCheckButton* button, gpointer) {
if (g_state.api == nullptr) return;
auto const id = static_cast<std::int64_t>(
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));
}

Expand Down Expand Up @@ -2108,7 +2110,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<std::int64_t>(GPOINTER_TO_INT(
auto const row_id = static_cast<std::int64_t>(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,
Expand All @@ -2125,7 +2127,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<std::int64_t>(GPOINTER_TO_INT(
auto const row_id = static_cast<std::int64_t>(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,
Expand All @@ -2139,7 +2141,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<std::int64_t>(GPOINTER_TO_INT(
auto const list_id = static_cast<std::int64_t>(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(),
Expand All @@ -2149,7 +2151,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<std::int64_t>(GPOINTER_TO_INT(
auto const task_id = static_cast<std::int64_t>(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(),
Expand Down Expand Up @@ -2201,7 +2203,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<std::int64_t>(GPOINTER_TO_INT(
auto const id = static_cast<std::int64_t>(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(
Expand Down Expand Up @@ -2248,7 +2250,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<gint>(task.id)));
GSIZE_TO_POINTER(static_cast<gsize>(task.id)));
g_signal_connect(check, "toggled", G_CALLBACK(on_task_check_toggled),
nullptr);
gtk_box_append(GTK_BOX(box), check);
Expand Down Expand Up @@ -2302,7 +2304,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<gint>(task.id)));
GSIZE_TO_POINTER(static_cast<gsize>(task.id)));
attach_row_gestures(row, false);
return row;
}
Expand Down Expand Up @@ -2466,7 +2468,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<gint>(list.id)));
GSIZE_TO_POINTER(static_cast<gsize>(list.id)));
attach_row_gestures(row, true);
return row;
}
Expand Down Expand Up @@ -2522,7 +2524,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<std::int64_t>(GPOINTER_TO_INT(
auto const row_id = static_cast<std::int64_t>(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<TaskEditor*>(
Expand Down Expand Up @@ -2558,7 +2560,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<std::int64_t>(GPOINTER_TO_INT(value)));
static_cast<std::int64_t>(GPOINTER_TO_SIZE(value)));
}
}
g_list_free(selected_rows);
Expand Down Expand Up @@ -3023,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(
Expand Down Expand Up @@ -3055,7 +3061,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<std::int64_t>(GPOINTER_TO_INT(
auto const id = static_cast<std::int64_t>(GPOINTER_TO_SIZE(
g_object_get_data(G_OBJECT(row), "list-id")));
g_state.view = ViewKind::List;
g_state.view_list_id = id;
Expand Down Expand Up @@ -3696,7 +3702,10 @@ 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());
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) {
Expand Down Expand Up @@ -3769,7 +3778,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<std::int64_t>(GPOINTER_TO_INT(data));
auto const id = static_cast<std::int64_t>(GPOINTER_TO_SIZE(data));
toggle_task_editor(row, id, false);
}), nullptr);
gtk_widget_add_css_class(task_list, "task-row-list");
Expand Down Expand Up @@ -3845,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);
Expand Down
2 changes: 1 addition & 1 deletion macos-host/Sources/RivetHost/GeneratedBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
73 changes: 73 additions & 0 deletions scripts/e2e/e2e-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
10 changes: 8 additions & 2 deletions scripts/e2e/e2e-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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-ByName $window 'Acceptance task' })

[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'
}
Expand Down
Loading
Loading