feat: initial TUI for administration #49
5 changed files with 117 additions and 29 deletions
|
|
@ -7,7 +7,16 @@ use super::state::{Mode, State};
|
|||
/// Event handling
|
||||
pub async fn handle_key(state: &mut State, key_event: KeyEvent) {
|
||||
match state.mode {
|
||||
Mode::List => match key_event.code {
|
||||
Mode::List => {
|
||||
if state.confirmation.is_some() {
|
||||
match key_event.code {
|
||||
KeyCode::Char('y') => state.proceed_confirmation().await,
|
||||
KeyCode::Char('n') | KeyCode::Esc => state.cancel_confirmation(),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match key_event.code {
|
||||
KeyCode::Char('d') => state.confirm_deletion(),
|
||||
KeyCode::Char('e') => state.mode = Mode::Edit,
|
||||
KeyCode::Home => state.selected_place.select_first(),
|
||||
KeyCode::End => state.selected_place.select_last(),
|
||||
|
|
@ -17,7 +26,9 @@ pub async fn handle_key(state: &mut State, key_event: KeyEvent) {
|
|||
KeyCode::Down | KeyCode::Char('j') => state.selected_place.select_next(),
|
||||
KeyCode::Esc | KeyCode::Char('q') => state.quit = true,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Edit => match (key_event.modifiers, key_event.code) {
|
||||
(KeyModifiers::NONE, KeyCode::Esc) => state.mode = Mode::List,
|
||||
(KeyModifiers::NONE, KeyCode::Tab) => {}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,6 @@ pub async fn tui(places_repository: DbPlacesRepository) -> Result<()> {
|
|||
Event::Tick => {
|
||||
state.fetch_places().await;
|
||||
}
|
||||
Event::SlowTick => {
|
||||
// state.cleanup_finished_tasks();
|
||||
}
|
||||
Event::Resize(_, h) => state.height = h,
|
||||
Event::Quit => state.quit = true,
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub struct State {
|
|||
places_repository: DbPlacesRepository,
|
||||
pub places: Vec<Place>,
|
||||
places_status: DataStatus,
|
||||
pub confirmation: Option<ConfirmationStatus>,
|
||||
pub selected_place: TableState,
|
||||
pub quit: bool,
|
||||
}
|
||||
|
|
@ -27,6 +28,12 @@ enum DataStatus {
|
|||
Old,
|
||||
}
|
||||
|
||||
pub enum ConfirmationStatus {
|
||||
Deletion(i64),
|
||||
#[expect(dead_code)]
|
||||
Save(i64),
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(places_repository: DbPlacesRepository, height: u16) -> Self {
|
||||
Self {
|
||||
|
|
@ -37,6 +44,7 @@ impl State {
|
|||
places_status: DataStatus::Old,
|
||||
places: vec![],
|
||||
selected_place: TableState::default(),
|
||||
confirmation: None,
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -74,4 +82,36 @@ impl State {
|
|||
}
|
||||
self.places_status = DataStatus::Fresh;
|
||||
}
|
||||
|
||||
pub fn confirm_deletion(&mut self) {
|
||||
if let Some(Some(id)) = self
|
||||
.selected_place
|
||||
.selected()
|
||||
.map(|index| self.places.get(index).map(|p| p.id))
|
||||
{
|
||||
self.confirmation = Some(ConfirmationStatus::Deletion(id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel_confirmation(&mut self) {
|
||||
self.confirmation = None;
|
||||
}
|
||||
|
||||
pub async fn proceed_confirmation(&mut self) {
|
||||
let Some(confirmation) = &self.confirmation else {
|
||||
return;
|
||||
};
|
||||
|
||||
match confirmation {
|
||||
ConfirmationStatus::Deletion(id) => {
|
||||
if let Err(err) = self.places_repository.delete_place(*id).await {
|
||||
tracing::error!("{err}");
|
||||
}
|
||||
}
|
||||
ConfirmationStatus::Save(_) => todo!(),
|
||||
}
|
||||
|
||||
self.confirmation = None;
|
||||
self.places_status = DataStatus::Old;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ pub enum Event {
|
|||
Closed,
|
||||
/// Triggers background actions
|
||||
Tick,
|
||||
/// Triggers less frequent background actions
|
||||
SlowTick,
|
||||
/// UI Render
|
||||
Render,
|
||||
FocusGained,
|
||||
|
|
@ -49,8 +47,7 @@ pub struct Tui {
|
|||
impl Tui {
|
||||
pub fn new() -> Result<Self> {
|
||||
let tick_rate = 4.0;
|
||||
let slow_tick_rate = 0.25;
|
||||
let frame_rate = 30.0;
|
||||
let frame_rate = 20.0;
|
||||
let terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
|
||||
let cancellation_token = CancellationToken::new();
|
||||
|
||||
|
|
@ -60,17 +57,14 @@ impl Tui {
|
|||
let cancellation_token = cancellation_token.clone();
|
||||
let sender = sender.clone();
|
||||
let tick_delay = std::time::Duration::from_secs_f64(1.0 / tick_rate);
|
||||
let slow_tick_delay = std::time::Duration::from_secs_f64(1.0 / slow_tick_rate);
|
||||
let render_delay = std::time::Duration::from_secs_f64(1.0 / frame_rate);
|
||||
tokio::spawn(async move {
|
||||
let mut reader = crossterm::event::EventStream::new();
|
||||
let mut tick_interval = tokio::time::interval(tick_delay);
|
||||
let mut slow_tick_interval = tokio::time::interval(slow_tick_delay);
|
||||
let mut render_interval = tokio::time::interval(render_delay);
|
||||
sender.send(Event::Init)?;
|
||||
loop {
|
||||
let tick_delay = tick_interval.tick();
|
||||
let slow_tick_delay = slow_tick_interval.tick();
|
||||
let render_delay = render_interval.tick();
|
||||
let crossterm_event = reader.next().fuse();
|
||||
tokio::select! {
|
||||
|
|
@ -104,9 +98,6 @@ impl Tui {
|
|||
_ = tick_delay => {
|
||||
sender.send(Event::Tick)?;
|
||||
},
|
||||
_ = slow_tick_delay => {
|
||||
sender.send(Event::SlowTick)?;
|
||||
},
|
||||
_ = render_delay => {
|
||||
sender.send(Event::Render)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
use itertools::Itertools;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::{Line, Span, ToSpan};
|
||||
use ratatui::widgets::{Paragraph, Row, Table};
|
||||
use ratatui::text::{Line, Span, Text, ToSpan};
|
||||
use ratatui::widgets::{Block, Clear, Padding, Paragraph, Row, Table};
|
||||
|
||||
use super::state::{Mode, State};
|
||||
use super::state::{ConfirmationStatus, Mode, State};
|
||||
|
||||
/// UI drawing
|
||||
pub fn ui_draw(state: &mut State, f: &mut Frame<'_>) {
|
||||
|
|
@ -54,7 +54,36 @@ fn main_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
|||
Mode::List => list_draw(state, f, area),
|
||||
Mode::Edit => edit_draw(state, f, area),
|
||||
}
|
||||
confirmation_dialog_draw(state, f, area);
|
||||
}
|
||||
|
||||
fn confirmation_dialog_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
||||
let Some(confirmation) = &state.confirmation else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dialog_area = center(area, Constraint::Percentage(80), Constraint::Percentage(60));
|
||||
let (action, id) = match confirmation {
|
||||
ConfirmationStatus::Deletion(id) => ("delete", id),
|
||||
ConfirmationStatus::Save(id) => ("save", id),
|
||||
};
|
||||
let confirmation_dialog = Paragraph::new(Text::from_iter([
|
||||
Line::from_iter([
|
||||
"Do you want to ".to_span(),
|
||||
action.to_span(),
|
||||
" place with id: ".to_span(),
|
||||
id.to_span(),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from("Y/N".to_span().bold()),
|
||||
]))
|
||||
.centered()
|
||||
.block(Block::bordered().padding(Padding::uniform(1)));
|
||||
|
||||
f.render_widget(Clear, dialog_area);
|
||||
f.render_widget(confirmation_dialog, dialog_area);
|
||||
}
|
||||
|
||||
fn list_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
||||
let places = state.places.iter().map(|p| {
|
||||
Row::new([
|
||||
|
|
@ -99,7 +128,18 @@ fn list_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
|||
f.render_stateful_widget(places_table, area, &mut state.selected_place);
|
||||
}
|
||||
|
||||
fn edit_draw(_state: &mut State, _f: &mut Frame<'_>, _area: Rect) {}
|
||||
fn edit_draw(_state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
||||
let dialog_area = center(area, Constraint::Percentage(80), Constraint::Percentage(60));
|
||||
let confirmation_dialog =
|
||||
Paragraph::new(Text::from_iter([
|
||||
Line::from("Not implemented yet :(").italic()
|
||||
]))
|
||||
.centered()
|
||||
.block(Block::bordered().padding(Padding::uniform(1)));
|
||||
|
||||
f.render_widget(Clear, dialog_area);
|
||||
f.render_widget(confirmation_dialog, dialog_area);
|
||||
}
|
||||
|
||||
#[expect(unstable_name_collisions)]
|
||||
fn footer_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
||||
|
|
@ -114,6 +154,7 @@ fn footer_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
|||
("PgUp", "Prev Page"),
|
||||
("PgDown", "Next Page"),
|
||||
("e", "Edit"),
|
||||
("d", "Delete"),
|
||||
]
|
||||
.map(|(key, action)| keybinding(key, action).to_vec())
|
||||
.into_iter()
|
||||
|
|
@ -145,3 +186,11 @@ fn keybinding(key: &'static str, action: &'static str) -> [Span<'static>; 5] {
|
|||
Span::styled(" ", black),
|
||||
]
|
||||
}
|
||||
|
||||
fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect {
|
||||
let [area] = Layout::horizontal([horizontal])
|
||||
.flex(Flex::Center)
|
||||
.areas(area);
|
||||
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
|
||||
area
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue