commit 05b09b576429cb4153384db6b4fe870719e3b82e Author: Alex Page Date: Fri May 13 16:21:27 2022 -0400 Start with tui example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f368bd2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,267 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "archinstaller" +version = "0.1.0" +dependencies = [ + "crossterm", + "tui", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + +[[package]] +name = "libc" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mio" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "tui" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b6c5369 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "archinstaller" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +crossterm = "0.23.2" +tui = "0.18.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1e3dbcd --- /dev/null +++ b/src/main.rs @@ -0,0 +1,286 @@ +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Corner, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, List, ListItem, ListState}, + Frame, Terminal, +}; + +struct StatefulList { + state: ListState, + items: Vec, +} + +impl StatefulList { + fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, + } + } + + fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + fn unselect(&mut self) { + self.state.select(None); + } +} + +/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper +/// around `ListState`. Keeping track of the items state let us render the associated widget with its state +/// and have access to features such as natural scrolling. +/// +/// Check the event handling at the bottom to see how to change the state on incoming events. +/// Check the drawing logic for items on how to specify the highlighting style for selected items. +struct App<'a> { + items: StatefulList<(&'a str, usize)>, + events: Vec<(&'a str, &'a str)>, +} + +impl<'a> App<'a> { + fn new() -> App<'a> { + App { + items: StatefulList::with_items(vec![ + ("Item0", 1), + ("Item1", 2), + ("Item2", 1), + ("Item3", 3), + ("Item4", 1), + ("Item5", 4), + ("Item6", 1), + ("Item7", 3), + ("Item8", 1), + ("Item9", 6), + ("Item10", 1), + ("Item11", 3), + ("Item12", 1), + ("Item13", 2), + ("Item14", 1), + ("Item15", 1), + ("Item16", 4), + ("Item17", 1), + ("Item18", 5), + ("Item19", 4), + ("Item20", 1), + ("Item21", 2), + ("Item22", 1), + ("Item23", 3), + ("Item24", 1), + ]), + events: vec![ + ("Event1", "INFO"), + ("Event2", "INFO"), + ("Event3", "CRITICAL"), + ("Event4", "ERROR"), + ("Event5", "INFO"), + ("Event6", "INFO"), + ("Event7", "WARNING"), + ("Event8", "INFO"), + ("Event9", "INFO"), + ("Event10", "INFO"), + ("Event11", "CRITICAL"), + ("Event12", "INFO"), + ("Event13", "INFO"), + ("Event14", "INFO"), + ("Event15", "INFO"), + ("Event16", "INFO"), + ("Event17", "ERROR"), + ("Event18", "ERROR"), + ("Event19", "INFO"), + ("Event20", "INFO"), + ("Event21", "WARNING"), + ("Event22", "INFO"), + ("Event23", "INFO"), + ("Event24", "WARNING"), + ("Event25", "INFO"), + ("Event26", "INFO"), + ], + } + } + + /// Rotate through the event list. + /// This only exists to simulate some kind of "progress" + fn on_tick(&mut self) { + let event = self.events.remove(0); + self.events.push(event); + } +} + +fn main() -> Result<(), Box> { + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); + loop { + terminal.draw(|f| ui(f, &mut app))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Left => app.items.unselect(), + KeyCode::Down => app.items.next(), + KeyCode::Up => app.items.previous(), + _ => {} + } + } + } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } + } +} + +fn ui(f: &mut Frame, app: &mut App) { + // Create two chunks with equal horizontal screen space + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(f.size()); + + // Iterate through all elements in the `items` app and append some debug text to it. + let items: Vec = app + .items + .items + .iter() + .map(|i| { + let mut lines = vec![Spans::from(i.0)]; + for _ in 0..i.1 { + lines.push(Spans::from(Span::styled( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + Style::default().add_modifier(Modifier::ITALIC), + ))); + } + ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White)) + }) + .collect(); + + // Create a List from all list items and highlight the currently selected one + let items = List::new(items) + .block(Block::default().borders(Borders::ALL).title("List")) + .highlight_style( + Style::default() + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + // We can now render the item list + f.render_stateful_widget(items, chunks[0], &mut app.items.state); + + // Let's do the same for the events. + // The event list doesn't have any state and only displays the current state of the list. + let events: Vec = app + .events + .iter() + .rev() + .map(|&(event, level)| { + // Colorcode the level depending on its type + let s = match level { + "CRITICAL" => Style::default().fg(Color::Red), + "ERROR" => Style::default().fg(Color::Magenta), + "WARNING" => Style::default().fg(Color::Yellow), + "INFO" => Style::default().fg(Color::Blue), + _ => Style::default(), + }; + // Add a example datetime and apply proper spacing between them + let header = Spans::from(vec![ + Span::styled(format!("{:<9}", level), s), + Span::raw(" "), + Span::styled( + "2020-01-01 10:00:00", + Style::default().add_modifier(Modifier::ITALIC), + ), + ]); + // The event gets its own line + let log = Spans::from(vec![Span::raw(event)]); + + // Here several things happen: + // 1. Add a `---` spacing line above the final list entry + // 2. Add the Level + datetime + // 3. Add a spacer line + // 4. Add the actual event + ListItem::new(vec![ + Spans::from("-".repeat(chunks[1].width as usize)), + header, + Spans::from(""), + log, + ]) + }) + .collect(); + let events_list = List::new(events) + .block(Block::default().borders(Borders::ALL).title("List")) + .start_corner(Corner::BottomLeft); + f.render_widget(events_list, chunks[1]); +}