@ -175,9 +175,30 @@ fn render_filter_bar(f: &mut Frame, area: Rect, state: &AppState) {
// Build the content line: label + input text (+ cursor when focused).
// Build the content line: label + input text (+ cursor when focused).
let label = Span ::styled ( "Filter: " , Style ::default ( ) . add_modifier ( Modifier ::BOLD ) ) ;
let label = Span ::styled ( "Filter: " , Style ::default ( ) . add_modifier ( Modifier ::BOLD ) ) ;
let input_text = if focused {
let input_text = if focused {
// Show a block cursor at end of input.
// Split the input at the cursor position and insert a block cursor glyph.
let mut spans = vec! [ label , Span ::raw ( state . filter_input . clone ( ) ) ] ;
let cursor = state . filter_cursor . min ( state . filter_input . len ( ) ) ;
spans . push ( Span ::styled ( "█" , Style ::default ( ) . fg ( Color ::Yellow ) ) ) ;
let before = & state . filter_input [ .. cursor ] ;
let after = & state . filter_input [ cursor .. ] ;
// The cursor glyph replaces the character under it (or is appended when at end).
let ( cursor_char , after_rest ) = if after . is_empty ( ) {
( "█" , "" )
} else {
let first_char_end = after
. char_indices ( )
. nth ( 1 )
. map ( | ( i , _ ) | i )
. unwrap_or ( after . len ( ) ) ;
( & after [ .. first_char_end ] , & after [ first_char_end .. ] )
} ;
let spans = vec! [
label ,
Span ::raw ( before . to_string ( ) ) ,
Span ::styled (
cursor_char ,
Style ::default ( ) . fg ( Color ::Black ) . bg ( Color ::Yellow ) ,
) ,
Span ::raw ( after_rest . to_string ( ) ) ,
] ;
Line ::from ( spans )
Line ::from ( spans )
} else if state . filter_input . is_empty ( ) & & state . filter_active . is_empty ( ) {
} else if state . filter_input . is_empty ( ) & & state . filter_active . is_empty ( ) {
Line ::from ( vec! [
Line ::from ( vec! [
@ -262,11 +283,13 @@ pub fn handle_session_list_event(event: Event, state: &mut AppState) -> bool {
// Focus the filter input directly.
// Focus the filter input directly.
KeyCode ::Char ( 't' ) | KeyCode ::Char ( '/' ) = > {
KeyCode ::Char ( 't' ) | KeyCode ::Char ( '/' ) = > {
state . focus = Focus ::FilterInput ;
state . focus = Focus ::FilterInput ;
state . filter_cursor = state . filter_input . len ( ) ;
true
true
}
}
// Tab cycles focus: list → filter → list.
// Tab cycles focus: list → filter → list.
KeyCode ::Tab = > {
KeyCode ::Tab = > {
state . focus = Focus ::FilterInput ;
state . focus = Focus ::FilterInput ;
state . filter_cursor = state . filter_input . len ( ) ;
true
true
}
}
// Quit — show confirmation dialog rather than exiting immediately.
// Quit — show confirmation dialog rather than exiting immediately.
@ -288,18 +311,66 @@ pub fn handle_session_list_event(event: Event, state: &mut AppState) -> bool {
/// Returns `true` when the event is consumed.
/// Returns `true` when the event is consumed.
fn handle_filter_input_event ( code : KeyCode , state : & mut AppState ) -> bool {
fn handle_filter_input_event ( code : KeyCode , state : & mut AppState ) -> bool {
match code {
match code {
// Character input: append to filter_input and reset history browsing.
// Character input: insert at cursor position and reset history browsing.
KeyCode ::Char ( c ) = > {
KeyCode ::Char ( c ) = > {
state . filter_input . push ( c ) ;
let pos = state . filter_cursor ;
state . filter_input . insert ( pos , c ) ;
state . filter_cursor + = c . len_utf8 ( ) ;
state . filter_history_pos = None ;
state . filter_history_pos = None ;
true
true
}
}
// Backspace: remove las t char.
// Backspace: remove the character *before* the curso r.
KeyCode ::Backspace = > {
KeyCode ::Backspace = > {
state . filter_input . pop ( ) ;
if state . filter_cursor > 0 {
// Find the start of the previous UTF-8 character.
let pos = state . filter_cursor ;
let prev = state . filter_input [ .. pos ]
. char_indices ( )
. next_back ( )
. map ( | ( i , _ ) | i )
. unwrap_or ( 0 ) ;
state . filter_input . remove ( prev ) ;
state . filter_cursor = prev ;
}
state . filter_history_pos = None ;
state . filter_history_pos = None ;
true
true
}
}
// Left arrow: move cursor one character to the left.
KeyCode ::Left = > {
if state . filter_cursor > 0 {
let pos = state . filter_cursor ;
let prev = state . filter_input [ .. pos ]
. char_indices ( )
. next_back ( )
. map ( | ( i , _ ) | i )
. unwrap_or ( 0 ) ;
state . filter_cursor = prev ;
}
true
}
// Right arrow: move cursor one character to the right.
KeyCode ::Right = > {
let pos = state . filter_cursor ;
if pos < state . filter_input . len ( ) {
let next = state . filter_input [ pos .. ]
. char_indices ( )
. nth ( 1 )
. map ( | ( i , _ ) | pos + i )
. unwrap_or ( state . filter_input . len ( ) ) ;
state . filter_cursor = next ;
}
true
}
// Home: jump cursor to start of input.
KeyCode ::Home = > {
state . filter_cursor = 0 ;
true
}
// End: jump cursor to end of input.
KeyCode ::End = > {
state . filter_cursor = state . filter_input . len ( ) ;
true
}
// Enter: apply the current input as the active filter and return focus to the list.
// Enter: apply the current input as the active filter and return focus to the list.
KeyCode ::Enter = > {
KeyCode ::Enter = > {
let query = state . filter_input . trim ( ) . to_string ( ) ;
let query = state . filter_input . trim ( ) . to_string ( ) ;
@ -326,6 +397,7 @@ fn handle_filter_input_event(code: KeyCode, state: &mut AppState) -> bool {
// Escape: clear the text input (but keep the panel visible).
// Escape: clear the text input (but keep the panel visible).
KeyCode ::Esc = > {
KeyCode ::Esc = > {
state . filter_input . clear ( ) ;
state . filter_input . clear ( ) ;
state . filter_cursor = 0 ;
state . filter_history_pos = None ;
state . filter_history_pos = None ;
true
true
}
}
@ -340,6 +412,7 @@ fn handle_filter_input_event(code: KeyCode, state: &mut AppState) -> bool {
} ;
} ;
state . filter_history_pos = Some ( new_pos ) ;
state . filter_history_pos = Some ( new_pos ) ;
state . filter_input = state . filter_history [ new_pos ] . clone ( ) ;
state . filter_input = state . filter_history [ new_pos ] . clone ( ) ;
state . filter_cursor = state . filter_input . len ( ) ;
true
true
}
}
// Down arrow: browse forward through history (or clear when past the end).
// Down arrow: browse forward through history (or clear when past the end).
@ -351,10 +424,12 @@ fn handle_filter_input_event(code: KeyCode, state: &mut AppState) -> bool {
let new_pos = pos + 1 ;
let new_pos = pos + 1 ;
state . filter_history_pos = Some ( new_pos ) ;
state . filter_history_pos = Some ( new_pos ) ;
state . filter_input = state . filter_history [ new_pos ] . clone ( ) ;
state . filter_input = state . filter_history [ new_pos ] . clone ( ) ;
state . filter_cursor = state . filter_input . len ( ) ;
} else {
} else {
// Past the end: clear input and stop browsing history.
// Past the end: clear input and stop browsing history.
state . filter_history_pos = None ;
state . filter_history_pos = None ;
state . filter_input . clear ( ) ;
state . filter_input . clear ( ) ;
state . filter_cursor = 0 ;
}
}
true
true
}
}
@ -642,8 +717,10 @@ mod tests {
let mut state = AppState ::new ( ) ;
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . focus = Focus ::FilterInput ;
state . filter_input = "mo" . to_string ( ) ;
state . filter_input = "mo" . to_string ( ) ;
state . filter_cursor = 2 ; // cursor at end
handle_session_list_event ( press ( KeyCode ::Backspace ) , & mut state ) ;
handle_session_list_event ( press ( KeyCode ::Backspace ) , & mut state ) ;
assert_eq! ( state . filter_input , "m" ) ;
assert_eq! ( state . filter_input , "m" ) ;
assert_eq! ( state . filter_cursor , 1 ) ;
}
}
#[ test ]
#[ test ]
@ -729,4 +806,110 @@ mod tests {
assert_eq! ( state . filter_input , "" ) ;
assert_eq! ( state . filter_input , "" ) ;
assert_eq! ( state . filter_history_pos , None ) ;
assert_eq! ( state . filter_history_pos , None ) ;
}
}
// ── Filter cursor movement ───────────────────────────────────────────────
#[ test ]
fn char_inserts_at_cursor_not_end ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "ac" . to_string ( ) ;
state . filter_cursor = 1 ; // cursor between 'a' and 'c'
handle_session_list_event ( press ( KeyCode ::Char ( 'b' ) ) , & mut state ) ;
assert_eq! ( state . filter_input , "abc" ) ;
assert_eq! ( state . filter_cursor , 2 ) ;
}
#[ test ]
fn backspace_deletes_before_cursor_not_end ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 2 ; // cursor between 'b' and 'c'
handle_session_list_event ( press ( KeyCode ::Backspace ) , & mut state ) ;
assert_eq! ( state . filter_input , "ac" ) ;
assert_eq! ( state . filter_cursor , 1 ) ;
}
#[ test ]
fn backspace_at_start_does_nothing ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 0 ;
handle_session_list_event ( press ( KeyCode ::Backspace ) , & mut state ) ;
assert_eq! ( state . filter_input , "abc" ) ;
assert_eq! ( state . filter_cursor , 0 ) ;
}
#[ test ]
fn left_moves_cursor_back ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 2 ;
handle_session_list_event ( press ( KeyCode ::Left ) , & mut state ) ;
assert_eq! ( state . filter_cursor , 1 ) ;
}
#[ test ]
fn left_clamps_at_zero ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 0 ;
handle_session_list_event ( press ( KeyCode ::Left ) , & mut state ) ;
assert_eq! ( state . filter_cursor , 0 ) ;
}
#[ test ]
fn right_moves_cursor_forward ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 1 ;
handle_session_list_event ( press ( KeyCode ::Right ) , & mut state ) ;
assert_eq! ( state . filter_cursor , 2 ) ;
}
#[ test ]
fn right_clamps_at_end ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 3 ;
handle_session_list_event ( press ( KeyCode ::Right ) , & mut state ) ;
assert_eq! ( state . filter_cursor , 3 ) ;
}
#[ test ]
fn home_jumps_to_start ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 3 ;
handle_session_list_event ( press ( KeyCode ::Home ) , & mut state ) ;
assert_eq! ( state . filter_cursor , 0 ) ;
}
#[ test ]
fn end_jumps_to_end ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 0 ;
handle_session_list_event ( press ( KeyCode ::End ) , & mut state ) ;
assert_eq! ( state . filter_cursor , 3 ) ;
}
#[ test ]
fn esc_resets_cursor_to_zero ( ) {
let mut state = AppState ::new ( ) ;
state . focus = Focus ::FilterInput ;
state . filter_input = "abc" . to_string ( ) ;
state . filter_cursor = 2 ;
handle_session_list_event ( press ( KeyCode ::Esc ) , & mut state ) ;
assert_eq! ( state . filter_input , "" ) ;
assert_eq! ( state . filter_cursor , 0 ) ;
}
}
}