first commit
This commit is contained in:
213
src/main.rs
Normal file
213
src/main.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
mod api;
|
||||
mod app;
|
||||
mod ui;
|
||||
|
||||
use std::{io, sync::mpsc, time::Duration};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyModifiers},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
use api::{do_login, fetch_pages, fetch_spaces};
|
||||
use app::{App, AppMsg, AppState, LoginField, Panel};
|
||||
use ui::{draw_login, draw_main};
|
||||
|
||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let res = run_app(&mut terminal).await;
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ─── App loop ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
|
||||
let mut app = App::new();
|
||||
let (tx, rx) = mpsc::channel::<AppMsg>();
|
||||
|
||||
loop {
|
||||
// Drain async messages
|
||||
while let Ok(msg) = rx.try_recv() {
|
||||
match msg {
|
||||
AppMsg::LoginSuccess { token, base_url } => {
|
||||
app.token = token.clone();
|
||||
app.base_url = base_url.clone();
|
||||
app.state = AppState::Main;
|
||||
app.login.submitting = false;
|
||||
let tx2 = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let msg = match fetch_spaces(&base_url, &token).await {
|
||||
Ok(spaces) => AppMsg::SpacesLoaded(spaces),
|
||||
Err(e) => AppMsg::ApiError(e),
|
||||
};
|
||||
let _ = tx2.send(msg);
|
||||
});
|
||||
}
|
||||
AppMsg::LoginError(e) => {
|
||||
app.login.error = Some(e);
|
||||
app.login.submitting = false;
|
||||
}
|
||||
AppMsg::SpacesLoaded(spaces) => {
|
||||
app.main.loading_spaces = false;
|
||||
app.main.spaces = spaces;
|
||||
app.main.selected_space = 0;
|
||||
app.main.pages = vec![];
|
||||
if let Some(space) = app.main.spaces.first() {
|
||||
app.main.loading_pages = true;
|
||||
let tx2 = tx.clone();
|
||||
let base_url = app.base_url.clone();
|
||||
let token = app.token.clone();
|
||||
let space_id = space.id.clone();
|
||||
tokio::spawn(async move {
|
||||
let msg = match fetch_pages(&base_url, &token, &space_id).await {
|
||||
Ok(pages) => AppMsg::PagesLoaded(pages),
|
||||
Err(e) => AppMsg::ApiError(e),
|
||||
};
|
||||
let _ = tx2.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
AppMsg::PagesLoaded(pages) => {
|
||||
app.main.loading_pages = false;
|
||||
app.main.pages = pages;
|
||||
app.main.selected_page = 0;
|
||||
}
|
||||
AppMsg::ApiError(e) => {
|
||||
app.main.loading_spaces = false;
|
||||
app.main.loading_pages = false;
|
||||
app.main.error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
terminal.draw(|f| match app.state {
|
||||
AppState::Login => draw_login(f, &app.login),
|
||||
AppState::Main => draw_main(f, &app.main),
|
||||
})?;
|
||||
|
||||
if !event::poll(Duration::from_millis(50))? {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.state {
|
||||
AppState::Login => {
|
||||
if app.login.submitting {
|
||||
continue;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Tab => app.login.active_field = app.login.active_field.next(),
|
||||
KeyCode::BackTab => app.login.active_field = app.login.active_field.prev(),
|
||||
KeyCode::Up => app.login.active_field = app.login.active_field.prev(),
|
||||
KeyCode::Down => app.login.active_field = app.login.active_field.next(),
|
||||
KeyCode::Enter => {
|
||||
if app.login.active_field != LoginField::Password {
|
||||
app.login.active_field = app.login.active_field.next();
|
||||
} else {
|
||||
app.login.error = None;
|
||||
app.login.submitting = true;
|
||||
let url = app.login.url.trim_end_matches('/').to_string();
|
||||
let email = app.login.email.clone();
|
||||
let password = app.login.password.clone();
|
||||
let tx2 = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let msg = match do_login(&url, &email, &password).await {
|
||||
Ok(token) => AppMsg::LoginSuccess { token, base_url: url },
|
||||
Err(e) => AppMsg::LoginError(e),
|
||||
};
|
||||
let _ = tx2.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.login.active_field_value_mut().pop();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' {
|
||||
return Ok(());
|
||||
}
|
||||
app.login.active_field_value_mut().push(c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
AppState::Main => match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
return Ok(())
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
app.main.focus = match app.main.focus {
|
||||
Panel::Spaces => Panel::Pages,
|
||||
Panel::Pages => Panel::Spaces,
|
||||
};
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => match app.main.focus {
|
||||
Panel::Spaces => {
|
||||
if app.main.selected_space + 1 < app.main.spaces.len() {
|
||||
app.main.selected_space += 1;
|
||||
spawn_fetch_pages(&app, &tx);
|
||||
}
|
||||
}
|
||||
Panel::Pages => {
|
||||
if app.main.selected_page + 1 < app.main.pages.len() {
|
||||
app.main.selected_page += 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
KeyCode::Up | KeyCode::Char('k') => match app.main.focus {
|
||||
Panel::Spaces => {
|
||||
if app.main.selected_space > 0 {
|
||||
app.main.selected_space -= 1;
|
||||
spawn_fetch_pages(&app, &tx);
|
||||
}
|
||||
}
|
||||
Panel::Pages => {
|
||||
if app.main.selected_page > 0 {
|
||||
app.main.selected_page -= 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_fetch_pages(app: &App, tx: &mpsc::Sender<AppMsg>) {
|
||||
if let Some(space) = app.main.spaces.get(app.main.selected_space) {
|
||||
let base_url = app.base_url.clone();
|
||||
let token = app.token.clone();
|
||||
let space_id = space.id.clone();
|
||||
let tx2 = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let msg = match fetch_pages(&base_url, &token, &space_id).await {
|
||||
Ok(pages) => AppMsg::PagesLoaded(pages),
|
||||
Err(e) => AppMsg::ApiError(e),
|
||||
};
|
||||
let _ = tx2.send(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user