Commit Graph

12 Commits

Author SHA1 Message Date
Levi Neely dc34f6467f Cycle a TODO keyword by clicking its pill
First direct-manipulation edit (v0.4): clicking a headline's keyword pill
cycles its TODO state, each change a single 9P write. Left-click / wheel-up
advance (TODO -> NEXT -> ... -> DONE -> none -> TODO); right-click / wheel-down
step back.

Per-document keyword set (server):
- keywords() previously advertised a hardcoded "TODO NEXT WAITING | DONE
  CANCELLED", but the parser only recognizes NEXT/WAITING/CANCELLED when the
  document declares them via #+TODO:. Writing an unrecognized keyword merged it
  into the title and changed the section's slug id.
- keywords() now returns the parser default "TODO | DONE"; new keywords_for(doc)
  returns a document's declared #+TODO: sequence or that default. Exposed as a
  per-document /<doc>/keywords file.

GUI:
- DocumentModel::setKeyword writes /<doc>/<id>/keyword and refreshes the row;
  cycleKeyword reads the per-document /<doc>/keywords so it only cycles to
  keywords the parser recognizes (keeping the section id stable), with a
  trailing empty state to clear the keyword.
- The keyword pill gains a hover highlight and a MouseArea for click/wheel
  cycling.

Verified: server tests (66 passed, +3: default keyword set, declared sequence,
id stable across set+clear). Live 9P run cycling both a plain document
(TODO|DONE) and one with a #+TODO: sequence confirms the section id stays
stable through a full cycle and every transition writes correctly; empty-write
clearing is covered by a unit test (the ork CLI can't pass an empty arg, but
the GUI's p9 client sends a real empty write). GUI builds clean.

Also refresh VISION.md milestones: v0.3 done; checkbox toggling, code
execution, and keyword cycling ticked under v0.4.
2026-10-02 12:37:28 +02:00
Levi Neely 66a1ca11b5 Make resolve offsets index the rendered /body text
Link resolution reported byte offsets relative to a section's raw
content region (headline end), but the GUI renders /body, which strips
the planning line, property drawer, and surrounding blank lines. An
offset into raw content therefore did not line up with the body the
GUI shows, so scrolling to a link target landed in the wrong place.

Server:
- Extract section_body_text() as a shared helper so section_body() and
  resolve_target() derive offsets from the exact same rendered string.
- Rewrite the dedicated-target, named-element, and plain-text branches
  of resolve_target() to search section_body_text() and report a
  /body-relative offset.
- Replace the affiliated-keyword lookup with find_name_line(), a text
  scan over /body: the parser only attaches #+NAME to some element
  kinds (not tables), so scanning the rendered body is more reliable.
- Drop the now-unused section_content() helper.
- Update tests with concrete /body-relative offset assertions
  (<<target>> at 22, blank-stripped at 5, #+NAME at 44).

GUI:
- sectionRevealed gains a bodyOffset argument, threaded from the
  resolve response through revealSection into the QML Connections.
- OutlineView positions the row at Beginning when the match lies inside
  the body (offset > 0) so the body is visible, else just Contain.

Scroll granularity is section-top: the row is scrolled into view, not
the exact character. True char-precise scrolling needs a single
addressable body text widget; /body is currently segmented into
text/code/table/list parts. Deferred.
2026-10-02 11:04:05 +02:00
Levi Neely 27e63f7a6f Follow org links in the GUI, resolving across the org path
Clicking an internal org link now navigates the outline to its target.
The GUI resolves the link via /<doc>/resolve and acts on the result:
external targets open in the OS handler; internal targets expand the
target's ancestors, scroll to it, and flash it.

Resolution spans the whole org path rather than a single document:

  - file:DOC::TARGET and file:DOC resolve TARGET within the named
    document (a file: link to a document not in the path stays external).
  - id:uuid links are globally unique and resolve in whichever document
    owns the ID.
  - #custom-id, *Headline, and bare fuzzy targets resolve in the current
    document first, then fall back across every other document.

The resolve result gains a `doc:` field naming the owning document. When
it differs from the current document, DocumentModel::followLink switches
the model to that document before revealing the section.

DocumentModel gains revealSection() and followLink(); OutlineView wires
link clicks to followLink and animates the revealed row via the new
sectionRevealed signal.
2026-10-02 09:26:03 +02:00
Levi Neely 39ab7108ff Cycle checkboxes with the mouse wheel
Scrolling over a checkbox glyph cycles its state
unchecked -> partial -> checked (and back), respecting scroll direction:
wheel up steps forward, wheel down steps in reverse. Left-click still
toggles checked/unchecked.

The shared rewriteCheckbox helper applies a state-mapping function to the
Nth cookie; toggleCheckbox and the new cycleCheckbox are thin callers.
2026-10-01 15:53:06 +02:00
Levi Neely 55856707c7 Make GUI checkboxes clickable
Clicking a checkbox glyph toggles its state and writes the change back.
DocumentModel::toggleCheckbox(row, ordinal) finds the Nth checkbox cookie
in the section body (document order), flips [ ]/[-] -> [X] and [X] -> [ ],
writes the body via /body, and refreshes the row.

The body parser assigns each checkbox item a stable ordinal across the
whole section, and the list delegate wires a pointer-cursor MouseArea on
the glyph (checkbox items only) to the model toggle.
2026-10-01 15:45:26 +02:00
Levi Neely 36a9607576 Add code block execution to the GUI outline view
Each executable source block now shows a "Run" button in the outline.
Clicking it runs the block server-side and renders the inserted
#+RESULTS: in place, with an exit-status indicator.

DocumentModel gains executeBlock(row, blockIndex), which runs
/ctl execute <doc> <id> <n>, refreshes the section body, and emits
dataChanged(BodyRole). isLanguageSupported(lang) backs button visibility
via /ctl languages (loaded once, cached).

OutlineView tags each parsed code part with its src-block ordinal so the
GUI index matches the server's block indexing, and the code block header
carries the Run control plus running/ok/error status.
2026-10-01 15:26:05 +02:00
Levi Neely 429c896f5a Add collapsible properties drawer
- PropertiesRole returns QVariantMap of key->value
- loadProperties() parses KEY=value lines from /properties file
- QML drawer shows ':PROPERTIES:' header with ▶/▼ toggle
- Properties shown as monospace :KEY: value pairs when expanded
- Drawer starts collapsed by default
2026-10-01 10:00:20 +02:00
Levi Neely 7f948de2ab Use listDirs() instead of list() for section loading
list() returns files AND directories. Section entries are directories only.
Files like 'title', 'keyword', 'body' were being incorrectly treated as
child sections. Using listDirs() filters to directories only.
2026-10-01 09:40:03 +02:00
Levi Neely 350ce66860 Fix DocumentModel section loading
- loadSections: recursively load first, then populate childIds after
  all sections exist (checking parentId matches)
- Remove childIdsFor helper (dead code)
- Remove isSectionVisible (dead code)
2026-10-01 09:37:55 +02:00
Levi Neely fec815ac50 Fix path prefix in DocumentModel::loadDocument
9P paths need leading slash. loadSections was called with bare document
name (e.g., 'test') instead of '/test'.
2026-10-01 09:35:42 +02:00
Levi Neely 70ec9509a4 Replace TreeView with ListView for document outline
TreeView (inherits TableView) is designed for file browsers with fixed row
heights, not structured document editing. ListView gives us:
- Full control over row heights (implicitHeight just works)
- Simpler delegate without TreeViewDelegate quirks
- Manual indentation via leftMargin based on level
- Expansion state tracked in model with hasChildren/expanded roles
- Click-to-expand triangles for sections with children

DocumentModel changed from QAbstractItemModel (tree) to QAbstractListModel
(flat list) with sections ordered depth-first. Tree structure preserved via
level, parentId, childIds fields. Expansion state controls visibility.
2026-10-01 09:32:56 +02:00
Levi Neely 71a89943ae Add v0.3 read-only Qt6/QML GUI (9P client)
A standalone Qt6/QML app in ui/ that speaks 9P to ork-server; no FFI,
no privileged path. All org logic stays in the server.

- p9client: minimal synchronous 9P2000 client over QTcp/QLocalSocket
- orkclient: QML-facing facade (read/list/listDirs/write/rdwr/docs)
- documentmodel: lazy QAbstractItemModel of the section tree, with a
  BodyRole that reads /<doc>/<id>/body (planning line + :PROPERTIES:
  drawer stripped, since those are shown as structural objects)
- Main.qml: document sidebar + outline; OutlineView.qml: folding
  TreeView rendering keyword/priority/title/tags/timestamps as distinct
  objects, with section body shown inline beneath each headline
- main.cpp: --addr/$ORK_ADDR, --selftest (headless outline dump),
  --shot (headless window grab for debugging)
- CMake build; binary emitted to bin/ to avoid the QML-module-URI vs
  target-name directory clash

Known issue: inline body layout still needs work (row sizing/overlap
when bodies wrap); to be iterated.
2026-10-01 02:02:53 +02:00