1use std::{io::Write, path::Path};
4
5use super::basic::load_dataset;
6use crate::tui::{DatasetAdapter, DatasetViewer};
7
8pub(crate) fn cmd_view(path: &Path, initial_search: Option<&str>) -> crate::Result<()> {
10 use std::io::stdout;
11
12 use crossterm::{cursor, execute, terminal};
13
14 let dataset = load_dataset(path)?;
15 let adapter = DatasetAdapter::from_dataset(&dataset)
16 .map_err(|e| crate::Error::storage(format!("TUI adapter error: {e}")))?;
17
18 let (width, height) = terminal::size().unwrap_or((80, 24));
20 let mut viewer = DatasetViewer::with_dimensions(adapter, width, height.saturating_sub(2));
21
22 if let Some(query) = initial_search {
24 viewer.search(query);
25 }
26
27 terminal::enable_raw_mode()
29 .map_err(|e| crate::Error::storage(format!("Terminal error: {e}")))?;
30
31 let mut stdout = stdout();
32
33 execute!(stdout, cursor::Hide)
35 .map_err(|e| crate::Error::storage(format!("Terminal error: {e}")))?;
36
37 let result = run_tui_loop(&mut viewer, &mut stdout, path);
38
39 if execute!(stdout, cursor::Show).is_err() {
41 eprintln!("Warning: failed to restore cursor visibility");
42 }
43 if terminal::disable_raw_mode().is_err() {
44 eprintln!("Warning: failed to disable raw mode — run 'reset' to fix terminal");
45 }
46
47 result
48}
49
50fn render_frame<W: Write>(
52 viewer: &DatasetViewer,
53 stdout: &mut W,
54 path: &std::path::Path,
55) -> crate::Result<()> {
56 use crossterm::{
57 cursor, execute,
58 style::{Attribute, Print, SetAttribute},
59 terminal::{self, Clear, ClearType},
60 };
61
62 execute!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0)).map_err(term_err)?;
63
64 let (width, _) = terminal::size().unwrap_or((80, 24));
65 let title = format!(
66 " {} | {} rows | {}",
67 path.file_name().unwrap_or_default().to_string_lossy(),
68 viewer.row_count(),
69 if viewer.adapter().is_streaming() {
70 "Streaming"
71 } else {
72 "InMemory"
73 }
74 );
75 execute!(
76 stdout,
77 SetAttribute(Attribute::Reverse),
78 Print(format!("{:width$}", title, width = width as usize)),
79 SetAttribute(Attribute::Reset),
80 Print("\r\n")
81 )
82 .map_err(term_err)?;
83
84 for line in viewer.render_lines() {
85 execute!(stdout, Print(&line), Print("\r\n")).map_err(term_err)?;
86 }
87
88 let status = format!(
89 " Row {}-{} of {} | {} scroll | PgUp/PgDn page | Home/End | /search | q quit ",
90 viewer.scroll_offset() + 1,
91 (viewer.scroll_offset() + viewer.visible_row_count() as usize).min(viewer.row_count()),
92 viewer.row_count(),
93 "\u{2191}\u{2193}"
94 );
95 execute!(
96 stdout,
97 SetAttribute(Attribute::Reverse),
98 Print(format!("{:width$}", status, width = width as usize)),
99 SetAttribute(Attribute::Reset)
100 )
101 .map_err(term_err)?;
102
103 stdout.flush().map_err(term_err)?;
104 Ok(())
105}
106
107fn handle_key_input<W: Write>(
109 viewer: &mut DatasetViewer,
110 stdout: &mut W,
111 key: crossterm::event::KeyEvent,
112) -> crate::Result<bool> {
113 use crossterm::event::{KeyCode, KeyModifiers};
114
115 match key.code {
116 KeyCode::Char('q') | KeyCode::Esc => return Ok(true),
117 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true),
118 KeyCode::Down | KeyCode::Char('j') => viewer.scroll_down(),
119 KeyCode::Up | KeyCode::Char('k') => viewer.scroll_up(),
120 KeyCode::PageDown | KeyCode::Char(' ') => viewer.page_down(),
121 KeyCode::PageUp => viewer.page_up(),
122 KeyCode::Home | KeyCode::Char('g') => viewer.home(),
123 KeyCode::End | KeyCode::Char('G') => viewer.end(),
124 KeyCode::Char('/') => {
125 if let Some(query) = prompt_search(stdout)? {
126 viewer.search(&query);
127 }
128 }
129 _ => {}
130 }
131 Ok(false)
132}
133
134pub(crate) fn run_tui_loop<W: Write>(
136 viewer: &mut DatasetViewer,
137 stdout: &mut W,
138 path: &std::path::Path,
139) -> crate::Result<()> {
140 use crossterm::event::{self, Event};
141
142 loop {
143 render_frame(viewer, stdout, path)?;
144
145 if event::poll(std::time::Duration::from_millis(100)).map_err(term_err)? {
146 if let Event::Key(key) = event::read().map_err(term_err)? {
147 if handle_key_input(viewer, stdout, key)? {
148 break;
149 }
150 }
151
152 if let Event::Resize(w, h) = event::read().map_err(term_err)? {
153 viewer.set_dimensions(w, h.saturating_sub(2));
154 }
155 }
156 }
157
158 Ok(())
159}
160
161fn term_err(e: impl std::fmt::Display) -> crate::Error {
163 crate::Error::storage(format!("Terminal error: {e}"))
164}
165
166enum PromptStep {
168 Finish(Option<String>),
170 Continue,
172}
173
174pub(crate) fn prompt_search<W: Write>(stdout: &mut W) -> crate::Result<Option<String>> {
176 use crossterm::event::{self, Event};
177
178 let height = show_search_prompt(stdout)?;
179 let mut query = String::new();
180
181 loop {
182 if let Event::Key(key) = event::read().map_err(term_err)? {
183 if let PromptStep::Finish(result) =
184 handle_prompt_key(stdout, key.code, &mut query, height)?
185 {
186 return Ok(result);
187 }
188 stdout.flush().map_err(term_err)?;
189 }
190 }
191}
192
193fn show_search_prompt<W: Write>(stdout: &mut W) -> crate::Result<u16> {
194 use crossterm::{
195 cursor, execute,
196 style::Print,
197 terminal::{self, Clear, ClearType},
198 };
199
200 let (_, height) = terminal::size().unwrap_or((80, 24));
201 execute!(
202 stdout,
203 cursor::MoveTo(0, height - 1),
204 Clear(ClearType::CurrentLine),
205 cursor::Show,
206 Print("Search: ")
207 )
208 .map_err(term_err)?;
209 stdout.flush().map_err(term_err)?;
210 Ok(height)
211}
212
213fn handle_prompt_key<W: Write>(
214 stdout: &mut W,
215 code: crossterm::event::KeyCode,
216 query: &mut String,
217 height: u16,
218) -> crate::Result<PromptStep> {
219 use crossterm::{cursor, event::KeyCode, execute};
220
221 match code {
222 KeyCode::Enter => {
223 execute!(stdout, cursor::Hide).map_err(term_err)?;
224 let out = if query.is_empty() {
225 None
226 } else {
227 Some(std::mem::take(query))
228 };
229 Ok(PromptStep::Finish(out))
230 }
231 KeyCode::Esc => {
232 execute!(stdout, cursor::Hide).map_err(term_err)?;
233 Ok(PromptStep::Finish(None))
234 }
235 KeyCode::Backspace => {
236 query.pop();
237 redraw_query(stdout, query, height)?;
238 Ok(PromptStep::Continue)
239 }
240 KeyCode::Char(c) => {
241 query.push(c);
242 execute!(stdout, crossterm::style::Print(c)).map_err(term_err)?;
243 Ok(PromptStep::Continue)
244 }
245 _ => Ok(PromptStep::Continue),
246 }
247}
248
249fn redraw_query<W: Write>(stdout: &mut W, query: &str, height: u16) -> crate::Result<()> {
250 use crossterm::{
251 cursor, execute,
252 style::Print,
253 terminal::{Clear, ClearType},
254 };
255 execute!(
256 stdout,
257 cursor::MoveTo(8, height - 1),
258 Clear(ClearType::UntilNewLine),
259 Print(query)
260 )
261 .map_err(term_err)
262}