11use color_eyre:: Result ;
2- use crossterm:: event:: { Event , EventStream , KeyCode , KeyEvent , KeyEventKind } ;
3- use ratatui:: layout:: { Constraint , Layout , Position } ;
2+ use crossterm:: event:: { Event , EventStream , KeyCode , KeyEvent , KeyEventKind , KeyModifiers } ;
3+ use ratatui:: layout:: { Constraint , Layout , Position , Rect } ;
44use ratatui:: style:: { Color , Style } ;
55use ratatui:: text:: { Line , Span } ;
66use ratatui:: widgets:: { Block , Paragraph } ;
@@ -51,6 +51,8 @@ struct App {
5151 editor_cursor : usize ,
5252 /// Scroll position in editor
5353 editor_scroll : usize ,
54+ /// Show a tip about how to send a message
55+ show_enter_tip : bool ,
5456}
5557
5658impl App {
@@ -63,6 +65,7 @@ impl App {
6365 editor_buffer : String :: new ( ) ,
6466 editor_cursor : 0 ,
6567 editor_scroll : 0 ,
68+ show_enter_tip : false ,
6669 }
6770 }
6871
@@ -89,44 +92,18 @@ impl App {
8992 continue ;
9093 }
9194
92- match key. code {
93- KeyCode :: Char ( 'q' ) | KeyCode :: Char ( 'c' )
94- if key
95- . modifiers
96- . contains ( crossterm:: event:: KeyModifiers :: CONTROL ) =>
97- {
98- return Ok ( ( ) ) ;
99- }
100- // Temporary: simulate agent message for testing
101- KeyCode :: Char ( 'r' )
102- if key
103- . modifiers
104- . contains ( crossterm:: event:: KeyModifiers :: CONTROL ) =>
105- {
106- self . simulate_agent_response ( & tx) ;
107- }
108- KeyCode :: Char ( c) => {
109- self . enter_char ( c) ;
110- }
111- KeyCode :: Backspace => {
112- self . delete_char ( ) ;
113- }
114- KeyCode :: Left => {
115- self . move_cursor_left ( ) ;
116- }
117- KeyCode :: Right => {
118- self . move_cursor_right ( ) ;
119- }
120- KeyCode :: Up => {
121- self . scroll_messages_up ( ) ;
122- }
123- KeyCode :: Down => {
124- self . scroll_messages_down ( ) ;
125- }
126- KeyCode :: Enter => {
127- self . send_message ( ) ;
128- }
129- _ => { }
95+ // Temporary: simulate agent message for testing
96+ if key. code == KeyCode :: Char ( 'r' )
97+ && key
98+ . modifiers
99+ . contains ( crossterm:: event:: KeyModifiers :: CONTROL )
100+ {
101+ self . simulate_agent_response ( & tx) ;
102+ continue ;
103+ }
104+
105+ if self . handle_key_code ( key. code , key. modifiers ) {
106+ return Ok ( ( ) ) ;
130107 }
131108 }
132109 UiEvent :: AgentTurnBegin => {
@@ -149,6 +126,50 @@ impl App {
149126 Ok ( ( ) )
150127 }
151128
129+ /// returns true if the app should quit
130+ fn handle_key_code ( & mut self , code : KeyCode , modifiers : KeyModifiers ) -> bool {
131+ self . show_enter_tip = false ;
132+
133+ match code {
134+ KeyCode :: Char ( 'q' ) | KeyCode :: Char ( 'c' )
135+ if modifiers. contains ( crossterm:: event:: KeyModifiers :: CONTROL ) =>
136+ {
137+ return true ;
138+ }
139+ KeyCode :: Char ( c) => {
140+ self . enter_char ( c) ;
141+ }
142+ KeyCode :: Backspace => {
143+ self . delete_char ( ) ;
144+ }
145+ KeyCode :: Left => {
146+ self . move_cursor_left ( ) ;
147+ }
148+ KeyCode :: Right => {
149+ self . move_cursor_right ( ) ;
150+ }
151+ KeyCode :: Up => {
152+ self . scroll_messages_up ( ) ;
153+ }
154+ KeyCode :: Down => {
155+ self . scroll_messages_down ( ) ;
156+ }
157+ KeyCode :: Enter => {
158+ if modifiers. contains ( crossterm:: event:: KeyModifiers :: SHIFT )
159+ || modifiers. contains ( crossterm:: event:: KeyModifiers :: SUPER )
160+ || modifiers. contains ( crossterm:: event:: KeyModifiers :: ALT )
161+ {
162+ self . send_message ( ) ;
163+ } else {
164+ self . enter_char ( '\n' ) ;
165+ self . show_enter_tip = true ;
166+ }
167+ }
168+ _ => { }
169+ }
170+ false
171+ }
172+
152173 async fn keyboard_task ( tx : mpsc:: UnboundedSender < UiEvent > ) {
153174 let mut events = EventStream :: new ( ) ;
154175 while let Some ( Ok ( event) ) = events. next ( ) . await {
@@ -214,10 +235,23 @@ impl App {
214235
215236 // Set cursor position
216237 #[ expect( clippy:: cast_possible_truncation) ]
238+ let ( cursor_x, cursor_y) = self . calculate_cursor_position ( ) ;
217239 frame. set_cursor_position ( Position :: new (
218- editor_area. x + self . editor_cursor as u16 + 1 ,
219- editor_area. y + 1 ,
240+ editor_area. x + cursor_x as u16 + 1 ,
241+ editor_area. y + cursor_y as u16 + 1 ,
220242 ) ) ;
243+
244+ if self . show_enter_tip {
245+ let tip_text = "Alt- or opt-enter to send message" ;
246+ let tip_area = Rect {
247+ x : editor_area. x + 1 ,
248+ y : editor_area. y + editor_area. height - 1 ,
249+ width : tip_text. len ( ) as u16 ,
250+ height : 1 ,
251+ } ;
252+ let tip_paragraph = Paragraph :: new ( tip_text) . style ( Style :: default ( ) . fg ( Color :: Yellow ) ) ;
253+ frame. render_widget ( tip_paragraph, tip_area) ;
254+ }
221255 }
222256
223257 // Editor methods (adapted from user-input example)
@@ -246,6 +280,7 @@ impl App {
246280 }
247281
248282 fn delete_char ( & mut self ) {
283+ self . show_enter_tip = false ;
249284 let is_not_cursor_leftmost = self . editor_cursor != 0 ;
250285 if is_not_cursor_leftmost {
251286 let current_index = self . editor_cursor ;
@@ -263,6 +298,23 @@ impl App {
263298 new_cursor_pos. clamp ( 0 , self . editor_buffer . chars ( ) . count ( ) )
264299 }
265300
301+ fn calculate_cursor_position ( & self ) -> ( usize , usize ) {
302+ let mut x = 0 ;
303+ let mut y = 0 ;
304+ for ( i, c) in self . editor_buffer . chars ( ) . enumerate ( ) {
305+ if i == self . editor_cursor {
306+ break ;
307+ }
308+ if c == '\n' {
309+ y += 1 ;
310+ x = 0 ;
311+ } else {
312+ x += 1 ;
313+ }
314+ }
315+ ( x, y)
316+ }
317+
266318 // Message scrolling
267319 fn scroll_messages_up ( & mut self ) {
268320 self . message_scroll = self . message_scroll . saturating_sub ( 1 ) ;
@@ -274,6 +326,7 @@ impl App {
274326
275327 // Send user message to message history
276328 fn send_message ( & mut self ) {
329+ self . show_enter_tip = false ;
277330 if !self . editor_buffer . is_empty ( ) {
278331 self . messages
279332 . push ( Message :: User ( self . editor_buffer . clone ( ) ) ) ;
0 commit comments