use anyhow::Result;
use std::path::PathBuf;
use crate::functions::{display::cmd_display, head::cmd_head, sample::cmd_sample};
pub async fn run_tui(root: PathBuf) -> Result<()> {
use crossterm::{
ExecutableCommand,
event::{self, Event, KeyCode},
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, ListItem, Paragraph},
};
use std::fs;
use std::io::stdout;
if !root.is_dir() {
return Err(anyhow::anyhow!(format!(
"Path is not a directory: {:?}",
root
)));
}
let mut entries: Vec<PathBuf> = fs::read_dir(&root)
.map_err(|e| anyhow::anyhow!(format!("Failed to read dir {:?}: {}", root, e)))?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.eq_ignore_ascii_case("lance"))
.unwrap_or(false)
})
.collect();
entries.sort();
if entries.is_empty() {
return Err(anyhow::anyhow!(format!(
"No .lance files found in directory {:?}. Tui command works with directories, \
for files use display command",
root
)));
}
#[derive(Clone, Copy)]
enum TuiCommand {
Head,
Sample,
Display,
}
let commands = [TuiCommand::Head, TuiCommand::Sample, TuiCommand::Display];
let mut selected_file_idx: usize = 0;
let mut selected_cmd_idx: usize = 0;
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
loop {
terminal.draw(|frame| {
let size = frame.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(5), Constraint::Length(5), ])
.split(size);
let header = Paragraph::new(format!(
"Javelin - Lance Inspector\nDirectory: {}",
root.display()
))
.block(Block::default().borders(Borders::ALL).title(" Info "));
frame.render_widget(header, chunks[0]);
let items: Vec<ListItem> = entries
.iter()
.map(|p| {
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("<?>");
ListItem::new(name.to_string())
})
.collect();
let file_list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Lance files "),
)
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
frame.render_stateful_widget(
file_list,
chunks[1],
&mut ratatui::widgets::ListState::default().with_selected(Some(selected_file_idx)),
);
let cmd_labels: Vec<&str> = commands
.iter()
.map(|c| match c {
TuiCommand::Head => "Head",
TuiCommand::Sample => "Sample",
TuiCommand::Display => "Display",
})
.collect();
let mut cmd_spans = String::new();
for (i, label) in cmd_labels.iter().enumerate() {
if i == selected_cmd_idx {
cmd_spans.push_str(&format!("[{}] ", label));
} else {
cmd_spans.push_str(&format!(" {} ", label));
}
}
let cmd_para = Paragraph::new(cmd_spans)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Command (←/→ to change, Enter to run, q to quit) "),
)
.style(Style::default().fg(Color::White));
frame.render_widget(cmd_para, chunks[2]);
})?;
if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
break;
}
KeyCode::Up | KeyCode::Char('k') => {
if selected_file_idx > 0 {
selected_file_idx -= 1;
}
}
KeyCode::Down | KeyCode::Char('j') => {
if selected_file_idx + 1 < entries.len() {
selected_file_idx += 1;
}
}
KeyCode::Left | KeyCode::Char('h') => {
if selected_cmd_idx > 0 {
selected_cmd_idx -= 1;
}
}
KeyCode::Right | KeyCode::Char('l') => {
if selected_cmd_idx + 1 < commands.len() {
selected_cmd_idx += 1;
}
}
KeyCode::Enter => {
let file = entries[selected_file_idx].clone();
let cmd = commands[selected_cmd_idx];
disable_raw_mode()?;
terminal.backend_mut().execute(LeaveAlternateScreen)?;
terminal.show_cursor()?;
match cmd {
TuiCommand::Head => {
cmd_head(&file, 20).await?;
}
TuiCommand::Sample => {
cmd_sample(&file, 20).await?;
}
TuiCommand::Display => {
cmd_display(&file).await?;
}
}
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
terminal = Terminal::new(backend)?;
}
_ => {}
}
}
}
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}