From 5484fed4d8cd7979d4530024626fb0ce4f436d00 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 11:07:16 +0100 Subject: [PATCH 01/17] firmware_selector: Began building a system for choosing between firmware vairants interactively --- src/firmware_selector.rs | 121 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 122 insertions(+) create mode 100644 src/firmware_selector.rs diff --git a/src/firmware_selector.rs b/src/firmware_selector.rs new file mode 100644 index 0000000..a3141a0 --- /dev/null +++ b/src/firmware_selector.rs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-FileCopyrightText: 2025 1BitSquared +// SPDX-FileContributor: Written by Rachel Mant + +use std::collections::BTreeMap; + +use color_eyre::eyre::eyre; +use color_eyre::eyre::Result; +use dialoguer::theme::ColorfulTheme; +use dialoguer::Select; + +use crate::metadata::structs::FirmwareDownload; + +pub struct FirmwareMultichoice<'a> +{ + state: State, + variants: Vec<&'a FirmwareDownload>, + friendly_names: Vec<&'a str>, +} + +#[derive(Default)] +enum State +{ + #[default] + PickFirmware, + PickAction(usize), + ShowDocs(usize), + FlashFirmware(usize), + Cancel, +} + +impl<'a> FirmwareMultichoice<'a> +{ + pub fn new(variants: &'a BTreeMap) -> Self + { + // Map the variant list to create selection items + let friendly_names: Vec<_> = variants + .iter() + .map(|(_, variant)| variant.friendly_name.as_str()) + .collect(); + + // Construct the new multi-choice object that will start in the default firmware selection state + Self { + state: State::default(), + variants: variants.values().collect(), + friendly_names + } + } + + /// Returns true if the FSM is finished and there are no further state transitions to go + pub fn complete(&self) -> bool + { + match self.state { + State::FlashFirmware(_) | State::Cancel => true, + _ => false, + } + } + + /// Step the FSM and perform the actions associated with that step + pub fn step(&mut self) -> Result<()> + { + self.state = match self.state { + State::PickFirmware => self.firmware_selection()?, + State::PickAction(index) => self.action_selection(index)?, + _ => todo!(), + }; + + Ok(()) + } + + /// Convert the FSM state into a firmware download selection + pub fn selection(&self) -> Option<&'a FirmwareDownload> + { + match self.state { + State::FlashFirmware(index) => Some(self.variants[index]), + _ => None, + } + } + + fn firmware_selection(&self) -> Result + { + // Figure out which one the user wishes to use + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Which firmware variant would you like to run on your probe?") + .items(self.friendly_names.as_slice()) + .interact_opt()?; + // Encode the result into a new FSM state + Ok(match selection { + Some(index) => State::PickAction(index), + None => State::Cancel, + }) + } + + fn action_selection(&self, index: usize) -> Result + { + // Convert from a friendly name index into the matching variant download + let friendly_name = self.friendly_names[index]; + let (index, _) = self.variants + .iter() + .enumerate() + .find(|(_, variant)| variant.friendly_name == friendly_name) + .unwrap(); // Can't fail anyway.. + + // Ask the user what they wish to do + let items = ["Flash to probe", "Show documentation", "Choose a different variant"]; + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("What action would you like to take with this firmware?") + .items(&items) + .interact_opt()?; + + Ok(match selection { + Some(item) => match item { + 0 => State::FlashFirmware(index), + 1 => State::ShowDocs(index), + 2 => State::PickFirmware, + _ => Err(eyre!("Impossible selection for action"))? + }, + None => State::Cancel, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3a7e42d..74dd64e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod bmp; pub mod error; pub mod elf; +pub mod firmware_selector; pub mod flasher; pub mod metadata; pub mod switcher; From 9aaae344a048472220d3f70a2ce5b459db9b5487 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 11:07:49 +0100 Subject: [PATCH 02/17] switcher: Make the firmware picking process use the new selection FSM to get the necessary interactivity --- src/switcher.rs | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/switcher.rs b/src/switcher.rs index 8229e55..bdee3f7 100644 --- a/src/switcher.rs +++ b/src/switcher.rs @@ -18,6 +18,7 @@ use log::error; use crate::bmp::BmpDevice; use crate::bmp::BmpMatcher; +use crate::firmware_selector::FirmwareMultichoice; use crate::flasher; use crate::metadata::download_metadata; use crate::metadata::structs::{Firmware, FirmwareDownload, Metadata, Probe}; @@ -205,30 +206,12 @@ fn pick_firmware(firmware: &Firmware) -> Result> } // Otherwise, if there's more than one we have to ask the user to make a choice _ => { - // Map the variant list to create selection items - let items: Vec<_> = firmware.variants - .iter() - .map(|(_, variant)| variant.friendly_name.as_str()) - .collect(); - - // Figure out which one the user wishes to use - let selection = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Which firmware variant would you like to run on your probe?") - .items(items.as_slice()) - .interact_opt()?; - // Extract and return that one, if the user didn't cancel selection - Ok( - selection - .map(|index| items[index]) - .and_then( - |friendly_name| { - firmware.variants - .iter() - .find(|(_, variant)| variant.friendly_name == friendly_name) - } - ) - .map(|(_, variant)| variant) - ) + // Enter the selection FSM to either extract a selection from the user, or cancellation + let mut chooser = FirmwareMultichoice::new(&firmware.variants); + while !chooser.complete() { + chooser.step()?; + } + Ok(chooser.selection()) } } } From 5b2c517a6ddc9bba8eaba2f2babff076900185a2 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 11:45:17 +0100 Subject: [PATCH 03/17] firmware_selector: Properly handle the FlashFirmware and Cancel states in the FSM step function --- src/firmware_selector.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/firmware_selector.rs b/src/firmware_selector.rs index a3141a0..1bec2b7 100644 --- a/src/firmware_selector.rs +++ b/src/firmware_selector.rs @@ -62,6 +62,10 @@ impl<'a> FirmwareMultichoice<'a> self.state = match self.state { State::PickFirmware => self.firmware_selection()?, State::PickAction(index) => self.action_selection(index)?, + // FlashFirmware and Cancel are both terminal actions whereby the FSM is done, + // so maintain homeostatis for them here. + State::FlashFirmware(index) => State::FlashFirmware(index), + State::Cancel => State::Cancel, _ => todo!(), }; From 755754f0bb531abfc9799c648d4949c0761bbfa1 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 12:12:04 +0100 Subject: [PATCH 04/17] firmware_selector: Begun building the documentation display system for the firmware to allow a user to read about and understand a release variant --- src/firmware_selector.rs | 46 ++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/firmware_selector.rs b/src/firmware_selector.rs index 1bec2b7..ad1f26a 100644 --- a/src/firmware_selector.rs +++ b/src/firmware_selector.rs @@ -3,9 +3,10 @@ // SPDX-FileContributor: Written by Rachel Mant use std::collections::BTreeMap; +use std::path::PathBuf; +use std::time::Duration; -use color_eyre::eyre::eyre; -use color_eyre::eyre::Result; +use color_eyre::eyre::{eyre, Result}; use dialoguer::theme::ColorfulTheme; use dialoguer::Select; @@ -24,7 +25,7 @@ enum State #[default] PickFirmware, PickAction(usize), - ShowDocs(usize), + ShowDocs(usize, usize), FlashFirmware(usize), Cancel, } @@ -62,11 +63,12 @@ impl<'a> FirmwareMultichoice<'a> self.state = match self.state { State::PickFirmware => self.firmware_selection()?, State::PickAction(index) => self.action_selection(index)?, + State::ShowDocs(name_index, variant_index) => + self.show_documentation(name_index, variant_index)?, // FlashFirmware and Cancel are both terminal actions whereby the FSM is done, // so maintain homeostatis for them here. State::FlashFirmware(index) => State::FlashFirmware(index), State::Cancel => State::Cancel, - _ => todo!(), }; Ok(()) @@ -95,11 +97,11 @@ impl<'a> FirmwareMultichoice<'a> }) } - fn action_selection(&self, index: usize) -> Result + fn action_selection(&self, name_index: usize) -> Result { // Convert from a friendly name index into the matching variant download - let friendly_name = self.friendly_names[index]; - let (index, _) = self.variants + let friendly_name = self.friendly_names[name_index]; + let (variant_index, _) = self.variants .iter() .enumerate() .find(|(_, variant)| variant.friendly_name == friendly_name) @@ -114,12 +116,38 @@ impl<'a> FirmwareMultichoice<'a> Ok(match selection { Some(item) => match item { - 0 => State::FlashFirmware(index), - 1 => State::ShowDocs(index), + 0 => State::FlashFirmware(variant_index), + 1 => State::ShowDocs(name_index, variant_index), 2 => State::PickFirmware, _ => Err(eyre!("Impossible selection for action"))? }, None => State::Cancel, }) } + + fn show_documentation(&self, name_index: usize, variant_index: usize) -> Result + { + // Extract which firmware download we're to work with + let variant = self.variants[variant_index]; + // Convert the path compoment of the download URI to a Path + let mut docs_path = PathBuf::from(variant.uri.path()); + // Replace the file extension from ".elf" to ".md" + docs_path.set_extension("md"); + // Convert back into a URI + let mut docs_uri = variant.uri.clone(); + docs_uri.set_path( + docs_path.to_str() + .expect("Something went terribly wrong building the documentation URI") + ); + + // Now try and download this documentation file + let client = reqwest::blocking::Client::new(); + let response = client.get(docs_uri) + // Use a 2 second timeout so we don't get stuck forever if the user is + // having connectivity problems - better to die early and have them retry + .timeout(Duration::from_secs(2)) + .send()?; + + Ok(State::PickAction(name_index)) + } } From 768978a835f748615ff8233c41cd62614a4b11c6 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 12:22:59 +0100 Subject: [PATCH 05/17] switcher: Make sure to grab and pass the selected release name through to the firmware selector FSM --- src/firmware_selector.rs | 4 +++- src/switcher.rs | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/firmware_selector.rs b/src/firmware_selector.rs index ad1f26a..e7e6534 100644 --- a/src/firmware_selector.rs +++ b/src/firmware_selector.rs @@ -15,6 +15,7 @@ use crate::metadata::structs::FirmwareDownload; pub struct FirmwareMultichoice<'a> { state: State, + release: &'a str, variants: Vec<&'a FirmwareDownload>, friendly_names: Vec<&'a str>, } @@ -32,7 +33,7 @@ enum State impl<'a> FirmwareMultichoice<'a> { - pub fn new(variants: &'a BTreeMap) -> Self + pub fn new(release: &'a str, variants: &'a BTreeMap) -> Self { // Map the variant list to create selection items let friendly_names: Vec<_> = variants @@ -43,6 +44,7 @@ impl<'a> FirmwareMultichoice<'a> // Construct the new multi-choice object that will start in the default firmware selection state Self { state: State::default(), + release, variants: variants.values().collect(), friendly_names } diff --git a/src/switcher.rs b/src/switcher.rs index bdee3f7..02ee5d7 100644 --- a/src/switcher.rs +++ b/src/switcher.rs @@ -56,7 +56,7 @@ pub fn switch_firmware(matches: &ArgMatches, paths: &ProjectDirs) -> Result<()> // Grab down the metadata index let metadata = download_metadata(cache)?; - let firmware = match pick_release(&metadata, &variant, &firmware_version)? { + let (release, firmware) = match pick_release(&metadata, &variant, &firmware_version)? { Some(firmware) => firmware, None => { println!("firmware release selection cancelled, stopping operation"); @@ -65,7 +65,7 @@ pub fn switch_firmware(matches: &ArgMatches, paths: &ProjectDirs) -> Result<()> }; // Now see which variant of the firmware the user wants to use - let firmware_variant = match pick_firmware(firmware)? { + let firmware_variant = match pick_firmware(release, firmware)? { Some(variant) => variant, None => { println!("firmware variant selection cancelled, stopping operation"); @@ -163,7 +163,7 @@ fn parse_firmware_identity(identity: &String) -> ProbeIdentity } fn pick_release<'a>(metadata: &'a Metadata, variant: &Probe, firmware_version: &String) -> - Result> + Result> { // Filter out releases that don't support this probe, and filter out the one the probe is currently running // if there is only a single variant in the release (multi-variant releases still need to be shown) @@ -190,10 +190,10 @@ fn pick_release<'a>(metadata: &'a Metadata, variant: &Probe, firmware_version: & Some(release) => release, None => return Ok(None), }; - Ok(Some(&metadata.releases[items[selection].as_str()].firmware[&variant])) + Ok(Some((items[selection].as_str(), &metadata.releases[items[selection].as_str()].firmware[&variant]))) } -fn pick_firmware(firmware: &Firmware) -> Result> +fn pick_firmware<'a>(release: &'a str, firmware: &'a Firmware) -> Result> { match firmware.variants.len() { // If there are now firmware variants for this release, that's an error @@ -207,7 +207,7 @@ fn pick_firmware(firmware: &Firmware) -> Result> // Otherwise, if there's more than one we have to ask the user to make a choice _ => { // Enter the selection FSM to either extract a selection from the user, or cancellation - let mut chooser = FirmwareMultichoice::new(&firmware.variants); + let mut chooser = FirmwareMultichoice::new(release, &firmware.variants); while !chooser.complete() { chooser.step()?; } From 96ed5176356b1ba068b56b80fae825f8376696f5 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 13:58:41 +0100 Subject: [PATCH 06/17] cargo: Added dependency on ratatui for the markdown docs viewer --- Cargo.lock | 639 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + 2 files changed, 635 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18cca14..c362e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +32,25 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ansi-to-tui" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67555e1f1ece39d737e28c8a017721287753af3f93225e4a445b29ccb0f5912c" +dependencies = [ + "nom 7.1.3", + "ratatui", + "simdutf8", + "smallvec", + "thiserror 1.0.69", +] + [[package]] name = "anstream" version = "0.6.18" @@ -104,7 +129,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -121,6 +146,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.66.1" @@ -196,6 +230,7 @@ dependencies = [ "libc", "log", "nusb", + "ratatui", "rc-zip-sync", "reqwest", "rustc_version", @@ -205,6 +240,7 @@ dependencies = [ "static_vcruntime", "termcolor", "thiserror 2.0.12", + "tui-markdown", "url", "wdi", "winapi", @@ -240,6 +276,21 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.19" @@ -321,7 +372,7 @@ dependencies = [ "clap_lex", "terminal_size", "unicase", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -363,6 +414,20 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "console" version = "0.15.11" @@ -372,7 +437,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -430,6 +495,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.0", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook 0.3.17", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -440,6 +530,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.100", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.100", +] + [[package]] name = "deelevate" version = "0.2.0" @@ -454,6 +579,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "dfu-core" version = "0.7.0" @@ -492,6 +626,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "diffy" version = "0.3.0" @@ -672,12 +812,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.8", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -773,6 +929,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -801,6 +963,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width 0.1.14", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -875,6 +1046,17 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1135,6 +1317,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1181,10 +1369,29 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", "web-time", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "io-kit-sys" version = "0.4.1" @@ -1218,6 +1425,24 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1310,6 +1535,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1328,12 +1559,31 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "mach2" version = "0.4.2" @@ -1376,6 +1626,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.3" @@ -1383,6 +1642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1434,6 +1694,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.3.3" @@ -1527,6 +1793,28 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1631,6 +1919,35 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathsearch" version = "0.2.0" @@ -1726,6 +2043,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plist" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -1752,6 +2082,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1767,6 +2103,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.32" @@ -1818,6 +2164,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.7" @@ -1947,6 +2321,27 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.0", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rc-zip" version = "5.3.1" @@ -1957,7 +2352,7 @@ dependencies = [ "chrono", "crc32fast", "encoding_rs", - "miniz_oxide", + "miniz_oxide 0.7.4", "num_enum", "oem_cp", "oval", @@ -1979,6 +2374,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -2030,6 +2434,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.15" @@ -2094,6 +2504,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rstest" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.100", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2202,6 +2642,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.27" @@ -2211,6 +2660,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "scroll" version = "0.12.0" @@ -2387,6 +2842,27 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook 0.3.17", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2396,6 +2872,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.1" @@ -2433,12 +2915,46 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "static_vcruntime" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "954e3e877803def9dc46075bf4060147c55cd70db97873077232eae0269dc89b" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.100", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2487,6 +3003,28 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "walkdir", + "yaml-rust", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -2584,7 +3122,7 @@ dependencies = [ "regex", "semver 0.11.0", "sha2 0.9.9", - "signal-hook", + "signal-hook 0.1.17", "terminfo", "termios", "thiserror 1.0.69", @@ -2644,6 +3182,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -2820,6 +3389,22 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-markdown" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf47229087fc49650d095a910a56aaf10c1c64181d042d2c2ba46fc3746ff534" +dependencies = [ + "ansi-to-tui", + "itertools 0.14.0", + "pretty_assertions", + "pulldown-cmark", + "ratatui", + "rstest", + "syntect", + "tracing", +] + [[package]] name = "typenum" version = "1.18.0" @@ -2850,6 +3435,23 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.0" @@ -2925,6 +3527,16 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3398,6 +4010,21 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 861ecda..26eafb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,8 @@ dialoguer = "0.11.0" directories = "6.0.0" sha2 = "0.10.8" color-eyre = "0.6.3" +tui-markdown = "0.3.3" +ratatui = "0.29.0" [target.'cfg(windows)'.dependencies] wdi = "0.1.0" From 1e8b67f622fafd41b12420e66341c6282d9694e4 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 13:59:06 +0100 Subject: [PATCH 07/17] docs_viewer: Begun building a Markdown-based docs viewer for the variant documentation --- src/docs_viewer.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 104 insertions(+) create mode 100644 src/docs_viewer.rs diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs new file mode 100644 index 0000000..e9478da --- /dev/null +++ b/src/docs_viewer.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-FileCopyrightText: 2025 1BitSquared +// SPDX-FileContributor: Written by Rachel Mant + +use color_eyre::eyre::Result; +use ratatui::buffer::Buffer; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use ratatui::layout::{Alignment, Rect}; +use ratatui::widgets::{Block, BorderType, Padding, Widget}; +use ratatui::DefaultTerminal; +use ratatui::Frame; + + +pub struct Viewer<'a> +{ + exit: bool, + title: &'a str, + docs: &'a str, +} + +impl<'a> Viewer<'a> +{ + pub fn display(title: &'a String, docs: &'a String) -> Result<()> + { + // Grab the console, putting it in TUI mode + let mut terminal = ratatui::init(); + // Turn the Markdown to display into a viewer + let mut viewer = Self::new(title, docs); + + // Run the viewer and wait to see what the user does + let result = viewer.run(&mut terminal); + // When they get done, put the console back and propagate any errors + ratatui::restore(); + result + } + + fn new(title: &'a String, docs: &'a String) -> Self + { + Self { + exit: false, + title: title.as_str(), + docs: docs.as_str(), + } + } + + fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> + { + while !self.exit { + terminal.draw(|frame| self.draw(frame))?; + self.handle_events()?; + } + Ok(()) + } + + fn draw(&mut self, frame: &mut Frame) + { + frame.render_widget(self, frame.area()) + } + + fn handle_events(&mut self) -> Result<()> + { + match event::read()? + { + Event::Key(key) => + { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q' | 'Q') => self.quit(), + _ => {}, + } + } + }, + _ => {}, + } + Ok(()) + } + + fn quit(&mut self) + { + self.exit = true + } +} + +impl Widget for &mut Viewer<'_> +{ + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized + { + let docs_text = tui_markdown::from_str(self.docs); + + // Build a bordered block for presentation + let block = Block::bordered() + .title(self.title) + .title_alignment(Alignment::Left) + .border_type(BorderType::Rounded) + .padding(Padding::horizontal(1)); + + // Render the contents of the block (the docs text), then the block itself + docs_text.render(block.inner(area), buf); + block.render(area, buf); + } +} diff --git a/src/lib.rs b/src/lib.rs index 74dd64e..7d06227 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ // SPDX-FileContributor: Modified by Rachel Mant pub mod bmp; +pub mod docs_viewer; pub mod error; pub mod elf; pub mod firmware_selector; From 4ac5c40581b1ba51c3bffcdc36a36299306d42b9 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 13:59:44 +0100 Subject: [PATCH 08/17] firmware_selector: Made use of the new viewer component to display documentation on a release variant --- src/firmware_selector.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/firmware_selector.rs b/src/firmware_selector.rs index e7e6534..0141d82 100644 --- a/src/firmware_selector.rs +++ b/src/firmware_selector.rs @@ -9,7 +9,9 @@ use std::time::Duration; use color_eyre::eyre::{eyre, Result}; use dialoguer::theme::ColorfulTheme; use dialoguer::Select; +use reqwest::StatusCode; +use crate::docs_viewer::Viewer; use crate::metadata::structs::FirmwareDownload; pub struct FirmwareMultichoice<'a> @@ -150,6 +152,14 @@ impl<'a> FirmwareMultichoice<'a> .timeout(Duration::from_secs(2)) .send()?; + match response.status() { + // XXX: Need to compute the release URI from the download URI and release name string + StatusCode::NOT_FOUND => println!("No documentation found, please go to <> to find out more"), + StatusCode::OK => Viewer::display(&variant.friendly_name, &response.text()?)?, + status => + Err(eyre!("Something went terribly wrong while grabbing the documentation to display: {}", status))? + }; + Ok(State::PickAction(name_index)) } } From e0e29e5023381887d7c1fe45b95a4dc6a5d0897c Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 14:10:19 +0100 Subject: [PATCH 09/17] docs_viewer: Implemented viewport size tracking --- src/docs_viewer.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index e9478da..dd7f73e 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -5,7 +5,7 @@ use color_eyre::eyre::Result; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use ratatui::layout::{Alignment, Rect}; +use ratatui::layout::{Alignment, Rect, Size}; use ratatui::widgets::{Block, BorderType, Padding, Widget}; use ratatui::DefaultTerminal; use ratatui::Frame; @@ -16,6 +16,8 @@ pub struct Viewer<'a> exit: bool, title: &'a str, docs: &'a str, + + viewport_size: Size, } impl<'a> Viewer<'a> @@ -25,7 +27,7 @@ impl<'a> Viewer<'a> // Grab the console, putting it in TUI mode let mut terminal = ratatui::init(); // Turn the Markdown to display into a viewer - let mut viewer = Self::new(title, docs); + let mut viewer = Self::new(title, docs, terminal.size()?); // Run the viewer and wait to see what the user does let result = viewer.run(&mut terminal); @@ -34,12 +36,13 @@ impl<'a> Viewer<'a> result } - fn new(title: &'a String, docs: &'a String) -> Self + fn new(title: &'a String, docs: &'a String, viewport_size: Size) -> Self { Self { exit: false, title: title.as_str(), docs: docs.as_str(), + viewport_size, } } @@ -70,6 +73,7 @@ impl<'a> Viewer<'a> } } }, + Event::Resize(width, height) => self.viewport_size = Size::new(width, height), _ => {}, } Ok(()) From c8333bd99587d49d575ec5f0af15c91cabe7ac0e Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 14:30:54 +0100 Subject: [PATCH 10/17] docs_viewer: Built out logic for displaying a scrollbar along side the documentation if necessary --- src/docs_viewer.rs | 49 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index dd7f73e..a937948 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -5,8 +5,9 @@ use color_eyre::eyre::Result; use ratatui::buffer::Buffer; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use ratatui::layout::{Alignment, Rect, Size}; -use ratatui::widgets::{Block, BorderType, Padding, Widget}; +use ratatui::layout::{Alignment, Margin, Rect, Size}; +use ratatui::symbols::scrollbar; +use ratatui::widgets::{Block, BorderType, Padding, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget}; use ratatui::DefaultTerminal; use ratatui::Frame; @@ -18,6 +19,9 @@ pub struct Viewer<'a> docs: &'a str, viewport_size: Size, + line_count: usize, + max_scroll: usize, + scroll_position: usize, } impl<'a> Viewer<'a> @@ -38,11 +42,18 @@ impl<'a> Viewer<'a> fn new(title: &'a String, docs: &'a String, viewport_size: Size) -> Self { + // Work out how any lines the documentation renders to + let line_count = tui_markdown::from_str(docs).lines.len(); + Self { exit: false, title: title.as_str(), docs: docs.as_str(), viewport_size, + line_count, + // Compute the maximum scrolling position for the scrollbar + max_scroll: line_count.saturating_sub(viewport_size.height.into()), + scroll_position: 0, } } @@ -73,7 +84,7 @@ impl<'a> Viewer<'a> } } }, - Event::Resize(width, height) => self.viewport_size = Size::new(width, height), + Event::Resize(width, height) => self.handle_resize(width, height), _ => {}, } Ok(()) @@ -83,6 +94,19 @@ impl<'a> Viewer<'a> { self.exit = true } + + fn handle_resize(&mut self, width: u16, height: u16) + { + // Grab the new viewport size and store that + self.viewport_size = Size::new(width, height); + // Figure out if the scroll position is still viable, and adjust it appropriately + let max_scroll = self.line_count.saturating_sub(height.into()); + if self.scroll_position > max_scroll { + self.scroll_position = max_scroll + } + // Update the max scroll position too + self.max_scroll = max_scroll; + } } impl Widget for &mut Viewer<'_> @@ -91,6 +115,7 @@ impl Widget for &mut Viewer<'_> where Self: Sized { + // Convert the documentation to display from Markdown let docs_text = tui_markdown::from_str(self.docs); // Build a bordered block for presentation @@ -103,5 +128,23 @@ impl Widget for &mut Viewer<'_> // Render the contents of the block (the docs text), then the block itself docs_text.render(block.inner(area), buf); block.render(area, buf); + + // Build the scrollbar state + let mut scroll_state = ScrollbarState::new(self.max_scroll) + .position(self.scroll_position); + // Build and render the scrollbar to track the content + StatefulWidget::render + ( + // Put the scrollbar on the right side, running down the text, and don't display + // the end arrows + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .symbols(scrollbar::VERTICAL) + .begin_symbol(None) + .end_symbol(None), + // Scrollbar should be displayed inside the side of the block, not overwriting the corners + area.inner(Margin::new(0, 1)), + buf, + &mut scroll_state + ); } } From c0cc5a00f1ac5e7680e0475c0d95caae654383be Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 14:34:37 +0100 Subject: [PATCH 11/17] docs_viewer: Implemented the key bindings for scrolling up and down in the documentation one line at a time --- src/docs_viewer.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index a937948..808449a 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -80,6 +80,8 @@ impl<'a> Viewer<'a> if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('q' | 'Q') => self.quit(), + KeyCode::Up => self.scroll_up(), + KeyCode::Down => self.scroll_down(), _ => {}, } } @@ -107,6 +109,22 @@ impl<'a> Viewer<'a> // Update the max scroll position too self.max_scroll = max_scroll; } + + fn scroll_up(&mut self) + { + // Scrolling up is easy.. just keep subtracting 1 until we reach 0 and keep it at 0 + self.scroll_position = self.scroll_position.saturating_sub(1) + } + + fn scroll_down(&mut self) + { + // Scrolling down is a bit harder - start by computing what the next scroll position should be + let new_position = self.scroll_position + 1; + // Now, if that does not exceed the actual max scroll position, we can update our scroll position + if new_position <= self.max_scroll { + self.scroll_position = new_position; + } + } } impl Widget for &mut Viewer<'_> From 7a20db43b3e48acfaaa3b99eeccd458b1d588370 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 15:00:49 +0100 Subject: [PATCH 12/17] docs_viewer: Implemented the key bindings for page scrolling up and down in the documentation --- src/docs_viewer.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index 808449a..c8b4b6e 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -82,6 +82,8 @@ impl<'a> Viewer<'a> KeyCode::Char('q' | 'Q') => self.quit(), KeyCode::Up => self.scroll_up(), KeyCode::Down => self.scroll_down(), + KeyCode::PageUp => self.scroll_page_up(), + KeyCode::PageDown => self.scroll_page_down(), _ => {}, } } @@ -125,6 +127,28 @@ impl<'a> Viewer<'a> self.scroll_position = new_position; } } + + fn scroll_page_up(&mut self) + { + // Scrolling up by a page also gets to use saturating subtraction so we can't scroll past the front + self.scroll_position = self.scroll_position.saturating_sub(self.viewport_size.height.into()) + } + + fn scroll_page_down(&mut self) + { + // Scrolling down by a page though needs extra handling too.. start by constructing + // what the new scroll position should be + let viewport_height: usize = self.viewport_size.height.into(); + let new_position = self.scroll_position + viewport_height; + // Now, if that new position exceeds the actual max scroll position, assign the max scroll + // position as the new scroll position + if new_position > self.max_scroll { + self.scroll_position = self.max_scroll; + } else { + // Otherwise, store the newly calculated position + self.scroll_position = new_position; + } + } } impl Widget for &mut Viewer<'_> @@ -151,8 +175,7 @@ impl Widget for &mut Viewer<'_> let mut scroll_state = ScrollbarState::new(self.max_scroll) .position(self.scroll_position); // Build and render the scrollbar to track the content - StatefulWidget::render - ( + StatefulWidget::render( // Put the scrollbar on the right side, running down the text, and don't display // the end arrows Scrollbar::new(ScrollbarOrientation::VerticalRight) From f0018e77ddd0f34ce2c86b413434220ba0f13839 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 17:08:22 +0100 Subject: [PATCH 13/17] docs_viewer: Cache the converted Markdown --- src/docs_viewer.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index c8b4b6e..306bd38 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -7,16 +7,18 @@ use ratatui::buffer::Buffer; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::layout::{Alignment, Margin, Rect, Size}; use ratatui::symbols::scrollbar; -use ratatui::widgets::{Block, BorderType, Padding, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget}; +use ratatui::text::Text; +use ratatui::widgets::{ + Block, BorderType, Padding, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget +}; use ratatui::DefaultTerminal; use ratatui::Frame; - pub struct Viewer<'a> { exit: bool, title: &'a str, - docs: &'a str, + docs: Text<'a>, viewport_size: Size, line_count: usize, @@ -42,13 +44,15 @@ impl<'a> Viewer<'a> fn new(title: &'a String, docs: &'a String, viewport_size: Size) -> Self { + // Convert the documentation to display from Markdown + let docs = tui_markdown::from_str(docs); // Work out how any lines the documentation renders to - let line_count = tui_markdown::from_str(docs).lines.len(); + let line_count = docs.height(); Self { exit: false, title: title.as_str(), - docs: docs.as_str(), + docs, viewport_size, line_count, // Compute the maximum scrolling position for the scrollbar @@ -157,9 +161,6 @@ impl Widget for &mut Viewer<'_> where Self: Sized { - // Convert the documentation to display from Markdown - let docs_text = tui_markdown::from_str(self.docs); - // Build a bordered block for presentation let block = Block::bordered() .title(self.title) @@ -168,7 +169,7 @@ impl Widget for &mut Viewer<'_> .padding(Padding::horizontal(1)); // Render the contents of the block (the docs text), then the block itself - docs_text.render(block.inner(area), buf); + self.docs.clone().render(block.inner(area), buf); block.render(area, buf); // Build the scrollbar state From df7116e54de7440a3ad135439d80bb64210fd55f Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 17:08:37 +0100 Subject: [PATCH 14/17] docs_viewer: Made the docs render scroll with the scroll bar --- src/docs_viewer.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index 306bd38..12d9148 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -9,7 +9,8 @@ use ratatui::layout::{Alignment, Margin, Rect, Size}; use ratatui::symbols::scrollbar; use ratatui::text::Text; use ratatui::widgets::{ - Block, BorderType, Padding, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget + Block, BorderType, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, + Widget }; use ratatui::DefaultTerminal; use ratatui::Frame; @@ -161,16 +162,18 @@ impl Widget for &mut Viewer<'_> where Self: Sized { - // Build a bordered block for presentation - let block = Block::bordered() - .title(self.title) - .title_alignment(Alignment::Left) - .border_type(BorderType::Rounded) - .padding(Padding::horizontal(1)); - // Render the contents of the block (the docs text), then the block itself - self.docs.clone().render(block.inner(area), buf); - block.render(area, buf); + Paragraph::new(self.docs.clone()) + .scroll((self.scroll_position as u16, 0)) + .block( + // Build a bordered block for presentation + Block::bordered() + .title(self.title) + .title_alignment(Alignment::Left) + .border_type(BorderType::Rounded) + .padding(Padding::horizontal(1)) + ) + .render(area, buf); // Build the scrollbar state let mut scroll_state = ScrollbarState::new(self.max_scroll) From 9af946bab20bce05767b2cac6fb2308d766cdd53 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 17:16:51 +0100 Subject: [PATCH 15/17] docs_viewer: Fix how we compute the number of lines the docs take to display so we handle word wrapping properly --- Cargo.toml | 2 +- src/docs_viewer.rs | 33 +++++++++++++++++---------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26eafb3..1b14876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ directories = "6.0.0" sha2 = "0.10.8" color-eyre = "0.6.3" tui-markdown = "0.3.3" -ratatui = "0.29.0" +ratatui = { version = "0.29.0", features = ["unstable-rendered-line-info"] } [target.'cfg(windows)'.dependencies] wdi = "0.1.0" diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index 12d9148..2e33d4f 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -7,10 +7,9 @@ use ratatui::buffer::Buffer; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::layout::{Alignment, Margin, Rect, Size}; use ratatui::symbols::scrollbar; -use ratatui::text::Text; use ratatui::widgets::{ Block, BorderType, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, - Widget + Widget, Wrap }; use ratatui::DefaultTerminal; use ratatui::Frame; @@ -18,8 +17,7 @@ use ratatui::Frame; pub struct Viewer<'a> { exit: bool, - title: &'a str, - docs: Text<'a>, + docs: Paragraph<'a>, viewport_size: Size, line_count: usize, @@ -46,13 +44,22 @@ impl<'a> Viewer<'a> fn new(title: &'a String, docs: &'a String, viewport_size: Size) -> Self { // Convert the documentation to display from Markdown - let docs = tui_markdown::from_str(docs); + let docs = Paragraph::new(tui_markdown::from_str(docs)) + // Do not trim indentation for word wrapping + .wrap(Wrap { trim: false }) + .block( + // Build a bordered block for presentation + Block::bordered() + .title(title.as_str()) + .title_alignment(Alignment::Left) + .border_type(BorderType::Rounded) + .padding(Padding::horizontal(1)) + ); // Work out how any lines the documentation renders to - let line_count = docs.height(); + let line_count = docs.line_count(viewport_size.width); Self { exit: false, - title: title.as_str(), docs, viewport_size, line_count, @@ -108,6 +115,8 @@ impl<'a> Viewer<'a> { // Grab the new viewport size and store that self.viewport_size = Size::new(width, height); + // Recompute the line count + self.line_count = self.docs.line_count(width); // Figure out if the scroll position is still viable, and adjust it appropriately let max_scroll = self.line_count.saturating_sub(height.into()); if self.scroll_position > max_scroll { @@ -163,16 +172,8 @@ impl Widget for &mut Viewer<'_> Self: Sized { // Render the contents of the block (the docs text), then the block itself - Paragraph::new(self.docs.clone()) + self.docs.clone() .scroll((self.scroll_position as u16, 0)) - .block( - // Build a bordered block for presentation - Block::bordered() - .title(self.title) - .title_alignment(Alignment::Left) - .border_type(BorderType::Rounded) - .padding(Padding::horizontal(1)) - ) .render(area, buf); // Build the scrollbar state From 35067eb99ff026eb71c450bf166c6e755e4bab14 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 21:02:01 +0100 Subject: [PATCH 16/17] docs_viewer: Added display of the key bindings in the viewer --- src/docs_viewer.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/docs_viewer.rs b/src/docs_viewer.rs index 2e33d4f..f514a46 100644 --- a/src/docs_viewer.rs +++ b/src/docs_viewer.rs @@ -7,6 +7,7 @@ use ratatui::buffer::Buffer; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::layout::{Alignment, Margin, Rect, Size}; use ratatui::symbols::scrollbar; +use ratatui::text::Text; use ratatui::widgets::{ Block, BorderType, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget, Wrap @@ -192,5 +193,10 @@ impl Widget for &mut Viewer<'_> buf, &mut scroll_state ); + + // Render the key bindings help + Text::from(" ⋏⋎: scroll, ⊼⊻: scroll page, q: quit to menu ") + .centered() + .render(area.rows().last().unwrap(), buf); } } From 4ef592a00c153fd0d3003c6d4eee77106197ba46 Mon Sep 17 00:00:00 2001 From: dragonmux Date: Sun, 27 Apr 2025 22:40:30 +0100 Subject: [PATCH 17/17] firmware_selector: Implemented generation of releases URIs from downloads URIs Not proud of this logic and it is likely quite brittle.. but it should work. --- src/firmware_selector.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/firmware_selector.rs b/src/firmware_selector.rs index 0141d82..7757cb7 100644 --- a/src/firmware_selector.rs +++ b/src/firmware_selector.rs @@ -10,6 +10,7 @@ use color_eyre::eyre::{eyre, Result}; use dialoguer::theme::ColorfulTheme; use dialoguer::Select; use reqwest::StatusCode; +use url::Url; use crate::docs_viewer::Viewer; use crate::metadata::structs::FirmwareDownload; @@ -129,6 +130,25 @@ impl<'a> FirmwareMultichoice<'a> }) } + fn compute_release_uri(&self, variant: &FirmwareDownload) -> Url + { + // Clone the download URI for this firmware variant and convert the path part into a Path + let mut uri = variant.uri.clone(); + let mut path = PathBuf::from(uri.path()); + // Find where the release tag component is in the path, stripping back to that + while path.components().count() != 0 && !path.ends_with(self.release) { + path.pop(); + } + // Now replace the preceeding `/download/` chunk with `/tag/` + path.pop(); + path.set_file_name("tag"); + path.push(self.release); + // Having completed that, replace the path component of the URI + uri.set_path(path.to_str().unwrap()); + // And now return the completed URI + uri + } + fn show_documentation(&self, name_index: usize, variant_index: usize) -> Result { // Extract which firmware download we're to work with @@ -154,7 +174,9 @@ impl<'a> FirmwareMultichoice<'a> match response.status() { // XXX: Need to compute the release URI from the download URI and release name string - StatusCode::NOT_FOUND => println!("No documentation found, please go to <> to find out more"), + StatusCode::NOT_FOUND => println!( + "No documentation found, please go to {} to find out more", self.compute_release_uri(variant) + ), StatusCode::OK => Viewer::display(&variant.friendly_name, &response.text()?)?, status => Err(eyre!("Something went terribly wrong while grabbing the documentation to display: {}", status))?