summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndrew Guschin <guschin@altlinux.org>2026-07-25 19:32:00 +0400
committerAndrew Guschin <guschin@altlinux.org>2026-07-25 19:32:11 +0400
commit9808b79f2076026217dbd0027de62b12ad299304 (patch)
tree68b62523fc50e70ea3bcc662af62c83e3dc7c3af /src
parent1f2dd740777115bbd490e939e9b02cd66bf379fc (diff)
add flasher-qt
Diffstat (limited to 'src')
-rw-r--r--src/main.rs558
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(
- &gtk::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: &gtk::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)
- }
-}