can't be bothered writing a commit message
This commit is contained in:
commit
697b009627
55 changed files with 21320 additions and 0 deletions
187
breadarr-tui/src/main.rs
Normal file
187
breadarr-tui/src/main.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
mod app;
|
||||
mod ui;
|
||||
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use breadarr_shared::{Config, DaemonClient};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
|
||||
use app::{App, Focus, Tab};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let config = Config::load()?;
|
||||
let base_url = format!("http://{}", config.daemon.listen_addr);
|
||||
let client = DaemonClient::new(base_url, &config.daemon.api_token);
|
||||
let root_folder = config.default_root_folder().to_string_lossy().to_string();
|
||||
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let mut app = App::new(client);
|
||||
let result = run(&mut terminal, &mut app, &root_folder).await;
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn run(
|
||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||
app: &mut App,
|
||||
root_folder: &str,
|
||||
) -> Result<()> {
|
||||
let mut last_refresh = tokio::time::Instant::now() - Duration::from_secs(10);
|
||||
|
||||
loop {
|
||||
if last_refresh.elapsed() >= Duration::from_secs(3) {
|
||||
app.refresh_active_tab().await;
|
||||
last_refresh = tokio::time::Instant::now();
|
||||
}
|
||||
|
||||
terminal.draw(|frame| ui::draw(frame, app))?;
|
||||
|
||||
if event::poll(Duration::from_millis(200))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
continue;
|
||||
}
|
||||
handle_key(app, key.code, root_folder).await;
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_key(app: &mut App, code: KeyCode, root_folder: &str) {
|
||||
// Typing into the add-show search box takes priority over global keys.
|
||||
if matches!(app.tab, Tab::Add) && matches!(app.focus, Focus::AddSearchInput) {
|
||||
match code {
|
||||
KeyCode::Enter => app.run_add_search().await,
|
||||
KeyCode::Char(c) => app.add_query.push(c),
|
||||
KeyCode::Backspace => {
|
||||
app.add_query.pop();
|
||||
}
|
||||
KeyCode::Esc => app.should_quit = true,
|
||||
KeyCode::Tab => cycle_tab(app),
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Typing a replacement value for the selected weight axis, same
|
||||
// priority-over-global-keys reasoning as the add-show search box above.
|
||||
if matches!(app.tab, Tab::Profiles) && matches!(app.focus, Focus::WeightInput) {
|
||||
match code {
|
||||
KeyCode::Enter => app.commit_weight_edit().await,
|
||||
KeyCode::Char(c) if c.is_ascii_digit() || c == '.' || c == '-' => {
|
||||
app.weight_input_buffer.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.weight_input_buffer.pop();
|
||||
}
|
||||
KeyCode::Esc => app.cancel_weight_edit(),
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Any key other than a second `x`/`d` clears a pending delete
|
||||
// confirmation — the confirmation must be the very next keypress, not
|
||||
// just "any keypress before the user gets distracted."
|
||||
if app.confirm_delete && code != KeyCode::Char('x') {
|
||||
app.confirm_delete = false;
|
||||
}
|
||||
if app.confirm_delete_file && code != KeyCode::Char('d') {
|
||||
app.confirm_delete_file = false;
|
||||
}
|
||||
|
||||
match code {
|
||||
KeyCode::Char('q') => app.should_quit = true,
|
||||
KeyCode::Tab => cycle_tab(app),
|
||||
KeyCode::Char('j') | KeyCode::Down => app.move_selection(1),
|
||||
KeyCode::Char('k') | KeyCode::Up => app.move_selection(-1),
|
||||
KeyCode::Esc => match app.tab {
|
||||
Tab::Library if matches!(app.focus, Focus::Candidates) => app.close_candidates(),
|
||||
Tab::Library if app.detail.is_some() => app.close_detail(),
|
||||
Tab::Add => app.focus = Focus::AddSearchInput,
|
||||
Tab::Profiles if app.profile_detail.is_some() => app.close_profile_detail(),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Enter => match app.tab {
|
||||
Tab::Library if matches!(app.focus, Focus::Candidates) => {
|
||||
app.grab_selected_candidate().await;
|
||||
}
|
||||
Tab::Library => app.open_detail().await,
|
||||
Tab::Add => match app.focus {
|
||||
Focus::AddResults => app.add_selected_search_result(root_folder).await,
|
||||
_ => app.focus = Focus::AddSearchInput,
|
||||
},
|
||||
Tab::Profiles if app.profile_detail.is_some() => {
|
||||
app.start_editing_selected_weight();
|
||||
}
|
||||
Tab::Profiles => app.open_profile_detail(),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Char('a') if matches!(app.tab, Tab::Review) => {
|
||||
app.approve_selected_review().await;
|
||||
}
|
||||
KeyCode::Char('r') if matches!(app.tab, Tab::Review) => {
|
||||
app.reject_selected_review().await;
|
||||
}
|
||||
KeyCode::Char('s') if matches!(app.tab, Tab::Library) && app.detail.is_some() => {
|
||||
app.search_now_selected().await;
|
||||
}
|
||||
KeyCode::Char('m') if matches!(app.tab, Tab::Library) && app.detail.is_some() => {
|
||||
app.toggle_monitor_selected().await;
|
||||
}
|
||||
KeyCode::Char('e') if matches!(app.tab, Tab::Library) && app.detail.is_some() => {
|
||||
app.toggle_monitor_selected_episode().await;
|
||||
}
|
||||
KeyCode::Char('S') if matches!(app.tab, Tab::Library) && app.detail.is_some() => {
|
||||
app.toggle_monitor_selected_season().await;
|
||||
}
|
||||
KeyCode::Char('x') if matches!(app.tab, Tab::Library) && app.detail.is_some() => {
|
||||
app.delete_selected().await;
|
||||
}
|
||||
KeyCode::Char('d') if matches!(app.tab, Tab::Library) && app.detail.is_some() => {
|
||||
app.delete_selected_file().await;
|
||||
}
|
||||
KeyCode::Char('c')
|
||||
if matches!(app.tab, Tab::Library)
|
||||
&& app.detail.is_some()
|
||||
&& !matches!(app.focus, Focus::Candidates) =>
|
||||
{
|
||||
app.fetch_candidates_for_selected().await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cycle_tab(app: &mut App) {
|
||||
let idx = Tab::ALL.iter().position(|t| *t == app.tab).unwrap_or(0);
|
||||
app.tab = Tab::ALL[(idx + 1) % Tab::ALL.len()];
|
||||
app.detail = None;
|
||||
app.profile_detail = None;
|
||||
app.profile_weight_state.select(None);
|
||||
app.focus = if matches!(app.tab, Tab::Add) {
|
||||
Focus::AddSearchInput
|
||||
} else {
|
||||
Focus::List
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue