Merge pull request 'full rewrite' (#1) from rewrite into main
Reviewed-on: #1
This commit is contained in:
commit
c0c9fc2813
6 changed files with 207 additions and 222 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -1,14 +1,20 @@
|
||||||
[package]
|
[package]
|
||||||
name = "open"
|
name = "open"
|
||||||
version = "0.6.0"
|
version = "1.0.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]
|
||||||
configparser = "3.0.0"
|
pico-args = "0.5.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"
|
||||||
|
|
||||||
|
|
26
README.md
26
README.md
|
@ -1,27 +1,5 @@
|
||||||
# open
|
# 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.
|
|
||||||
|
|
|
@ -4,11 +4,14 @@ use std::{
|
||||||
path::Path
|
path::Path
|
||||||
};
|
};
|
||||||
|
|
||||||
use configparser::ini::Ini;
|
use toml::{
|
||||||
|
Value,
|
||||||
|
map::Map
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub local: Option<Ini>,
|
pub local: Option<Map<String, Value>>,
|
||||||
pub global: Option<Ini>,
|
pub global: Option<Map<String, Value>>,
|
||||||
pub local_path: Option<String>,
|
pub local_path: Option<String>,
|
||||||
pub global_path: Option<String>
|
pub global_path: Option<String>
|
||||||
}
|
}
|
||||||
|
@ -18,14 +21,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.conf");
|
let i_path_global = dir_global.join(".config/open.toml");
|
||||||
let path_global = i_path_global.as_path();
|
let path_global = i_path_global.as_path();
|
||||||
let mut global: Option<Ini> = None;
|
let mut global = None;
|
||||||
if path_global.exists() {
|
if path_global.exists() {
|
||||||
let i_global = read_to_string(path_global).unwrap();
|
let raw_conf = read_to_string(path_global).unwrap();
|
||||||
let mut tmp = Ini::new();
|
let toml_conf: Value = toml::from_str(raw_conf.as_str()).unwrap();
|
||||||
tmp.read(i_global).ok();
|
let toml = toml_conf.as_table().unwrap();
|
||||||
global = Some(tmp);
|
global = Some(toml.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate local config, if it exists.
|
// Instantiate local config, if it exists.
|
||||||
|
@ -34,16 +37,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: Option<Ini> = None;
|
let mut local = None;
|
||||||
loop {
|
loop {
|
||||||
if dir_local == root {
|
if dir_local == root {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if path_local.exists() {
|
if path_local.exists() {
|
||||||
let i_local = read_to_string(path_local).unwrap();
|
let raw_conf = read_to_string(path_local).unwrap();
|
||||||
let mut tmp = Ini::new();
|
let toml_conf: Value = toml::from_str(raw_conf.as_str()).unwrap();
|
||||||
tmp.read(i_local).ok();
|
let toml = toml_conf.as_table().unwrap();
|
||||||
local = Some(tmp);
|
local = Some(toml.to_owned());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dir_local = dir_local.parent().unwrap();
|
dir_local = dir_local.parent().unwrap();
|
||||||
|
@ -76,55 +79,20 @@ impl Config {
|
||||||
};
|
};
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
pub fn get(&self, section: &str, key: &str) -> Option<String> {
|
pub fn get(&self, key: &str) -> Option<Value> {
|
||||||
let mut output: Option<String> = None;
|
let mut output: Option<Value> = None;
|
||||||
if self.local.is_some() {
|
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() {
|
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;
|
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
30
src/error.rs
Normal file
30
src/error.rs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
|
272
src/main.rs
272
src/main.rs
|
@ -1,157 +1,155 @@
|
||||||
use std::{
|
use std::{
|
||||||
env::{args, current_dir, var},
|
env::current_dir,
|
||||||
path::Path,
|
path::Path,
|
||||||
process::{Command, exit, Stdio}
|
process::{ Command, 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() {
|
||||||
// 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 i_dir = current_dir().unwrap();
|
||||||
let dir = i_dir.as_path();
|
let dir = i_dir.as_path();
|
||||||
let mut config = Config::new();
|
let config = Config::new();
|
||||||
|
|
||||||
// Parse arguments and handle them.
|
// path flag (-p / --path)
|
||||||
let args: Vec<String> = args().collect();
|
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<String> = None;
|
// get target
|
||||||
let mut file_operand = false;
|
let arg_target = args.subcommand().unwrap();
|
||||||
for arg in &args[1..] {
|
let i_target = arg_target.unwrap_or(String::from("."));
|
||||||
match arg.as_str() {
|
let target = Path::new(&i_target);
|
||||||
"-h" |
|
if !target.exists() { error::not_found(&target); }
|
||||||
"--help" => {
|
|
||||||
println!("open v0.6.0
|
|
||||||
Valerie Wolfe <sleeplessval@gmail.com>
|
|
||||||
A Rust reimplementation of \"xdg-open\" configurable with an ini file.
|
|
||||||
|
|
||||||
USAGE:
|
// get section
|
||||||
open [FLAGS] [OPERAND]
|
// ordering: filename -> type (ext/dir)
|
||||||
|
let mut section = None;
|
||||||
|
|
||||||
FLAGS:
|
// by exact filename
|
||||||
-h, --help Prints this help text
|
let filename = target.file_name();
|
||||||
-a, --add Add a handler for a operand type
|
if filename.is_some() {
|
||||||
-p, --path Prints the config path used
|
let filename_section = config.get(filename.unwrap().to_str().unwrap());
|
||||||
-v, --version Prints the version number
|
if filename_section.is_some() {
|
||||||
");
|
section = filename_section;
|
||||||
return;
|
}
|
||||||
},
|
}
|
||||||
"-a" |
|
|
||||||
"--add" => {
|
// handle types; dir first
|
||||||
if args.len() < 4 {
|
if section.is_none() && target.is_dir() {
|
||||||
println!("open: too few arguments.");
|
let dir_section = config.get("dir");
|
||||||
exit(1);
|
if dir_section.is_some() {
|
||||||
}
|
section = dir_section;
|
||||||
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";
|
// handle types; extensions second
|
||||||
println!("{} {} {}", ext, exe, shell);
|
if section.is_none() {
|
||||||
config.add(ext, "command", exe.to_string());
|
let extension = target.extension();
|
||||||
if shell {
|
if extension.is_some() {
|
||||||
config.add(ext, "shell", "true".to_string());
|
let extension = extension.unwrap().to_str();
|
||||||
}
|
|
||||||
config.write().ok();
|
// pull extension array and filter matches
|
||||||
return;
|
let i_macrosection: Option<Value> = config.get("extension");
|
||||||
},
|
let macrosection: Array = i_macrosection.unwrap().as_array().unwrap().to_owned();
|
||||||
"-p" |
|
let matches = macrosection.iter().filter(|value| {
|
||||||
"--path" => {
|
let table = value.as_table().unwrap();
|
||||||
let local = config.local_path;
|
let i_target = table.get("match").unwrap();
|
||||||
if local.is_some() {
|
let target = i_target.as_str();
|
||||||
println!("{}", local.unwrap());
|
target == extension
|
||||||
} else {
|
}).map(|value| value.to_owned() );
|
||||||
println!("{}", config.global_path.unwrap());
|
|
||||||
}
|
let sections: Vec<Value> = matches.collect();
|
||||||
return;
|
if sections.len() > 0 {
|
||||||
},
|
section = sections.get(0).cloned();
|
||||||
_ => {
|
|
||||||
if file_operand {
|
|
||||||
error = Some("open: too many file operands.".to_string());
|
|
||||||
} else {
|
|
||||||
file_operand = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
// default or fail on missing session
|
||||||
if is_sh {
|
if section.is_none() {
|
||||||
command.stdout(Stdio::inherit())
|
let default = config.get("default");
|
||||||
.stderr(Stdio::inherit())
|
if default.is_some() { section = default; }
|
||||||
.stdin(Stdio::inherit());
|
else { error::no_section(target); }
|
||||||
command.output().ok();
|
}
|
||||||
} else {
|
|
||||||
command.stdout(Stdio::null())
|
// unwrap our section
|
||||||
.stderr(Stdio::null());
|
let properties = section.unwrap();
|
||||||
command.spawn().ok();
|
|
||||||
|
// 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 <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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
5
src/util.rs
Normal file
5
src/util.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use std::{
|
||||||
|
process::{ Command, Stdio }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue