Compare commits

...

6 commits
v1.0.0 ... main

7 changed files with 175 additions and 105 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "open" name = "open"
version = "1.0.0" version = "2.0.1"
authors = ["Valerie Wolfe <sleeplessval@gmail.com>"] authors = ["Valerie Wolfe <sleeplessval@gmail.com>"]
edition = "2018" edition = "2018"

View file

@ -3,3 +3,11 @@
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 Normal file
View file

@ -0,0 +1,70 @@
.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.

View file

@ -1,5 +1,4 @@
use std::{ use std::{
fs::read_to_string,
env::{current_dir, var}, env::{current_dir, var},
path::Path path::Path
}; };
@ -9,6 +8,8 @@ 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>>,
@ -18,81 +19,51 @@ pub struct Config {
impl Config { impl Config {
pub fn new() -> Config { pub fn new() -> Config {
// Instantiate global config // initialize global config, if it exists
let i_dir_global = String::from(var("HOME").ok().unwrap()); let str_path_global = var("HOME").unwrap() + "/.config/open.toml";
let dir_global = Path::new(i_dir_global.as_str()); let path_global = Path::new(&str_path_global);
let i_path_global = dir_global.join(".config/open.toml"); let global = util::read_toml(path_global);
let path_global = i_path_global.as_path();
let mut global = None;
if path_global.exists() {
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. // propagate up for local config, if it exists
let i_dir_local = current_dir().unwrap(); let cwd = current_dir().unwrap();
let mut dir_local = i_dir_local.as_path(); let mut path_local = Path::new(&cwd);
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; let mut local = None;
loop { while let Some(parent) = path_local.parent() {
if dir_local == root { let file_local = path_local.join(".open");
if let Some(toml) = util::read_toml(file_local.as_path()) {
local = Some(toml);
break; 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() { path_local = parent;
panic!("No configuration found.");
} }
if global.is_none() && local.is_none() { error::no_configs(); }
// prepare path vars // prepare path vars
let global_path: Option<String>; let global_path: Option<String> =
if global.is_some() { if global.is_some() { Some(path_global.to_string_lossy().into()) }
global_path = Some(path_global.to_str().unwrap().to_string()); else { None };
} else { let local_path =
global_path = None if local.is_some() { Some(path_local.join(".open").to_string_lossy().into()) }
} else { None };
let local_path: Option<String>;
if local.is_some() { Config {
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> {
let mut output: Option<Value> = None; if let Some(local) = &self.local {
if self.local.is_some() { if let Some(result) = local.get(key) { return Some(result.to_owned()); }
let result = self.local.as_ref().unwrap().get(key);
if result.is_some() {
output = Some(result.unwrap().to_owned());
} }
if let Some(global) = &self.global {
if let Some(result) = global.get(key) { return Some(result.to_owned()); }
} }
if output.is_none() && self.global.is_some() {
let result = self.global.as_ref().unwrap().get(key); None
if result.is_some() {
output = Some(result.unwrap().to_owned());
}
}
return output;
} }
} }

View file

@ -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);
} }

View file

@ -1,14 +1,17 @@
use std::{ use std::{
env::current_dir, env::current_dir,
io::{ stdout, IsTerminal },
path::Path, path::Path,
process::{ Command, Stdio } process::{ exit, Command, Stdio }
}; };
use pico_args::Arguments; use pico_args::Arguments;
use toml::value::{ Array, Value }; use toml::value::Value;
mod config; mod config;
mod error; mod error;
mod util;
use config::Config; use config::Config;
fn main() { fn main() {
@ -32,17 +35,8 @@ fn main() {
// path flag (-p / --path) // path flag (-p / --path)
if args.contains(["-p", "--path"]) { if args.contains(["-p", "--path"]) {
let local = config.local_path; if let Some(local) = config.local_path { println!("{local}"); }
let global = config.global_path; else if let Some(global) = config.global_path { println!("{global}"); }
if local.is_some() {
println!("{}", local.unwrap());
return;
}
if global.is_some() {
println!("{}", global.unwrap());
return;
}
error::no_configs();
return; return;
} }
@ -53,16 +47,17 @@ 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) // ordering: filename -> type (ext/dir) -> default
let mut section = None; let mut section = None;
// by exact filename // by exact filename
let filename = target.file_name(); if let Some(filename) = target.file_name() {
if filename.is_some() { let array = config.get("filename");
let filename_section = config.get(filename.unwrap().to_str().unwrap()); let matches: Vec<Value>;
if filename_section.is_some() { if let Some(Value::Array(array)) = array { matches = util::matches(array, filename.to_string_lossy().into()); }
section = filename_section; else { matches = Vec::new(); }
}
section = if matches.len() > 0 { matches.get(0).cloned() } else { None };
} }
// handle types; dir first // handle types; dir first
@ -75,24 +70,13 @@ fn main() {
// handle types; extensions second // handle types; extensions second
if section.is_none() { if section.is_none() {
let extension = target.extension(); if let Some(extension) = target.extension() {
if extension.is_some() { let array: Option<Value> = config.get("extension");
let extension = extension.unwrap().to_str(); let matches: Vec<Value>;
if let Some(Value::Array(array)) = array { matches = util::matches(array, extension.to_string_lossy().into()); }
else { matches = Vec::new(); }
// pull extension array and filter matches section = if matches.len() > 0 { matches.get(0).cloned() } else { None };
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();
}
} }
} }
@ -110,6 +94,11 @@ 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());

View file

@ -1,5 +1,37 @@
use std::{ use std::{
process::{ Command, Stdio } fs::read_to_string,
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
}