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
162 changes: 137 additions & 25 deletions ACTIVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,116 @@ and the WFLOW Jira project for issue-level status.

## last session ended

new top-nav landed. matthew and claude mocked up four directions in
/tmp/wflow-nav-mockups.html (top app bar, left-docked nav, bottom
HUD pill, spotlight palette) and matthew picked the flat top app
bar. ChromeFloating.qml now renders a 48px strip at top:0 with the
brand mark + nav docked left and @cush + cog docked right;
StackLayout anchors below it instead of behind it. WorkflowPage and
SettingsPage dropped the `topMargin: 70` / `topMargin: 80`
workarounds that existed only to dodge the old floating pill. The
editor's doc-tab strip and toolbar now sit cleanly under the app
bar with no orphan-tab gap. cargo build clean, smoke-tested in the
GUI, matthew confirmed it reads right.

WFLOW-64 (KDL syntax highlighting in the view-source pane) is also
sitting on disk uncommitted from the prior session: tokenizer in
`src/kdl_format/highlight.rs`, qinvokable `tokenize_kdl` on the
workflow controller, HTML-with-spans rendering in ViewSourcePane,
a `Theme.kdlColor(kind)` mapper so all four palette skins read
right. cargo build + 137 tests green. GUI eyeball still pending.
KDL-first-class thread moved forward in two big steps before
matthew relogged. (1) The flat top app bar shipped + pushed at
17daaca, replacing the floating navpill that was forcing the
editor's toolbar to dodge it; doc-tab strip and toolbar now sit
flush under the app bar with no orphan-tab gap. (2) WFLOW-64 (kdl
syntax highlighting in the view-source pane) turned out to be
already committed at 29d0a70 from a prior session, pushed it in
the same run. (3) Editable view-source pane (WFLOW-66) shipped
end-to-end across d0003a8 → 1a21651 in a single long iteration:
parse-and-apply scaffolding, tab insertion fix, four attempts at
live highlighting before landing on a hand-written C++
QSyntaxHighlighter. Smoke-tested green at 1a21651. Unpushed.

## WFLOW-66 shipped: editable view-source pane

State: end-to-end working. Smoke-tested at 1a21651.

What's built:
- `WorkflowController::apply_kdl_source(current_workflow_json, kdl)`
in `src/bridge/workflow.rs`. Parses KDL, swaps the in-memory
workflow on success, returns "" on success or the parse error
message on failure. Preserves the workflow id from
current_workflow_json. Includes a `preserve_step_ids` helper that
walks old + new step lists in order, copies old.id onto new.id
when the action variant matches (via `std::mem::discriminant` so
new action kinds need no edit here), recurses into repeat /
conditional inner steps. Without that, every step looks new
on re-parse and the canvas re-layouts the whole graph.
- `qml/components/workflow/ViewSourcePane.qml`: `editable`,
`workflowController`, `parseError` props, `applyRequested` signal,
upstream-rebind gated on `_editing` (flips true on first printable
key, false on focus-loss), 600ms `applyTimer` for parse-and-apply,
150ms `rehighlightTimer` for live re-tokenize, "● unparsed" chip in
the header on parse failure, Tab-to-4-spaces intercept,
`tabStopDistance` set to 4 space widths.
- `qml/pages/WorkflowPage.qml`: passes `editable: !fragmentMode`,
`workflowController: wfCtrl`, `parseError: root.sourceParseError`;
`onApplyRequested` calls apply_kdl_source, schedules a save on
success, clears parseError when sourceKdl moves, and
`Qt.callLater(canvasView._zoomToFit)` after a successful apply so
the camera frames the new layout.

What works:
- Edit a string value, ~600ms later the canvas updates and saveState
flips dirty, saving, saved.
- Type broken KDL, coral "● unparsed" chip appears in the header,
canvas stays at last-good state, parse error in the tooltip.
- Click a canvas step while broken-KDL is in the pane, last-edit-wins,
pane snaps back to canonical, broken draft discarded.
- Add or remove a step line, existing cards keep their positions
(preserve_step_ids), new card lands at the canvas default spot,
camera zooms to fit the union. Matthew confirmed this reads right.

How the highlighting works now:
- The body TextEdit is plain text. No HTML pipeline.
- `cpp/kdl_syntax_highlighter.h` is a hand-written
`QSyntaxHighlighter` subclass, registered with QML at
`import Wflow 1.0; KdlSyntaxHighlighter {}` via a tiny bridge
in `src/bridge/kdl_highlight.rs` whose
`register_kdl_qml_types()` function `main.rs` calls before
`QQmlApplicationEngine::load`.
- ViewSourcePane attaches the highlighter to `body.textDocument`,
feeds it a `colors` map (Theme.kdlColor for every kind), and
drives `spansJson` from `wfCtrl.tokenize_kdl(plain)` on every
textChanged during edit, falling back to the parent's
`kdlSpansJson` outside an edit burst.
- `setFormat()` applies QTextCharFormat ranges to the existing
QTextDocument without rebuilding it, so the cursor stays put
and the live re-tokenize doesn't fight Qt the way the old
RichText/HTML rebuild did.

