feat: consolidate, license

This commit is contained in:
Kat Inskip 2025-10-25 02:09:48 -07:00
commit 0ade7d9539
Signed by: kat
GPG key ID: 465E64DECEA8CF0F
19 changed files with 2088 additions and 0 deletions

76
src/cache.rs Normal file
View 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
View 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
View 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
View 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
View 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")
}
}