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).
This commit is contained in:
Levi Neely 2026-09-30 22:27:50 +02:00
parent 7ee65a4a2d
commit 33173bcc29
7 changed files with 38 additions and 377 deletions

View File

@ -14,8 +14,8 @@ Current state: **v0.1** — parser and CLI complete, no UI yet.
crates/
├── org-ast/ # AST types (Document, Section, Element, etc.)
├── org-parser/ # Tree-sitter parser, AST conversion
├── orkmode-core/ # High-level API, C FFI
└── ork-cli/ # CLI tool (bin name: ork)
├── ork-server/ # 9P server exposing org files (bin: ork-server)
└── ork-cli/ # CLI tool, a 9P client (bin name: ork)
```
## Working with the Code
@ -64,11 +64,11 @@ Use `-j/--json` for machine-readable output. Use `-n/--dry-run` for mutations.
- `Document` → `Section`* → `Element`* / `Section`*
- Inline objects (bold, links) are partially implemented
### FFI
### 9P server
- C-compatible FFI in `orkmode-core/src/ffi.rs`
- Functions prefixed `ork_*`
- Caller owns returned strings (free with `ork_string_free`)
- `ork-server` exposes org files as a 9P namespace (docs, sections, fields, agenda, query)
- Structural ops via the `ctl` file: rm, move, promote, demote, refile, archive, lint
- `ork` is a pure 9P client; all org logic lives in the server
## Conventions
@ -100,16 +100,16 @@ Use `-j/--json` for machine-readable output. Use `-n/--dry-run` for mutations.
3. **Timestamp text** — displays "set" placeholder, not actual date
4. **No incremental parsing** — full reparse on every call
## Next Steps (v0.2)
## Next Steps (v0.3)
Priority: Qt/QML read-only document viewer.
Priority: Qt/QML read-only document viewer, over 9P.
Tasks:
1. Set up Qt6/QML project in `ui/`
2. Link orkmode-core via C FFI
3. Implement document model (QAbstractItemModel)
2. Connect to `ork-server` over 9P (no FFI)
3. Implement document model (QAbstractItemModel) from the 9P namespace
4. Basic headline rendering with folding
5. Syntax highlighting
5. Structural styling (keywords, priorities, tags, timestamps)
## Testing Changes

37
Cargo.lock generated
View File

@ -346,12 +346,6 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.106"
@ -571,18 +565,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "orkmode-core"
version = "0.1.0"
dependencies = [
"org-ast",
"org-parser",
"pretty_assertions",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -754,19 +736,6 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -1197,9 +1166,3 @@ name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

View File

@ -3,7 +3,6 @@ resolver = "2"
members = [
"crates/org-parser",
"crates/org-ast",
"crates/orkmode-core",
"crates/ork-cli",
"crates/ork-server",
]

View File

@ -25,14 +25,17 @@ orkmode/
│ │ ├── convert.rs # Tree-sitter → AST conversion
│ │ ├── ts.rs # Tree-sitter language bindings
│ │ └── error.rs # Error types
│ ├── orkmode-core/ # Core library with FFI
│ ├── ork-cli/ # Command-line interface (9P client)
│ │ └── src/
│ │ ├── lib.rs # High-level API
│ │ └── ffi.rs # C FFI for Qt integration
│ └── ork-cli/ # Command-line interface
│ │ ├── main.rs # CLI commands (base + friendly verbs)
│ │ └── client.rs # 9P2000 client
│ └── ork-server/ # 9P server exposing org files
│ └── src/
│ ├── main.rs # CLI entry point
│ └── commands.rs # Command implementations
│ ├── bin/ork-server.rs # Server entry point
│ ├── state.rs # Document state, section ops, ctl verbs
│ ├── namespace.rs # 9P namespace (docs, sections, agenda, query)
│ ├── p9.rs # 9P protocol handling
│ └── virtfs.rs # Virtual filesystem tree
└── ui/ # (future) Qt/QML UI
```
@ -189,31 +192,28 @@ for section in doc.all_sections() {
}
```
### C/C++ FFI
### 9P server & client
```c
#include "orkmode.h"
Serve a directory of org files over 9P, then act on it with `ork`:
// Parse a document
OrkDocument* doc = ork_parse("* Hello World");
if (doc) {
// Get as JSON
char* json = ork_document_to_json(doc);
printf("%s\n", json);
ork_string_free(json);
// Get title
char* title = ork_document_title(doc);
if (title) {
printf("Title: %s\n", title);
ork_string_free(title);
}
// Cleanup
ork_document_free(doc);
}
```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
ork docs # list documents
ork tree work # outline of work.org
ork todos # TODO items across all docs
ork set work my-task keyword DONE
ork refile work my-task archive --parent done-items
ork query 'tag:work+todo:TODO'
```
Any 9P client works too (e.g. plan9port `9p`); the server owns all org
logic and reads/writes the real `.org` files.
## Architecture
### Parser Design

