diff --git a/Cargo.lock b/Cargo.lock index 4f89671..f3ae8e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,7 +689,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2669,7 +2669,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.3", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2933,6 +2933,16 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "tui-input" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19" +dependencies = [ + "ratatui", + "unicode-width 0.2.0", +] + [[package]] name = "typenum" version = "1.18.0" @@ -3144,6 +3154,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "tui-input", "vergen-gix", "windows-sys 0.61.2", "xml", diff --git a/config/config.json5 b/config/config.json5 index d9ddca5..eb90ed6 100644 --- a/config/config.json5 +++ b/config/config.json5 @@ -215,7 +215,6 @@ "": "Suspend" }, "SetUserName": { - "": "Process", "": "PrevScreen", "": "Quit", "": "Quit", diff --git a/core/src/action.rs b/core/src/action.rs index 7675fd5..3559c2e 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -57,6 +57,7 @@ pub enum Action { Shutdown, // App (Win-Installer) FindWimNetwork, + SetUserName(String), // Screens DismissPopup, DisplayPopup(PopupType, String), diff --git a/win_installer/Cargo.toml b/win_installer/Cargo.toml index e801d4d..8dc0dd2 100644 --- a/win_installer/Cargo.toml +++ b/win_installer/Cargo.toml @@ -43,6 +43,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } tempfile = "3.23.0" windows-sys = { version = "0.61.1", features = ["Win32_NetworkManagement_WNet"] } xml = "1.1.0" +tui-input = "0.14.0" [build-dependencies] anyhow = "1.0.86" diff --git a/win_installer/src/app.rs b/win_installer/src/app.rs index 672ede3..0520d15 100644 --- a/win_installer/src/app.rs +++ b/win_installer/src/app.rs @@ -45,7 +45,11 @@ use ratatui::{ use tokio::sync::mpsc; use tracing::{debug, info}; -use crate::{components::wim_scan::WimScan, state::State}; +use crate::{ + components::{set_username::InputUsername, wim_scan::WimScan}, + state::State, + wim::WimImage, +}; pub struct App { // TUI @@ -81,6 +85,7 @@ impl App { Box::new(Left::new()), Box::new(Right::new()), Box::new(WimScan::new(wim_sources)), + Box::new(InputUsername::new()), Box::new(Footer::new()), Box::new(popup::Popup::new()), ], @@ -356,6 +361,10 @@ impl App { self.action_tx.send(build_right_items(self))?; self.action_tx.send(Action::Select(None, None))?; } + Action::SetUserName(ref name) => { + self.state.username = Some(name.clone()); + self.action_tx.send(Action::NextScreen)?; + } Action::TasksComplete => self.action_tx.send(Action::NextScreen)?, _ => {} } @@ -376,10 +385,10 @@ impl App { fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { - if let [header, _body, footer, center, left, right, popup] = + if let [header, _body, footer, center, left, right, username, popup] = get_chunks(frame.area())[..] { - let component_areas = vec![header, center, left, right, footer, popup]; + let component_areas = vec![header, center, left, right, username, footer, popup]; for (component, area) in zip(self.components.iter_mut(), component_areas) { if let Err(err) = component.draw(frame, area) { let _ = self @@ -464,7 +473,7 @@ fn build_left_items(app: &App) -> Action { .for_each(|wim_file| items.push(wim_file.path.clone())); } } - Mode::SelectWinImage => { + Mode::SelectWinImage | Mode::SetUserName => { select_type = SelectionType::One; title = String::from("Select Windows Image"); if let Ok(wim_sources) = app.state.wim_sources.lock() @@ -491,11 +500,6 @@ fn build_left_items(app: &App) -> Action { items.push(format!("{}", PartitionTableType::Guid)); items.push(format!("{}", PartitionTableType::Legacy)); } - Mode::SetUserName => { - select_type = SelectionType::Loop; - title = String::from("Customize"); - // TODO: FIXME - } Mode::Confirm => { select_type = SelectionType::Loop; title = String::from("Confirm Selections"); @@ -627,7 +631,8 @@ fn build_right_items(app: &App) -> Action { }); } } - Mode::SelectWinImage => { + Mode::SelectWinImage | Mode::SetUserName | Mode::Confirm => { + info!("Building right items for: {:?}", &app.cur_mode); let source; if let Ok(wim_sources) = app.state.wim_sources.lock() && let Some(index) = app.state.wim_file_index @@ -679,17 +684,45 @@ fn build_right_items(app: &App) -> Action { line_colors: vec![Color::Blue], }, ]); - labels.push(label_dv_lines); // WIM Info - source.images.iter().for_each(|image| { - items.push(vec![DVLine { - line_parts: vec![format!("{image}")], - line_colors: vec![Color::Reset], - }]) - }); + match app.cur_mode { + Mode::SelectWinImage => { + source.images.iter().for_each(|image| { + items.push(vec![DVLine { + line_parts: vec![format!("{image}")], + line_colors: vec![Color::Reset], + }]) + }); + } + Mode::Confirm => { + if let Some(index) = app.state.wim_image_index + && let Some(image) = source.images.get(index) + { + label_dv_lines.append(&mut vec![ + DVLine { + line_parts: vec![format!("{image}")], + line_colors: vec![Color::Reset], + }, + DVLine::blank(), + ]); + } + if let Some(username) = &app.state.username { + label_dv_lines.append(&mut vec![DVLine { + line_parts: vec![String::from("Username: "), username.clone()], + line_colors: vec![Color::Green, Color::Reset], + }]); + } + items.push(vec![DVLine::blank()]); + } + _ => {} + } + + // Done + info!("label_dv_lines: {:?}", &label_dv_lines); + labels.push(label_dv_lines); } - Mode::SelectTableType | Mode::SetUserName | Mode::Confirm => { + Mode::SelectTableType => { // Labels let dest_dv_line = DVLine { line_parts: vec![ @@ -777,6 +810,9 @@ fn get_chunks(r: Rect) -> Vec { // Center chunks.push(center); + // Set username + chunks.push(centered_rect(60, 20, r)); + // Popup chunks.push(centered_rect(60, 25, r)); diff --git a/win_installer/src/components.rs b/win_installer/src/components.rs index 51833fd..92f5847 100644 --- a/win_installer/src/components.rs +++ b/win_installer/src/components.rs @@ -13,4 +13,5 @@ // You should have received a copy of the GNU General Public License // along with Deja-Vu. If not, see . // +pub mod set_username; pub mod wim_scan; diff --git a/win_installer/src/components/set_username.rs b/win_installer/src/components/set_username.rs new file mode 100644 index 0000000..ff63778 --- /dev/null +++ b/win_installer/src/components/set_username.rs @@ -0,0 +1,135 @@ +// This file is part of Deja-Vu. +// +// Deja-Vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-Vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-Vu. If not, see . +// + +use color_eyre::Result; +use core::{action::Action, components::Component, config::Config, state::Mode, tui::Event}; +use crossterm::event::KeyCode; +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Clear, Paragraph}, +}; +use tokio::sync::mpsc::UnboundedSender; +use tui_input::{Input, InputRequest}; + +#[derive(Default)] +pub struct InputUsername { + command_tx: Option>, + config: Config, + input: Input, + mode: Mode, +} + +impl InputUsername { + #[must_use] + pub fn new() -> Self { + Self { + input: Input::new(String::from("")), + ..Default::default() + } + } +} + +impl Component for InputUsername { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } + + fn handle_events(&mut self, event: Option) -> Result> { + if self.mode != Mode::SetUserName { + return Ok(None); + } + let action = match event { + Some(Event::Key(key_event)) => match key_event.code { + KeyCode::Backspace => { + self.input.handle(InputRequest::DeletePrevChar); + None + } + KeyCode::Char(c) => { + let ok_chars: Vec = vec![' ', '-', '_']; + if c.is_ascii_alphanumeric() || ok_chars.contains(&c) { + self.input.handle(InputRequest::InsertChar(c)); + } + None + } + KeyCode::Enter => { + let username = self.input.value(); + Some(Action::SetUserName(String::from(username))) + } + KeyCode::Esc => Some(Action::SetMode(Mode::Home)), + _ => None, + }, + Some(Event::Mouse(_)) => None, + _ => None, + }; + Ok(action) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::SetMode(mode) => self.mode = mode.clone(), + _ => {} + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.mode != Mode::SetUserName { + // Bail early + return Ok(()); + } + + // Set areas + let [_, center_area, _] = Layout::horizontal([ + Constraint::Length(1), + Constraint::Min(1), + Constraint::Length(1), + ]) + .areas(area); + let [_, input_area, _] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + ]) + .areas(center_area); + + frame.render_widget(Clear, area); + let outer_block = Block::bordered().cyan().bold(); + frame.render_widget(outer_block, area); + + // Input Box + let width = input_area.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let scroll = self.input.visual_scroll(width as usize); + let input = Paragraph::new(self.input.value()) + .scroll((0, scroll as u16)) + .white() + .block(Block::new().borders(Borders::ALL).title("Enter Username")); + frame.render_widget(input, input_area); + + // Ratatui hides the cursor unless it's explicitly set. Position the cursor past the + // end of the input text and one line down from the border to the input line + let x = self.input.visual_cursor().max(scroll) - scroll + 1; + frame.set_cursor_position((input_area.x + x as u16, input_area.y + 1)); + + // Done + Ok(()) + } +} diff --git a/win_installer/src/state.rs b/win_installer/src/state.rs index d679d48..68d9ebd 100644 --- a/win_installer/src/state.rs +++ b/win_installer/src/state.rs @@ -44,6 +44,7 @@ pub struct State { pub driver: Option, pub driver_list: Vec, pub table_type: Option, + pub username: Option, pub wim_file_index: Option, pub wim_image_index: Option, pub wim_sources: Arc>,