From 57ebb91cbd7d78acb4fffe943aebb2d32a276590 Mon Sep 17 00:00:00 2001 From: Valerie Date: Mon, 26 Jun 2023 18:43:39 -0400 Subject: [PATCH 1/2] full rewrite of project flow, rewrote config module to use toml --- Cargo.toml | 5 +- README.md | 26 +---- src/config.rs | 86 +++++----------- src/error.rs | 30 ++++++ src/main.rs | 272 +++++++++++++++++++++++++------------------------- src/util.rs | 5 + 6 files changed, 202 insertions(+), 222 deletions(-) create mode 100644 src/error.rs create mode 100644 src/util.rs diff --git a/Cargo.toml b/Cargo.toml index fe299e3..ea5fecc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "open" -version = "0.6.0" +version = "1.0.0" authors = ["Valerie Wolfe "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -configparser = "3.0.0" +pico-args = "0.5.0" +toml = "0.7.5" [profile.release] strip = "debuginfo" diff --git a/README.md b/README.md index 98061c7..dc3c034 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,5 @@ # open -A Rust reimplementation of `xdg-open` command. It's written to be quickly and easily customizable, features separate local and global configs, and a zero-operand command allowing the user to specify how files should be opened differently, and for opening a project, etc. +An opinionated, friendly alternative to `xdg-open` focused on opening files from a +terminal. Easily understandable and configurable with a simple toml file. -For example, for - -```ini -[open] -# zero-operand command -command = atom . -# use $EDITOR to edit files without specified commands? -use_editor = true - -[.md] -command = typora - -[.rs] -command = atom - -[filename:.gitignore] -command = vim -shell = true -``` - -I can use `open` to open the directory in Atom, or I could use `open src/main.rs` to open `main.rs` in Atom, and I can specify these on a per-project basis. - -For directories with a local config, any missing values will be filled in by the global config (`~/.config/open.conf`), which means local configs can be shorter. diff --git a/src/config.rs b/src/config.rs index 6588dab..35e7cb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,11 +4,14 @@ use std::{ path::Path }; -use configparser::ini::Ini; +use toml::{ + Value, + map::Map +}; pub struct Config { - pub local: Option, - pub global: Option, + pub local: Option>, + pub global: Option>, pub local_path: Option, pub global_path: Option } @@ -18,14 +21,14 @@ impl Config { // Instantiate global config let i_dir_global = String::from(var("HOME").ok().unwrap()); let dir_global = Path::new(i_dir_global.as_str()); - let i_path_global = dir_global.join(".config/open.conf"); + let i_path_global = dir_global.join(".config/open.toml"); let path_global = i_path_global.as_path(); - let mut global: Option = None; + let mut global = None; if path_global.exists() { - let i_global = read_to_string(path_global).unwrap(); - let mut tmp = Ini::new(); - tmp.read(i_global).ok(); - global = Some(tmp); + let raw_conf = read_to_string(path_global).unwrap(); + let toml_conf: Value = toml::from_str(raw_conf.as_str()).unwrap(); + let toml = toml_conf.as_table().unwrap(); + global = Some(toml.to_owned()); } // Instantiate local config, if it exists. @@ -34,16 +37,16 @@ impl Config { let mut i_path_local = dir_local.join(".open"); let mut path_local = i_path_local.as_path(); let root = Path::new("/"); - let mut local: Option = None; + let mut local = None; loop { if dir_local == root { break; } if path_local.exists() { - let i_local = read_to_string(path_local).unwrap(); - let mut tmp = Ini::new(); - tmp.read(i_local).ok(); - local = Some(tmp); + let raw_conf = read_to_string(path_local).unwrap(); + let toml_conf: Value = toml::from_str(raw_conf.as_str()).unwrap(); + let toml = toml_conf.as_table().unwrap(); + local = Some(toml.to_owned()); break; } dir_local = dir_local.parent().unwrap(); @@ -76,55 +79,20 @@ impl Config { }; return output; } - pub fn get(&self, section: &str, key: &str) -> Option { - let mut output: Option = None; + pub fn get(&self, key: &str) -> Option { + let mut output: Option = None; if self.local.is_some() { - output = self.local.as_ref().unwrap().get(section, key); + let result = self.local.as_ref().unwrap().get(key); + if result.is_some() { + output = Some(result.unwrap().to_owned()); + } } if output.is_none() && self.global.is_some() { - output = self.global.as_ref().unwrap().get(section, key); + let result = self.global.as_ref().unwrap().get(key); + if result.is_some() { + output = Some(result.unwrap().to_owned()); + } } return output; } - pub fn getbool(&self, section: &str, key: &str) -> Option { - let mut output = None; - if self.local.is_some() { - let i_out = self.local.as_ref().unwrap().getbool(section, key); - output = i_out.unwrap_or(None); - } - if output.is_none() && self.global.is_some() { - let i_out = self.global.as_ref().unwrap().getbool(section, key); - output = i_out.unwrap_or(None); - } - return output; - } - pub fn add(&mut self, section: &str, key: &str, value: String) { - let mut ini: Ini; - let local = self.local.is_some(); - if local { - ini = self.local.clone().unwrap(); - } else { - ini = self.global.clone().unwrap(); - } - ini.set(section, key, Some(value)); - if local { - self.local = Some(ini); - } else { - self.global = Some(ini); - } - } - pub fn add_global(&mut self, section: &str, key: &str, value: String) { - let mut global = self.global.clone().unwrap(); - global.set(section, key, Some(value)); - self.global = Some(global); - } - pub fn write(&self) -> std::io::Result<()> { - let mut path = self.local_path.as_ref(); - if path.is_some() { - let result = self.local.as_ref().unwrap().write(path.unwrap().as_str()); - return result; - } - path = self.global_path.as_ref(); - self.global.as_ref().unwrap().write(path.unwrap().as_str()) - } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..3243bc5 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,30 @@ +use std::{ + path::Path, + process::exit +}; + +pub fn no_configs() { + println!("open: no configurations found"); + exit(1); +} + +pub fn many_args() { + println!("open: too many arguments supplied"); + exit(2); +} + +pub fn editor_unset() { + println!("open: $EDITOR is not set"); + exit(3); +} + +pub fn not_found(path: &Path) { + println!("open: {path:?} does not exist"); + exit(4); +} + +pub fn no_section(path: &Path) { + println!("open: no appropriate sections for {path:?}"); + exit(5); +} + diff --git a/src/main.rs b/src/main.rs index 754e85d..da8f92e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,157 +1,155 @@ use std::{ - env::{args, current_dir, var}, + env::current_dir, path::Path, - process::{Command, exit, Stdio} + process::{ Command, Stdio } }; +use pico_args::Arguments; +use toml::value::{ Array, Value }; + mod config; +mod error; use config::Config; fn main() { - // Prepare config file + let mut args = Arguments::from_env(); + + // help flag (-h / --help) + if args.contains(["-h", "--help"]) { + help_text(); + return; + } + // version flag (-v / --version) + if args.contains(["-v", "--version"]) { + println!("{}", env!("CARGO_PKG_VERSION")); + return; + } + + // prepare configs let i_dir = current_dir().unwrap(); let dir = i_dir.as_path(); - let mut config = Config::new(); + let config = Config::new(); - // Parse arguments and handle them. - let args: Vec = args().collect(); + // path flag (-p / --path) + if args.contains(["-p", "--path"]) { + let local = config.local_path; + let global = config.global_path; + if local.is_some() { + println!("{}", local.unwrap()); + return; + } + if global.is_some() { + println!("{}", global.unwrap()); + return; + } + error::no_configs(); + return; + } - let mut error: Option = None; - let mut file_operand = false; - for arg in &args[1..] { - match arg.as_str() { - "-h" | - "--help" => { - println!("open v0.6.0 -Valerie Wolfe -A Rust reimplementation of \"xdg-open\" configurable with an ini file. + // get target + let arg_target = args.subcommand().unwrap(); + let i_target = arg_target.unwrap_or(String::from(".")); + let target = Path::new(&i_target); + if !target.exists() { error::not_found(&target); } -USAGE: - open [FLAGS] [OPERAND] + // get section + // ordering: filename -> type (ext/dir) + let mut section = None; -FLAGS: - -h, --help Prints this help text - -a, --add Add a handler for a operand type - -p, --path Prints the config path used - -v, --version Prints the version number -"); - return; - }, - "-a" | - "--add" => { - if args.len() < 4 { - println!("open: too few arguments."); - exit(1); - } - let ext = args.get(2).unwrap(); - let exe = args.get(3).unwrap(); - let tmp = args.get(4); - let shell = tmp.is_some() && tmp.unwrap() == "shell"; - println!("{} {} {}", ext, exe, shell); - config.add(ext, "command", exe.to_string()); - if shell { - config.add(ext, "shell", "true".to_string()); - } - config.write().ok(); - return; - }, - "-p" | - "--path" => { - let local = config.local_path; - if local.is_some() { - println!("{}", local.unwrap()); - } else { - println!("{}", config.global_path.unwrap()); - } - return; - }, - _ => { - if file_operand { - error = Some("open: too many file operands.".to_string()); - } else { - file_operand = true; - } + // by exact filename + let filename = target.file_name(); + if filename.is_some() { + let filename_section = config.get(filename.unwrap().to_str().unwrap()); + if filename_section.is_some() { + section = filename_section; + } + } + + // handle types; dir first + if section.is_none() && target.is_dir() { + let dir_section = config.get("dir"); + if dir_section.is_some() { + section = dir_section; + } + } + + // handle types; extensions second + if section.is_none() { + let extension = target.extension(); + if extension.is_some() { + let extension = extension.unwrap().to_str(); + + // pull extension array and filter matches + let i_macrosection: Option = config.get("extension"); + let macrosection: Array = i_macrosection.unwrap().as_array().unwrap().to_owned(); + let matches = macrosection.iter().filter(|value| { + let table = value.as_table().unwrap(); + let i_target = table.get("match").unwrap(); + let target = i_target.as_str(); + target == extension + }).map(|value| value.to_owned() ); + + let sections: Vec = matches.collect(); + if sections.len() > 0 { + section = sections.get(0).cloned(); } } } - if error.is_some() { - println!("{}", error.unwrap()); - exit(1); - } - let default = ".".to_string(); - let arg_target = args.get(1); - let i_target = arg_target.unwrap_or(&default); - let target = Path::new(i_target); - if !target.exists() { - println!("open: \"{}\" does not exist.", i_target); - exit(1); - } - let i_ext = target.extension(); - let i_filename: String; - let ext: &str; - if target.is_dir() { - if arg_target.is_none() { - ext = "open"; - } else { - ext = "dir"; - } - } else { - if i_ext.is_none() { - i_filename = ["filename", target.file_name().unwrap().to_str().unwrap()].join(":"); - } else { - i_filename = [".", i_ext.unwrap().to_str().unwrap()].join(""); - } - ext = i_filename.as_str(); - } - let i_exe = config.get(ext, "command"); - if i_exe.is_none() { - let use_editor = config.getbool("open", "use_editor"); - if use_editor.unwrap_or(false) { - let i_editor = var("EDITOR"); - if i_editor.is_err() { - println!("open: encountered an error trying to access $EDITOR"); - exit(1); - } - let editor = i_editor.ok().unwrap(); - if editor.is_empty() { - println!("open: $EDITOR is not defined."); - exit(1); - } - let mut command = Command::new(editor); - command.args(vec![i_target]); - command.stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .stdin(Stdio::inherit()); - command.output().ok(); - exit(0); - } else { - match ext { - "dir" => println!("open: no command specified for directories."), - _ => println!("open: no command specified for \"{}\" files.", ext) - } - exit(1); - } - } - let exe = i_exe.unwrap(); - let mut parts = exe.split(" "); - let mut command = Command::new(parts.next().unwrap()); - let mut param: Vec<&str> = vec![]; - for part in parts { - param.push(part); - } - param.push(i_target); - command.args(param) - .current_dir(dir); - let is_sh = config.getbool(ext, "shell").unwrap_or(false); - if is_sh { - command.stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .stdin(Stdio::inherit()); - command.output().ok(); - } else { - command.stdout(Stdio::null()) - .stderr(Stdio::null()); - command.spawn().ok(); + // default or fail on missing session + if section.is_none() { + let default = config.get("default"); + if default.is_some() { section = default; } + else { error::no_section(target); } + } + + // unwrap our section + let properties = section.unwrap(); + + // collect properties + let command = properties.get("command").unwrap().as_str().unwrap().to_string(); + let shell = properties.get("shell").unwrap_or(&Value::Boolean(false)).as_bool().unwrap(); + + // build child + let mut parts = command.split(" "); + let mut child = Command::new(parts.next().unwrap()); + let mut param: Vec<&str> = parts.collect(); + param.push(&i_target); + child + .args(param) + .current_dir(dir); + + // shell processes inherit stdio (and use `output()`) + if shell { + child + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + child.output().ok(); + } + // non-shell processes should redirect to dev/null (and use `spawn()`) + else { + child + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + child.spawn().ok(); } } + +pub fn help_text() { + println!("open v{} +Valerie Wolfe +A Rust reimagining of \"xdg-open\" configurable with an toml file. + +usage: open [flags] + +flags: + -h, --help Prints this help text + -p, --path Prints the path to the config file + -v, --version Prints the version number +", + env!("CARGO_PKG_VERSION") + ); +} + diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..069382e --- /dev/null +++ b/src/util.rs @@ -0,0 +1,5 @@ +use std::{ + process::{ Command, Stdio } +}; + + From 1d7515147ad2eb3553c49fb2f4b69a16b906fea5 Mon Sep 17 00:00:00 2001 From: Valerie Date: Mon, 26 Jun 2023 18:44:56 -0400 Subject: [PATCH 2/2] created release profile --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ea5fecc..514f571 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,10 @@ pico-args = "0.5.0" toml = "0.7.5" [profile.release] +opt-level = 's' +codegen-units = 1 +debug = false +lto = true +panic = "abort" strip = "debuginfo"