View File

@ -1,20 +0,0 @@
[package]
name = "orkmode-core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
description = "Core library for orkmode - org-mode implementation"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
org-ast = { path = "../org-ast" }
org-parser = { path = "../org-parser" }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }

View File

@ -1,185 +0,0 @@
//! FFI bindings for C/C++ integration.
//!
//! This module provides a C-compatible API for the orkmode parser,
//! enabling integration with Qt/C++ applications.
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use crate::{parse, parse_to_json_pretty};
/// Opaque handle to a parsed document.
pub struct OrkDocument {
doc: org_ast::Document,
_source: String, // Keep source alive for spans
}
/// Parse an org document from a C string.
///
/// Returns a handle to the parsed document, or null on error.
/// The caller must free the document with `ork_document_free`.
///
/// # Safety
/// - `source` must be a valid null-terminated UTF-8 string.
#[no_mangle]
pub unsafe extern "C" fn ork_parse(source: *const c_char) -> *mut OrkDocument {
if source.is_null() {
return ptr::null_mut();
}
let source_str = match CStr::from_ptr(source).to_str() {
Ok(s) => s.to_string(),
Err(_) => return ptr::null_mut(),
};
match parse(&source_str) {
Ok(doc) => Box::into_raw(Box::new(OrkDocument {
doc,
_source: source_str,
})),
Err(_) => ptr::null_mut(),
}
}
/// Free a parsed document.
///
/// # Safety
/// - `doc` must be a valid pointer returned by `ork_parse`, or null.
/// - After this call, `doc` must not be used.
#[no_mangle]
pub unsafe extern "C" fn ork_document_free(doc: *mut OrkDocument) {
if !doc.is_null() {
drop(Box::from_raw(doc));
}
}
/// Get the document as JSON.
///
/// Returns a newly allocated string that must be freed with `ork_string_free`.
/// Returns null on error.
///
/// # Safety
/// - `doc` must be a valid pointer returned by `ork_parse`.
#[no_mangle]
pub unsafe extern "C" fn ork_document_to_json(doc: *const OrkDocument) -> *mut c_char {
if doc.is_null() {
return ptr::null_mut();
}
let doc = &(*doc).doc;
match serde_json::to_string_pretty(doc) {
Ok(json) => match CString::new(json) {
Ok(cstr) => cstr.into_raw(),
Err(_) => ptr::null_mut(),
},
Err(_) => ptr::null_mut(),
}
}
/// Parse an org document and return JSON directly.
///
/// This is a convenience function that combines parsing and JSON serialization.
/// Returns a newly allocated string that must be freed with `ork_string_free`.
/// Returns null on error.
///
/// # Safety
/// - `source` must be a valid null-terminated UTF-8 string.
#[no_mangle]
pub unsafe extern "C" fn ork_parse_to_json(source: *const c_char) -> *mut c_char {
if source.is_null() {
return ptr::null_mut();
}
let source_str = match CStr::from_ptr(source).to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
match parse_to_json_pretty(source_str) {
Ok(json) => match CString::new(json) {
Ok(cstr) => cstr.into_raw(),
Err(_) => ptr::null_mut(),
},
Err(_) => ptr::null_mut(),
}
}
/// Free a string returned by orkmode functions.
///
/// # Safety
/// - `s` must be a valid pointer returned by an orkmode function, or null.
/// - After this call, `s` must not be used.
#[no_mangle]
pub unsafe extern "C" fn ork_string_free(s: *mut c_char) {
if !s.is_null() {
drop(CString::from_raw(s));
}
}
/// Get the document title.
///
/// Returns a newly allocated string that must be freed with `ork_string_free`.
/// Returns null if no title is set.
///
/// # Safety
/// - `doc` must be a valid pointer returned by `ork_parse`.
#[no_mangle]
pub unsafe extern "C" fn ork_document_title(doc: *const OrkDocument) -> *mut c_char {
if doc.is_null() {
return ptr::null_mut();
}
match (*doc).doc.title() {
Some(title) => match CString::new(title) {
Ok(cstr) => cstr.into_raw(),
Err(_) => ptr::null_mut(),
},
None => ptr::null_mut(),
}
}
/// Get the number of top-level sections.
///
/// # Safety
/// - `doc` must be a valid pointer returned by `ork_parse`.
#[no_mangle]
pub unsafe extern "C" fn ork_document_section_count(doc: *const OrkDocument) -> usize {
if doc.is_null() {
return 0;
}
(*doc).doc.sections.len()
}
/// Get the total number of sections (including nested).
///
/// # Safety
/// - `doc` must be a valid pointer returned by `ork_parse`.
#[no_mangle]
pub unsafe extern "C" fn ork_document_total_section_count(doc: *const OrkDocument) -> usize {
if doc.is_null() {
return 0;
}
(*doc).doc.all_sections().count()
}
/// Get the number of TODO items.
///
/// # Safety
/// - `doc` must be a valid pointer returned by `ork_parse`.
#[no_mangle]
pub unsafe extern "C" fn ork_document_todo_count(doc: *const OrkDocument) -> usize {
if doc.is_null() {
return 0;
}
(*doc).doc.todos().count()
}
// Version info
/// Get the library version as a string.
#[no_mangle]
pub extern "C" fn ork_version() -> *const c_char {
// This is a static string, so it doesn't need to be freed.
static VERSION: &[u8] = b"0.1.0\0";
VERSION.as_ptr() as *const c_char
}

