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.
This commit is contained in:
parent
623aef4f42
commit
32e4fd0007
|
|
@ -550,12 +550,6 @@ name = "ork-cli"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"org-ast",
|
||||
"org-parser",
|
||||
"orkmode-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -11,10 +11,4 @@ name = "ork"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
org-ast = { path = "../org-ast" }
|
||||
org-parser = { path = "../org-parser" }
|
||||
orkmode-core = { path = "../orkmode-core" }
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -233,12 +233,6 @@ impl Client {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
/// Read file as string.
|
||||
pub fn read_string(&mut self, path: &str) -> io::Result<String> {
|
||||
let data = self.read(path)?;
|
||||
Ok(String::from_utf8_lossy(&data).trim().to_string())
|
||||
}
|
||||
|
||||
/// Write to a file.
|
||||
pub fn write(&mut self, path: &str, data: &[u8]) -> io::Result<()> {
|
||||
let fid = self.walk(path)?;
|
||||
|
|
@ -370,8 +364,9 @@ impl Client {
|
|||
break;
|
||||
}
|
||||
|
||||
// Skip to name field: size(2) + type(2) + dev(4) + qid(13) + mode(4) + atime(4) + mtime(4) + length(8) = 41
|
||||
let name_offset = pos + 2 + 41;
|
||||
// Stat layout from entry start: size(2) + type(2) + dev(4) + qid(13)
|
||||
// + mode(4) + atime(4) + mtime(4) + length(8) = 41 bytes, then name[s].
|
||||
let name_offset = pos + 41;
|
||||
if name_offset + 2 > data.len() {
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,353 +1,363 @@
|
|||
//! ork - CLI for org-mode file manipulation
|
||||
//! ork - 9P client for ork-server.
|
||||
//!
|
||||
//! Operates on org directories or individual files.
|
||||
//! Provides commands for:
|
||||
//! - Querying org files (list todos, sections, tags)
|
||||
//! - Outputting as JSON for programmatic use
|
||||
//! - Modifying org files (add/update sections, toggle todos)
|
||||
//! `ork` is only a 9P client: it connects to a running `ork-server` and
|
||||
//! operates on its namespace. All org-mode intelligence lives in the server.
|
||||
//! The base verbs (ls/read/write/rdwr) speak raw 9P; the friendly verbs
|
||||
//! (docs/sections/todos/agenda/get/set/add/mkdoc/reload) are thin compositions
|
||||
//! of those same 9P operations against the exposed namespace.
|
||||
//!
|
||||
//! Address resolution: `--addr` flag, then `$ORK_ADDR`, then
|
||||
//! `tcp://localhost:5640`. Accepts `tcp://host:port` or `unix:///path/to/sock`.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process;
|
||||
|
||||
mod commands;
|
||||
mod client;
|
||||
|
||||
use client::Client;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "ork")]
|
||||
#[command(author, version, about = "CLI for org-mode file manipulation")]
|
||||
#[command(author, version, about = "9P client for ork-server")]
|
||||
#[command(propagate_version = true)]
|
||||
#[command(after_help = "PATH can be a directory (operates on all .org files) or a single .org file.")]
|
||||
#[command(after_help = "\
|
||||
PATH is a path in the ork-server 9P namespace (e.g. /idx, /test/task/keyword).
|
||||
|
||||
Address resolution order: --addr, then $ORK_ADDR, then tcp://localhost:5640.
|
||||
Accepts tcp://host:port or unix:///path/to/socket.")]
|
||||
struct Cli {
|
||||
/// Server address (default: $ORK_ADDR or tcp://localhost:5640)
|
||||
#[arg(long, short, global = true)]
|
||||
addr: Option<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Parse and display document information
|
||||
Info {
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
/// List a directory in the namespace
|
||||
Ls {
|
||||
/// Namespace path (default: /)
|
||||
#[arg(default_value = "/")]
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// List sections/headlines
|
||||
/// Read a file from the namespace to stdout
|
||||
Read {
|
||||
/// Namespace path
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// Write to a file (DATA argument, or stdin if omitted)
|
||||
Write {
|
||||
/// Namespace path
|
||||
path: String,
|
||||
/// Data to write; if omitted, read from stdin
|
||||
data: Option<String>,
|
||||
},
|
||||
|
||||
/// Write then read a request/response file (e.g. ctl, new)
|
||||
Rdwr {
|
||||
/// Namespace path
|
||||
path: String,
|
||||
/// Data to write; if omitted, read from stdin
|
||||
data: Option<String>,
|
||||
},
|
||||
|
||||
/// List documents on the server
|
||||
Docs,
|
||||
|
||||
/// List top-level sections of a document
|
||||
Sections {
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Filter by tag
|
||||
#[arg(long, short)]
|
||||
tag: Option<String>,
|
||||
/// Filter by TODO state (TODO, DONE, or custom)
|
||||
#[arg(long)]
|
||||
todo: Option<String>,
|
||||
/// Maximum depth to display (default: all)
|
||||
#[arg(long, short)]
|
||||
depth: Option<u8>,
|
||||
/// Only show TODO items
|
||||
#[arg(long)]
|
||||
todos_only: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
/// Document name (as shown by `docs`)
|
||||
doc: String,
|
||||
},
|
||||
|
||||
/// List all tags across files
|
||||
Tags {
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
/// List TODO items across all documents
|
||||
Todos {
|
||||
/// Only show TODOs from this document
|
||||
doc: Option<String>,
|
||||
},
|
||||
|
||||
/// Query sections by various criteria
|
||||
Query {
|
||||
/// Query expression (tag:work, todo:TODO, priority:A, file:pattern, etc.)
|
||||
query: String,
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
},
|
||||
|
||||
/// Get a specific section by ID
|
||||
Get {
|
||||
/// Section identifier (CUSTOM_ID, ID, or headline text)
|
||||
id: String,
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Include body content
|
||||
#[arg(long, short)]
|
||||
body: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
},
|
||||
|
||||
/// Export as JSON
|
||||
Export {
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Section ID to export (default: all)
|
||||
#[arg(long)]
|
||||
section: Option<String>,
|
||||
/// Pretty print JSON
|
||||
#[arg(long, short)]
|
||||
pretty: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
},
|
||||
|
||||
/// Toggle TODO state of a section
|
||||
Toggle {
|
||||
/// Section identifier (searches all files)
|
||||
id: String,
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Dry run - show what would change
|
||||
#[arg(long, short = 'n')]
|
||||
dry_run: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
},
|
||||
|
||||
/// Add a new section
|
||||
Add {
|
||||
/// Headline title
|
||||
title: String,
|
||||
/// Target file (required for directory paths)
|
||||
#[arg(long, short)]
|
||||
file: Option<PathBuf>,
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Headline level (1-6)
|
||||
#[arg(long, short, default_value = "1")]
|
||||
level: u8,
|
||||
/// TODO keyword
|
||||
#[arg(long)]
|
||||
todo: Option<String>,
|
||||
/// Priority (A, B, C)
|
||||
#[arg(long)]
|
||||
priority: Option<char>,
|
||||
/// Tags (comma-separated)
|
||||
#[arg(long, short)]
|
||||
tags: Option<String>,
|
||||
/// Body content
|
||||
#[arg(long, short)]
|
||||
body: Option<String>,
|
||||
/// Insert after section with this ID
|
||||
#[arg(long)]
|
||||
after: Option<String>,
|
||||
/// Insert as child of section with this ID
|
||||
#[arg(long)]
|
||||
parent: Option<String>,
|
||||
/// Dry run - show what would be added
|
||||
#[arg(long, short = 'n')]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Set or update a property
|
||||
Property {
|
||||
/// Section identifier
|
||||
id: String,
|
||||
/// Property name
|
||||
name: String,
|
||||
/// Property value (omit to delete)
|
||||
value: Option<String>,
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Dry run
|
||||
#[arg(long, short = 'n')]
|
||||
dry_run: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
},
|
||||
|
||||
/// Set or add a tag
|
||||
Tag {
|
||||
/// Section identifier
|
||||
id: String,
|
||||
/// Tag to add/remove
|
||||
tag: String,
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Remove the tag instead of adding
|
||||
#[arg(long, short)]
|
||||
remove: bool,
|
||||
/// Dry run
|
||||
#[arg(long, short = 'n')]
|
||||
dry_run: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
},
|
||||
|
||||
/// Validate org file syntax
|
||||
Check {
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
},
|
||||
|
||||
/// Output agenda-relevant items
|
||||
/// Show an agenda view
|
||||
Agenda {
|
||||
/// Path to org directory or file
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Number of days to show (default: 7)
|
||||
#[arg(long, short, default_value = "7")]
|
||||
days: u32,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Include done items
|
||||
#[arg(long)]
|
||||
include_done: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
/// View: today, week, or todos (default: todos)
|
||||
#[arg(default_value = "todos")]
|
||||
view: String,
|
||||
},
|
||||
|
||||
/// List org files in directory
|
||||
Files {
|
||||
/// Path to org directory
|
||||
#[arg(default_value = ".")]
|
||||
path: PathBuf,
|
||||
/// Output as JSON
|
||||
#[arg(long, short)]
|
||||
json: bool,
|
||||
/// Recurse into subdirectories
|
||||
#[arg(long, short = 'R')]
|
||||
recursive: bool,
|
||||
/// Show a section's fields
|
||||
Get {
|
||||
/// Document name
|
||||
doc: String,
|
||||
/// Section id (slug or UUID, as shown by `sections`)
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Set a field on a section
|
||||
Set {
|
||||
/// Document name
|
||||
doc: String,
|
||||
/// Section id
|
||||
id: String,
|
||||
/// Field: title, keyword, priority, tags, scheduled, deadline, closed, properties, body
|
||||
field: String,
|
||||
/// New value
|
||||
value: String,
|
||||
},
|
||||
|
||||
/// Add a new section to a document
|
||||
Add {
|
||||
/// Document name
|
||||
doc: String,
|
||||
/// Headline text, e.g. "* TODO Write docs :work:"
|
||||
headline: String,
|
||||
/// Add as a child of this section id instead of top-level
|
||||
#[arg(long, short)]
|
||||
parent: Option<String>,
|
||||
},
|
||||
|
||||
/// Create a new document
|
||||
Mkdoc {
|
||||
/// Document name (without .org)
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Ask the server to rescan org files from disk
|
||||
Reload,
|
||||
}
|
||||
|
||||
/// Section fields that may be written via `set`.
|
||||
const SETTABLE_FIELDS: &[&str] = &[
|
||||
"title", "keyword", "priority", "tags", "scheduled", "deadline", "closed",
|
||||
"properties", "body",
|
||||
];
|
||||
|
||||
/// Fields shown by `get`, in display order (read-only "headline" first).
|
||||
const GET_FIELDS: &[&str] = &[
|
||||
"headline", "keyword", "priority", "tags", "scheduled", "deadline", "closed",
|
||||
"properties", "body",
|
||||
];
|
||||
|
||||
/// Entries under a document dir that are not sections.
|
||||
const DOC_FILES: &[&str] = &["raw", "body", "meta", "new"];
|
||||
|
||||
/// Resolve the server address: explicit --addr, then $ORK_ADDR, then default.
|
||||
fn resolve_addr(addr: Option<&str>) -> String {
|
||||
addr.map(str::to_string)
|
||||
.or_else(|| std::env::var("ORK_ADDR").ok())
|
||||
.unwrap_or_else(|| "tcp://localhost:5640".to_string())
|
||||
}
|
||||
|
||||
/// Read stdin to end (used when a write/rdwr DATA argument is omitted).
|
||||
fn read_stdin() -> io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
io::stdin().read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Read a namespace file and return its contents as a trimmed string.
|
||||
fn read_str(client: &mut Client, path: &str) -> io::Result<String> {
|
||||
let data = client.read(path)?;
|
||||
Ok(String::from_utf8_lossy(&data).trim_end().to_string())
|
||||
}
|
||||
|
||||
// ── Friendly commands (pure 9P compositions) ───────────────────────────────
|
||||
|
||||
/// `docs` — list documents from /idx as "name title".
|
||||
fn cmd_docs(client: &mut Client) -> io::Result<()> {
|
||||
let idx = read_str(client, "/idx")?;
|
||||
let mut out = io::stdout().lock();
|
||||
for line in idx.lines().filter(|l| !l.is_empty()) {
|
||||
// idx format: name\tpath\ttitle
|
||||
let mut cols = line.splitn(3, '\t');
|
||||
let name = cols.next().unwrap_or("");
|
||||
let _path = cols.next().unwrap_or("");
|
||||
let title = cols.next().unwrap_or("");
|
||||
writeln!(out, "{:<20} {}", name, title)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `sections <doc>` — list top-level sections with their headlines.
|
||||
fn cmd_sections(client: &mut Client, doc: &str) -> io::Result<()> {
|
||||
let entries = client.list(&format!("/{}", doc))?;
|
||||
let ids: Vec<String> = entries
|
||||
.into_iter()
|
||||
.filter(|e| !DOC_FILES.contains(&e.as_str()))
|
||||
.collect();
|
||||
let mut out = io::stdout().lock();
|
||||
if ids.is_empty() {
|
||||
writeln!(out, "(no sections)")?;
|
||||
return Ok(());
|
||||
}
|
||||
for id in ids {
|
||||
let headline = read_str(client, &format!("/{}/{}/headline", doc, id))
|
||||
.unwrap_or_default();
|
||||
writeln!(out, "{:<28} {}", id, headline)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `todos [doc]` — read /agenda/todos and optionally filter by document.
|
||||
fn cmd_todos(client: &mut Client, doc: Option<&str>) -> io::Result<()> {
|
||||
let todos = read_str(client, "/agenda/todos")?;
|
||||
let mut out = io::stdout().lock();
|
||||
for line in todos.lines().filter(|l| !l.is_empty()) {
|
||||
// agenda format: id\tdoc\ttitle\tkeyword
|
||||
let cols: Vec<&str> = line.splitn(4, '\t').collect();
|
||||
if cols.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
let (id, ldoc, title, kw) = (cols[0], cols[1], cols[2], cols[3]);
|
||||
if let Some(want) = doc {
|
||||
if ldoc != want {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
writeln!(out, "{:<10} {:<12} {:<28} {}", kw, ldoc, id, title)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `agenda [view]` — read /agenda/<view> verbatim.
|
||||
fn cmd_agenda(client: &mut Client, view: &str) -> io::Result<()> {
|
||||
match view {
|
||||
"today" | "week" | "todos" => {}
|
||||
other => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unknown agenda view '{}' (use today, week, or todos)", other),
|
||||
));
|
||||
}
|
||||
}
|
||||
let data = read_str(client, &format!("/agenda/{}", view))?;
|
||||
let mut out = io::stdout().lock();
|
||||
if data.is_empty() {
|
||||
writeln!(out, "(empty)")?;
|
||||
} else {
|
||||
writeln!(out, "{}", data)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `get <doc> <id>` — print a section's non-empty fields.
|
||||
fn cmd_get(client: &mut Client, doc: &str, id: &str) -> io::Result<()> {
|
||||
// Confirm the section exists by walking its headline first.
|
||||
let base = format!("/{}/{}", doc, id);
|
||||
let headline = read_str(client, &format!("{}/headline", base))
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::NotFound,
|
||||
format!("section '{}' not found in '{}'", id, doc)))?;
|
||||
let mut out = io::stdout().lock();
|
||||
writeln!(out, "{}", headline)?;
|
||||
for field in GET_FIELDS {
|
||||
if *field == "headline" {
|
||||
continue;
|
||||
}
|
||||
let val = read_str(client, &format!("{}/{}", base, field)).unwrap_or_default();
|
||||
if !val.is_empty() {
|
||||
writeln!(out, " {:<11} {}", format!("{}:", field), val)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `set <doc> <id> <field> <value>` — write one section field.
|
||||
fn cmd_set(client: &mut Client, doc: &str, id: &str, field: &str, value: &str) -> io::Result<()> {
|
||||
if !SETTABLE_FIELDS.contains(&field) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("cannot set '{}'; settable fields: {}", field, SETTABLE_FIELDS.join(", ")),
|
||||
));
|
||||
}
|
||||
client.write(&format!("/{}/{}/{}", doc, id, field), value.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `add <doc> <headline> [--parent id]` — create a section, print its UUID.
|
||||
fn cmd_add(client: &mut Client, doc: &str, headline: &str, parent: Option<&str>) -> io::Result<()> {
|
||||
let path = match parent {
|
||||
Some(pid) => format!("/{}/{}/new", doc, pid),
|
||||
None => format!("/{}/new", doc),
|
||||
};
|
||||
let resp = client.rdwr(&path, headline.as_bytes())?;
|
||||
io::stdout().write_all(&resp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `mkdoc <name>` — create a new document, print its filename.
|
||||
fn cmd_mkdoc(client: &mut Client, name: &str) -> io::Result<()> {
|
||||
let resp = client.rdwr("/new", name.as_bytes())?;
|
||||
io::stdout().write_all(&resp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `reload` — ask the server to rescan.
|
||||
fn cmd_reload(client: &mut Client) -> io::Result<()> {
|
||||
let resp = client.rdwr("/ctl", b"reload")?;
|
||||
io::stdout().write_all(&resp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> io::Result<()> {
|
||||
let addr = resolve_addr(cli.addr.as_deref());
|
||||
let mut client = Client::connect(&addr)
|
||||
.map_err(|e| io::Error::new(e.kind(), format!("connect {}: {}", addr, e)))?;
|
||||
|
||||
match cli.command {
|
||||
Commands::Ls { path } => {
|
||||
let mut out = io::stdout().lock();
|
||||
for name in client.list(&path)? {
|
||||
writeln!(out, "{}", name)?;
|
||||
}
|
||||
}
|
||||
Commands::Read { path } => {
|
||||
let data = client.read(&path)?;
|
||||
io::stdout().write_all(&data)?;
|
||||
}
|
||||
Commands::Write { path, data } => {
|
||||
let bytes = match data {
|
||||
Some(s) => s.into_bytes(),
|
||||
None => read_stdin()?,
|
||||
};
|
||||
client.write(&path, &bytes)?;
|
||||
}
|
||||
Commands::Rdwr { path, data } => {
|
||||
let bytes = match data {
|
||||
Some(s) => s.into_bytes(),
|
||||
None => read_stdin()?,
|
||||
};
|
||||
let resp = client.rdwr(&path, &bytes)?;
|
||||
io::stdout().write_all(&resp)?;
|
||||
}
|
||||
Commands::Docs => cmd_docs(&mut client)?,
|
||||
Commands::Sections { doc } => cmd_sections(&mut client, &doc)?,
|
||||
Commands::Todos { doc } => cmd_todos(&mut client, doc.as_deref())?,
|
||||
Commands::Agenda { view } => cmd_agenda(&mut client, &view)?,
|
||||
Commands::Get { doc, id } => cmd_get(&mut client, &doc, &id)?,
|
||||
Commands::Set { doc, id, field, value } => {
|
||||
cmd_set(&mut client, &doc, &id, &field, &value)?
|
||||
}
|
||||
Commands::Add { doc, headline, parent } => {
|
||||
cmd_add(&mut client, &doc, &headline, parent.as_deref())?
|
||||
}
|
||||
Commands::Mkdoc { name } => cmd_mkdoc(&mut client, &name)?,
|
||||
Commands::Reload => cmd_reload(&mut client)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::Info { path, json, recursive } => {
|
||||
commands::info(&path, json, recursive)
|
||||
if let Err(e) = run(cli) {
|
||||
// A downstream reader closing early (e.g. `| head`) is not an error.
|
||||
if e.kind() == io::ErrorKind::BrokenPipe {
|
||||
process::exit(0);
|
||||
}
|
||||
Commands::Sections {
|
||||
path,
|
||||
json,
|
||||
tag,
|
||||
todo,
|
||||
depth,
|
||||
todos_only,
|
||||
recursive,
|
||||
} => commands::sections(&path, json, tag, todo, depth, todos_only, recursive),
|
||||
Commands::Tags { path, json, recursive } => {
|
||||
commands::tags(&path, json, recursive)
|
||||
}
|
||||
Commands::Query { query, path, json, recursive } => {
|
||||
commands::query(&path, &query, json, recursive)
|
||||
}
|
||||
Commands::Get { id, path, json, body, recursive } => {
|
||||
commands::get(&path, &id, json, body, recursive)
|
||||
}
|
||||
Commands::Export { path, section, pretty, recursive } => {
|
||||
commands::export(&path, section.as_deref(), pretty, recursive)
|
||||
}
|
||||
Commands::Toggle { id, path, dry_run, recursive } => {
|
||||
commands::toggle(&path, &id, dry_run, recursive)
|
||||
}
|
||||
Commands::Add {
|
||||
title,
|
||||
file,
|
||||
path,
|
||||
level,
|
||||
todo,
|
||||
priority,
|
||||
tags,
|
||||
body,
|
||||
after,
|
||||
parent,
|
||||
dry_run,
|
||||
} => commands::add(
|
||||
&path,
|
||||
file.as_deref(),
|
||||
level,
|
||||
&title,
|
||||
todo.as_deref(),
|
||||
priority,
|
||||
tags.as_deref(),
|
||||
body.as_deref(),
|
||||
after.as_deref(),
|
||||
parent.as_deref(),
|
||||
dry_run,
|
||||
),
|
||||
Commands::Property {
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
path,
|
||||
dry_run,
|
||||
recursive,
|
||||
} => commands::property(&path, &id, &name, value.as_deref(), dry_run, recursive),
|
||||
Commands::Tag {
|
||||
id,
|
||||
tag,
|
||||
path,
|
||||
remove,
|
||||
dry_run,
|
||||
recursive,
|
||||
} => commands::tag(&path, &id, &tag, remove, dry_run, recursive),
|
||||
Commands::Check { path, json, recursive } => {
|
||||
commands::check(&path, json, recursive)
|
||||
}
|
||||
Commands::Agenda {
|
||||
path,
|
||||
days,
|
||||
json,
|
||||
include_done,
|
||||
recursive,
|
||||
} => commands::agenda(&path, days, json, include_done, recursive),
|
||||
Commands::Files { path, json, recursive } => {
|
||||
commands::files(&path, json, recursive)
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("error: {}", e);
|
||||
std::process::exit(1);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue