Compare commits

..

No commits in common. "1d7515147ad2eb3553c49fb2f4b69a16b906fea5" and "5b20a34c7d6d6660390bfb60759d56540c0ead3b" have entirely different histories.

6 changed files with 221 additions and 206 deletions

View file

@ -1,20 +1,14 @@
[package] [package]
name = "open" name = "open"
version = "1.0.0" version = "0.6.0"
authors = ["Valerie Wolfe <sleeplessval@gmail.com>"] authors = ["Valerie Wolfe <sleeplessval@gmail.com>"]
edition = "2018" edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
pico-args = "0.5.0" configparser = "3.0.0"
toml = "0.7.5"
[profile.release] [profile.release]
opt-level = 's'
codegen-units = 1
debug = false
lto = true
panic = "abort"
strip = "debuginfo" strip = "debuginfo"

View file

@ -1,5 +1,27 @@
# open # open
An opinionated, friendly alternative to `xdg-open` focused on opening files from a 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.
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.

View file

@ -4,14 +4,11 @@ use std::{
path::Path path::Path
}; };
use toml::{ use configparser::ini::Ini;
Value,
map::Map
};
pub struct Config { pub struct Config {
pub local: Option<Map<String, Value>>, pub local: Option<Ini>,
pub global: Option<Map<String, Value>>, pub global: Option<Ini>,
pub local_path: Option<String>, pub local_path: Option<String>,
pub global_path: Option<String> pub global_path: Option<String>
} }
@ -21,14 +18,14 @@ impl Config {
// Instantiate global config // Instantiate global config
let i_dir_global = String::from(var("HOME").ok().unwrap()); let i_dir_global = String::from(var("HOME").ok().unwrap());
let dir_global = Path::new(i_dir_global.as_str()); let dir_global = Path::new(i_dir_global.as_str());
let i_path_global = dir_global.join(".config/open.toml"); let i_path_global = dir_global.join(".config/open.conf");
let path_global = i_path_global.as_path(); let path_global = i_path_global.as_path();
let mut global = None; let mut global: Option<Ini> = None;
if path_global.exists() { if path_global.exists() {
let raw_conf = read_to_string(path_global).unwrap(); let i_global = read_to_string(path_global).unwrap();
let toml_conf: Value = toml::from_str(raw_conf.as_str()).unwrap(); let mut tmp = Ini::new();
let toml = toml_conf.as_table().unwrap(); tmp.read(i_global).ok();
global = Some(toml.to_owned()); global = Some(tmp);
} }
// Instantiate local config, if it exists. // Instantiate local config, if it exists.
@ -37,16 +34,16 @@ impl Config {
let mut i_path_local = dir_local.join(".open"); let mut i_path_local = dir_local.join(".open");
let mut path_local = i_path_local.as_path(); let mut path_local = i_path_local.as_path();
let root = Path::new("/"); let root = Path::new("/");
let mut local = None; let mut local: Option<Ini> = None;
loop { loop {
if dir_local == root { if dir_local == root {
break; break;
} }
if path_local.exists() { if path_local.exists() {
let raw_conf = read_to_string(path_local).unwrap(); let i_local = read_to_string(path_local).unwrap();
let toml_conf: Value = toml::from_str(raw_conf.as_str()).unwrap(); let mut tmp = Ini::new();
let toml = toml_conf.as_table().unwrap(); tmp.read(i_local).ok();
local = Some(toml.to_owned()); local = Some(tmp);
break; break;
} }
dir_local = dir_local.parent().unwrap(); dir_local = dir_local.parent().unwrap();
@ -79,20 +76,55 @@ impl Config {
}; };
return output; return output;
} }
pub fn get(&self, key: &str) -> Option<Value> { pub fn get(&self, section: &str, key: &str) -> Option<String> {
let mut output: Option<Value> = None; let mut output: Option<String> = None;
if self.local.is_some() { if self.local.is_some() {
let result = self.local.as_ref().unwrap().get(key); output = self.local.as_ref().unwrap().get(section, key);
if result.is_some() {
output = Some(result.unwrap().to_owned());
}
} }
if output.is_none() && self.global.is_some() { if output.is_none() && self.global.is_some() {
let result = self.global.as_ref().unwrap().get(key); output = self.global.as_ref().unwrap().get(section, key);
if result.is_some() {
output = Some(result.unwrap().to_owned());
}
} }
return output; return output;
} }
pub fn getbool(&self, section: &str, key: &str) -> Option<bool> {
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())
}
} }