View File

@ -1,96 +0,0 @@
//! Orkmode Core Library
//!
//! This crate provides the core functionality for orkmode, including:
//! - Document parsing and AST manipulation
//! - FFI bindings for C++/Qt integration
//! - Serialization for data exchange
pub use org_ast;
pub use org_parser;
pub mod ffi;
use org_ast::Document;
use org_parser::{Parser, ParseError};
/// Parse an org document from a string.
pub fn parse(source: &str) -> Result<Document, ParseError> {
let mut parser = Parser::new();
parser.parse(source)
}
/// Parse an org document and return it as JSON.
pub fn parse_to_json(source: &str) -> Result<String, String> {
let doc = parse(source).map_err(|e| e.to_string())?;
serde_json::to_string(&doc).map_err(|e| e.to_string())
}
/// Parse an org document and return it as pretty-printed JSON.
pub fn parse_to_json_pretty(source: &str) -> Result<String, String> {
let doc = parse(source).map_err(|e| e.to_string())?;
serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple() {
let doc = parse("* Hello World\nSome content").unwrap();
assert_eq!(doc.sections.len(), 1);
assert_eq!(doc.sections[0].headline.title_text(), "Hello World");
}
#[test]
fn test_parse_to_json() {
let json = parse_to_json("* Test").unwrap();
assert!(json.contains("\"sections\""));
assert!(json.contains("Test"));
}
#[test]
fn test_complex_document() {
let source = r#"#+TITLE: Test Document
#+TODO: TODO WAITING | DONE CANCELLED
* TODO [#A] First Task :work:urgent:
DEADLINE: <2025-01-15 Wed 14:00>
:PROPERTIES:
:CUSTOM_ID: task-1
:END:
Some description here.
** Subtask
- [ ] Item 1
- [X] Item 2
* DONE Second Task
CLOSED: [2025-01-14 Tue 10:00]
#+BEGIN_SRC python
def hello():
print("Hello, World!")
#+END_SRC
"#;
let doc = parse(source).unwrap();
assert_eq!(doc.settings.title, Some("Test Document".to_string()));
assert_eq!(doc.sections.len(), 2);
// First section
let first = &doc.sections[0];
assert!(first.is_todo());
assert!(first.has_tag("work"));
assert!(first.has_tag("urgent"));
assert_eq!(first.headline.priority, Some(org_ast::Priority::A));
// Check children
assert_eq!(first.children.len(), 1);
// Second section
let second = &doc.sections[1];
assert!(second.is_done());
}
}