@ -5,7 +5,7 @@
//! a scrollable chat log below.
//! a scrollable chat log below.
use ratatui ::Frame ;
use ratatui ::Frame ;
use ratatui ::crossterm ::event ::{ Event , KeyCode , KeyEventKind };
use ratatui ::crossterm ::event ::{ Event , KeyCode , KeyEventKind , KeyModifiers };
use ratatui ::layout ::{ Constraint , Direction , Layout , Rect } ;
use ratatui ::layout ::{ Constraint , Direction , Layout , Rect } ;
use ratatui ::style ::{ Color , Modifier , Style } ;
use ratatui ::style ::{ Color , Modifier , Style } ;
use ratatui ::text ::{ Line , Span , Text } ;
use ratatui ::text ::{ Line , Span , Text } ;
@ -301,7 +301,7 @@ pub fn render_transcript(f: &mut Frame, area: Rect, state: &AppState) {
} ;
} ;
let chat_block = Block ::default ( )
let chat_block = Block ::default ( )
. title ( " Transcript [↑/↓ scroll · ←/→ h-scroll · Tab focus · Esc back · ? help ] ")
. title ( " Transcript [↑/↓ scroll · Spc/PgDn page · ←/→ h-scroll · Tab · Esc · ?] ")
. borders ( Borders ::ALL )
. borders ( Borders ::ALL )
. border_style ( chat_border_style ) ;
. border_style ( chat_border_style ) ;
@ -423,6 +423,23 @@ pub fn handle_transcript_event(event: Event, state: &mut AppState) -> bool {
true
true
}
}
} ,
} ,
// Page scrolling — always acts on the chat log, matching less/more.
// Shift+Space must come before the plain-Space arm so the guard fires first.
KeyCode ::Char ( ' ' ) if key . modifiers . contains ( KeyModifiers ::SHIFT ) = > {
let page = ( state . transcript_page_height as usize ) . max ( 1 ) ;
state . transcript_scroll = state . transcript_scroll . saturating_sub ( page ) ;
true
}
KeyCode ::PageUp = > {
let page = ( state . transcript_page_height as usize ) . max ( 1 ) ;
state . transcript_scroll = state . transcript_scroll . saturating_sub ( page ) ;
true
}
KeyCode ::PageDown | KeyCode ::Char ( ' ' ) = > {
let page = ( state . transcript_page_height as usize ) . max ( 1 ) ;
state . transcript_scroll = state . transcript_scroll . saturating_add ( page ) ;
true
}
// Horizontal scroll — only meaningful in ChatLog.
// Horizontal scroll — only meaningful in ChatLog.
KeyCode ::Left | KeyCode ::Char ( 'h' ) = > {
KeyCode ::Left | KeyCode ::Char ( 'h' ) = > {
state . transcript_h_scroll = state . transcript_h_scroll . saturating_sub ( 1 ) ;
state . transcript_h_scroll = state . transcript_h_scroll . saturating_sub ( 1 ) ;
@ -947,6 +964,69 @@ mod tests {
assert! ( state . show_help ) ;
assert! ( state . show_help ) ;
}
}
fn press_with_mod ( code : KeyCode , mods : KeyModifiers ) -> Event {
Event ::Key ( KeyEvent {
code ,
modifiers : mods ,
kind : KeyEventKind ::Press ,
state : KeyEventState ::NONE ,
} )
}
#[ test ]
fn space_scrolls_down_by_page ( ) {
let mut state = transcript_state ( ) ;
state . transcript_page_height = 20 ;
handle_transcript_event ( press ( KeyCode ::Char ( ' ' ) ) , & mut state ) ;
assert_eq! ( state . transcript_scroll , 20 ) ;
}
#[ test ]
fn pagedown_scrolls_down_by_page ( ) {
let mut state = transcript_state ( ) ;
state . transcript_page_height = 15 ;
handle_transcript_event ( press ( KeyCode ::PageDown ) , & mut state ) ;
assert_eq! ( state . transcript_scroll , 15 ) ;
}
#[ test ]
fn shift_space_scrolls_up_by_page ( ) {
let mut state = transcript_state ( ) ;
state . transcript_page_height = 10 ;
state . transcript_scroll = 25 ;
handle_transcript_event (
press_with_mod ( KeyCode ::Char ( ' ' ) , KeyModifiers ::SHIFT ) ,
& mut state ,
) ;
assert_eq! ( state . transcript_scroll , 15 ) ;
}
#[ test ]
fn pageup_scrolls_up_by_page ( ) {
let mut state = transcript_state ( ) ;
state . transcript_page_height = 10 ;
state . transcript_scroll = 25 ;
handle_transcript_event ( press ( KeyCode ::PageUp ) , & mut state ) ;
assert_eq! ( state . transcript_scroll , 15 ) ;
}
#[ test ]
fn pageup_clamps_at_zero ( ) {
let mut state = transcript_state ( ) ;
state . transcript_page_height = 20 ;
state . transcript_scroll = 5 ;
handle_transcript_event ( press ( KeyCode ::PageUp ) , & mut state ) ;
assert_eq! ( state . transcript_scroll , 0 ) ;
}
#[ test ]
fn page_scroll_uses_one_when_height_is_zero ( ) {
let mut state = transcript_state ( ) ;
state . transcript_page_height = 0 ;
handle_transcript_event ( press ( KeyCode ::Char ( ' ' ) ) , & mut state ) ;
assert_eq! ( state . transcript_scroll , 1 ) ;
}
#[ test ]
#[ test ]
fn unhandled_key_not_consumed ( ) {
fn unhandled_key_not_consumed ( ) {
let mut state = transcript_state ( ) ;
let mut state = transcript_state ( ) ;