pax_global_header00006660000000000000000000000064145763270240014524gustar00rootroot0000000000000052 comment=3355931339a59d0567972eb8950f8b3e3e9fe9a0 ./mnt-reform-setup-wizard/000077500000000000000000000000001457632702400157475ustar00rootroot00000000000000./mnt-reform-setup-wizard/.cargo/000077500000000000000000000000001457632702400171205ustar00rootroot00000000000000./mnt-reform-setup-wizard/.cargo/config.toml000066400000000000000000000001051457632702400212560ustar00rootroot00000000000000[target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" ./mnt-reform-setup-wizard/.gitignore000066400000000000000000000000101457632702400177260ustar00rootroot00000000000000/target ./mnt-reform-setup-wizard/.gitlab-ci.yml000066400000000000000000000013441457632702400204050ustar00rootroot00000000000000image: debian:testing-slim build-cross: stage: build script: | dpkg --add-architecture arm64 apt-get update -y apt-get install -y curl curl https://sh.rustup.rs -sSf | sh -s -- -y source "$HOME/.cargo/env" rustup target add aarch64-unknown-linux-gnu apt-get install -y crossbuild-essential-arm64 libgtk-4-1:arm64 libadwaita-1-0:arm64 libgtk-4-dev:arm64 libadwaita-1-dev:arm64 upx export PKG_CONFIG_SYSROOT_DIR=/ export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/ cargo build --release --target aarch64-unknown-linux-gnu upx target/aarch64-unknown-linux-gnu/release/reform-setup artifacts: when: always paths: - target/aarch64-unknown-linux-gnu/release/reform-setup ./mnt-reform-setup-wizard/Cargo.toml000066400000000000000000000006521457632702400177020ustar00rootroot00000000000000[package] name = "reform-setup" version = "0.1.0" edition = "2021" [profile.release] strip = true # Automatically strip symbols from the binary. opt-level = "z" # Optimize for size. lto = true [dependencies] adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] } gtk = { version = "0.8.0", package = "gtk4", features = ["v4_12"] } gdk-pixbuf = "0.19.2" regex = "1.10.3" quick-xml = "0.31.0" pwhash = "1" ./mnt-reform-setup-wizard/cleanup.sh000077500000000000000000000005001457632702400177300ustar00rootroot00000000000000#!/bin/sh # This script is run after the setup wizard has completed successfully. # 1. Remove autostart of reform-setup rm /etc/profile.d/reform-setup # 2. Remove root autologin rm /etc/systemd/system/getty@tty1.service.d/override.conf # 3. Enable and start greetd systemctl enable greetd systemctl start greetd ./mnt-reform-setup-wizard/gfx/000077500000000000000000000000001457632702400165335ustar00rootroot00000000000000./mnt-reform-setup-wizard/gfx/setup-wizard-sway.svg000066400000000000000000000166651457632702400227110ustar00rootroot00000000000000 reform@reform:~$ reform@reform: ~ reform@reform:~$ reform@reform: ~ reform@reform:~$ reform@reform: ~ ./mnt-reform-setup-wizard/gfx/setup-wizard-wayfire.svg000066400000000000000000000311141457632702400233560ustar00rootroot00000000000000 ./mnt-reform-setup-wizard/src/000077500000000000000000000000001457632702400165365ustar00rootroot00000000000000./mnt-reform-setup-wizard/src/main.rs000066400000000000000000000633551457632702400200440ustar00rootroot00000000000000use adw::prelude::*; use gtk::{glib, ApplicationWindow, Stack, StackSidebar, StackPage, Box, Button, Label, DropDown, StringList, Entry, PasswordEntry, CheckButton, Image}; use adw::{Application, NavigationSplitView, NavigationPage, HeaderBar, ToolbarView, StatusPage}; use gtk::gdk; use crate::glib::clone; use std::process::Command; use regex::Regex; use quick_xml::reader::Reader as XmlReader; use quick_xml::events::Event as XmlEvent; use std::collections::HashMap; use std::io::Write; use pwhash::bcrypt; use std::cell::RefCell; use std::rc::Rc; use std::io::BufRead; const APP_ID: &str = "com.mntre.reform-setup"; const MARGIN: i32 = 48; const PATH_ETC_DEFAULT_KEYBOARD: &str = "/etc/default/keyboard"; const TEMPLATE_ETC_DEFAULT_KEYBOARD: &str = r#"# KEYBOARD CONFIGURATION FILE # Consult the keyboard(5) manual page. XKBMODEL="pc105" XKBLAYOUT="${LAYOUT}" XKBVARIANT="${VARIANT}" XKBOPTIONS="" BACKSPACE="guess" "#; const PATH_ETC_HOSTNAME: &str = "/etc/hostname"; const PATH_ETC_HOSTS: &str = "/etc/hosts"; const TEMPLATE_ETC_HOSTS: &str = r#"127.0.0.1 localhost ${HOSTNAME} ::1 localhost ip6-localhost ip6-loopback ${HOSTNAME} "#; const DESKTOP_BINARIES : [&str; 2] = [ "sway", "wayfire" ]; const PATH_ETC_GREETD_CONFIG: &str = "/etc/greetd/config.toml"; const TEMPLATE_ETC_GREETD_CONFIG: &str = r#"[terminal] # The VT to run the greeter on. Can be "next", "current" or a number # designating the VT. vt = 7 # The default session, also known as the greeter. [default_session] command = "/usr/bin/tuigreet --window-padding 4 --remember --asterisks --cmd /usr/bin/${DESKTOP}" # The user to run the command as. The privileges this user must have depends # on the greeter. A graphical greeter may for example require the user to be # in the `video` group. user = "_greetd" "#; const PATH_SKEL_SWAY_INPUT : &str = "/etc/skel/.config/sway/config.d/input"; const TEMPLATE_SKEL_SWAY_INPUT: &str = r#"# change to de if you have a QWERTZ keyboard, for example input * { xkb_layout ${LAYOUT} xkb_variant ${VARIANT} xkb_options lv3:ralt_switch } "#; const PATH_SKEL_WAYFIRE_INI: &str = "/etc/skel/.config/wayfire.ini"; const PATH_CLEANUP_SCRIPTS: &str = "/usr/share/reform-setup-wizard/cleanup.d"; // see also: // - https://martinber.github.io/gtk-rs.github.io/docs-src/tutorial/closures // - https://docs.rs/libadwaita/latest/libadwaita // - https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/ // - https://kerkour.com/rust-cross-compilation fn valid_password(pw: &str) -> bool { return pw.len() > 0 } fn valid_hostname(hostname: &str) -> bool { let regex = Regex::new(r"^[a-z0-9][a-z0-9-]*$").unwrap(); return hostname.len() > 1 && hostname.len() < 64 && regex.is_match(hostname); } fn valid_username(name: &str) -> bool { // as per https://systemd.io/USER_NAMES/#common-core return Regex::new(r"^[a-z][a-z0-9-]{0,30}$").unwrap().is_match(name) } fn run_command(description: &str, executable: &str, params: Vec<&str>) -> Result { println!("STEP: {}", &description); return match Command::new(&executable).args(¶ms).output() { Ok(output) => { let stderr = match String::from_utf8(output.stderr.clone()) { Ok(s) => s, _ => return Err(format!("Error: {}: {} {:?}: failed to parse utf8 output: {:?}", description, executable, params, &output.stderr)) }; let stdout = match String::from_utf8(output.stdout.clone()) { Ok(s) => s, _ => return Err(format!("Error: {}: {} {:?}: failed to parse utf8 output: {:?}", description, executable, params, &output.stdout)) }; println!("STATUS: {:?}", output.status.code()); println!("STDOUT: {}", &stdout); println!("STDERR: {}", &stderr); match output.status.code() { Some(c) if c == 0 => Ok("".to_string()), _ => Err(format!("Error: {}: {} {:?}: {}", description, executable, params, &stderr)) } }, Err(e) => Err(format!("Error: {}: {} {:?}: {}", description, executable, params, e)) }; } fn set_password(description: &str, pw: &str, username: &str) -> Result<(), String> { let digest = match bcrypt::hash(&pw) { Ok(v) => v.to_string(), Err(e) => return Err(format!("Error: Password not hashable: {}", e).to_string()) }; run_command(description, "usermod", ["--password", &digest as &str, username].to_vec())?; Ok(()) } fn write_file(path: &str, content: &str) -> Result<(),String> { match std::path::Path::new(path).parent() { Some(p) => match std::fs::create_dir_all(p) { Ok(_) => (), Err(e) => return Err(format!("Trouble ensuring {}: {}", path, e)) }, _ => () }; match std::fs::File::create(path) { Ok(mut f) => { match f.write_all(content.as_bytes()) { Ok(_) => Ok(println!("{} successfully written.", path)), Err(e) => Err(format!("Trouble writing {}: {}", path, e)) } }, Err(e) => Err(format!("Trouble opening {} for writing: {}", path, e)) } } fn main() -> glib::ExitCode { let app = Application::builder().application_id(APP_ID).build(); app.connect_activate(build_ui); app.run() } fn build_box() -> Box { return Box::builder() .orientation(gtk::Orientation::Vertical) .margin_top(MARGIN) .margin_bottom(MARGIN) .margin_start(MARGIN) .margin_end(MARGIN) .spacing(MARGIN/2) .valign(gtk::Align::Center) .build(); } fn build_welcome_page(stack: &Stack) -> StackPage { let button_next = Button::builder().label("Next").build(); let page_box = build_box(); let status = StatusPage::builder() .title("Welcome to MNT Reform") .build(); status.set_vexpand(true); page_box.append(&status); page_box.append(&button_next); button_next.connect_clicked( clone!(@weak stack => move |_| { stack.set_visible_child_full("keyboard", gtk::StackTransitionType::SlideLeft); }) ); return stack.add_titled(&page_box, Some("welcome"), "Welcome"); } #[derive(Clone)] struct XkbVariant { layout: String, variant: String, } fn parse_keyboard_layout_file(file_path: &str) -> HashMap { let mut layouts = HashMap::::new(); let mut reader = XmlReader::from_file(file_path).unwrap(); let mut in_layout_list = false; let mut in_config_item = false; let mut in_name = false; let mut in_description = false; let mut in_variant = false; let mut variant = String::new(); let mut layout = String::new(); let mut description = String::new(); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf) { Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), Ok(XmlEvent::Eof) => break, Ok(XmlEvent::Start(e)) => { match e.name().as_ref() { b"layoutList" => in_layout_list = true, b"configItem" => in_config_item = true, b"name" => in_name = true, b"description" => in_description = true, b"variant" => in_variant = true, _ => () } }, Ok(XmlEvent::End(e)) => { match e.name().as_ref() { b"layoutList" => in_layout_list = false, b"layout" => { layout = "".to_string(); }, b"configItem" => { description = "".to_string(); variant = "".to_string(); in_config_item = false; }, b"name" => in_name = false, b"description" => in_description = false, b"variant" => in_variant = false, _ => () }; }, Ok(XmlEvent::Text(e)) if in_layout_list && in_config_item => { let val = e.unescape().unwrap().clone(); if in_name { if in_variant { variant = val.to_string(); } else { layout = val.to_string(); } } else if in_description { description = val.to_string(); } if !layout.is_empty() && !description.is_empty() { layouts.entry(description.to_string()).or_insert(XkbVariant { layout: layout.to_string(), variant: variant.to_string() }); } }, _ => () } buf.clear(); } return layouts; } fn build_keyboard_page(stack: &Stack) -> StackPage { let button_next = Button::builder().label("Next").build(); let page_box = build_box(); let mut layouts_map = parse_keyboard_layout_file("/usr/share/X11/xkb/rules/base.xml"); layouts_map.extend(parse_keyboard_layout_file("/usr/share/X11/xkb/rules/base.extras.xml")); layouts_map.retain(|_, v| v.layout != "custom"); let mut dropdown_pairs: Vec<(String, XkbVariant)> = layouts_map.into_iter().collect(); dropdown_pairs.sort_by(|a, b| a.0.cmp(&b.0)); let layout_keys: StringList = dropdown_pairs.iter().map(|(k, _)| k.clone()).collect(); let layout_vals: Vec = dropdown_pairs.iter().map(|(_, v)| v.clone()).collect(); let dropdown = DropDown::builder() .model(&layout_keys) .enable_search(true) .build(); let search_exp = gtk::PropertyExpression::new(gtk::StringObject::static_type(), None::, "string"); dropdown.set_expression(Some(search_exp)); dropdown.set_search_match_mode(gtk::StringFilterMatchMode::Substring); let default_index = layout_vals.iter().position(|v| v.layout == "us" && v.variant == "").unwrap(); dropdown.set_selected(default_index as u32); page_box.append(&Label::new(Some("Please select your keyboard layout:"))); page_box.append(&dropdown); let error_label = Label::new(None); page_box.append(&error_label); page_box.append(&button_next); button_next.connect_clicked( clone!(@weak stack => move |_| { // TODO: find out to what degree this actually sets the keyboard layout let layout: &XkbVariant = &layout_vals[dropdown.selected() as usize]; println!("Selected keyboard layout: {:?} variant: {:?}", &layout.layout, &layout.variant); let new_default = TEMPLATE_ETC_DEFAULT_KEYBOARD.replace("${LAYOUT}", &layout.layout).replace("${VARIANT}", &layout.variant); match write_file(PATH_ETC_DEFAULT_KEYBOARD, &new_default) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return; } } let new_sway_input = TEMPLATE_SKEL_SWAY_INPUT.replace("${LAYOUT}", &layout.layout).replace("${VARIANT}", &layout.variant); match write_file(PATH_SKEL_SWAY_INPUT, &new_sway_input) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return; } } match run_command("write xkb_layout into wayfire.ini", "sh", ["-c", &format!("sed -i -E 's/^xkb_layout ?=.*$/xkb_layout = {}/g' {}", &layout.layout, PATH_SKEL_WAYFIRE_INI)].to_vec()) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return } }; match run_command("write xkb_variant into wayfire.ini", "sh", ["-c", &format!("sed -i -E 's/^xkb_variant ?=.*$/xkb_variant = {}/g' {}", &layout.variant, PATH_SKEL_WAYFIRE_INI)].to_vec()) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return } }; stack.set_visible_child_full("time", gtk::StackTransitionType::SlideLeft); }) ); return stack.add_titled(&page_box, Some("keyboard"), "Keyboard Layout"); } fn build_time_page(stack: &Stack) -> StackPage { let button_next = Button::builder().label("Next").build(); let page_box = build_box(); // let output = String::from_utf8(Command::new("timedatectl") // .args(["list-timezones"]) // .output() // .expect("failed to execute timedatectl").stdout) // .expect("failed to parse utf8 output of timedatectl"); // NB: above wouldn't run easily within chroot, so for now let's use this let output = String::from_utf8(Command::new("sh") .args(["-c", "awk '/^Z/ { print $2 }; /^L/ { print $3 }' /usr/share/zoneinfo/tzdata.zi | sort"]) .output() .expect("failed to collect timezones").stdout) .expect("failed to parse utf8 output of awk"); let lines:Vec<&str> = output.lines().collect(); let list = StringList::new(&lines); let dropdown = DropDown::builder() .model(&list) .enable_search(true) .build(); let search_exp = gtk::PropertyExpression::new(gtk::StringObject::static_type(), None::, "string"); dropdown.set_expression(Some(search_exp)); dropdown.set_search_match_mode(gtk::StringFilterMatchMode::Substring); let default_index = lines.iter().position(|v| v == &"Europe/Berlin").unwrap(); dropdown.set_selected(default_index as u32); page_box.append(&Label::new(Some("Please choose your timezone:"))); page_box.append(&dropdown); let error_label = Label::new(None); page_box.append(&error_label); page_box.append(&button_next); button_next.connect_clicked( clone!(@weak stack => move |_| { // in a non-chroot environment we might want to use timedatectl instead let tz = match list.string(dropdown.selected()) { Some(s) => s, _ => { error_label.set_label("no timezone in dropdown for selection"); return } }; match run_command("set timezone", "sh", ["-c", &format!("echo '{}' > /etc/timezone", tz)].to_vec()) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return } }; match run_command("set localtime", "ln", ["-sf", &format!("/usr/share/zoneinfo/{}", tz), "/etc/localtime"].to_vec()) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return } }; stack.set_visible_child_full("desktop", gtk::StackTransitionType::SlideLeft); }) ); return stack.add_titled(&page_box, Some("time"), "Time"); } fn image_from_bytes(bytes: &[u8]) -> Image { let loader = gdk_pixbuf::PixbufLoader::with_type("svg").unwrap(); loader.write(bytes).unwrap(); loader.close().unwrap(); let pixbuf = loader.pixbuf().unwrap(); return gtk::Image::from_paintable(Some(&gdk::Texture::for_pixbuf(&pixbuf))); } fn build_desktop_page(stack: &Stack, desktop_index: Rc>) -> StackPage { let button_next = Button::builder().label("Next").build(); let page_box = build_box(); let choice_sway = CheckButton::builder().label("Sway (Tiling)").build(); let choice_wayfire = CheckButton::builder().label("Wayfire (Traditional)").build(); choice_wayfire.set_group(Some(&choice_sway)); choice_wayfire.set_active(true); let sway_box = Box::builder() .orientation(gtk::Orientation::Horizontal) .valign(gtk::Align::Center) .build(); let img_sway = image_from_bytes(include_bytes!("../gfx/setup-wizard-sway.svg")); img_sway.set_size_request(392, 278); sway_box.append(&img_sway); sway_box.append(&Label::new(Some("Sway: A tiling desktop with a strong focus on keyboard shortcuts, for advanced users."))); choice_sway.set_child(Some(&sway_box)); let wayfire_box = Box::builder() .orientation(gtk::Orientation::Horizontal) .valign(gtk::Align::Center) .build(); let img_wayfire = image_from_bytes(include_bytes!("../gfx/setup-wizard-wayfire.svg")); img_wayfire.set_size_request(392, 278); wayfire_box.append(&img_wayfire); wayfire_box.append(&Label::new(Some("Wayfire: A more traditional desktop with overlapping windows, suitable for beginners."))); choice_wayfire.set_child(Some(&wayfire_box)); page_box.append(&Label::new(Some("Please select your preferred desktop:"))); page_box.append(&choice_wayfire); page_box.append(&choice_sway); let error_label = Label::new(None); page_box.append(&error_label); page_box.append(&button_next); button_next.connect_clicked( clone!(@weak stack => move |_| { let mut index = desktop_index.borrow_mut(); *index = choice_wayfire.is_active() as usize; stack.set_visible_child_full("root-password", gtk::StackTransitionType::SlideLeft); }) ); return stack.add_titled(&page_box, Some("desktop"), "Desktop"); } fn build_root_pw_page(stack: &Stack) -> StackPage { let button_next = Button::builder().label("Next").build(); let page_box = build_box(); let pw_entry = PasswordEntry::builder().build(); let pw_entry_rep = PasswordEntry::builder().build(); page_box.append(&Label::new(Some("Please choose a password for the root account:"))); page_box.append(&pw_entry); page_box.append(&Label::new(Some("And repeat it for safety:"))); page_box.append(&pw_entry_rep); let error_label = Label::new(None); page_box.append(&error_label); page_box.append(&button_next); button_next.connect_clicked( clone!(@weak stack => move |_| { let pw = pw_entry.text(); let pw_retry = pw_entry_rep.text(); if pw != pw_retry { error_label.set_label("Error: Passwords don't match."); return; } else if !valid_password(&pw) { error_label.set_label(&format!("Error: Password must have at least one character.")); return; } match set_password("set root pw", &pw, "root") { Ok(_) => (), Err(e) => { error_label.set_label(&format!("Error: Cannot set password: {}", e)); return } }; stack.set_visible_child_full("hostname", gtk::StackTransitionType::SlideLeft); }) ); return stack.add_titled(&page_box, Some("root-password"), "Root Password"); } fn build_hostname_page(stack: &Stack) -> StackPage { let button_next = Button::builder().label("Next").build(); let page_box = build_box(); let hostname_entry = Entry::builder().build(); page_box.append(&Label::new(Some("Please gives this computer a name (lower-case characters only):"))); page_box.append(&hostname_entry); let error_label = Label::new(None); page_box.append(&error_label); page_box.append(&button_next); button_next.connect_clicked( clone!(@weak stack => move |_| { let hostname = hostname_entry.text(); if !valid_hostname(&hostname) { error_label.set_label("Error: Invalid hostname format."); return; } match write_file(PATH_ETC_HOSTNAME, &hostname) { Ok(_) => (), Err(e) => { error_label.set_label(&format!("Error: Cannot write {}: {}", PATH_ETC_HOSTNAME, e)); return } }; let head_hosts = TEMPLATE_ETC_HOSTS.replace("${HOSTNAME}", &hostname); let hosts_file = match std::fs::File::open(PATH_ETC_HOSTS) { Ok(f) => f, Err(e) => { error_label.set_label(&format!("Error: Cannot write {}: {}", PATH_ETC_HOSTNAME, e)); return } }; let reader = std::io::BufReader::new(hosts_file); let tail_hosts = match reader.lines().skip(2). map(|result| result.map(|line| line + "\n")). collect::>() { Ok(s) => s, Err(e) => { error_label.set_label(&format!("Error: Cannot properly read {}: {}", PATH_ETC_HOSTNAME, e)); return } }; let to_join = [head_hosts, tail_hosts]; match write_file(PATH_ETC_HOSTS, &to_join.join("\n")) { Ok(_) => (), Err(e) => { error_label.set_label(&format!("Error: Cannot update {}: {}", PATH_ETC_HOSTS, e)); return } } stack.set_visible_child_full("account", gtk::StackTransitionType::SlideLeft); }) ); return stack.add_titled(&page_box, Some("hostname"), "Computer Name"); } fn build_account_page(stack: &Stack, desktop_index: Rc>, window: &ApplicationWindow) -> StackPage { let button_next = Button::builder().label("Next").build(); let page_box = build_box(); let username_entry = Entry::builder().build(); let pw_entry = PasswordEntry::builder().build(); let pw_entry_rep = PasswordEntry::builder().build(); page_box.append(&Label::new(Some("Please choose a username for your personal account:"))); page_box.append(&username_entry); page_box.append(&Label::new(Some("And a password for your account:"))); page_box.append(&pw_entry); page_box.append(&Label::new(Some("And repeat it for safety:"))); page_box.append(&pw_entry_rep); let error_label = Label::new(None); page_box.append(&error_label); page_box.append(&button_next); let cleanup_script = format!("for script in `ls -1 {}`; do \"{}/$script\"; done", PATH_CLEANUP_SCRIPTS, PATH_CLEANUP_SCRIPTS); button_next.connect_clicked( clone!(@weak window => move |_| { let username = username_entry.text(); if !valid_username(&username) { error_label.set_label("Error: Invalid username format."); return; } let pw = pw_entry.text(); let pw_retry = pw_entry_rep.text(); if pw != pw_retry { error_label.set_label("Error: Passwords don't match."); return; } else if !valid_password(&pw) { error_label.set_label("Error: Password must have at least one character."); return; } match run_command("adduser", "adduser", ["--disabled-password", "--comment", "", &username].to_vec()) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return } }; match run_command("add to sudoers", "usermod", ["-aG", "sudo", &username].to_vec()) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return } }; let greetd_config = TEMPLATE_ETC_GREETD_CONFIG.replace("${DESKTOP}", &DESKTOP_BINARIES[*desktop_index.borrow()]); match write_file(PATH_ETC_GREETD_CONFIG, &greetd_config) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return; } } match set_password("set user pw", &pw, &username) { Ok(_) => (), Err(e) => { error_label.set_label(&format!("Error: Cannot set password: {}", e)); return } }; match run_command("run cleanup scripts", "sh", ["-c", &cleanup_script].to_vec()) { Ok(_) => (), Err(e) => { error_label.set_label(&e); return } }; let _ = window.close(); }) ); return stack.add_titled(&page_box, Some("account"), "Account"); } fn build_ui(app: &Application) { let header_bar = HeaderBar::default(); let stack = Stack::builder().build(); let toolbar_view = ToolbarView::builder() .content(&stack) .build(); let content_page = NavigationPage::builder() .title("Content") .child(&toolbar_view) .build(); // navigation sidebar let sidebar = StackSidebar::builder() .stack(&stack) .build(); let nav_page = NavigationPage::builder() .title("Sidebar") .child(&sidebar) .build(); // split view let nav_view = NavigationSplitView::builder() .sidebar(&nav_page) .content(&content_page) .build(); let window = ApplicationWindow::builder() .application(app) .default_width(640) .default_height(480) .title("MNT Reform Setup Wizard") .child(&nav_view) .build(); window.set_titlebar(Some(&header_bar)); window.present(); let desktop_index = Rc::new(RefCell::new(0)); // main content stack let _welcome_page = build_welcome_page(&stack); let _keyboard_page = build_keyboard_page(&stack); let _time_page = build_time_page(&stack); let _desktop_page = build_desktop_page(&stack, desktop_index.clone()); let _root_pw_page = build_root_pw_page(&stack); let _hostname_page = build_hostname_page(&stack); let _account_page = build_account_page(&stack, desktop_index.clone(), &window); } ./mnt-reform-setup-wizard/test_with_chroot.sh000077500000000000000000000030451457632702400217000ustar00rootroot00000000000000#!/bin/sh set -e # set up variables and check existence of chroot system if [ -z "${CHROOT_PATH}" ]; then echo "Need to set CHROOT_PATH!" exit 1 fi wizard_filename="reform-setup" cleanup_script_filename="cleanup.sh" repo_path_wizard="./target/debug/${wizard_filename}" repo_path_cleanup_script="./${cleanup_script_filename}" chroot_dir_root="${CHROOT_PATH}/root" chroot_path_wizard="./root/${wizard_filename}" if [ ! -d "${chroot_dir_root}" ]; then echo "Can't find ${chroot_dir_root}, suspecting no system for chrooting set up." echo "To set it up, run something like this:" echo "# mkdir -p ${CHROOT_PATH}" echo "# debootstrap unstable ${CHROOT_PATH} http://deb.debian.org/debian/" exit 1 fi # ensure the current variants of the needed executables are inside the chrooted system sudo cp "${repo_path_wizard}" "${chroot_dir_root}/" sudo cp "${repo_path_cleanup_script}" "${chroot_dir_root}/" # packages needed for GTK4 sudo chroot "${CHROOT_PATH}" /bin/apt remove -y libssl3 sudo chroot "${CHROOT_PATH}" /bin/apt install -y libgtk-4-1 libadwaita-1-0 # packages needed for keyboard configuration (?) sudo chroot "${CHROOT_PATH}" /bin/apt install -y keyboard-configuration locales sudo chroot "${CHROOT_PATH}" /bin/sh -c 'echo locales locales/locales_to_be_generated multiselect en_US.UTF-8 | debconf-set-selections' sudo chroot "${CHROOT_PATH}" dpkg-reconfigure --frontend noninteractive locales # xhost +/- allows the Wizard to access the X Server for display xhost + sudo chroot "${CHROOT_PATH}" "${chroot_path_wizard}" xhost - exit 0