1use crate::colors::*;
2use crate::state::TuiState;
3use crate::utils::score_to_color;
4use ck_core::SearchMode;
5use ratatui::Frame;
6use ratatui::layout::Rect;
7use ratatui::style::{Modifier, Style};
8use ratatui::text::{Line, Span};
9use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
10
11pub fn draw_query_input(f: &mut Frame, area: Rect, state: &TuiState) {
12 let (title, style) = if state.command_mode {
13 (
15 "Command (Enter to execute, /help for help)".to_string(),
16 Style::default().fg(COLOR_CYAN).add_modifier(Modifier::BOLD),
17 )
18 } else {
19 let mode_indicator = match state.mode {
21 SearchMode::Semantic => "[SEM]",
22 SearchMode::Regex => "[REG]",
23 SearchMode::Hybrid => "[HYB]",
24 SearchMode::Lexical => "[LEX]",
25 };
26 (
27 format!(
28 "Search {} (Tab to cycle, /help for commands)",
29 mode_indicator
30 ),
31 Style::default().fg(COLOR_YELLOW),
32 )
33 };
34
35 let input = Paragraph::new(state.query.as_str())
36 .style(style)
37 .block(Block::default().borders(Borders::ALL).title(title));
38 f.render_widget(input, area);
39}
40
41pub fn draw_results_list(f: &mut Frame, area: Rect, state: &TuiState, list_state: &mut ListState) {
42 let items: Vec<ListItem> = state
43 .results
44 .iter()
45 .enumerate()
46 .map(|(idx, result)| {
47 let score_color = score_to_color(result.score);
48 let is_selected = state.selected_files.contains(&result.file);
49 let prefix = if is_selected { "✓ " } else { " " };
50 let content = format!(
51 "{}[{:.3}] {}:{}",
52 prefix,
53 result.score,
54 result.file.display(),
55 result.span.line_start
56 );
57 let style = if idx == state.selected_idx {
58 Style::default()
59 .fg(COLOR_BLACK)
60 .bg(score_color)
61 .add_modifier(Modifier::BOLD)
62 } else if is_selected {
63 Style::default()
64 .fg(score_color)
65 .add_modifier(Modifier::BOLD)
66 } else {
67 Style::default().fg(score_color)
68 };
69 ListItem::new(content).style(style)
70 })
71 .collect();
72
73 let title = format!("Results ({}/{})", state.results.len(), state.results.len());
74 let list = List::new(items)
75 .block(Block::default().borders(Borders::ALL).title(title))
76 .highlight_style(Style::default().add_modifier(Modifier::BOLD));
77
78 f.render_stateful_widget(list, area, list_state);
79}
80
81pub fn draw_preview(f: &mut Frame, area: Rect, state: &TuiState) {
82 let view_mode = if state.full_file_mode {
84 "Full File"
85 } else {
86 "Snippet"
87 };
88 let title = format!(
89 "{}: {:?} (^V: view | ^F: toggle | PgUp/Dn: scroll)",
90 view_mode, state.preview_mode
91 );
92
93 let preview = if !state.preview_lines.is_empty() {
94 Paragraph::new(state.preview_lines.clone())
95 .block(Block::default().borders(Borders::ALL).title(title.clone()))
96 } else {
97 let preview_text = if state.preview_content.is_empty() {
99 "No preview available"
100 } else {
101 &state.preview_content
102 };
103 Paragraph::new(preview_text)
104 .style(Style::default().fg(COLOR_WHITE))
105 .block(Block::default().borders(Borders::ALL).title(title))
106 };
107
108 f.render_widget(preview, area);
109}
110
111pub fn draw_status_bar(f: &mut Frame, area: Rect, state: &TuiState) {
112 let help_text = " ↑↓: Nav | Tab: Mode | ^V: View | ^Space: Select | Enter: Open | ^↑↓: History | Esc/q: Quit ";
113
114 let mut status_spans = vec![Span::styled(
115 state.status_message.clone(),
116 Style::default().fg(COLOR_CYAN),
117 )];
118
119 if state.indexing_active {
120 let spinner_idx = state
121 .indexing_started_at
122 .map(|start| ((start.elapsed().as_millis() / 120) as usize) % SPINNER_FRAMES.len())
123 .unwrap_or(0);
124 let spinner = SPINNER_FRAMES[spinner_idx];
125
126 status_spans.push(Span::raw(" | "));
127 status_spans.push(Span::styled(
128 format!("{} ", spinner),
129 Style::default().fg(COLOR_YELLOW),
130 ));
131
132 if let Some(progress) = state.indexing_progress {
134 let pct = (progress * 100.0).clamp(0.0, 100.0).round() as i32;
135 status_spans.push(Span::styled(
136 format!("[{:>3}%] ", pct),
137 Style::default()
138 .fg(COLOR_GREEN)
139 .add_modifier(Modifier::BOLD),
140 ));
141 }
142
143 if let Some(message) = state.indexing_message.as_ref() {
145 let parts: Vec<&str> = message.split(" • ").collect();
147 for (i, part) in parts.iter().enumerate() {
148 if i > 0 {
149 status_spans.push(Span::styled(" • ", Style::default().fg(COLOR_DARK_GRAY)));
150 }
151 let color = if i == 0 {
152 COLOR_CYAN } else {
154 COLOR_GRAY };
156 status_spans.push(Span::styled(*part, Style::default().fg(color)));
157 }
158 } else {
159 status_spans.push(Span::styled("Indexing...", Style::default().fg(COLOR_CYAN)));
160 }
161 } else if let Some(message) = state.indexing_message.as_ref() {
162 status_spans.push(Span::raw(" | "));
163 status_spans.push(Span::styled(
164 message.clone(),
165 Style::default().fg(COLOR_GRAY),
166 ));
167 }
168
169 if !state.selected_files.is_empty() {
170 status_spans.push(Span::raw(" | "));
171 status_spans.push(Span::styled(
172 format!("{} selected", state.selected_files.len()),
173 Style::default().fg(COLOR_MAGENTA),
174 ));
175 }
176
177 let index_info = if let Some(stats) = state.index_stats.as_ref() {
178 format!(
179 "Index: {} files, {} chunks",
180 stats.total_files, stats.total_chunks
181 )
182 } else if let Some(err) = state.index_stats_error.as_ref() {
183 format!("Index error: {}", err)
184 } else {
185 "Index: --".to_string()
186 };
187 status_spans.push(Span::raw(" | "));
188 status_spans.push(Span::styled(index_info, Style::default().fg(COLOR_GRAY)));
189
190 status_spans.push(Span::raw(" | "));
191 status_spans.push(Span::styled(
192 help_text,
193 Style::default().fg(COLOR_DARK_GRAY),
194 ));
195
196 let status =
197 Paragraph::new(Line::from(status_spans)).block(Block::default().borders(Borders::ALL));
198 f.render_widget(status, area);
199}