11// Copyright © 2025 Huly Labs. Use of this source code is governed by the MIT license.
2- use std:: collections:: HashSet ;
2+ use std:: collections:: { HashMap , HashSet } ;
33use std:: path:: PathBuf ;
44
55use crate :: agent:: event:: { AgentCommandStatus , AgentState , AgentStatus } ;
@@ -13,6 +13,8 @@ use crate::{
1313 } ,
1414} ;
1515use crossterm:: event:: KeyEventKind ;
16+ use ratatui:: layout:: Position ;
17+ use ratatui:: prelude:: Rect ;
1618use ratatui:: {
1719 crossterm:: event:: { KeyCode , KeyEvent , KeyModifiers } ,
1820 widgets:: ScrollbarState ,
@@ -25,7 +27,7 @@ use tui_widget_list::ListState;
2527
2628use super :: filetree:: FileTreeState ;
2729
28- #[ derive( Debug ) ]
30+ #[ derive( Debug , Clone , Eq , Hash , PartialEq ) ]
2931#[ repr( u8 ) ]
3032pub enum FocusedComponent {
3133 /// Input text area field
@@ -69,6 +71,7 @@ pub struct UiState<'a> {
6971 pub history_state : ListState ,
7072 pub history_opened_state : HashSet < usize > ,
7173 pub throbber_state : throbber_widgets_tui:: ThrobberState ,
74+ pub widget_areas : HashMap < FocusedComponent , Rect > ,
7275}
7376
7477#[ derive( Debug ) ]
@@ -92,8 +95,9 @@ impl UiState<'_> {
9295 terminal_scroll_position : 0 ,
9396 tree_state : FileTreeState :: new ( workspace) ,
9497 history_state : ListState :: default ( ) ,
95- history_opened_state : HashSet :: new ( ) ,
98+ history_opened_state : HashSet :: default ( ) ,
9699 throbber_state : throbber_widgets_tui:: ThrobberState :: default ( ) ,
100+ widget_areas : HashMap :: default ( ) ,
97101 }
98102 }
99103}
@@ -130,54 +134,85 @@ impl App<'_> {
130134
131135 match self . events . next ( ) . await ? {
132136 UiEvent :: Tick => self . tick ( ) ,
133- UiEvent :: Crossterm ( event) => {
134- if let crossterm:: event:: Event :: Key ( KeyEvent {
135- code : KeyCode :: Tab ,
136- kind : KeyEventKind :: Press ,
137- ..
138- } ) = event
139- {
140- self . ui . focus = ( self . ui . focus as u8 + 1u8 ) . into ( ) ;
141- self . ui . tree_state . focused =
142- matches ! ( self . ui. focus, FocusedComponent :: Tree ) ;
143- } else {
144- if let crossterm:: event:: Event :: Key ( key_event) = event {
145- self . handle_global_key_events ( key_event) ?
146- }
147- match self . ui . focus {
148- FocusedComponent :: Input => {
149- if Self :: handle_text_input ( & mut self . ui . textarea , & event)
150- && !self . ui . textarea . is_empty ( )
151- {
152- self . ui . textarea . select_all ( ) ;
153- self . ui . textarea . cut ( ) ;
154- self . model . last_error = None ;
155- self . agent_sender
156- . send ( agent:: AgentControlEvent :: SendMessage (
157- self . ui . textarea . yank_text ( ) ,
158- ) )
159- . unwrap ( ) ;
137+ UiEvent :: Crossterm ( event) => match event {
138+ crossterm:: event:: Event :: Key ( key_event) => {
139+ if !self . handle_global_key_events ( key_event) ? {
140+ match self . ui . focus {
141+ FocusedComponent :: Input => {
142+ if Self :: handle_text_input ( & mut self . ui . textarea , & event)
143+ && !self . ui . textarea . is_empty ( )
144+ {
145+ self . ui . textarea . select_all ( ) ;
146+ self . ui . textarea . cut ( ) ;
147+ self . model . last_error = None ;
148+ self . agent_sender
149+ . send ( agent:: AgentControlEvent :: SendMessage (
150+ self . ui . textarea . yank_text ( ) ,
151+ ) )
152+ . unwrap ( ) ;
153+ }
154+ }
155+ FocusedComponent :: Tree => {
156+ Self :: handle_tree_input ( & mut self . ui . tree_state , & event) ;
157+ }
158+ FocusedComponent :: History => {
159+ Self :: handle_list_input (
160+ & mut self . ui . history_state ,
161+ & mut self . ui . history_opened_state ,
162+ & event,
163+ ) ;
164+ }
165+ FocusedComponent :: Terminal => {
166+ Self :: handle_scroll_input (
167+ & mut self . ui . terminal_scroll_position ,
168+ & event,
169+ ) ;
160170 }
161171 }
162- FocusedComponent :: Tree => {
163- Self :: handle_tree_input ( & mut self . ui . tree_state , & event) ;
164- }
165- FocusedComponent :: History => {
166- Self :: handle_list_input (
167- & mut self . ui . history_state ,
168- & mut self . ui . history_opened_state ,
169- & event,
170- ) ;
172+ }
173+ }
174+ crossterm:: event:: Event :: Mouse ( mouse_event) => {
175+ if matches ! ( mouse_event. kind, crossterm:: event:: MouseEventKind :: Down ( _) ) {
176+ let focus = self . ui . widget_areas . iter ( ) . find_map ( |( k, v) | {
177+ if v. contains ( Position {
178+ x : mouse_event. column ,
179+ y : mouse_event. row ,
180+ } ) {
181+ Some ( k)
182+ } else {
183+ None
184+ }
185+ } ) ;
186+ if let Some ( focus) = focus {
187+ self . ui . focus = focus. clone ( ) ;
188+ self . ui . tree_state . focused =
189+ matches ! ( self . ui. focus, FocusedComponent :: Tree ) ;
171190 }
172- FocusedComponent :: Terminal => {
173- Self :: handle_scroll_input (
174- & mut self . ui . terminal_scroll_position ,
175- & event,
176- ) ;
191+ match self . ui . focus {
192+ FocusedComponent :: Input => {
193+ Self :: handle_text_input ( & mut self . ui . textarea , & event) ;
194+ }
195+ FocusedComponent :: Tree => {
196+ Self :: handle_tree_input ( & mut self . ui . tree_state , & event) ;
197+ }
198+ FocusedComponent :: History => {
199+ Self :: handle_list_input (
200+ & mut self . ui . history_state ,
201+ & mut self . ui . history_opened_state ,
202+ & event,
203+ ) ;
204+ }
205+ FocusedComponent :: Terminal => {
206+ Self :: handle_scroll_input (
207+ & mut self . ui . terminal_scroll_position ,
208+ & event,
209+ ) ;
210+ }
177211 }
178212 }
179213 }
180- }
214+ _ => { }
215+ } ,
181216 UiEvent :: App ( app_event) => match app_event {
182217 AppEvent :: Quit => self . quit ( ) ,
183218 AppEvent :: Agent ( evt) => match evt {
@@ -308,9 +343,9 @@ impl App<'_> {
308343 }
309344 }
310345
311- pub fn handle_global_key_events ( & mut self , key_event : KeyEvent ) -> color_eyre:: Result < ( ) > {
346+ pub fn handle_global_key_events ( & mut self , key_event : KeyEvent ) -> color_eyre:: Result < bool > {
312347 if key_event. kind != KeyEventKind :: Press {
313- return Ok ( ( ) ) ;
348+ return Ok ( false ) ;
314349 }
315350
316351 match key_event. code {
@@ -328,9 +363,35 @@ impl App<'_> {
328363 . agent_sender
329364 . send ( AgentControlEvent :: CancelTask )
330365 . unwrap ( ) ,
331- _ => { }
366+ KeyCode :: BackTab => {
367+ let mut focus = self . ui . focus . clone ( ) as u8 ;
368+ if focus == 0 {
369+ focus = FocusedComponent :: Terminal as u8 ;
370+ } else {
371+ focus = focus - 1 ;
372+ }
373+ self . ui . focus = focus. into ( ) ;
374+ }
375+ KeyCode :: Tab => {
376+ self . ui . focus = ( self . ui . focus . clone ( ) as u8 + 1u8 ) . into ( ) ;
377+ }
378+ KeyCode :: Char ( '1' ) | KeyCode :: Char ( '2' ) | KeyCode :: Char ( '3' ) | KeyCode :: Char ( '4' )
379+ if key_event. modifiers == KeyModifiers :: ALT =>
380+ {
381+ match key_event. code {
382+ KeyCode :: Char ( '1' ) => self . ui . focus = FocusedComponent :: Input . into ( ) ,
383+ KeyCode :: Char ( '2' ) => self . ui . focus = FocusedComponent :: History . into ( ) ,
384+ KeyCode :: Char ( '3' ) => self . ui . focus = FocusedComponent :: Tree . into ( ) ,
385+ KeyCode :: Char ( '4' ) => self . ui . focus = FocusedComponent :: Terminal . into ( ) ,
386+ _ => { }
387+ } ;
388+ }
389+ _ => {
390+ return Ok ( false ) ;
391+ }
332392 }
333- Ok ( ( ) )
393+ self . ui . tree_state . focused = matches ! ( self . ui. focus, FocusedComponent :: Tree ) ;
394+ Ok ( true )
334395 }
335396
336397 pub fn current_task_text ( & self ) -> String {
0 commit comments