diff options
| -rw-r--r-- | Cargo.lock | 696 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | src/main.rs | 559 |
3 files changed, 1257 insertions, 2 deletions
@@ -3,5 +3,701 @@ version = 4 [[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cairo-rs" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] name = "flasher" version = "0.1.0" +dependencies = [ + "gtk4", + "libadwaita", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gio" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys", +] + +[[package]] +name = "glib" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gtk4-sys" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libadwaita" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb09e12bf8f73342b3315c839d0a7668cc0ccebd78490c49fec48bab15d5484b" +dependencies = [ + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "libc", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7f94227ba87eb596fecada2491f04e357d507324142f77bf76d9e6be4a3e31" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pango" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] @@ -2,5 +2,9 @@ name = "flasher" version = "0.1.0" edition = "2024" +description = "A GTK interface for preparing ALT Linux images for single-board computers" +license = "GPL-2.0-or-later" [dependencies] +adw = { package = "libadwaita", version = "0.8", features = ["v1_5"] } +gtk = { package = "gtk4", version = "0.10", features = ["v4_10"] } diff --git a/src/main.rs b/src/main.rs index e7a11a9..b82def3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,558 @@ -fn main() { - println!("Hello, world!"); +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) + } } |