summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock696
-rw-r--r--Cargo.toml4
-rw-r--r--src/main.rs559
3 files changed, 1257 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2623781..4af0b09 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 9ad9aa9..75f7dc3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(
+ &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)
+ }
}