Commit Graph

28 Commits

Author SHA1 Message Date
Levi Neely 7c9779b2fc Expose /cfg to read and write an on-disk config file
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.
2026-10-02 12:07:52 +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 c6bb21800a Resolve fuzzy org link targets across the full search ladder
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.
2026-10-02 09:34:33 +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 b21b675980 Add /<doc>/resolve for link target resolution
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>).
2026-10-01 16:31:41 +02:00
Levi Neely d073c8fa5e Honor :results header args on code execution
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.
2026-10-01 13:48:13 +02:00
Levi Neely a13f76cd40 Add languages and reload-languages ctl verbs
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.
2026-10-01 13:39:16 +02:00
Levi Neely 84101145d7 Add language plugin registry and Emacs-style #+RESULTS: formatting
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.
2026-10-01 13:36:38 +02:00
Levi Neely 4a865d7546 Write #+RESULTS: blocks after code execution
- 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
2026-10-01 11:46:16 +02:00
Levi Neely 17f18b044c Add Landlock sandbox for code execution
- sandbox.rs: Landlock filesystem restrictions applied via pre_exec
- Library paths (/lib, /usr/lib) need execute permission for dynamic linker
- 30s timeout with SIGKILL on expiry
- Network access allowed (Landlock only restricts filesystem)
- Minimal env: PATH, HOME=/tmp, LANG, TERM
2026-10-01 11:38:43 +02:00
Levi Neely 445177bbfa Add execute ctl verb for org-babel style code execution
- 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.
2026-10-01 10:51:39 +02:00
Levi Neely 43565ad1b6 Fix planning line stripping in section_body
- 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
2026-10-01 10:10:45 +02:00
Levi Neely f5f2bffce4 Strip planning line and drawers from section body
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.
2026-10-01 10:09:39 +02:00
Levi Neely ed73ae5cca Clean up docs and fix set_planning_field
- README: update CLI docs to reflect 9P client architecture
- README: update roadmap to reflect v0.3 progress (server, UI)
- AGENTS.md: update CLI examples and current limitations
- state.rs: rewrite set_planning_field to properly parse/rebuild
  planning lines (removes dead code, fixes replace logic)
2026-10-01 08:53:22 +02:00
Levi Neely 33173bcc29 Remove dead orkmode-core (FFI) crate
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).
2026-09-30 22:27:50 +02:00
Levi Neely 943c05b61c Add rm, mv, promote, demote, refile, archive, lint, query to ork
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.
2026-09-30 21:52:33 +02:00
Levi Neely 12869fb9f0 Add structural ops, query, lint, and real agenda views to ork-server
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.
2026-09-30 21:52:33 +02:00
Levi Neely 73c4eb6cff Add tag, prop, todo, and tree commands to ork
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.
2026-09-30 21:40:39 +02:00
Levi Neely 32e4fd0007 Make ork a pure 9P client with friendly commands
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.
2026-09-30 21:29:09 +02:00
Levi Neely 623aef4f42 Fix phantom namespace nodes and child section insertion
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.
2026-09-30 21:28:52 +02:00
Levi Neely 17f4caa37f Add write handlers for section properties
- 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.
2026-09-30 16:09:44 +02:00
Levi Neely 94862b7f26 Add 9P client to ork-cli
Simple 9P2000 client supporting:
- Connect via Unix socket or TCP
- read/write/rdwr operations
- Directory listing

Preparation for reworking CLI to use 9P server.
2026-09-30 15:11:10 +02:00
Levi Neely 9e2fcb9010 Add 9P2000 server implementation
- p9.rs: Full 9P2000 protocol handler
  - Tversion/Tattach/Twalk/Topen/Tread/Twrite/Tclunk/Tstat
  - Directory listing with stat encoding
  - File read/write with buffering
  - Rdwr support for command files

- ork-server binary:
  - TCP or Unix socket listener
  - Threaded connection handling
  - Usage: ork-server -a unix:///tmp/ork.sock ~/org

Tested with plan9port 9p client - read/write/ls all working.
2026-09-30 15:09:59 +02:00
Levi Neely 96f89acd65 Add complete section properties to 9P namespace
Section files now include:
- level (read-only): headline level 1-6
- scheduled: SCHEDULED timestamp
- deadline: DEADLINE timestamp
- closed: CLOSED timestamp
- properties: key=value lines from property drawer
- body: section content (text between headline and first child)

All timestamp fields use org-mode format: <YYYY-MM-DD Day HH:MM +repeat>
2026-09-30 14:16:18 +02:00
Levi Neely 6bb2994dff Add section creation with UUID IDs
- 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
2026-09-30 10:41:19 +02:00
Levi Neely 246bd9bb5a Add ork-server crate with virtfs EDSL
- virtfs: Declarative filesystem EDSL (port of ollie's pattern)
  - dir(), file(), each(), dir_dynamic() constructors
  - read(), write(), rdwr(), rw() handlers
  - FsNode with walk(), list(), read(), write()

- state: OrkState manages parsed documents
  - Scans org directory, parses on load
  - Section ID from CUSTOM_ID > ID > slugified title
  - Read/write document content

- namespace: 9P namespace builder
  - ctl, idx, keywords at root
  - agenda/todos, agenda/today, agenda/week
  - {doc}/ with raw, meta, and sections
  - {section}/ with headline, title, keyword, priority, tags
  - Nested children for subsections
2026-09-30 10:31:11 +02:00
Levi Neely 7e24a9cf02 Implement property/tag writes, timestamp formatting, add tests
- property command: set/delete properties in property drawer
- tag command: add/remove tags on headlines
- Timestamp formatting: agenda shows actual dates instead of 'set'
- Added 17 unit tests for parser (headlines, TODOs, tags, planning, etc.)
2026-09-30 09:52:14 +02:00
Levi Neely 14fc3233e9 Initial commit: org-mode parser and CLI
- org-ast: AST types for org documents
- org-parser: tree-sitter based parser
- orkmode-core: library + C FFI
- ork-cli: directory-aware CLI tool

CLI operates on directories by default, supports -R for recursive.
2026-09-30 09:43:21 +02:00