use crate::app::App;
use crate::browser::app::{BrowserApp, Focus, Viewer};
use crate::browser::ui::browser_ui;
use crate::browser::{load_file_for_browser, EntryKind, FileBrowser};
use crate::config::MAX_TEXT_BYTES;
use crate::events::dispatch_viewer_key;
use crate::text_viewer::{dispatch_text_viewer_key, load_text, TextApp, TextLoadError};
use crossterm::event::{self, KeyModifiers};
pub fn run_browser_app(
terminal: &mut ratatui::DefaultTerminal,
mut app: BrowserApp,
) -> Result<(), Box<dyn std::error::Error>> {
while !app.should_quit {
terminal.draw(|frame| browser_ui(frame, &mut app))?;
if let event::Event::Key(key) = event::read()? {
if key.kind != event::KeyEventKind::Press {
continue;
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if app.picker.is_some() {
if let Some(picker) = app.picker.as_mut() {
match key.code {
event::KeyCode::Char('j') | event::KeyCode::Down => {
let next = picker.move_down();
app.theme = next;
if let Some(ref mut viewer) = app.viewer {
viewer.set_theme(next);
}
}
event::KeyCode::Char('k') | event::KeyCode::Up => {
let prev = picker.move_up();
app.theme = prev;
if let Some(ref mut viewer) = app.viewer {
viewer.set_theme(prev);
}
}
event::KeyCode::Enter => {
if let Some(path) = crate::theme::state_path() {
if let Err(e) =
crate::theme::write_state_theme_at(&path, app.theme.name)
{
eprintln!("warning: could not save theme to {:?}: {}", path, e);
}
}
app.picker = None;
}
event::KeyCode::Esc => {
let original = picker.original_theme();
app.theme = original;
if let Some(ref mut viewer) = app.viewer {
viewer.set_theme(original);
}
app.picker = None;
}
_ => {}
}
}
continue;
}
let viewer_typing = app.focus == Focus::Viewer
&& app.viewer.as_ref().map(|v| v.is_typing()).unwrap_or(false);
if key.code == event::KeyCode::Char('T') && !viewer_typing {
app.picker = Some(crate::theme_picker::ThemePicker::open(app.theme));
continue;
}
if ctrl && key.code == event::KeyCode::Char('e') {
app.browser_visible = !app.browser_visible;
if !app.browser_visible && app.focus == Focus::Browser && app.viewer.is_some() {
app.focus = Focus::Viewer;
}
continue;
}
if key.code == event::KeyCode::Tab {
match app.focus {
Focus::Browser if app.viewer.is_some() => app.focus = Focus::Viewer,
Focus::Viewer => {
app.browser_visible = true;
app.focus = Focus::Browser;
}
_ => {}
}
continue;
}
match app.focus {
Focus::Browser => handle_browser_key(&mut app, &key),
Focus::Viewer => {
if let Some(ref mut viewer) = app.viewer {
match viewer {
Viewer::DataFrame(a) => dispatch_viewer_key(a, &key),
Viewer::Text(t) => dispatch_text_viewer_key(t, &key),
}
if viewer.should_quit() {
app.should_quit = true;
}
}
}
}
}
}
Ok(())
}
fn handle_browser_key(app: &mut BrowserApp, key: &event::KeyEvent) {
match key.code {
event::KeyCode::Char('j') | event::KeyCode::Down => app.navigate_down(),
event::KeyCode::Char('k') | event::KeyCode::Up => app.navigate_up(),
event::KeyCode::Esc => app.ascend(),
event::KeyCode::Char('.') | event::KeyCode::Enter => open_or_descend(app),
event::KeyCode::Char('q') if app.viewer.is_none() => app.should_quit = true,
_ => {}
}
}
fn open_or_descend(app: &mut BrowserApp) {
let entry = match app.entries.get(app.cursor) {
Some(e) => e.clone(),
None => return,
};
match entry.kind {
EntryKind::Dir => app.descend(),
EntryKind::Binary => {
app.status = Some(format!("Cannot preview {}: binary file", entry.name));
}
EntryKind::Data => {
let ext = entry.name.rsplit('.').next().unwrap_or("").to_lowercase();
if ext == "json" && peek_json_root(&entry.path, app.backend.as_ref()) != Some(b'[') {
open_as_text(app, &entry.path, &entry.name);
return;
}
match load_file_for_browser(&entry.path, app.backend.as_ref()) {
Ok((df, title)) => {
app.viewer = Some(Viewer::DataFrame(Box::new(App::new(df, title, app.theme))));
app.focus = Focus::Viewer;
app.status = None;
}
Err(e) => {
if ext == "json" {
open_as_text(app, &entry.path, &entry.name);
} else {
app.status = Some(format!("Error loading file: {}", e));
}
}
}
}
EntryKind::Text => open_as_text(app, &entry.path, &entry.name),
}
}
fn peek_json_root(path: &str, backend: &dyn FileBrowser) -> Option<u8> {
let bytes = if path.starts_with("az://") || path.starts_with("s3://") {
backend.download_bytes(path).ok()?
} else {
use std::io::Read;
let mut file = std::fs::File::open(path).ok()?;
let mut buf = [0u8; 4096];
let n = file.read(&mut buf).ok()?;
buf[..n].to_vec()
};
bytes.iter().copied().find(|b| !b.is_ascii_whitespace())
}
fn open_as_text(app: &mut BrowserApp, path: &str, name: &str) {
match load_text(path, app.backend.as_ref(), MAX_TEXT_BYTES) {
Ok(load) => {
app.viewer = Some(Viewer::Text(TextApp::new(
load,
path.to_string(),
app.theme,
)));
app.focus = Focus::Viewer;
app.status = None;
}
Err(TextLoadError::Binary) => {
app.status = Some(format!("Cannot preview {}: not a text file", name));
}
Err(TextLoadError::Io(e)) => {
app.status = Some(format!("Error loading file: {}", e));
}
}
}