- load config from json - cli commands to launch existing profiles - tui to pick custom directories and venv
147 lines
4.6 KiB
Rust
147 lines
4.6 KiB
Rust
mod custom;
|
|
mod entries;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::Result;
|
|
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
|
use ratatui::layout::{Constraint, Layout};
|
|
use ratatui::style::{Modifier, Style};
|
|
use ratatui::widgets::{Block, Borders, Paragraph, Tabs};
|
|
use ratatui::{DefaultTerminal, Frame};
|
|
|
|
use crate::config::Config;
|
|
use custom::CustomTab;
|
|
use entries::EntriesTab;
|
|
|
|
const TAB_TITLES: [&str; 2] = ["Entries", "Custom"];
|
|
|
|
struct App<'a> {
|
|
/// Index of the currently selected tab.
|
|
selected_tab: usize,
|
|
entries: EntriesTab<'a>,
|
|
custom: CustomTab,
|
|
}
|
|
|
|
impl<'a> App<'a> {
|
|
fn new(config: &'a Config) -> Self {
|
|
App {
|
|
selected_tab: 0,
|
|
entries: EntriesTab::new(&config.entries),
|
|
custom: CustomTab::new(&config.apps_base, &config.venvs_base),
|
|
}
|
|
}
|
|
|
|
fn next_tab(&mut self) {
|
|
self.selected_tab = (self.selected_tab + 1) % TAB_TITLES.len();
|
|
}
|
|
|
|
fn previous_tab(&mut self) {
|
|
self.selected_tab = (self.selected_tab + TAB_TITLES.len() - 1) % TAB_TITLES.len();
|
|
}
|
|
}
|
|
|
|
/// Run the launcher TUI, returning the command the user chose (or `None` if
|
|
/// they quit without choosing).
|
|
pub fn run(config: &Config) -> Result<Option<(PathBuf, PathBuf)>> {
|
|
let mut terminal = ratatui::init();
|
|
let result = run_app(&mut terminal, config);
|
|
ratatui::restore();
|
|
result
|
|
}
|
|
|
|
fn run_app(
|
|
terminal: &mut DefaultTerminal,
|
|
config: &Config,
|
|
) -> Result<Option<(PathBuf, PathBuf)>> {
|
|
let mut app = App::new(config);
|
|
|
|
loop {
|
|
terminal.draw(|frame| draw(frame, &mut app))?;
|
|
|
|
if let Event::Key(key) = event::read()? {
|
|
if key.kind != KeyEventKind::Press {
|
|
continue;
|
|
}
|
|
|
|
// While the launch dialog is open it captures all input.
|
|
if app.entries.dialog_open() {
|
|
match key.code {
|
|
KeyCode::Enter => {
|
|
if let Some(command) = app.entries.confirm() {
|
|
return Ok(Some(command));
|
|
}
|
|
}
|
|
KeyCode::Esc => app.entries.cancel_dialog(),
|
|
KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right | KeyCode::Tab => {
|
|
app.entries.toggle_dialog()
|
|
}
|
|
_ => {}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Global keys: quit and tab switching.
|
|
match key.code {
|
|
KeyCode::Char('q') | KeyCode::Esc => return Ok(None),
|
|
KeyCode::Tab => app.next_tab(),
|
|
KeyCode::BackTab => app.previous_tab(),
|
|
// Everything else is routed to the active tab.
|
|
_ => match app.selected_tab {
|
|
0 => match key.code {
|
|
KeyCode::Down => app.entries.next(),
|
|
KeyCode::Up => app.entries.previous(),
|
|
KeyCode::Enter => app.entries.open_dialog(),
|
|
_ => {}
|
|
},
|
|
_ => match key.code {
|
|
KeyCode::Left | KeyCode::Right => app.custom.toggle_focus(),
|
|
KeyCode::Down => app.custom.next(),
|
|
KeyCode::Up => app.custom.previous(),
|
|
_ => {}
|
|
},
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw(frame: &mut Frame, app: &mut App) {
|
|
let [tabs_area, content_area, footer_area] = Layout::vertical([
|
|
Constraint::Length(3),
|
|
Constraint::Min(0),
|
|
Constraint::Length(1),
|
|
])
|
|
.areas(frame.area());
|
|
|
|
let tabs = Tabs::new(TAB_TITLES)
|
|
.block(
|
|
Block::default()
|
|
.borders(Borders::ALL)
|
|
.title("aare launcher"),
|
|
)
|
|
.select(app.selected_tab)
|
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED));
|
|
frame.render_widget(tabs, tabs_area);
|
|
|
|
match app.selected_tab {
|
|
0 => app.entries.draw(frame, content_area),
|
|
_ => app.custom.draw(frame, content_area),
|
|
}
|
|
|
|
let footer = Paragraph::new(help_text(app))
|
|
.style(Style::default().add_modifier(Modifier::DIM));
|
|
frame.render_widget(footer, footer_area);
|
|
}
|
|
|
|
/// Context-sensitive interaction hints shown in the footer.
|
|
fn help_text(app: &App) -> &'static str {
|
|
if app.entries.dialog_open() {
|
|
"↑/↓: choose Enter: launch Esc: cancel"
|
|
} else if app.selected_tab == 0 {
|
|
"Tab: switch tab ↑/↓: select Enter: launch q: quit"
|
|
} else {
|
|
"Tab: switch tab ←/→: switch list ↑/↓: select q: quit"
|
|
}
|
|
}
|