diff --git a/Cargo.toml b/Cargo.toml index a0c3459..6b07273 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ anyhow = "1.0.71" clap = { version = "4.3.8", features = ["cargo", "derive", "env", "wrap_help"] } directories = "5.0.1" downloader = { version = "0.2.7", default-features = false, features = ["rustls-tls"] } +indoc = "2.0.1" regex = "1.8.4" serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.99" diff --git a/src/main.rs b/src/main.rs index ce017c4..c0bbd38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ -use std::sync::OnceLock; +use std::{path::PathBuf, sync::OnceLock}; use anyhow::{ensure, Context}; use clap::{Parser, Subcommand}; use config::Config; use directories::{ProjectDirs, UserDirs}; use persistent_state::PersistentState; +use target::Target; use target_list::TargetList; +use url::Url; mod config; mod persistent_state; @@ -50,13 +52,12 @@ fn main() -> anyhow::Result<()> { CMD::PersistentState => { println!("{persistent}"); } - CMD::List { cmd } => 'list: { - if let ListCommand::Create { + CMD::List { cmd } => match cmd { + ListCommand::Create { name, - keep_current_active, + keep_current_selected: keep_current_active, comment, - } = cmd - { + } => { if TargetList::exists(&name) { eprintln!("list already exists"); } else { @@ -67,30 +68,42 @@ fn main() -> anyhow::Result<()> { if !keep_current_active { persistent.set_list(&name); } - - break 'list; - } else if let ListCommand::Select { name } = cmd { + } + ListCommand::Select { name } => { if name == "none" { persistent.clear_list(); - } else if !TargetList::exists(&name) { - eprintln!("list doesn't exist"); - } else { + } else if TargetList::exists(&name) { persistent.set_list(&name); + } else { + eprintln!("list doesn't exist"); } - break 'list; } - + }, + CMD::Target { cmd } => { let list = persistent.list().context("no list selected")?; let mut list = TargetList::load(&list).context("failed to load list")?; match cmd { - ListCommand::Create { - name: _, - keep_current_active: _, - comment: _, + TargetCommand::Create { + file, + url, + comment, + keep_current_selected, + } => { + let target = Target::new(url, &file, comment.as_ref().map(|c| c.as_str())) + .context("invalid target")?; + list.add_target(target); + + if !keep_current_selected { + persistent.set_target(list.len_targets() - 1); + } } - | ListCommand::Select { name: _ } => { - panic!("late list command"); + TargetCommand::Select { index } => { + if index < list.len_targets() { + persistent.set_target(index); + } else { + eprintln!("target doesn't exist"); + } } } @@ -124,6 +137,11 @@ enum CMD { #[clap(subcommand)] cmd: ListCommand, }, + /// Individual target operations. + Target { + #[clap(subcommand)] + cmd: TargetCommand, + }, } #[derive(Subcommand)] @@ -140,14 +158,16 @@ enum ListCommand { /// /// Invalid examples: none, 14, _hi, hi_, h__i name: String, - /// Don't activate the newly created list. - #[clap(long, short)] - keep_current_active: bool, - /// A comment to remember what a list is for. + /// A comment to remember what the list is meant to do. #[clap(long, short)] comment: Option, + /// Don't select the newly created list. + #[clap(long, short)] + keep_current_selected: bool, }, /// Select an existing list. + /// + /// List selection is important for the `target` subcommand. Select { /// The name of the list. /// @@ -156,3 +176,26 @@ enum ListCommand { name: String, }, } + +#[derive(Subcommand)] +enum TargetCommand { + /// Create a new target. + Create { + /// The local file name. + file: PathBuf, + /// A list of URLs the file is available at. + url: Vec, + /// A comment to remember why the target is in the list. + #[clap(long, short)] + comment: Option, + /// Don't select the newly created target. + #[clap(long, short)] + keep_current_selected: bool, + }, + /// Select an existing target. + /// Target selection is important for the `url` subcommand. + Select { + /// The index of the target. + index: usize, + }, +} diff --git a/src/persistent_state.rs b/src/persistent_state.rs index d752841..cfc8098 100644 --- a/src/persistent_state.rs +++ b/src/persistent_state.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::{bail, ensure, Context}; +use indoc::writedoc; use serde::{Deserialize, Serialize}; use crate::{target_list::TargetList, PROJ_DIRS}; @@ -16,6 +17,7 @@ const PERSISTENT_FILE: &str = "persistent_state.json"; #[serde(deny_unknown_fields, default)] pub struct PersistentState { list: Option, + target: Option, } impl PersistentState { @@ -25,10 +27,22 @@ impl PersistentState { pub fn set_list(&mut self, list: &str) { self.list = Some(list.to_string()); + self.target = None; } pub fn clear_list(&mut self) { self.list = None; + self.target = None; + } + + pub fn target(&self) -> Option { + self.target + } + + pub fn set_target(&mut self, index: usize) { + if self.list.is_some() { + self.target = Some(index); + } } } @@ -95,10 +109,15 @@ impl PersistentState { impl Display for PersistentState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( + writedoc!( f, - "current list: {}", - self.list.as_ref().map(AsRef::as_ref).unwrap_or("none") + " + current list: {} + current target: {}", + self.list.as_ref().map(AsRef::as_ref).unwrap_or("none"), + self.target + .map(|t| t.to_string()) + .unwrap_or("none".to_string()) ) } } diff --git a/src/target.rs b/src/target.rs index f09a596..860e897 100644 --- a/src/target.rs +++ b/src/target.rs @@ -23,6 +23,11 @@ impl Target { "file must be file or nonexistent" ); + for url in &urls { + let scheme = url.scheme(); + ensure!(scheme == "http" || scheme == "https", "url is not http(s)"); + } + Ok(Self { urls, file: file.to_path_buf(), diff --git a/src/target_list.rs b/src/target_list.rs index 9253acc..50b29e3 100644 --- a/src/target_list.rs +++ b/src/target_list.rs @@ -1,6 +1,6 @@ use std::{ fs::{create_dir_all, File, OpenOptions}, - io::{BufReader, BufWriter}, + io::{BufReader, BufWriter, Seek, SeekFrom}, path::{Path, PathBuf}, }; @@ -20,6 +20,14 @@ impl TargetList { pub fn name(&self) -> &str { &self.inner.name } + + pub fn add_target(&mut self, target: Target) { + self.inner.targets.push(target); + } + + pub fn len_targets(&self) -> usize { + self.inner.targets.len() + } } impl TargetList { @@ -115,7 +123,13 @@ impl TargetList { } pub fn save(&mut self) -> anyhow::Result<()> { - self.file.set_len(0); + let _ = self + .file + .set_len(0) + .context("failed to clear target list file"); + self.file + .seek(SeekFrom::Start(0)) + .context("failed to rewind target list file")?; serde_json::to_writer(BufWriter::new(&self.file), &self.inner) .context("failed to save target list") }