From da4fba9203d33fce6e662446b1b1f8bdba2d4591 Mon Sep 17 00:00:00 2001 From: antifallobst Date: Sat, 23 Sep 2023 14:45:03 +0200 Subject: [PATCH] feat: started work on the main ui --- src/main.rs | 8 +-- src/ui/login.rs | 45 ++-------------- src/ui/master.rs | 132 +++++++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 55 ++++++++++++++++++-- 4 files changed, 190 insertions(+), 50 deletions(-) create mode 100644 src/ui/master.rs diff --git a/src/main.rs b/src/main.rs index 3484bd3..63134ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,8 +28,6 @@ async fn main() -> Result<()> { let args = Args::parse(); let base_url = args.base_url.unwrap(); - let stdout = ui::prepare()?; - let api = match if let Some(token) = args.token { match Api::from_token(&base_url, token).await? { Ok(a) => Some(a), @@ -42,12 +40,14 @@ async fn main() -> Result<()> { } { Some(a) => a, None => { - let mut login = ui::login::UI::new(stdout, base_url)?; + let mut login = ui::login::UI::new(ui::prepare()?, base_url)?; login.run().await? } }; - println!("{:#?}", api); + let mut ui = ui::master::UI::new(api, ui::prepare()?)?; + + ui.run().await?; Ok(()) } diff --git a/src/ui/login.rs b/src/ui/login.rs index 6751e72..cd04479 100644 --- a/src/ui/login.rs +++ b/src/ui/login.rs @@ -1,10 +1,6 @@ -use crate::api::Api; +use crate::{api::Api, ui, ui::TextAreaExt}; use anyhow::Result; -use crossterm::{ - event::{DisableMouseCapture, Event, KeyCode, KeyEvent}, - execute, - terminal::{disable_raw_mode, LeaveAlternateScreen}, -}; +use crossterm::event::{Event, KeyCode, KeyEvent}; use std::io::Stdout; use tui::{ backend::CrosstermBackend, @@ -16,33 +12,6 @@ use tui::{ }; use tui_textarea::TextArea; -trait TextAreaExt { - fn disable(&mut self); - fn enable(&mut self); -} - -impl TextAreaExt for TextArea<'_> { - fn disable(&mut self) { - self.set_cursor_line_style(Style::default()); - self.set_cursor_style(Style::default()); - let b = self - .block() - .cloned() - .unwrap_or_else(|| Block::default().borders(Borders::ALL)); - self.set_block(b.style(Style::default().fg(Color::DarkGray))); - } - - fn enable(&mut self) { - self.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED)); - self.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); - let b = self - .block() - .cloned() - .unwrap_or_else(|| Block::default().borders(Borders::ALL)); - self.set_block(b.style(Style::default())); - } -} - enum InputPosition { Username, Password, @@ -63,15 +32,7 @@ pub struct UI<'a> { impl Drop for UI<'_> { fn drop(&mut self) { - disable_raw_mode().expect("While destructing UI -> Failed to disable raw mode"); - execute!( - self.terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)"); - self.terminal - .show_cursor() - .expect("While destructing UI -> Failed to re-enable cursor"); + ui::restore(&mut self.terminal); } } diff --git a/src/ui/master.rs b/src/ui/master.rs new file mode 100644 index 0000000..0b1c9ac --- /dev/null +++ b/src/ui/master.rs @@ -0,0 +1,132 @@ +use crate::api::Api; +use anyhow::Result; +use crossterm::event::{Event, KeyCode, KeyEvent}; +use std::io::Stdout; +use tui::widgets::{Block, Borders, List, ListItem, ListState}; +use tui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + Terminal, +}; + +enum InputPosition { + Sidebar, + Content, +} + +pub struct UI<'a> { + terminal: Terminal>, + sidebar_items: Vec<&'a str>, + sidebar_state: ListState, + position: InputPosition, + + api: Api, +} + +impl UI<'_> { + pub fn new(api: Api, stdout: Stdout) -> Result { + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + + let sidebar_items = vec!["Overview", "Users", "Projects", "Server Status", "Backup"]; + let mut sidebar_state = ListState::default(); + sidebar_state.select(Some(0)); + + Ok(Self { + terminal, + sidebar_items, + sidebar_state, + position: InputPosition::Sidebar, + api, + }) + } + pub async fn run(&mut self) -> Result<()> { + loop { + self.draw().await?; + let event = crossterm::event::read()?; + + match event { + Event::Key(KeyEvent { + code: KeyCode::Esc, .. + }) => { + return Ok(()); + } + Event::Key(KeyEvent { + code: KeyCode::Tab, .. + }) => { + self.cycle(); + } + input => match self.position { + InputPosition::Sidebar => match input { + Event::Key(KeyEvent { + code: KeyCode::Up, .. + }) => { + if let Some(i) = self.sidebar_state.selected() { + if i > 0 { + self.sidebar_state.select(Some(i - 1)); + } + } + } + Event::Key(KeyEvent { + code: KeyCode::Down, + .. + }) => { + if let Some(i) = self.sidebar_state.selected() { + if i < self.sidebar_items.len() - 1 { + self.sidebar_state.select(Some(i + 1)); + } + } + } + _ => (), + }, + InputPosition::Content => (), + }, + } + } + } + + fn cycle(&mut self) { + self.position = match self.position { + InputPosition::Sidebar => InputPosition::Content, + InputPosition::Content => InputPosition::Sidebar, + } + } + + async fn draw(&mut self) -> Result<()> { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Length(32), // Sidebar + Constraint::Min(32), // Content + ] + .as_ref(), + ) + .split(self.terminal.size()?); + + let sidebar = List::new( + self.sidebar_items + .iter() + .map(|&x| ListItem::new(x)) + .collect::>(), + ) + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::White)) + .highlight_style( + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol("> "); + + let content_block = Block::default().borders(Borders::ALL); + + self.terminal.draw(|f| { + f.render_stateful_widget(sidebar, chunks[0], &mut self.sidebar_state); + f.render_widget(content_block, chunks[1]); + })?; + + Ok(()) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f9806c5..3705946 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,16 +1,63 @@ pub mod login; +pub mod master; -use std::{io, io::Stdout}; use anyhow::{Context, Result}; use crossterm::{ - event::EnableMouseCapture, + event::{DisableMouseCapture, EnableMouseCapture}, execute, - terminal::{enable_raw_mode, EnterAlternateScreen}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use std::{io, io::Stdout}; +use tui::{ + backend::CrosstermBackend, + style::{Color, Modifier, Style}, + widgets::{Block, Borders}, + Terminal, +}; +use tui_textarea::TextArea; + +pub trait TextAreaExt { + fn disable(&mut self); + fn enable(&mut self); +} + +impl TextAreaExt for TextArea<'_> { + fn disable(&mut self) { + self.set_cursor_line_style(Style::default()); + self.set_cursor_style(Style::default()); + let b = self + .block() + .cloned() + .unwrap_or_else(|| Block::default().borders(Borders::ALL)); + self.set_block(b.style(Style::default().fg(Color::DarkGray))); + } + + fn enable(&mut self) { + self.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED)); + self.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); + let b = self + .block() + .cloned() + .unwrap_or_else(|| Block::default().borders(Borders::ALL)); + self.set_block(b.style(Style::default())); + } +} pub fn prepare() -> Result { enable_raw_mode().context("Failed to enable raw mode")?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; Ok(stdout) -} \ No newline at end of file +} + +pub fn restore(terminal: &mut Terminal>) { + disable_raw_mode().expect("While destructing UI -> Failed to disable raw mode"); + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture, + ).expect("While destructing UI -> Failed execute backend commands (LeaveAlternateScreen and DisableMouseCapture)"); + terminal + .show_cursor() + .expect("While destructing UI -> Failed to re-enable cursor"); +}