mod app;
mod handler;
mod ui;
use std::{
io::{Stdout, stdout},
panic,
time::Duration,
};
use crossterm::{
event::{DisableBracketedPaste, EnableBracketedPaste, poll, read},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use langcodec::Codec;
use ratatui::{Terminal, backend::CrosstermBackend};
use self::{
app::App,
handler::{HandlerResult, handle_event},
};
pub struct BrowseOptions {
pub input: String,
pub lang: Option<String>,
}
struct TermGuard {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl TermGuard {
fn new() -> Result<Self, String> {
enable_raw_mode().map_err(|e| {
format!("Failed to enable raw mode: {e}\nHint: 'browse' requires an interactive terminal (TTY).")
})?;
let mut out = stdout();
execute!(out, EnterAlternateScreen, EnableBracketedPaste)
.map_err(|e| format!("Failed to enter alternate screen: {e}"))?;
let backend = CrosstermBackend::new(out);
let terminal = Terminal::new(backend)
.map_err(|e| format!("Failed to create terminal: {e}"))?;
Ok(Self { terminal })
}
}
impl Drop for TermGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(
self.terminal.backend_mut(),
DisableBracketedPaste,
LeaveAlternateScreen
);
let _ = self.terminal.show_cursor();
}
}
pub fn run_browse_command(opts: BrowseOptions) -> Result<(), String> {
let file_path = opts.input.clone();
let inferred_format = langcodec::infer_format_from_path(&file_path)
.ok_or_else(|| format!("Cannot detect format for '{file_path}'"))?;
let mut codec = Codec::new();
codec
.read_file_by_extension(&file_path, opts.lang)
.map_err(|e| format!("Failed to read '{file_path}': {e}"))?;
if codec.resources.is_empty() {
return Err(format!("No localization data found in '{file_path}'"));
}
let mut app = App::new(codec, file_path, inferred_format);
let hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
let _ = disable_raw_mode();
let mut out = stdout();
let _ = execute!(out, DisableBracketedPaste, LeaveAlternateScreen);
hook(info);
}));
let mut term = TermGuard::new()?;
loop {
term.terminal
.draw(|frame| ui::render(frame, &mut app))
.map_err(|e| format!("Render error: {e}"))?;
if poll(Duration::from_millis(50))
.map_err(|e| format!("Input poll error: {e}"))?
{
let event = read().map_err(|e| format!("Input read error: {e}"))?;
if let HandlerResult::Quit = handle_event(&mut app, event) {
break;
}
}
}
Ok(())
}