graphrag_cli/ui/components/
status_bar.rs1use crate::{
4 action::{Action, QueryMode, StatusType},
5 theme::Theme,
6 ui::Spinner,
7};
8use ratatui::{
9 layout::{Alignment, Rect},
10 style::{Color, Modifier, Style},
11 text::{Line, Span},
12 widgets::{Block, Borders, Paragraph},
13 Frame,
14};
15
16pub struct StatusBar {
18 message: String,
19 status_type: StatusType,
20 progress_active: bool,
21 progress_message: String,
22 spinner: Spinner,
23 query_mode: QueryMode,
25 theme: Theme,
26}
27
28impl StatusBar {
29 pub fn new() -> Self {
30 Self {
31 message: "Ready — use /config to load a configuration".to_string(),
32 status_type: StatusType::Info,
33 progress_active: false,
34 progress_message: String::new(),
35 spinner: Spinner::new(),
36 query_mode: QueryMode::default(),
37 theme: Theme::default(),
38 }
39 }
40
41 pub fn set_status(&mut self, status_type: StatusType, message: String) {
42 self.status_type = status_type;
43 self.message = message;
44 self.progress_active = false;
45 }
46
47 pub fn clear(&mut self) {
48 self.message = "Ready".to_string();
49 self.status_type = StatusType::Info;
50 self.progress_active = false;
51 }
52
53 pub fn start_progress(&mut self, message: String) {
54 self.progress_active = true;
55 self.progress_message = message;
56 self.status_type = StatusType::Progress;
57 }
58
59 pub fn stop_progress(&mut self) {
60 self.progress_active = false;
61 self.progress_message.clear();
62 }
63}
64
65impl super::Component for StatusBar {
66 fn handle_action(&mut self, action: &Action) -> Option<Action> {
67 match action {
68 Action::SetStatus(status_type, message) => {
69 self.set_status(*status_type, message.clone());
70 None
71 },
72 Action::ClearStatus => {
73 self.clear();
74 None
75 },
76 Action::StartProgress(message) => {
77 self.start_progress(message.clone());
78 None
79 },
80 Action::StopProgress => {
81 self.stop_progress();
82 None
83 },
84 Action::SetQueryMode(mode) => {
85 self.query_mode = *mode;
86 None
87 },
88 _ => None,
89 }
90 }
91
92 fn render(&mut self, f: &mut Frame, area: Rect) {
93 let block = Block::default()
94 .borders(Borders::ALL)
95 .border_style(self.theme.border());
96
97 let display_message = if self.progress_active {
98 let spinner_frame = self.spinner.tick();
99 format!(
100 "{} {} {}",
101 spinner_frame,
102 self.status_type.icon(),
103 self.progress_message
104 )
105 } else {
106 format!("{} {}", self.status_type.icon(), self.message)
107 };
108
109 let msg_style = Style::default().fg(self.status_type.color()).add_modifier(
110 if matches!(self.status_type, StatusType::Error | StatusType::Warning) {
111 Modifier::BOLD
112 } else {
113 Modifier::empty()
114 },
115 );
116
117 let (mode_color, mode_label) = match self.query_mode {
119 QueryMode::Ask => (Color::DarkGray, " [ASK] "),
120 QueryMode::Explain => (Color::Cyan, " [EXPLAIN] "),
121 QueryMode::Reason => (Color::Magenta, " [REASON] "),
122 };
123
124 let mode_badge = Span::styled(
125 mode_label.to_owned(),
126 Style::default()
127 .fg(Color::Black)
128 .bg(mode_color)
129 .add_modifier(Modifier::BOLD),
130 );
131
132 let hint = Span::styled(
133 " | Ctrl+N next | ↑↓ scroll | ? help | Esc input | Ctrl+C quit".to_owned(),
134 self.theme.dimmed(),
135 );
136
137 let line = Line::from(vec![
138 Span::styled(display_message, msg_style),
139 hint,
140 mode_badge,
141 ]);
142
143 let paragraph = Paragraph::new(line).block(block).alignment(Alignment::Left);
144
145 f.render_widget(paragraph, area);
146 }
147}
148
149impl Default for StatusBar {
150 fn default() -> Self {
151 Self::new()
152 }
153}