@@ -54,6 +54,7 @@ enum AppEvent {
5454#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
5555pub ( crate ) enum ActiveScreen {
5656 Usage ,
57+ Activity ,
5758 Read ,
5859}
5960
@@ -63,12 +64,22 @@ enum InputOutcome {
6364 Quit ,
6465}
6566
67+ #[ derive( Debug , Clone , Copy ) ]
68+ enum ActivityCommand {
69+ RefreshAll ,
70+ ToggleMetric ,
71+ IncreaseProjects ,
72+ DecreaseProjects ,
73+ ToggleHelp ,
74+ }
75+
6676#[ derive( Debug ) ]
6777pub ( crate ) struct AppState {
6878 pub ( crate ) active_screen : ActiveScreen ,
6979 pub ( crate ) metric : UsageMetric ,
7080 pub ( crate ) range : ChartRange ,
7181 pub ( crate ) orientation : ChartOrientation ,
82+ pub ( crate ) activity_project_limit : usize ,
7283 pub ( crate ) show_help : bool ,
7384 pub ( crate ) workspace_path : Option < std:: path:: PathBuf > ,
7485 pub ( crate ) no_sessions_confirm_open : bool ,
@@ -89,12 +100,16 @@ pub(crate) struct AppState {
89100const STATE_STORE_SCHEMA_VERSION : u32 = 1 ;
90101const STATE_STORE_FILE_NAME : & str = "state.json" ;
91102const STATE_SAVE_DEBOUNCE : Duration = Duration :: from_millis ( 400 ) ;
103+ pub ( crate ) const DEFAULT_ACTIVITY_PROJECT_LIMIT : usize = 5 ;
104+ pub ( crate ) const MIN_ACTIVITY_PROJECT_LIMIT : usize = 1 ;
105+ pub ( crate ) const MAX_ACTIVITY_PROJECT_LIMIT : usize = 50 ;
92106
93107#[ derive( Debug , Clone , PartialEq , Eq ) ]
94108struct PersistedUiState {
95109 metric : UsageMetric ,
96110 range : ChartRange ,
97111 orientation : ChartOrientation ,
112+ activity_project_limit : usize ,
98113 workspace_path : Option < PathBuf > ,
99114 no_sessions_confirm_dismissed : bool ,
100115}
@@ -105,6 +120,7 @@ impl PersistedUiState {
105120 metric : UsageMetric :: Tokens ,
106121 range : ChartRange :: Week ,
107122 orientation : ChartOrientation :: Horizontal ,
123+ activity_project_limit : DEFAULT_ACTIVITY_PROJECT_LIMIT ,
108124 workspace_path,
109125 no_sessions_confirm_dismissed : false ,
110126 }
@@ -115,6 +131,7 @@ impl PersistedUiState {
115131 metric : state. metric ,
116132 range : state. range ,
117133 orientation : state. orientation ,
134+ activity_project_limit : state. activity_project_limit ,
118135 workspace_path : state. workspace_path . clone ( ) ,
119136 no_sessions_confirm_dismissed : state. no_sessions_confirm_dismissed ,
120137 }
@@ -145,6 +162,7 @@ struct StoredGlobalState {
145162 metric : Option < String > ,
146163 range : Option < String > ,
147164 orientation : Option < String > ,
165+ activity_project_limit : Option < usize > ,
148166 last_workspace_path : Option < String > ,
149167 updated_at : i64 ,
150168}
@@ -342,6 +360,7 @@ async fn run_inner(
342360 metric : restored_ui_state. metric ,
343361 range : restored_ui_state. range ,
344362 orientation : restored_ui_state. orientation ,
363+ activity_project_limit : restored_ui_state. activity_project_limit ,
345364 show_help : false ,
346365 workspace_path : restored_ui_state. workspace_path . clone ( ) ,
347366 no_sessions_confirm_open : false ,
@@ -476,7 +495,8 @@ fn handle_input_event(
476495 ( KeyCode :: Char ( 'c' ) , KeyModifiers :: CONTROL ) => return Ok ( InputOutcome :: Quit ) ,
477496 ( KeyCode :: Char ( 's' ) , _) | ( KeyCode :: Char ( 'S' ) , _) | ( KeyCode :: F ( 2 ) , _) => {
478497 state. active_screen = match state. active_screen {
479- ActiveScreen :: Usage => ActiveScreen :: Read ,
498+ ActiveScreen :: Usage => ActiveScreen :: Activity ,
499+ ActiveScreen :: Activity => ActiveScreen :: Read ,
480500 ActiveScreen :: Read => ActiveScreen :: Usage ,
481501 } ;
482502 return Ok ( InputOutcome :: Continue ( true ) ) ;
@@ -506,6 +526,14 @@ fn handle_input_event(
506526 let dirty = handle_usage_command ( state, command, usage_refresh_tx, limits_refresh_tx) ;
507527 Ok ( InputOutcome :: Continue ( dirty) )
508528 }
529+ ActiveScreen :: Activity => {
530+ let Some ( command) = map_event_to_activity_cmd ( event) else {
531+ return Ok ( InputOutcome :: Continue ( false ) ) ;
532+ } ;
533+ let dirty =
534+ handle_activity_command ( state, command, usage_refresh_tx, limits_refresh_tx) ;
535+ Ok ( InputOutcome :: Continue ( dirty) )
536+ }
509537 ActiveScreen :: Read => Ok ( InputOutcome :: Continue ( read:: tui:: handle_event (
510538 & mut state. read_browser ,
511539 event,
@@ -536,6 +564,29 @@ fn map_event_to_usage_cmd(event: Event) -> Option<UsageCommand> {
536564 }
537565}
538566
567+ fn map_event_to_activity_cmd ( event : Event ) -> Option < ActivityCommand > {
568+ match event {
569+ Event :: Key ( key) => {
570+ if key. kind != KeyEventKind :: Press {
571+ return None ;
572+ }
573+ match ( key. code , key. modifiers ) {
574+ ( KeyCode :: Char ( 'r' ) , _) | ( KeyCode :: F ( 5 ) , _) => Some ( ActivityCommand :: RefreshAll ) ,
575+ ( KeyCode :: Tab , _) => Some ( ActivityCommand :: ToggleMetric ) ,
576+ ( KeyCode :: Char ( '+' ) , _) | ( KeyCode :: Char ( '=' ) , _) | ( KeyCode :: Char ( ']' ) , _) => {
577+ Some ( ActivityCommand :: IncreaseProjects )
578+ }
579+ ( KeyCode :: Char ( '-' ) , _) | ( KeyCode :: Char ( '[' ) , _) => {
580+ Some ( ActivityCommand :: DecreaseProjects )
581+ }
582+ ( KeyCode :: Char ( '?' ) , _) => Some ( ActivityCommand :: ToggleHelp ) ,
583+ _ => None ,
584+ }
585+ }
586+ _ => None ,
587+ }
588+ }
589+
539590fn handle_usage_command (
540591 state : & mut AppState ,
541592 cmd : UsageCommand ,
@@ -588,6 +639,51 @@ fn handle_usage_command(
588639 }
589640}
590641
642+ fn handle_activity_command (
643+ state : & mut AppState ,
644+ cmd : ActivityCommand ,
645+ usage_refresh_tx : & mpsc:: Sender < ( ) > ,
646+ limits_refresh_tx : & mpsc:: Sender < ( ) > ,
647+ ) -> bool {
648+ match cmd {
649+ ActivityCommand :: ToggleHelp => {
650+ state. show_help = !state. show_help ;
651+ true
652+ }
653+ ActivityCommand :: ToggleMetric => {
654+ state. metric = match state. metric {
655+ UsageMetric :: Tokens => UsageMetric :: Time ,
656+ UsageMetric :: Time => UsageMetric :: Runs ,
657+ UsageMetric :: Runs => UsageMetric :: Tokens ,
658+ } ;
659+ true
660+ }
661+ ActivityCommand :: IncreaseProjects => {
662+ let next = state
663+ . activity_project_limit
664+ . saturating_add ( 1 )
665+ . min ( MAX_ACTIVITY_PROJECT_LIMIT ) ;
666+ let changed = next != state. activity_project_limit ;
667+ state. activity_project_limit = next;
668+ changed
669+ }
670+ ActivityCommand :: DecreaseProjects => {
671+ let next = state
672+ . activity_project_limit
673+ . saturating_sub ( 1 )
674+ . max ( MIN_ACTIVITY_PROJECT_LIMIT ) ;
675+ let changed = next != state. activity_project_limit ;
676+ state. activity_project_limit = next;
677+ changed
678+ }
679+ ActivityCommand :: RefreshAll => {
680+ let _ = usage_refresh_tx. try_send ( ( ) ) ;
681+ let _ = limits_refresh_tx. try_send ( ( ) ) ;
682+ true
683+ }
684+ }
685+ }
686+
591687fn handle_app_event ( state : & mut AppState , evt : AppEvent ) -> bool {
592688 match evt {
593689 AppEvent :: UsageUpdated ( res) => {
@@ -653,6 +749,10 @@ fn load_persisted_ui_state(
653749 state. orientation = orientation;
654750 }
655751 }
752+ if let Some ( limit) = store. global . activity_project_limit {
753+ state. activity_project_limit =
754+ limit. clamp ( MIN_ACTIVITY_PROJECT_LIMIT , MAX_ACTIVITY_PROJECT_LIMIT ) ;
755+ }
656756
657757 if let Some ( workspace_path) = state. workspace_path . as_ref ( ) {
658758 let workspace_key = workspace_path. to_string_lossy ( ) ;
@@ -675,6 +775,11 @@ fn save_persisted_ui_state(comon_home: &Path, state: &PersistedUiState) -> Resul
675775 store. global . metric = Some ( usage_metric_to_store ( state. metric ) . to_string ( ) ) ;
676776 store. global . range = Some ( chart_range_to_store ( state. range ) . to_string ( ) ) ;
677777 store. global . orientation = Some ( chart_orientation_to_store ( state. orientation ) . to_string ( ) ) ;
778+ store. global . activity_project_limit = Some (
779+ state
780+ . activity_project_limit
781+ . clamp ( MIN_ACTIVITY_PROJECT_LIMIT , MAX_ACTIVITY_PROJECT_LIMIT ) ,
782+ ) ;
678783 store. global . last_workspace_path = workspace_path_text. clone ( ) ;
679784 store. global . updated_at = now;
680785
0 commit comments