add basic support for custom keys
this also adds an example for a vaultwarden client
This commit is contained in:
parent
d15f178f27
commit
96455074e9
6 changed files with 2382 additions and 34 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
.idea
|
||||
*target*
|
||||
|
|
2143
examples/worf-warden/Cargo.lock
generated
Normal file
2143
examples/worf-warden/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
9
examples/worf-warden/Cargo.toml
Normal file
9
examples/worf-warden/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "worf-warden"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
gdk4 = "0.9.6"
|
||||
worf = {path = "../.."}
|
||||
enigo = "0.1.3"
|
106
examples/worf-warden/src/main.rs
Normal file
106
examples/worf-warden/src/main.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use enigo::{Enigo, Key, KeyboardControllable};
|
||||
|
||||
use worf_lib::{config, gui, Error};
|
||||
use worf_lib::config::Config;
|
||||
use worf_lib::gui::{CustomKey, ItemProvider, MenuItem};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PasswordProvider {
|
||||
items: Vec<MenuItem<String>>
|
||||
}
|
||||
|
||||
impl PasswordProvider {
|
||||
fn new(config: &Config) -> Self {
|
||||
let output = Command::new("rbw")
|
||||
.arg("list")
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// todo the own solution should support images.
|
||||
let mut items: Vec<_>= stdout.lines().map(|line|
|
||||
MenuItem::new(line.to_owned(), None, None, vec![], None, 0.0, Some(String::new()))
|
||||
).collect();
|
||||
gui::apply_sort(&mut items, &config.sort_order());
|
||||
|
||||
Self {
|
||||
items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemProvider<String> for PasswordProvider {
|
||||
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<String>>) {
|
||||
(false, self.items.clone())
|
||||
}
|
||||
|
||||
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> (bool, Option<Vec<MenuItem<String>>>) {
|
||||
(false, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn rbw_get(name: &str, field: &str) -> String {
|
||||
let output = Command::new("rbw")
|
||||
.arg("get")
|
||||
.arg(name)
|
||||
.arg("--field")
|
||||
.arg(field)
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
|
||||
String::from_utf8_lossy(&output.stdout).trim_end().to_string()
|
||||
}
|
||||
|
||||
fn rbw_get_user(name: &str) -> String{
|
||||
rbw_get(name, "user")
|
||||
}
|
||||
|
||||
fn rbw_get_password(name: &str) -> String {
|
||||
rbw_get(name, "password")
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = config::parse_args();
|
||||
let config = config::load_config(Some(&args)).unwrap_or(args);
|
||||
|
||||
// todo eventually use a propper rust client for this, for now rbw is good enough
|
||||
let provider = PasswordProvider::new(&config);
|
||||
|
||||
let type_all = CustomKey {
|
||||
key: gdk4::Key::_1, // todo do not expose gdk4
|
||||
modifiers: gdk4::ModifierType::ALT_MASK,
|
||||
label: "<b>Alt+1</b> Type All".to_string(),
|
||||
};
|
||||
|
||||
let type_user = CustomKey {
|
||||
key: gdk4::Key::_2, // todo do not expose gdk4
|
||||
modifiers: gdk4::ModifierType::ALT_MASK,
|
||||
label: "<b>Alt+2</b> Type All".to_string(),
|
||||
};
|
||||
|
||||
match gui::show(config, provider, false, None, Some(vec![type_all.clone(), type_user])) {
|
||||
Ok(selection) => {
|
||||
let mut enigo = Enigo::new();
|
||||
let id = selection.menu.label.replace("\n", "");
|
||||
sleep(Duration::from_millis(250));
|
||||
if let Some(key) = selection.custom_key {
|
||||
if key.label == type_all.label {
|
||||
enigo.key_sequence(&rbw_get_user(&id));
|
||||
enigo.key_down(Key::Tab);
|
||||
enigo.key_up(Key::Tab);
|
||||
enigo.key_sequence(&rbw_get_password(&id));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.ne(&Error::NoSelection) {
|
||||
println!("Error occurred: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
126
src/lib/gui.rs
126
src/lib/gui.rs
|
@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crossbeam::channel;
|
||||
|
@ -9,7 +10,7 @@ use crossbeam::channel::Sender;
|
|||
use gdk4::gio::File;
|
||||
use gdk4::glib::{Propagation, timeout_add_local};
|
||||
use gdk4::prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt};
|
||||
use gdk4::{Display, Key};
|
||||
use gdk4::{Display, Key, ModifierType};
|
||||
use gtk4::glib::ControlFlow;
|
||||
use gtk4::prelude::{
|
||||
ApplicationExt, ApplicationExtManual, BoxExt, EditableExt, FlowBoxChildExt, GestureSingleExt,
|
||||
|
@ -31,7 +32,12 @@ use crate::{Error, config, desktop};
|
|||
|
||||
type ArcMenuMap<T> = Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>;
|
||||
type ArcProvider<T> = Arc<Mutex<dyn ItemProvider<T> + Send>>;
|
||||
type MenuItemSender<T> = Sender<Result<MenuItem<T>, Error>>;
|
||||
|
||||
pub struct Selection<T: Clone + Send> {
|
||||
pub menu: MenuItem<T>,
|
||||
pub custom_key: Option<CustomKey>,
|
||||
}
|
||||
type SelectionSender<T> = Sender<Result<Selection<T>, Error>>;
|
||||
|
||||
pub trait ItemProvider<T: Clone> {
|
||||
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>);
|
||||
|
@ -104,6 +110,13 @@ pub struct MenuItem<T: Clone> {
|
|||
visible: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct CustomKey {
|
||||
pub key: Key,
|
||||
pub modifiers: ModifierType, // acts as a mask, so multiple things can be set.
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl<T: Clone> MenuItem<T> {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
|
@ -135,9 +148,9 @@ impl<T: Clone> AsRef<MenuItem<T>> for MenuItem<T> {
|
|||
}
|
||||
}
|
||||
|
||||
struct MetaData<T: Clone> {
|
||||
struct MetaData<T: Clone + Send> {
|
||||
item_provider: ArcProvider<T>,
|
||||
selected_sender: MenuItemSender<T>,
|
||||
selected_sender: SelectionSender<T>,
|
||||
config: Rc<Config>,
|
||||
new_on_empty: bool,
|
||||
search_ignored_words: Option<Vec<Regex>>,
|
||||
|
@ -161,7 +174,8 @@ pub fn show<T, P>(
|
|||
item_provider: P,
|
||||
new_on_empty: bool,
|
||||
search_ignored_words: Option<Vec<Regex>>,
|
||||
) -> Result<MenuItem<T>, Error>
|
||||
custom_keys: Option<Vec<CustomKey>>,
|
||||
) -> Result<Selection<T>, Error>
|
||||
where
|
||||
T: Clone + 'static + Send,
|
||||
P: ItemProvider<T> + 'static + Clone + Send,
|
||||
|
@ -192,6 +206,7 @@ where
|
|||
app.clone(),
|
||||
new_on_empty,
|
||||
search_ignored_words.clone(),
|
||||
custom_keys.clone(),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -203,10 +218,11 @@ where
|
|||
fn build_ui<T, P>(
|
||||
config: &Config,
|
||||
item_provider: P,
|
||||
sender: Sender<Result<MenuItem<T>, Error>>,
|
||||
sender: Sender<Result<Selection<T>, Error>>,
|
||||
app: Application,
|
||||
new_on_empty: bool,
|
||||
search_ignored_words: Option<Vec<Regex>>,
|
||||
custom_keys: Option<Vec<CustomKey>>,
|
||||
) where
|
||||
T: Clone + 'static + Send,
|
||||
P: ItemProvider<T> + 'static + Send,
|
||||
|
@ -245,7 +261,7 @@ fn build_ui<T, P>(
|
|||
});
|
||||
|
||||
// handle keys as soon as possible
|
||||
setup_key_event_handler(&ui_elements, &meta);
|
||||
setup_key_event_handler(&ui_elements, &meta, &custom_keys);
|
||||
|
||||
log::debug!("keyboard ready after {:?}", start.elapsed());
|
||||
|
||||
|
@ -272,6 +288,8 @@ fn build_ui<T, P>(
|
|||
let outer_box = gtk4::Box::new(config.orientation().into(), 0);
|
||||
outer_box.set_widget_name("outer-box");
|
||||
outer_box.append(&ui_elements.search);
|
||||
build_custom_key_view(config, &ui_elements, &custom_keys, &outer_box);
|
||||
|
||||
ui_elements.window.set_child(Some(&outer_box));
|
||||
|
||||
let scroll = ScrolledWindow::new();
|
||||
|
@ -315,7 +333,6 @@ fn build_ui<T, P>(
|
|||
|
||||
log::debug!("Building UI took {:?}", start.elapsed(),);
|
||||
}
|
||||
|
||||
fn build_main_box<T: Clone + 'static>(config: &Config, ui_elements: &Rc<UiElements<T>>) {
|
||||
ui_elements.main_box.set_widget_name("inner-box");
|
||||
ui_elements.main_box.set_css_classes(&["inner-box"]);
|
||||
|
@ -348,7 +365,11 @@ fn build_main_box<T: Clone + 'static>(config: &Config, ui_elements: &Rc<UiElemen
|
|||
});
|
||||
}
|
||||
|
||||
fn build_search_entry<T: Clone>(config: &Config, ui_elements: &UiElements<T>, meta: &MetaData<T>) {
|
||||
fn build_search_entry<T: Clone + Send>(
|
||||
config: &Config,
|
||||
ui_elements: &UiElements<T>,
|
||||
meta: &MetaData<T>,
|
||||
) {
|
||||
ui_elements.search.set_widget_name("input");
|
||||
ui_elements.search.set_css_classes(&["input"]);
|
||||
ui_elements
|
||||
|
@ -363,7 +384,35 @@ fn build_search_entry<T: Clone>(config: &Config, ui_elements: &UiElements<T>, me
|
|||
}
|
||||
}
|
||||
|
||||
fn set_search_text<T: Clone>(text: &str, ui: &UiElements<T>, meta: &MetaData<T>) {
|
||||
fn build_custom_key_view<T>(
|
||||
config: &Config,
|
||||
ui: &Rc<UiElements<T>>,
|
||||
custom_keys: &Option<Vec<CustomKey>>,
|
||||
outer_box: >k4::Box,
|
||||
) where
|
||||
T: 'static + Clone + Send,
|
||||
{
|
||||
let inner_box = gtk4::Box::new(Orientation::Horizontal, 0);
|
||||
inner_box.set_halign(Align::Start);
|
||||
inner_box.set_widget_name("custom-key-box");
|
||||
if let Some(custom_keys) = custom_keys {
|
||||
for key in custom_keys {
|
||||
let label_box = gtk4::Box::new(Orientation::Horizontal, 0);
|
||||
label_box.set_halign(Align::Start);
|
||||
label_box.set_widget_name("custom-key-label-box");
|
||||
inner_box.append(&label_box);
|
||||
let label = Label::new(Some(&key.label));
|
||||
label.set_use_markup(true);
|
||||
label.set_hexpand(true);
|
||||
label.set_widget_name("custom-key-label-text");
|
||||
label.set_wrap(true);
|
||||
label_box.append(&label);
|
||||
}
|
||||
}
|
||||
outer_box.append(&inner_box);
|
||||
}
|
||||
|
||||
fn set_search_text<T: Clone + Send>(text: &str, ui: &UiElements<T>, meta: &MetaData<T>) {
|
||||
let mut lock = ui.search_text.lock().unwrap();
|
||||
text.clone_into(&mut lock);
|
||||
if let Some(pw) = meta.config.password() {
|
||||
|
@ -377,7 +426,7 @@ fn set_search_text<T: Clone>(text: &str, ui: &UiElements<T>, meta: &MetaData<T>)
|
|||
}
|
||||
}
|
||||
|
||||
fn build_ui_from_menu_items<T: Clone + 'static>(
|
||||
fn build_ui_from_menu_items<T: Clone + 'static + Send>(
|
||||
ui: &Rc<UiElements<T>>,
|
||||
meta: &Rc<MetaData<T>>,
|
||||
mut items: Vec<MenuItem<T>>,
|
||||
|
@ -443,21 +492,31 @@ fn build_ui_from_menu_items<T: Clone + 'static>(
|
|||
fn setup_key_event_handler<T: Clone + 'static + Send>(
|
||||
ui: &Rc<UiElements<T>>,
|
||||
meta: &Rc<MetaData<T>>,
|
||||
custom_keys: &Option<Vec<CustomKey>>,
|
||||
) {
|
||||
let key_controller = EventControllerKey::new();
|
||||
|
||||
let ui_clone = Rc::clone(ui);
|
||||
let meta_clone = Rc::clone(meta);
|
||||
key_controller.connect_key_pressed(move |_, key_value, _, _| {
|
||||
handle_key_press(&ui_clone, &meta_clone, key_value)
|
||||
let keys_clone = custom_keys.clone();
|
||||
key_controller.connect_key_pressed(move |_, key_value, _, modifier| {
|
||||
handle_key_press(
|
||||
&ui_clone,
|
||||
&meta_clone,
|
||||
key_value,
|
||||
modifier,
|
||||
keys_clone.as_ref(),
|
||||
)
|
||||
});
|
||||
|
||||
ui.window.add_controller(key_controller);
|
||||
}
|
||||
fn handle_key_press<T: Clone + 'static>(
|
||||
fn handle_key_press<T: Clone + 'static + Send>(
|
||||
ui: &Rc<UiElements<T>>,
|
||||
meta: &Rc<MetaData<T>>,
|
||||
keyboard_key: Key,
|
||||
modifier_type: ModifierType,
|
||||
custom_keys: Option<&Vec<CustomKey>>,
|
||||
) -> Propagation {
|
||||
let update_view = |query: &String| {
|
||||
let mut lock = ui.menu_rows.lock().unwrap();
|
||||
|
@ -480,6 +539,24 @@ fn handle_key_press<T: Clone + 'static>(
|
|||
update_view(query);
|
||||
};
|
||||
|
||||
if let Some(custom_keys) = custom_keys {
|
||||
for custom_key in custom_keys {
|
||||
if custom_key.key == keyboard_key && custom_key.modifiers == modifier_type {
|
||||
let search_lock = ui.search_text.lock().unwrap();
|
||||
if let Err(e) = handle_selected_item(
|
||||
ui,
|
||||
meta,
|
||||
Some(&search_lock),
|
||||
None,
|
||||
meta.new_on_empty,
|
||||
Some(&custom_key),
|
||||
) {
|
||||
log::error!("{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match keyboard_key {
|
||||
Key::Escape => {
|
||||
if let Err(e) = meta.selected_sender.send(Err(Error::NoSelection)) {
|
||||
|
@ -490,7 +567,7 @@ fn handle_key_press<T: Clone + 'static>(
|
|||
Key::Return => {
|
||||
let search_lock = ui.search_text.lock().unwrap();
|
||||
if let Err(e) =
|
||||
handle_selected_item(ui, meta, Some(&search_lock), None, meta.new_on_empty)
|
||||
handle_selected_item(ui, meta, Some(&search_lock), None, meta.new_on_empty, None)
|
||||
{
|
||||
log::error!("{e}");
|
||||
}
|
||||
|
@ -627,12 +704,17 @@ fn handle_selected_item<T>(
|
|||
query: Option<&str>,
|
||||
item: Option<MenuItem<T>>,
|
||||
new_on_empty: bool,
|
||||
custom_key: Option<&CustomKey>,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
T: Clone,
|
||||
T: Clone + Send,
|
||||
{
|
||||
if let Some(selected_item) = item {
|
||||
if let Err(e) = meta.selected_sender.send(Ok(selected_item.clone())) {
|
||||
close_gui(ui.app.clone(), ui.window.clone(), &meta.config);
|
||||
if let Err(e) = meta.selected_sender.send(Ok(Selection {
|
||||
menu: selected_item.clone(),
|
||||
custom_key: custom_key.map(|k| k.clone()),
|
||||
})) {
|
||||
log::error!("failed to send message {e}");
|
||||
}
|
||||
|
||||
|
@ -663,7 +745,10 @@ where
|
|||
visible: true,
|
||||
};
|
||||
|
||||
if let Err(e) = meta.selected_sender.send(Ok(item.clone())) {
|
||||
if let Err(e) = meta.selected_sender.send(Ok(Selection {
|
||||
menu: item.clone(),
|
||||
custom_key: custom_key.map(|k| k.clone()),
|
||||
})) {
|
||||
log::error!("failed to send message {e}");
|
||||
}
|
||||
close_gui(&ui.app);
|
||||
|
@ -673,7 +758,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn add_menu_item<T: Clone + 'static>(
|
||||
fn add_menu_item<T: Clone + 'static + Send>(
|
||||
ui: &Rc<UiElements<T>>,
|
||||
meta: &Rc<MetaData<T>>,
|
||||
element_to_add: &MenuItem<T>,
|
||||
|
@ -718,7 +803,7 @@ fn add_menu_item<T: Clone + 'static>(
|
|||
child
|
||||
}
|
||||
|
||||
fn create_menu_row<T: Clone + 'static>(
|
||||
fn create_menu_row<T: Clone + 'static + Send>(
|
||||
ui: &Rc<UiElements<T>>,
|
||||
meta: &Rc<MetaData<T>>,
|
||||
element_to_add: &MenuItem<T>,
|
||||
|
@ -782,6 +867,7 @@ fn create_menu_row<T: Clone + 'static>(
|
|||
None,
|
||||
Some(element_clone.clone()),
|
||||
false,
|
||||
None,
|
||||
) {
|
||||
log::error!("{e}");
|
||||
}
|
||||
|
|
|
@ -712,9 +712,9 @@ pub fn d_run(config: &Config) -> Result<(), Error> {
|
|||
let mut cache = provider.cache.clone();
|
||||
|
||||
// todo ues a arc instead of cloning the config
|
||||
let selection_result = gui::show(config.clone(), provider, false, None);
|
||||
let selection_result = gui::show(config.clone(), provider, false, None, None);
|
||||
match selection_result {
|
||||
Ok(s) => update_drun_cache_and_run(cache_path, &mut cache, s)?,
|
||||
Ok(s) => update_drun_cache_and_run(cache_path, &mut cache, s.menu)?,
|
||||
Err(_) => {
|
||||
log::error!("No item selected");
|
||||
}
|
||||
|
@ -732,9 +732,9 @@ pub fn run(config: &Config) -> Result<(), Error> {
|
|||
let cache_path = provider.cache_path.clone();
|
||||
let mut cache = provider.cache.clone();
|
||||
|
||||
let selection_result = gui::show(config.clone(), provider, false, None);
|
||||
let selection_result = gui::show(config.clone(), provider, false, None, None);
|
||||
match selection_result {
|
||||
Ok(s) => update_run_cache_and_run(cache_path, &mut cache, s)?,
|
||||
Ok(s) => update_run_cache_and_run(cache_path, &mut cache, s.menu)?,
|
||||
Err(_) => {
|
||||
log::error!("No item selected");
|
||||
}
|
||||
|
@ -768,9 +768,11 @@ pub fn auto(config: &Config) -> Result<(), Error> {
|
|||
.map(|s| Regex::new(s).unwrap())
|
||||
.collect(),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
if let Ok(mut selection_result) = selection_result {
|
||||
let mut selection_result = selection_result.menu;
|
||||
if let Some(data) = &selection_result.data {
|
||||
match data {
|
||||
AutoRunType::Math => {
|
||||
|
@ -829,9 +831,10 @@ pub fn file(config: &Config) -> Result<(), Error> {
|
|||
provider,
|
||||
false,
|
||||
Some(vec![Regex::new("^\\$\\w+").unwrap()]),
|
||||
None,
|
||||
)?;
|
||||
if let Some(action) = selection_result.action {
|
||||
spawn_fork(&action, selection_result.working_dir.as_ref())
|
||||
if let Some(action) = selection_result.menu.action {
|
||||
spawn_fork(&action, selection_result.menu.working_dir.as_ref())
|
||||
} else {
|
||||
Err(Error::MissingAction)
|
||||
}
|
||||
|
@ -866,9 +869,9 @@ fn ssh_launch<T: Clone>(menu_item: &MenuItem<T>, config: &Config) -> Result<(),
|
|||
/// * if it didn't find a terminal
|
||||
pub fn ssh(config: &Config) -> Result<(), Error> {
|
||||
let provider = SshProvider::new(0, &config.sort_order());
|
||||
let selection_result = gui::show(config.clone(), provider, true, None);
|
||||
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
||||
if let Ok(mi) = selection_result {
|
||||
ssh_launch(&mi, config)?;
|
||||
ssh_launch(&mi.menu, config)?;
|
||||
} else {
|
||||
log::error!("No item selected");
|
||||
}
|
||||
|
@ -881,9 +884,9 @@ pub fn math(config: &Config) {
|
|||
loop {
|
||||
let mut provider = MathProvider::new(String::new());
|
||||
provider.add_elements(&mut calc.clone());
|
||||
let selection_result = gui::show(config.clone(), provider, true, None);
|
||||
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
||||
if let Ok(mi) = selection_result {
|
||||
calc.push(mi);
|
||||
calc.push(mi.menu);
|
||||
} else {
|
||||
log::error!("No item selected");
|
||||
break;
|
||||
|
@ -897,8 +900,8 @@ pub fn math(config: &Config) {
|
|||
/// Forwards errors from the gui. See `gui::show` for details.
|
||||
pub fn emoji(config: &Config) -> Result<(), Error> {
|
||||
let provider = EmojiProvider::new(0, &config.sort_order());
|
||||
let selection_result = gui::show(config.clone(), provider, true, None)?;
|
||||
match selection_result.action {
|
||||
let selection_result = gui::show(config.clone(), provider, true, None, None)?;
|
||||
match selection_result.menu.action {
|
||||
None => Err(Error::MissingAction),
|
||||
Some(action) => copy_to_clipboard(action),
|
||||
}
|
||||
|
@ -911,10 +914,10 @@ pub fn emoji(config: &Config) -> Result<(), Error> {
|
|||
pub fn dmenu(config: &Config) -> Result<(), Error> {
|
||||
let provider = DMenuProvider::new(&config.sort_order())?;
|
||||
|
||||
let selection_result = gui::show(config.clone(), provider, true, None);
|
||||
let selection_result = gui::show(config.clone(), provider, true, None, None);
|
||||
match selection_result {
|
||||
Ok(s) => {
|
||||
println!("{}", s.label);
|
||||
println!("{}", s.menu.label);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(Error::InvalidSelection),
|
||||
|
|
Loading…
Add table
Reference in a new issue