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
166pub(crate) fn prompt_search<W: Write>(stdout: &mut W) -> crate::Result<Option<String>> {
168 use crossterm::{
169 cursor,
170 event::{self, Event, KeyCode},
171 execute,
172 style::Print,
173 terminal::{self, Clear, ClearType},
174 };
175
176 let (_, height) = terminal::size().unwrap_or((80, 24));
177
178 execute!(
180 stdout,
181 cursor::MoveTo(0, height - 1),
182 Clear(ClearType::CurrentLine),
183 cursor::Show,
184 Print("Search: ")
185 )
186 .map_err(term_err)?;
187 stdout.flush().map_err(term_err)?;
188
189 let mut query = String::new();
190
191 loop {
192 if let Event::Key(key) = event::read().map_err(term_err)? {
193 match key.code {
194 KeyCode::Enter => {
195 execute!(stdout, cursor::Hide).map_err(term_err)?;
196 return Ok(if query.is_empty() { None } else { Some(query) });
197 }
198 KeyCode::Esc => {
199 execute!(stdout, cursor::Hide).map_err(term_err)?;
200 return Ok(None);
201 }
202 KeyCode::Backspace => {
203 query.pop();
204 execute!(
205 stdout,
206 cursor::MoveTo(8, height - 1),
207 Clear(ClearType::UntilNewLine),
208 Print(&query)
209 )
210 .map_err(term_err)?;
211 }
212 KeyCode::Char(c) => {
213 query.push(c);
214 execute!(stdout, Print(c)).map_err(term_err)?;
215 }
216 _ => {}
217 }
218 stdout.flush().map_err(term_err)?;
219 }
220 }
221}