diff --git a/.helix/languages.toml b/.helix/languages.toml index 9143ae0..b9df9d8 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,5 +1,5 @@ [language-server.filaments] -command = "/Users/suri/dev/projects/filaments/target/debug/fil" +command = "/home/suri/doc/projects/filaments/target/debug/fil" args = ["lsp"] diff --git a/Cargo.lock b/Cargo.lock index 87b8ac5..489e70d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -788,6 +788,70 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -2319,6 +2383,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "axum", "better-panic", "chrono", "clap", @@ -3693,12 +3758,51 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "human-panic" version = "2.0.8" @@ -3724,6 +3828,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -4365,6 +4504,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "maybe-async" version = "0.2.11" @@ -4429,6 +4574,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -6391,6 +6542,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -6411,6 +6573,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.7" @@ -6972,6 +7146,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -7379,6 +7559,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -7403,7 +7599,7 @@ dependencies = [ "serde_json", "tokio", "tokio-util", - "tower", + "tower 0.4.13", "tower-lsp-macros", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index bb5e575..9f64982 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ tokio-util = "0.7.18" tower-lsp = "0.20.0" tracing-error = "0.2.1" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +axum = {version = "0.8.9", features = ["macros"]} [build-dependencies] anyhow = "1.0.102" diff --git a/crates/dto/migration/src/types/color.rs b/crates/dto/migration/src/types/color.rs index ced59f4..8fa8ba5 100644 --- a/crates/dto/migration/src/types/color.rs +++ b/crates/dto/migration/src/types/color.rs @@ -1,12 +1,13 @@ use rand::RngExt; use rgb::RGB8; use sea_orm::DeriveValueType; +use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; /// Color type /// /// We store it as a u32, but its actually 00000000rrrrrrrrggggggggbbbbbbbb -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Copy, DeriveValueType)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Copy, DeriveValueType, Serialize, Deserialize)] pub struct Color(u32); impl Color { diff --git a/crates/dto/migration/src/types/priority.rs b/crates/dto/migration/src/types/priority.rs index 8421500..b1bdd7b 100644 --- a/crates/dto/migration/src/types/priority.rs +++ b/crates/dto/migration/src/types/priority.rs @@ -1,6 +1,9 @@ use sea_orm::DeriveValueType; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType, Default, PartialOrd, Ord)] +#[derive( + Clone, Debug, PartialEq, Eq, DeriveValueType, Default, PartialOrd, Ord, Serialize, Deserialize, +)] #[sea_orm(value_type = "String")] pub enum Priority { Asap, diff --git a/crates/fil-core/src/color.rs b/crates/fil-core/src/color.rs index d4e5f0a..c9bb957 100644 --- a/crates/fil-core/src/color.rs +++ b/crates/fil-core/src/color.rs @@ -2,10 +2,11 @@ use std::fmt::Display; use dto::ColorDTO; use ratatui::style::Color as RatColor; +use serde::{Deserialize, Serialize}; /// Agnostic Color type. /// internally represented as RGB -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default, Serialize, Deserialize)] pub struct Color(ColorDTO); impl Display for Color { diff --git a/crates/fil-core/src/due.rs b/crates/fil-core/src/due.rs index 04215f8..7af1ac3 100644 --- a/crates/fil-core/src/due.rs +++ b/crates/fil-core/src/due.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime}; +use serde::{Deserialize, Serialize}; use crate::{Error, Result}; @@ -70,7 +71,7 @@ const DATE_FORMATS: &[&str] = &[ const YEARLESS_FORMATS: &[&str] = &["%m/%d", "%m-%d"]; /// Due `DateTime` representation -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Due(pub Option); impl Due { diff --git a/crates/fil-core/src/group.rs b/crates/fil-core/src/group.rs index c712230..ffccb53 100644 --- a/crates/fil-core/src/group.rs +++ b/crates/fil-core/src/group.rs @@ -2,11 +2,12 @@ use dto::{ DateTime, GroupActiveModel, GroupEntity, GroupModelEx, IntoActiveModel as _, NanoId, TagActiveModel, TagEntity, ZettelEntity, }; +use serde::{Deserialize, Serialize}; use crate::{Kasten, Priority, Zettel, frontmatter, zettel::Tag}; /// A `Group` which contains tasks! -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Group { /// The unique identifier for this group. pub id: NanoId, diff --git a/crates/fil-core/src/kasten/index/mod.rs b/crates/fil-core/src/kasten/index/mod.rs index 55df338..0892d2d 100644 --- a/crates/fil-core/src/kasten/index/mod.rs +++ b/crates/fil-core/src/kasten/index/mod.rs @@ -11,13 +11,13 @@ use dto::{ }; use pulldown_cmark::{Event, Options, Parser}; use rayon::iter::{ParallelBridge, ParallelIterator}; +use serde::{Deserialize, Serialize}; use tracing::info; use crate::zettel::{Link, ZettelId, frontmatter::Body}; /// A datastructure useful for indexing the current `Kasten`. -#[derive(Debug, Clone)] - +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Index { pub(super) zods: HashMap, /// All of the outgoing links from each `Zettel`. @@ -26,7 +26,7 @@ pub struct Index { } /// The file-contents of a Zettel, parsed. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct ZettelOnDisk { /// The parsed frontmatter of the zettel. pub fm: FrontMatter, diff --git a/crates/fil-core/src/kasten/mod.rs b/crates/fil-core/src/kasten/mod.rs index e3bf184..ce697f6 100644 --- a/crates/fil-core/src/kasten/mod.rs +++ b/crates/fil-core/src/kasten/mod.rs @@ -3,14 +3,17 @@ use std::{ sync::Arc, }; -use dto::{Database, DatabaseConnection, Migrator, MigratorTrait}; +use dto::{ + Database, DatabaseConnection, Migrator, MigratorTrait, QueryOrder, TagEntity, ZettelColumns, + ZettelEntity, +}; use tokio::{ fs::{File, create_dir_all}, sync::RwLock, }; use tracing::debug; -use crate::{frontmatter::FrontMatter, zettel::ZettelId}; +use crate::{Zettel, frontmatter::FrontMatter, zettel::ZettelId}; mod index; pub use index::Index; @@ -145,4 +148,18 @@ impl Kasten { Ok(()) } + + /// # Errors + /// Will error if we cannot communicate with the database. + //TODO: this needs a permanent home thats not this + pub async fn fetch_all_zettels(&self) -> crate::Result> { + Ok(ZettelEntity::load() + .with(TagEntity) + .order_by_desc(ZettelColumns::ModifiedAt) + .all(&self.db) + .await? + .into_iter() + .map(Into::into) + .collect()) + } } diff --git a/crates/fil-core/src/kasten/todo_tree.rs b/crates/fil-core/src/kasten/todo_tree.rs index befa156..94dcdee 100644 --- a/crates/fil-core/src/kasten/todo_tree.rs +++ b/crates/fil-core/src/kasten/todo_tree.rs @@ -4,12 +4,13 @@ use dto::{ ColumnTrait as _, DatabaseConnection, GroupColumns, GroupEntity, NanoId, QueryFilter as _, TagEntity, TaskColumns, TaskEntity, ZettelEntity, }; +use serde::{Deserialize, Serialize}; use tree::{InsertBehavior, Node, NodeId, Tree}; use crate::{Group, Task}; /// The kind of item represented by a [`TodoNode`]. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TodoNodeKind { /// The synthetic root of the todo tree. Root, @@ -20,7 +21,7 @@ pub enum TodoNodeKind { } /// A node stored in the todo tree. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TodoNode { /// The node's nesting depth below the root. pub depth: usize, @@ -43,7 +44,7 @@ impl TodoNode { } /// An in-memory hierarchy of groups and tasks. -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TodoTree { /// The underlying tree containing todo nodes. pub tree: Tree, diff --git a/crates/fil-core/src/priority.rs b/crates/fil-core/src/priority.rs index a2ef423..60c8a7d 100644 --- a/crates/fil-core/src/priority.rs +++ b/crates/fil-core/src/priority.rs @@ -1,10 +1,11 @@ use std::fmt::Display; use dto::PriorityDTO; +use serde::{Deserialize, Serialize}; /// An Enum for the various `Priority` levels /// for `Task`s and `Group`s -#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Priority(PriorityDTO); impl Priority { diff --git a/crates/fil-core/src/task.rs b/crates/fil-core/src/task.rs index 20738a2..40a8927 100644 --- a/crates/fil-core/src/task.rs +++ b/crates/fil-core/src/task.rs @@ -3,11 +3,12 @@ use dto::{ DateTime, GroupEntity, HasOne, IntoActiveModel as _, NanoId, TagEntity, TaskActiveModel, TaskEntity, TaskModelEx, ZettelEntity, }; +use serde::{Deserialize, Serialize}; use crate::{Group, Kasten, Priority, Zettel, due::Due, frontmatter}; /// a `Task` that you have to complete! -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct Task { /// The task's unique identifier. @@ -194,7 +195,8 @@ impl Task { Ok(()) } - /// Toggles a task between completed and incomplete. + /// Toggles a task between completed and incomplete. Will return what it is + /// set to after toggling. /// /// Marks an incomplete task as complete using the current local time, or /// clears its completion time when it is already complete. @@ -206,7 +208,7 @@ impl Task { /// # Panics /// /// Panics if `id` does not identify an existing task. - pub async fn toggle_finish(id: NanoId, kt: &Kasten) -> crate::Result<()> { + pub async fn toggle_finish(id: NanoId, kt: &Kasten) -> crate::Result> { let now = Local::now().naive_local(); let model = TaskEntity::load() @@ -227,7 +229,7 @@ impl Task { .update(&kt.db) .await?; - Ok(()) + Ok(new_finished_at) } /// Calcualtes the `p_score` of this `Task` diff --git a/crates/fil-core/src/zettel/mod.rs b/crates/fil-core/src/zettel/mod.rs index 9274386..c06f4a4 100644 --- a/crates/fil-core/src/zettel/mod.rs +++ b/crates/fil-core/src/zettel/mod.rs @@ -6,6 +6,7 @@ use dto::{ }; use dto::NanoId; +use serde::{Deserialize, Serialize}; use tokio::{fs::File, io::AsyncWriteExt}; use crate::zettel::frontmatter::{Body, FrontMatter}; @@ -24,8 +25,7 @@ pub use tag::*; /// A `Zettel` is a note about a single idea. /// It can have many `Tag`s, just meaning it can fall under many /// categories. - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[non_exhaustive] pub struct Zettel { /// The zettel's unique identifier. diff --git a/crates/fil-core/src/zettel/tag.rs b/crates/fil-core/src/zettel/tag.rs index d687287..83370c8 100644 --- a/crates/fil-core/src/zettel/tag.rs +++ b/crates/fil-core/src/zettel/tag.rs @@ -1,10 +1,11 @@ use dto::{IntoActiveModel as _, NanoId, TagEntity, TagModel, TagModelEx, ZettelEntity}; +use serde::{Deserialize, Serialize}; use crate::{Color, Kasten, Zettel}; /// Represents a `Tag` in a `ZettelKasten` note taking method. /// Easy way to link multiple notes under one simple word. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[non_exhaustive] pub struct Tag { /// A unique `NanoId` diff --git a/crates/tree/src/node.rs b/crates/tree/src/node.rs index 5700649..da34b4d 100644 --- a/crates/tree/src/node.rs +++ b/crates/tree/src/node.rs @@ -6,6 +6,7 @@ use crate::NodeId; #[derive(Debug, Serialize, Deserialize, Ord, Eq, PartialOrd)] #[cfg_attr(feature = "automerge", derive(Reconcile, Hydrate))] +#[derive(Debug, Serialize, Deserialize, Ord, Eq, PartialOrd, Clone)] pub struct Node { pub(crate) data: T, pub(crate) parent: Option, @@ -25,7 +26,7 @@ where impl Node { /// Creates a new `Node` with the provided data /// - /// ``` + /// ``` /// use tree::Node; /// /// let _one: Node = Node::new(1); diff --git a/crates/tree/src/tree.rs b/crates/tree/src/tree.rs index ab5f0c6..e07c845 100644 --- a/crates/tree/src/tree.rs +++ b/crates/tree/src/tree.rs @@ -160,6 +160,7 @@ impl TreeBuilder { feature = "automerge", derive(autosurgeon::Reconcile, autosurgeon::Hydrate) )] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Tree { root: Option, pub(crate) nodes: Vec>>, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 167183b..1d59d00 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -38,6 +38,9 @@ pub enum Commands { /// Spawn the `LSP` Lsp, + /// Spawn the `Server` that runs in the background + Serve, + /// Initalize Filaments. /// /// This will write a default config to ~/.config/filaments, diff --git a/src/cli/process.rs b/src/cli/process.rs index 537c5b9..4c25f35 100644 --- a/src/cli/process.rs +++ b/src/cli/process.rs @@ -12,6 +12,7 @@ use crate::{ cli::{Commands, ZettelSubcommand}, config::{Config, get_config_dir}, lsp::Backend, + server::FilServer, }; impl Commands { @@ -90,6 +91,11 @@ impl Commands { } } } + + Self::Serve => { + let server = FilServer::new().await?; + server.run().await; + } } Ok(()) diff --git a/src/logging.rs b/src/logging.rs index f3e2874..a6a3021 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,5 +1,6 @@ use std::{ fs::{OpenOptions, create_dir_all}, + io::stderr, sync::LazyLock, }; @@ -19,9 +20,14 @@ pub static LOG_FILE: LazyLock = LazyLock::new(|| format!("{}.log", env!( /// Initializes the logger, which writes logs to a `log_file` in the data dir. /// +/// `stderr` logging is opt-in via `enable_stderr` — the TUI renders to the +/// alternate screen buffer over stderr/stdout, so a stderr-writing layer +/// would corrupt the display. Only `fil serve` (which has no terminal UI to +/// clobber) should pass `true`. +/// /// NOTE: log level is configurable via the `RUST_LOG` env var or the /// `FILAMENTS_LOG_LEVEL` env var -pub fn init() -> Result<()> { +pub fn init(enable_stderr: bool) -> Result<()> { let directory = config::get_data_dir(); create_dir_all(&directory)?; @@ -38,7 +44,9 @@ pub fn init() -> Result<()> { // If `RUST_LOG` is set, use that as default,, or use value of `LOG_ENV` variable. let env_filter = env_filter .try_from_env() - .or_else(|_| env_filter.with_env_var(LOG_LEVEL_ENV.clone()).from_env())?; + .or_else(|_| env_filter.with_env_var(LOG_LEVEL_ENV.clone()).from_env())? + .add_directive("sqlx=off".parse().expect("valid filter directive")) + .add_directive("sqlx_core=off".parse().expect("valid filter directive")); let file_subscriber = fmt::layer() .with_file(true) @@ -46,10 +54,21 @@ pub fn init() -> Result<()> { .with_writer(log_file) .with_target(false) .with_ansi(false) - .with_filter(env_filter); + .with_filter(env_filter.clone()); + + let stderr_subscriber = enable_stderr.then(|| { + fmt::layer() + .with_file(true) + .with_line_number(true) + .with_writer(stderr) + .with_target(false) + .with_ansi(true) + .with_filter(env_filter) + }); tracing_subscriber::registry() .with(file_subscriber) + .with(stderr_subscriber) .with(ErrorLayer::default()) .try_init()?; diff --git a/src/main.rs b/src/main.rs index d51105a..ba2c5cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,15 +17,19 @@ mod errors; mod filaments; mod logging; mod lsp; +mod server; mod tui; mod viz; fn main() -> color_eyre::Result<()> { - errors::init()?; - logging::init()?; - let args = Cli::parse(); + // we only want to enable logging into stderr if we are serving + let is_serve = matches!(args.command, Some(cli::Commands::Serve)); + + errors::init()?; + logging::init(is_serve)?; + let rt = Arc::new(tokio::runtime::Runtime::new()?); // Handle commands @@ -33,17 +37,19 @@ fn main() -> color_eyre::Result<()> { return rt.block_on(async { command.process().await }); } + // some need a kasten handle and some dont, some need // Kasten + //WARN: this needs to be removed let kh: KastenHandle = rt.block_on(async { let cfg = Config::parse()?; - debug!("Config: {cfg:#?}"); - Ok::(Arc::new(RwLock::new( Kasten::instansiate(cfg.fil_dir).await?, ))) })?; + // what if we make this kasten handle optional and then pass that optional in. + debug!("Kasten Handle: {kh:#?}"); let (signal_tx, signal_rx) = mpsc::unbounded_channel(); diff --git a/src/server/error.rs b/src/server/error.rs new file mode 100644 index 0000000..db79b89 --- /dev/null +++ b/src/server/error.rs @@ -0,0 +1,25 @@ +//! Maps `fil_core::Error` onto HTTP responses for the API server. +use axum::{http::StatusCode, response::IntoResponse}; + +pub struct ApiError(fil_core::Error); + +impl From for ApiError { + fn from(err: fil_core::Error) -> Self { + Self(err) + } +} + + +impl IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + let status = match &self.0 { + fil_core::Error::GroupDoesNotExist => StatusCode::NOT_FOUND, + fil_core::Error::InvalidDueDate { .. } | fil_core::Error::InvalidPriority => { + StatusCode::BAD_REQUEST + } + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + + (status, self.0.to_string()).into_response() + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..c86db3c --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,51 @@ +//! A API server that all the other surfaces interact with. +//! +use std::sync::Arc; + +use axum::{Router, routing::get}; +use color_eyre::eyre::Result; +use fil_core::{Kasten, KastenHandle}; +use tokio::sync::RwLock; +use tracing::info; + +use crate::config::Config; + +const SERVER_ADDR: &str = "0.0.0.0:6767"; + +mod error; +mod todo_router; +mod viz_router; +mod zettel_router; + +pub struct FilServer { + _kh: KastenHandle, + router: Router, +} + +impl FilServer { + pub async fn new() -> Result { + let kh = { + let cfg = Config::parse()?; + Arc::new(RwLock::new(Kasten::instansiate(cfg.fil_dir).await?)) + }; + + let router = Router::new() + //TODO: this would eventually become the server for the visualizer + .route("/", get(|| async { "Hello, World!" })) + .nest("/api/todo", todo_router::router()) + .nest("/api/zettels", zettel_router::router()) + .nest("/api/graph", viz_router::router()) + .with_state(kh.clone()); + + Ok(Self { _kh: kh, router }) + } + + pub async fn run(self) { + let listener = tokio::net::TcpListener::bind(SERVER_ADDR).await.unwrap(); + info!("Serving on address {SERVER_ADDR}"); + + axum::serve(listener, self.router) + .await + .expect("Axum must not panic!"); + } +} diff --git a/src/server/todo_router.rs b/src/server/todo_router.rs new file mode 100644 index 0000000..72853f6 --- /dev/null +++ b/src/server/todo_router.rs @@ -0,0 +1,91 @@ +use axum::{ + Json, Router, + extract::{Path, State}, + response::IntoResponse, + routing::{get, post}, +}; +use dto::{DateTime, NanoId}; +use fil_core::{Group, KastenHandle, Priority, Task}; +use serde::Deserialize; +use tracing::instrument; + +use super::error::ApiError; + +pub fn router() -> Router { + Router::new() + .route("/tree", get(get_todo_tree)) + .route("/task", post(create_task)) + .route("/group", post(create_group)) + .route("/{id}/toggle", post(toggle_task_completion)) +} + +#[axum::debug_handler] +#[instrument(skip(kh))] +async fn get_todo_tree(axum::extract::State(kh): State) -> impl IntoResponse { + let kt = kh.read().await; + Json(kt.todo_tree.clone()) +} + +#[axum::debug_handler] +#[instrument(skip(kh))] +async fn create_task( + State(kh): State, + Json(params): Json, +) -> Result { + let mut kt = kh.write().await; + + let task = Task::new( + params.name, + params.parent_id, + &mut kt, + params.due, + params.priority, + ) + .await?; + + kt.todo_tree.insert_task(&task); + + drop(kt); + + Ok(Json(task)) +} +#[axum::debug_handler] +#[instrument(skip(kh))] +async fn create_group( + State(kh): State, + Json(params): Json, +) -> Result { + let mut kt = kh.write().await; + + let group = Group::new(params.name, params.parent_id, &mut kt).await?; + + kt.todo_tree.insert_group(&group); + drop(kt); + + Ok(Json(group)) +} + +#[axum::debug_handler] +#[instrument(skip(kh))] +async fn toggle_task_completion( + Path(id): Path, + State(kh): State, +) -> Result { + let kt = kh.write().await; + let curr = Task::toggle_finish(id, &kt).await?; + drop(kt); + Ok(Json(curr)) +} + +#[derive(Deserialize, Debug)] +struct TaskCreationParams { + name: String, + parent_id: NanoId, + due: Option, + priority: Priority, +} +#[derive(Deserialize, Debug)] +struct GroupCreationParams { + name: String, + parent_id: Option, +} diff --git a/src/server/viz_router.rs b/src/server/viz_router.rs new file mode 100644 index 0000000..0ccf556 --- /dev/null +++ b/src/server/viz_router.rs @@ -0,0 +1,18 @@ +use axum::{Json, Router, extract::State, response::IntoResponse, routing::get}; +pub use fil_core::KastenHandle; +use tracing::instrument; + +use super::error::ApiError; + +pub fn router() -> Router { + Router::new().route("/index", get(fetch_index)) +} + +#[axum::debug_handler] +#[instrument(skip(kh))] +async fn fetch_index( + axum::extract::State(kh): State, +) -> Result { + let kt = kh.read().await; + Ok(Json(kt.index.clone())) +} diff --git a/src/server/zettel_router.rs b/src/server/zettel_router.rs new file mode 100644 index 0000000..fdad68e --- /dev/null +++ b/src/server/zettel_router.rs @@ -0,0 +1,87 @@ +use axum::{ + Json, Router, + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, +}; +pub use fil_core::{KastenHandle, Tag, Zettel}; +use serde::Deserialize; +use tracing::instrument; + +use super::error::ApiError; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_zettels)) + .route("/", post(create_zettel)) + .route("/search", get(search_zettels)) + + // .route("/{id}/toggle", post(toggle_task_completion)) +} + +#[axum::debug_handler] +#[instrument(skip(kh))] +async fn list_zettels( + axum::extract::State(kh): State, +) -> Result { + let kt = kh.read().await; + let all_zettels = kt.fetch_all_zettels().await?; + drop(kt); + Ok(Json(all_zettels)) +} + +#[axum::debug_handler] +#[instrument(skip(kh))] +async fn create_zettel( + State(kh): State, + Json(params): Json, +) -> Result { + let mut kt = kh.write().await; + let zettel = Zettel::new(params.title, &mut kt, params.tags).await?; + drop(kt); + Ok(Json(zettel)) +} + +#[axum::debug_handler] +#[instrument(skip(kh))] +#[expect(unreachable_code)] +async fn search_zettels( + State(kh): State, + Query(params): Query, +) -> Result { + let kt = kh.read().await; + // let _ = (kt, params); + + drop(kt); + todo!("dispatch on params.scope once search is implemented"); + + Ok(StatusCode::OK) +} + +#[derive(Deserialize, Debug)] +struct ZettelCreationParams { + title: String, + tags: Vec, +} + +#[derive(Deserialize, Debug, Default, Clone, Copy)] +#[serde(rename_all = "snake_case")] +enum SearchScope { + // honestly wont be doing all these just yet but its ok + Name, + Content, + Tags, + #[default] + All, +} + +#[derive(Deserialize, Debug)] +#[expect(dead_code)] +struct SearchParams { + q: String, + #[serde(default)] + scope: SearchScope, +} + +// what would we need from zettels diff --git a/src/tui/components/zk/mod.rs b/src/tui/components/zk/mod.rs index b495dcf..7ac7977 100644 --- a/src/tui/components/zk/mod.rs +++ b/src/tui/components/zk/mod.rs @@ -119,6 +119,7 @@ impl Zk<'_> { // ideally we just keep the same selection as we had originally + // ok this is not fucking good lol let fetch_all = async || -> Result> { Ok(ZettelEntity::load() .with(TagEntity)