Two non-obvious things load-bearing on this working:
- **`defaultColor` on the highlighter**, primed via
`setFormat(0, len, baseFmt)` at the top of `highlightBlock`.
Once any QTextCharFormat is on the document, the QML TextEdit's
`color` property stops acting as a render-time fallback for
unformatted ranges, so chars not covered by a span end up with
an unset foreground and effectively invisible. The default-color
sweep keeps them on `Theme.text`; per-token formats layer on top.
- **`restoreMode: Binding.RestoreNone`** on the `Binding on text`.
Qt's default (`RestoreBindingOrValue`) actively restores the
property to its pre-binding value when `when` flips false. For
`TextEdit.text` that pre-value is `""`, so the document wiped
the moment the user typed and the typed char landed alone in an
empty buffer (`apply_kdl_source` then surfaced "unknown
top-level node `ff`" in the chip). `RestoreNone` keeps the last
binding-driven value when the binding releases.

The four attempts that didn't survive, for context if any of this
needs to be rethought:
- 2f9d6ab tried flipping `textFormat` to PlainText during the
edit burst. Backfired because setting body.text=plain in
RichText round-trips through Qt's default HTML serialization
(DOCTYPE/head/style/p), and the format switch then renders
that markup literally.
- ef9ad9b reverted to RichText and swapped the colored HTML for
a no-spans `<pre>` on edit-start. Worked but decolored the
entire pane the moment you typed.
- 33ce134 removed the swap entirely, leaving existing colored
spans in place during edit. Visually OK but didn't actually
re-highlight new text.
- 7a2eb6f introduced the C++ `QSyntaxHighlighter`. Highlighting
worked; the document-wipe and invisible-default-color bugs
surfaced and got fixed in 1a21651.

Plus a small bookkeeping item: `scripts/jira/issues.csv` has WFLOW-66
appended; the row is already on the Jira side (created via
`seed.py import-csv` then `start WFLOW-66`). The CSV mod just needs
to land with the next commit that touches the catalog so the two
sources of truth match.

## where the work stands

Expand Down Expand Up @@ -100,9 +192,25 @@ toolbar buttons clear the floating navpill.

## recently landed (since 9dffb8e)

- (this commit) top app bar replaces the floating navpill; drop the
- (unpushed) 1a21651 follow-up fixes on top of the C++
highlighter: defaultColor priming + Binding.RestoreNone. See
the WFLOW-66 shipped section above.
- (unpushed) 7a2eb6f live KDL highlighting via a C++
`QSyntaxHighlighter` attached to body.textDocument. Replaces
the holding patterns from 2f9d6ab/ef9ad9b/33ce134.
- (unpushed) 33ce134 interim "leave colors alone" fallback while
the proper highlighter was being built. Replaced by 7a2eb6f.
- (unpushed) ef9ad9b followup that walked back the textFormat
flip from 2f9d6ab. Replaced by 7a2eb6f.
- (unpushed) 2f9d6ab tab insertion + first stab at the live
highlight fix. Tab still ships; live-highlight piece is
superseded by 7a2eb6f.
- (unpushed) d0003a8 editable view-source pane scaffolding,
WIP (WFLOW-66). Working: parse + apply, position preservation,
zoom-to-fit.
- 17daaca top app bar replaces the floating navpill; drop the
topMargin workarounds in WorkflowPage / SettingsPage (WFLOW-65)
- (unc'd) kdl syntax highlighting in the view-source pane (WFLOW-64)
- 29d0a70 kdl syntax highlighting in the view-source pane (WFLOW-64)
- 0a8536b view-source pane: render on first open, fix Copy overlap,
dodge the navpill (WFLOW-54)
- 44f5910 view-source pane on the canvas mirrors the live workflow
Expand Down Expand Up @@ -135,15 +243,19 @@ the Canvas task (work item 14) for the WFLOW-64 / WFLOW-65 tickets
created above. Tracked + ready to commit alongside the next change
that touches the catalog.

`target/debug/wflow` is freshly built off this commit and includes
the new top app bar + the view-source pane. Any running instance
needs a relaunch to pick it up (the running proc maps a deleted
inode).
`target/debug/wflow` is freshly built off the WFLOW-66 WIP commit
and includes the new top app bar + the view-source pane + the
editable pane scaffolding. Any running instance needs a relaunch
to pick it up (the running proc maps a deleted inode).

`.impeccable.md` is modified locally (synced to the current CLAUDE.md
Design Context: two palettes, six-step radii ladder, Theme.accentWash
selection). uncommitted; ship when convenient, separate concern from
the chrome change.
the editable-pane work.

`scripts/jira/issues.csv` has WFLOW-66 appended (Editable view-source
pane, parent 14). Already on the Jira side via `seed.py import-csv`;
the CSV row is part of the WIP commit.

## environment

Expand Down
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,62 @@ breaks.

---

## [1.3.0] - 2026-05-22

The view-source pane lands in the workflow editor, showing the current
workflow as KDL alongside the canvas and accepting edits that round-trip
back to the canvas. Plus the flat top app bar that walked back the
floating navpill, KDL copy/paste through the system clipboard, and
drag-a-`.kdl`-on-the-canvas import.

### Added

- **View-source pane in the workflow editor.** `</> Source` in the
editor toolbar slides a panel in from the right of the canvas
showing the current workflow as KDL, syntax-highlighted, re-encoded
live as you change the canvas. The pane is editable: typing parses
on a 600ms debounce and round-trips through `apply_kdl_source`,
which swaps the in-memory workflow on success. Existing card
positions survive the round-trip via a `preserve_step_ids` helper
that walks old and new step lists by `std::mem::discriminant` of
the action variant, so adding or removing a line doesn't relayout
the unchanged cards. Tab inserts 4 spaces. Broken KDL surfaces a
coral "● unparsed" chip in the header (parse error in a tooltip)
and the canvas holds at last-good state until you click out or fix
the source. Last-edit-wins on focus-loss; a broken draft gets
discarded. Highlighting uses a hand-written C++ `QSyntaxHighlighter`
(`cpp/kdl_syntax_highlighter.h`) attached to the body TextEdit's
`QTextDocument`, re-tokenizing the local buffer per keystroke
through `wfCtrl.tokenize_kdl` and applying `QTextCharFormat` ranges
via `setFormat`. The document itself isn't rebuilt on each
keystroke, so the cursor stays where the user left it.
- **Flat top app bar in place of the floating navpill.** The pill that
floated over the canvas was forcing the editor toolbar to dodge it.
The new app bar sits flush at the top of the window; the doc-tab
strip and editor toolbar align underneath with no orphan-tab gap.
- **Copy and paste workflow steps as KDL through the system clipboard.**
Right-click a card and Copy as KDL writes the kdl fragment to the
clipboard via arboard (wlr-data-control with X11 fallback); paste on
the canvas inserts at the current crumb. Multi-selection copies all
selected top cards as one fragment; paste dedupes inner cards of any
selected top.
- **Drop a `.kdl` file on the canvas to import it as steps.**
Single-file drop lands the steps at the current crumb; multi-file
drop loops over the dropped URLs. Pairs with the deeplink import
path so a `.kdl` file reaches the editor by any route a file
manager gives you.

### Changed

- **Explore drawer prefers the catalog trail over `kindSamples` while
detail loads.** The drawer used to render the `kindSamples` preview
during the loading window, then swap to the `kdlSource`-parsed shape
once detail arrived; the swap was visible. Trail values match the
final shape closely enough that reading from them first keeps the
drawer steady from open through fully-loaded.

---

## [1.2.0] - 2026-05-09

Editor hot-reload for full workflow content, an `else` branch with first-class
Expand Down
2 changes: 1 addition & 1 deletion 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 Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "wflow"
version = "1.2.0"
version = "1.3.0"
edition = "2021"
description = "A CLI workflow engine (and GUI) for Wayland automation"
license = "MIT OR Apache-2.0"
Expand Down
9 changes: 9 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,19 @@ fn main() {
.file("src/bridge/auth.rs")
.file("src/bridge/deeplink_inbox.rs")
.file("src/bridge/explore.rs")
.file("src/bridge/kdl_highlight.rs")
.file("src/bridge/library.rs")
.file("src/bridge/state.rs")
.file("src/bridge/workflow.rs")
.file("src/bridge/recorder.rs")
// Hand-written C++ QSyntaxHighlighter subclass for the view-source
// pane. moc runs on the header automatically (it ends in `.h`),
// and the parent dir is added to the cc include path by
// cxx-qt-build, so `kdl_qml_register.h` can `#include "kdl_syntax_highlighter.h"`
// directly. Registration is invoked from main.rs before the
// QQmlApplicationEngine loads any QML.
.cpp_file("cpp/kdl_syntax_highlighter.h")
.cpp_file("cpp/kdl_qml_register.h")
.qt_module("Quick")
.qt_module("QuickControls2")
.build();
Expand Down
12 changes: 12 additions & 0 deletions cpp/kdl_qml_register.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Runtime QML type registration for the KDL syntax highlighter.
// Called from Rust main before QQmlApplicationEngine loads any QML, so
// `import Wflow 1.0; KdlSyntaxHighlighter {}` resolves at QML parse
// time.
#pragma once

#include "kdl_syntax_highlighter.h"
#include <QtQml/qqml.h>

inline void register_kdl_qml_types() {
qmlRegisterType<KdlSyntaxHighlighter>("Wflow", 1, 0, "KdlSyntaxHighlighter");
}
Loading
Loading