View file

@ -1,30 +0,0 @@
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);
}

View file

@ -1,155 +1,157 @@
use std::{ use std::{
env::current_dir, env::{args, current_dir, var},
path::Path, path::Path,
process::{ Command, Stdio } process::{Command, exit, Stdio}
}; };
use pico_args::Arguments;
use toml::value::{ Array, Value };
mod config; mod config;
mod error;
use config::Config; use config::Config;
fn main() { fn main() {
let mut args = Arguments::from_env(); // Prepare config file
// 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 i_dir = current_dir().unwrap();
let dir = i_dir.as_path(); let dir = i_dir.as_path();
let config = Config::new(); let mut config = Config::new();
// path flag (-p / --path) // Parse arguments and handle them.
if args.contains(["-p", "--path"]) { let args: Vec<String> = args().collect();
let mut error: Option<String> = None;
let mut file_operand = false;
for arg in &args[1..] {
match arg.as_str() {
"-h" |
"--help" => {
println!("open v0.6.0
Valerie Wolfe <sleeplessval@gmail.com>
A Rust reimplementation of \"xdg-open\" configurable with an ini file.
USAGE:
open [FLAGS] [OPERAND]
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; let local = config.local_path;
let global = config.global_path;
if local.is_some() { if local.is_some() {
println!("{}", local.unwrap()); println!("{}", local.unwrap());
} else {
println!("{}", config.global_path.unwrap());
}
return; return;
} },
if global.is_some() { _ => {
println!("{}", global.unwrap()); if file_operand {
return; error = Some("open: too many file operands.".to_string());
} } else {
error::no_configs(); file_operand = true;
return;
}
// 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); }
// get section
// ordering: filename -> type (ext/dir)
let mut section = None;
// 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<Value> = 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<Value> = matches.collect();
if sections.len() > 0 {
section = sections.get(0).cloned();
} }
} }
} }
// 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); }
} }
if error.is_some() {
// unwrap our section println!("{}", error.unwrap());
let properties = section.unwrap(); exit(1);
}
// collect properties let default = ".".to_string();
let command = properties.get("command").unwrap().as_str().unwrap().to_string(); let arg_target = args.get(1);
let shell = properties.get("shell").unwrap_or(&Value::Boolean(false)).as_bool().unwrap(); let i_target = arg_target.unwrap_or(&default);
let target = Path::new(i_target);
// build child if !target.exists() {
let mut parts = command.split(" "); println!("open: \"{}\" does not exist.", i_target);
let mut child = Command::new(parts.next().unwrap()); exit(1);
let mut param: Vec<&str> = parts.collect(); }
param.push(&i_target); let i_ext = target.extension();
child let i_filename: String;
.args(param) 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); .current_dir(dir);
// shell processes inherit stdio (and use `output()`) let is_sh = config.getbool(ext, "shell").unwrap_or(false);
if shell { if is_sh {
child command.stdout(Stdio::inherit())
.stdin(Stdio::inherit()) .stderr(Stdio::inherit())
.stdout(Stdio::inherit()) .stdin(Stdio::inherit());
.stderr(Stdio::inherit()); command.output().ok();
child.output().ok(); } else {
} command.stdout(Stdio::null())
// non-shell processes should redirect to dev/null (and use `spawn()`)
else {
child
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null()); .stderr(Stdio::null());
child.spawn().ok(); command.spawn().ok();
} }
} }
pub fn help_text() {
println!("open v{}
Valerie Wolfe <sleeplessval@gmail.com>
A Rust reimagining of \"xdg-open\" configurable with an toml file.
usage: open [flags] <target>
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")
);
}

View file

@ -1,5 +0,0 @@
use std::{
process::{ Command, Stdio }
};