use crossterm::event::{Event, KeyCode, KeyModifiers};
use std::process::{Command, Stdio};
use crate::state::{AppState, InputMode};
#[derive(Debug, PartialEq, Eq)]
pub enum Action {
Continue,
Quit,
}
pub fn handle_key_event(event: &Event, state: &mut AppState) -> Action {
match event {
Event::Key(key) => {
if state.show_help {
return handle_help_mode(key.code, key.modifiers, state);
}
match state.input_mode {
InputMode::Normal => handle_normal_mode(key.code, key.modifiers, state),
InputMode::Input => handle_input_mode(key.code, key.modifiers, state),
InputMode::Title => handle_title_mode(key.code, key.modifiers, state),
InputMode::Broadcast => handle_broadcast_mode(key.code, key.modifiers, state),
InputMode::Scroll => handle_scroll_mode(key.code, key.modifiers, state),
}
}
Event::Paste(text) => handle_paste_event(text, state),
_ => Action::Continue,
}
}
fn handle_normal_mode(code: KeyCode, modifiers: KeyModifiers, state: &mut AppState) -> Action {
if code != KeyCode::Esc {
state.esc_source_mode = None;
}
if state.pending_g {
state.pending_g = false;
return match code {
KeyCode::Char('g') => {
let max = state.preview_height.saturating_mul(3);
state.preview_scroll = max;
if state.preview_scroll > 0 {
state.input_mode = InputMode::Scroll;
}
Action::Continue
}
_ => handle_normal_mode(code, modifiers, state),
};
}
if modifiers.contains(KeyModifiers::CONTROL) {
return match code {
KeyCode::Char('u') => {
let half = state.preview_height / 2;
let max = state.preview_height.saturating_mul(3);
state.scroll_preview_up(half, max);
if state.preview_scroll > 0 {
state.input_mode = InputMode::Scroll;
}
Action::Continue
}
KeyCode::Char('d') => {
let half = state.preview_height / 2;
state.scroll_preview_down(half);
Action::Continue
}
_ => Action::Continue,
};
}
match code {
KeyCode::Esc => {
match state.esc_source_mode.take() {
Some(InputMode::Input) => {
if let Some(pane_id) = state.selected_pane_id() {
run_send_keys(pane_id, &["Escape"]);
run_send_keys(pane_id, &["Escape"]);
}
state.input_mode = InputMode::Input;
}
Some(InputMode::Broadcast) => {
for pane_id in state.marked_pane_ids() {
run_send_keys(&pane_id, &["Escape"]);
run_send_keys(&pane_id, &["Escape"]);
}
state.input_mode = InputMode::Broadcast;
}
_ => {}
}
Action::Continue
}
KeyCode::Char('q') => Action::Quit,
KeyCode::Char('j') | KeyCode::Down => {
state.select_next();
Action::Continue
}
KeyCode::Char('k') | KeyCode::Up => {
state.select_prev();
Action::Continue
}
KeyCode::Char(' ') => {
state.toggle_mark();
Action::Continue
}
KeyCode::Char('i') => {
if state.selected_pane_id().is_some() {
state.input_mode = InputMode::Input;
state.reset_preview_scroll();
}
Action::Continue
}
KeyCode::Char('I') => {
if !state.marked_pane_ids().is_empty() {
state.input_mode = InputMode::Broadcast;
state.reset_preview_scroll();
}
Action::Continue
}
KeyCode::Char('e') => {
if let Some(session) = state.selected_session() {
state.input_buffer = session.title.clone().unwrap_or_default();
state.input_mode = InputMode::Title;
}
Action::Continue
}
KeyCode::Char('s') => {
if let Some(pane_id) = state.selected_pane_id() {
tmux_claude_state::tmux::switch_to_pane(pane_id);
}
Action::Continue
}
KeyCode::Char('G') => {
state.reset_preview_scroll();
Action::Continue
}
KeyCode::Char('g') => {
state.pending_g = true;
Action::Continue
}
KeyCode::Char('o') => {
state.claudeye_visible = !state.claudeye_visible;
Action::Continue
}
KeyCode::Char('?') => {
state.show_help = true;
Action::Continue
}
_ => Action::Continue,
}
}
fn handle_input_mode(code: KeyCode, modifiers: KeyModifiers, state: &mut AppState) -> Action {
if code == KeyCode::Esc {
state.esc_source_mode = Some(InputMode::Input);
state.input_mode = InputMode::Normal;
} else {
send_key_to_pane(code, modifiers, state);
}
Action::Continue
}
fn handle_broadcast_mode(code: KeyCode, modifiers: KeyModifiers, state: &mut AppState) -> Action {
if code == KeyCode::Esc {
state.esc_source_mode = Some(InputMode::Broadcast);
state.input_mode = InputMode::Normal;
} else {
send_key_to_marked_panes(code, modifiers, state);
}
Action::Continue
}
fn help_line_count() -> u16 {
crate::ui::HELP_TEXT.lines().count() as u16
}
fn handle_help_mode(code: KeyCode, modifiers: KeyModifiers, state: &mut AppState) -> Action {
if modifiers.contains(KeyModifiers::CONTROL) {
return match code {
KeyCode::Char('u') => {
state.help_scroll = state.help_scroll.saturating_sub(10);
Action::Continue
}
KeyCode::Char('d') => {
let max = help_line_count();
state.help_scroll = state.help_scroll.saturating_add(10).min(max);
Action::Continue
}
_ => Action::Continue,
};
}
match code {
KeyCode::Char('?') | KeyCode::Esc => {
state.show_help = false;
state.help_scroll = 0;
Action::Continue
}
KeyCode::Char('j') | KeyCode::Down => {
let max = help_line_count();
state.help_scroll = state.help_scroll.saturating_add(1).min(max);
Action::Continue
}
KeyCode::Char('k') | KeyCode::Up => {
state.help_scroll = state.help_scroll.saturating_sub(1);
Action::Continue
}
KeyCode::Char('g') => {
state.help_scroll = 0;
Action::Continue
}
KeyCode::Char('G') => {
state.help_scroll = help_line_count();
Action::Continue
}
_ => Action::Continue,
}
}
fn handle_scroll_mode(code: KeyCode, modifiers: KeyModifiers, state: &mut AppState) -> Action {
if state.pending_g {
state.pending_g = false;
return match code {
KeyCode::Char('g') => {
let max = state.preview_height.saturating_mul(3);
state.preview_scroll = max;
Action::Continue
}
_ => handle_scroll_mode(code, modifiers, state),
};
}
if modifiers.contains(KeyModifiers::CONTROL) {
return match code {
KeyCode::Char('u') => {
let half = state.preview_height / 2;
let max = state.preview_height.saturating_mul(3);
state.scroll_preview_up(half, max);
Action::Continue
}
KeyCode::Char('d') => {
let half = state.preview_height / 2;
state.scroll_preview_down(half);
if state.preview_scroll == 0 {
state.input_mode = InputMode::Normal;
}
Action::Continue
}
_ => Action::Continue,
};
}
match code {
KeyCode::Char('j') | KeyCode::Down => {
state.scroll_preview_down(1);
if state.preview_scroll == 0 {
state.input_mode = InputMode::Normal;
}
Action::Continue
}
KeyCode::Char('k') | KeyCode::Up => {
let max = state.preview_height.saturating_mul(3);
state.scroll_preview_up(1, max);
Action::Continue
}
#[allow(clippy::match_same_arms)]
KeyCode::Char('G') => {
state.reset_preview_scroll();
state.input_mode = InputMode::Normal;
Action::Continue
}
KeyCode::Char('g') => {
state.pending_g = true;
Action::Continue
}
KeyCode::Esc => {
state.reset_preview_scroll();
state.input_mode = InputMode::Normal;
Action::Continue
}
KeyCode::Char('i') => {
if state.selected_pane_id().is_some() {
state.reset_preview_scroll();
state.input_mode = InputMode::Input;
}
Action::Continue
}
KeyCode::Char('I') => {
if !state.marked_pane_ids().is_empty() {
state.reset_preview_scroll();
state.input_mode = InputMode::Broadcast;
}
Action::Continue
}
_ => Action::Continue,
}
}
fn handle_title_mode(code: KeyCode, _modifiers: KeyModifiers, state: &mut AppState) -> Action {
match code {
KeyCode::Esc => {
save_title(state);
Action::Continue
}
KeyCode::Backspace => {
state.input_buffer.pop();
Action::Continue
}
KeyCode::Char(c) => {
state.input_buffer.push(c);
Action::Continue
}
_ => Action::Continue,
}
}
fn save_title(state: &mut AppState) {
let trimmed = state.input_buffer.trim().to_string();
let title = if trimmed.is_empty() {
None
} else {
Some(trimmed)
};
if let Some(session) = state.selected_session_mut() {
session.title = title;
}
state.input_buffer.clear();
state.input_mode = InputMode::Normal;
}
fn handle_paste_event(text: &str, state: &AppState) -> Action {
if state.show_help {
return Action::Continue;
}
match state.input_mode {
InputMode::Input => {
if let Some(pane_id) = state.selected_pane_id() {
send_paste_to_panes(&[pane_id], text);
}
}
InputMode::Broadcast => {
let pane_ids = state.marked_pane_ids();
let refs: Vec<&str> = pane_ids.iter().map(|s| s.as_str()).collect();
send_paste_to_panes(&refs, text);
}
InputMode::Normal | InputMode::Title | InputMode::Scroll => {}
}
Action::Continue
}
fn send_paste_to_panes(pane_ids: &[&str], text: &str) {
if pane_ids.is_empty() {
return;
}
run_tmux(&["set-buffer", "-b", "crmux-paste", "--", text]);
for pane_id in pane_ids {
run_tmux(&["paste-buffer", "-b", "crmux-paste", "-t", pane_id, "-p"]);
}
run_tmux(&["delete-buffer", "-b", "crmux-paste"]);
}
fn send_encoded_key(pane_id: &str, code: KeyCode, modifiers: KeyModifiers) {
if modifiers.contains(KeyModifiers::CONTROL) {
if let KeyCode::Char(c) = code {
let key_name = format!("C-{c}");
run_send_keys(pane_id, &[&key_name]);
}
return;
}
match code {
KeyCode::Char(c) => {
let s = c.to_string();
run_send_keys(pane_id, &["-l", &s]);
}
_ => {
if let Some(key_name) = keycode_to_tmux_name(code) {
run_send_keys(pane_id, &[key_name]);
}
}
}
}
fn send_key_to_pane(code: KeyCode, modifiers: KeyModifiers, state: &AppState) {
let Some(pane_id) = state.selected_pane_id() else {
return;
};
send_encoded_key(pane_id, code, modifiers);
}
fn send_key_to_marked_panes(code: KeyCode, modifiers: KeyModifiers, state: &AppState) {
for pane_id in state.marked_pane_ids() {
send_encoded_key(&pane_id, code, modifiers);
}
}
fn keycode_to_tmux_name(code: KeyCode) -> Option<&'static str> {
match code {
KeyCode::Enter => Some("Enter"),
KeyCode::Backspace => Some("BSpace"),
KeyCode::Tab => Some("Tab"),
KeyCode::Left => Some("Left"),
KeyCode::Right => Some("Right"),
KeyCode::Up => Some("Up"),
KeyCode::Down => Some("Down"),
KeyCode::Home => Some("Home"),
KeyCode::End => Some("End"),
KeyCode::PageUp => Some("PageUp"),
KeyCode::PageDown => Some("PageDown"),
KeyCode::BackTab => Some("BTab"),
KeyCode::Delete => Some("DC"),
KeyCode::Esc => Some("Escape"),
KeyCode::Insert => Some("IC"),
_ => None,
}
}
fn run_tmux(args: &[&str]) {
let _ = Command::new("tmux")
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.output();
}
fn run_send_keys(pane_id: &str, extra_args: &[&str]) {
let mut args = vec!["send-keys", "-t", pane_id];
args.extend(extra_args);
run_tmux(&args);
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyEvent, KeyModifiers};
use std::time::Instant;
use tmux_claude_state::claude_state::ClaudeState;
fn make_key_event(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
fn make_state_with_session() -> AppState {
use crate::state::ManagedSession;
let mut state = AppState::new(None);
state.sessions.push(ManagedSession {
pid: 100,
pane_id: "%1".to_string(),
project_name: "test-project".to_string(),
state: ClaudeState::Idle,
state_changed_at: Instant::now(),
marked: false,
title: None,
session_id: None,
model: None,
context_percent: None,
cwd: "/home/user/test-project".to_string(),
git_branch: None,
auto_title: None,
});
state
}
#[test]
fn test_quit_on_q() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char('q')), &mut state);
assert_eq!(action, Action::Quit);
}
#[test]
fn test_esc_continues_in_normal_mode() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_navigate_j() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char('j')), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_navigate_k() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char('k')), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_navigate_down_arrow() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Down), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_navigate_up_arrow() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Up), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_s_continues() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char('s')), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_other_key_continues() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char('x')), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_space_toggles_mark() {
let mut state = make_state_with_session();
assert!(!state.sessions[0].marked);
let action = handle_key_event(&make_key_event(KeyCode::Char(' ')), &mut state);
assert_eq!(action, Action::Continue);
assert!(state.sessions[0].marked);
}
#[test]
fn test_space_unmarks_marked_session() {
let mut state = make_state_with_session();
state.sessions[0].marked = true;
handle_key_event(&make_key_event(KeyCode::Char(' ')), &mut state);
assert!(!state.sessions[0].marked);
}
#[test]
fn test_space_on_empty_sessions() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char(' ')), &mut state);
assert_eq!(action, Action::Continue);
}
#[test]
fn test_i_enters_input_mode_with_session() {
let mut state = make_state_with_session();
let action = handle_key_event(&make_key_event(KeyCode::Char('i')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Input);
}
#[test]
fn test_i_resets_scroll_on_enter_input_mode() {
let mut state = make_state_with_session();
state.preview_scroll = 42;
handle_key_event(&make_key_event(KeyCode::Char('i')), &mut state);
assert_eq!(state.preview_scroll, 0);
}
#[test]
fn test_i_does_nothing_without_session() {
let mut state = AppState::new(None);
handle_key_event(&make_key_event(KeyCode::Char('i')), &mut state);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_input_mode_esc_returns_to_normal() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Input;
let action = handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_input_mode_q_does_not_quit() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Input;
let action = handle_key_event(&make_key_event(KeyCode::Char('q')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Input);
}
#[test]
fn test_input_mode_keys_do_not_modify_buffer() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Input;
handle_key_event(&make_key_event(KeyCode::Char('h')), &mut state);
handle_key_event(&make_key_event(KeyCode::Char('i')), &mut state);
assert!(state.input_buffer.is_empty());
}
#[test]
fn test_input_mode_enter_does_not_modify_buffer() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Input;
let action = handle_key_event(&make_key_event(KeyCode::Enter), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Input);
assert!(state.input_buffer.is_empty());
}
#[test]
fn test_input_mode_special_keys_stay_in_input() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Input;
for key in [KeyCode::Backspace, KeyCode::Tab, KeyCode::BackTab, KeyCode::Left, KeyCode::Right] {
let action = handle_key_event(&make_key_event(key), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Input);
}
}
#[test]
fn test_e_enters_title_mode() {
let mut state = make_state_with_session();
let action = handle_key_event(&make_key_event(KeyCode::Char('e')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Title);
}
#[test]
fn test_e_does_nothing_without_session() {
let mut state = AppState::new(None);
handle_key_event(&make_key_event(KeyCode::Char('e')), &mut state);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_e_prefills_buffer() {
let mut state = make_state_with_session();
state.sessions[0].title = Some("existing title".to_string());
handle_key_event(&make_key_event(KeyCode::Char('e')), &mut state);
assert_eq!(state.input_buffer, "existing title");
}
#[test]
fn test_e_clears_buffer_when_no_title() {
let mut state = make_state_with_session();
state.input_buffer = "leftover".to_string();
handle_key_event(&make_key_event(KeyCode::Char('e')), &mut state);
assert!(state.input_buffer.is_empty());
}
#[test]
fn test_title_esc_saves_and_exits() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Title;
state.input_buffer = "new title".to_string();
let action = handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Normal);
assert!(state.input_buffer.is_empty());
assert_eq!(state.sessions[0].title, Some("new title".to_string()));
}
#[test]
fn test_title_esc_empty_stores_none() {
let mut state = make_state_with_session();
state.sessions[0].title = Some("old".to_string());
state.input_mode = InputMode::Title;
state.input_buffer.clear();
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(state.sessions[0].title, None);
}
#[test]
fn test_title_esc_whitespace_only_stores_none() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Title;
state.input_buffer = " \t ".to_string();
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(state.sessions[0].title, None);
}
#[test]
fn test_title_enter_stays_in_title_mode() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Title;
state.input_buffer = "abc".to_string();
let action = handle_key_event(&make_key_event(KeyCode::Enter), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Title);
}
#[test]
fn test_title_char_appended() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Title;
handle_key_event(&make_key_event(KeyCode::Char('a')), &mut state);
handle_key_event(&make_key_event(KeyCode::Char('b')), &mut state);
assert_eq!(state.input_buffer, "ab");
}
#[test]
fn test_title_backspace() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Title;
state.input_buffer = "abc".to_string();
handle_key_event(&make_key_event(KeyCode::Backspace), &mut state);
assert_eq!(state.input_buffer, "ab");
}
#[test]
fn test_question_mark_opens_help() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char('?')), &mut state);
assert_eq!(action, Action::Continue);
assert!(state.show_help);
}
#[test]
fn test_question_mark_closes_help() {
let mut state = AppState::new(None);
state.show_help = true;
let action = handle_key_event(&make_key_event(KeyCode::Char('?')), &mut state);
assert_eq!(action, Action::Continue);
assert!(!state.show_help);
}
#[test]
fn test_esc_closes_help() {
let mut state = AppState::new(None);
state.show_help = true;
let action = handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(action, Action::Continue);
assert!(!state.show_help);
}
#[test]
fn test_q_does_not_quit_during_help() {
let mut state = AppState::new(None);
state.show_help = true;
let action = handle_key_event(&make_key_event(KeyCode::Char('q')), &mut state);
assert_eq!(action, Action::Continue);
assert!(state.show_help);
}
#[test]
fn test_help_j_scrolls_down() {
let mut state = AppState::new(None);
state.show_help = true;
handle_key_event(&make_key_event(KeyCode::Char('j')), &mut state);
assert_eq!(state.help_scroll, 1);
assert!(state.show_help);
}
#[test]
fn test_help_k_scrolls_up() {
let mut state = AppState::new(None);
state.show_help = true;
state.help_scroll = 5;
handle_key_event(&make_key_event(KeyCode::Char('k')), &mut state);
assert_eq!(state.help_scroll, 4);
}
#[test]
fn test_help_k_clamps_to_zero() {
let mut state = AppState::new(None);
state.show_help = true;
state.help_scroll = 0;
handle_key_event(&make_key_event(KeyCode::Char('k')), &mut state);
assert_eq!(state.help_scroll, 0);
}
#[test]
fn test_help_esc_resets_scroll() {
let mut state = AppState::new(None);
state.show_help = true;
state.help_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert!(!state.show_help);
assert_eq!(state.help_scroll, 0);
}
#[test]
fn test_help_shift_g_scrolls_to_bottom() {
let mut state = AppState::new(None);
state.show_help = true;
handle_key_event(&make_key_event(KeyCode::Char('G')), &mut state);
assert!(state.help_scroll > 0);
assert!(state.show_help);
}
#[test]
fn test_help_g_scrolls_to_top() {
let mut state = AppState::new(None);
state.show_help = true;
state.help_scroll = 20;
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
assert_eq!(state.help_scroll, 0);
}
#[test]
fn test_help_ctrl_d_scrolls_down() {
let mut state = AppState::new(None);
state.show_help = true;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('d')), &mut state);
assert_eq!(state.help_scroll, 10);
}
#[test]
fn test_help_ctrl_u_scrolls_up() {
let mut state = AppState::new(None);
state.show_help = true;
state.help_scroll = 15;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('u')), &mut state);
assert_eq!(state.help_scroll, 5);
}
fn make_state_with_marked_sessions() -> AppState {
use crate::state::ManagedSession;
let mut state = AppState::new(None);
state.sessions.push(ManagedSession {
pid: 100,
pane_id: "%1".to_string(),
project_name: "project-a".to_string(),
state: ClaudeState::Idle,
state_changed_at: Instant::now(),
marked: true,
title: None,
session_id: None,
model: None,
context_percent: None,
cwd: "/home/user/project-a".to_string(),
git_branch: None,
auto_title: None,
});
state.sessions.push(ManagedSession {
pid: 200,
pane_id: "%2".to_string(),
project_name: "project-b".to_string(),
state: ClaudeState::Idle,
state_changed_at: Instant::now(),
marked: false,
title: None,
session_id: None,
model: None,
context_percent: None,
cwd: "/home/user/project-b".to_string(),
git_branch: None,
auto_title: None,
});
state.sessions.push(ManagedSession {
pid: 300,
pane_id: "%3".to_string(),
project_name: "project-c".to_string(),
state: ClaudeState::Idle,
state_changed_at: Instant::now(),
marked: true,
title: None,
session_id: None,
model: None,
context_percent: None,
cwd: "/home/user/project-c".to_string(),
git_branch: None,
auto_title: None,
});
state
}
#[test]
fn test_shift_i_enters_broadcast_mode_with_marked_sessions() {
let mut state = make_state_with_marked_sessions();
let action = handle_key_event(&make_key_event(KeyCode::Char('I')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Broadcast);
}
#[test]
fn test_shift_i_resets_scroll_on_enter_broadcast_mode() {
let mut state = make_state_with_marked_sessions();
state.preview_scroll = 42;
handle_key_event(&make_key_event(KeyCode::Char('I')), &mut state);
assert_eq!(state.preview_scroll, 0);
}
#[test]
fn test_shift_i_does_nothing_without_marked_sessions() {
let mut state = make_state_with_session();
let action = handle_key_event(&make_key_event(KeyCode::Char('I')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_shift_i_does_nothing_without_sessions() {
let mut state = AppState::new(None);
let action = handle_key_event(&make_key_event(KeyCode::Char('I')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_broadcast_mode_esc_returns_to_normal() {
let mut state = make_state_with_marked_sessions();
state.input_mode = InputMode::Broadcast;
let action = handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_broadcast_mode_q_does_not_quit() {
let mut state = make_state_with_marked_sessions();
state.input_mode = InputMode::Broadcast;
let action = handle_key_event(&make_key_event(KeyCode::Char('q')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Broadcast);
}
#[test]
fn test_broadcast_mode_keys_do_not_modify_buffer() {
let mut state = make_state_with_marked_sessions();
state.input_mode = InputMode::Broadcast;
handle_key_event(&make_key_event(KeyCode::Char('h')), &mut state);
handle_key_event(&make_key_event(KeyCode::Char('i')), &mut state);
assert!(state.input_buffer.is_empty());
}
fn make_ctrl_key_event(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::CONTROL))
}
#[test]
fn test_ctrl_u_scrolls_up() {
let mut state = make_state_with_session();
state.preview_height = 30;
let action = handle_key_event(&make_ctrl_key_event(KeyCode::Char('u')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.preview_scroll, 15); }
#[test]
fn test_ctrl_d_scrolls_down() {
let mut state = make_state_with_session();
state.preview_height = 30;
state.preview_scroll = 20;
let action = handle_key_event(&make_ctrl_key_event(KeyCode::Char('d')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.preview_scroll, 5); }
#[test]
fn test_shift_g_resets_scroll() {
let mut state = make_state_with_session();
state.preview_scroll = 42;
let action = handle_key_event(&make_key_event(KeyCode::Char('G')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.preview_scroll, 0);
}
#[test]
fn test_ctrl_u_clamps_to_max() {
let mut state = make_state_with_session();
state.preview_height = 30;
for _ in 0..10 {
handle_key_event(&make_ctrl_key_event(KeyCode::Char('u')), &mut state);
}
assert_eq!(state.preview_scroll, 90); }
#[test]
fn test_ctrl_d_clamps_to_zero() {
let mut state = make_state_with_session();
state.preview_height = 30;
state.preview_scroll = 5;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('d')), &mut state);
assert_eq!(state.preview_scroll, 0); }
#[test]
fn test_g_sets_pending_g() {
let mut state = make_state_with_session();
let action = handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
assert_eq!(action, Action::Continue);
assert!(state.pending_g);
}
#[test]
fn test_gg_scrolls_to_top() {
let mut state = make_state_with_session();
state.preview_height = 30;
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
let action = handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.preview_scroll, 90); assert!(!state.pending_g);
}
#[test]
fn test_g_then_other_key_cancels_pending() {
let mut state = make_state_with_session();
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
assert!(state.pending_g);
let action = handle_key_event(&make_key_event(KeyCode::Char('j')), &mut state);
assert_eq!(action, Action::Continue);
assert!(!state.pending_g);
}
fn make_paste_event(text: &str) -> Event {
Event::Paste(text.to_string())
}
#[test]
fn test_paste_ignored_in_normal_mode() {
let mut state = make_state_with_session();
let action = handle_key_event(&make_paste_event("hello\nworld"), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Normal);
assert!(state.input_buffer.is_empty());
}
#[test]
fn test_paste_ignored_in_title_mode() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Title;
state.input_buffer = "existing".to_string();
let action = handle_key_event(&make_paste_event("pasted text"), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Title);
assert_eq!(state.input_buffer, "existing");
}
#[test]
fn test_paste_ignored_during_help() {
let mut state = make_state_with_session();
state.show_help = true;
let action = handle_key_event(&make_paste_event("pasted text"), &mut state);
assert_eq!(action, Action::Continue);
assert!(state.show_help);
}
#[test]
fn test_paste_in_input_mode_does_not_change_state() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Input;
let action = handle_key_event(&make_paste_event("hello\nworld"), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Input);
assert!(state.input_buffer.is_empty());
}
#[test]
fn test_paste_in_broadcast_mode_does_not_change_state() {
let mut state = make_state_with_marked_sessions();
state.input_mode = InputMode::Broadcast;
let action = handle_key_event(&make_paste_event("hello\nworld"), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Broadcast);
assert!(state.input_buffer.is_empty());
}
#[test]
fn test_paste_ignored_in_scroll_mode() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
let action = handle_key_event(&make_paste_event("hello"), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_ctrl_u_enters_scroll_mode() {
let mut state = make_state_with_session();
state.preview_height = 30;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('u')), &mut state);
assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_gg_enters_scroll_mode() {
let mut state = make_state_with_session();
state.preview_height = 30;
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_ctrl_d_does_not_enter_scroll_mode_at_bottom() {
let mut state = make_state_with_session();
state.preview_height = 30;
state.preview_scroll = 0;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('d')), &mut state);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_scroll_mode_j_scrolls_down() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Char('j')), &mut state);
assert_eq!(state.preview_scroll, 9);
assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_scroll_mode_k_scrolls_up() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
state.preview_height = 30;
handle_key_event(&make_key_event(KeyCode::Char('k')), &mut state);
assert_eq!(state.preview_scroll, 11);
assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_scroll_mode_down_arrow_scrolls_down() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 5;
handle_key_event(&make_key_event(KeyCode::Down), &mut state);
assert_eq!(state.preview_scroll, 4);
}
#[test]
fn test_scroll_mode_up_arrow_scrolls_up() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 5;
state.preview_height = 30;
handle_key_event(&make_key_event(KeyCode::Up), &mut state);
assert_eq!(state.preview_scroll, 6);
}
#[test]
fn test_scroll_mode_j_to_zero_exits_scroll() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 1;
handle_key_event(&make_key_event(KeyCode::Char('j')), &mut state);
assert_eq!(state.preview_scroll, 0);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_scroll_mode_ctrl_u_scrolls_up() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
state.preview_height = 30;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('u')), &mut state);
assert_eq!(state.preview_scroll, 25); assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_scroll_mode_ctrl_d_scrolls_down() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 20;
state.preview_height = 30;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('d')), &mut state);
assert_eq!(state.preview_scroll, 5); assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_scroll_mode_ctrl_d_to_zero_exits_scroll() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 5;
state.preview_height = 30;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('d')), &mut state);
assert_eq!(state.preview_scroll, 0);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_scroll_mode_shift_g_exits_scroll() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 42;
handle_key_event(&make_key_event(KeyCode::Char('G')), &mut state);
assert_eq!(state.preview_scroll, 0);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_scroll_mode_esc_resets_and_exits() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 42;
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(state.preview_scroll, 0);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_scroll_mode_i_enters_input_mode() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Char('i')), &mut state);
assert_eq!(state.input_mode, InputMode::Input);
assert_eq!(state.preview_scroll, 0);
}
#[test]
fn test_scroll_mode_shift_i_enters_broadcast_mode() {
let mut state = make_state_with_marked_sessions();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Char('I')), &mut state);
assert_eq!(state.input_mode, InputMode::Broadcast);
assert_eq!(state.preview_scroll, 0);
}
#[test]
fn test_scroll_mode_shift_i_does_nothing_without_marks() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Char('I')), &mut state);
assert_eq!(state.input_mode, InputMode::Scroll);
assert_eq!(state.preview_scroll, 10);
}
#[test]
fn test_scroll_mode_gg_scrolls_to_top() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_height = 30;
state.preview_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
assert_eq!(state.preview_scroll, 90); assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_scroll_mode_q_does_not_quit() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
let action = handle_key_event(&make_key_event(KeyCode::Char('q')), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_scroll_mode_g_then_other_key_cancels_pending() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Char('g')), &mut state);
assert!(state.pending_g);
handle_key_event(&make_key_event(KeyCode::Char('j')), &mut state);
assert!(!state.pending_g);
assert_eq!(state.preview_scroll, 9);
}
#[test]
fn test_scroll_mode_i_does_nothing_without_session() {
let mut state = AppState::new(None);
state.input_mode = InputMode::Scroll;
state.preview_scroll = 10;
handle_key_event(&make_key_event(KeyCode::Char('i')), &mut state);
assert_eq!(state.input_mode, InputMode::Scroll);
}
#[test]
fn test_input_mode_esc_sets_esc_source_mode() {
let mut state = make_state_with_session();
state.input_mode = InputMode::Input;
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(state.input_mode, InputMode::Normal);
assert_eq!(state.esc_source_mode, Some(InputMode::Input));
}
#[test]
fn test_broadcast_mode_esc_sets_esc_source_mode() {
let mut state = make_state_with_marked_sessions();
state.input_mode = InputMode::Broadcast;
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(state.input_mode, InputMode::Normal);
assert_eq!(state.esc_source_mode, Some(InputMode::Broadcast));
}
#[test]
fn test_normal_esc_with_input_source_returns_to_input() {
let mut state = make_state_with_session();
state.esc_source_mode = Some(InputMode::Input);
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(state.input_mode, InputMode::Input);
assert_eq!(state.esc_source_mode, None);
}
#[test]
fn test_normal_esc_with_broadcast_source_returns_to_broadcast() {
let mut state = make_state_with_marked_sessions();
state.esc_source_mode = Some(InputMode::Broadcast);
handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(state.input_mode, InputMode::Broadcast);
assert_eq!(state.esc_source_mode, None);
}
#[test]
fn test_normal_esc_without_source_does_nothing() {
let mut state = make_state_with_session();
state.esc_source_mode = None;
let action = handle_key_event(&make_key_event(KeyCode::Esc), &mut state);
assert_eq!(action, Action::Continue);
assert_eq!(state.input_mode, InputMode::Normal);
assert_eq!(state.esc_source_mode, None);
}
#[test]
fn test_normal_other_key_clears_esc_source_mode() {
let mut state = make_state_with_session();
state.esc_source_mode = Some(InputMode::Input);
handle_key_event(&make_key_event(KeyCode::Char('j')), &mut state);
assert_eq!(state.esc_source_mode, None);
assert_eq!(state.input_mode, InputMode::Normal);
}
#[test]
fn test_esc_source_mode_cleared_on_ctrl_key() {
let mut state = make_state_with_session();
state.esc_source_mode = Some(InputMode::Input);
state.preview_height = 30;
handle_key_event(&make_ctrl_key_event(KeyCode::Char('u')), &mut state);
assert_eq!(state.esc_source_mode, None);
}
#[test]
fn test_o_toggles_claudeye_visible() {
let mut state = AppState::new(None);
assert!(!state.claudeye_visible);
handle_key_event(&make_key_event(KeyCode::Char('o')), &mut state);
assert!(state.claudeye_visible);
handle_key_event(&make_key_event(KeyCode::Char('o')), &mut state);
assert!(!state.claudeye_visible);
}
}