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",
"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",

View file

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

View file

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

View file

@ -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"

View file

@ -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<Rect> {
// Center
chunks.push(center);
// Set username
chunks.push(centered_rect(60, 20, r));
// Popup
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
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
//
pub mod set_username;
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_list: Vec<drivers::Driver>,
pub table_type: Option<PartitionTableType>,
pub username: Option<String>,
pub wim_file_index: Option<usize>,
pub wim_image_index: Option<usize>,
pub wim_sources: Arc<Mutex<WimSources>>,