mod app;
mod cli;
mod config;
mod core;
mod error;
mod keybindings;
mod progress;
mod ui;
use crate::app::App;
use crate::cli::{get_profile_path, scan_directory};
use crate::config::Config;
use crate::core::EpubReader;
use crate::keybindings::{Action, handle_key_event};
use crate::progress::ProgressStore;
use crate::ui::{BookView, ContinuousView, DefaultView, View};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Terminal,
backend::{Backend, CrosstermBackend},
};
use ratatui_image::picker::Picker;
use std::path::{Path, PathBuf};
use std::process::exit;
use std::{error::Error, io};
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let mut profile_dir: Option<PathBuf> = None;
let mut scan_dir: Option<String> = None;
let mut epub_path: Option<String> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--profile" => {
if i + 1 < args.len() {
profile_dir = Some(get_profile_path(&args[i + 1]));
i += 2;
} else {
eprintln!("Error: --profile requires a name");
exit(1);
}
}
"--scan" => {
if i + 1 < args.len() {
scan_dir = Some(args[i + 1].clone());
i += 2;
} else {
eprintln!("Error: --scan requires a directory");
exit(1);
}
}
path if !path.starts_with('-') => {
epub_path = Some(path.to_string());
i += 1;
}
_ => {
eprintln!("Unknown argument: {}", args[i]);
exit(1);
}
}
}
if let Some(ref dir) = scan_dir {
scan_directory(Path::new(dir), profile_dir.clone())?;
if epub_path.is_none() {
return Ok(());
}
}
let progress_path = profile_dir.as_ref().map(|p| {
let mut p = p.clone();
p.push("progress.json");
p
});
let progress_store = ProgressStore::load(progress_path);
let mut current_path = if let Some(path) = epub_path {
path
} else if let Some(ref last_path) = progress_store.last_book_path {
last_path.clone()
} else {
eprintln!("Usage: {} [options] <epub_file>", args[0]);
eprintln!("Options:");
eprintln!(" --profile <name> Use a specific profile");
eprintln!(" --scan <dir> Scan directory for EPUB files");
exit(1);
};
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
loop {
let absolute_path = match std::fs::canonicalize(¤t_path) {
Ok(p) => p,
Err(e) => {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
eprintln!("Failed to resolve path '{}': {}", current_path, e);
return Ok(());
}
};
let file_path_str = absolute_path.to_string_lossy().to_string();
let reader = match EpubReader::new(&file_path_str) {
Ok(r) => r,
Err(e) => {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
eprintln!("Failed to open EPUB '{}': {}", file_path_str, e);
return Ok(());
}
};
let book_id = Path::new(&file_path_str)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| file_path_str.clone());
let progress_path = profile_dir.as_ref().map(|p| {
let mut p = p.clone();
p.push("progress.json");
p
});
let progress_store = ProgressStore::load(progress_path);
let book_progress = progress_store.get_book(&book_id);
let mut picker = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
let config_path = profile_dir.as_ref().map(|p| {
let mut p = p.clone();
p.push("config.toml");
p
});
let config = Config::load(config_path);
let mut app = App::new(
reader,
book_id,
file_path_str,
&mut picker,
book_progress,
config.clone(),
profile_dir.clone(),
);
let mut picker_clone = Picker::from_query_stdio().unwrap_or_else(|_| Picker::halfblocks());
let res = run_app(&mut terminal, &mut app, &mut picker_clone, &config);
app.save_progress();
match res {
Ok(Some(new_path)) => {
current_path = new_path;
terminal.clear()?;
}
Ok(None) => break,
Err(err) => {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
return Err(err);
}
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
app: &mut App,
picker: &mut Picker,
_config: &Config,
) -> Result<Option<String>, Box<dyn Error>>
where
B::Error: 'static,
{
let default_view = DefaultView::new();
let book_view = BookView::new();
let continuous_view = ContinuousView::new();
loop {
let view: &dyn View = match app.config.view_type.as_str() {
"book" => &book_view,
"continuous" => &continuous_view,
_ => &default_view,
};
terminal.draw(|f| view.render(f, app, picker))?;
if let Event::Key(key) = event::read()? {
match handle_key_event(app, key, picker)? {
Action::Exit => return Ok(None),
Action::SwitchBook(path) => return Ok(Some(path)),
Action::None => {}
}
}
}
}