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
2 changes: 1 addition & 1 deletion .claude/skills/fix-issue/references/platform-linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Pass `--platform linux`, which a Linux machine picks by default. Every step runs

## Screen

Run the app from the tree with `meson devenv -C _build ./crates/app/tablepro` inside `linux/`. A development build uses its own application ID and data directories, so it never touches an installed one; point the `XDG_*_HOME` variables at a scratch directory as well when the shot must not show the user's real connections. Nothing drives the app from a script yet: only `app.*` actions reach D-Bus, and the window's actions do not. Launch it in the state the shot needs and capture with the `org.freedesktop.portal.Screenshot` portal, which GNOME allows for an unsandboxed caller; `org.gnome.Shell.Screenshot` refuses. The portal captures the whole screen, so crop to the window. Dark and light: `ADW_DEBUG_COLOR_SCHEME=force-dark` or `force-light`.
Run the app from the tree with `meson devenv -C _build ./crates/app/tablepro` inside `linux/`. A development build uses its own application ID and data directories, so it never touches an installed one; point the `XDG_*_HOME` variables at a scratch directory as well when the shot must not show the user's real connections. Nothing drives the app from a script yet: only `app.*` actions reach D-Bus, and the window's actions do not. Launch it in the state the shot needs and capture with the `org.freedesktop.portal.Screenshot` portal, which GNOME allows for an unsandboxed caller; `org.gnome.Shell.Screenshot` refuses. The portal captures the whole screen, so crop to the window. Dark and light: `ADW_DEBUG_COLOR_SCHEME=prefer-dark` or `prefer-light`; libadwaita ignores any other value.

## Ship

Expand Down
15 changes: 12 additions & 3 deletions linux/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,21 @@ See [docs/state-management.md](docs/state-management.md) for the patterns and na
Every key the app binds or relies on is one `Accelerator` in `ACCELERATORS` (`crates/app/src/application/accelerators.rs`): the action it fires, its triggers, its `AcceleratorScope` and where the shortcuts dialog lists it.

- **Application** keys go in with `set_accels_for_action` at startup. GtkWindow runs them in the capture phase, before the focused widget, so none of them is a key a text field or a list uses.
- **Local** keys are a `local_shortcut_controller(owner)` on the owner's widget, firing the owner's action group through `NamedAction`: `browse` on a table tab and its column view, `editor` on the SQL editor's source view and, for find and replace, on the whole editor tab, `structure`, `filter`, the sidebar's list, which opens the selected table's menu, the query history's list, which opens the focused row's menu, and a picker row, which opens its menu. A disabled action lets its key go on. Bubble phase leaves a text field inside the owner its own Ctrl+Z and Ctrl+C; the source view's keys run in the capture phase, because GtkTextView binds Ctrl+/ itself.
- **Component** keys are bound by a component's own controller (the grid cell keys, the cell editors), which reads them from the table through `component_trigger` or `component_matches`.
- **Local** keys are a `local_shortcut_controller(owner)` on the owner's widget, firing the owner's action group through `NamedAction`: `browse` on a table tab, `grid` on a data grid's column view (two owners, one per phase), `editor` on the SQL editor's source view and, for find and replace, on the whole editor tab, `structure`, `filter`, the sidebar's list, which opens the selected table's menu, the query history's list, which opens the focused row's menu, and a picker row, which opens its menu. A disabled action lets its key go on. Bubble phase leaves a text field inside the owner its own Ctrl+Z and Ctrl+C; the source view's keys run in the capture phase, because GtkTextView binds Ctrl+/ itself.
- **Component** keys are bound by a component's own controller (the grid's inline and detail pane editors), which reads them from the table through `component_trigger` or `component_matches`.
- **Stock** keys belong to GTK or libadwaita: GtkListBase's selection and movement, AdwTabView's tab switching, AdwApplication's Ctrl+?. The workspace removes AdwTabView's Ctrl+Home and Ctrl+End pairs so the lists and the editor in its tabs keep them.

`data/resources/shortcuts-dialog.ui` lists every listed entry under an id made from its action, and `shortcuts_dialog_matches_accelerator_table` holds the two in step both ways. Menus show a Local or Component key through `menu_item`, and `describe_icon_button` puts a button's key on its accessible node instead of in its tooltip.

### The data grid

Every surface that shows rows, the table tab, the SQL editor's results and the key tab, shows them in one `DataGrid` (`crates/app/src/ui/data_grid/`, [ADR 0027](docs/decisions/0027-one-data-grid.md)): a `GtkColumnView` with the `.data-table` style, row selection plus a focused cell, a row number column first, and a row detail pane on the right (`AdwOverlaySplitView`, Alt+Return). The rules it applies, with no GTK, are in `crates/app/src/services/grid/`: one `GridColumn` per column, the display text, the locks and editor kinds, the edit policy, the Copy rule, the column layout, find and the filter rule from a cell.

- **Rows are stable slots.** The grid's store holds `GridRow` objects, and a reload writes the new rows into the slots it has, so GTK keeps its row and cell widgets and only the cells draw again. A slot's cells change only as a new `Arc`, which copies and exports share off the GTK thread. A slot moves its generation when it is given another row, and anything that acts later (a menu item, an editor, a paste target, a confirmation) holds the row as a `PinnedRow`, which stops resolving once the slot holds another row. Before any slot is written the open inline or detail pane editor commits against its pinned row. A grid that sorts the rows it holds takes new rows as new slots, since its sort model hears no write into a slot.
- **One action group.** Every operation is a `grid.*` action with one enabled state, read from the `EditPolicy`, the target (the cell a menu opened on, else the focused cell, and its rows) and whether an editor is open. Menus, keys and a host's own buttons all name it; a host inserts the grid's group on its root so its toolbar reaches it.
- **Keys.** Two controllers on the column view, from the accelerator table: the `GridCell` owner in the capture phase for the keys GtkListFactoryWidget binds itself (Return, Space, Menu), and the `Grid` owner in the bubble phase, after a focused editor had its turn. A disabled action lets its key go on. Return and a double click reach the grid as the view's `activate`: an editable cell opens its editor, any other cell its value in the detail pane. Typed text opens the editor and the key is forwarded to it, so its input method sees the first key.
- **What a host hears.** The grid asks its host only for what it cannot do itself, through its `request` signal: a server sort, an export, a foreign key to follow, a filter rule from a cell, a toast, and "pending changes changed". It also says when its selection and its editing state change, so the table tab's Save stays on while a value is being typed.

## Driver layer

How the app reaches a database: the contract in `tablepro-core`, a transport that carries the bytes, TLS each driver builds from the connection's trust settings, and a session that watches the link.
Expand Down Expand Up @@ -317,7 +326,7 @@ Each workspace owns two registries holding the in-flight edit state for its tabs
| `ChangeTrackerRegistry` | `TabTrackerHandle` | Row-level INSERT / UPDATE / DELETE for Table tabs | `TabTrackerHandle::change_set` → `TrackedChanges` |
| `StructureTrackerRegistry` | `StructureTrackerHandle` | The table's definition, the user's `TableDraft` and the `TableChanges` between them, for Structure tabs | `PendingStructure::plan` through the connection's `DdlDialect` → `DdlPlan` |

