section_body_text() checked for a drawer START (:NAME:) before checking
for a drawer END (:END:). But :END: itself matches the generic :NAME:
pattern, so the end line re-opened a drawer instead of closing it,
leaving in_drawer set and swallowing the body that followed. A section
whose only content came after a property drawer therefore rendered an
empty /body (and, by extension, empty fuzzy-resolve bodies and GUI
sections).
Reorder the two checks: when already inside a drawer, handle :END:
first and consume the line, before the drawer-start pattern can match
it. This also leaves planning-line and leading-blank handling intact.
Add regression tests: body after a drawer directly under the headline,
and body after a planning line plus drawer. Server tests: 63 passed.
Add a root /cfg file to the 9P namespace backed by a TOML config file on
physical storage, so clients can read and change server settings without
restarting. The first setting is org_path: the directory scanned for org
files, with a leading ~ expanded to $HOME.
New config module (config.rs):
- Resolve the config file at $XDG_CONFIG_HOME/orkmode/config.toml, else
~/.config/orkmode/config.toml.
- expand_tilde(), read_raw(), write_raw(), parse(), and load().
- Config holds an optional org_path. Unit tests cover tilde expansion
(bare ~, ~/sub, non-tilde, ~user left intact) and TOML parsing.
Mutable org directory (state.rs):
- org_dir becomes an RwLock<PathBuf> with an org_dir() getter and a
set_org_dir() that validates the directory and rescans.
- config_raw() backs the /cfg read; apply_config() validates the TOML and
the target directory, applies org_path, then persists. A rejected write
(bad TOML or missing directory) never reaches disk and never mutates
state.
Namespace (namespace.rs):
- /cfg is a read+write file: read returns the raw config, write calls
apply_config.
Startup (ork-server.rs):
- With no -d/positional directory, fall back to the config file's
org_path (tilde-expanded), else the current directory. An explicit CLI
directory still takes precedence. Help text documents the config file.
Verified over 9P with an isolated HOME: /cfg reads empty on a fresh
install; writing org_path="~/org" expands the tilde and live-switches the
served documents; absolute paths work; nonexistent directories and
invalid TOML are rejected without changing state or disk; the setting
persists; and a no-flag restart loads org_path from the config file.
Server tests: 61 passed.
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.
Fuzzy link targets previously resolved only to an exact headline title or
a slug id, always with offset 0. Extend resolve_in_doc to follow the
Emacs org-link-search precedence:
1. a <<target>> dedicated target (or <<<radio>>> radio target)
2. an element named with #+NAME: target
3. a headline whose title matches
4. (compat) a section whose slug id matches
5. the target text appearing literally in a section's content
(case-insensitive)
Each branch returns the owning section plus a byte offset relative to the
start of that section's raw content, so a caller can later scroll within
/body. Dedicated-target and plain-text matches use lightweight scans over
the raw section slice (find_org_target, find_plain_text); #+NAME uses the
parsed affiliated keywords. The new section_content helper yields a
section's raw content region and its starting offset.
Fuzzy matching inherits the cross-document fallback already wrapping
resolve_in_doc, so a bare target can reach a <<target>>, named element,
or body text in another file.
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.
A read-write 9P file per document: write a link target, read back its
structural resolution. resolve_target dispatches by target syntax,
mirroring Emacs org-link-search precedence but terminating in a section
id rather than a buffer position:
#custom-id / id:uuid / *Headline -> dedicated (exact section)
http(s)/mailto/ftp/file -> external (OS-handled)
bare target naming a heading -> fuzzy (section)
otherwise -> notfound
Response schema carries kind/section/offset; the offset field is wired
now (0 for current cases) so Phase 2 fuzzy text/<<target>>/#+NAME search
with an intra-section byte offset slots in without changing the contract.
Resolved ids are directly valid 9P paths (/<doc>/<id>).
Parse the :results switch from a src block's header arguments and apply
Emacs org-babel semantics when writing results:
- silent/none: execute and report, but do not write a #+RESULTS: block
- raw: insert output verbatim, no prefix or wrapping
- drawer: wrap output in a :results: ... 🔚 drawer
- default: fixed-width for <10 lines, #+begin_example for 10+
The insert_results detection loop now recognizes and replaces all four
wrapping styles in place, so re-execution and cross-mode transitions
(e.g. drawer to fixed-width) update cleanly without duplication. Raw
results, which have no delimiters, terminate at a blank line or heading.
Expose the language registry via ctl: `languages` lists supported src
block languages (one per line), `reload-languages` reloads plugin configs
from /etc/orkmode/languages and ~/.config/orkmode/languages and reports
how many were loaded.
Code execution now uses a LanguageRegistry with built-in defaults
(bash, python, ruby, perl, javascript) that can be overridden by TOML
config files in /etc/orkmode/languages or ~/.config/orkmode/languages.
Match Emacs org-babel result formatting: fixed-width (": " prefix) for
output under 10 lines, #+begin_example blocks for 10+ lines
(org-babel-min-lines-for-block-output).
Fix insert_results duplication: the source block span end already points
to the start of the line after #+END_SRC, so searching for the next
newline skipped the existing #+RESULTS: line and prepended duplicates on
re-execution. Also handle replacing existing #+begin_example result blocks.
Fix parser: #+begin_example, comment, and export blocks were misparsed as
SourceBlock with a bogus language, causing "unsupported language" errors
on re-execution. Map them to their correct AST element kinds.
- Execute returns results and writes them to the org file
- Replaces existing #+RESULTS: block if present
- Results formatted with : prefix per org convention
- Process blocks in reverse order to preserve byte offsets
- Blank line added after results for proper separation
- execute <doc> — all blocks in document
- execute <doc> <section> — all blocks in subtree
- execute <doc> <section> <idx> — single block by index
- execute <doc> <section> <name> — block by #+NAME in section
- execute <doc> :<name> — named block anywhere in doc
Parser now extracts #+NAME: from block's affiliated keywords.
Supports bash, python, ruby, perl, and node interpreters.
Basic sandboxing via env_clear() and restricted PATH.
- Use 'contains' instead of 'starts_with' to handle combined lines
like 'SCHEDULED: <...> DEADLINE: <...>'
- Track 'seen_content' to strip planning/drawers only at the start
- Skip empty lines before content
- Continue stripping planning lines until real content appears
section_body() now filters out:
- Leading planning line (SCHEDULED/DEADLINE/CLOSED)
- Property drawers (:PROPERTIES: ... :END:)
- Any other drawers (:LOGBOOK:, etc.)
- Leading/trailing blank lines
These are displayed separately in the UI.
Dropping the C FFI layer left orkmode-core unused: nothing in the
workspace depends on it, and the GUI will speak 9P instead. Delete the
crate, drop it from the workspace, and scrub FFI references from the
docs (README structure/usage, AGENTS structure/FFI/next-steps).
Client verbs for the new server capabilities, each a thin wrapper over
the base 9P operations: the structural verbs write to /ctl, and query
writes to /query and prints the matches.
Rework /ctl into a self-describing dispatch table (name, desc, fn),
inspired by the ollie 9P server: an empty write or "help" lists the
verbs, and unknown verbs report the valid set.
New control verbs, each operating on a section subtree:
- rm: delete a section and its subtree
- move up|down: reorder among siblings
- promote/demote: shift the subtree's outline level
- refile <to-doc> [parent]: move a subtree across or within documents,
re-leveling to the target depth
- archive: move a subtree to the companion <doc>.org_archive file
- lint [doc]: reparse and report per document
Add a root /query file (rdwr): write a tag:X+todo:Y+priority:Z match
expression, read back matching sections.
Implement the previously stubbed agenda today/week views with real date
filtering, using a dependency-free civil-date day-number calculation.
Subtree edits share span-based helpers (subtree_range, sibling_ranges,
shift_headline_levels, insert_at). Covered by unit tests.
Four more friendly verbs, all pure compositions of the base 9P
operations against the existing namespace (no server change):
- tag <doc> <id> <tag> [--remove]: toggle a single tag, mirroring
org-toggle-tag, instead of rewriting the whole tag list.
- prop <doc> <id> <key> [value] [--delete]: get, set, or delete one
property, mirroring org-set-property, instead of the whole drawer.
- todo <doc> <id> [state]: set the keyword, or with no argument cycle
to the next state following the /keywords sequence, like org-todo.
- tree <doc>: print the recursive section outline.
Each edit reads the relevant namespace file, modifies it, and writes it
back; all validate the section exists first for a clear error.
ork now operates solely as a 9P client against ork-server; all org
intelligence lives in the server. Remove the local file-parsing command
layer (commands.rs) and its org-ast/org-parser/orkmode-core/serde deps.
Base verbs speak raw 9P: ls, read, write, rdwr. Address resolves from
--addr, then $ORK_ADDR, then tcp://localhost:5640.
Add friendly commands, all thin compositions of the base 9P operations:
docs, sections, todos, agenda, get, set, add (--parent), mkdoc, reload.
Fix the directory-listing parser in client.rs: the stat-entry name offset
double-counted the size field (pos + 2 + 41 instead of pos + 41), so ls
returned nothing. Handle a downstream reader closing early (e.g. | head)
by treating BrokenPipe as a clean exit instead of panicking.
Dynamic child resolvers returned Some for any name, fabricating phantom
document and section directories so walks to nonexistent paths appeared
to succeed. Validate existence and return None instead:
- root resolver checks get_doc
- document resolver checks new OrkState::section_exists
- section resolver checks child_ids
Also fix create_child_section, which corrupted the org file:
- find_section_end used the parent's headline-end offset, inserting the
child before the parent's PROPERTIES/ID drawer and detaching its ID.
Use section.span.end, which covers planning, drawer, body, children.
- The child headline was written verbatim; demote it to parent.level + 1
via the new restar helper.
Add regression tests for restar and child section nesting.
- set_section_keyword/priority/title/tags
- set_section_scheduled/deadline/closed
- set_section_properties/body
- rebuild_headline helper for atomic headline updates
- Fixed replace_headline to preserve DEADLINE/:PROPERTIES: lines
- Clear read cache on write in 9P server
All writes tested working via 9p client.
Simple 9P2000 client supporting:
- Connect via Unix socket or TCP
- read/write/rdwr operations
- Directory listing
Preparation for reworking CLI to use 9P server.
- new file at root: create org file
- new file at doc level: create top-level section
- new file at section level: create child section
- All created sections get UUID in :ID: property
- body file added to doc (alias for raw)
- Test for section creation via 9P namespace