use std::{ env::current_dir, fs::{File, create_dir_all}, io::Write, }; use color_eyre::eyre::{Context, Result, eyre}; use fil_core::Kasten; use tower_lsp::{LspService, Server}; use crate::{ cli::{Commands, TodoSubcommand, TuiArgs, ZettelSubcommand}, client::Client, config::{Config, get_config_dir}, elm::Elmui, lsp::Backend, server::FilServer, }; impl Commands { pub async fn process(self, port: Option) -> Result<()> { match self { Self::Init { name } => { let dir = current_dir() .context("Failed to get current directory")? .join(&name); Kasten::initialize(dir.clone()).await?; // Write config that sets the filaments directory to current Dir! let config_str = dbg! {Config::generate(&dir)}?; // create the config Dir let config_dir = get_config_dir(); create_dir_all(config_dir).expect("creating the config dir should not error"); let mut config_file = File::create(get_config_dir().join("config.ron")) .context("Failed to create config file")?; write!(config_file, "{config_str}")?; println!("wrote config to {config_file:#?}"); println!("Initialized successfully!"); } // the daemon builds the archive, so this works from any client // and never opens the vault alongside a running daemon Self::Export { path } => { let conf = config(port)?; let fil = Client::new(&conf.daemon_url); let path = path.unwrap_or_else(|| Kasten::default_export_name().into()); let files = fil .export(&path) .await .with_context(|| daemon_hint(&conf)) .with_context(|| format!("Failed to export to {}", path.display()))?; println!("Exported {files} files to {}", path.display()); } Self::Import { archive, dir, keep_config, } => { let kt = Kasten::import(&archive, &dir) .await .with_context(|| format!("Failed to import {}", archive.display()))?; println!("Imported {} into {}", archive.display(), kt.root.display()); if !keep_config { let config = Config::install(&kt.root)?; println!("wrote config to {}", config.display()); } } // these go through the daemon so its state, and every client // following it, sees the change Self::Zettel(command) => zettel(command, port).await?, Self::Todo(command) => todo(command, port).await?, Self::Lsp => { // everything comes from the daemon let conf = config(port).with_context(|| "Failed to parse the config!!")?; let fil = Client::new(&conf.daemon_url); // get the in and out let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); // start the LSP let (service, socket) = LspService::new(|client| Backend::new(client, fil)); Server::new(stdin, stdout, socket).serve(service).await; } Self::Serve => { let server = FilServer::new(config(port)?).await?; server.run().await?; } } Ok(()) } } /// Runs the TUI, which talks to `fil serve` over HTTP: what `fil` does when /// given no command. pub async fn run_tui(TuiArgs { tick_rate }: TuiArgs, port: Option) -> Result<()> { let conf = config(port)?; Elmui::new(tick_rate, &conf.daemon_url, conf.keymap)? .run() .await } /// The config, with `--port` applied. fn config(port: Option) -> Result { Config::parse()?.with_port(port) } /// Context for a failed daemon request, which also covers `fil serve` not /// running at all. fn daemon_hint(conf: &Config) -> String { format!("Request to `fil serve` at {} failed", conf.daemon_url) } async fn zettel(command: ZettelSubcommand, port: Option) -> Result<()> { let conf = config(port)?; let fil = Client::new(&conf.daemon_url); match command { ZettelSubcommand::New { title } => { let zettel = fil .create_zettel(title) .await .with_context(|| daemon_hint(&conf))?; println!("created zettel {} ({})", zettel.title, zettel.id); } ZettelSubcommand::List { by_tag } => { let mut zettels = fil .fetch_zettels() .await .with_context(|| daemon_hint(&conf))?; // zettels name their tags by id let tag_id = match by_tag { Some(name) => { let tags = fil.fetch_tags().await.with_context(|| daemon_hint(&conf))?; let Some(tag) = tags.into_iter().find(|tag| tag.name == name) else { return Err(eyre!("no tag named {name}")); }; Some(tag.id) } None => None, }; zettels.sort_by_key(|z| std::cmp::Reverse(z.modified_at)); for zettel in zettels .iter() .filter(|z| tag_id.as_ref().is_none_or(|id| z.tag_ids.contains(id))) { println!("{} {}", zettel.id, zettel.title); } } } Ok(()) } async fn todo(command: TodoSubcommand, port: Option) -> Result<()> { let conf = config(port)?; let fil = Client::new(&conf.daemon_url); match command { TodoSubcommand::Group { name, parent_id } => { let group = fil .create_group(name, parent_id) .await .with_context(|| daemon_hint(&conf))?; println!("created group {} ({})", group.name, group.id); } TodoSubcommand::Task { name, parent_id } => { //TODO: need to expose the due date and priority through the CLI let task = fil .create_task(name, parent_id) .await .with_context(|| daemon_hint(&conf))?; println!("created task {} ({})", task.name, task.id); } } Ok(()) }