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>
This commit is contained in:
parent
6bb2994dff
commit
96f89acd65
|
|
@ -156,80 +156,142 @@ fn build_section_node(state: Arc<OrkState>, doc_name: &str, section_id: &str) ->
|
|||
let doc = doc_name.to_string();
|
||||
let id = section_id.to_string();
|
||||
|
||||
let s1 = state.clone();
|
||||
let s2 = state.clone();
|
||||
let s3 = state.clone();
|
||||
let s4 = state.clone();
|
||||
let s5 = state.clone();
|
||||
let s6 = state.clone();
|
||||
let s7 = state.clone();
|
||||
let s8 = state.clone();
|
||||
// Clone state for each closure
|
||||
let s_headline = state.clone();
|
||||
let s_title = state.clone();
|
||||
let s_keyword = state.clone();
|
||||
let s_priority = state.clone();
|
||||
let s_tags = state.clone();
|
||||
let s_level = state.clone();
|
||||
let s_scheduled = state.clone();
|
||||
let s_deadline = state.clone();
|
||||
let s_closed = state.clone();
|
||||
let s_properties = state.clone();
|
||||
let s_body = state.clone();
|
||||
let s_new = state.clone();
|
||||
let s_list = state.clone();
|
||||
let s_child = state.clone();
|
||||
|
||||
let d1 = doc.clone();
|
||||
let d2 = doc.clone();
|
||||
let d3 = doc.clone();
|
||||
let d4 = doc.clone();
|
||||
let d5 = doc.clone();
|
||||
let d6 = doc.clone();
|
||||
let d7 = doc.clone();
|
||||
let d8 = doc.clone();
|
||||
// Clone doc/id for each closure
|
||||
let d_headline = doc.clone();
|
||||
let d_title = doc.clone();
|
||||
let d_keyword = doc.clone();
|
||||
let d_priority = doc.clone();
|
||||
let d_tags = doc.clone();
|
||||
let d_level = doc.clone();
|
||||
let d_scheduled = doc.clone();
|
||||
let d_deadline = doc.clone();
|
||||
let d_closed = doc.clone();
|
||||
let d_properties = doc.clone();
|
||||
let d_body = doc.clone();
|
||||
let d_new = doc.clone();
|
||||
let d_list = doc.clone();
|
||||
let d_child = doc.clone();
|
||||
|
||||
let i1 = id.clone();
|
||||
let i2 = id.clone();
|
||||
let i3 = id.clone();
|
||||
let i4 = id.clone();
|
||||
let i5 = id.clone();
|
||||
let i6 = id.clone();
|
||||
let i8 = id.clone();
|
||||
let i_headline = id.clone();
|
||||
let i_title = id.clone();
|
||||
let i_keyword = id.clone();
|
||||
let i_priority = id.clone();
|
||||
let i_tags = id.clone();
|
||||
let i_level = id.clone();
|
||||
let i_scheduled = id.clone();
|
||||
let i_deadline = id.clone();
|
||||
let i_closed = id.clone();
|
||||
let i_properties = id.clone();
|
||||
let i_body = id.clone();
|
||||
let i_new = id.clone();
|
||||
let i_list = id.clone();
|
||||
|
||||
virtfs::dir_dynamic(&id,
|
||||
vec![
|
||||
// headline - full reconstructed headline
|
||||
virtfs::file("headline", 0o444, virtfs::read(move || {
|
||||
s1.section_headline(&d1, &i1)
|
||||
s_headline.section_headline(&d_headline, &i_headline)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "not found"))
|
||||
})),
|
||||
|
||||
// title
|
||||
virtfs::file("title", 0o644, virtfs::read(move || {
|
||||
s2.section_title(&d2, &i2)
|
||||
s_title.section_title(&d_title, &i_title)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "not found"))
|
||||
})),
|
||||
|
||||
// keyword (TODO/DONE/etc)
|
||||
virtfs::file("keyword", 0o644, virtfs::read(move || {
|
||||
Ok(s3.section_keyword(&d3, &i3)
|
||||
Ok(s_keyword.section_keyword(&d_keyword, &i_keyword)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
|
||||
// priority
|
||||
virtfs::file("priority", 0o644, virtfs::read(move || {
|
||||
Ok(s4.section_priority(&d4, &i4)
|
||||
Ok(s_priority.section_priority(&d_priority, &i_priority)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
|
||||
// tags
|
||||
virtfs::file("tags", 0o644, virtfs::read(move || {
|
||||
Ok(s5.section_tags(&d5, &i5)
|
||||
Ok(s_tags.section_tags(&d_tags, &i_tags)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
|
||||
// level
|
||||
virtfs::file("level", 0o444, virtfs::read(move || {
|
||||
s_level.section_level(&d_level, &i_level)
|
||||
.map(|l| format!("{}\n", l).into_bytes())
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "not found"))
|
||||
})),
|
||||
|
||||
// scheduled
|
||||
virtfs::file("scheduled", 0o644, virtfs::read(move || {
|
||||
Ok(s_scheduled.section_scheduled(&d_scheduled, &i_scheduled)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
|
||||
// deadline
|
||||
virtfs::file("deadline", 0o644, virtfs::read(move || {
|
||||
Ok(s_deadline.section_deadline(&d_deadline, &i_deadline)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
|
||||
// closed
|
||||
virtfs::file("closed", 0o644, virtfs::read(move || {
|
||||
Ok(s_closed.section_closed(&d_closed, &i_closed)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
|
||||
// properties
|
||||
virtfs::file("properties", 0o644, virtfs::read(move || {
|
||||
Ok(s_properties.section_properties(&d_properties, &i_properties)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
|
||||
// body - section content (not including children)
|
||||
virtfs::file("body", 0o644, virtfs::read(move || {
|
||||
Ok(s_body.section_body(&d_body, &i_body)
|
||||
.map(|s| s.into_bytes())
|
||||
.unwrap_or_else(|| b"".to_vec()))
|
||||
})),
|
||||
|
||||
// new - create child section
|
||||
virtfs::file("new", 0o222, virtfs::rdwr(move |data| {
|
||||
let headline = String::from_utf8_lossy(data);
|
||||
s8.create_child_section(&d8, &i8, &headline)
|
||||
s_new.create_child_section(&d_new, &i_new, &headline)
|
||||
.map(|uuid| format!("{}\n", uuid).into_bytes())
|
||||
})),
|
||||
],
|
||||
// children - nested sections
|
||||
move || Ok(s6.child_ids(&d6, &i6)),
|
||||
move || Ok(s_list.child_ids(&d_list, &i_list)),
|
||||
move |child_id| {
|
||||
Some(build_section_node(s7.clone(), &d7, child_id))
|
||||
Some(build_section_node(s_child.clone(), &d_child, child_id))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Server state: manages org documents and their parsed state.
|
||||
|
||||
use org_ast::Document;
|
||||
use org_ast::{Document, Timestamp, TimestampValue};
|
||||
use org_parser::Parser;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -9,6 +9,47 @@ use std::io::{self, Result};
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Format a timestamp for display.
|
||||
fn format_timestamp(ts: &Timestamp) -> String {
|
||||
match &ts.value {
|
||||
TimestampValue::Point(p) => {
|
||||
let mut s = format!("<{:04}-{:02}-{:02}", p.date.year, p.date.month, p.date.day);
|
||||
if let Some(day_name) = &p.date.day_name {
|
||||
s.push(' ');
|
||||
s.push_str(day_name);
|
||||
}
|
||||
if let Some(time) = &p.time {
|
||||
s.push_str(&format!(" {:02}:{:02}", time.hour, time.minute));
|
||||
}
|
||||
if let Some(r) = &p.repeater {
|
||||
let kind = match r.kind {
|
||||
org_ast::RepeaterKind::Cumulative => "+",
|
||||
org_ast::RepeaterKind::CatchUp => "++",
|
||||
org_ast::RepeaterKind::Restart => ".+",
|
||||
};
|
||||
let unit = match r.unit {
|
||||
org_ast::TimeUnit::Hour => "h",
|
||||
org_ast::TimeUnit::Day => "d",
|
||||
org_ast::TimeUnit::Week => "w",
|
||||
org_ast::TimeUnit::Month => "m",
|
||||
org_ast::TimeUnit::Year => "y",
|
||||
};
|
||||
s.push_str(&format!(" {}{}{}", kind, r.value, unit));
|
||||
}
|
||||
s.push('>');
|
||||
s
|
||||
}
|
||||
TimestampValue::Range { start, end } => {
|
||||
format!(
|
||||
"<{:04}-{:02}-{:02}>--<{:04}-{:02}-{:02}>",
|
||||
start.date.year, start.date.month, start.date.day,
|
||||
end.date.year, end.date.month, end.date.day
|
||||
)
|
||||
}
|
||||
TimestampValue::Diary(sexp) => sexp.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed document with metadata.
|
||||
pub struct DocState {
|
||||
pub path: PathBuf,
|
||||
|
|
@ -269,6 +310,79 @@ impl OrkState {
|
|||
self.find_section(&d.doc, id).map(|s| s.headline.tags.join(":"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section level.
|
||||
pub fn section_level(&self, doc_name: &str, id: &str) -> Option<u8> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id).map(|s| s.headline.level)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section scheduled timestamp.
|
||||
pub fn section_scheduled(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id)
|
||||
.and_then(|s| s.planning.as_ref())
|
||||
.and_then(|p| p.scheduled.as_ref())
|
||||
.map(|ts| format_timestamp(ts))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section deadline timestamp.
|
||||
pub fn section_deadline(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id)
|
||||
.and_then(|s| s.planning.as_ref())
|
||||
.and_then(|p| p.deadline.as_ref())
|
||||
.map(|ts| format_timestamp(ts))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section closed timestamp.
|
||||
pub fn section_closed(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id)
|
||||
.and_then(|s| s.planning.as_ref())
|
||||
.and_then(|p| p.closed.as_ref())
|
||||
.map(|ts| format_timestamp(ts))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section properties as key=value lines.
|
||||
pub fn section_properties(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id)
|
||||
.and_then(|s| s.properties.as_ref())
|
||||
.map(|props| {
|
||||
props.properties.iter()
|
||||
.map(|p| format!("{}={}", p.key, p.value))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section body content (text between headline and first child).
|
||||
pub fn section_body(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id).map(|s| {
|
||||
// Body is section content minus children
|
||||
// For now, extract from raw content using spans
|
||||
let start = s.headline.span.end.offset;
|
||||
let end = if let Some(first_child) = s.children.first() {
|
||||
first_child.headline.span.start.offset
|
||||
} else {
|
||||
s.span.end.offset
|
||||
};
|
||||
|
||||
if start < end && end <= d.content.len() {
|
||||
d.content[start..end].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ ork/
|
|||
├── ctl # rdwr: "reload", "quit", "rescan"
|
||||
├── idx # read: list of org files (name\tpath\ttitle)
|
||||
├── keywords # read: configured TODO keywords
|
||||
├── capture # write: append "* TODO text" to inbox.org
|
||||
├── new # rdwr: create org file, returns filename
|
||||
│
|
||||
├── agenda/ # computed views
|
||||
│ ├── today # read: items scheduled/deadline today
|
||||
|
|
@ -16,22 +16,25 @@ ork/
|
|||
│
|
||||
└── {doc}/ # e.g., "todo" for todo.org
|
||||
├── raw # read/write: full org file content
|
||||
├── body # read/write: full org file content (alias)
|
||||
├── meta # read: title, author, filetags (tsv)
|
||||
├── new # rdwr: create section, returns UUID
|
||||
│
|
||||
└── {h1-id}/ # section by ID (nested hierarchy)
|
||||
├── headline # read: full headline text "* TODO [#A] Title :tag:"
|
||||
├── title # read/write: plain title
|
||||
└── {uuid}/ # section by UUID
|
||||
├── headline # read: full headline "* TODO [#A] Title :tag:"
|
||||
├── title # read/write: plain title text
|
||||
├── keyword # read/write: TODO/DONE/etc or empty
|
||||
├── priority # read/write: A/B/C or empty
|
||||
├── tags # read/write: colon-separated "work:urgent"
|
||||
├── level # read: headline level (1-6)
|
||||
├── scheduled # read/write: "<2025-01-15 Wed>"
|
||||
├── deadline # read/write: "<2025-01-20 Mon>"
|
||||
├── closed # read/write: "<2025-01-21 Tue>"
|
||||
├── properties # read/write: "KEY=value\n" lines
|
||||
├── body # read/write: section body (content only, not children)
|
||||
├── body # read/write: section content (not children)
|
||||
├── new # rdwr: create child section, returns UUID
|
||||
│
|
||||
└── {h2-id}/ # nested children, same structure
|
||||
├── headline
|
||||
├── title
|
||||
└── {child-uuid}/ # nested children (same structure)
|
||||
└── ...
|
||||
```
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue