diff --git a/Cargo.lock b/Cargo.lock index b915cde..fc30e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,7 +762,7 @@ dependencies = [ ] [[package]] -name = "wfassoc_dylib" +name = "wfassoc_cdylib" version = "0.1.0" dependencies = [ "cbindgen", @@ -779,7 +779,6 @@ dependencies = [ "serde", "thiserror", "toml 0.9.8", - "wfassoc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5835c1b..15f1374 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["wfassoc", "wfassoc_dylib", "wfassoc_exec"] +members = ["wfassoc", "wfassoc_cdylib", "wfassoc_exec"] [workspace.dependencies] thiserror = "2.0.12" diff --git a/wfassoc_dylib/Cargo.toml b/wfassoc_cdylib/Cargo.toml similarity index 93% rename from wfassoc_dylib/Cargo.toml rename to wfassoc_cdylib/Cargo.toml index b458c3f..a92048e 100644 --- a/wfassoc_dylib/Cargo.toml +++ b/wfassoc_cdylib/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wfassoc_dylib" +name = "wfassoc_cdylib" version = "0.1.0" authors = ["yyc12345"] edition = "2024" diff --git a/wfassoc_dylib/README.md b/wfassoc_cdylib/README.md similarity index 100% rename from wfassoc_dylib/README.md rename to wfassoc_cdylib/README.md diff --git a/wfassoc_dylib/src/last_error.rs b/wfassoc_cdylib/src/last_error.rs similarity index 100% rename from wfassoc_dylib/src/last_error.rs rename to wfassoc_cdylib/src/last_error.rs diff --git a/wfassoc_dylib/src/lib.rs b/wfassoc_cdylib/src/lib.rs similarity index 100% rename from wfassoc_dylib/src/lib.rs rename to wfassoc_cdylib/src/lib.rs diff --git a/wfassoc_dylib/src/string_cache.rs b/wfassoc_cdylib/src/string_cache.rs similarity index 100% rename from wfassoc_dylib/src/string_cache.rs rename to wfassoc_cdylib/src/string_cache.rs diff --git a/wfassoc_exec/Cargo.toml b/wfassoc_exec/Cargo.toml index 4ea0559..3412158 100644 --- a/wfassoc_exec/Cargo.toml +++ b/wfassoc_exec/Cargo.toml @@ -8,7 +8,7 @@ license = "SPDX:MIT" [dependencies] thiserror = { workspace = true } -wfassoc = { path="../wfassoc" } +#wfassoc = { path="../wfassoc" } clap = { version="4.5.48", features=["derive"]} serde = { version = "1.0.228", features=["derive"]} toml = "0.9.8" diff --git a/wfassoc_exec/src/cli.rs b/wfassoc_exec/src/cli.rs index 7124fb6..53afacf 100644 --- a/wfassoc_exec/src/cli.rs +++ b/wfassoc_exec/src/cli.rs @@ -1,48 +1,39 @@ use clap::{Parser, Subcommand, ValueEnum}; -use wfassoc::Scope; -#[derive(Debug, Clone, ValueEnum)] -pub enum Target { +// region: Clap Declaration + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum)] +enum Target { #[value(name = "user")] User, #[value(name = "system")] System, } -impl From for Scope { - fn from(target: Target) -> Self { - match target { - Target::User => Scope::User, - Target::System => Scope::System, - } - } -} - /// Simple program to manage Windows file associations #[derive(Parser)] #[command(name = "Windows File Association Operator", version, about)] -pub struct Cli { - /// The TOML file representing the complete program +struct Cli { + /// The TOML manifest file representing the complete program #[arg( - short = 'c', - long = "config", - value_name = "PROG_CONFIG", + short = 'm', + long = "manifest", + value_name = "PROG_MANIFEST", required = true )] - pub(crate) config_file: String, - + manifest_file: String, #[command(subcommand)] - pub(crate) command: Commands, + command: CliCommands, } #[derive(Subcommand)] -pub enum Commands { +enum CliCommands { /// Register the program #[command(name = "register")] #[command(about = "Register application with given manifest and scope.")] Register { /// The scope where wfassoc operate - #[arg(short = 't', long = "target", value_name = "TARGET", value_enum, default_value_t = Target::User)] + #[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = Target::User)] target: Target, }, /// Unregister the program @@ -50,11 +41,76 @@ pub enum Commands { #[command(about = "Unregister application with given manifest and scope.")] Unregister { /// The scope where wfassoc operate - #[arg(short = 't', long = "target", value_name = "TARGET", value_enum, default_value_t = Target::User)] + #[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = Target::User)] target: Target, }, - /// Query file associations - #[command(name = "query")] - #[command(about = "Query file extensions association infos according toh given manifest represented application.")] - Query, + /// Fetch current registration status + #[command(name = "status")] + #[command( + about = "Fetch the status of registration for given manifest represented application." + )] + Status, + #[command(name = "ext")] + #[command(about = "File extension related operations according to given program manifest.")] + Ext { + #[command(subcommand)] + command: CliExtCommands, + }, +} + +#[derive(Subcommand)] +enum CliExtCommands { + #[command(name = "link")] + #[command(about = "Link user given file extension to the program declared in manifest file.")] + Link { + // The file extensions used for this operation. Specify * for all file extension. + #[arg(required = true, value_name = "EXTS", num_args = 1..)] + exts: Vec, + }, + #[command(name = "unlink")] + #[command( + about = "Unlink user given file extension from the program declared in manifest file." + )] + Unlink { + // The file extensions used for this operation. Specify * for all file extension. + #[arg(required = true, value_name = "EXTS", num_args = 1..)] + exts: Vec, + }, + #[command(name = "list")] + #[command( + about = "List the association status for all extensions declared in given program manifest." + )] + List, +} + +// endregion + +// region: Exposed Type + +#[derive(Debug)] +pub struct Request { + +} + +#[derive(Debug)] +pub struct RequestCommand { + +} + +#[derive(Debug)] +pub struct RequestExtCommand { + +} + +// endregion + +/// +/// +/// Please note that if there is "help" or "version" command matched, +/// or any command line parse error occurs, +/// this function will order program exit immediately. +/// This is the mechanism of clap crate. +pub fn parse() -> Request { + let cli = Cli::parse(); + } diff --git a/wfassoc_exec/src/main.rs b/wfassoc_exec/src/main.rs index ca633ea..bb84080 100644 --- a/wfassoc_exec/src/main.rs +++ b/wfassoc_exec/src/main.rs @@ -1,161 +1,33 @@ -pub(crate) mod cli; pub(crate) mod manifest; +pub(crate) mod cli; +pub(crate) mod runner; -use clap::Parser; -use cli::{Cli, Commands, Target}; -use comfy_table::Table; -use manifest::Manifest; -use std::collections::HashMap; +use std::path::Path; use std::process; use thiserror::Error as TeError; -use wfassoc::{Program, Scope, Token, View}; - -// region: Basic Types /// All errors occurs in this executable. #[derive(TeError, Debug)] enum Error { - /// Error from wfassoc core. - #[error("{0}")] - Core(#[from] wfassoc::Error), /// Error when parsing manifest TOML file. - #[error("invalid manifest file: {0}")] + #[error("{0}")] Manifest(#[from] manifest::Error), - - /// Error when specifying invalid manner name for extension. - #[error("extension {ext} associated manner {manner} is invalid in manifest file")] - InvalidMannerName { manner: String, ext: String }, } /// Result type used in this executable. type Result = std::result::Result; -// endregion - -// region: Correponding Runner - -struct Composition { - program: Program, - ext_tokens: Vec, -} - -impl Composition { - pub fn new(cli: &Cli) -> Result { - // Open file and read manifest TOML file - let mf = Manifest::from_file(&cli.config_file)?; - // Create instance - let mut program = Program::new(&mf.identifier, &cli.config_file)?; - // Setup manner - let mut manner_token_map: HashMap<&str, Token> = HashMap::new(); - for (k, v) in mf.manners.iter() { - let token = program.add_manner(v.as_str())?; - manner_token_map.insert(k.as_str(), token); - } - // Setup extension - let mut ext_tokens = Vec::new(); - for (k, v) in mf.exts.iter() { - let token = match manner_token_map.get(v.as_str()) { - Some(v) => v, - None => { - return Err(Error::InvalidMannerName { - manner: v.to_string(), - ext: k.clone(), - }); - } - }; - let token = program.add_ext(k.as_str(), *token)?; - ext_tokens.push(token); - } - // Okey - Ok(Self { - program, - ext_tokens, - }) - } - - pub fn get_program(&self) -> &Program { - &self.program - } - - pub fn iter_ext_tokens(&self) -> impl Iterator { - self.ext_tokens.iter().copied() - } -} - -fn run_register(cli: &Cli, target: &Target) -> Result<()> { - let composition = Composition::new(cli)?; - let scope = target.clone().into(); - - // Register program - let program = composition.get_program(); - program.register(scope)?; - // Link all file extensions - for token in composition.iter_ext_tokens() { - program.link_ext(token, scope)?; - } - - println!("Register OK."); +fn runner() -> Result<()> { + let raw_mf = manifest::RawManifest::from_file(Path::new(r#"D:\Repo\wfassoc\example\ppic.toml"#))?; + //let mf = raw_mf.to_checked()?; + println!("{:?}", raw_mf); Ok(()) } -fn run_unregister(cli: &Cli, target: &Target) -> Result<()> { - let composition = Composition::new(cli)?; - let scope = target.clone().into(); - - // Unlink all file extensions - let program = composition.get_program(); - for token in composition.iter_ext_tokens() { - program.unlink_ext(token, scope)?; - } - // Unregister prorgam - program.unregister(scope)?; - - println!("Unregister OK."); - Ok(()) -} - -fn run_query(cli: &Cli) -> Result<()> { - let composition = Composition::new(cli)?; - - // Show file association - let mut table = Table::new(); - table.set_header(["Extension", "Hybrid", "User", "System"]); - - let program = composition.get_program(); - for token in composition.iter_ext_tokens() { - let cell_ext = program.get_ext_str(token).unwrap(); - let cell_hybrid = program - .query_ext(token, View::Hybrid)? - .map(|pi| pi.to_string()) - .unwrap_or_default(); - let cell_user = program - .query_ext(token, View::User)? - .map(|pi| pi.to_string()) - .unwrap_or_default(); - let cell_system = program - .query_ext(token, View::System)? - .map(|pi| pi.to_string()) - .unwrap_or_default(); - - table.add_row([cell_ext, cell_hybrid, cell_user, cell_system]); - } - - println!("{table}"); - - Ok(()) -} - -// endregion - fn main() { - let cli = Cli::parse(); + let cli = cli::parse(); - let rv = match &cli.command { - Commands::Register { target } => run_register(&cli, target), - Commands::Unregister { target } => run_unregister(&cli, target), - Commands::Query => run_query(&cli), - }; - rv.unwrap_or_else(|e| { + runner().unwrap_or_else(|e| { eprintln!("Runtime error: {}.", e); process::exit(1) }); diff --git a/wfassoc_exec/src/manifest.rs b/wfassoc_exec/src/manifest.rs index 892a19e..1d2eb44 100644 --- a/wfassoc_exec/src/manifest.rs +++ b/wfassoc_exec/src/manifest.rs @@ -1,30 +1,90 @@ use serde::Deserialize; use std::collections::HashMap; +use std::path::Path; use thiserror::Error as TeError; use toml; +/// Error occurs in this module. #[derive(Debug, TeError)] #[error("{0}")] pub enum Error { + /// Io error Io(#[from] std::io::Error), + /// Toml deserialization error Toml(#[from] toml::de::Error), } -pub type Result = std::result::Result; +/// Result type used in this module. +type Result = std::result::Result; +// region: Raw Manifest + +/// Raw user input manifest. +/// +/// This manifest is the raw input of user. +/// Some fields may not be checked due to user invalid input. #[derive(Debug, Deserialize)] +pub struct RawManifest { + identifier: String, + path: String, + clsid: String, + icons: HashMap, + behaviors: HashMap, + exts: HashMap, +} + +impl RawManifest { + pub fn into_checked(&self) -> Result { + todo!() + } +} + +/// The sub-type in raw user input manifest. +#[derive(Debug, Deserialize)] +pub struct RawManifestExt { + name: String, + icon: String, + behavior: String, +} + +impl RawManifestExt { + pub fn into_checked(&self) -> Result { + todo!() + } +} + +impl RawManifest { + /// Read raw user manifest. + pub fn from_file(path: &Path) -> Result { + let contents = std::fs::read_to_string(path)?; + let config: RawManifest = toml::from_str(&contents)?; + Ok(config) + } +} + +// endregion + +// region: Checked Manifest + +/// Converted user input manifest. +/// +/// This manifest struct is prepared for final use. +/// All fields loacted in this struct is checked and ready to be used. +#[derive(Debug)] pub struct Manifest { - pub(crate) identifier: String, - pub(crate) path: String, - pub(crate) clsid: String, - pub(crate) manners: HashMap, - pub(crate) exts: HashMap, + } impl Manifest { - pub fn from_file(path: &str) -> Result { - let contents = std::fs::read_to_string(path)?; - let config: Manifest = toml::from_str(&contents)?; - Ok(config) - } -} \ No newline at end of file + +} + +pub struct ManifestExt { + +} + +impl ManifestExt { + +} + +// endregion diff --git a/wfassoc_exec/src/old_main.rs b/wfassoc_exec/src/old_main.rs new file mode 100644 index 0000000..ca633ea --- /dev/null +++ b/wfassoc_exec/src/old_main.rs @@ -0,0 +1,162 @@ +pub(crate) mod cli; +pub(crate) mod manifest; + +use clap::Parser; +use cli::{Cli, Commands, Target}; +use comfy_table::Table; +use manifest::Manifest; +use std::collections::HashMap; +use std::process; +use thiserror::Error as TeError; +use wfassoc::{Program, Scope, Token, View}; + +// region: Basic Types + +/// All errors occurs in this executable. +#[derive(TeError, Debug)] +enum Error { + /// Error from wfassoc core. + #[error("{0}")] + Core(#[from] wfassoc::Error), + /// Error when parsing manifest TOML file. + #[error("invalid manifest file: {0}")] + Manifest(#[from] manifest::Error), + + /// Error when specifying invalid manner name for extension. + #[error("extension {ext} associated manner {manner} is invalid in manifest file")] + InvalidMannerName { manner: String, ext: String }, +} + +/// Result type used in this executable. +type Result = std::result::Result; + +// endregion + +// region: Correponding Runner + +struct Composition { + program: Program, + ext_tokens: Vec, +} + +impl Composition { + pub fn new(cli: &Cli) -> Result { + // Open file and read manifest TOML file + let mf = Manifest::from_file(&cli.config_file)?; + // Create instance + let mut program = Program::new(&mf.identifier, &cli.config_file)?; + // Setup manner + let mut manner_token_map: HashMap<&str, Token> = HashMap::new(); + for (k, v) in mf.manners.iter() { + let token = program.add_manner(v.as_str())?; + manner_token_map.insert(k.as_str(), token); + } + // Setup extension + let mut ext_tokens = Vec::new(); + for (k, v) in mf.exts.iter() { + let token = match manner_token_map.get(v.as_str()) { + Some(v) => v, + None => { + return Err(Error::InvalidMannerName { + manner: v.to_string(), + ext: k.clone(), + }); + } + }; + let token = program.add_ext(k.as_str(), *token)?; + ext_tokens.push(token); + } + // Okey + Ok(Self { + program, + ext_tokens, + }) + } + + pub fn get_program(&self) -> &Program { + &self.program + } + + pub fn iter_ext_tokens(&self) -> impl Iterator { + self.ext_tokens.iter().copied() + } +} + +fn run_register(cli: &Cli, target: &Target) -> Result<()> { + let composition = Composition::new(cli)?; + let scope = target.clone().into(); + + // Register program + let program = composition.get_program(); + program.register(scope)?; + // Link all file extensions + for token in composition.iter_ext_tokens() { + program.link_ext(token, scope)?; + } + + println!("Register OK."); + Ok(()) +} + +fn run_unregister(cli: &Cli, target: &Target) -> Result<()> { + let composition = Composition::new(cli)?; + let scope = target.clone().into(); + + // Unlink all file extensions + let program = composition.get_program(); + for token in composition.iter_ext_tokens() { + program.unlink_ext(token, scope)?; + } + // Unregister prorgam + program.unregister(scope)?; + + println!("Unregister OK."); + Ok(()) +} + +fn run_query(cli: &Cli) -> Result<()> { + let composition = Composition::new(cli)?; + + // Show file association + let mut table = Table::new(); + table.set_header(["Extension", "Hybrid", "User", "System"]); + + let program = composition.get_program(); + for token in composition.iter_ext_tokens() { + let cell_ext = program.get_ext_str(token).unwrap(); + let cell_hybrid = program + .query_ext(token, View::Hybrid)? + .map(|pi| pi.to_string()) + .unwrap_or_default(); + let cell_user = program + .query_ext(token, View::User)? + .map(|pi| pi.to_string()) + .unwrap_or_default(); + let cell_system = program + .query_ext(token, View::System)? + .map(|pi| pi.to_string()) + .unwrap_or_default(); + + table.add_row([cell_ext, cell_hybrid, cell_user, cell_system]); + } + + println!("{table}"); + + Ok(()) +} + +// endregion + +fn main() { + let cli = Cli::parse(); + + let rv = match &cli.command { + Commands::Register { target } => run_register(&cli, target), + Commands::Unregister { target } => run_unregister(&cli, target), + Commands::Query => run_query(&cli), + }; + rv.unwrap_or_else(|e| { + eprintln!("Runtime error: {}.", e); + process::exit(1) + }); +} diff --git a/wfassoc_exec/src/runner.rs b/wfassoc_exec/src/runner.rs new file mode 100644 index 0000000..e69de29