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)
This commit is contained in:
Levi Neely 2026-10-01 08:53:22 +02:00
parent e129f9ee94
commit ed73ae5cca
3 changed files with 151 additions and 158 deletions

View File

@ -39,18 +39,20 @@ cargo test
### CLI ### CLI
The `ork` CLI operates on directories by default: The `ork` CLI is a 9P client that talks to `ork-server`. Start a server first, then:
```bash ```bash
ork info . # Aggregate info for all .org files export ORK_ADDR=unix:///tmp/ork.sock
ork sections --todos-only # List TODOs across files
ork query "tag:work" -R # Recursive search ork docs # List documents
ork get "section-id" -j # JSON output ork tree work # Outline of work.org
ork toggle "task-id" -n # Dry run ork todos # All TODOs
ork add "New Task" file.org --todo TODO ork get work my-task # Section fields
ork set work my-task keyword DONE
ork add work "* TODO New task :work:"
``` ```
Use `-j/--json` for machine-readable output. Use `-n/--dry-run` for mutations. Base 9P operations are available as `ork ls`, `ork read`, `ork write`, `ork rdwr`.
## Architecture Decisions ## Architecture Decisions
@ -99,9 +101,8 @@ Use `-j/--json` for machine-readable output. Use `-n/--dry-run` for mutations.
## Current Limitations ## Current Limitations
1. **Inline objects incomplete** — tree-sitter-org doesn't fully parse bold/italic/links 1. **Inline objects incomplete** — tree-sitter-org doesn't fully parse bold/italic/links
2. **Property/tag write commands** — stubs only, not implemented 2. **No incremental parsing** — full reparse on every write
3. **Timestamp text** — displays "set" placeholder, not actual date 3. **Section ID instability** — slug-derived IDs change when titles change (UUID IDs via `:ID:` are stable)
4. **No incremental parsing** — full reparse on every call
## Next Steps (v0.4) ## Next Steps (v0.4)
@ -126,8 +127,14 @@ Before committing:
```bash ```bash
cargo build --release cargo build --release
cargo test cargo test
./target/release/ork check .
./target/release/ork info test.org -j # Integration test (start server, run CLI, stop server)
ork-server -a unix:///tmp/ork-test.sock -d /tmp/test-org &
SERVER_PID=$!
export ORK_ADDR=unix:///tmp/ork-test.sock
ork docs
ork lint
kill $SERVER_PID
``` ```
## Questions? ## Questions?

209
README.md
View File

