Codex Handbook
tui/src/keymap_setup.rs 1932 lines
//! Guided keymap remapping UI for `/keymap`.//!//! This module owns the interactive editing flow that starts from a resolved//! [`RuntimeKeymap`] and produces a new root-level [`TuiKeymap`] override. The//! picker and action menus show users the currently active binding, which may//! come from defaults, global fallback, or explicit config, while writes always//! target the concrete `tui.keymap.<context>.<action>` slot selected by the//! user.//!//! The flow is intentionally split into three steps: choose an action, choose//! whether to replace/add/remove a binding, then capture exactly one terminal//! key event. Validation happens after capture by reusing runtime keymap//! resolution, so conflict rules stay centralized in `keymap.rs` instead of//! being duplicated in the UI.//!//! This module does not persist config files directly. It emits app events with//! the edited config so the app layer can decide how to save, reload, and//! surface errors.mod actions;mod debug;mod picker;pub(crate) use actions::KeymapActionFilter;pub(crate) use debug::build_keymap_debug_view;pub(crate) use picker::KEYMAP_PICKER_VIEW_ID;#[cfg(test)]pub(crate) use picker::build_keymap_picker_params;#[cfg(test)]pub(crate) use picker::build_keymap_picker_params_for_selected_action;pub(crate) use picker::build_keymap_picker_params_for_selected_action_with_filter;pub(crate) use picker::build_keymap_picker_params_with_filter;use codex_config::types::KeybindingSpec;use codex_config::types::KeybindingsSpec;use codex_config::types::MAX_FUNCTION_KEY;use codex_config::types::TuiKeymap;use crossterm::event::KeyCode;use crossterm::event::KeyEvent;use crossterm::event::KeyEventKind;use crossterm::event::KeyModifiers;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::widgets::Paragraph;use ratatui::widgets::Widget;use crate::app_event::AppEvent;use crate::app_event::KeymapEditIntent;use crate::app_event_sender::AppEventSender;use crate::bottom_pane::BottomPaneView;use crate::bottom_pane::CancellationEvent;use crate::bottom_pane::ColumnWidthMode;use crate::bottom_pane::SelectionItem;use crate::bottom_pane::SelectionViewParams;use crate::bottom_pane::popup_consts::standard_popup_hint_line;use crate::key_hint::KeyBinding;use crate::keymap::RuntimeKeymap;use crate::render::renderable::ColumnRenderable;use crate::render::renderable::Renderable;use actions::KEYMAP_ACTIONS;use actions::action_label;use actions::binding_slot;use actions::bindings_for_action;use actions::format_binding_summary;#[cfg(test)]use debug::KeymapDebugView;pub(crate) const KEYMAP_ACTION_MENU_VIEW_ID: &str = "keymap-action-menu";pub(crate) const KEYMAP_REPLACE_BINDING_MENU_VIEW_ID: &str = "keymap-replace-binding-menu";#[derive(Debug, PartialEq, Eq)]pub(crate) enum KeymapEditOutcome {    /// The edit produced a new config snapshot and user-facing status message.    Updated {        keymap_config: Box<TuiKeymap>,        bindings: Vec<String>,        message: String,    },    /// The requested edit resolved to the same effective binding set.    Unchanged { message: String },}fn key_binding_span(binding: &str) -> ratatui::text::Span<'static> {    if binding == "unbound" {        binding.to_string().dim()    } else {        binding.to_string().cyan()    }}fn keymap_action_menu_hint_line() -> Line<'static> {    Line::from(vec![        "enter".cyan(),        " select · ".dim(),        "esc".cyan(),        " back".dim(),    ])}fn open_capture_action(    context: String,    action: String,    intent: KeymapEditIntent,) -> Box<dyn Fn(&AppEventSender) + Send + Sync> {    Box::new(move |tx| {        tx.send(AppEvent::OpenKeymapCapture {            context: context.clone(),            action: action.clone(),            intent: intent.clone(),        });    })}fn action_menu_item(    name: &str,    description: &str,    selected_description: String,    context: &str,    action: &str,    intent: KeymapEditIntent,) -> SelectionItem {    SelectionItem {        name: name.to_string(),        description: Some(description.to_string()),        selected_description: Some(selected_description),        actions: vec![open_capture_action(            context.to_string(),            action.to_string(),            intent,        )],        ..Default::default()    }}/// Build the action-specific menu after a user chooses a shortcut row.////// The menu is based on both active runtime bindings and root config state: the/// active bindings decide whether replace/add choices are available, while the/// config state decides whether "remove custom binding" can restore fallback/// behavior. Passing stale context/action strings yields a generic fallback/// menu rather than panicking, because selection views can outlive config reloads.pub(crate) fn build_keymap_action_menu_params(    context: String,    action: String,    runtime_keymap: &RuntimeKeymap,    keymap_config: &TuiKeymap,) -> SelectionViewParams {    let current_bindings =        active_binding_specs(runtime_keymap, &context, &action).unwrap_or_else(|_| Vec::new());    let current_binding = if current_bindings.is_empty() {        "unbound".to_string()    } else {        current_bindings.join(", ")    };    let active_binding_count = current_bindings.len();    let custom_binding = has_custom_binding(keymap_config, &context, &action).unwrap_or(false);    let descriptor = KEYMAP_ACTIONS        .iter()        .find(|descriptor| descriptor.context == context && descriptor.action == action);    let context_label = descriptor        .map(|descriptor| descriptor.context_label)        .unwrap_or(context.as_str())        .to_string();    let description = descriptor        .map(|descriptor| descriptor.description)        .unwrap_or("Configure this shortcut.");    let remove_disabled_reason = (!custom_binding)        .then(|| "There is no custom root binding for this action to remove.".to_string());    let label = action_label(&action);    let remove_context = context.clone();    let remove_action = action.clone();    let config_path = format!("tui.keymap.{context}.{action}");    let source = if custom_binding {        "Custom root override".cyan()    } else {        "Default keymap".dim()    };    let mut header = ColumnRenderable::new();    header.push(Line::from("Edit Shortcut".bold()));    header.push(Line::from(vec![        label.bold(),        " · ".dim(),        context_label.dim(),    ]));    header.push(Line::from(vec![        "Current ".dim(),        key_binding_span(&current_binding),        " · ".dim(),        source,    ]));    header.push(Line::from(vec![        "Config ".dim(),        format!("`{config_path}`").cyan(),    ]));    header.push(Line::from(description.to_string().dim()));    let mut items = Vec::new();    match active_binding_count {        0 => {            items.push(action_menu_item(                "Set key",                "Capture a key for this unbound action.",                "Capture one key and bind this action.".to_string(),                &context,                &action,                KeymapEditIntent::ReplaceAll,            ));        }        1 => {            items.push(action_menu_item(                "Replace binding",                "Capture a replacement key.",                format!("Capture one key and replace `{current_binding}`."),                &context,                &action,                KeymapEditIntent::ReplaceAll,            ));            items.push(action_menu_item(                "Add alternate binding",                "Keep the current binding and add another key.",                format!("Capture one key and keep `{current_binding}` as an alternate."),                &context,                &action,                KeymapEditIntent::AddAlternate,            ));        }        _ => {            let replace_one_context = context.clone();            let replace_one_action = action.clone();            items.push(SelectionItem {                name: "Replace one binding...".to_string(),                description: Some("Choose which existing binding to replace.".to_string()),                selected_description: Some(                    "Pick one current binding, then capture its replacement.".to_string(),                ),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::OpenKeymapReplaceBindingMenu {                        context: replace_one_context.clone(),                        action: replace_one_action.clone(),                    });                })],                ..Default::default()            });            items.push(action_menu_item(                "Replace all bindings",                "Replace every current binding with one key.",                format!("Capture one key and replace `{current_binding}`."),                &context,                &action,                KeymapEditIntent::ReplaceAll,            ));            items.push(action_menu_item(                "Add alternate binding",                "Keep current bindings and add another key.",                format!("Capture one key and keep `{current_binding}`."),                &context,                &action,                KeymapEditIntent::AddAlternate,            ));        }    }    items.push(SelectionItem {        name: "Remove custom binding".to_string(),        description: Some(if custom_binding {            "Restore the default keymap binding.".to_string()        } else {            "No root override to remove.".to_string()        }),        selected_description: Some(            "Delete the root override and use the default keymap again.".to_string(),        ),        disabled_reason: remove_disabled_reason,        actions: vec![Box::new(move |tx| {            tx.send(AppEvent::KeymapCleared {                context: remove_context.clone(),                action: remove_action.clone(),            });        })],        ..Default::default()    });    items.push(SelectionItem {        name: "Back to shortcuts".to_string(),        description: Some("Return to the shortcut list.".to_string()),        dismiss_on_select: true,        ..Default::default()    });    SelectionViewParams {        view_id: Some(KEYMAP_ACTION_MENU_VIEW_ID),        header: Box::new(header),        footer_note: Some(Line::from(vec![            "Changes write the root ".dim(),            "`tui.keymap.*`".cyan(),            " override.".dim(),        ])),        footer_hint: Some(keymap_action_menu_hint_line()),        items,        col_width_mode: ColumnWidthMode::Fixed,        ..Default::default()    }}pub(crate) fn build_keymap_replace_binding_menu_params(    context: String,    action: String,    runtime_keymap: &RuntimeKeymap,) -> SelectionViewParams {    let bindings = active_binding_specs(runtime_keymap, &context, &action).unwrap_or_default();    let label = action_label(&action);    let mut header = ColumnRenderable::new();    header.push(Line::from("Replace Binding".bold()));    header.push(Line::from(vec![        label.bold(),        " · ".dim(),        format!("{context}.{action}").dim(),    ]));    header.push(Line::from("Choose the binding to replace.".dim()));    let items = bindings        .into_iter()        .map(|binding| {            let capture_context = context.clone();            let capture_action = action.clone();            let old_key = binding.clone();            SelectionItem {                name: binding.clone(),                description: Some("Replace this binding.".to_string()),                selected_description: Some(format!("Capture a new key to replace `{binding}`.")),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::OpenKeymapCapture {                        context: capture_context.clone(),                        action: capture_action.clone(),                        intent: KeymapEditIntent::ReplaceOne {                            old_key: old_key.clone(),                        },                    });                })],                dismiss_on_select: true,                ..Default::default()            }        })        .collect();    SelectionViewParams {        view_id: Some(KEYMAP_REPLACE_BINDING_MENU_VIEW_ID),        header: Box::new(header),        footer_hint: Some(keymap_action_menu_hint_line()),        items,        col_width_mode: ColumnWidthMode::Fixed,        ..Default::default()    }}pub(crate) fn build_keymap_conflict_params(    context: String,    action: String,    key: String,    intent: KeymapEditIntent,    error: String,) -> SelectionViewParams {    let retry_context = context.clone();    let retry_action = action.clone();    let retry_intent = intent;    SelectionViewParams {        title: Some("Shortcut Conflict".to_string()),        subtitle: Some(format!("{context}.{action} cannot use `{key}`.")),        footer_note: Some(Line::from(error)),        footer_hint: Some(standard_popup_hint_line()),        items: vec![            SelectionItem {                name: "Pick another key".to_string(),                description: Some("Return to key capture for this action.".to_string()),                actions: vec![Box::new(move |tx| {                    tx.send(AppEvent::OpenKeymapCapture {                        context: retry_context.clone(),                        action: retry_action.clone(),                        intent: retry_intent.clone(),                    });                })],                dismiss_on_select: true,                ..Default::default()            },            SelectionItem {                name: "Cancel".to_string(),                description: Some("Leave keymap unchanged.".to_string()),                dismiss_on_select: true,                ..Default::default()            },        ],        col_width_mode: ColumnWidthMode::Fixed,        ..Default::default()    }}/// Build the transient capture view for the selected keymap edit.////// The view displays the current binding summary from the latest runtime map/// and then delegates the captured key back to the app event loop. Unknown/// actions are rendered as unbound so the eventual edit path can report the/// stale selection with a precise error.pub(crate) fn build_keymap_capture_view(    context: String,    action: String,    intent: KeymapEditIntent,    runtime_keymap: &RuntimeKeymap,    app_event_tx: AppEventSender,) -> KeymapCaptureView {    let current_binding = format_binding_summary(        bindings_for_action(runtime_keymap, &context, &action).unwrap_or(&[]),    );    let label = action_label(&action);    KeymapCaptureView::new(        context,        action,        intent,        label,        current_binding,        app_event_tx,    )}#[cfg(test)]fn keymap_with_replacement(    keymap: &TuiKeymap,    context: &str,    action: &str,    key: &str,) -> Result<TuiKeymap, String> {    keymap_with_bindings(keymap, context, action, &[key.to_string()])}/// Apply a captured key to one action and return the edited root config.////// The current effective bindings come from `runtime_keymap`, so adding an/// alternate to a default-only action first materializes those defaults into/// root config before appending the captured key. Replacing one binding guards/// against stale menus by requiring the selected `old_key` to still be active;/// otherwise a user could overwrite a binding that changed after the menu was/// opened.pub(crate) fn keymap_with_edit(    keymap: &TuiKeymap,    runtime_keymap: &RuntimeKeymap,    context: &str,    action: &str,    key: &str,    intent: &KeymapEditIntent,) -> Result<KeymapEditOutcome, String> {    let current_bindings = active_binding_specs(runtime_keymap, context, action)?;    let next_bindings = match intent {        KeymapEditIntent::ReplaceAll => vec![key.to_string()],        KeymapEditIntent::AddAlternate => {            if current_bindings.iter().any(|binding| binding == key) {                return Ok(KeymapEditOutcome::Unchanged {                    message: format!("No change: `{context}.{action}` already uses `{key}`."),                });            }            let mut bindings = current_bindings.clone();            bindings.push(key.to_string());            bindings        }        KeymapEditIntent::ReplaceOne { old_key } => {            if !current_bindings.iter().any(|binding| binding == old_key) {                return Err(format!(                    "`{context}.{action}` no longer uses `{old_key}`. Reopen /keymap and choose a binding again."                ));            }            let bindings = current_bindings                .iter()                .map(|binding| {                    if binding == old_key {                        key.to_string()                    } else {                        binding.clone()                    }                })                .collect::<Vec<_>>();            dedup_bindings(bindings)        }    };    if next_bindings == current_bindings {        return Ok(KeymapEditOutcome::Unchanged {            message: format!("No change: `{context}.{action}` already uses `{key}`."),        });    }    let message = match intent {        KeymapEditIntent::ReplaceAll => format!("Remapped `{context}.{action}` to `{key}`."),        KeymapEditIntent::AddAlternate => format!("Added `{key}` to `{context}.{action}`."),        KeymapEditIntent::ReplaceOne { old_key } => {            format!("Replaced `{old_key}` with `{key}` for `{context}.{action}`.")        }    };    Ok(KeymapEditOutcome::Updated {        keymap_config: Box::new(keymap_with_bindings(            keymap,            context,            action,            &next_bindings,        )?),        bindings: next_bindings,        message,    })}fn keymap_with_bindings(    keymap: &TuiKeymap,    context: &str,    action: &str,    keys: &[String],) -> Result<TuiKeymap, String> {    let mut keymap = keymap.clone();    let slot = binding_slot(&mut keymap, context, action).ok_or_else(|| {        format!("Unknown keymap action `{context}.{action}`. Reopen /keymap and choose an action.")    })?;    *slot = Some(match keys {        [key] => KeybindingsSpec::One(KeybindingSpec(key.clone())),        keys => KeybindingsSpec::Many(            keys.iter()                .map(|key| KeybindingSpec(key.clone()))                .collect::<Vec<_>>(),        ),    });    Ok(keymap)}/// Return the active config key specs for one runtime action.////// This converts resolved [`crate::key_hint::KeyBinding`] values back into/// canonical config strings for display and for edit operations that need to/// preserve existing bindings. Callers should treat errors as stale UI state,/// because valid menu entries should always point at known actions.pub(crate) fn active_binding_specs(    runtime_keymap: &RuntimeKeymap,    context: &str,    action: &str,) -> Result<Vec<String>, String> {    let bindings = bindings_for_action(runtime_keymap, context, action).ok_or_else(|| {        format!("Unknown keymap action `{context}.{action}`. Reopen /keymap and choose an action.")    })?;    bindings        .iter()        .map(|binding| binding_to_config_key_spec(*binding))        .collect()}fn dedup_bindings(bindings: Vec<String>) -> Vec<String> {    bindings.into_iter().fold(Vec::new(), |mut deduped, key| {        if !deduped.contains(&key) {            deduped.push(key);        }        deduped    })}/// Remove the root-level custom binding for one action.////// Clearing the slot with `None` is different from setting an empty binding/// list: `None` restores default/global fallback behavior, while an empty list/// explicitly unbinds the action in runtime resolution.pub(crate) fn keymap_without_custom_binding(    keymap: &TuiKeymap,    context: &str,    action: &str,) -> Result<TuiKeymap, String> {    let mut keymap = keymap.clone();    let slot = binding_slot(&mut keymap, context, action).ok_or_else(|| {        format!("Unknown keymap action `{context}.{action}`. Reopen /keymap and choose an action.")    })?;    *slot = None;    Ok(keymap)}fn has_custom_binding(keymap: &TuiKeymap, context: &str, action: &str) -> Result<bool, String> {    let mut keymap = keymap.clone();    let slot = binding_slot(&mut keymap, context, action).ok_or_else(|| {        format!("Unknown keymap action `{context}.{action}`. Reopen /keymap and choose an action.")    })?;    Ok(slot.is_some())}/// Bottom-pane view that captures a single key event for a pending `/keymap` edit.////// The view is deliberately transient: it renders instructions, accepts one/// keypress, and emits the captured key to the app layer. It does not mutate/// config itself, because mutation needs the latest runtime keymap to detect/// conflicts and stale selections.pub(crate) struct KeymapCaptureView {    context: String,    action: String,    intent: KeymapEditIntent,    label: String,    current_binding: String,    app_event_tx: AppEventSender,    complete: bool,    error_message: Option<String>,}impl KeymapCaptureView {    fn new(        context: String,        action: String,        intent: KeymapEditIntent,        label: String,        current_binding: String,        app_event_tx: AppEventSender,    ) -> Self {        Self {            context,            action,            intent,            label,            current_binding,            app_event_tx,            complete: false,            error_message: None,        }    }    fn lines(&self, width: u16) -> Vec<Line<'static>> {        let wrap_width = usize::from(width.max(1));        let mut lines = vec![            Line::from("Remap Shortcut".bold()),            Line::from(vec![                "Action: ".dim(),                self.label.clone().into(),                "  ".into(),                format!("{}.{}", self.context, self.action).dim(),            ]),            Line::from(vec!["Current: ".dim(), self.current_binding.clone().cyan()]),            Line::from("Press the new key now. Esc cancels.".dim()),        ];        if let Some(error) = &self.error_message {            lines.push(Line::from(""));            let options = textwrap::Options::new(wrap_width)                .initial_indent("Error: ")                .subsequent_indent("       ");            lines.extend(                textwrap::wrap(error, options)                    .into_iter()                    .map(|line| Line::from(line.into_owned().red())),            );        }        lines    }}impl Renderable for KeymapCaptureView {    fn render(&self, area: Rect, buf: &mut Buffer) {        Paragraph::new(self.lines(area.width)).render(area, buf);    }    fn desired_height(&self, width: u16) -> u16 {        self.lines(width).len() as u16    }}impl BottomPaneView for KeymapCaptureView {    fn handle_key_event(&mut self, key_event: KeyEvent) {        if key_event.kind == KeyEventKind::Release {            return;        }        if key_event.code == KeyCode::Esc {            self.complete = true;            return;        }        match key_event_to_config_key_spec(key_event) {            Ok(key) => {                self.app_event_tx.send(AppEvent::KeymapCaptured {                    context: self.context.clone(),                    action: self.action.clone(),                    key,                    intent: self.intent.clone(),                });                self.complete = true;            }            Err(error) => {                self.error_message = Some(error);            }        }    }    fn is_complete(&self) -> bool {        self.complete    }    fn on_ctrl_c(&mut self) -> CancellationEvent {        self.complete = true;        CancellationEvent::Handled    }    fn prefer_esc_to_handle_key_event(&self) -> bool {        true    }}fn key_event_to_config_key_spec(key_event: KeyEvent) -> Result<String, String> {    binding_to_config_key_spec(KeyBinding::from_event(key_event))}fn binding_to_config_key_spec(binding: KeyBinding) -> Result<String, String> {    let (code, modifiers) = binding.parts();    key_parts_to_config_key_spec(code, modifiers)}fn key_parts_to_config_key_spec(    code: KeyCode,    mut modifiers: KeyModifiers,) -> Result<String, String> {    let (code, normalized_modifiers) = crate::key_hint::normalize_key_parts(code, modifiers);    modifiers = normalized_modifiers;    let supported_modifiers = KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT;    if !modifiers.difference(supported_modifiers).is_empty() {        return Err(            "Only ctrl, alt, and shift modifiers can be stored in `tui.keymap`.".to_string(),        );    }    let key = match code {        KeyCode::Enter => "enter".to_string(),        KeyCode::Tab => "tab".to_string(),        KeyCode::Backspace => "backspace".to_string(),        KeyCode::Esc => "esc".to_string(),        KeyCode::Delete => "delete".to_string(),        KeyCode::Up => "up".to_string(),        KeyCode::Down => "down".to_string(),        KeyCode::Left => "left".to_string(),        KeyCode::Right => "right".to_string(),        KeyCode::Home => "home".to_string(),        KeyCode::End => "end".to_string(),        KeyCode::PageUp => "page-up".to_string(),        KeyCode::PageDown => "page-down".to_string(),        KeyCode::F(number) if (1..=MAX_FUNCTION_KEY).contains(&number) => format!("f{number}"),        KeyCode::F(_) => {            return Err(format!(                "Only function keys F1 through F{MAX_FUNCTION_KEY} can be stored in `tui.keymap`."            ));        }        KeyCode::Char(' ') => "space".to_string(),        KeyCode::Char(mut ch) => {            if ch == '-' {                return Ok(format_key_spec(modifiers, "minus"));            }            if !ch.is_ascii() || ch.is_ascii_control() {                return Err("Only printable ASCII keys can be stored in `tui.keymap`.".to_string());            }            if ch.is_ascii_uppercase() {                modifiers.insert(KeyModifiers::SHIFT);                ch = ch.to_ascii_lowercase();            }            ch.to_string()        }        _ => {            return Err("That key is not supported by `tui.keymap`.".to_string());        }    };    Ok(format_key_spec(modifiers, &key))}fn format_key_spec(modifiers: KeyModifiers, key: &str) -> String {    let mut parts = Vec::new();    if modifiers.contains(KeyModifiers::CONTROL) {        parts.push("ctrl");    }    if modifiers.contains(KeyModifiers::ALT) {        parts.push("alt");    }    if modifiers.contains(KeyModifiers::SHIFT) {        parts.push("shift");    }    parts.push(key);    parts.join("-")}#[cfg(test)]mod tests {    use super::picker::KEYMAP_ALL_TAB_ID;    use super::picker::KEYMAP_COMMON_TAB_ID;    use super::picker::KEYMAP_CUSTOM_TAB_ID;    use super::picker::KEYMAP_DEBUG_TAB_ID;    use super::picker::KEYMAP_UNBOUND_TAB_ID;    use super::*;    use crate::bottom_pane::BottomPane;    use crate::bottom_pane::BottomPaneParams;    use crate::bottom_pane::ListSelectionView;    use crate::bottom_pane::SelectionTab;    use crate::tui::FrameRequester;    use insta::assert_snapshot;    use pretty_assertions::assert_eq;    use ratatui::buffer::Buffer;    use tokio::sync::mpsc::UnboundedReceiver;    use tokio::sync::mpsc::unbounded_channel;    fn app_event_sender() -> AppEventSender {        let (tx, _rx) = unbounded_channel();        AppEventSender::new(tx)    }    fn render_capture(view: &KeymapCaptureView, width: u16, height: u16) -> Buffer {        let area = Rect::new(0, 0, width, height);        let mut buf = Buffer::empty(area);        view.render(area, &mut buf);        buf    }    fn render_debug(view: &KeymapDebugView, width: u16) -> String {        let height = view.desired_height(width);        let area = Rect::new(0, 0, width, height);        let mut buf = Buffer::empty(area);        view.render(area, &mut buf);        render_buffer(&buf)    }    fn render_picker(params: SelectionViewParams, width: u16) -> String {        let view =            ListSelectionView::new(params, app_event_sender(), RuntimeKeymap::defaults().list);        render_picker_from_view(&view, width)    }    fn render_picker_from_view(view: &ListSelectionView, width: u16) -> String {        let height = view.desired_height(width);        let area = Rect::new(0, 0, width, height);        let mut buf = Buffer::empty(area);        view.render(area, &mut buf);        render_buffer(&buf)    }    fn fast_mode_action_filter() -> KeymapActionFilter {        KeymapActionFilter {            fast_mode_enabled: true,        }    }    fn render_buffer(buf: &Buffer) -> String {        let area = buf.area();        (0..area.height)            .map(|row| {                let mut line = String::new();                for col in 0..area.width {                    let symbol = buf[(col, row)].symbol();                    if symbol.is_empty() {                        line.push(' ');                    } else {                        line.push_str(symbol);                    }                }                line.trim_end().to_string()            })            .collect::<Vec<_>>()            .join("\n")    }    fn test_pane() -> (BottomPane, AppEventSender, UnboundedReceiver<AppEvent>) {        let (tx_raw, rx) = unbounded_channel::<AppEvent>();        let tx = AppEventSender::new(tx_raw);        let pane = BottomPane::new(BottomPaneParams {            app_event_tx: tx.clone(),            frame_requester: FrameRequester::test_dummy(),            has_input_focus: true,            enhanced_keys_supported: false,            placeholder_text: "Ask Codex to do anything".to_string(),            disable_paste_burst: false,            animations_enabled: false,            skills: Some(Vec::new()),        });        (pane, tx, rx)    }    fn selection_tab<'a>(params: &'a SelectionViewParams, id: &str) -> &'a SelectionTab {        params            .tabs            .iter()            .find(|tab| tab.id == id)            .expect("selection tab")    }    fn selection_item<'a>(params: &'a SelectionViewParams, name: &str) -> &'a SelectionItem {        params            .items            .iter()            .find(|item| item.name == name)            .expect("selection item")    }    fn action_menu_rows(params: &SelectionViewParams) -> String {        params            .items            .iter()            .map(|item| {                format!(                    "{} | {} | {}",                    item.name,                    item.description.as_deref().unwrap_or_default(),                    item.disabled_reason.as_deref().unwrap_or("enabled")                )            })            .collect::<Vec<_>>()            .join("\n")    }    #[test]    fn picker_covers_every_replaceable_action() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params_with_filter(            &runtime,            &TuiKeymap::default(),            fast_mode_action_filter(),        );        let all_tab = selection_tab(&params, KEYMAP_ALL_TAB_ID);        assert!(params.items.is_empty());        assert_eq!(all_tab.items.len(), KEYMAP_ACTIONS.len());        assert!(            all_tab.items.iter().all(|item| !item.dismiss_on_select),            "keymap picker should stay open behind the action menu"        );        assert!(KEYMAP_ACTIONS.iter().all(|descriptor| {            binding_slot(                &mut TuiKeymap::default(),                descriptor.context,                descriptor.action,            )            .is_some()        }));        assert!(KEYMAP_ACTIONS.iter().all(|descriptor| {            bindings_for_action(&runtime, descriptor.context, descriptor.action).is_some()        }));    }    #[test]    fn picker_hides_fast_mode_action_when_feature_is_disabled() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let all_tab = selection_tab(&params, KEYMAP_ALL_TAB_ID);        assert!(            all_tab                .items                .iter()                .all(|item| item.name != "Toggle Fast Mode")        );    }    #[test]    fn picker_shows_fast_mode_action_when_feature_is_enabled() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params_with_filter(            &runtime,            &TuiKeymap::default(),            fast_mode_action_filter(),        );        let all_tab = selection_tab(&params, KEYMAP_ALL_TAB_ID);        let common_tab = selection_tab(&params, KEYMAP_COMMON_TAB_ID);        let app_tab = selection_tab(&params, "app-shortcuts");        let unbound_tab = selection_tab(&params, KEYMAP_UNBOUND_TAB_ID);        for tab in [all_tab, common_tab, app_tab, unbound_tab] {            assert!(                tab.items.iter().any(|item| item.name == "Toggle Fast Mode"),                "expected Toggle Fast Mode in {}",                tab.label            );        }    }    #[test]    fn keymap_picker_fast_mode_enabled_snapshot() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params_with_filter(            &runtime,            &TuiKeymap::default(),            fast_mode_action_filter(),        );        assert_snapshot!(            "keymap_picker_fast_mode_enabled",            render_picker(params, /*width*/ 120)        );    }    #[test]    fn picker_common_tab_lists_curated_actions() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let common_tab = selection_tab(&params, KEYMAP_COMMON_TAB_ID);        let actions = common_tab            .items            .iter()            .map(|item| {                item.search_value                    .as_deref()                    .unwrap_or_default()                    .split_whitespace()                    .take(2)                    .collect::<Vec<_>>()                    .join(".")            })            .collect::<Vec<_>>();        assert_eq!(            actions,            vec![                "Composer.submit",                "Chat.interrupt_turn",                "Editor.insert_newline",                "Composer.queue",                "Global.open_external_editor",                "Global.copy",                "Global.toggle_vim_mode",                "Editor.delete_backward_word",                "Editor.delete_forward_word",                "Editor.move_word_left",                "Editor.move_word_right",                "Global.open_transcript",                "Pager.close",                "Pager.page_up",                "Pager.page_down",                "Approval.open_fullscreen",                "Approval.approve",                "Approval.approve_for_session",                "Approval.decline",                "Approval.cancel",            ]        );    }    #[test]    fn picker_approval_tab_lists_all_approval_actions() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let approval_tab = selection_tab(&params, "approval-shortcuts");        let actions = approval_tab            .items            .iter()            .map(|item| {                item.search_value                    .as_deref()                    .unwrap_or_default()                    .split_whitespace()                    .take(2)                    .collect::<Vec<_>>()                    .join(".")            })            .collect::<Vec<_>>();        assert_eq!(            actions,            vec![                "Approval.open_fullscreen",                "Approval.open_thread",                "Approval.approve",                "Approval.approve_for_session",                "Approval.approve_for_prefix",                "Approval.deny",                "Approval.decline",                "Approval.cancel",            ]        );    }    #[test]    fn picker_content_snapshot() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let all_tab = selection_tab(&params, KEYMAP_ALL_TAB_ID);        let snapshot = params            .tabs            .iter()            .map(|tab| {                let selectable = tab.items.iter().filter(|item| !item.is_disabled).count();                format!("tab: {} ({selectable} selectable)", tab.label)            })            .chain(all_tab.items.iter().take(12).map(|item| {                format!(                    "{} | {} | {}",                    item.name,                    item.description.as_deref().unwrap_or_default(),                    item.search_value.as_deref().unwrap_or_default()                )            }))            .collect::<Vec<_>>()            .join("\n");        assert_snapshot!("keymap_picker_first_actions", snapshot);    }    #[test]    fn picker_customized_tab_contains_root_overrides() {        let keymap =            keymap_with_replacement(&TuiKeymap::default(), "composer", "submit", "ctrl-enter")                .expect("replace binding");        let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");        let params = build_keymap_picker_params(&runtime, &keymap);        let custom_tab = selection_tab(&params, KEYMAP_CUSTOM_TAB_ID);        let composer_tab = selection_tab(&params, "composer-shortcuts");        assert_eq!(            custom_tab                .items                .iter()                .map(|item| item.name.as_str())                .collect::<Vec<_>>(),            vec!["Submit"]        );        assert!(            composer_tab                .items                .iter()                .any(|item| item.description.as_deref() == Some("ctrl-enter"))        );    }    #[test]    fn picker_unbound_tab_lists_default_unbound_actions() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let unbound_tab = selection_tab(&params, KEYMAP_UNBOUND_TAB_ID);        assert_eq!(unbound_tab.items.len(), 2);        assert_eq!(unbound_tab.items[0].name, "Toggle Vim Mode");        assert_eq!(unbound_tab.items[0].description.as_deref(), Some("unbound"));        assert!(!unbound_tab.items[0].is_disabled);        assert_eq!(unbound_tab.items[1].name, "Kill Whole Line");        assert_eq!(unbound_tab.items[1].description.as_deref(), Some("unbound"));        assert!(!unbound_tab.items[1].is_disabled);    }    #[test]    fn picker_debug_tab_is_last_and_opens_inspector() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let debug_tab = params.tabs.last().expect("debug tab");        assert_eq!(debug_tab.id, KEYMAP_DEBUG_TAB_ID);        assert_eq!(debug_tab.label, "Debug");        assert_eq!(debug_tab.items.len(), 1);        assert_eq!(debug_tab.items[0].name, "Inspect keypresses");        assert_eq!(            debug_tab.items[0].description.as_deref(),            Some("Press Enter to start. Then press any key to inspect it; Ctrl+C exits.")        );        assert!(            params                .tab_footer_hints                .iter()                .any(|(tab_id, _)| tab_id == KEYMAP_DEBUG_TAB_ID)        );    }    #[test]    fn picker_selected_action_starts_on_matching_all_tab_row() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params_for_selected_action(            &runtime,            &TuiKeymap::default(),            "composer",            "submit",        );        let all_tab = selection_tab(&params, KEYMAP_ALL_TAB_ID);        assert_eq!(params.initial_tab_id.as_deref(), Some(KEYMAP_ALL_TAB_ID));        assert_eq!(            params.initial_selected_idx,            all_tab.items.iter().position(|item| item.name == "Submit")        );    }    #[test]    fn picker_all_tab_items_remain_searchable() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let all_tab = selection_tab(&params, KEYMAP_ALL_TAB_ID);        let snapshot = all_tab            .items            .iter()            .take(12)            .map(|item| {                format!(                    "{} | {} | {}",                    item.name,                    item.description.as_deref().unwrap_or_default(),                    item.search_value.as_deref().unwrap_or_default()                )            })            .collect::<Vec<_>>()            .join("\n");        assert_snapshot!("keymap_picker_all_tab_search", snapshot);    }    #[test]    fn picker_wide_render_snapshot() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        assert_snapshot!("keymap_picker_wide", render_picker(params, /*width*/ 120));    }    #[test]    fn picker_narrow_render_snapshot() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        assert_snapshot!("keymap_picker_narrow", render_picker(params, /*width*/ 78));    }    #[test]    fn picker_custom_render_snapshot() {        let keymap =            keymap_with_replacement(&TuiKeymap::default(), "composer", "submit", "ctrl-enter")                .expect("replace binding");        let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");        let params = build_keymap_picker_params(&runtime, &keymap);        assert_snapshot!("keymap_picker_custom", render_picker(params, /*width*/ 120));    }    #[test]    fn picker_narrow_uses_compact_tabs() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());        let rendered = render_picker(params, /*width*/ 78);        assert!(rendered.contains("Keymap"));        assert!(rendered.contains("Open Transcript"));        assert!(rendered.contains("ctrl-t"));        assert!(!rendered.contains("Selected Action"));        assert!(!rendered.contains("Source: default keymap"));    }    #[test]    fn action_menu_content_snapshot() {        let unbound_keymap = keymap_with_bindings(&TuiKeymap::default(), "global", "copy", &[])            .expect("unbound copy");        let unbound_runtime = RuntimeKeymap::from_config(&unbound_keymap).expect("runtime keymap");        let unbound_params = build_keymap_action_menu_params(            "global".to_string(),            "copy".to_string(),            &unbound_runtime,            &unbound_keymap,        );        let single_keymap =            keymap_with_replacement(&TuiKeymap::default(), "composer", "submit", "ctrl-enter")                .expect("replace binding");        let single_runtime = RuntimeKeymap::from_config(&single_keymap).expect("runtime keymap");        let single_params = build_keymap_action_menu_params(            "composer".to_string(),            "submit".to_string(),            &single_runtime,            &single_keymap,        );        let multi_keymap = keymap_with_bindings(            &TuiKeymap::default(),            "composer",            "submit",            &["ctrl-enter".to_string(), "alt-shift-enter".to_string()],        )        .expect("multi binding");        let multi_runtime = RuntimeKeymap::from_config(&multi_keymap).expect("runtime keymap");        let multi_params = build_keymap_action_menu_params(            "composer".to_string(),            "submit".to_string(),            &multi_runtime,            &multi_keymap,        );        let replace_params = build_keymap_replace_binding_menu_params(            "composer".to_string(),            "submit".to_string(),            &multi_runtime,        );        let snapshot = [            "unbound:",            &action_menu_rows(&unbound_params),            "",            "single:",            &action_menu_rows(&single_params),            "",            "multi:",            &action_menu_rows(&multi_params),            "",            "replace picker:",            &action_menu_rows(&replace_params),        ]        .join("\n");        assert_snapshot!("keymap_action_menu", snapshot);    }    #[test]    fn action_menu_disables_clear_when_action_has_no_custom_binding() {        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_action_menu_params(            "composer".to_string(),            "submit".to_string(),            &runtime,            &TuiKeymap::default(),        );        assert_eq!(params.view_id, Some(KEYMAP_ACTION_MENU_VIEW_ID));        let replace = selection_item(&params, "Replace binding");        let add_alternate = selection_item(&params, "Add alternate binding");        let remove = selection_item(&params, "Remove custom binding");        let back = selection_item(&params, "Back to shortcuts");        assert_eq!(            remove.disabled_reason.as_deref(),            Some("There is no custom root binding for this action to remove.")        );        assert!(            !replace.dismiss_on_select,            "replace should keep the action menu under key capture"        );        assert!(            !add_alternate.dismiss_on_select,            "add alternate should keep the action menu under key capture"        );        assert!(!remove.dismiss_on_select, "clear-key waits for save result");        assert!(            back.dismiss_on_select,            "back should dismiss the action menu"        );    }    #[test]    fn capture_view_snapshot() {        let view = KeymapCaptureView::new(            "composer".to_string(),            "submit".to_string(),            KeymapEditIntent::ReplaceAll,            "Submit".to_string(),            "enter".to_string(),            app_event_sender(),        );        assert_snapshot!(            "keymap_capture_view",            format!("{:?}", render_capture(&view, /*width*/ 80, /*height*/ 8))        );    }    #[test]    fn debug_view_initial_snapshot() {        let view = build_keymap_debug_view(&RuntimeKeymap::defaults(), &TuiKeymap::default());        assert_snapshot!(            "keymap_debug_view_initial",            render_debug(&view, /*width*/ 80)        );    }    #[test]    fn debug_view_shows_delayed_missing_key_hint() {        let mut view = build_keymap_debug_view(&RuntimeKeymap::defaults(), &TuiKeymap::default());        view.show_delayed_hint_for_test();        let rendered = render_debug(&view, /*width*/ 100);        assert!(rendered.contains("Still waiting?"));        assert_snapshot!("keymap_debug_view_delayed_hint", rendered);    }    #[test]    fn debug_view_reports_detected_key_and_matching_actions() {        let mut view = build_keymap_debug_view(&RuntimeKeymap::defaults(), &TuiKeymap::default());        view.show_delayed_hint_for_test();        view.handle_key_event(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));        let rendered = render_debug(&view, /*width*/ 100);        assert!(!rendered.contains("Still waiting?"));        assert_snapshot!("keymap_debug_view_match", rendered);    }    #[test]    fn debug_view_uses_custom_binding_source() {        let keymap =            keymap_with_replacement(&TuiKeymap::default(), "global", "copy", "ctrl-x").unwrap();        let runtime = RuntimeKeymap::from_config(&keymap).unwrap();        let mut view = build_keymap_debug_view(&runtime, &keymap);        view.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL));        let rendered = render_debug(&view, /*width*/ 100);        assert!(rendered.contains("global.copy (Copy)"));        assert!(rendered.contains("[Custom]"));    }    #[test]    fn debug_view_labels_custom_global_fallback_source() {        let mut keymap = TuiKeymap::default();        keymap.global.queue = Some(KeybindingsSpec::One(KeybindingSpec("ctrl-q".to_string())));        let runtime = RuntimeKeymap::from_config(&keymap).unwrap();        let mut view = build_keymap_debug_view(&runtime, &keymap);        view.handle_key_event(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL));        let rendered = render_debug(&view, /*width*/ 100);        assert!(rendered.contains("composer.queue (Queue)"));        assert!(rendered.contains("[Custom global]"));    }    #[test]    fn capture_completion_returns_to_selected_keymap_picker_row() {        let (mut pane, tx, mut rx) = test_pane();        let runtime = RuntimeKeymap::defaults();        pane.show_selection_view(build_keymap_picker_params(&runtime, &TuiKeymap::default()));        pane.show_selection_view(build_keymap_action_menu_params(            "composer".to_string(),            "submit".to_string(),            &runtime,            &TuiKeymap::default(),        ));        pane.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));        let AppEvent::OpenKeymapCapture {            context,            action,            intent,        } = rx.try_recv().expect("open capture event")        else {            panic!("expected OpenKeymapCapture event");        };        assert_eq!(intent, KeymapEditIntent::ReplaceAll);        assert_eq!(pane.active_view_id(), Some(KEYMAP_ACTION_MENU_VIEW_ID));        pane.show_view(Box::new(build_keymap_capture_view(            context, action, intent, &runtime, tx,        )));        pane.handle_key_event(KeyEvent::new(KeyCode::Char('K'), KeyModifiers::CONTROL));        let AppEvent::KeymapCaptured {            context,            action,            key,            intent,        } = rx.try_recv().expect("captured key event")        else {            panic!("expected KeymapCaptured event");        };        assert_eq!(context, "composer");        assert_eq!(action, "submit");        assert_eq!(key, "ctrl-shift-k");        assert_eq!(intent, KeymapEditIntent::ReplaceAll);        assert_eq!(pane.active_view_id(), Some(KEYMAP_ACTION_MENU_VIEW_ID));        let keymap =            keymap_with_replacement(&TuiKeymap::default(), &context, &action, &key).unwrap();        let runtime = RuntimeKeymap::from_config(&keymap).unwrap();        let params =            build_keymap_picker_params_for_selected_action(&runtime, &keymap, &context, &action);        let selected_idx = params.initial_selected_idx;        assert!(            pane.replace_active_views_with_selection_view(                &[                    KEYMAP_PICKER_VIEW_ID,                    KEYMAP_ACTION_MENU_VIEW_ID,                    KEYMAP_REPLACE_BINDING_MENU_VIEW_ID,                ],                params            ),            "successful assignment should return to the main picker"        );        assert_eq!(pane.active_view_id(), Some(KEYMAP_PICKER_VIEW_ID));        assert_eq!(            pane.selected_index_for_active_view(KEYMAP_PICKER_VIEW_ID),            selected_idx        );        pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert!(            pane.no_modal_or_popup_active(),            "the original picker should not remain behind the refreshed picker"        );    }    #[test]    fn clear_completion_returns_to_selected_keymap_picker_row() {        let (mut pane, _tx, mut rx) = test_pane();        let keymap =            keymap_with_replacement(&TuiKeymap::default(), "composer", "submit", "ctrl-enter")                .unwrap();        let runtime = RuntimeKeymap::from_config(&keymap).unwrap();        pane.show_selection_view(build_keymap_picker_params(&runtime, &keymap));        pane.show_selection_view(build_keymap_action_menu_params(            "composer".to_string(),            "submit".to_string(),            &runtime,            &keymap,        ));        pane.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));        pane.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));        pane.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));        let AppEvent::KeymapCleared { context, action } =            rx.try_recv().expect("clear keymap event")        else {            panic!("expected KeymapCleared event");        };        assert_eq!(context, "composer");        assert_eq!(action, "submit");        assert_eq!(pane.active_view_id(), Some(KEYMAP_ACTION_MENU_VIEW_ID));        let runtime = RuntimeKeymap::defaults();        let params = build_keymap_picker_params_for_selected_action(            &runtime,            &TuiKeymap::default(),            &context,            &action,        );        let selected_idx = params.initial_selected_idx;        assert!(            pane.replace_active_views_with_selection_view(                &[                    KEYMAP_PICKER_VIEW_ID,                    KEYMAP_ACTION_MENU_VIEW_ID,                    KEYMAP_REPLACE_BINDING_MENU_VIEW_ID,                ],                params            ),            "successful clear should return to the main picker"        );        assert_eq!(pane.active_view_id(), Some(KEYMAP_PICKER_VIEW_ID));        assert_eq!(            pane.selected_index_for_active_view(KEYMAP_PICKER_VIEW_ID),            selected_idx        );        pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert!(            pane.no_modal_or_popup_active(),            "the original picker should not remain behind the refreshed picker"        );    }    #[test]    fn replace_one_completion_drops_focused_keymap_submenus() {        let (mut pane, _tx, _rx) = test_pane();        let runtime = RuntimeKeymap::defaults();        pane.show_selection_view(build_keymap_picker_params(&runtime, &TuiKeymap::default()));        pane.show_selection_view(build_keymap_action_menu_params(            "composer".to_string(),            "toggle_shortcuts".to_string(),            &runtime,            &TuiKeymap::default(),        ));        pane.show_selection_view(build_keymap_replace_binding_menu_params(            "composer".to_string(),            "toggle_shortcuts".to_string(),            &runtime,        ));        assert_eq!(            pane.active_view_id(),            Some(KEYMAP_REPLACE_BINDING_MENU_VIEW_ID)        );        let params = build_keymap_picker_params_for_selected_action(            &runtime,            &TuiKeymap::default(),            "composer",            "toggle_shortcuts",        );        assert!(            pane.replace_active_views_with_selection_view(                &[                    KEYMAP_PICKER_VIEW_ID,                    KEYMAP_ACTION_MENU_VIEW_ID,                    KEYMAP_REPLACE_BINDING_MENU_VIEW_ID,                ],                params            ),            "successful replace-one should return to the main picker"        );        assert_eq!(pane.active_view_id(), Some(KEYMAP_PICKER_VIEW_ID));        pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));        assert!(            pane.no_modal_or_popup_active(),            "the parent action menu should not remain behind the picker"        );    }    #[test]    fn key_capture_serializes_modifier_order_for_config() {        let event = KeyEvent::new(            KeyCode::Char('K'),            KeyModifiers::CONTROL | KeyModifiers::ALT,        );        assert_eq!(            key_event_to_config_key_spec(event),            Ok("ctrl-alt-shift-k".to_string())        );    }    #[test]    fn key_capture_serializes_special_keys() {        assert_eq!(            key_event_to_config_key_spec(KeyEvent::new(KeyCode::PageDown, KeyModifiers::SHIFT)),            Ok("shift-page-down".to_string())        );    }    #[test]    fn key_capture_serializes_function_keys_through_f24() {        assert_eq!(            key_event_to_config_key_spec(KeyEvent::from(KeyCode::F(13))),            Ok("f13".to_string())        );        assert_eq!(            key_event_to_config_key_spec(KeyEvent::from(KeyCode::F(24))),            Ok("f24".to_string())        );        assert_eq!(            key_event_to_config_key_spec(KeyEvent::from(KeyCode::F(25))),            Err("Only function keys F1 through F24 can be stored in `tui.keymap`.".to_string())        );    }    #[test]    fn key_capture_serializes_c0_control_chars_as_ctrl_bindings() {        assert_eq!(            key_event_to_config_key_spec(KeyEvent::new(                KeyCode::Char('\u{000a}'),                KeyModifiers::NONE,            )),            Ok("ctrl-j".to_string())        );        assert_eq!(            key_event_to_config_key_spec(KeyEvent::new(                KeyCode::Char('\u{0015}'),                KeyModifiers::NONE,            )),            Ok("ctrl-u".to_string())        );        assert_eq!(            key_event_to_config_key_spec(KeyEvent::new(                KeyCode::Char('\u{0010}'),                KeyModifiers::NONE,            )),            Ok("ctrl-p".to_string())        );    }    #[test]    fn key_capture_serializes_minus_as_named_key() {        assert_eq!(            key_event_to_config_key_spec(KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE)),            Ok("minus".to_string())        );        assert_eq!(            key_event_to_config_key_spec(KeyEvent::new(KeyCode::Char('-'), KeyModifiers::ALT)),            Ok("alt-minus".to_string())        );        assert_eq!(            key_event_to_config_key_spec(KeyEvent::new(                KeyCode::Char('-'),                KeyModifiers::CONTROL | KeyModifiers::ALT,            )),            Ok("ctrl-alt-minus".to_string())        );    }    #[test]    fn replacement_sets_single_binding() {        let keymap =            keymap_with_replacement(&TuiKeymap::default(), "composer", "submit", "ctrl-enter")                .expect("replace binding");        assert_eq!(            keymap.composer.submit,            Some(KeybindingsSpec::One(KeybindingSpec(                "ctrl-enter".to_string()            )))        );    }    #[test]    fn replace_all_collapses_multi_binding_to_single() {        let keymap = keymap_with_bindings(            &TuiKeymap::default(),            "composer",            "submit",            &["ctrl-enter".to_string(), "alt-shift-enter".to_string()],        )        .expect("multi binding");        let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");        let outcome = keymap_with_edit(            &keymap,            &runtime,            "composer",            "submit",            "ctrl-shift-enter",            &KeymapEditIntent::ReplaceAll,        )        .expect("replace all");        let KeymapEditOutcome::Updated {            keymap_config,            bindings,            ..        } = outcome        else {            panic!("expected updated keymap");        };        assert_eq!(bindings, vec!["ctrl-shift-enter"]);        assert_eq!(            keymap_config.composer.submit,            Some(KeybindingsSpec::One(KeybindingSpec(                "ctrl-shift-enter".to_string()            )))        );    }    #[test]    fn add_alternate_grows_single_binding() {        let runtime = RuntimeKeymap::defaults();        let outcome = keymap_with_edit(            &TuiKeymap::default(),            &runtime,            "composer",            "submit",            "ctrl-enter",            &KeymapEditIntent::AddAlternate,        )        .expect("add alternate");        let KeymapEditOutcome::Updated {            keymap_config,            bindings,            ..        } = outcome        else {            panic!("expected updated keymap");        };        assert_eq!(bindings, vec!["enter", "ctrl-enter"]);        assert_eq!(            keymap_config.composer.submit,            Some(KeybindingsSpec::Many(vec![                KeybindingSpec("enter".to_string()),                KeybindingSpec("ctrl-enter".to_string())            ]))        );    }    #[test]    fn add_alternate_grows_default_multi_binding() {        let runtime = RuntimeKeymap::defaults();        let outcome = keymap_with_edit(            &TuiKeymap::default(),            &runtime,            "editor",            "move_left",            "ctrl-shift-b",            &KeymapEditIntent::AddAlternate,        )        .expect("add alternate");        let KeymapEditOutcome::Updated {            keymap_config,            bindings,            ..        } = outcome        else {            panic!("expected updated keymap");        };        assert_eq!(bindings, vec!["left", "ctrl-b", "ctrl-shift-b"]);        assert_eq!(            keymap_config.editor.move_left,            Some(KeybindingsSpec::Many(vec![                KeybindingSpec("left".to_string()),                KeybindingSpec("ctrl-b".to_string()),                KeybindingSpec("ctrl-shift-b".to_string())            ]))        );    }    #[test]    fn add_alternate_duplicate_is_noop() {        let runtime = RuntimeKeymap::defaults();        let outcome = keymap_with_edit(            &TuiKeymap::default(),            &runtime,            "composer",            "submit",            "enter",            &KeymapEditIntent::AddAlternate,        )        .expect("duplicate alternate");        assert_eq!(            outcome,            KeymapEditOutcome::Unchanged {                message: "No change: `composer.submit` already uses `enter`.".to_string()            }        );    }    #[test]    fn replace_one_preserves_other_bindings() {        let keymap = keymap_with_bindings(            &TuiKeymap::default(),            "composer",            "submit",            &["ctrl-enter".to_string(), "alt-shift-enter".to_string()],        )        .expect("multi binding");        let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");        let outcome = keymap_with_edit(            &keymap,            &runtime,            "composer",            "submit",            "ctrl-shift-enter",            &KeymapEditIntent::ReplaceOne {                old_key: "ctrl-enter".to_string(),            },        )        .expect("replace one");        let KeymapEditOutcome::Updated {            keymap_config,            bindings,            ..        } = outcome        else {            panic!("expected updated keymap");        };        assert_eq!(bindings, vec!["ctrl-shift-enter", "alt-shift-enter"]);        assert_eq!(            keymap_config.composer.submit,            Some(KeybindingsSpec::Many(vec![                KeybindingSpec("ctrl-shift-enter".to_string()),                KeybindingSpec("alt-shift-enter".to_string())            ]))        );    }    #[test]    fn replace_one_deduplicates_replacement() {        let keymap = keymap_with_bindings(            &TuiKeymap::default(),            "composer",            "submit",            &["ctrl-enter".to_string(), "ctrl-shift-enter".to_string()],        )        .expect("multi binding");        let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");        let outcome = keymap_with_edit(            &keymap,            &runtime,            "composer",            "submit",            "ctrl-shift-enter",            &KeymapEditIntent::ReplaceOne {                old_key: "ctrl-enter".to_string(),            },        )        .expect("replace one");        let KeymapEditOutcome::Updated {            keymap_config,            bindings,            ..        } = outcome        else {            panic!("expected updated keymap");        };        assert_eq!(bindings, vec!["ctrl-shift-enter"]);        assert_eq!(            keymap_config.composer.submit,            Some(KeybindingsSpec::One(KeybindingSpec(                "ctrl-shift-enter".to_string()            )))        );    }    #[test]    fn replace_one_rejects_stale_old_key() {        let runtime = RuntimeKeymap::defaults();        let err = keymap_with_edit(            &TuiKeymap::default(),            &runtime,            "composer",            "submit",            "ctrl-enter",            &KeymapEditIntent::ReplaceOne {                old_key: "alt-enter".to_string(),            },        )        .expect_err("stale old key");        assert!(err.contains("composer.submit"));        assert!(err.contains("alt-enter"));    }    #[test]    fn clear_removes_custom_binding() {        let keymap =            keymap_with_replacement(&TuiKeymap::default(), "composer", "submit", "ctrl-enter")                .expect("replace binding");        assert_eq!(has_custom_binding(&keymap, "composer", "submit"), Ok(true));        let cleared =            keymap_without_custom_binding(&keymap, "composer", "submit").expect("clear binding");        assert_eq!(cleared.composer.submit, None);        assert_eq!(            has_custom_binding(&cleared, "composer", "submit"),            Ok(false)        );    }    #[test]    fn replacement_rejects_unknown_action() {        let err = keymap_with_replacement(&TuiKeymap::default(), "composer", "nope", "ctrl-enter")            .expect_err("unknown action");        assert!(err.contains("composer.nope"));    }}