Add username section

This commit is contained in:
2Shirt 2025-11-29 03:25:22 -08:00
parent 88dfe52b07
commit dd4733c991
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
8 changed files with 206 additions and 21 deletions

15
Cargo.lock generated
View file

@ -689,7 +689,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -2669,7 +2669,7 @@ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.2",
"once_cell", "once_cell",
"rustix 1.0.3", "rustix 1.0.3",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -2933,6 +2933,16 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" 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]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"
@ -3144,6 +3154,7 @@ dependencies = [
"tracing", "tracing",
"tracing-error", "tracing-error",
"tracing-subscriber", "tracing-subscriber",
"tui-input",
"vergen-gix", "vergen-gix",
"windows-sys 0.61.2", "windows-sys 0.61.2",
"xml", "xml",

View file

@ -215,7 +215,6 @@
"<Ctrl-z>": "Suspend" "<Ctrl-z>": "Suspend"
}, },
"SetUserName": { "SetUserName": {
"<Enter>": "Process",
"<Esc>": "PrevScreen", "<Esc>": "PrevScreen",
"<Ctrl-d>": "Quit", "<Ctrl-d>": "Quit",
"<Ctrl-c>": "Quit", "<Ctrl-c>": "Quit",

View file

@ -57,6 +57,7 @@ pub enum Action {
Shutdown, Shutdown,
// App (Win-Installer) // App (Win-Installer)
FindWimNetwork, FindWimNetwork,
SetUserName(String),
// Screens // Screens
DismissPopup, DismissPopup,
DisplayPopup(PopupType, String), DisplayPopup(PopupType, String),

View file

@ -43,6 +43,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
tempfile = "3.23.0" tempfile = "3.23.0"
windows-sys = { version = "0.61.1", features = ["Win32_NetworkManagement_WNet"] } windows-sys = { version = "0.61.1", features = ["Win32_NetworkManagement_WNet"] }
xml = "1.1.0" xml = "1.1.0"
tui-input = "0.14.0"
[build-dependencies] [build-dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"

View file

@ -45,7 +45,11 @@ use ratatui::{
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, info}; 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 { pub struct App {
// TUI // TUI
@ -81,6 +85,7 @@ impl App {
Box::new(Left::new()), Box::new(Left::new()),
Box::new(Right::new()), Box::new(Right::new()),
Box::new(WimScan::new(wim_sources)), Box::new(WimScan::new(wim_sources)),
Box::new(InputUsername::new()),
Box::new(Footer::new()), Box::new(Footer::new()),
Box::new(popup::Popup::new()), Box::new(popup::Popup::new()),
], ],
@ -356,6 +361,10 @@ impl App {
self.action_tx.send(build_right_items(self))?; self.action_tx.send(build_right_items(self))?;
self.action_tx.send(Action::Select(None, None))?; 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)?, Action::TasksComplete => self.action_tx.send(Action::NextScreen)?,
_ => {} _ => {}
} }
@ -376,10 +385,10 @@ impl App {
fn render(&mut self, tui: &mut Tui) -> Result<()> { fn render(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| { 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())[..] 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) { for (component, area) in zip(self.components.iter_mut(), component_areas) {
if let Err(err) = component.draw(frame, area) { if let Err(err) = component.draw(frame, area) {
let _ = self let _ = self
@ -464,7 +473,7 @@ fn build_left_items(app: &App) -> Action {
.for_each(|wim_file| items.push(wim_file.path.clone())); .for_each(|wim_file| items.push(wim_file.path.clone()));
} }
} }
Mode::SelectWinImage => { Mode::SelectWinImage | Mode::SetUserName => {
select_type = SelectionType::One; select_type = SelectionType::One;
title = String::from("Select Windows Image"); title = String::from("Select Windows Image");
if let Ok(wim_sources) = app.state.wim_sources.lock() 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::Guid));
items.push(format!("{}", PartitionTableType::Legacy)); items.push(format!("{}", PartitionTableType::Legacy));
} }
Mode::SetUserName => {
select_type = SelectionType::Loop;
title = String::from("Customize");
// TODO: FIXME
}
Mode::Confirm => { Mode::Confirm => {
select_type = SelectionType::Loop; select_type = SelectionType::Loop;
title = String::from("Confirm Selections"); 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; let source;
if let Ok(wim_sources) = app.state.wim_sources.lock() if let Ok(wim_sources) = app.state.wim_sources.lock()
&& let Some(index) = app.state.wim_file_index && let Some(index) = app.state.wim_file_index
@ -679,17 +684,45 @@ fn build_right_items(app: &App) -> Action {
line_colors: vec![Color::Blue], line_colors: vec![Color::Blue],
}, },
]); ]);
labels.push(label_dv_lines);
// WIM Info // WIM Info
source.images.iter().for_each(|image| { match app.cur_mode {
items.push(vec![DVLine { Mode::SelectWinImage => {
line_parts: vec![format!("{image}")], source.images.iter().for_each(|image| {
line_colors: vec![Color::Reset], 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 // Labels
let dest_dv_line = DVLine { let dest_dv_line = DVLine {
line_parts: vec![ line_parts: vec![
@ -777,6 +810,9 @@ fn get_chunks(r: Rect) -> Vec<Rect> {
// Center // Center
chunks.push(center); chunks.push(center);
// Set username
chunks.push(centered_rect(60, 20, r));
// Popup // Popup
chunks.push(centered_rect(60, 25, r)); chunks.push(centered_rect(60, 25, r));

View file

@ -13,4 +13,5 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>. // along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
// //
pub mod set_username;
pub mod wim_scan; pub mod wim_scan;

View file

@ -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 <https://www.gnu.org/licenses/>.
//
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<UnboundedSender<Action>>,
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<Action>) -> 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<Event>) -> Result<Option<Action>> {
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<char> = 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<Option<Action>> {
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(())
}
}

View file

@ -44,6 +44,7 @@ pub struct State {
pub driver: Option<drivers::Driver>, pub driver: Option<drivers::Driver>,
pub driver_list: Vec<drivers::Driver>, pub driver_list: Vec<drivers::Driver>,
pub table_type: Option<PartitionTableType>, pub table_type: Option<PartitionTableType>,
pub username: Option<String>,
pub wim_file_index: Option<usize>, pub wim_file_index: Option<usize>,
pub wim_image_index: Option<usize>, pub wim_image_index: Option<usize>,
pub wim_sources: Arc<Mutex<WimSources>>, pub wim_sources: Arc<Mutex<WimSources>>,