use std::{io::Write, path::Path};
use super::basic::load_dataset;
use crate::tui::{DatasetAdapter, DatasetViewer};
pub(crate) fn cmd_view(path: &Path, initial_search: Option<&str>) -> crate::Result<()> {
use std::io::stdout;
use crossterm::{cursor, execute, terminal};
let dataset = load_dataset(path)?;
let adapter = DatasetAdapter::from_dataset(&dataset)
.map_err(|e| crate::Error::storage(format!("TUI adapter error: {e}")))?;
let (width, height) = terminal::size().unwrap_or((80, 24));
let mut viewer = DatasetViewer::with_dimensions(adapter, width, height.saturating_sub(2));
if let Some(query) = initial_search {
viewer.search(query);
}
terminal::enable_raw_mode()
.map_err(|e| crate::Error::storage(format!("Terminal error: {e}")))?;
let mut stdout = stdout();
execute!(stdout, cursor::Hide)
.map_err(|e| crate::Error::storage(format!("Terminal error: {e}")))?;
let result = run_tui_loop(&mut viewer, &mut stdout, path);
if execute!(stdout, cursor::Show).is_err() {
eprintln!("Warning: failed to restore cursor visibility");
}
if terminal::disable_raw_mode().is_err() {
eprintln!("Warning: failed to disable raw mode — run 'reset' to fix terminal");
}
result
}
fn render_frame<W: Write>(
viewer: &DatasetViewer,
stdout: &mut W,
path: &std::path::Path,
) -> crate::Result<()> {
use crossterm::{
cursor, execute,
style::{Attribute, Print, SetAttribute},
terminal::{self, Clear, ClearType},
};
execute!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0)).map_err(term_err)?;
let (width, _) = terminal::size().unwrap_or((80, 24));
let title = format!(
" {} | {} rows | {}",
path.file_name().unwrap_or_default().to_string_lossy(),
viewer.row_count(),
if viewer.adapter().is_streaming() {
"Streaming"
} else {
"InMemory"
}
);
execute!(
stdout,
SetAttribute(Attribute::Reverse),
Print(format!("{:width$}", title, width = width as usize)),
SetAttribute(Attribute::Reset),
Print("\r\n")
)
.map_err(term_err)?;
for line in viewer.render_lines() {
execute!(stdout, Print(&line), Print("\r\n")).map_err(term_err)?;
}
let status = format!(
" Row {}-{} of {} | {} scroll | PgUp/PgDn page | Home/End | /search | q quit ",
viewer.scroll_offset() + 1,
(viewer.scroll_offset() + viewer.visible_row_count() as usize).min(viewer.row_count()),
viewer.row_count(),
"\u{2191}\u{2193}"
);
execute!(
stdout,
SetAttribute(Attribute::Reverse),
Print(format!("{:width$}", status, width = width as usize)),
SetAttribute(Attribute::Reset)
)
.map_err(term_err)?;
stdout.flush().map_err(term_err)?;
Ok(())
}
fn handle_key_input<W: Write>(
viewer: &mut DatasetViewer,
stdout: &mut W,
key: crossterm::event::KeyEvent,
) -> crate::Result<bool> {
use crossterm::event::{KeyCode, KeyModifiers};
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(true),
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return Ok(true),
KeyCode::Down | KeyCode::Char('j') => viewer.scroll_down(),
KeyCode::Up | KeyCode::Char('k') => viewer.scroll_up(),
KeyCode::PageDown | KeyCode::Char(' ') => viewer.page_down(),
KeyCode::PageUp => viewer.page_up(),
KeyCode::Home | KeyCode::Char('g') => viewer.home(),
KeyCode::End | KeyCode::Char('G') => viewer.end(),
KeyCode::Char('/') => {
if let Some(query) = prompt_search(stdout)? {
viewer.search(&query);
}
}
_ => {}
}
Ok(false)
}
pub(crate) fn run_tui_loop<W: Write>(
viewer: &mut DatasetViewer,
stdout: &mut W,
path: &std::path::Path,
) -> crate::Result<()> {
use crossterm::event::{self, Event};
loop {
render_frame(viewer, stdout, path)?;
if event::poll(std::time::Duration::from_millis(100)).map_err(term_err)? {
if let Event::Key(key) = event::read().map_err(term_err)? {
if handle_key_input(viewer, stdout, key)? {
break;
}
}
if let Event::Resize(w, h) = event::read().map_err(term_err)? {
viewer.set_dimensions(w, h.saturating_sub(2));
}
}
}
Ok(())
}
fn term_err(e: impl std::fmt::Display) -> crate::Error {
crate::Error::storage(format!("Terminal error: {e}"))
}
enum PromptStep {
Finish(Option<String>),
Continue,
}
pub(crate) fn prompt_search<W: Write>(stdout: &mut W) -> crate::Result<Option<String>> {
use crossterm::event::{self, Event};
let height = show_search_prompt(stdout)?;
let mut query = String::new();
loop {
if let Event::Key(key) = event::read().map_err(term_err)? {
if let PromptStep::Finish(result) =
handle_prompt_key(stdout, key.code, &mut query, height)?
{
return Ok(result);
}
stdout.flush().map_err(term_err)?;
}
}
}
fn show_search_prompt<W: Write>(stdout: &mut W) -> crate::Result<u16> {
use crossterm::{
cursor, execute,
style::Print,
terminal::{self, Clear, ClearType},
};
let (_, height) = terminal::size().unwrap_or((80, 24));
execute!(
stdout,
cursor::MoveTo(0, height - 1),
Clear(ClearType::CurrentLine),
cursor::Show,
Print("Search: ")
)
.map_err(term_err)?;
stdout.flush().map_err(term_err)?;
Ok(height)
}
fn handle_prompt_key<W: Write>(
stdout: &mut W,
code: crossterm::event::KeyCode,
query: &mut String,
height: u16,
) -> crate::Result<PromptStep> {
use crossterm::{cursor, event::KeyCode, execute};
match code {
KeyCode::Enter => {
execute!(stdout, cursor::Hide).map_err(term_err)?;
let out = if query.is_empty() {
None
} else {
Some(std::mem::take(query))
};
Ok(PromptStep::Finish(out))
}
KeyCode::Esc => {
execute!(stdout, cursor::Hide).map_err(term_err)?;
Ok(PromptStep::Finish(None))
}
KeyCode::Backspace => {
query.pop();
redraw_query(stdout, query, height)?;
Ok(PromptStep::Continue)
}
KeyCode::Char(c) => {
query.push(c);
execute!(stdout, crossterm::style::Print(c)).map_err(term_err)?;
Ok(PromptStep::Continue)
}
_ => Ok(PromptStep::Continue),
}
}
fn redraw_query<W: Write>(stdout: &mut W, query: &str, height: u16) -> crate::Result<()> {
use crossterm::{
cursor, execute,
style::Print,
terminal::{Clear, ClearType},
};
execute!(
stdout,
cursor::MoveTo(8, height - 1),
Clear(ClearType::UntilNewLine),
Print(query)
)
.map_err(term_err)
}