add support for a hint label in custom keys

this is used in worf-warden to
show that shift can be used as additional modifier
to send enter
This commit is contained in:
Alexander Mohr 2025-05-20 22:00:31 +02:00
parent 0557593230
commit 340b0fbfea
5 changed files with 179 additions and 67 deletions

View file

@ -65,6 +65,9 @@ Styling names and classes are inspired by wofi, so most of the documentation and
| `row` | Row containing the entry, used to control hover effects. |
| `custom-key-label-text` | The label for custom keys |
| `custom-key-label-box` | Box containing the label, can be used for borders etc. |
| `custom-key-hint-text` | The label for custom keys hint |
| `custom-key-hint-box` | Box containing the hint, can be used for borders etc. |
Checkout more showcases in the [styles directory of this repo](styles).

View file

@ -3,9 +3,9 @@ use std::env;
use std::process::Command;
use std::thread::sleep;
use std::time::Duration;
use worf_lib::config::Config;
use worf_lib::config::{Config, CustomKeyHintLocation};
use worf_lib::desktop::{copy_to_clipboard, spawn_fork};
use worf_lib::gui::{ItemProvider, Key, KeyBinding, MenuItem, Modifier};
use worf_lib::gui::{CustomKeyHint, CustomKeys, ItemProvider, Key, KeyBinding, MenuItem, Modifier};
use worf_lib::{config, gui};
#[derive(Clone)]
@ -189,6 +189,16 @@ fn key_type_all() -> KeyBinding {
key: Key::Num1,
modifiers: vec![Modifier::Alt].into_iter().collect(),
label: "<b>Alt+1</b> Type All".to_string(),
visible: true,
}
}
fn key_type_all_and_enter() -> KeyBinding {
KeyBinding {
key: Key::Exclamation,
modifiers: vec![Modifier::Alt, Modifier::Shift].into_iter().collect(),
label: String::new(),
visible: false,
}
}
@ -197,6 +207,16 @@ fn key_type_user() -> KeyBinding {
key: Key::Num2,
modifiers: vec![Modifier::Alt].into_iter().collect(),
label: "<b>Alt+2</b> Type User".to_string(),
visible: true,
}
}
fn key_type_user_and_enter() -> KeyBinding {
KeyBinding {
key: Key::At,
modifiers: vec![Modifier::Alt, Modifier::Shift].into_iter().collect(),
label: String::new(),
visible: false,
}
}
@ -205,6 +225,7 @@ fn key_type_password() -> KeyBinding {
key: Key::Num3,
modifiers: vec![Modifier::Alt].into_iter().collect(),
label: "<b>Alt+3</b> Type Password".to_string(),
visible: true,
}
}
@ -212,7 +233,8 @@ fn key_type_password_and_enter() -> KeyBinding {
KeyBinding {
key: Key::Hash,
modifiers: vec![Modifier::Alt, Modifier::Shift].into_iter().collect(),
label: "<b>Alt+Shift+3</b> Type Password + Enter".to_string(),
label: String::new(),
visible: false,
}
}
@ -221,6 +243,16 @@ fn key_type_totp() -> KeyBinding {
key: Key::Num4,
modifiers: vec![Modifier::Alt].into_iter().collect(),
label: "<b>Alt+4</b> Type Totp".to_string(),
visible: true,
}
}
fn key_type_totp_and_enter() -> KeyBinding {
KeyBinding {
key: Key::Dollar,
modifiers: vec![Modifier::Alt, Modifier::Shift].into_iter().collect(),
label: String::new(),
visible: false,
}
}
@ -229,6 +261,7 @@ fn key_sync() -> KeyBinding {
key: Key::R,
modifiers: vec![Modifier::Alt].into_iter().collect(),
label: "<b>Alt+r</b> Sync".to_string(),
visible: true,
}
}
@ -238,6 +271,7 @@ fn key_totp() -> KeyBinding {
key: Key::T,
modifiers: vec![Modifier::Alt].into_iter().collect(),
label: "<b>Alt+t</b> Totp".to_string(),
visible: true,
}
}
@ -246,6 +280,7 @@ fn key_lock() -> KeyBinding {
key: Key::L,
modifiers: vec![Modifier::Alt].into_iter().collect(),
label: "<b>Alt+l</b> Lock".to_string(),
visible: true,
}
}
@ -255,16 +290,25 @@ fn show(config: Config, provider: PasswordProvider) -> Result<(), String> {
provider,
false,
None,
Some(vec![
Some(CustomKeys {
bindings: vec![
key_type_all(),
key_type_all_and_enter(),
key_type_user(),
key_type_user_and_enter(),
key_type_password(),
key_type_password_and_enter(),
key_type_totp(),
key_type_totp_and_enter(),
key_sync(),
key_totp(),
key_lock(),
]),
],
hint: Some(CustomKeyHint {
label: "Use Shift as additional modifier to send enter".to_string(),
location: CustomKeyHintLocation::Top,
}),
}),
) {
Ok(selection) => {
if let Some(meta) = selection.menu.data {
@ -276,18 +320,15 @@ fn show(config: Config, provider: PasswordProvider) -> Result<(), String> {
sleep(Duration::from_millis(500));
if let Some(key) = selection.custom_key {
if key == key_type_all() {
if key == key_type_all() || key == key_type_all_and_enter() {
keyboard_type(&rbw_get_user(id, false)?);
keyboard_tab();
keyboard_type(&rbw_get_password(id, false)?);
} else if key == key_type_user() {
} else if key == key_type_user() || key == key_type_user_and_enter() {
keyboard_type(&rbw_get_user(id, false)?);
} else if key == key_type_password() {
} else if key == key_type_password() || key == key_type_password_and_enter() {
keyboard_type(&rbw_get_password(id, false)?);
} else if key == key_type_password_and_enter() {
keyboard_type(&rbw_get_password(id, false)?);
keyboard_return();
} else if key == key_type_totp() {
} else if key == key_type_totp() || key == key_type_totp_and_enter() {
keyboard_type(&rbw_get_totp(id, false)?);
} else if key == key_lock() {
rbw("lock", None)?;
@ -296,6 +337,10 @@ fn show(config: Config, provider: PasswordProvider) -> Result<(), String> {
} else if key == key_totp() {
rbw_get_totp(id, true)?;
}
if key.modifiers.contains(&Modifier::Shift) {
keyboard_return();
}
} else {
let pw = rbw_get_password(id, true)?;
if let Err(e) = copy_to_clipboard(pw, None) {

View file

@ -74,8 +74,15 @@ font-family: DejaVu;
margin-bottom: 0.25em;
border-right: 1px solid rgba(214, 174, 0, 1);
border-left: 1px solid rgba(214, 174, 0, 1);
border-bottom: 1px solid rgba(214, 174, 0, 1);
padding-left: 1em;
}
#custom-key-hint-box {
margin-top: 0.25em;
margin-bottom: 0.25em;
padding-left: 1em;
}
#custom-key-label-text {
}
}

View file

@ -49,6 +49,12 @@ pub enum SortOrder {
Alphabetical,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum CustomKeyHintLocation {
Top,
Bottom,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Mode {
/// searches `$PATH` for executables and allows them to be run by selecting them.

View file

@ -25,7 +25,7 @@ use gtk4_layer_shell::{Edge, KeyboardMode, LayerShell};
use log;
use regex::Regex;
use crate::config::{Anchor, Config, MatchMethod, SortOrder, WrapMode};
use crate::config::{Anchor, Config, CustomKeyHintLocation, MatchMethod, SortOrder, WrapMode};
use crate::desktop::known_image_extension_regex_pattern;
use crate::{Error, config, desktop};
@ -83,17 +83,11 @@ impl From<config::Align> for Align {
}
}
fn into_core_order(gtk_order: &Ordering) -> core::cmp::Ordering {
fn into_core_order(gtk_order: Ordering) -> core::cmp::Ordering {
match gtk_order {
Ordering::Smaller => {
core::cmp::Ordering::Less
}
Ordering::Larger => {
core::cmp::Ordering::Greater
}
_ => {
core::cmp::Ordering::Equal
}
Ordering::Smaller => core::cmp::Ordering::Less,
Ordering::Larger => core::cmp::Ordering::Greater,
_ => core::cmp::Ordering::Equal,
}
}
@ -386,6 +380,19 @@ pub struct KeyBinding {
pub key: Key,
pub modifiers: HashSet<Modifier>,
pub label: String,
pub visible: bool,
}
#[derive(Clone, PartialEq, Debug)]
pub struct CustomKeyHint {
pub label: String,
pub location: CustomKeyHintLocation,
}
#[derive(Clone, PartialEq, Debug)]
pub struct CustomKeys {
pub bindings: Vec<KeyBinding>,
pub hint: Option<CustomKeyHint>,
}
impl<T: Clone> MenuItem<T> {
@ -445,7 +452,7 @@ pub fn show<T, P>(
item_provider: P,
new_on_empty: bool,
search_ignored_words: Option<Vec<Regex>>,
custom_keys: Option<Vec<KeyBinding>>,
custom_keys: Option<CustomKeys>,
) -> Result<Selection<T>, Error>
where
T: Clone + 'static + Send,
@ -503,7 +510,7 @@ fn build_ui<T, P>(
app: Application,
new_on_empty: bool,
search_ignored_words: Option<Vec<Regex>>,
custom_keys: Option<&Vec<KeyBinding>>,
custom_keys: Option<&CustomKeys>,
) where
T: Clone + 'static + Send,
P: ItemProvider<T> + 'static + Send,
@ -569,7 +576,9 @@ 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);
if let Some(custom_keys) = custom_keys {
build_custom_key_view(custom_keys, &outer_box);
}
ui_elements.window.set_child(Some(&outer_box));
@ -595,15 +604,11 @@ fn build_ui<T, P>(
log::debug!("got items after {:?}", wait_for_items.elapsed());
let active_cfg = config.clone();
//let map_cfg = config.clone();
let animate_window = ui_elements.window.clone();
animate_window.connect_is_active_notify(move |w| {
window_show_resize(&active_cfg.clone(), w);
});
// animate_window.connect_map(move |w| {
// window_show_resize(&map_cfg.clone(), w);
// });
build_ui_from_menu_items(&ui_elements, &meta, provider_elements);
@ -626,7 +631,6 @@ fn build_main_box<T: Clone + 'static>(config: &Config, ui_elements: &Rc<UiElemen
.main_box
.set_max_children_per_line(config.columns());
ui_elements.main_box.set_activate_on_single_click(true);
ui_elements.main_box.set_halign(config.halign().into());
ui_elements.main_box.set_valign(config.valign().into());
if config.orientation() == config::Orientation::Horizontal {
@ -664,31 +668,75 @@ fn build_search_entry<T: Clone + Send>(
}
}
fn build_custom_key_view(custom_keys: Option<&Vec<KeyBinding>>, outer_box: &gtk4::Box) {
let inner_box = FlowBox::new();
inner_box.set_halign(Align::Fill);
inner_box.set_widget_name("custom-key-box");
if let Some(custom_keys) = custom_keys {
for key in custom_keys {
fn build_custom_key_view(custom_keys: &CustomKeys, outer_box: &gtk4::Box) {
fn create_label(inner_box: &FlowBox, text: &str, label_css: &str, box_css: &str) {
let label_box = FlowBoxChild::new();
label_box.set_halign(Align::Fill);
inner_box.set_valign(Align::Start);
label_box.set_widget_name("custom-key-label-box");
label_box.set_widget_name(box_css);
inner_box.append(&label_box);
inner_box.set_vexpand(false);
inner_box.set_hexpand(false);
let label = Label::new(Some(&key.label));
let label = Label::new(Some(text));
label.set_halign(Align::Fill);
label.set_valign(Align::Start);
label.set_use_markup(true);
label.set_hexpand(true);
label.set_vexpand(false);
label.set_widget_name("custom-key-label-text");
label.set_widget_name(label_css);
label.set_wrap(false);
label.set_xalign(0.0);
label_box.set_child(Some(&label));
}
let inner_box = gtk4::Box::new(Orientation::Vertical, 0);
inner_box.set_halign(Align::Fill);
let hint_box = FlowBox::new();
hint_box.set_halign(Align::Fill);
hint_box.set_widget_name("custom-key-box");
let custom_key_box = FlowBox::new();
custom_key_box.set_halign(Align::Fill);
custom_key_box.set_widget_name("custom-key-box");
inner_box.append(&custom_key_box);
let make_key_labels = || {
for key in custom_keys.bindings.iter().filter(|key| key.visible) {
create_label(
&custom_key_box,
key.label.as_ref(),
"custom-key-label-text",
"custom-key-label-box",
);
}
};
if let Some(hint) = custom_keys.hint.as_ref() {
match hint.location {
CustomKeyHintLocation::Top => {
inner_box.append(&hint_box);
create_label(
&hint_box,
&hint.label,
"custom-key-hint-text",
"custom-key-hint-box",
);
make_key_labels();
} // todo this surely can be done better
CustomKeyHintLocation::Bottom => {
make_key_labels();
create_label(
&hint_box,
&hint.label,
"custom-key-hint-text",
"custom-key-hint-box",
);
inner_box.append(&hint_box);
}
}
}
outer_box.append(&inner_box);
}
@ -771,7 +819,7 @@ fn build_ui_from_menu_items<T: Clone + 'static + Send>(
fn setup_key_event_handler<T: Clone + 'static + Send>(
ui: &Rc<UiElements<T>>,
meta: &Rc<MetaData<T>>,
custom_keys: Option<&Vec<KeyBinding>>,
custom_keys: Option<&CustomKeys>,
) {
let key_controller = EventControllerKey::new();
@ -797,7 +845,7 @@ fn handle_key_press<T: Clone + 'static + Send>(
meta: &Rc<MetaData<T>>,
keyboard_key: gdk4::Key,
modifier_type: gdk4::ModifierType,
custom_keys: Option<&Vec<KeyBinding>>,
custom_keys: Option<&CustomKeys>,
) -> Propagation {
let update_view = |query: &String| {
let mut lock = ui.menu_rows.write().unwrap();
@ -820,7 +868,7 @@ fn handle_key_press<T: Clone + 'static + Send>(
if let Some(custom_keys) = custom_keys {
let mods = modifiers_from_mask(modifier_type);
for custom_key in custom_keys {
for custom_key in &custom_keys.bindings {
log::debug!(
"comparing custom key {custom_key:?} to mask {mods:?} and key {keyboard_key}"
);
@ -935,7 +983,10 @@ fn sort_flow_box_childs<T: Clone>(
sort_menu_items_by_score(m1, m2)
}
fn sort_menu_items_by_score<T: Clone>(m1: Option<&MenuItem<T>>, m2: Option<&MenuItem<T>>) -> Ordering {
fn sort_menu_items_by_score<T: Clone>(
m1: Option<&MenuItem<T>>,
m2: Option<&MenuItem<T>>,
) -> Ordering {
match (m1, m2) {
(Some(menu1), Some(menu2)) => {
fn compare(a: f64, b: f64) -> Ordering {
@ -1376,7 +1427,7 @@ pub fn apply_sort<T: Clone>(items: &mut [MenuItem<T>], order: &SortOrder) {
}
}
items.sort_by(|l, r| into_core_order(&sort_menu_items_by_score(Some(l), Some(r))));
items.sort_by(|l, r| into_core_order(sort_menu_items_by_score(Some(l), Some(r))));
}
}
}