add ssh mode

This commit is contained in:
Alexander Mohr 2025-05-01 00:09:22 +02:00
parent 00df9f4d88
commit bb4dcb6908
5 changed files with 171 additions and 39 deletions

View file

@ -7,11 +7,9 @@ It aims to be a drop in replacement for wofi in most part, so it is (almost) com
configuration and css files. See below for differences configuration and css files. See below for differences
## Not finished ## Not finished
* [ ] dmenu
* [ ] run * [ ] run
* [ ] key support * [ ] key support
* [ ] full config support * [ ] full config support
* [ ] ssh mode
* [ ] web search mode * [ ] web search mode
* [ ] emoji finder * [ ] emoji finder
* [ ] publish library * [ ] publish library

View file

@ -7,6 +7,7 @@ use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use thiserror::Error; use thiserror::Error;
use which::which;
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigurationError { pub enum ConfigurationError {
@ -76,6 +77,9 @@ pub enum Mode {
/// Use is as calculator /// Use is as calculator
Math, Math,
/// Connect via ssh to a given host
Ssh,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -108,6 +112,7 @@ impl FromStr for Mode {
"dmenu" => Ok(Mode::Dmenu), "dmenu" => Ok(Mode::Dmenu),
"file" => Ok(Mode::File), "file" => Ok(Mode::File),
"math" => Ok(Mode::Math), "math" => Ok(Mode::Math),
"ssh" => Ok(Mode::Ssh),
"auto" => Ok(Mode::Auto), "auto" => Ok(Mode::Auto),
_ => Err(ArgsError::InvalidParameter( _ => Err(ArgsError::InvalidParameter(
format!("{s} is not a valid argument, see help for details").to_owned(), 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")] #[clap(short = 'k', long = "cache-file")]
pub cache_file: Option<String>, pub cache_file: Option<String>,
/// 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")] #[clap(short = 't', long = "term")]
pub term: Option<String>, pub term: Option<String>,
@ -570,6 +588,29 @@ pub fn default_width() -> Option<String> {
Some("50%".to_owned()) Some("50%".to_owned())
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_terminal() -> Option<String> {
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 // allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps)]
#[must_use] #[must_use]
@ -692,6 +733,7 @@ pub fn load_config(args_opt: Option<Config>) -> Result<Config, ConfigurationErro
Mode::Math => merge_result.prompt = Some("math".to_owned()), Mode::Math => merge_result.prompt = Some("math".to_owned()),
Mode::File => merge_result.prompt = Some("file".to_owned()), Mode::File => merge_result.prompt = Some("file".to_owned()),
Mode::Auto => merge_result.prompt = Some("auto".to_owned()), Mode::Auto => merge_result.prompt = Some("auto".to_owned()),
Mode::Ssh => merge_result.prompt = Some("ssh".to_owned()),
}, },
} }
} }

View file

@ -15,7 +15,10 @@ use gdk4::glib::{Propagation, timeout_add_local};
use gdk4::prelude::{Cast, DisplayExt, MonitorExt}; use gdk4::prelude::{Cast, DisplayExt, MonitorExt};
use gdk4::{Display, Key}; use gdk4::{Display, Key};
use gtk4::glib::ControlFlow; 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::{ use gtk4::{
Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label,
ListBox, ListBoxRow, NaturalWrapMode, Ordering, PolicyType, ScrolledWindow, SearchEntry, ListBox, ListBoxRow, NaturalWrapMode, Ordering, PolicyType, ScrolledWindow, SearchEntry,

View file

@ -316,6 +316,56 @@ impl<T: Clone> ItemProvider<T> for FileItemProvider<T> {
} }
} }
#[derive(Clone)]
struct SshProvider<T: Clone> {
elements: Vec<MenuItem<T>>,
}
impl<T: Clone> SshProvider<T> {
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::<Vec<_>>()
})
.collect::<Vec<_>>()
})
.collect();
Self { elements: items }
}
}
impl<T: Clone> ItemProvider<T> for SshProvider<T> {
fn get_elements(&mut self, _: Option<&str>) -> Vec<MenuItem<T>> {
self.elements.clone()
}
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> Option<Vec<MenuItem<T>>> {
None
}
}
#[derive(Clone)] #[derive(Clone)]
struct MathProvider<T: Clone> { struct MathProvider<T: Clone> {
menu_item_data: T, menu_item_data: T,
@ -415,7 +465,7 @@ enum AutoRunType {
Math, Math,
DRun, DRun,
File, File,
// Ssh, Ssh,
// WebSearch, // WebSearch,
// Emoji, // Emoji,
// Run, // Run,
@ -426,14 +476,16 @@ struct AutoItemProvider {
drun: DRunProvider<AutoRunType>, drun: DRunProvider<AutoRunType>,
file: FileItemProvider<AutoRunType>, file: FileItemProvider<AutoRunType>,
math: MathProvider<AutoRunType>, math: MathProvider<AutoRunType>,
ssh: SshProvider<AutoRunType>,
} }
impl AutoItemProvider { impl AutoItemProvider {
fn new() -> Self { fn new(config: &Config) -> Self {
AutoItemProvider { AutoItemProvider {
drun: DRunProvider::new(AutoRunType::DRun), drun: DRunProvider::new(AutoRunType::DRun),
file: FileItemProvider::new(AutoRunType::File), file: FileItemProvider::new(AutoRunType::File),
math: MathProvider::new(AutoRunType::Math), math: MathProvider::new(AutoRunType::Math),
ssh: SshProvider::new(AutoRunType::Ssh, config),
} }
} }
} }
@ -453,6 +505,8 @@ impl ItemProvider<AutoRunType> for AutoItemProvider {
|| trimmed_search.starts_with('~') || trimmed_search.starts_with('~')
{ {
self.file.get_elements(search_opt) self.file.get_elements(search_opt)
} else if trimmed_search.starts_with("ssh") {
self.ssh.get_elements(search_opt)
} else { } else {
self.drun.get_elements(search_opt) self.drun.get_elements(search_opt)
} }
@ -494,17 +548,16 @@ pub fn d_run(config: &Config) -> Result<(), ModeError> {
/// Will return `Err` /// Will return `Err`
/// * if it was not able to spawn the process /// * if it was not able to spawn the process
pub fn auto(config: &Config) -> Result<(), ModeError> { 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 cache_path = provider.drun.cache_path.clone();
let mut cache = provider.drun.cache.clone(); let mut cache = provider.drun.cache.clone();
let mut cfg_clone = config.clone(); let mut cfg_clone = config.clone();
loop { loop {
// todo ues a arc instead of cloning the config // 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 { if let Ok(mut selection_result) = selection_result {
Ok(selection_result) => {
if let Some(data) = &selection_result.data { if let Some(data) = &selection_result.data {
match data { match data {
AutoRunType::Math => { AutoRunType::Math => {
@ -521,14 +574,19 @@ pub fn auto(config: &Config) -> Result<(), ModeError> {
} }
break; break;
} }
} AutoRunType::Ssh => {
} ssh_launch(&selection_result, config)?;
}
Err(_) => {
log::error!("No item selected");
break; break;
} }
} }
} else if selection_result.label.starts_with("ssh") {
selection_result.label = selection_result.label.chars().skip(4).collect();
ssh_launch(&selection_result, config)?;
}
} else {
log::error!("No item selected");
break;
}
} }
Ok(()) Ok(())
@ -557,6 +615,37 @@ pub fn file(config: &Config) -> Result<(), ModeError> {
Ok(()) Ok(())
} }
fn ssh_launch<T: Clone>(menu_item: &MenuItem<T>, 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) { pub fn math(config: &Config) {
let mut cfg_clone = config.clone(); let mut cfg_clone = config.clone();
let mut calc: Vec<MenuItem<String>> = vec![]; let mut calc: Vec<MenuItem<String>> = vec![];
@ -564,17 +653,14 @@ pub fn math(config: &Config) {
let mut provider = MathProvider::new(String::new()); let mut provider = MathProvider::new(String::new());
provider.add_elements(&mut calc.clone()); provider.add_elements(&mut calc.clone());
let selection_result = gui::show(cfg_clone.clone(), provider, true); let selection_result = gui::show(cfg_clone.clone(), provider, true);
match selection_result { if let Ok(mi) = selection_result {
Ok(mi) => {
cfg_clone.prompt = Some(mi.label.clone()); cfg_clone.prompt = Some(mi.label.clone());
calc.push(mi); calc.push(mi);
} } else {
Err(_) => {
log::error!("No item selected"); log::error!("No item selected");
break; break;
} }
} }
}
} }
/// # Errors /// # Errors

View file

@ -31,6 +31,9 @@ fn main() -> anyhow::Result<()> {
Mode::Math => { Mode::Math => {
mode::math(&config); mode::math(&config);
} }
Mode::Ssh => {
mode::ssh(&config).map_err(|e| anyhow!(e))?;
}
Mode::Auto => { Mode::Auto => {
mode::auto(&config).map_err(|e| anyhow!(e))?; mode::auto(&config).map_err(|e| anyhow!(e))?;
} }