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
This commit is contained in:
parent
0c5f56aad9
commit
246bd9bb5a
|
|
@ -2,6 +2,21 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
|
|
@ -47,7 +62,7 @@ version = "1.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -58,9 +73,54 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.5.1"
|
||||
|
|
@ -71,6 +131,12 @@ dependencies = [
|
|||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
|
|
@ -102,7 +168,7 @@ dependencies = [
|
|||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -117,24 +183,133 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "failure"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"failure_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "failure_derive"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
|
|
@ -147,12 +322,162 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nine"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7eca0c850b4d39133ee5a1c0939027b0ef5c27c1927d90df3ce796c7ebdf49"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"byteorder",
|
||||
"failure",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
|
|
@ -192,6 +517,23 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ork-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nine",
|
||||
"notify",
|
||||
"org-ast",
|
||||
"org-parser",
|
||||
"parking_lot",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orkmode-core"
|
||||
version = "0.1.0"
|
||||
|
|
@ -204,6 +546,35 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
|
|
@ -232,6 +603,21 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
|
|
@ -261,6 +647,40 @@ version = "0.8.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
|
|
@ -288,7 +708,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -304,18 +724,75 @@ dependencies = [
|
|||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
|
|
@ -327,6 +804,31 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
|
|
@ -344,7 +846,105 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio 1.2.3",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -363,18 +963,64 @@ version = "1.0.26"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
|
@ -384,6 +1030,63 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ members = [
|
|||
"crates/org-ast",
|
||||
"crates/orkmode-core",
|
||||
"crates/ork-cli",
|
||||
"crates/ork-server",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "ork-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "9P server for orkmode"
|
||||
|
||||
[dependencies]
|
||||
org-ast = { path = "../org-ast" }
|
||||
org-parser = { path = "../org-parser" }
|
||||
|
||||
# 9P protocol
|
||||
nine = "0.2"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# Error handling
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# File watching
|
||||
notify = "6"
|
||||
|
||||
# Utilities
|
||||
parking_lot = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
//! Orkmode 9P server.
|
||||
//!
|
||||
//! Exposes org documents via 9P filesystem protocol.
|
||||
|
||||
pub mod virtfs;
|
||||
pub mod state;
|
||||
pub mod namespace;
|
||||
|
||||
pub use state::OrkState;
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
//! Build the 9P namespace from server state.
|
||||
|
||||
use crate::state::OrkState;
|
||||
use crate::virtfs::{self, FsNode};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Build the orkmode 9P namespace.
|
||||
pub fn build_namespace(state: Arc<OrkState>) -> FsNode {
|
||||
let s = state.clone();
|
||||
let s2 = state.clone();
|
||||
|
||||
// Root directory with static files + dynamic doc directories
|
||||
virtfs::dir_dynamic("/",
|
||||
vec![
|
||||
// ctl - control commands
|
||||
virtfs::file("ctl", 0o222, virtfs::rdwr({
|
||||
let state = state.clone();
|
||||
move |data| {
|
||||
let cmd = String::from_utf8_lossy(data);
|
||||
state.handle_ctl(&cmd)
|
||||
.map(|s| s.into_bytes())
|
||||
}
|
||||
})),
|
||||
|
||||
// idx - document index
|
||||
virtfs::file("idx", 0o444, virtfs::read({
|
||||
let state = state.clone();
|
||||
move || Ok(state.doc_index().into_bytes())
|
||||
})),
|
||||
|
||||
// keywords - TODO keywords
|
||||
virtfs::file("keywords", 0o444, virtfs::read({
|
||||
let state = state.clone();
|
||||
move || Ok(state.keywords().into_bytes())
|
||||
})),
|
||||
|
||||
// agenda - computed views
|
||||
virtfs::dir("agenda", vec![
|
||||
virtfs::file("todos", 0o444, virtfs::read({
|
||||
let state = state.clone();
|
||||
move || Ok(build_todos_view(&state).into_bytes())
|
||||
})),
|
||||
virtfs::file("today", 0o444, virtfs::read({
|
||||
let state = state.clone();
|
||||
move || Ok(build_today_view(&state).into_bytes())
|
||||
})),
|
||||
virtfs::file("week", 0o444, virtfs::read({
|
||||
let state = state.clone();
|
||||
move || Ok(build_week_view(&state).into_bytes())
|
||||
})),
|
||||
]),
|
||||
],
|
||||
// Dynamic children: document directories
|
||||
move || Ok(s.doc_names()),
|
||||
move |name| {
|
||||
let doc_name = name.to_string();
|
||||
Some(build_doc_node(s2.clone(), &doc_name))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a document node.
|
||||
fn build_doc_node(state: Arc<OrkState>, doc_name: &str) -> FsNode {
|
||||
let name = doc_name.to_string();
|
||||
let s = state.clone();
|
||||
let s2 = state.clone();
|
||||
let n = name.clone();
|
||||
let n2 = name.clone();
|
||||
|
||||
virtfs::dir_dynamic(&name,
|
||||
vec![
|
||||
// raw - full org content
|
||||
virtfs::file("raw", 0o644, virtfs::rw(
|
||||
{
|
||||
let state = state.clone();
|
||||
let name = name.clone();
|
||||
move || {
|
||||
state.doc_raw(&name)
|
||||
.map(|s| s.into_bytes())
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "not found"))
|
||||
}
|
||||
},
|
||||
{
|
||||
let state = state.clone();
|
||||
let name = name.clone();
|
||||
move |data| {
|
||||
let content = String::from_utf8_lossy(data);
|
||||
state.write_doc_raw(&name, &content)
|
||||
}
|
||||
},
|
||||
)),
|
||||
|
||||
// meta - document metadata
|
||||
virtfs::file("meta", 0o444, virtfs::read({
|
||||
let state = state.clone();
|
||||
let name = name.clone();
|
||||
move || {
|
||||
state.doc_meta(&name)
|
||||
.map(|s| s.into_bytes())
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "not found"))
|
||||
}
|
||||
})),
|
||||
],
|
||||
// Dynamic section directories
|
||||
move || Ok(s.section_ids(&n)),
|
||||
move |id| {
|
||||
Some(build_section_node(s2.clone(), &n2, id))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a section node (recursive for children).
|
||||
fn build_section_node(state: Arc<OrkState>, doc_name: &str, section_id: &str) -> FsNode {
|
||||
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 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 i1 = id.clone();
|
||||
let i2 = id.clone();
|
||||
let i3 = id.clone();
|
||||
let i4 = id.clone();
|
||||
let i5 = id.clone();
|
||||
let i6 = id.clone();
|
||||
|
||||
virtfs::dir_dynamic(&id,
|
||||
vec![
|
||||
// headline - full reconstructed headline
|
||||
virtfs::file("headline", 0o444, virtfs::read(move || {
|
||||
s1.section_headline(&d1, &i1)
|
||||
.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)
|
||||
.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)
|
||||
.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)
|
||||
.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)
|
||||
.map(|s| format!("{}\n", s).into_bytes())
|
||||
.unwrap_or_else(|| b"\n".to_vec()))
|
||||
})),
|
||||
],
|
||||
// children - nested sections
|
||||
move || Ok(s6.child_ids(&d6, &i6)),
|
||||
move |child_id| {
|
||||
Some(build_section_node(s7.clone(), &d7, child_id))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Agenda Views
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
fn build_todos_view(state: &OrkState) -> String {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for doc_name in state.doc_names() {
|
||||
if let Some(doc_state) = state.get_doc(&doc_name) {
|
||||
for section in doc_state.doc.all_sections() {
|
||||
if section.is_todo() {
|
||||
let id = if let Some(cid) = section.custom_id() {
|
||||
cid.to_string()
|
||||
} else {
|
||||
section.headline.title_text().to_lowercase().replace(' ', "-")
|
||||
};
|
||||
let kw = section.headline.keyword.as_ref()
|
||||
.map(|k| k.keyword.as_str())
|
||||
.unwrap_or("");
|
||||
lines.push(format!("{}\t{}\t{}\t{}",
|
||||
id, doc_name, section.headline.title_text(), kw));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.sort();
|
||||
lines.join("\n") + "\n"
|
||||
}
|
||||
|
||||
fn build_today_view(state: &OrkState) -> String {
|
||||
// TODO: implement with actual date filtering
|
||||
build_todos_view(state)
|
||||
}
|
||||
|
||||
fn build_week_view(state: &OrkState) -> String {
|
||||
// TODO: implement with actual date filtering
|
||||
build_todos_view(state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_namespace_structure() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::write(dir.path().join("test.org"), "#+TITLE: Test\n* TODO Task\n").unwrap();
|
||||
|
||||
let state = Arc::new(OrkState::new(dir.path()).unwrap());
|
||||
let root = build_namespace(state);
|
||||
|
||||
// Check root files
|
||||
assert!(root.walk("ctl").is_some());
|
||||
assert!(root.walk("idx").is_some());
|
||||
assert!(root.walk("keywords").is_some());
|
||||
assert!(root.walk("agenda").is_some());
|
||||
assert!(root.walk("agenda/todos").is_some());
|
||||
|
||||
// Check document
|
||||
assert!(root.walk("test").is_some());
|
||||
assert!(root.walk("test/raw").is_some());
|
||||
assert!(root.walk("test/meta").is_some());
|
||||
|
||||
// Check section
|
||||
assert!(root.walk("test/task").is_some());
|
||||
assert!(root.walk("test/task/title").is_some());
|
||||
assert!(root.walk("test/task/keyword").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_files() {
|
||||
let dir = tempdir().unwrap();
|
||||
fs::write(dir.path().join("work.org"),
|
||||
"#+TITLE: Work Tasks\n* TODO [#A] Important :urgent:\nSome body text\n").unwrap();
|
||||
|
||||
let state = Arc::new(OrkState::new(dir.path()).unwrap());
|
||||
let root = build_namespace(state);
|
||||
|
||||
// Read idx
|
||||
let idx = root.walk("idx").unwrap();
|
||||
let content = String::from_utf8(idx.read().unwrap()).unwrap();
|
||||
assert!(content.contains("work"));
|
||||
assert!(content.contains("Work Tasks"));
|
||||
|
||||
// Read section title
|
||||
let title = root.walk("work/important/title").unwrap();
|
||||
let content = String::from_utf8(title.read().unwrap()).unwrap();
|
||||
assert_eq!(content.trim(), "Important");
|
||||
|
||||
// Read keyword
|
||||
let kw = root.walk("work/important/keyword").unwrap();
|
||||
let content = String::from_utf8(kw.read().unwrap()).unwrap();
|
||||
assert_eq!(content.trim(), "TODO");
|
||||
|
||||
// Read priority
|
||||
let pri = root.walk("work/important/priority").unwrap();
|
||||
let content = String::from_utf8(pri.read().unwrap()).unwrap();
|
||||
assert_eq!(content.trim(), "A");
|
||||
|
||||
// Read tags
|
||||
let tags = root.walk("work/important/tags").unwrap();
|
||||
let content = String::from_utf8(tags.read().unwrap()).unwrap();
|
||||
assert_eq!(content.trim(), "urgent");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
//! Server state: manages org documents and their parsed state.
|
||||
|
||||
use org_ast::Document;
|
||||
use org_parser::Parser;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{self, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Parsed document with metadata.
|
||||
pub struct DocState {
|
||||
pub path: PathBuf,
|
||||
pub content: String,
|
||||
pub doc: Document,
|
||||
}
|
||||
|
||||
/// Server state: all loaded documents.
|
||||
pub struct OrkState {
|
||||
/// Directory containing org files.
|
||||
pub org_dir: PathBuf,
|
||||
|
||||
/// Parsed documents by name (without .org extension).
|
||||
docs: RwLock<HashMap<String, Arc<DocState>>>,
|
||||
|
||||
/// Parser instance.
|
||||
parser: RwLock<Parser>,
|
||||
}
|
||||
|
||||
impl OrkState {
|
||||
/// Create new state watching the given directory.
|
||||
pub fn new(org_dir: impl AsRef<Path>) -> Result<Self> {
|
||||
let org_dir = org_dir.as_ref().to_path_buf();
|
||||
if !org_dir.is_dir() {
|
||||
return Err(io::Error::new(io::ErrorKind::NotADirectory, "not a directory"));
|
||||
}
|
||||
|
||||
let state = Self {
|
||||
org_dir,
|
||||
docs: RwLock::new(HashMap::new()),
|
||||
parser: RwLock::new(Parser::new()),
|
||||
};
|
||||
|
||||
state.scan()?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Scan org directory for files.
|
||||
pub fn scan(&self) -> Result<()> {
|
||||
let mut docs = self.docs.write();
|
||||
docs.clear();
|
||||
|
||||
for entry in fs::read_dir(&self.org_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map_or(false, |e| e == "org") {
|
||||
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
match self.load_doc(&path) {
|
||||
Ok(doc_state) => {
|
||||
docs.insert(name.to_string(), Arc::new(doc_state));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("warning: failed to load {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load and parse a single document.
|
||||
fn load_doc(&self, path: &Path) -> Result<DocState> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let doc = self.parser.write().parse(&content)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
||||
|
||||
Ok(DocState {
|
||||
path: path.to_path_buf(),
|
||||
content,
|
||||
doc,
|
||||
})
|
||||
}
|
||||
|
||||
/// List document names.
|
||||
pub fn doc_names(&self) -> Vec<String> {
|
||||
self.docs.read().keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get a document by name.
|
||||
pub fn get_doc(&self, name: &str) -> Option<Arc<DocState>> {
|
||||
self.docs.read().get(name).cloned()
|
||||
}
|
||||
|
||||
/// Get document index: name\tpath\ttitle.
|
||||
pub fn doc_index(&self) -> String {
|
||||
let docs = self.docs.read();
|
||||
let mut lines: Vec<String> = docs.iter()
|
||||
.map(|(name, state)| {
|
||||
let title = state.doc.title().unwrap_or("-");
|
||||
format!("{}\t{}\t{}", name, state.path.display(), title)
|
||||
})
|
||||
.collect();
|
||||
lines.sort();
|
||||
lines.join("\n") + "\n"
|
||||
}
|
||||
|
||||
/// Get raw content of a document.
|
||||
pub fn doc_raw(&self, name: &str) -> Option<String> {
|
||||
self.get_doc(name).map(|d| d.content.clone())
|
||||
}
|
||||
|
||||
/// Write raw content to a document.
|
||||
pub fn write_doc_raw(&self, name: &str, content: &str) -> Result<()> {
|
||||
let doc_state = self.get_doc(name)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "document not found"))?;
|
||||
|
||||
// Parse first to validate
|
||||
let new_doc = self.parser.write().parse(content)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
||||
|
||||
// Write to file
|
||||
fs::write(&doc_state.path, content)?;
|
||||
|
||||
// Update state
|
||||
let new_state = Arc::new(DocState {
|
||||
path: doc_state.path.clone(),
|
||||
content: content.to_string(),
|
||||
doc: new_doc,
|
||||
});
|
||||
self.docs.write().insert(name.to_string(), new_state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get document metadata.
|
||||
pub fn doc_meta(&self, name: &str) -> Option<String> {
|
||||
self.get_doc(name).map(|d| {
|
||||
let title = d.doc.title().unwrap_or("");
|
||||
let author = d.doc.author().unwrap_or("");
|
||||
let filetags = d.doc.settings.filetags.join(":");
|
||||
format!("title\t{}\nauthor\t{}\nfiletags\t{}\n", title, author, filetags)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all TODO keywords configuration.
|
||||
pub fn keywords(&self) -> String {
|
||||
// Return default TODO keywords
|
||||
// TODO: aggregate from all docs or use config
|
||||
"TODO NEXT WAITING | DONE CANCELLED\n".to_string()
|
||||
}
|
||||
|
||||
/// Handle control commands.
|
||||
pub fn handle_ctl(&self, cmd: &str) -> Result<String> {
|
||||
let cmd = cmd.trim();
|
||||
match cmd {
|
||||
"reload" | "rescan" => {
|
||||
self.scan()?;
|
||||
Ok("ok\n".to_string())
|
||||
}
|
||||
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("unknown command: {}", cmd))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Section Access
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
impl OrkState {
|
||||
/// Get section IDs for a document (top-level only).
|
||||
pub fn section_ids(&self, doc_name: &str) -> Vec<String> {
|
||||
self.get_doc(doc_name)
|
||||
.map(|d| {
|
||||
d.doc.sections.iter()
|
||||
.map(|s| self.section_id(s))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get child section IDs.
|
||||
pub fn child_ids(&self, doc_name: &str, parent_id: &str) -> Vec<String> {
|
||||
self.get_doc(doc_name)
|
||||
.and_then(|d| {
|
||||
d.doc.all_sections()
|
||||
.find(|s| self.section_id(s) == parent_id)
|
||||
.map(|s| {
|
||||
s.children.iter()
|
||||
.map(|c| self.section_id(c))
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Generate section ID: CUSTOM_ID > ID > slugified title.
|
||||
fn section_id(&self, section: &org_ast::Section) -> String {
|
||||
if let Some(id) = section.custom_id() {
|
||||
return id.to_string();
|
||||
}
|
||||
if let Some(id) = section.id() {
|
||||
return id.to_string();
|
||||
}
|
||||
// Slugify title
|
||||
let title = section.headline.title_text();
|
||||
slugify(&title)
|
||||
}
|
||||
|
||||
/// Find section by ID in document.
|
||||
pub fn find_section<'a>(&self, doc: &'a Document, id: &str) -> Option<&'a org_ast::Section> {
|
||||
doc.all_sections().find(|s| self.section_id(s) == id)
|
||||
}
|
||||
|
||||
/// Get section headline.
|
||||
pub fn section_headline(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id).map(|s| {
|
||||
// Reconstruct headline
|
||||
let mut line = "*".repeat(s.headline.level as usize);
|
||||
line.push(' ');
|
||||
if let Some(kw) = &s.headline.keyword {
|
||||
line.push_str(&kw.keyword);
|
||||
line.push(' ');
|
||||
}
|
||||
if let Some(p) = s.headline.priority {
|
||||
line.push_str(&format!("[#{}] ", p.as_char()));
|
||||
}
|
||||
line.push_str(&s.headline.title_text());
|
||||
if !s.headline.tags.is_empty() {
|
||||
line.push_str(&format!(" :{}:", s.headline.tags.join(":")));
|
||||
}
|
||||
line
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section title.
|
||||
pub fn section_title(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id).map(|s| s.headline.title_text())
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section keyword.
|
||||
pub fn section_keyword(&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.headline.keyword.as_ref().map(|k| k.keyword.clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section priority.
|
||||
pub fn section_priority(&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.headline.priority.map(|p| p.as_char().to_string())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Get section tags.
|
||||
pub fn section_tags(&self, doc_name: &str, id: &str) -> Option<String> {
|
||||
self.get_doc(doc_name).and_then(|d| {
|
||||
self.find_section(&d.doc, id).map(|s| s.headline.tags.join(":"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Slugify a string for use as an ID.
|
||||
fn slugify(s: &str) -> String {
|
||||
s.to_lowercase()
|
||||
.chars()
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||
.collect::<String>()
|
||||
.split('-')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_slugify() {
|
||||
assert_eq!(slugify("Hello World"), "hello-world");
|
||||
assert_eq!(slugify("TODO: Fix bug #123"), "todo-fix-bug-123");
|
||||
assert_eq!(slugify(" multiple spaces "), "multiple-spaces");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
//! Declarative virtual filesystem EDSL.
|
||||
//!
|
||||
//! Port of ollie's virtfs pattern: declare filesystem structure statically,
|
||||
//! populate dynamically via closures.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let fs = dir("/",
|
||||
//! file("version", 0o444, read(|| Ok(b"1.0\n".to_vec()))),
|
||||
//! file("ctl", 0o222, rdwr(|data| {
|
||||
//! // handle command, return response
|
||||
//! Ok(b"ok\n".to_vec())
|
||||
//! })),
|
||||
//! each("docs", || list_docs(), |name| {
|
||||
//! dir(&name,
|
||||
//! file("title", 0o444, read(move || get_title(&name))),
|
||||
//! )
|
||||
//! }),
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// File mode (Unix permissions).
|
||||
pub type Mode = u32;
|
||||
|
||||
/// Read handler: returns file content.
|
||||
pub type ReadFn = Arc<dyn Fn() -> Result<Vec<u8>> + Send + Sync>;
|
||||
|
||||
/// Write handler: receives data, returns Ok on success.
|
||||
pub type WriteFn = Arc<dyn Fn(&[u8]) -> Result<()> + Send + Sync>;
|
||||
|
||||
/// Rdwr handler: write triggers computation, returns response.
|
||||
pub type RdwrFn = Arc<dyn Fn(&[u8]) -> Result<Vec<u8>> + Send + Sync>;
|
||||
|
||||
/// List handler: returns dynamic child names.
|
||||
pub type ListFn = Arc<dyn Fn() -> Result<Vec<String>> + Send + Sync>;
|
||||
|
||||
/// Child builder: given a name, returns the node for that child.
|
||||
pub type ChildFn = Arc<dyn Fn(&str) -> Option<FsNode> + Send + Sync>;
|
||||
|
||||
/// A node in the virtual filesystem.
|
||||
#[derive(Clone)]
|
||||
pub struct FsNode {
|
||||
pub name: String,
|
||||
pub mode: Mode,
|
||||
pub is_dir: bool,
|
||||
|
||||
// File operations
|
||||
pub read_fn: Option<ReadFn>,
|
||||
pub write_fn: Option<WriteFn>,
|
||||
pub rdwr_fn: Option<RdwrFn>,
|
||||
|
||||
// Directory operations
|
||||
pub children: HashMap<String, FsNode>, // Static children
|
||||
pub list_fn: Option<ListFn>, // Dynamic child listing
|
||||
pub child_fn: Option<ChildFn>, // Dynamic child builder
|
||||
}
|
||||
|
||||
impl FsNode {
|
||||
/// Create an empty node.
|
||||
pub fn new(name: &str, mode: Mode, is_dir: bool) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
mode,
|
||||
is_dir,
|
||||
read_fn: None,
|
||||
write_fn: None,
|
||||
rdwr_fn: None,
|
||||
children: HashMap::new(),
|
||||
list_fn: None,
|
||||
child_fn: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk to a child node by path.
|
||||
pub fn walk(&self, path: &str) -> Option<FsNode> {
|
||||
if path.is_empty() || path == "." {
|
||||
return Some(self.clone());
|
||||
}
|
||||
|
||||
let path = path.trim_start_matches('/');
|
||||
let (first, rest) = match path.find('/') {
|
||||
Some(i) => (&path[..i], &path[i + 1..]),
|
||||
None => (path, ""),
|
||||
};
|
||||
|
||||
// Try static children first
|
||||
if let Some(child) = self.children.get(first) {
|
||||
if rest.is_empty() {
|
||||
return Some(child.clone());
|
||||
} else {
|
||||
return child.walk(rest);
|
||||
}
|
||||
}
|
||||
|
||||
// Try dynamic child builder
|
||||
if let Some(ref child_fn) = self.child_fn {
|
||||
if let Some(child) = child_fn(first) {
|
||||
if rest.is_empty() {
|
||||
return Some(child);
|
||||
} else {
|
||||
return child.walk(rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// List children of this directory.
|
||||
pub fn list(&self) -> Result<Vec<String>> {
|
||||
if !self.is_dir {
|
||||
return Err(io::Error::new(io::ErrorKind::NotADirectory, "not a directory"));
|
||||
}
|
||||
|
||||
let mut names: Vec<String> = self.children.keys().cloned().collect();
|
||||
|
||||
// Add dynamic children
|
||||
if let Some(ref list_fn) = self.list_fn {
|
||||
let dynamic = list_fn()?;
|
||||
names.extend(dynamic);
|
||||
}
|
||||
|
||||
names.sort();
|
||||
names.dedup();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// Read file content.
|
||||
pub fn read(&self) -> Result<Vec<u8>> {
|
||||
if let Some(ref f) = self.read_fn {
|
||||
f()
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::PermissionDenied, "not readable"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Write to file.
|
||||
pub fn write(&self, data: &[u8]) -> Result<()> {
|
||||
if let Some(ref f) = self.write_fn {
|
||||
f(data)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::PermissionDenied, "not writable"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rdwr: write then read (for command files).
|
||||
pub fn rdwr(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
if let Some(ref f) = self.rdwr_fn {
|
||||
f(data)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::PermissionDenied, "not rdwr"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if readable.
|
||||
pub fn can_read(&self) -> bool {
|
||||
self.read_fn.is_some() || self.is_dir
|
||||
}
|
||||
|
||||
/// Check if writable.
|
||||
pub fn can_write(&self) -> bool {
|
||||
self.write_fn.is_some() || self.rdwr_fn.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FsNode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FsNode")
|
||||
.field("name", &self.name)
|
||||
.field("mode", &format!("{:04o}", self.mode))
|
||||
.field("is_dir", &self.is_dir)
|
||||
.field("children", &self.children.keys().collect::<Vec<_>>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// EDSL Constructors
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Create a directory node with static children.
|
||||
pub fn dir(name: &str, children: Vec<FsNode>) -> FsNode {
|
||||
let mut node = FsNode::new(name, 0o755, true);
|
||||
for child in children {
|
||||
node.children.insert(child.name.clone(), child);
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
/// Create a directory node with dynamic children added to root.
|
||||
/// Unlike `each`, these children appear as siblings, not inside a named container.
|
||||
pub fn dir_dynamic<L, C>(name: &str, static_children: Vec<FsNode>, list_fn: L, child_fn: C) -> FsNode
|
||||
where
|
||||
L: Fn() -> Result<Vec<String>> + Send + Sync + 'static,
|
||||
C: Fn(&str) -> Option<FsNode> + Send + Sync + 'static,
|
||||
{
|
||||
let mut node = FsNode::new(name, 0o755, true);
|
||||
for child in static_children {
|
||||
node.children.insert(child.name.clone(), child);
|
||||
}
|
||||
node.list_fn = Some(Arc::new(list_fn));
|
||||
node.child_fn = Some(Arc::new(child_fn));
|
||||
node
|
||||
}
|
||||
|
||||
/// Create a file node with the given options.
|
||||
pub fn file(name: &str, mode: Mode, opts: Vec<FileOpt>) -> FsNode {
|
||||
let mut node = FsNode::new(name, mode, false);
|
||||
for opt in opts {
|
||||
match opt {
|
||||
FileOpt::Read(f) => node.read_fn = Some(f),
|
||||
FileOpt::Write(f) => node.write_fn = Some(f),
|
||||
FileOpt::Rdwr(f) => node.rdwr_fn = Some(f),
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
/// Create a dynamic directory: children listed by list_fn, built by child_fn.
|
||||
pub fn each<L, C>(name: &str, list_fn: L, child_fn: C) -> FsNode
|
||||
where
|
||||
L: Fn() -> Result<Vec<String>> + Send + Sync + 'static,
|
||||
C: Fn(&str) -> Option<FsNode> + Send + Sync + 'static,
|
||||
{
|
||||
let mut node = FsNode::new(name, 0o755, true);
|
||||
node.list_fn = Some(Arc::new(list_fn));
|
||||
node.child_fn = Some(Arc::new(child_fn));
|
||||
node
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// File Options
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Options for configuring file nodes.
|
||||
pub enum FileOpt {
|
||||
Read(ReadFn),
|
||||
Write(WriteFn),
|
||||
Rdwr(RdwrFn),
|
||||
}
|
||||
|
||||
/// Create a read handler.
|
||||
pub fn read<F>(f: F) -> Vec<FileOpt>
|
||||
where
|
||||
F: Fn() -> Result<Vec<u8>> + Send + Sync + 'static,
|
||||
{
|
||||
vec![FileOpt::Read(Arc::new(f))]
|
||||
}
|
||||
|
||||
/// Create a write handler.
|
||||
pub fn write<F>(f: F) -> Vec<FileOpt>
|
||||
where
|
||||
F: Fn(&[u8]) -> Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
vec![FileOpt::Write(Arc::new(f))]
|
||||
}
|
||||
|
||||
/// Create a rdwr handler.
|
||||
pub fn rdwr<F>(f: F) -> Vec<FileOpt>
|
||||
where
|
||||
F: Fn(&[u8]) -> Result<Vec<u8>> + Send + Sync + 'static,
|
||||
{
|
||||
vec![FileOpt::Rdwr(Arc::new(f))]
|
||||
}
|
||||
|
||||
/// Create combined read + write handlers.
|
||||
pub fn rw<R, W>(read_f: R, write_f: W) -> Vec<FileOpt>
|
||||
where
|
||||
R: Fn() -> Result<Vec<u8>> + Send + Sync + 'static,
|
||||
W: Fn(&[u8]) -> Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
vec![
|
||||
FileOpt::Read(Arc::new(read_f)),
|
||||
FileOpt::Write(Arc::new(write_f)),
|
||||
]
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Tests
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_static_tree() {
|
||||
let root = dir("/", vec![
|
||||
file("version", 0o444, read(|| Ok(b"1.0\n".to_vec()))),
|
||||
dir("subdir", vec![
|
||||
file("info", 0o444, read(|| Ok(b"hello\n".to_vec()))),
|
||||
]),
|
||||
]);
|
||||
|
||||
// Walk to version
|
||||
let v = root.walk("version").unwrap();
|
||||
assert_eq!(v.name, "version");
|
||||
assert_eq!(v.read().unwrap(), b"1.0\n");
|
||||
|
||||
// Walk to subdir/info
|
||||
let info = root.walk("subdir/info").unwrap();
|
||||
assert_eq!(info.read().unwrap(), b"hello\n");
|
||||
|
||||
// List root
|
||||
let names = root.list().unwrap();
|
||||
assert!(names.contains(&"version".to_string()));
|
||||
assert!(names.contains(&"subdir".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_children() {
|
||||
let items = vec!["alice".to_string(), "bob".to_string()];
|
||||
let items_clone = items.clone();
|
||||
|
||||
let root = dir("/", vec![
|
||||
each("users",
|
||||
move || Ok(items_clone.clone()),
|
||||
|name| {
|
||||
let n = name.to_string();
|
||||
let n2 = n.clone();
|
||||
Some(dir(&n, vec![
|
||||
file("name", 0o444, read(move || Ok(format!("{}\n", n2).into_bytes()))),
|
||||
]))
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
// List users
|
||||
let users = root.walk("users").unwrap();
|
||||
let names = users.list().unwrap();
|
||||
assert!(names.contains(&"alice".to_string()));
|
||||
assert!(names.contains(&"bob".to_string()));
|
||||
|
||||
// Read alice's name
|
||||
let alice_name = root.walk("users/alice/name").unwrap();
|
||||
assert_eq!(alice_name.read().unwrap(), b"alice\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write() {
|
||||
use std::sync::Mutex;
|
||||
let data = Arc::new(Mutex::new(String::new()));
|
||||
let data_clone = data.clone();
|
||||
|
||||
let root = dir("/", vec![
|
||||
file("ctl", 0o222, write(move |d| {
|
||||
*data_clone.lock().unwrap() = String::from_utf8_lossy(d).to_string();
|
||||
Ok(())
|
||||
})),
|
||||
]);
|
||||
|
||||
let ctl = root.walk("ctl").unwrap();
|
||||
ctl.write(b"hello").unwrap();
|
||||
assert_eq!(*data.lock().unwrap(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rdwr() {
|
||||
let root = dir("/", vec![
|
||||
file("echo", 0o666, rdwr(|d| {
|
||||
let mut response = b"got: ".to_vec();
|
||||
response.extend_from_slice(d);
|
||||
Ok(response)
|
||||
})),
|
||||
]);
|
||||
|
||||
let echo = root.walk("echo").unwrap();
|
||||
let resp = echo.rdwr(b"test").unwrap();
|
||||
assert_eq!(resp, b"got: test");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue