diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 558 |
1 files changed, 0 insertions, 558 deletions
diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index b82def3..0000000 --- a/src/main.rs +++ /dev/null @@ -1,558 +0,0 @@ -use adw::prelude::*; -use gtk::glib; -use std::{cell::RefCell, fs, path::Path, rc::Rc}; - -const APP_ID: &str = "org.altlinux.Flasher"; -const BOARDS: &str = include_str!("../alt-rootfs-installer/SUPPORTED-BOARDS"); - -#[derive(Default)] -struct State { - source: Option<String>, - source_is_image: bool, - output_file: Option<String>, - output_is_file: bool, -} - -fn main() -> glib::ExitCode { - let app = adw::Application::builder().application_id(APP_ID).build(); - app.connect_activate(build_ui); - app.run() -} - -fn build_ui(app: &adw::Application) { - let state = Rc::new(RefCell::new(State::default())); - - let window = adw::ApplicationWindow::builder() - .application(app) - .title("ALT Image Writer") - .default_width(720) - .default_height(760) - .build(); - - let toast_overlay = adw::ToastOverlay::new(); - let toolbar_view = adw::ToolbarView::new(); - toast_overlay.set_child(Some(&toolbar_view)); - - let header = adw::HeaderBar::new(); - let about_button = gtk::Button::builder() - .icon_name("help-about-symbolic") - .tooltip_text("About ALT Image Writer") - .build(); - header.pack_end(&about_button); - toolbar_view.add_top_bar(&header); - - let page = adw::PreferencesPage::new(); - page.set_title("Create Installation Media"); - page.set_description( - "Prepare an ALT Linux root filesystem or disk image for your board.", - ); - toolbar_view.set_content(Some(&page)); - - // Source - let source_group = adw::PreferencesGroup::builder() - .title("Installation Source") - .description("Choose a root filesystem archive or a ready-made disk image.") - .build(); - - let source_type_row = adw::ActionRow::builder().title("Source type").build(); - let source_types = gtk::Box::new(gtk::Orientation::Horizontal, 0); - source_types.add_css_class("linked"); - source_types.set_valign(gtk::Align::Center); - let archive_button = gtk::ToggleButton::with_label("Root filesystem"); - archive_button.set_active(true); - let image_button = gtk::ToggleButton::with_label("Disk image"); - image_button.set_group(Some(&archive_button)); - source_types.append(&archive_button); - source_types.append(&image_button); - source_type_row.add_suffix(&source_types); - source_group.add(&source_type_row); - - let source_row = adw::ActionRow::builder() - .title("No source selected") - .subtitle("Supported: .tar, .tar.gz, .tar.xz") - .activatable(true) - .build(); - source_row.add_prefix(&image("document-open-symbolic")); - let source_choose = gtk::Button::builder() - .label("Choose…") - .valign(gtk::Align::Center) - .build(); - source_row.add_suffix(&source_choose); - source_row.set_activatable_widget(Some(&source_choose)); - source_group.add(&source_row); - page.add(&source_group); - - // Hardware - let hardware_group = adw::PreferencesGroup::builder() - .title("Target Hardware") - .description("The matching bootloader and device tree will be installed.") - .build(); - let boards = supported_boards(); - let board_count = boards.len(); - // ComboRow does not have native section headers, so insert non-selectable, - // clearly marked headings while keeping the actual board labels compact. - let mut board_names = vec!["Choose a board…".to_owned()]; - let mut board_platforms = vec![String::new()]; - let mut board_targets = vec![None]; - let mut previous_platform = None; - for (name, platform) in boards { - if previous_platform.as_deref() != Some(platform.as_str()) { - board_names.push(format!("── {platform} ──")); - board_platforms.push(String::new()); - board_targets.push(None); - previous_platform = Some(platform.clone()); - } - board_names.push(name.clone()); - board_platforms.push(platform); - board_targets.push(Some(name)); - } - let board_model = gtk::StringList::new( - &board_names - .iter() - .map(String::as_str) - .collect::<Vec<_>>(), - ); - let board_row = adw::ComboRow::builder() - .title("Target board") - .subtitle(format!( - "Search or choose from {board_count} supported boards" - )) - .model(&board_model) - .enable_search(true) - .build(); - hardware_group.add(&board_row); - page.add(&hardware_group); - - // Destination - let destination_group = adw::PreferencesGroup::builder() - .title("Destination") - .description("Write to removable media or create an image file.") - .build(); - let destination_type_row = adw::ActionRow::builder().title("Output type").build(); - let destination_types = gtk::Box::new(gtk::Orientation::Horizontal, 0); - destination_types.add_css_class("linked"); - destination_types.set_valign(gtk::Align::Center); - let drive_button = gtk::ToggleButton::with_label("Drive"); - drive_button.set_active(true); - let file_button = gtk::ToggleButton::with_label("Image file"); - file_button.set_group(Some(&drive_button)); - destination_types.append(&drive_button); - destination_types.append(&file_button); - destination_type_row.add_suffix(&destination_types); - destination_group.add(&destination_type_row); - - let drive_model = gtk::StringList::new( - &available_drives() - .iter() - .map(String::as_str) - .collect::<Vec<_>>(), - ); - let drive_row = adw::ComboRow::builder() - .title("Storage device") - .subtitle("Only removable devices are shown") - .model(&drive_model) - .build(); - destination_group.add(&drive_row); - - let output_row = adw::ActionRow::builder() - .title("Choose where to save the image") - .subtitle("Raw disk image (.img)") - .visible(false) - .activatable(true) - .build(); - output_row.add_prefix(&image("document-save-symbolic")); - let output_choose = gtk::Button::builder() - .label("Choose…") - .valign(gtk::Align::Center) - .build(); - output_row.add_suffix(&output_choose); - output_row.set_activatable_widget(Some(&output_choose)); - destination_group.add(&output_row); - page.add(&destination_group); - - // Options mirroring the command line's commonly used switches. - let options_group = adw::PreferencesGroup::builder() - .title("Options") - .description("Partitioning and first-boot settings") - .build(); - let filesystem_model = gtk::StringList::new(&["ext4", "f2fs"]); - let filesystem_row = adw::ComboRow::builder() - .title("Root filesystem") - .model(&filesystem_model) - .build(); - options_group.add(&filesystem_row); - options_group.add(&switch_row( - "Resize root partition", - "Grow the root filesystem to fill the destination", - "view-fullscreen-symbolic", - true, - )); - options_group.add(&switch_row( - "Separate boot partition", - "Create a 512 MiB boot partition", - "drive-harddisk-symbolic", - false, - )); - options_group.add(&switch_row( - "Encrypt root partition", - "Protect the root filesystem with LUKS", - "changes-prevent-symbolic", - false, - )); - options_group.add(&switch_row( - "Serial console", - "Add the system serial console to boot arguments", - "utilities-terminal-symbolic", - false, - )); - page.add(&options_group); - - let advanced_group = adw::PreferencesGroup::builder().title("Advanced").build(); - let efi_model = gtk::StringList::new(&["None", "EFI (GPT)", "EFI (MBR)"]); - advanced_group.add( - &adw::ComboRow::builder() - .title("EFI system partition") - .model(&efi_model) - .build(), - ); - let vnc_model = gtk::StringList::new(&["Use image default", "Enabled", "Disabled"]); - advanced_group.add( - &adw::ComboRow::builder() - .title("First-boot VNC") - .model(&vnc_model) - .build(), - ); - let extra_row = adw::ActionRow::builder() - .title("Extra options file") - .subtitle("Optional installer configuration") - .build(); - extra_row.add_suffix( - >k::Button::builder() - .icon_name("document-open-symbolic") - .tooltip_text("Choose extra options file") - .valign(gtk::Align::Center) - .build(), - ); - advanced_group.add(&extra_row); - page.add(&advanced_group); - - let action_bar = gtk::Box::new(gtk::Orientation::Horizontal, 12); - action_bar.set_margin_top(12); - action_bar.set_margin_bottom(12); - action_bar.set_margin_start(12); - action_bar.set_margin_end(12); - let action_hint = gtk::Label::new(Some("Nothing will be written until you confirm.")); - action_hint.add_css_class("dim-label"); - action_hint.set_hexpand(true); - action_hint.set_halign(gtk::Align::Start); - let start_button = gtk::Button::with_label("Review and Write"); - start_button.add_css_class("suggested-action"); - start_button.set_sensitive(false); - action_bar.append(&action_hint); - action_bar.append(&start_button); - toolbar_view.add_bottom_bar(&action_bar); - - window.set_content(Some(&toast_overlay)); - - // Interactions - { - let window = window.clone(); - about_button.connect_clicked(move |_| { - adw::AboutDialog::builder() - .application_name("ALT Image Writer") - .application_icon(APP_ID) - .developer_name("ALT Linux Team") - .version(env!("CARGO_PKG_VERSION")) - .comments("Prepare ALT Linux installation media for supported single-board computers.") - .website("https://www.altlinux.org") - .license_type(gtk::License::Gpl20) - .build() - .present(Some(&window)); - }); - } - { - let source_row = source_row.clone(); - let state = state.clone(); - let file_button = file_button.clone(); - archive_button.connect_toggled(move |button| { - if button.is_active() { - state.borrow_mut().source_is_image = false; - source_row.set_subtitle("Supported: .tar, .tar.gz, .tar.xz"); - file_button.set_sensitive(true); - } - }); - } - { - let source_row = source_row.clone(); - let state = state.clone(); - let file_button = file_button.clone(); - let drive_button = drive_button.clone(); - image_button.connect_toggled(move |button| { - if button.is_active() { - state.borrow_mut().source_is_image = true; - source_row.set_subtitle("Supported: .img, .img.xz"); - drive_button.set_active(true); - file_button.set_sensitive(false); - } - }); - } - { - let window = window.clone(); - let row = source_row.clone(); - let state = state.clone(); - let board_row = board_row.clone(); - let drive_row = drive_row.clone(); - let start_button = start_button.clone(); - source_choose.connect_clicked(move |_| { - let chooser = gtk::FileDialog::builder() - .title("Choose Installation Source") - .accept_label("Select") - .modal(true) - .build(); - let row = row.clone(); - let state = state.clone(); - let board_row = board_row.clone(); - let drive_row = drive_row.clone(); - let start_button = start_button.clone(); - chooser.open(Some(&window), gtk::gio::Cancellable::NONE, move |result| { - if let Some(path) = result.ok().and_then(|file| file.path()) { - row.set_title(&display_name(&path)); - row.set_subtitle(path.parent().and_then(Path::to_str).unwrap_or("")); - state.borrow_mut().source = Some(path.display().to_string()); - update_start(&start_button, &state, &board_row, &drive_row); - } - }); - }); - } - { - let state = state.clone(); - let drive_row = drive_row.clone(); - let start_button = start_button.clone(); - let board_platforms = board_platforms.clone(); - let board_targets = board_targets.clone(); - board_row.connect_selected_notify(move |row| { - let selected = row.selected() as usize; - if selected > 0 - && board_targets - .get(selected) - .is_some_and(Option::is_none) - { - // Section headings are labels, not valid installation targets. - row.set_selected(0); - return; - } - if selected == 0 { - let count = board_targets.iter().flatten().count(); - row.set_subtitle(&format!( - "Search or choose from {count} supported boards" - )); - } else if let Some(platform) = board_platforms.get(selected) { - row.set_subtitle(platform); - } - update_start(&start_button, &state, row, &drive_row) - }); - } - { - let state = state.clone(); - let board_row = board_row.clone(); - let start_button = start_button.clone(); - drive_row.connect_selected_notify(move |row| { - update_start(&start_button, &state, &board_row, row) - }); - } - { - let drive_row = drive_row.clone(); - let output_row = output_row.clone(); - let state = state.clone(); - let board_row = board_row.clone(); - let start_button = start_button.clone(); - file_button.connect_toggled(move |button| { - let file = button.is_active(); - drive_row.set_visible(!file); - output_row.set_visible(file); - state.borrow_mut().output_is_file = file; - update_start(&start_button, &state, &board_row, &drive_row); - }); - } - { - let window = window.clone(); - let row = output_row.clone(); - let state = state.clone(); - let board_row = board_row.clone(); - let drive_row = drive_row.clone(); - let start_button = start_button.clone(); - output_choose.connect_clicked(move |_| { - let chooser = gtk::FileDialog::builder() - .title("Create Disk Image") - .accept_label("Select") - .initial_name("alt-linux.img") - .modal(true) - .build(); - let row = row.clone(); - let state = state.clone(); - let board_row = board_row.clone(); - let drive_row = drive_row.clone(); - let start_button = start_button.clone(); - chooser.save(Some(&window), gtk::gio::Cancellable::NONE, move |result| { - if let Some(path) = result.ok().and_then(|file| file.path()) { - row.set_title(&display_name(&path)); - row.set_subtitle(path.parent().and_then(Path::to_str).unwrap_or("")); - state.borrow_mut().output_file = Some(path.display().to_string()); - update_start(&start_button, &state, &board_row, &drive_row); - } - }); - }); - } - { - let window = window.clone(); - let toast_overlay = toast_overlay.clone(); - let state = state.clone(); - let board_row = board_row.clone(); - let board_targets = board_targets.clone(); - let drive_model = drive_model.clone(); - let drive_row = drive_row.clone(); - start_button.connect_clicked(move |_| { - let state = state.borrow(); - let board = board_targets - .get(board_row.selected() as usize) - .and_then(Option::as_deref) - .unwrap_or("Unknown board"); - let destination = if state.output_is_file { - state - .output_file - .clone() - .unwrap_or_else(|| "No output file".into()) - } else { - drive_model - .string(drive_row.selected()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "No drive".into()) - }; - let dialog = adw::AlertDialog::builder() - .heading("Ready to write?") - .body(format!( - "Board: {board}\nDestination: {destination}\n\nThis is an interface preview. No data will be written." - )) - .build(); - dialog.add_response("cancel", "Cancel"); - dialog.add_response("write", "Write"); - dialog.set_response_appearance("write", adw::ResponseAppearance::Destructive); - dialog.set_default_response(Some("cancel")); - dialog.set_close_response("cancel"); - let toast_overlay = toast_overlay.clone(); - dialog.connect_response(Some("write"), move |_, _| { - toast_overlay.add_toast(adw::Toast::new("Flashing is not implemented in this preview")); - }); - dialog.present(Some(&window)); - }); - } - - window.present(); -} - -fn image(icon_name: &str) -> gtk::Image { - let image = gtk::Image::from_icon_name(icon_name); - image.set_pixel_size(20); - image -} - -fn switch_row(title: &str, subtitle: &str, icon_name: &str, active: bool) -> adw::SwitchRow { - let row = adw::SwitchRow::builder() - .title(title) - .subtitle(subtitle) - .active(active) - .build(); - row.add_prefix(&image(icon_name)); - row -} - -fn display_name(path: &Path) -> String { - path.file_name() - .and_then(|name| name.to_str()) - .unwrap_or("Selected file") - .to_owned() -} - -fn update_start( - button: >k::Button, - state: &Rc<RefCell<State>>, - board: &adw::ComboRow, - drive: &adw::ComboRow, -) { - let state = state.borrow(); - let has_destination = if state.output_is_file { - state.output_file.is_some() - } else { - drive.model().map(|m| m.n_items() > 0).unwrap_or(false) - }; - button.set_sensitive( - state.source.is_some() - && board.selected() != gtk::INVALID_LIST_POSITION - && board.selected() > 0 - && has_destination, - ); -} - -fn supported_boards() -> Vec<(String, String)> { - let mut result = Vec::new(); - let mut family = "Other"; - for line in BOARDS.lines() { - let line = line.trim(); - if line.is_empty() { - continue; - } - if let Some(heading) = line.strip_suffix(':') { - family = heading; - continue; - } - for board in line.split_whitespace() { - result.push((board.to_owned(), family.to_owned())); - } - } - result -} - -fn available_drives() -> Vec<String> { - let mut drives = Vec::new(); - let Ok(entries) = fs::read_dir("/sys/block") else { - return drives; - }; - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().into_owned(); - if name.starts_with("loop") || name.starts_with("ram") || name.starts_with("zram") { - continue; - } - let removable = fs::read_to_string(entry.path().join("removable")) - .map(|value| value.trim() == "1") - .unwrap_or(false); - if !removable && !name.starts_with("mmcblk") { - continue; - } - let sectors = fs::read_to_string(entry.path().join("size")) - .ok() - .and_then(|value| value.trim().parse::<u64>().ok()) - .unwrap_or(0); - let size = format_size(sectors.saturating_mul(512)); - let model = fs::read_to_string(entry.path().join("device/model")) - .unwrap_or_default() - .trim() - .to_owned(); - if model.is_empty() { - drives.push(format!("/dev/{name} · {size}")); - } else { - drives.push(format!("{model} · {size} · /dev/{name}")); - } - } - drives.sort(); - drives -} - -fn format_size(bytes: u64) -> String { - const GIB: f64 = 1024.0 * 1024.0 * 1024.0; - const MIB: f64 = 1024.0 * 1024.0; - if bytes as f64 >= GIB { - format!("{:.1} GB", bytes as f64 / GIB) - } else { - format!("{:.0} MB", bytes as f64 / MIB) - } -} |