A handle is a newtype over `Rc<RefCell<_>>`. Every method borrows only for its own body, calls no GTK and no callbacks, and returns owned data (`CellOverlay`, `RowState`, `TrackerDelta`), so a grid bind that runs while a tab is applying a change never meets a held borrow. A row edit goes in as a `TrackerEdit` and comes back as a `TrackerDelta`: the rows to redraw, the draft rows the list store gains or loses, and the new pending count. The tab applies it to its `gio::ListStore` after the call returns. A grid row is a `RowObject` whose identity is fixed when it is made: a saved row's key and cells as fetched, or a draft's id, whose cells live only in the tracker. A tracked change bumps the row's `revision` property, and each cell bound to the row listens for it and draws again in place, so the store never swaps a row and its selection and focus stay put. The tab finds the row through a `GridRowIndex` of weak references keyed like the tracker. A grid cell resolves its row through its column view cell when an event fires, so an edit, a menu action or a toggle reaches the row it was made on even after rows above it came or went. Every pending change carries a `Touch` from the tracker's counter. An `UndoOp` holds the pending state on both sides of a step, touch included, so undo after A, B, C shows B, not the fetched value. `change_set` emits one change per row in touch order, and `plan_save` keeps that order: a row deleted and replaced by a draft with the same unique value saves, because its DELETE runs first. `WorkspaceHandle::dirty_tabs` reads both registries, with each editor's open transaction, for the close prompts, the teardown guard and the tabs' unsaved icon. CI fails on a `thread_local!` under `crates/app/src/services`, since a process-wide registry would share one window's unsaved edits with every other window.
A handle is a newtype over `Rc<RefCell<_>>`. Every method borrows only for its own body, calls no GTK and no callbacks, and returns owned data (`CellOverlay`, `RowState`, `TrackerDelta`), so a grid bind that runs while a tab is applying a change never meets a held borrow. A row edit goes in as a `TrackerEdit` and comes back as a `TrackerDelta`: the rows to redraw, the draft rows the grid gains or loses, and the new pending count. The grid applies every delta in one place, `DataGrid::apply_delta`, whether its own editor made the change or the tab did (a new save target, a save settling); a change whose delta is gone, as a close prompt's Discard, is followed by `DataGrid::resync`. Every pending change carries a `Touch` from the tracker's counter. An `UndoOp` holds the pending state on both sides of a step, touch included, so undo after A, B, C shows B, not the fetched value. `change_set` emits one change per row in touch order, and `plan_save` keeps that order: a row deleted and replaced by a draft with the same unique value saves, because its DELETE runs first. `WorkspaceHandle::dirty_tabs` reads both registries, with each editor's open transaction, for the close prompts, the teardown guard and the tabs' unsaved icon. CI fails on a `thread_local!` under `crates/app/src/services`, since a process-wide registry would share one window's unsaved edits with every other window.

The Structure tab is **definition + draft**, not a per-change log. The `TableDefinition` one read returned is what the tab plans against; `diff_table` turns the draft into `TableChanges`, and the engine's `DdlDialect` plans them into statements in its own order. Discard copies the definition back over the draft, and is the only restore point. The tracker holds the latest `PendingStructure`, so the close prompt and the save dispatcher read the same change without the tab, and the save plans it again with the dialect of the connection that runs it ([ADR 0010](docs/decisions/0010-ddl-plans-and-table-definitions.md)). A save that loses something, or that has several statements on an engine that applies DDL one statement at a time, first shows the user its `ChangeReview` and runs only the statements reviewed, whether the tab's Save or a close prompt asked for it ([state-management.md](docs/state-management.md#rules-for-every-view)).

Expand Down
4 changes: 2 additions & 2 deletions linux/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion linux/build-aux/debian/copyright

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion linux/build-aux/rpm/tablepro.spec
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ appstreamcli validate --no-net %{buildroot}%{_metainfodir}/app.tablepro.TablePro
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_BUILD_JOBS=%{_smp_build_ncpus}
# %%meson_test expands to lines of its own; shrink joins them to this one.
%{shrink:xvfb-run -a dbus-run-session -- %meson_test}
# Fedora's xvfb-run screen is 640x480, which the app-scale-2 test halves
# below the smallest screen the app supports; this is Debian's default.
%{shrink:xvfb-run -a -s "-screen 0 1280x1024x24" dbus-run-session -- %meson_test}
%endif

%files -f %{name}.lang
Expand Down
1 change: 1 addition & 0 deletions linux/changelog.d/3836.added.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Find text in SQL results and key tabs with Ctrl+F.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.added.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Filter a table by a cell's value, or exclude it, from the cell's menu.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hide, show and reorder columns from the Columns dialog or the header; each table keeps its layout.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Table tabs, SQL results and key tabs show rows in one grid with row numbers and a Row Details pane.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Insert Row and Duplicate in the menu of a table without a primary key added rows that saved without a key.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ctrl+S skipped the value still being typed in a cell.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A cell of a row marked for deletion could still be edited, and the edit was never saved.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sorting or editing a MongoDB field whose name has a dot or starts with $ failed at the server.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The selection count offered Delete on rows that Delete could not remove.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Booleans showed as check boxes, text or numbers depending on the grid, and made rows taller.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The AI assistant opened by itself when the window got wider.
1 change: 1 addition & 0 deletions linux/changelog.d/3836.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ctrl+C copied nothing in SQL results and key tabs.
2 changes: 1 addition & 1 deletion linux/crates/app/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ test(
args: gtk_args + [
'--lib',
'--',
'ui::grid::',
'ui::data_grid::',
'ui::diagram::',
'ui::editor::editor_view::',
'ui::window_geometry::',
Expand Down
Loading
Loading