diff --git a/README.md b/README.md index b63754d..cebc66c 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,10 @@ Worf is written in Rust on top of GTK4. It aims to be a drop in replacement for wofi in most part, so it is (almost) compatible with its configuration and css files. See below for differences -## Not finished -* [ ] dmenu +## Not finished * [ ] run * [ ] key support * [ ] full config support -* [ ] ssh mode * [ ] web search mode * [ ] emoji finder * [ ] publish library diff --git a/src/lib/config.rs b/src/lib/config.rs index d30e342..993bc81 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -7,6 +7,7 @@ use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; +use which::which; #[derive(Debug)] pub enum ConfigurationError { @@ -76,6 +77,9 @@ pub enum Mode { /// Use is as calculator Math, + + /// Connect via ssh to a given host + Ssh, } #[derive(Debug, Error)] @@ -108,6 +112,7 @@ impl FromStr for Mode { "dmenu" => Ok(Mode::Dmenu), "file" => Ok(Mode::File), "math" => Ok(Mode::Math), + "ssh" => Ok(Mode::Ssh), "auto" => Ok(Mode::Auto), _ => Err(ArgsError::InvalidParameter( format!("{s} is not a valid argument, see help for details").to_owned(), @@ -192,6 +197,19 @@ pub struct Config { #[clap(short = 'k', long = "cache-file")] pub cache_file: Option, + /// Defines which terminal to use. defaults to the first one found: + /// * kitty + /// * gnome-terminal + /// * konsole + /// * xfce4-terminal + /// * lxterminal + /// * xterm + /// * alacritty + /// * terminator + /// + /// Must be configured including the needed arguments to launch something + /// i.e. 'kitty -c' + #[serde(default = "default_terminal")] #[clap(short = 't', long = "term")] pub term: Option, @@ -570,6 +588,29 @@ pub fn default_width() -> Option { Some("50%".to_owned()) } +// allowed because option is needed for serde macro +#[allow(clippy::unnecessary_wraps)] +#[must_use] +pub fn default_terminal() -> Option { + let terminals = [ + ("gnome-terminal", vec!["--"]), + ("konsole", vec!["-e"]), + ("xfce4-terminal", vec!["--command"]), + ("xterm", vec!["-e"]), + ("alacritty", vec!["-e"]), + ("lxterminal", vec!["-e"]), + ("kitty", vec!["-e"]), + ("tilix", vec!["-e"]), + ]; + + for (term, launch) in &terminals { + if which(term).is_ok() { + return Some(format!("{term} {}", launch.join(" "))); + } + } + None +} + // allowed because option is needed for serde macro #[allow(clippy::unnecessary_wraps)] #[must_use] @@ -692,6 +733,7 @@ pub fn load_config(args_opt: Option) -> Result merge_result.prompt = Some("math".to_owned()), Mode::File => merge_result.prompt = Some("file".to_owned()), Mode::Auto => merge_result.prompt = Some("auto".to_owned()), + Mode::Ssh => merge_result.prompt = Some("ssh".to_owned()), }, } } diff --git a/src/lib/gui.rs b/src/lib/gui.rs index 221e41d..9208214 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -15,7 +15,10 @@ use gdk4::glib::{Propagation, timeout_add_local}; use gdk4::prelude::{Cast, DisplayExt, MonitorExt}; use gdk4::{Display, Key}; use gtk4::glib::ControlFlow; -use gtk4::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, EditableExt, FlowBoxChildExt, GestureSingleExt, GtkWindowExt, ListBoxRowExt, NativeExt, OrientableExt, WidgetExt}; +use gtk4::prelude::{ + ApplicationExt, ApplicationExtManual, BoxExt, EditableExt, FlowBoxChildExt, GestureSingleExt, + GtkWindowExt, ListBoxRowExt, NativeExt, OrientableExt, WidgetExt, +}; use gtk4::{ Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, ListBox, ListBoxRow, NaturalWrapMode, Ordering, PolicyType, ScrolledWindow, SearchEntry, @@ -243,7 +246,7 @@ fn build_ui( .set_keyboard_mode(KeyboardMode::Exclusive); ui_elements.window.set_namespace(Some("worf")); } - + let window_done = Instant::now(); if let Some(location) = config.location.as_ref() { diff --git a/src/lib/mode.rs b/src/lib/mode.rs index 90263fb..815b47e 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -316,6 +316,56 @@ impl ItemProvider for FileItemProvider { } } +#[derive(Clone)] +struct SshProvider { + elements: Vec>, +} + +impl SshProvider { + fn new(menu_item_data: T, config: &Config) -> Self { + let re = Regex::new(r"(?m)^\s*Host\s+(.+)$").unwrap(); + let items: Vec<_> = dirs::home_dir() + .map(|home| home.join(".ssh").join("config")) + .filter(|path| path.exists()) + .map(|path| fs::read_to_string(&path).unwrap_or_default()) + .into_iter() + .flat_map(|content| { + re.captures_iter(&content) + .flat_map(|cap| { + cap[1] + .split_whitespace() + .map(|host| { + log::debug!("found ssh host {host}"); + MenuItem::new( + host.to_owned(), + Some("computer".to_owned()), + config.term.clone().map(|cmd| format!("{cmd} ssh {host}")), + vec![], + None, + 0.0, + Some(menu_item_data.clone()), + ) + }) + .collect::>() + }) + .collect::>() + }) + .collect(); + + Self { elements: items } + } +} + +impl ItemProvider for SshProvider { + fn get_elements(&mut self, _: Option<&str>) -> Vec> { + self.elements.clone() + } + + fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { + None + } +} + #[derive(Clone)] struct MathProvider { menu_item_data: T, @@ -415,7 +465,7 @@ enum AutoRunType { Math, DRun, File, - // Ssh, + Ssh, // WebSearch, // Emoji, // Run, @@ -426,14 +476,16 @@ struct AutoItemProvider { drun: DRunProvider, file: FileItemProvider, math: MathProvider, + ssh: SshProvider, } impl AutoItemProvider { - fn new() -> Self { + fn new(config: &Config) -> Self { AutoItemProvider { drun: DRunProvider::new(AutoRunType::DRun), file: FileItemProvider::new(AutoRunType::File), math: MathProvider::new(AutoRunType::Math), + ssh: SshProvider::new(AutoRunType::Ssh, config), } } } @@ -453,6 +505,8 @@ impl ItemProvider for AutoItemProvider { || trimmed_search.starts_with('~') { self.file.get_elements(search_opt) + } else if trimmed_search.starts_with("ssh") { + self.ssh.get_elements(search_opt) } else { self.drun.get_elements(search_opt) } @@ -494,40 +548,44 @@ pub fn d_run(config: &Config) -> Result<(), ModeError> { /// Will return `Err` /// * if it was not able to spawn the process pub fn auto(config: &Config) -> Result<(), ModeError> { - let mut provider = AutoItemProvider::new(); + let mut provider = AutoItemProvider::new(config); let cache_path = provider.drun.cache_path.clone(); let mut cache = provider.drun.cache.clone(); let mut cfg_clone = config.clone(); loop { // todo ues a arc instead of cloning the config - let selection_result = gui::show(cfg_clone.clone(), provider.clone(), false); + let selection_result = gui::show(cfg_clone.clone(), provider.clone(), true); - match selection_result { - Ok(selection_result) => { - if let Some(data) = &selection_result.data { - match data { - AutoRunType::Math => { - cfg_clone.prompt = Some(selection_result.label.clone()); - provider.math.elements.push(selection_result); - } - AutoRunType::DRun => { - update_drun_cache_and_run(cache_path, &mut cache, selection_result)?; - break; - } - AutoRunType::File => { - if let Some(action) = selection_result.action { - spawn_fork(&action, selection_result.working_dir.as_ref())?; - } - break; + if let Ok(mut selection_result) = selection_result { + if let Some(data) = &selection_result.data { + match data { + AutoRunType::Math => { + cfg_clone.prompt = Some(selection_result.label.clone()); + provider.math.elements.push(selection_result); + } + AutoRunType::DRun => { + update_drun_cache_and_run(cache_path, &mut cache, selection_result)?; + break; + } + AutoRunType::File => { + if let Some(action) = selection_result.action { + spawn_fork(&action, selection_result.working_dir.as_ref())?; } + break; + } + AutoRunType::Ssh => { + ssh_launch(&selection_result, config)?; + break; } } + } else if selection_result.label.starts_with("ssh") { + selection_result.label = selection_result.label.chars().skip(4).collect(); + ssh_launch(&selection_result, config)?; } - Err(_) => { - log::error!("No item selected"); - break; - } + } else { + log::error!("No item selected"); + break; } } @@ -557,6 +615,37 @@ pub fn file(config: &Config) -> Result<(), ModeError> { Ok(()) } +fn ssh_launch(menu_item: &MenuItem, config: &Config) -> Result<(), ModeError> { + if let Some(action) = &menu_item.action { + spawn_fork(action, None)?; + } else { + let cmd = config + .term + .clone() + .map(|s| format!("{s} ssh {}", menu_item.label)); + if let Some(cmd) = cmd { + spawn_fork(&cmd, None)?; + } + } + Err(ModeError::MissingAction) +} + +/// # Errors +/// +/// Will return `Err` +/// * if it was not able to spawn the process +/// * if it didn't find a terminal +pub fn ssh(config: &Config) -> Result<(), ModeError> { + let provider = SshProvider::new(String::new(), config); + let selection_result = gui::show(config.clone(), provider, true); + if let Ok(mi) = selection_result { + ssh_launch(&mi, config)?; + } else { + log::error!("No item selected"); + } + Ok(()) +} + pub fn math(config: &Config) { let mut cfg_clone = config.clone(); let mut calc: Vec> = vec![]; @@ -564,15 +653,12 @@ pub fn math(config: &Config) { let mut provider = MathProvider::new(String::new()); provider.add_elements(&mut calc.clone()); let selection_result = gui::show(cfg_clone.clone(), provider, true); - match selection_result { - Ok(mi) => { - cfg_clone.prompt = Some(mi.label.clone()); - calc.push(mi); - } - Err(_) => { - log::error!("No item selected"); - break; - } + if let Ok(mi) = selection_result { + cfg_clone.prompt = Some(mi.label.clone()); + calc.push(mi); + } else { + log::error!("No item selected"); + break; } } } diff --git a/src/main.rs b/src/main.rs index 4c9d440..0b91117 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,9 @@ fn main() -> anyhow::Result<()> { Mode::Math => { mode::math(&config); } + Mode::Ssh => { + mode::ssh(&config).map_err(|e| anyhow!(e))?; + } Mode::Auto => { mode::auto(&config).map_err(|e| anyhow!(e))?; }