mirror of
https://github.com/kittywitch/fzfdapter.git
synced 2026-02-09 06:39:19 -08:00
feat: consolidate, license
This commit is contained in:
commit
0ade7d9539
19 changed files with 2088 additions and 0 deletions
76
src/cache.rs
Normal file
76
src/cache.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use {
|
||||
crate::{handlers::WhatDo, CFG_DIR},
|
||||
anyhow::anyhow,
|
||||
freedesktop_desktop_entry::{default_paths, get_languages_from_env, DesktopEntry, Iter},
|
||||
indexmap::IndexMap,
|
||||
is_executable::IsExecutable,
|
||||
rmp_serde::Serializer,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{read_to_string, File},
|
||||
io::{pipe, BufReader, Write},
|
||||
mem,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
},
|
||||
};
|
||||
// honestly it's probably inefficient to store a usize for how many times a program has been opened
|
||||
// but maybe you want to open it usize::MAX times???
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub(crate) struct AdapterCache(IndexMap<String, usize>);
|
||||
|
||||
impl AdapterCache {
|
||||
const FNA: &str = "cache.msgpack";
|
||||
pub fn load() -> anyhow::Result<Self> {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix(CFG_DIR);
|
||||
match xdg_dirs.find_cache_file(Self::FNA) {
|
||||
Some(p) => {
|
||||
let f = File::open(p)?;
|
||||
let reader = BufReader::new(f);
|
||||
let mut ac: Self = rmp_serde::from_read(reader)?;
|
||||
ac.sort();
|
||||
Ok(ac)
|
||||
}
|
||||
None => {
|
||||
let p = xdg_dirs.place_cache_file(Self::FNA)?;
|
||||
|
||||
let mut f = File::create_new(p)?;
|
||||
let ac: Self = Default::default();
|
||||
let mut ac_buf = Vec::new();
|
||||
ac.serialize(&mut Serializer::new(&mut ac_buf)).unwrap();
|
||||
f.write_all(&ac_buf)?;
|
||||
Ok(ac)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix(CFG_DIR);
|
||||
let p = xdg_dirs.place_cache_file(Self::FNA)?;
|
||||
|
||||
let mut f = File::create(p)?;
|
||||
let mut ac_buf = Vec::new();
|
||||
self.serialize(&mut Serializer::new(&mut ac_buf)).unwrap();
|
||||
f.write_all(&ac_buf)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn sort(&mut self) {
|
||||
self.0.sort_by(|_ak, av, _bk, bv| bv.cmp(av))
|
||||
}
|
||||
pub fn add(&mut self, name: &str) -> anyhow::Result<()> {
|
||||
*self.0.entry(name.to_string()).or_insert(0) += 1;
|
||||
self.sort();
|
||||
self.save()?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn transfer(&self, recipient: &mut IndexMap<String, WhatDo>) {
|
||||
for (idx, (key, _opens)) in self.0.iter().enumerate() {
|
||||
if let Some(idx_recipient) = recipient.get_index_of(key) {
|
||||
recipient.swap_indices(idx_recipient, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/config.rs
Normal file
98
src/config.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use {
|
||||
crate::CFG_DIR,
|
||||
anyhow::anyhow,
|
||||
freedesktop_desktop_entry::{default_paths, get_languages_from_env, DesktopEntry, Iter},
|
||||
indexmap::IndexMap,
|
||||
is_executable::IsExecutable,
|
||||
rmp_serde::Serializer,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{read_to_string, File},
|
||||
io::{pipe, BufReader, Write},
|
||||
mem,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
},
|
||||
};
|
||||
|
||||
fn fuzzy_exec() -> String {
|
||||
"fzf".to_string()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub(crate) struct AdapterConfig {
|
||||
terminal_exec: Option<String>,
|
||||
#[serde(default = "fuzzy_exec")]
|
||||
fuzzy_exec: String,
|
||||
}
|
||||
|
||||
impl AdapterConfig {
|
||||
const FNA: &str = "config.toml";
|
||||
pub fn load() -> anyhow::Result<Self> {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix(CFG_DIR);
|
||||
match xdg_dirs.find_config_file(Self::FNA) {
|
||||
Some(p) => {
|
||||
let file = read_to_string(p)?;
|
||||
let ac: Self = toml::from_str(&file)?;
|
||||
Ok(ac)
|
||||
}
|
||||
None => {
|
||||
let ac: Self = Default::default();
|
||||
let p = xdg_dirs.place_config_file(Self::FNA)?;
|
||||
|
||||
let mut f = File::create_new(p)?;
|
||||
let self_string = toml::to_string_pretty(&ac)?;
|
||||
f.write_all(self_string.as_bytes())?;
|
||||
Ok(ac)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix(CFG_DIR);
|
||||
let p = xdg_dirs.place_config_file(Self::FNA)?;
|
||||
|
||||
let mut f = File::create(p)?;
|
||||
let self_string = toml::to_string_pretty(self)?;
|
||||
f.write_all(self_string.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn terminal_bin(&self) -> Option<String> {
|
||||
if let Some(exec) = &self.terminal_exec {
|
||||
exec.split_once(" ").map(|(e, _)| e.to_string())
|
||||
} else {
|
||||
env::var("TERMINAL").ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn terminal_args(&self) -> Vec<String> {
|
||||
if let Some(exec) = &self.terminal_exec {
|
||||
let mut ret = exec
|
||||
.split_whitespace()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
ret.remove(0);
|
||||
ret
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fuzzy_bin(&self) -> String {
|
||||
self.fuzzy_exec.split(" ").collect::<Vec<_>>()[0].to_string()
|
||||
}
|
||||
|
||||
pub fn fuzzy_args(&self) -> Vec<String> {
|
||||
let mut ret = self
|
||||
.fuzzy_exec
|
||||
.split_whitespace()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
ret.remove(0);
|
||||
ret
|
||||
}
|
||||
}
|
||||
59
src/handlers.rs
Normal file
59
src/handlers.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use {
|
||||
crate::config::AdapterConfig,
|
||||
anyhow::anyhow,
|
||||
freedesktop_desktop_entry::{default_paths, get_languages_from_env, DesktopEntry, Iter},
|
||||
indexmap::IndexMap,
|
||||
is_executable::IsExecutable,
|
||||
rmp_serde::Serializer,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{read_to_string, File},
|
||||
io::{pipe, BufReader, Write},
|
||||
mem,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum WhatDo {
|
||||
XdgApplication(Vec<String>),
|
||||
XdgTerminal(Vec<String>),
|
||||
PathExec(Arc<Path>),
|
||||
}
|
||||
|
||||
pub(crate) fn handle_xdg(exec: Vec<String>) -> anyhow::Result<()> {
|
||||
let args = exec.get(1..).unwrap_or_default();
|
||||
let exec_run = Command::new(exec.first().ok_or(anyhow!(
|
||||
"Command not provided within the XDG desktop file correctly?"
|
||||
))?)
|
||||
.args(args)
|
||||
.stdout(Stdio::null())
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
mem::forget(exec_run);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn handle_terminal(config: &AdapterConfig, args: &[&str]) -> anyhow::Result<()> {
|
||||
let mut in_args = args.iter().map(|x| x.to_string()).collect();
|
||||
let mut term_args = config.terminal_args();
|
||||
term_args.append(&mut in_args);
|
||||
let term_run = Command::new(
|
||||
config
|
||||
.terminal_bin()
|
||||
.ok_or(anyhow!("No defined or available terminal"))?,
|
||||
)
|
||||
.args(term_args)
|
||||
.stdout(Stdio::null())
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
mem::forget(term_run);
|
||||
Ok(())
|
||||
}
|
||||
109
src/main.rs
Normal file
109
src/main.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use {
|
||||
crate::{
|
||||
cache::AdapterCache,
|
||||
config::AdapterConfig,
|
||||
handlers::{handle_terminal, handle_xdg, WhatDo},
|
||||
store::AdapterStore,
|
||||
},
|
||||
anyhow::anyhow,
|
||||
clap::{Parser, ValueEnum},
|
||||
freedesktop_desktop_entry::{default_paths, get_languages_from_env, DesktopEntry, Iter},
|
||||
indexmap::IndexMap,
|
||||
is_executable::IsExecutable,
|
||||
rmp_serde::Serializer,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{read_to_string, File},
|
||||
io::{pipe, BufReader, Write},
|
||||
mem,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
},
|
||||
};
|
||||
|
||||
mod cache;
|
||||
mod config;
|
||||
mod handlers;
|
||||
mod store;
|
||||
|
||||
const CFG_DIR: &str = "fzfdapter";
|
||||
|
||||
#[derive(Clone, PartialEq, ValueEnum)]
|
||||
enum Mode {
|
||||
All,
|
||||
Desktop,
|
||||
Path,
|
||||
}
|
||||
|
||||
#[derive(Parser, Clone)]
|
||||
#[command(name = "fzfdapter")]
|
||||
#[command(about = "A PATH and desktop file executor that uses fzf/skim/...", long_about = None)]
|
||||
#[command(arg_required_else_help = true)]
|
||||
struct Args {
|
||||
#[arg(short, long, use_value_delimiter = true, value_delimiter = ',', num_args = 1.., help = "How to source programs")]
|
||||
mode: Vec<Mode>,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let mut aca = AdapterCache::load()?;
|
||||
let aco = AdapterConfig::load()?;
|
||||
|
||||
let args = Args::parse();
|
||||
let mut store = AdapterStore::new();
|
||||
if args.mode.contains(&Mode::All) || args.mode.contains(&Mode::Desktop) {
|
||||
store.load_desktop();
|
||||
}
|
||||
if args.mode.contains(&Mode::All) || args.mode.contains(&Mode::Path) {
|
||||
store.load_path()?;
|
||||
}
|
||||
store.configure(&aca);
|
||||
|
||||
if !args.mode.is_empty() {
|
||||
let (reader, mut writer) = pipe()?;
|
||||
|
||||
let fuzz_command = aco.fuzzy_bin();
|
||||
let fuzz_args = aco.fuzzy_args();
|
||||
let fuzzy = Command::new(fuzz_command)
|
||||
.args(&fuzz_args)
|
||||
.stdin(reader)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let fzf_input = store.input();
|
||||
writer.write_all(fzf_input.as_bytes())?;
|
||||
writer.flush()?;
|
||||
drop(writer);
|
||||
|
||||
let fuzz = fuzzy.wait_with_output()?;
|
||||
|
||||
let fuzz = String::from_utf8(fuzz.stdout)?;
|
||||
let fuzz = fuzz.strip_suffix("\n").unwrap_or(&fuzz);
|
||||
|
||||
if let Some(whatdo) = store.storage.get(fuzz) {
|
||||
aca.add(fuzz)?;
|
||||
match whatdo {
|
||||
WhatDo::XdgApplication(exec) => {
|
||||
handle_xdg(exec.clone())?;
|
||||
}
|
||||
WhatDo::XdgTerminal(exec) => {
|
||||
let args: Vec<_> = exec.iter().map(|x| x.as_str()).collect();
|
||||
handle_terminal(&aco, &args)?;
|
||||
}
|
||||
WhatDo::PathExec(path) => {
|
||||
let path_arg = match path.to_path_buf().into_os_string().into_string() {
|
||||
Ok(path) => path,
|
||||
Err(os) => os.to_string_lossy().to_string(),
|
||||
};
|
||||
let args = [path_arg.as_str()];
|
||||
handle_terminal(&aco, &args)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
102
src/store.rs
Normal file
102
src/store.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use {
|
||||
crate::{cache::AdapterCache, handlers::WhatDo},
|
||||
anyhow::anyhow,
|
||||
freedesktop_desktop_entry::{default_paths, get_languages_from_env, DesktopEntry, Iter},
|
||||
indexmap::IndexMap,
|
||||
is_executable::IsExecutable,
|
||||
rmp_serde::Serializer,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
collections::HashSet,
|
||||
env,
|
||||
fs::{read_to_string, File},
|
||||
io::{pipe, BufReader, Write},
|
||||
mem,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct AdapterStore {
|
||||
pub storage: IndexMap<String, WhatDo>,
|
||||
locales: Vec<String>,
|
||||
entries: Vec<DesktopEntry>,
|
||||
}
|
||||
|
||||
impl AdapterStore {
|
||||
pub fn new() -> Self {
|
||||
let storage = Default::default();
|
||||
let locales = get_languages_from_env();
|
||||
let entries = Iter::new(default_paths())
|
||||
.entries(Some(&locales))
|
||||
.collect::<Vec<_>>();
|
||||
AdapterStore {
|
||||
storage,
|
||||
locales,
|
||||
entries,
|
||||
}
|
||||
}
|
||||
pub fn load_desktop(&mut self) {
|
||||
for entry in &self.entries {
|
||||
let name = entry.name(&self.locales).unwrap_or_default();
|
||||
let selectable = format!("{} ({})", name, entry.id());
|
||||
let entry_type = entry.type_();
|
||||
let type_check = entry_type.is_none() || entry_type == Some("Application");
|
||||
if !entry.hidden()
|
||||
&& type_check
|
||||
&& !entry.no_display()
|
||||
&& let Ok(entry_whatdo_inner) = entry.parse_exec()
|
||||
{
|
||||
let entry_whatdo = if entry.terminal() {
|
||||
WhatDo::XdgTerminal(entry_whatdo_inner)
|
||||
} else {
|
||||
WhatDo::XdgApplication(entry_whatdo_inner)
|
||||
};
|
||||
self.storage.insert(selectable, entry_whatdo);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn load_path(&mut self) -> anyhow::Result<()> {
|
||||
let mut dedup = HashSet::new();
|
||||
let path_var = env::var("PATH").unwrap_or_default();
|
||||
let paths = env::split_paths(&path_var);
|
||||
for path in paths {
|
||||
if let Ok(mut dir_entries) = path.read_dir() {
|
||||
while let Some(Ok(entry)) = dir_entries.next() {
|
||||
let path = entry.path();
|
||||
let filename = entry.file_name();
|
||||
if path.is_file() && path.is_executable() {
|
||||
let filename_string = match filename.into_string() {
|
||||
Ok(filename) => filename,
|
||||
Err(os) => os.to_string_lossy().to_string(),
|
||||
};
|
||||
let path_string = path.clone().into_os_string().into_string();
|
||||
if let Ok(path_string) = path_string {
|
||||
let full_string =
|
||||
format!("{} (Path: {})", filename_string, path_string);
|
||||
if !dedup.contains(&filename_string) {
|
||||
dedup.insert(filename_string.clone());
|
||||
let entry_path = Arc::new(path.clone());
|
||||
let entry_whatdo = WhatDo::PathExec(entry_path.as_path().into());
|
||||
self.storage.insert(full_string, entry_whatdo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn configure(&mut self, config: &AdapterCache) {
|
||||
config.transfer(&mut self.storage);
|
||||
}
|
||||
pub fn keys(&self) -> Vec<String> {
|
||||
self.storage.keys().cloned().collect()
|
||||
}
|
||||
pub fn input(&self) -> String {
|
||||
self.keys().join("\n")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue