feat(dmenu): add full dmenu compatibility

- Add free-form text input (output typed text when no item matches)
- Add proper exit codes (0=selection, 1=cancelled)
- Detect dmenu mode via ProviderManager::is_dmenu_mode()

This enables standard dmenu usage patterns like:
  echo -e "yes\nno" | owlry -m dmenu && echo "selected"
This commit is contained in:
2026-01-02 16:36:40 +01:00
parent 1aa92ee1e5
commit b85f85c4da

View File

@@ -73,6 +73,8 @@ pub struct MainWindow {
lazy_state: Rc<RefCell<LazyLoadState>>,
/// Debounce source ID for cancelling pending searches
debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>,
/// Whether we're in dmenu mode (stdin pipe input)
is_dmenu_mode: bool,
}
impl MainWindow {
@@ -197,6 +199,9 @@ impl MainWindow {
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default()));
// Check if we're in dmenu mode (stdin pipe input)
let is_dmenu_mode = providers.borrow().is_dmenu_mode();
let main_window = Self {
window,
search_entry,
@@ -215,6 +220,7 @@ impl MainWindow {
custom_prompt,
lazy_state,
debounce_source: Rc::new(RefCell::new(None)),
is_dmenu_mode,
};
main_window.setup_signals();
@@ -735,12 +741,14 @@ impl MainWindow {
let mode_label_for_activate = self.mode_label.clone();
let hints_label_for_activate = self.hints_label.clone();
let search_entry_for_activate = self.search_entry.clone();
let is_dmenu_mode_for_activate = self.is_dmenu_mode;
self.search_entry.connect_activate(move |entry| {
let selected = results_list_for_activate
.selected_row()
.or_else(|| results_list_for_activate.row_at_index(0));
// Handle the case where we have a selected item
if let Some(row) = selected {
let index = row.index() as usize;
let results = current_results_for_activate.borrow();
@@ -787,6 +795,10 @@ impl MainWindow {
&providers_for_activate,
);
if should_close {
// In dmenu mode, exit with success code
if is_dmenu_mode_for_activate {
std::process::exit(0);
}
window_for_activate.close();
} else {
// Trigger search refresh for updated widget state
@@ -794,6 +806,16 @@ impl MainWindow {
}
}
}
return;
}
}
// No item selected/matched - in dmenu mode, output the typed text
if is_dmenu_mode_for_activate {
let text = entry.text();
if !text.is_empty() {
println!("{}", text);
std::process::exit(0);
}
}
});
@@ -834,6 +856,7 @@ impl MainWindow {
let hints_label = self.hints_label.clone();
let submenu_state = self.submenu_state.clone();
let tab_order = self.tab_order.clone();
let is_dmenu_mode = self.is_dmenu_mode;
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
@@ -856,6 +879,10 @@ impl MainWindow {
);
gtk4::glib::Propagation::Stop
} else {
// In dmenu mode, exit with cancel code (1)
if is_dmenu_mode {
std::process::exit(1);
}
window.close();
gtk4::glib::Propagation::Stop
}
@@ -873,6 +900,10 @@ impl MainWindow {
);
gtk4::glib::Propagation::Stop
} else {
// In dmenu mode, exit with cancel code (1)
if is_dmenu_mode {
std::process::exit(1);
}
window.close();
gtk4::glib::Propagation::Stop
}