Compare commits
No commits in common. "main" and "rewrite" have entirely different histories.
7 changed files with 109 additions and 179 deletions
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "open"
|
name = "open"
|
||||||
version = "2.0.1"
|
version = "1.0.0"
|
||||||
authors = ["Valerie Wolfe <sleeplessval@gmail.com>"]
|
authors = ["Valerie Wolfe <sleeplessval@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,3 @@
|
||||||
An opinionated, friendly alternative to `xdg-open` focused on opening files from a
|
An opinionated, friendly alternative to `xdg-open` focused on opening files from a
|
||||||
terminal. Easily understandable and configurable with a simple toml file.
|
terminal. Easily understandable and configurable with a simple toml file.
|
||||||
|
|
||||||
Does technically break convention. If you're using the `open` shell builtin, or
|
|
||||||
otherwise care about compliance, consider installing under a different binary name.
|
|
||||||
|
|
||||||
## Libraries
|
|
||||||
|
|
||||||
- [pico-args](https://crates.io/crates/pico-args) — argument parsing
|
|
||||||
- [toml](https://crates.io/crates/toml) — TOML file parsing
|
|
||||||
|
|
||||||
|
|
70
man/open.1
70
man/open.1
|
@ -1,70 +0,0 @@
|
||||||
.Dd $Mdocdate$
|
|
||||||
.Dt OPEN 1
|
|
||||||
.Os
|
|
||||||
.Sh NAME
|
|
||||||
.Nm open
|
|
||||||
.Nd opens files with a user-defined program.
|
|
||||||
.Sh SYNOPSIS
|
|
||||||
.Nm open
|
|
||||||
.Op Ar file
|
|
||||||
.Nm open
|
|
||||||
.Op Fl hpv
|
|
||||||
.Sh DESCRIPTION
|
|
||||||
.Nm
|
|
||||||
is a replacement for
|
|
||||||
.Xr xdg-open 1
|
|
||||||
that is more easily configurable with a TOML file. Its options are as follows:
|
|
||||||
.Bl -tag -width Ds
|
|
||||||
.It Fl h\ |\ --help
|
|
||||||
Displays a brief help text.
|
|
||||||
.It Fl p\ |\ --path
|
|
||||||
Displays the path to the configuration file being used.
|
|
||||||
.It Fl v\ |\ --version
|
|
||||||
Displays version information.
|
|
||||||
.It Ar file
|
|
||||||
The file to open. If not provided, the current directory is used.
|
|
||||||
.El
|
|
||||||
.Sh FILES
|
|
||||||
.Bl -tag -width DS
|
|
||||||
.It $HOME/.config/open.toml
|
|
||||||
The global configuration file in TOML format.
|
|
||||||
.It .open
|
|
||||||
The local configuration file in TOML format.
|
|
||||||
.Nm open
|
|
||||||
will search upwards to try to find a local file. Local configuration items are prioritized.
|
|
||||||
.El
|
|
||||||
.Sh CONFIGURATION
|
|
||||||
Files can be matched on extension or exact name. Filenames are in the 'filename' array, and extensions are in the 'extension' array.
|
|
||||||
.Pp
|
|
||||||
.Dl [[extension]]
|
|
||||||
.Dl match = (string or array; matching value(s))
|
|
||||||
.Dl command = (string; the command to open with)
|
|
||||||
.Dl shell = (boolean; decides if the command is run in the terminal)
|
|
||||||
.Pp
|
|
||||||
.Pp
|
|
||||||
The "dir" section is used to set associations for directories:
|
|
||||||
.Pp
|
|
||||||
.Dl [dir]
|
|
||||||
.Dl command = (string; the command to open with)
|
|
||||||
.Dl shell = (boolean; decides if the command is run in the terminal)
|
|
||||||
.Pp
|
|
||||||
.Sh EXIT STATUS
|
|
||||||
.Bl -tag -width Ds
|
|
||||||
.It 1
|
|
||||||
No configuration file was found.
|
|
||||||
.It 4
|
|
||||||
The target file does not exist.
|
|
||||||
.It 5
|
|
||||||
No matching configuration section was found for the target file.
|
|
||||||
.El
|
|
||||||
.Sh SEE ALSO
|
|
||||||
.Xr xdg-open 1 ,
|
|
||||||
.Xr open 3p
|
|
||||||
.Sh AUTHORS
|
|
||||||
.An -nosplit
|
|
||||||
.An Valerie Wolfe Aq Mt sleeplessval@gmail.com
|
|
||||||
.Sh BUGS
|
|
||||||
.Nm
|
|
||||||
hides the
|
|
||||||
.Xr open 3p
|
|
||||||
builtin, breaking convention and possibly some older script files.
|
|
101
src/config.rs
101
src/config.rs
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
fs::read_to_string,
|
||||||
env::{current_dir, var},
|
env::{current_dir, var},
|
||||||
path::Path
|
path::Path
|
||||||
};
|
};
|
||||||
|
@ -8,8 +9,6 @@ use toml::{
|
||||||
map::Map
|
map::Map
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{ error, util };
|
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub local: Option<Map<String, Value>>,
|
pub local: Option<Map<String, Value>>,
|
||||||
pub global: Option<Map<String, Value>>,
|
pub global: Option<Map<String, Value>>,
|
||||||
|
@ -19,51 +18,81 @@ pub struct Config {
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Config {
|
pub fn new() -> Config {
|
||||||
// initialize global config, if it exists
|
// Instantiate global config
|
||||||
let str_path_global = var("HOME").unwrap() + "/.config/open.toml";
|
let i_dir_global = String::from(var("HOME").ok().unwrap());
|
||||||
let path_global = Path::new(&str_path_global);
|
let dir_global = Path::new(i_dir_global.as_str());
|
||||||
let global = util::read_toml(path_global);
|
let i_path_global = dir_global.join(".config/open.toml");
|
||||||
|
let path_global = i_path_global.as_path();
|
||||||
// propagate up for local config, if it exists
|
let mut global = None;
|
||||||
let cwd = current_dir().unwrap();
|
if path_global.exists() {
|
||||||
let mut path_local = Path::new(&cwd);
|
let raw_conf = read_to_string(path_global).unwrap();
|
||||||
let mut local = None;
|
let toml_conf: Value = toml::from_str(raw_conf.as_str()).unwrap();
|
||||||
while let Some(parent) = path_local.parent() {
|
let toml = toml_conf.as_table().unwrap();
|
||||||
let file_local = path_local.join(".open");
|
global = Some(toml.to_owned());
|
||||||
if let Some(toml) = util::read_toml(file_local.as_path()) {
|
|
||||||
local = Some(toml);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
path_local = parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if global.is_none() && local.is_none() { error::no_configs(); }
|
// Instantiate local config, if it exists.
|
||||||
|
let i_dir_local = current_dir().unwrap();
|
||||||
|
let mut dir_local = i_dir_local.as_path();
|
||||||
|
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 = None;
|
||||||
|
loop {
|
||||||
|
if dir_local == root {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if path_local.exists() {
|
||||||
|
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();
|
||||||
|
i_path_local = dir_local.join(".open");
|
||||||
|
path_local = i_path_local.as_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
if global.is_none() && local.is_none() {
|
||||||
|
panic!("No configuration found.");
|
||||||
|
}
|
||||||
|
|
||||||
// prepare path vars
|
// prepare path vars
|
||||||
let global_path: Option<String> =
|
let global_path: Option<String>;
|
||||||
if global.is_some() { Some(path_global.to_string_lossy().into()) }
|
if global.is_some() {
|
||||||
else { None };
|
global_path = Some(path_global.to_str().unwrap().to_string());
|
||||||
let local_path =
|
} else {
|
||||||
if local.is_some() { Some(path_local.join(".open").to_string_lossy().into()) }
|
global_path = None
|
||||||
else { None };
|
}
|
||||||
|
let local_path: Option<String>;
|
||||||
Config {
|
if local.is_some() {
|
||||||
|
local_path = Some(dir_local.join(".open").to_str().unwrap().to_string());
|
||||||
|
} else {
|
||||||
|
local_path = None;
|
||||||
|
}
|
||||||
|
let output = Config {
|
||||||
global,
|
global,
|
||||||
local,
|
local,
|
||||||
local_path,
|
local_path,
|
||||||
global_path
|
global_path
|
||||||
}
|
};
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
pub fn get(&self, key: &str) -> Option<Value> {
|
pub fn get(&self, key: &str) -> Option<Value> {
|
||||||
if let Some(local) = &self.local {
|
let mut output: Option<Value> = None;
|
||||||
if let Some(result) = local.get(key) { return Some(result.to_owned()); }
|
if self.local.is_some() {
|
||||||
|
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 let Some(global) = &self.global {
|
let result = self.global.as_ref().unwrap().get(key);
|
||||||
if let Some(result) = global.get(key) { return Some(result.to_owned()); }
|
if result.is_some() {
|
||||||
|
output = Some(result.unwrap().to_owned());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return output;
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/error.rs
10
src/error.rs
|
@ -3,27 +3,27 @@ use std::{
|
||||||
process::exit
|
process::exit
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn no_configs() -> ! {
|
pub fn no_configs() {
|
||||||
println!("open: no configurations found");
|
println!("open: no configurations found");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn many_args() -> ! {
|
pub fn many_args() {
|
||||||
println!("open: too many arguments supplied");
|
println!("open: too many arguments supplied");
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn editor_unset() -> ! {
|
pub fn editor_unset() {
|
||||||
println!("open: $EDITOR is not set");
|
println!("open: $EDITOR is not set");
|
||||||
exit(3);
|
exit(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn not_found(path: &Path) -> ! {
|
pub fn not_found(path: &Path) {
|
||||||
println!("open: {path:?} does not exist");
|
println!("open: {path:?} does not exist");
|
||||||
exit(4);
|
exit(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn no_section(path: &Path) -> ! {
|
pub fn no_section(path: &Path) {
|
||||||
println!("open: no appropriate sections for {path:?}");
|
println!("open: no appropriate sections for {path:?}");
|
||||||
exit(5);
|
exit(5);
|
||||||
}
|
}
|
||||||
|
|
63
src/main.rs
63
src/main.rs
|
@ -1,17 +1,14 @@
|
||||||
use std::{
|
use std::{
|
||||||
env::current_dir,
|
env::current_dir,
|
||||||
io::{ stdout, IsTerminal },
|
|
||||||
path::Path,
|
path::Path,
|
||||||
process::{ exit, Command, Stdio }
|
process::{ Command, Stdio }
|
||||||
};
|
};
|
||||||
|
|
||||||
use pico_args::Arguments;
|
use pico_args::Arguments;
|
||||||
use toml::value::Value;
|
use toml::value::{ Array, Value };
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod util;
|
|
||||||
|
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -35,8 +32,17 @@ fn main() {
|
||||||
|
|
||||||
// path flag (-p / --path)
|
// path flag (-p / --path)
|
||||||
if args.contains(["-p", "--path"]) {
|
if args.contains(["-p", "--path"]) {
|
||||||
if let Some(local) = config.local_path { println!("{local}"); }
|
let local = config.local_path;
|
||||||
else if let Some(global) = config.global_path { println!("{global}"); }
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,17 +53,16 @@ fn main() {
|
||||||
if !target.exists() { error::not_found(&target); }
|
if !target.exists() { error::not_found(&target); }
|
||||||
|
|
||||||
// get section
|
// get section
|
||||||
// ordering: filename -> type (ext/dir) -> default
|
// ordering: filename -> type (ext/dir)
|
||||||
let mut section = None;
|
let mut section = None;
|
||||||
|
|
||||||
// by exact filename
|
// by exact filename
|
||||||
if let Some(filename) = target.file_name() {
|
let filename = target.file_name();
|
||||||
let array = config.get("filename");
|
if filename.is_some() {
|
||||||
let matches: Vec<Value>;
|
let filename_section = config.get(filename.unwrap().to_str().unwrap());
|
||||||
if let Some(Value::Array(array)) = array { matches = util::matches(array, filename.to_string_lossy().into()); }
|
if filename_section.is_some() {
|
||||||
else { matches = Vec::new(); }
|
section = filename_section;
|
||||||
|
}
|
||||||
section = if matches.len() > 0 { matches.get(0).cloned() } else { None };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle types; dir first
|
// handle types; dir first
|
||||||
|
@ -70,13 +75,24 @@ fn main() {
|
||||||
|
|
||||||
// handle types; extensions second
|
// handle types; extensions second
|
||||||
if section.is_none() {
|
if section.is_none() {
|
||||||
if let Some(extension) = target.extension() {
|
let extension = target.extension();
|
||||||
let array: Option<Value> = config.get("extension");
|
if extension.is_some() {
|
||||||
let matches: Vec<Value>;
|
let extension = extension.unwrap().to_str();
|
||||||
if let Some(Value::Array(array)) = array { matches = util::matches(array, extension.to_string_lossy().into()); }
|
|
||||||
else { matches = Vec::new(); }
|
|
||||||
|
|
||||||
section = if matches.len() > 0 { matches.get(0).cloned() } else { None };
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,11 +110,6 @@ fn main() {
|
||||||
let command = properties.get("command").unwrap().as_str().unwrap().to_string();
|
let command = properties.get("command").unwrap().as_str().unwrap().to_string();
|
||||||
let shell = properties.get("shell").unwrap_or(&Value::Boolean(false)).as_bool().unwrap();
|
let shell = properties.get("shell").unwrap_or(&Value::Boolean(false)).as_bool().unwrap();
|
||||||
|
|
||||||
if !stdout().is_terminal() {
|
|
||||||
println!("{command} {i_target}");
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// build child
|
// build child
|
||||||
let mut parts = command.split(" ");
|
let mut parts = command.split(" ");
|
||||||
let mut child = Command::new(parts.next().unwrap());
|
let mut child = Command::new(parts.next().unwrap());
|
||||||
|
|
34
src/util.rs
34
src/util.rs
|
@ -1,37 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs::read_to_string,
|
process::{ Command, Stdio }
|
||||||
path::Path
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use toml::{
|
|
||||||
self,
|
|
||||||
Value,
|
|
||||||
|
|
||||||
map::Map,
|
|
||||||
value::Array,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// gets array entries with matching "match" values.
|
|
||||||
pub fn matches(macrosection: Array, to_match: String) -> Vec<Value> {
|
|
||||||
macrosection.iter().filter(|value| {
|
|
||||||
if let Some(table) = value.as_table() {
|
|
||||||
match table.get("match").unwrap() {
|
|
||||||
Value::String(target) => *target == to_match,
|
|
||||||
Value::Array(values) => values.contains(&Value::String(to_match.clone())),
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
} else { false }
|
|
||||||
}).map(|value| value.to_owned()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_toml(path: &Path) -> Option<Map<String, Value>> {
|
|
||||||
if path.exists() {
|
|
||||||
if let Ok(raw) = read_to_string(path) {
|
|
||||||
if let Ok(Value::Table(toml)) = toml::from_str(&raw) {
|
|
||||||
return Some(toml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue