Wire ↑↓↵ keyboard navigation in the command overlay

The footer chips already advertised ↑↓ to navigate and ↵ to open;
they now match reality.

- chrome::command_match exposes COMMAND_ACTIONS / matching_actions
  alongside the tab/history/bookmark match helpers and a
  CommandSelection enum + visible_command_rows that returns the flat
  ordered list of activatable rows. command_overlay drops its private
  copies of the action const and matcher and consumes them from the
  shared module so render and key handling share one source of truth.

- ElyShell tracks command_selected_index with command_select_next /
  command_select_prev (cyclic, no notify when index doesn't change)
  and activate_selected_command which dispatches the right shell call
  for the currently selected CommandSelection variant and dismisses
  the overlay. Dismissing command mode resets the index to 0.

- The shell root captures key_down via on_command_overlay_key_down.
  When the live snapshot's command_query starts with '>', up / down
  / enter run the matching helper and the event stops propagating so
  the omnibar input doesn't move its caret.

- Each rendered row receives a `selected` flag. The selected row gets
  the design's tinted bg + accent-bar on the left edge so the active
  result is unambiguous at any keyboard step.
This commit is contained in:
2026-05-09 20:12:31 -04:00
parent 4403eacf15
commit 49e4816bce
5 changed files with 266 additions and 67 deletions
+67
View File
@@ -81,6 +81,73 @@ impl ElyShell {
self.command_input.update(cx, |input, cx| {
input.set_value("", window, cx);
});
self.command_selected_index = 0;
cx.notify();
}
pub(crate) fn command_select_next(&mut self, total_rows: usize, cx: &mut Context<Self>) {
if total_rows == 0 {
self.command_selected_index = 0;
return;
}
let next = (self.command_selected_index + 1) % total_rows;
if next != self.command_selected_index {
self.command_selected_index = next;
cx.notify();
}
}
pub(crate) fn command_select_prev(&mut self, total_rows: usize, cx: &mut Context<Self>) {
if total_rows == 0 {
self.command_selected_index = 0;
return;
}
let prev = if self.command_selected_index == 0 {
total_rows - 1
} else {
self.command_selected_index - 1
};
if prev != self.command_selected_index {
self.command_selected_index = prev;
cx.notify();
}
}
pub(crate) fn activate_selected_command(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) {
let snapshot = match &self.state {
ShellState::Ready(core) => match core.snapshot() {
Ok(snapshot) => snapshot,
Err(_) => return,
},
ShellState::StartupError(_) => return,
};
let needle_owned = snapshot.command_query.as_str();
let Some(stripped) = needle_owned.strip_prefix('>') else {
return;
};
let needle = stripped.trim().to_lowercase();
let rows = crate::shell::chrome::command_match::visible_command_rows(
&snapshot, &needle,
);
if rows.is_empty() {
return;
}
let index = self.command_selected_index.min(rows.len() - 1);
match rows[index].clone() {
crate::shell::chrome::command_match::CommandSelection::SelectTab(tab_id) => {
self.select_tab(&tab_id, window, cx);
}
crate::shell::chrome::command_match::CommandSelection::OpenUrl(url) => {
self.open_internal_tab(url.as_str(), window, cx);
}
crate::shell::chrome::command_match::CommandSelection::OpenRoute(route) => {
self.open_internal_tab(route, window, cx);
}
}
self.dismiss_command_mode(window, cx);
}
}