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

@ -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 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 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,
@ -243,7 +246,7 @@ fn build_ui<T, P>(
.set_keyboard_mode(KeyboardMode::Exclusive); .set_keyboard_mode(KeyboardMode::Exclusive);
ui_elements.window.set_namespace(Some("worf")); ui_elements.window.set_namespace(Some("worf"));
} }
let window_done = Instant::now(); let window_done = Instant::now();
if let Some(location) = config.location.as_ref() { if let Some(location) = config.location.as_ref() {

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,40 +548,44 @@ 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 => { cfg_clone.prompt = Some(selection_result.label.clone());
cfg_clone.prompt = Some(selection_result.label.clone()); provider.math.elements.push(selection_result);
provider.math.elements.push(selection_result); }
} AutoRunType::DRun => {
AutoRunType::DRun => { update_drun_cache_and_run(cache_path, &mut cache, selection_result)?;
update_drun_cache_and_run(cache_path, &mut cache, selection_result)?; break;
break; }
} AutoRunType::File => {
AutoRunType::File => { if let Some(action) = selection_result.action {
if let Some(action) = selection_result.action { spawn_fork(&action, selection_result.working_dir.as_ref())?;
spawn_fork(&action, selection_result.working_dir.as_ref())?;
}
break;
} }
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(_) => { } else {
log::error!("No item selected"); log::error!("No item selected");
break; break;
}
} }
} }
@ -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,15 +653,12 @@ 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 {
} log::error!("No item selected");
Err(_) => { break;
log::error!("No item selected");
break;
}
} }
} }
} }

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