1
0

feat: upgrade wfassoc_exec

This commit is contained in:
2025-12-30 23:21:01 +08:00
parent 7a03d38f68
commit 27ab6721de
13 changed files with 331 additions and 182 deletions

3
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"

View File

@@ -1,5 +1,5 @@
[package]
name = "wfassoc_dylib"
name = "wfassoc_cdylib"
version = "0.1.0"
authors = ["yyc12345"]
edition = "2024"

View File

@@ -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"

View File

@@ -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<Target> 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<String>,
},
#[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<String>,
},
#[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();
}

View File

@@ -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<T> = std::result::Result<T, Error>;
// endregion
// region: Correponding Runner
struct Composition {
program: Program,
ext_tokens: Vec<Token>,
}
impl Composition {
pub fn new(cli: &Cli) -> Result<Composition> {
// 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<Item = Token> {
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)
});

View File

@@ -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<T> = std::result::Result<T, Error>;
/// Result type used in this module.
type Result<T> = std::result::Result<T, Error>;
// 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<String, String>,
behaviors: HashMap<String, String>,
exts: HashMap<String, RawManifestExt>,
}
impl RawManifest {
pub fn into_checked(&self) -> Result<Manifest> {
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<ManifestExt> {
todo!()
}
}
impl RawManifest {
/// Read raw user manifest.
pub fn from_file(path: &Path) -> Result<RawManifest> {
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<String, String>,
pub(crate) exts: HashMap<String, String>,
}
impl Manifest {
pub fn from_file(path: &str) -> Result<Manifest> {
let contents = std::fs::read_to_string(path)?;
let config: Manifest = toml::from_str(&contents)?;
Ok(config)
}
}
}
pub struct ManifestExt {
}
impl ManifestExt {
}
// endregion

View File

@@ -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<T> = std::result::Result<T, Error>;
// endregion
// region: Correponding Runner
struct Composition {
program: Program,
ext_tokens: Vec<Token>,
}
impl Composition {
pub fn new(cli: &Cli) -> Result<Composition> {
// 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<Item = Token> {
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)
});
}

View File