@ -116,126 +116,91 @@ cmake --build ui/build -j
## Usage ## Usage
### CLI (ork) ### Server
The `ork` command operates on org directories or individual files. All commands default to Start the 9P server to expose an org directory:
the current directory (`.`) when no path is specified. Use `-R` for recursive subdirectory scanning.
```bash ```bash
# List org files # Unix socket (preferred for local use)
ork files # Current directory ork-server -a unix:///tmp/ork.sock -d ~/org
ork files ~/org # Specific directory
ork files -R . # Recursive
# Document info (aggregated across all files) # TCP socket
ork info # Current directory ork-server -a tcp://localhost:5640 -d ~/org
ork info ~/org -R # Recursive
ork info file.org # Single file
ork info --json # JSON output
# List sections
ork sections # All sections in cwd
ork sections --todos-only # Only TODOs
ork sections --tag work # Filter by tag
ork sections --depth 2 # Max depth
ork sections -R -j # Recursive, JSON
# Query sections
ork query "tag:work" # By tag
ork query "todo:TODO" # By TODO state
ork query "priority:A" # By priority
ork query "title:pattern" # By title
ork query "file:notes" # By filename
ork query "done:false" -j # JSON output
# Get specific section by ID or title
ork get "my-task-id" # Searches all files
ork get "My Task" -j # JSON output
ork get "intro" ~/org -R # In specific path
# List tags with counts
ork tags # All tags in cwd
ork tags -R --json # Recursive, JSON
# Export as JSON
ork export --pretty # All documents
ork export --section my-id # Specific section
# Add section
ork add "New Task" file.org # To specific file
ork add "New Task" --file notes.org # When path is directory
ork add "New Task" file.org --todo TODO --priority A --tags "work,urgent"
ork add "New Task" file.org -n # Dry run
# Toggle TODO state
ork toggle "task-id" # Searches all files
ork toggle "task-id" -n # Dry run
# Agenda view
ork agenda # Current directory
ork agenda ~/org -R # Recursive
ork agenda --days 14 --json # 14 days, JSON
# Validate syntax
ork check # Current directory
ork check -R --json # Recursive, JSON
``` ```
### Rust API ### CLI (ork)
```rust `ork` is a 9P client that talks to `ork-server`. All org-mode intelligence lives in the server.
use org_parser::Parser;
let mut parser = Parser::new(); ```bash
let doc = parser.parse(r#" # Set the server address (or use -a on each call)
#+TITLE: My Document
#+TODO: TODO WAITING | DONE
* TODO [#A] First Task :work:
DEADLINE: <2025-01-15 Wed 14:00>
Some description.
** Subtask
- [ ] Item 1
- [X] Item 2
"#).unwrap();
// Access document properties
println!("Title: {:?}", doc.title());
println!("Sections: {}", doc.sections.len());
println!("TODOs: {}", doc.todos().count());
// Iterate sections
for section in doc.all_sections() {
println!("{} {}",
"*".repeat(section.level() as usize),
section.headline.title_text()
);
}
```
### 9P server & client
Serve a directory of org files over 9P, then act on it with `ork`:
```sh
# Start the server (unix socket or tcp)
ork-server -a unix:///tmp/ork.sock -d ~/org &
# Point the client at it (or pass -a on each call)
export ORK_ADDR=unix:///tmp/ork.sock export ORK_ADDR=unix:///tmp/ork.sock
ork docs # list documents # Base 9P operations
ork tree work # outline of work.org ork ls / # List root namespace
ork todos # TODO items across all docs ork read /idx # Read a file
ork set work my-task keyword DONE ork write /work/task/keyword DONE # Write a value
ork refile work my-task archive --parent done-items
ork query 'tag:work+todo:TODO' # Friendly verbs (compositions of 9P ops)
ork docs # List documents (name + title)
ork sections work # Top-level sections in work.org
ork tree work # Full section outline
ork todos # All TODO items across docs
ork todos work # TODOs in work.org only
# Section details
ork get work my-task # Show section fields
ork set work my-task keyword DONE # Set a field
ork set work my-task tags work:urgent # Set tags (colon-separated)
# Create sections
ork mkdoc notes # Create notes.org
ork add work "* TODO New task :work:" # Add top-level section
ork add work "* Subtask" -p parent-id # Add child section
# TODO management
ork todo work my-task # Cycle to next state
ork todo work my-task DONE # Set explicit state
ork tag work my-task urgent # Add tag
ork tag work my-task urgent -r # Remove tag
# Structural operations
ork mv work my-task up # Reorder among siblings
ork promote work my-task # Decrease level
ork demote work my-task # Increase level
ork rm work my-task # Delete section and subtree
ork refile work my-task archive # Move to another doc
ork refile work my-task archive -p done # As child of 'done' section
ork archive work my-task # Move to work.org_archive
# Properties
ork prop work my-task CUSTOM_ID # Get property
ork prop work my-task EFFORT 2h # Set property
ork prop work my-task EFFORT -d # Delete property
# Agenda views
ork agenda todos # All TODOs
ork agenda today # Items scheduled/due today
ork agenda week # Next 7 days
# Query
ork query 'tag:work' # Match sections by tag
ork query 'tag:work+todo:TODO' # Compound query
ork query 'priority:A' # By priority
# Maintenance
ork reload # Rescan files from disk
ork lint # Validate all documents
ork lint work # Validate one document
``` ```
Any 9P client works too (e.g. plan9port `9p`); the server owns all org Any 9P client works (e.g., plan9port `9p`):
logic and reads/writes the real `.org` files.
```bash
9p -a 'unix!/tmp/ork.sock' ls /
9p -a 'unix!/tmp/ork.sock' read /idx
9p -a 'unix!/tmp/ork.sock' write /work/task/keyword DONE
```
## Architecture ## Architecture
@ -263,30 +228,36 @@ All nodes include source span information for editor integration.
## Roadmap ## Roadmap
### Phase 1: Parser (Current) ### Phase 1: Parser ✓
- [x] Tree-sitter integration - [x] Tree-sitter integration
- [x] Core AST types - [x] Core AST types
- [x] Headline/section parsing - [x] Headline/section parsing
- [x] Block element parsing - [x] Block element parsing
- [ ] Complete inline object parsing - [ ] Complete inline object parsing
### Phase 2: Qt UI ### Phase 2: 9P Server ✓
- [x] Document state with locking
- [x] Read/write section fields (keyword, priority, title, tags, timestamps, body)
- [x] Create documents and sections
- [x] Structural ops (delete, move, promote, demote, refile, archive)
- [x] Agenda views (todos, today, week)
- [x] Query interface (tag, todo, priority)
- [x] ctl verbs (reload, lint)
### Phase 3: Qt UI (Current)
- [x] Document rendering (read-only) - [x] Document rendering (read-only)
- [x] Structural styling (keywords, priorities, tags, timestamps) - [x] Structural styling (keywords, priorities, tags, timestamps)
- [x] Folding/cycling - [x] Folding/cycling (TreeView)
- [x] Navigation - [x] Navigation
- [ ] Syntax highlighting for section bodies
### Phase 3: Editing
- [ ] Inline editing - [ ] Inline editing
- [ ] TODO state cycling - [ ] TODO state cycling (click to toggle)
- [ ] Tag editing - [ ] Tag/property editing
- [ ] Property editing - [ ] Syntax highlighting for bodies
### Phase 4: Agenda ### Phase 4: Agenda
- [ ] Basic agenda view - [ ] Dedicated agenda view panel
- [ ] Day/week/month views - [ ] Day/week/month views
- [ ] Filtering - [ ] Filtering and sorting
- [ ] Custom views - [ ] Custom views
### Phase 5: Advanced Features ### Phase 5: Advanced Features

View File

@ -594,42 +594,57 @@ impl OrkState {
let planning_end = planning.span.end.offset; let planning_end = planning.span.end.offset;
let planning_text = &content[planning_start..planning_end]; let planning_text = &content[planning_start..planning_end];
// Parse existing planning fields
let mut fields: HashMap<&str, &str> = HashMap::new();
for part in planning_text.split_whitespace() {
if part.ends_with(':') {
let key = &part[..part.len()-1];
continue;
}
}
// Rebuild planning line // Rebuild planning line
let new_planning = if timestamp.is_empty() { let new_planning = if timestamp.is_empty() {
// Remove this field // Remove this field: strip "FIELD: <timestamp>" pattern
let filtered: String = planning_text let mut result = String::new();
.replace(&format!("{}: ", field), "") let mut skip_next_ts = false;
.split_whitespace() for part in planning_text.split_whitespace() {
.filter(|s| !s.starts_with('<') || !planning_text.contains(&format!("{}: {}", field, s))) if part == &format!("{}:", field) {
.collect::<Vec<_>>() skip_next_ts = true;
.join(" "); continue;
if filtered.trim().is_empty() { }
if skip_next_ts && part.starts_with('<') {
skip_next_ts = false;
continue;
}
skip_next_ts = false;
if !result.is_empty() { result.push(' '); }
result.push_str(part);
}
if result.trim().is_empty() {
String::new() String::new()
} else { } else {
format!("{}\n", filtered) format!("{}\n", result.trim())
} }
} else { } else {
// Add/update this field // Add/update this field
let ts = if timestamp.starts_with('<') { timestamp.to_string() } else { format!("<{}>", timestamp) }; let ts = if timestamp.starts_with('<') { timestamp.to_string() } else { format!("<{}>", timestamp) };
if planning_text.contains(&format!("{}:", field)) { if planning_text.contains(&format!("{}:", field)) {
// Replace existing // Replace existing: rebuild with new timestamp for this field
let re_pattern = format!(r"{}: <[^>]+>", field); let mut result = String::new();
planning_text.replace(&planning_text let mut replace_next_ts = false;
.split_whitespace() for part in planning_text.split_whitespace() {
.collect::<Vec<_>>() if part == &format!("{}:", field) {
.join(" "), &format!("{}: {}", field, ts)) replace_next_ts = true;
if !result.is_empty() { result.push(' '); }
result.push_str(part);
continue;
}
if replace_next_ts && part.starts_with('<') {
result.push(' ');
result.push_str(&ts);
replace_next_ts = false;
continue;
}
replace_next_ts = false;
if !result.is_empty() { result.push(' '); }
result.push_str(part);
}
format!("{}\n", result.trim())
} else { } else {
// Add new // Add new field
format!("{}{}: {}\n", planning_text.trim_end(), field, ts) format!("{} {}: {}\n", planning_text.trim(), field, ts)
} }
}; };