Replace alt-key scheme with directional focus (ctrl-arrows + F2)
Alt-modified keys never reach the app on keyboard layouts where Alt composes characters (e.g. dk_mac_fixed: alt-c = ©, alt-q = @), so the modifier-split model was unusable. New scheme, layout-proof: - F2 show/hide the claude pane - ctrl-down focus the claude pane (spawns/shows it if hidden) - ctrl-up focus the feed - claude focused: every other key goes to claude (bold cyan border) - feed focused: original plain keybindings unchanged - CT_DEBUG_KEYS=1 surfaces raw key events in the status bar Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
76
src/ui.rs
76
src/ui.rs
@@ -14,19 +14,24 @@ use std::time::Duration;
|
||||
struct EmbedUi {
|
||||
term: Option<EmbeddedTerm>,
|
||||
visible: bool,
|
||||
/// Keyboard focus is on the claude pane (vs. the feed above it).
|
||||
/// Directional: ctrl-↓ moves focus into the pane, ctrl-↑ back to the feed.
|
||||
claude_focused: bool,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl EmbedUi {
|
||||
/// Pane is visible and the child is still running → it owns plain keys.
|
||||
/// Pane is visible, child alive, and holds keyboard focus → it gets keys.
|
||||
fn focused(&self) -> bool {
|
||||
self.visible && self.term.as_ref().is_some_and(|t| !t.exited())
|
||||
self.visible
|
||||
&& self.claude_focused
|
||||
&& self.term.as_ref().is_some_and(|t| !t.exited())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(app: SharedApp, port: u16) -> anyhow::Result<()> {
|
||||
let mut terminal = ratatui::init();
|
||||
let mut eui = EmbedUi { term: None, visible: false, port };
|
||||
let mut eui = EmbedUi { term: None, visible: false, claude_focused: false, port };
|
||||
let res = event_loop(&mut terminal, app, &mut eui);
|
||||
ratatui::restore();
|
||||
res
|
||||
@@ -35,6 +40,7 @@ pub fn run(app: SharedApp, port: u16) -> anyhow::Result<()> {
|
||||
fn toggle_embed(eui: &mut EmbedUi, app: &SharedApp) {
|
||||
if eui.visible {
|
||||
eui.visible = false;
|
||||
eui.claude_focused = false;
|
||||
// A dead child is dropped on hide so the next toggle respawns.
|
||||
if eui.term.as_ref().is_some_and(|t| t.exited()) {
|
||||
eui.term = None;
|
||||
@@ -42,6 +48,11 @@ fn toggle_embed(eui: &mut EmbedUi, app: &SharedApp) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
show_embed_pane(eui, app);
|
||||
}
|
||||
|
||||
/// Show (spawning if needed) the claude pane and give it keyboard focus.
|
||||
fn show_embed_pane(eui: &mut EmbedUi, app: &SharedApp) {
|
||||
if eui.term.as_ref().is_some_and(|t| t.exited()) {
|
||||
eui.term = None;
|
||||
}
|
||||
@@ -62,6 +73,7 @@ fn toggle_embed(eui: &mut EmbedUi, app: &SharedApp) {
|
||||
}
|
||||
}
|
||||
eui.visible = true;
|
||||
eui.claude_focused = true;
|
||||
}
|
||||
|
||||
fn event_loop(
|
||||
@@ -78,21 +90,37 @@ fn event_loop(
|
||||
if k.kind == KeyEventKind::Release {
|
||||
continue;
|
||||
}
|
||||
// Alt-c toggles the embedded claude pane in every state.
|
||||
if k.code == KeyCode::Char('c') && k.modifiers.contains(KeyModifiers::ALT) {
|
||||
// `CT_DEBUG_KEYS=1`: surface every key event in the status bar,
|
||||
// for diagnosing what the outer terminal actually delivers.
|
||||
if std::env::var_os("CT_DEBUG_KEYS").is_some() {
|
||||
app.lock().unwrap().status =
|
||||
format!("key: {:?} mods={:?} kind={:?}", k.code, k.modifiers, k.kind);
|
||||
}
|
||||
// Pane controls, available in every state (alt-keys are out:
|
||||
// they compose characters on some keyboard layouts):
|
||||
// F2 show/hide the claude pane
|
||||
// ctrl-↓ focus the claude pane (showing it if hidden)
|
||||
// ctrl-↑ focus the feed
|
||||
let ctrl = k.modifiers.contains(KeyModifiers::CONTROL);
|
||||
if k.code == KeyCode::F(2) {
|
||||
if k.kind == KeyEventKind::Press {
|
||||
toggle_embed(eui, &app);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let embed_focused = eui.focused();
|
||||
// Modifier-split routing: while the pane is focused, plain keys
|
||||
// (incl. ctrl-c!) belong to claude; ALT-keys control the feed.
|
||||
// The filter popup is modal and keeps its plain keys.
|
||||
if embed_focused
|
||||
&& !k.modifiers.contains(KeyModifiers::ALT)
|
||||
&& app.lock().unwrap().filter_popup.is_none()
|
||||
{
|
||||
if ctrl && k.code == KeyCode::Down {
|
||||
if k.kind == KeyEventKind::Press {
|
||||
app.lock().unwrap().filter_popup = None;
|
||||
show_embed_pane(eui, &app);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ctrl && k.code == KeyCode::Up {
|
||||
eui.claude_focused = false;
|
||||
continue;
|
||||
}
|
||||
// While the claude pane has focus, everything else belongs to it.
|
||||
if eui.focused() {
|
||||
if let Some(et) = &eui.term {
|
||||
et.key(k);
|
||||
}
|
||||
@@ -103,10 +131,7 @@ fn event_loop(
|
||||
}
|
||||
let mut a = app.lock().unwrap();
|
||||
let nsess = a.sessions.len();
|
||||
if !embed_focused
|
||||
&& k.code == KeyCode::Char('c')
|
||||
&& k.modifiers.contains(KeyModifiers::CONTROL)
|
||||
{
|
||||
if k.code == KeyCode::Char('c') && ctrl {
|
||||
return Ok(());
|
||||
}
|
||||
// Filter popup captures input while open.
|
||||
@@ -324,6 +349,7 @@ fn draw(f: &mut Frame, app: &SharedApp, eui: &mut EmbedUi) {
|
||||
a.follow = new_follow;
|
||||
|
||||
// Embedded claude pane
|
||||
let embed_focused = eui.focused();
|
||||
if show_embed {
|
||||
let et = eui.term.as_mut().unwrap();
|
||||
let exited = et.exited();
|
||||
@@ -333,12 +359,17 @@ fn draw(f: &mut Frame, app: &SharedApp, eui: &mut EmbedUi) {
|
||||
} else {
|
||||
format!(" claude · {id} ")
|
||||
};
|
||||
let block = Block::bordered().title(title);
|
||||
// Bold cyan border = the pane has keyboard focus.
|
||||
let block = if embed_focused {
|
||||
Block::bordered().title(title).border_style(Style::new().cyan().bold())
|
||||
} else {
|
||||
Block::bordered().title(title).border_style(Style::new().dark_gray())
|
||||
};
|
||||
let inner = block.inner(embed_area);
|
||||
f.render_widget(block, embed_area);
|
||||
if exited {
|
||||
f.render_widget(
|
||||
Paragraph::new("\n claude exited — alt-c to close this pane")
|
||||
Paragraph::new("\n claude exited — F2 to close this pane")
|
||||
.dark_gray(),
|
||||
inner,
|
||||
);
|
||||
@@ -350,13 +381,14 @@ fn draw(f: &mut Frame, app: &SharedApp, eui: &mut EmbedUi) {
|
||||
}
|
||||
}
|
||||
|
||||
let embed_focused = eui.focused();
|
||||
let keys = if a.filter_popup.is_some() {
|
||||
"space toggle · j/k move · f/esc close"
|
||||
} else if embed_focused {
|
||||
"alt-c hide claude · alt-q quit · alt-j/k scroll · alt-f filter · alt-n/p session"
|
||||
"ctrl-↑ feed · F2 hide claude"
|
||||
} else if show_embed {
|
||||
"ctrl-↓ claude · q quit · tab session · j/k scroll · f filter · F2 hide"
|
||||
} else {
|
||||
"q quit · tab session · s sessions · j/k scroll · f filter · g top · G bottom · alt-c claude"
|
||||
"q quit · tab session · s sessions · j/k scroll · f filter · g top · G bottom · ctrl-↓/F2 claude"
|
||||
};
|
||||
f.render_widget(
|
||||
Paragraph::new(Line::from(format!(" {} | {keys}", a.status)).dark_gray()),
|
||||
|
||||
Reference in New Issue
Block a user