summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock160
-rw-r--r--README.md14
-rw-r--r--crates/flasher-qt/Cargo.toml8
-rw-r--r--crates/flasher-qt/build.rs25
-rw-r--r--crates/flasher-qt/qml/Main.qml394
-rw-r--r--crates/flasher-qt/src/main.rs50
-rw-r--r--crates/flasher-qt/src/ui.cpp409
7 files changed, 480 insertions, 580 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cb7eb08..e4847cc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,15 +3,6 @@
version = 4
[[package]]
-name = "aho-corasick"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
-dependencies = [
- "memchr",
-]
-
-[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,12 +15,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
-name = "byteorder"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
-
-[[package]]
name = "cairo-rs"
version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -73,53 +58,6 @@ dependencies = [
]
[[package]]
-name = "cpp"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1f6422bf20bc0654eac481a29a03e6d9121ad9f12345a03773b8a76a8701915"
-dependencies = [
- "cpp_macros",
-]
-
-[[package]]
-name = "cpp_build"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f6fed3200ba0708c2adca5f6ed5ae202edd824bd4cbac7935a85edac9bcddce"
-dependencies = [
- "cc",
- "cpp_common",
- "proc-macro2",
- "regex",
- "syn 2.0.119",
- "unicode-xid",
-]
-
-[[package]]
-name = "cpp_common"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7280a73ef92e18d27d2ec3005b57fe0043b51d1b506be86b0bf66f588f9857b"
-dependencies = [
- "proc-macro2",
- "syn 2.0.119",
-]
-
-[[package]]
-name = "cpp_macros"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffc7fd49a5b246251229ea4d4bf94c8d418689a1f9986ef96e00b64992922169"
-dependencies = [
- "aho-corasick",
- "byteorder",
- "cpp_common",
- "proc-macro2",
- "quote",
- "syn 2.0.119",
-]
-
-[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -153,7 +91,8 @@ dependencies = [
name = "flasher-qt"
version = "0.1.0"
dependencies = [
- "qmetaobject",
+ "cc",
+ "pkg-config",
]
[[package]]
@@ -489,12 +428,6 @@ dependencies = [
]
[[package]]
-name = "lazy_static"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
-[[package]]
name = "libadwaita"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -532,12 +465,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
-name = "log"
-version = "0.4.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
-
-[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -607,43 +534,6 @@ dependencies = [
]
[[package]]
-name = "qmetaobject"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "426a57e85d36f055a0c82cb0a8a261d49ba051ab2a2ef5471835f69d477816cd"
-dependencies = [
- "cpp",
- "cpp_build",
- "lazy_static",
- "log",
- "qmetaobject_impl",
- "qttypes",
- "semver",
-]
-
-[[package]]
-name = "qmetaobject_impl"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc24897c707dcd6963e359e7f2b123857c508f129bed8ac4d3bd575c1a47627"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "qttypes"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7edf5b38c97ad8900ad2a8418ee44b4adceaa866a4a3405e2f1c909871d7ebd"
-dependencies = [
- "cpp",
- "cpp_build",
- "semver",
-]
-
-[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -653,35 +543,6 @@ dependencies = [
]
[[package]]
-name = "regex"
-version = "1.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
-
-[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -745,17 +606,6 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
@@ -853,12 +703,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
-name = "unicode-xid"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
-
-[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/README.md b/README.md
index c2fd06d..dc9468d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
This workspace contains two independent interfaces for `alt-rootfs-installer`:
- `crates/flasher-gtk` — GTK 4/libadwaita
-- `crates/flasher-qt` — Qt 5/Qt Quick Controls 2
+- `crates/flasher-qt` — native Qt 5 Widgets
Both are interface previews. They inspect supported boards and removable drives, but do not write to any device.
@@ -12,18 +12,10 @@ Both are interface previews. They inspect supported boards and removable drives,
On ALT Linux:
```sh
-sudo apt-get install gcc-c++ libgtk4-devel libadwaita-devel \
- qt5-base-devel qt5-declarative-devel qt5-quickcontrols2-devel \
- qt5-quickcontrols2
+sudo apt-get install gcc-c++ libgtk4-devel libadwaita-devel qt5-base-devel
```
-The Qt bindings discover Qt through `qmake`. If several Qt versions are installed, select Qt 5 explicitly:
-
-```sh
-export QMAKE=/usr/lib64/qt5/bin/qmake
-```
-
-(The exact `qmake` path can be found with `rpm -ql qt5-base-devel | grep '/qmake$'`.)
+The Qt crate discovers Qt 5 through `pkg-config` and uses the platform's native Qt widget style. It does not require QML or Qt Quick.
## Build
diff --git a/crates/flasher-qt/Cargo.toml b/crates/flasher-qt/Cargo.toml
index 5bbf918..d964edc 100644
--- a/crates/flasher-qt/Cargo.toml
+++ b/crates/flasher-qt/Cargo.toml
@@ -2,8 +2,10 @@
name = "flasher-qt"
version = "0.1.0"
edition = "2024"
-description = "A Qt interface for preparing ALT Linux SBC images"
+description = "A native Qt Widgets interface for preparing ALT Linux SBC images"
license = "GPL-2.0-or-later"
+build = "build.rs"
-[dependencies]
-qmetaobject = "0.2.10"
+[build-dependencies]
+cc = "1.4"
+pkg-config = "0.3"
diff --git a/crates/flasher-qt/build.rs b/crates/flasher-qt/build.rs
new file mode 100644
index 0000000..e4bdfc2
--- /dev/null
+++ b/crates/flasher-qt/build.rs
@@ -0,0 +1,25 @@
+fn main() {
+ println!("cargo:rerun-if-changed=src/ui.cpp");
+
+ let qt = pkg_config::Config::new()
+ .atleast_version("5.12")
+ .cargo_metadata(false)
+ .probe("Qt5Widgets")
+ .expect("Qt 5 Widgets development files were not found (install qt5-base-devel)");
+
+ let mut build = cc::Build::new();
+ build.cpp(true).file("src/ui.cpp").flag_if_supported("-std=c++17");
+ for path in &qt.include_paths {
+ build.include(path);
+ }
+ build.compile("flasher_qt_ui");
+
+ // Keep shared Qt libraries after our static C++ archive on the linker
+ // command line; GNU ld resolves static archives from left to right.
+ for path in &qt.link_paths {
+ println!("cargo:rustc-link-search=native={}", path.display());
+ }
+ println!("cargo:rustc-link-lib=Qt5Widgets");
+ println!("cargo:rustc-link-lib=Qt5Gui");
+ println!("cargo:rustc-link-lib=Qt5Core");
+}
diff --git a/crates/flasher-qt/qml/Main.qml b/crates/flasher-qt/qml/Main.qml
deleted file mode 100644
index a41064d..0000000
--- a/crates/flasher-qt/qml/Main.qml
+++ /dev/null
@@ -1,394 +0,0 @@
-import QtQuick 2.12
-import QtQuick.Controls 2.12
-import QtQuick.Layouts 1.12
-import Qt.labs.platform 1.1 as Platform
-
-ApplicationWindow {
- id: window
- visible: true
- width: 760
- height: 820
- minimumWidth: 620
- minimumHeight: 620
- title: qsTr("ALT Image Writer — Qt")
-
- property bool sourceIsImage: false
- property bool outputIsFile: false
- property string sourcePath: ""
- property string outputPath: ""
- property string selectedBoard: ""
- property string selectedPlatform: ""
- property var driveNames: availableDrivesText.length ? availableDrivesText.split("\n") : []
-
- function localPath(url) {
- var value = url.toString()
- return value.indexOf("file://") === 0 ? decodeURIComponent(value.substring(7)) : value
- }
-
- function fileName(path) {
- var parts = path.split("/")
- return parts[parts.length - 1]
- }
-
- function ready() {
- return sourcePath.length > 0 && selectedBoard.length > 0
- && (outputIsFile ? outputPath.length > 0 : driveBox.currentIndex >= 0)
- }
-
- header: ToolBar {
- RowLayout {
- anchors.fill: parent
- anchors.leftMargin: 16
- anchors.rightMargin: 8
- Label {
- text: qsTr("ALT Image Writer")
- font.pixelSize: 18
- font.bold: true
- Layout.fillWidth: true
- }
- ToolButton {
- text: "ⓘ"
- font.pixelSize: 18
- Accessible.name: qsTr("About")
- onClicked: aboutDialog.open()
- }
- }
- }
-
- ScrollView {
- id: scroll
- anchors.fill: parent
- contentWidth: availableWidth
- clip: true
-
- ColumnLayout {
- width: Math.min(scroll.availableWidth - 40, 700)
- x: Math.max(20, (scroll.availableWidth - width) / 2)
- spacing: 16
-
- Item { Layout.preferredHeight: 8 }
- Label {
- text: qsTr("Create Installation Media")
- font.pixelSize: 26
- font.bold: true
- Layout.alignment: Qt.AlignHCenter
- }
- Label {
- text: qsTr("Prepare an ALT Linux root filesystem or disk image for your board.")
- color: palette.mid
- wrapMode: Text.WordWrap
- horizontalAlignment: Text.AlignHCenter
- Layout.fillWidth: true
- }
-
- Frame {
- Layout.fillWidth: true
- ColumnLayout {
- anchors.fill: parent
- spacing: 12
- Label { text: qsTr("Installation Source"); font.bold: true; font.pixelSize: 17 }
- Label { text: qsTr("Choose a root filesystem archive or a ready-made disk image."); color: palette.mid }
- ButtonGroup { id: sourceGroup }
- RowLayout {
- Label { text: qsTr("Source type"); Layout.fillWidth: true }
- RadioButton {
- text: qsTr("Root filesystem")
- checked: true
- ButtonGroup.group: sourceGroup
- onCheckedChanged: if (checked) sourceIsImage = false
- }
- RadioButton {
- text: qsTr("Disk image")
- ButtonGroup.group: sourceGroup
- onCheckedChanged: if (checked) {
- sourceIsImage = true
- outputIsFile = false
- driveOutput.checked = true
- }
- }
- }
- RowLayout {
- spacing: 12
- Label {
- text: sourcePath.length ? fileName(sourcePath) : qsTr("No source selected")
- font.bold: sourcePath.length > 0
- elide: Text.ElideMiddle
- Layout.fillWidth: true
- }
- Button { text: qsTr("Choose…"); onClicked: sourceDialog.open() }
- }
- Label {
- text: sourcePath.length ? sourcePath
- : sourceIsImage ? qsTr("Supported: .img, .img.xz")
- : qsTr("Supported: .tar, .tar.gz, .tar.xz")
- color: palette.mid
- elide: Text.ElideMiddle
- Layout.fillWidth: true
- }
- }
- }
-
- Frame {
- Layout.fillWidth: true
- ColumnLayout {
- anchors.fill: parent
- spacing: 10
- Label { text: qsTr("Target Hardware"); font.bold: true; font.pixelSize: 17 }
- Label { text: qsTr("Boards are grouped by platform, SoC family, and architecture."); color: palette.mid }
- RowLayout {
- ColumnLayout {
- Layout.fillWidth: true
- Label {
- text: selectedBoard.length ? selectedBoard : qsTr("No board selected")
- font.bold: selectedBoard.length > 0
- }
- Label {
- text: selectedBoard.length ? selectedPlatform : qsTr("Choose from the supported board list")
- color: palette.mid
- }
- }
- Button { text: qsTr("Select Board…"); onClicked: boardDialog.open() }
- }
- }
- }
-
- Frame {
- Layout.fillWidth: true
- ColumnLayout {
- anchors.fill: parent
- spacing: 12
- Label { text: qsTr("Destination"); font.bold: true; font.pixelSize: 17 }
- Label { text: qsTr("Write to removable media or create an image file."); color: palette.mid }
- ButtonGroup { id: outputGroup }
- RowLayout {
- Label { text: qsTr("Output type"); Layout.fillWidth: true }
- RadioButton {
- id: driveOutput
- text: qsTr("Drive")
- checked: true
- ButtonGroup.group: outputGroup
- onCheckedChanged: if (checked) outputIsFile = false
- }
- RadioButton {
- text: qsTr("Image file")
- enabled: !sourceIsImage
- ButtonGroup.group: outputGroup
- onCheckedChanged: if (checked) outputIsFile = true
- }
- }
- RowLayout {
- visible: !outputIsFile
- Label { text: qsTr("Storage device"); Layout.preferredWidth: 150 }
- ComboBox {
- id: driveBox
- Layout.fillWidth: true
- model: driveNames
- displayText: currentIndex < 0 ? qsTr("No removable drives detected") : currentText
- }
- }
- RowLayout {
- visible: outputIsFile
- Label {
- text: outputPath.length ? outputPath : qsTr("Choose where to save the image")
- elide: Text.ElideMiddle
- Layout.fillWidth: true
- }
- Button { text: qsTr("Choose…"); onClicked: outputDialog.open() }
- }
- }
- }
-
- Frame {
- Layout.fillWidth: true
- ColumnLayout {
- anchors.fill: parent
- spacing: 8
- Label { text: qsTr("Options"); font.bold: true; font.pixelSize: 17 }
- RowLayout {
- Label { text: qsTr("Root filesystem"); Layout.fillWidth: true }
- ComboBox { model: ["ext4", "f2fs"] }
- }
- CheckBox { text: qsTr("Resize root partition to fill the destination"); checked: true }
- CheckBox { text: qsTr("Create a separate 512 MiB boot partition") }
- CheckBox { text: qsTr("Encrypt the root partition with LUKS") }
- CheckBox { text: qsTr("Add the system serial console") }
- RowLayout {
- Label { text: qsTr("EFI system partition"); Layout.fillWidth: true }
- ComboBox { model: [qsTr("None"), qsTr("EFI (GPT)"), qsTr("EFI (MBR)")] }
- }
- RowLayout {
- Label { text: qsTr("First-boot VNC"); Layout.fillWidth: true }
- ComboBox { model: [qsTr("Use image default"), qsTr("Enabled"), qsTr("Disabled")] }
- }
- }
- }
- Item { Layout.preferredHeight: 80 }
- }
- }
-
- footer: ToolBar {
- RowLayout {
- anchors.fill: parent
- anchors.margins: 10
- Label { text: qsTr("Nothing will be written until you confirm."); color: palette.mid; Layout.fillWidth: true }
- Button {
- text: qsTr("Review and Write")
- highlighted: true
- enabled: ready()
- onClicked: reviewDialog.open()
- }
- }
- }
-
- Dialog {
- id: boardDialog
- parent: Overlay.overlay
- anchors.centerIn: parent
- width: Math.min(window.width - 48, 620)
- height: Math.min(window.height - 80, 650)
- modal: true
- title: qsTr("Select Target Board")
- standardButtons: Dialog.Cancel
-
- ListModel { id: allBoards }
- ListModel { id: filteredBoards }
-
- function populate() {
- if (allBoards.count > 0) return
- var platform = "Other"
- var lines = supportedBoardsText.split("\n")
- for (var i = 0; i < lines.length; ++i) {
- var line = lines[i].trim()
- if (!line.length) continue
- if (line.charAt(line.length - 1) === ":") {
- platform = line.substring(0, line.length - 1)
- } else {
- var names = line.split(/\s+/)
- for (var j = 0; j < names.length; ++j)
- allBoards.append({ boardName: names[j], platformName: platform })
- }
- }
- filter("")
- }
-
- function filter(query) {
- filteredBoards.clear()
- var needle = query.toLowerCase()
- for (var i = 0; i < allBoards.count; ++i) {
- var item = allBoards.get(i)
- if (!needle.length || item.boardName.toLowerCase().indexOf(needle) >= 0
- || item.platformName.toLowerCase().indexOf(needle) >= 0)
- filteredBoards.append({ boardName: item.boardName, platformName: item.platformName })
- }
- }
-
- onOpened: { populate(); searchField.forceActiveFocus() }
-
- ColumnLayout {
- anchors.fill: parent
- TextField {
- id: searchField
- Layout.fillWidth: true
- placeholderText: qsTr("Search boards or platforms…")
- selectByMouse: true
- onTextChanged: boardDialog.filter(text)
- }
- Label {
- text: qsTr("%1 matching boards").arg(filteredBoards.count)
- color: palette.mid
- }
- ListView {
- id: boardList
- Layout.fillWidth: true
- Layout.fillHeight: true
- clip: true
- model: filteredBoards
- spacing: 2
- section.property: "platformName"
- section.criteria: ViewSection.FullString
- section.delegate: Rectangle {
- width: boardList.width
- height: 42
- color: window.palette.alternateBase
- Label {
- anchors.left: parent.left
- anchors.leftMargin: 12
- anchors.verticalCenter: parent.verticalCenter
- text: section
- font.bold: true
- color: window.palette.highlight
- }
- }
- delegate: ItemDelegate {
- width: boardList.width
- text: boardName
- onClicked: {
- selectedBoard = boardName
- selectedPlatform = platformName
- boardDialog.close()
- }
- }
- ScrollBar.vertical: ScrollBar {}
- }
- }
- }
-
- Dialog {
- id: reviewDialog
- parent: Overlay.overlay
- anchors.centerIn: parent
- modal: true
- title: qsTr("Ready to write?")
- standardButtons: Dialog.Cancel | Dialog.Ok
- onAccepted: previewNotice.open()
- ColumnLayout {
- Label { text: qsTr("Board"); font.bold: true }
- Label { text: selectedBoard + " — " + selectedPlatform }
- Label { text: qsTr("Destination"); font.bold: true }
- Label { text: outputIsFile ? outputPath : driveBox.currentText; elide: Text.ElideMiddle; Layout.maximumWidth: 450 }
- Label { text: qsTr("This is an interface preview. No data will be written."); color: palette.mid }
- }
- }
-
- Dialog {
- id: aboutDialog
- parent: Overlay.overlay
- anchors.centerIn: parent
- modal: true
- title: qsTr("About ALT Image Writer")
- standardButtons: Dialog.Ok
- Label {
- text: qsTr("ALT Image Writer — Qt\nVersion 0.1.0\n\nPrepare ALT Linux installation media for supported SBCs.")
- horizontalAlignment: Text.AlignHCenter
- }
- }
-
- Popup {
- id: previewNotice
- parent: Overlay.overlay
- x: (parent.width - width) / 2
- y: parent.height - height - 24
- padding: 16
- closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
- Label { text: qsTr("Flashing is not implemented in this preview") }
- Timer { interval: 3500; running: previewNotice.visible; onTriggered: previewNotice.close() }
- }
-
- Platform.FileDialog {
- id: sourceDialog
- title: qsTr("Choose Installation Source")
- fileMode: Platform.FileDialog.OpenFile
- nameFilters: sourceIsImage ? [qsTr("Disk images (*.img *.img.xz)"), qsTr("All files (*)")]
- : [qsTr("Root filesystem archives (*.tar *.tar.gz *.tar.xz)"), qsTr("All files (*)")]
- onAccepted: sourcePath = localPath(file)
- }
-
- Platform.FileDialog {
- id: outputDialog
- title: qsTr("Create Disk Image")
- fileMode: Platform.FileDialog.SaveFile
- defaultSuffix: "img"
- nameFilters: [qsTr("Raw disk images (*.img)"), qsTr("All files (*)")]
- onAccepted: outputPath = localPath(file)
- }
-}
diff --git a/crates/flasher-qt/src/main.rs b/crates/flasher-qt/src/main.rs
index f131bc5..e304418 100644
--- a/crates/flasher-qt/src/main.rs
+++ b/crates/flasher-qt/src/main.rs
@@ -1,21 +1,43 @@
-use qmetaobject::{QmlEngine, QString, QVariant};
-use std::fs;
+use std::{
+ env,
+ ffi::CString,
+ fs,
+ os::raw::{c_char, c_int},
+};
const BOARDS: &str = include_str!("../../../alt-rootfs-installer/SUPPORTED-BOARDS");
-const QML: &str = include_str!("../qml/Main.qml");
+
+unsafe extern "C" {
+ fn run_qt_app(
+ argc: c_int,
+ argv: *mut *mut c_char,
+ boards: *const c_char,
+ drives: *const c_char,
+ ) -> c_int;
+}
fn main() {
- let mut engine = QmlEngine::new();
- engine.set_property(
- QString::from("supportedBoardsText"),
- QVariant::from(QString::from(BOARDS)),
- );
- engine.set_property(
- QString::from("availableDrivesText"),
- QVariant::from(QString::from(available_drives().join("\n"))),
- );
- engine.load_data(QML.into());
- engine.exec();
+ let arguments: Vec<CString> = env::args()
+ .map(|arg| CString::new(arg).expect("application argument contains a NUL byte"))
+ .collect();
+ let mut argument_pointers: Vec<*mut c_char> = arguments
+ .iter()
+ .map(|arg| arg.as_ptr().cast_mut())
+ .collect();
+ let boards = CString::new(BOARDS).expect("board list contains a NUL byte");
+ let drives = CString::new(available_drives().join("\n"))
+ .expect("drive description contains a NUL byte");
+
+ // QApplication only borrows argv and the two strings for the duration of
+ // this call; all backing CStrings remain alive until the event loop exits.
+ unsafe {
+ run_qt_app(
+ argument_pointers.len() as c_int,
+ argument_pointers.as_mut_ptr(),
+ boards.as_ptr(),
+ drives.as_ptr(),
+ );
+ }
}
fn available_drives() -> Vec<String> {
diff --git a/crates/flasher-qt/src/ui.cpp b/crates/flasher-qt/src/ui.cpp
new file mode 100644
index 0000000..eb34457
--- /dev/null
+++ b/crates/flasher-qt/src/ui.cpp
@@ -0,0 +1,409 @@
+#include <QAction>
+#include <QApplication>
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QFileDialog>
+#include <QFileInfo>
+#include <QFont>
+#include <QFormLayout>
+#include <QFrame>
+#include <QGroupBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QMainWindow>
+#include <QMap>
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QPalette>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QRegExp>
+#include <QScrollArea>
+#include <QStatusBar>
+#include <QStyle>
+#include <QTreeWidget>
+#include <QVBoxLayout>
+
+namespace {
+
+struct Board {
+ QString name;
+ QString platform;
+};
+
+QVector<Board> parseBoards(const QString &text) {
+ QVector<Board> boards;
+ QString platform = QStringLiteral("Other");
+ for (const QString &rawLine : text.split('\n')) {
+ const QString line = rawLine.trimmed();
+ if (line.isEmpty())
+ continue;
+ if (line.endsWith(':')) {
+ platform = line.left(line.size() - 1);
+ continue;
+ }
+ for (const QString &name : line.split(QRegExp(QStringLiteral("\\s+")), Qt::SkipEmptyParts))
+ boards.push_back({name, platform});
+ }
+ return boards;
+}
+
+QLabel *description(const QString &text) {
+ auto *label = new QLabel(text);
+ label->setWordWrap(true);
+ label->setForegroundRole(QPalette::Mid);
+ return label;
+}
+
+QGroupBox *group(const QString &title, QLayout *layout) {
+ auto *box = new QGroupBox(title);
+ box->setLayout(layout);
+ return box;
+}
+
+class BoardDialog final : public QDialog {
+public:
+ explicit BoardDialog(const QVector<Board> &boards, QWidget *parent = nullptr)
+ : QDialog(parent), boards_(boards) {
+ setWindowTitle(tr("Select Target Board"));
+ resize(620, 620);
+ setModal(true);
+
+ auto *layout = new QVBoxLayout(this);
+ search_ = new QLineEdit;
+ search_->setPlaceholderText(tr("Search boards, platforms, or architectures…"));
+ search_->setClearButtonEnabled(true);
+ tree_ = new QTreeWidget;
+ tree_->setHeaderHidden(true);
+ tree_->setRootIsDecorated(true);
+ tree_->setAlternatingRowColors(true);
+ tree_->setUniformRowHeights(true);
+ layout->addWidget(search_);
+ layout->addWidget(tree_, 1);
+
+ auto *buttons = new QDialogButtonBox(QDialogButtonBox::Cancel);
+ layout->addWidget(buttons);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ connect(search_, &QLineEdit::textChanged, this, [this](const QString &text) { populate(text); });
+ connect(tree_, &QTreeWidget::itemActivated, this, [this](QTreeWidgetItem *item) {
+ if (item && item->parent()) {
+ selectedName_ = item->data(0, Qt::UserRole).toString();
+ selectedPlatform_ = item->parent()->text(0);
+ accept();
+ }
+ });
+ populate({});
+ }
+
+ QString selectedName() const { return selectedName_; }
+ QString selectedPlatform() const { return selectedPlatform_; }
+
+private:
+ void populate(const QString &query) {
+ tree_->clear();
+ QMap<QString, QTreeWidgetItem *> headings;
+ const QString needle = query.trimmed();
+ for (const Board &board : boards_) {
+ if (!needle.isEmpty() && !board.name.contains(needle, Qt::CaseInsensitive)
+ && !board.platform.contains(needle, Qt::CaseInsensitive))
+ continue;
+ auto *&heading = headings[board.platform];
+ if (!heading) {
+ heading = new QTreeWidgetItem(tree_, {board.platform});
+ QFont font = heading->font(0);
+ font.setBold(true);
+ heading->setFont(0, font);
+ heading->setFlags(heading->flags() & ~Qt::ItemIsSelectable);
+ }
+ auto *item = new QTreeWidgetItem(heading, {board.name});
+ item->setData(0, Qt::UserRole, board.name);
+ }
+ tree_->expandAll();
+ if (!query.isEmpty() && tree_->topLevelItemCount() > 0) {
+ auto *heading = tree_->topLevelItem(0);
+ if (heading->childCount() > 0)
+ tree_->setCurrentItem(heading->child(0));
+ }
+ }
+
+ QVector<Board> boards_;
+ QLineEdit *search_ = nullptr;
+ QTreeWidget *tree_ = nullptr;
+ QString selectedName_;
+ QString selectedPlatform_;
+};
+
+class MainWindow final : public QMainWindow {
+public:
+ MainWindow(const QString &boardsText, const QString &drivesText)
+ : boards_(parseBoards(boardsText)) {
+ setWindowTitle(tr("ALT Image Writer"));
+ resize(780, 820);
+ setMinimumSize(620, 600);
+
+ auto *about = menuBar()->addMenu(tr("&Help"))->addAction(tr("&About"));
+ connect(about, &QAction::triggered, this, [this] {
+ QMessageBox::about(this, tr("About ALT Image Writer"),
+ tr("<h3>ALT Image Writer</h3><p>Version 0.1.0</p>"
+ "<p>Prepare ALT Linux installation media for supported "
+ "single-board computers.</p>"));
+ });
+
+ auto *scroll = new QScrollArea;
+ scroll->setWidgetResizable(true);
+ scroll->setFrameShape(QFrame::NoFrame);
+ setCentralWidget(scroll);
+
+ auto *viewport = new QWidget;
+ auto *outer = new QHBoxLayout(viewport);
+ outer->setContentsMargins(20, 20, 20, 20);
+ auto *content = new QWidget;
+ content->setMaximumWidth(700);
+ auto *layout = new QVBoxLayout(content);
+ layout->setSpacing(16);
+ outer->addStretch();
+ outer->addWidget(content, 1);
+ outer->addStretch();
+ scroll->setWidget(viewport);
+
+ auto *title = new QLabel(tr("Create Installation Media"));
+ QFont titleFont = title->font();
+ titleFont.setPointSize(titleFont.pointSize() + 7);
+ titleFont.setBold(true);
+ title->setFont(titleFont);
+ title->setAlignment(Qt::AlignCenter);
+ layout->addWidget(title);
+ auto *intro = description(tr("Prepare an ALT Linux root filesystem or disk image for your board."));
+ intro->setAlignment(Qt::AlignCenter);
+ layout->addWidget(intro);
+
+ createSource(layout);
+ createHardware(layout);
+ createDestination(layout, drivesText);
+ createOptions(layout);
+ layout->addStretch();
+
+ auto *bar = new QWidget;
+ auto *barLayout = new QHBoxLayout(bar);
+ barLayout->setContentsMargins(12, 8, 12, 8);
+ barLayout->addWidget(description(tr("Nothing will be written until you confirm.")), 1);
+ writeButton_ = new QPushButton(tr("Review and Write"));
+ writeButton_->setDefault(true);
+ writeButton_->setEnabled(false);
+ barLayout->addWidget(writeButton_);
+ statusBar()->addPermanentWidget(bar, 1);
+ connect(writeButton_, &QPushButton::clicked, this, [this] { review(); });
+ }
+
+private:
+ void createSource(QVBoxLayout *page) {
+ auto *layout = new QVBoxLayout;
+ layout->addWidget(description(tr("Choose a root filesystem archive or a ready-made disk image.")));
+ auto *types = new QHBoxLayout;
+ types->addWidget(new QLabel(tr("Source type")));
+ types->addStretch();
+ rootfsButton_ = new QRadioButton(tr("Root filesystem"));
+ imageButton_ = new QRadioButton(tr("Disk image"));
+ rootfsButton_->setChecked(true);
+ types->addWidget(rootfsButton_);
+ types->addWidget(imageButton_);
+ layout->addLayout(types);
+
+ auto *picker = new QHBoxLayout;
+ sourceLabel_ = new QLabel(tr("No source selected"));
+ sourceLabel_->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ auto *choose = new QPushButton(tr("Choose…"));
+ choose->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton));
+ picker->addWidget(sourceLabel_, 1);
+ picker->addWidget(choose);
+ layout->addLayout(picker);
+ sourceHint_ = description(tr("Supported: .tar, .tar.gz, .tar.xz"));
+ layout->addWidget(sourceHint_);
+ page->addWidget(group(tr("Installation Source"), layout));
+
+ connect(rootfsButton_, &QRadioButton::toggled, this, [this](bool checked) {
+ if (checked) {
+ sourceHint_->setText(tr("Supported: .tar, .tar.gz, .tar.xz"));
+ fileOutputButton_->setEnabled(true);
+ }
+ });
+ connect(imageButton_, &QRadioButton::toggled, this, [this](bool checked) {
+ if (checked) {
+ sourceHint_->setText(tr("Supported: .img, .img.xz"));
+ driveOutputButton_->setChecked(true);
+ fileOutputButton_->setEnabled(false);
+ }
+ });
+ connect(choose, &QPushButton::clicked, this, [this] {
+ const QString filter = imageButton_->isChecked()
+ ? tr("Disk images (*.img *.img.xz);;All files (*)")
+ : tr("Root filesystem archives (*.tar *.tar.gz *.tar.xz);;All files (*)");
+ const QString path = QFileDialog::getOpenFileName(this, tr("Choose Installation Source"), {}, filter);
+ if (!path.isEmpty()) {
+ sourcePath_ = path;
+ sourceLabel_->setText(QFileInfo(path).fileName());
+ sourceLabel_->setToolTip(path);
+ sourceHint_->setText(QFileInfo(path).absolutePath());
+ updateReady();
+ }
+ });
+ }
+
+ void createHardware(QVBoxLayout *page) {
+ auto *layout = new QVBoxLayout;
+ layout->addWidget(description(tr("Boards are grouped by platform, SoC family, and architecture.")));
+ auto *row = new QHBoxLayout;
+ auto *labels = new QVBoxLayout;
+ boardLabel_ = new QLabel(tr("No board selected"));
+ boardHint_ = description(tr("Choose from %1 supported boards").arg(boards_.size()));
+ labels->addWidget(boardLabel_);
+ labels->addWidget(boardHint_);
+ auto *select = new QPushButton(tr("Select Board…"));
+ row->addLayout(labels, 1);
+ row->addWidget(select);
+ layout->addLayout(row);
+ page->addWidget(group(tr("Target Hardware"), layout));
+
+ connect(select, &QPushButton::clicked, this, [this] {
+ BoardDialog dialog(boards_, this);
+ if (dialog.exec() == QDialog::Accepted) {
+ selectedBoard_ = dialog.selectedName();
+ selectedPlatform_ = dialog.selectedPlatform();
+ boardLabel_->setText(selectedBoard_);
+ QFont font = boardLabel_->font();
+ font.setBold(true);
+ boardLabel_->setFont(font);
+ boardHint_->setText(selectedPlatform_);
+ updateReady();
+ }
+ });
+ }
+
+ void createDestination(QVBoxLayout *page, const QString &drivesText) {
+ auto *layout = new QVBoxLayout;
+ layout->addWidget(description(tr("Write to removable media or create an image file.")));
+ auto *types = new QHBoxLayout;
+ types->addWidget(new QLabel(tr("Output type")));
+ types->addStretch();
+ driveOutputButton_ = new QRadioButton(tr("Drive"));
+ fileOutputButton_ = new QRadioButton(tr("Image file"));
+ driveOutputButton_->setChecked(true);
+ types->addWidget(driveOutputButton_);
+ types->addWidget(fileOutputButton_);
+ layout->addLayout(types);
+
+ driveRow_ = new QWidget;
+ auto *driveLayout = new QFormLayout(driveRow_);
+ driveBox_ = new QComboBox;
+ const QStringList drives = drivesText.split('\n', Qt::SkipEmptyParts);
+ driveBox_->addItems(drives);
+ if (drives.isEmpty()) {
+ driveBox_->addItem(tr("No removable drives detected"));
+ driveBox_->setEnabled(false);
+ }
+ driveLayout->addRow(tr("Storage device"), driveBox_);
+ layout->addWidget(driveRow_);
+
+ fileRow_ = new QWidget;
+ auto *fileLayout = new QHBoxLayout(fileRow_);
+ fileLayout->setContentsMargins(0, 0, 0, 0);
+ outputLabel_ = new QLabel(tr("Choose where to save the image"));
+ auto *choose = new QPushButton(tr("Choose…"));
+ choose->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton));
+ fileLayout->addWidget(outputLabel_, 1);
+ fileLayout->addWidget(choose);
+ fileRow_->hide();
+ layout->addWidget(fileRow_);
+ page->addWidget(group(tr("Destination"), layout));
+
+ connect(driveOutputButton_, &QRadioButton::toggled, this, [this](bool checked) {
+ driveRow_->setVisible(checked);
+ fileRow_->setVisible(!checked);
+ updateReady();
+ });
+ connect(choose, &QPushButton::clicked, this, [this] {
+ const QString path = QFileDialog::getSaveFileName(this, tr("Create Disk Image"),
+ QStringLiteral("alt-linux.img"), tr("Raw disk images (*.img);;All files (*)"));
+ if (!path.isEmpty()) {
+ outputPath_ = path;
+ outputLabel_->setText(QFileInfo(path).fileName());
+ outputLabel_->setToolTip(path);
+ updateReady();
+ }
+ });
+ }
+
+ void createOptions(QVBoxLayout *page) {
+ auto *layout = new QFormLayout;
+ auto *filesystem = new QComboBox;
+ filesystem->addItems({QStringLiteral("ext4"), QStringLiteral("f2fs")});
+ layout->addRow(tr("Root filesystem"), filesystem);
+ auto *resize = new QCheckBox(tr("Grow to fill the destination"));
+ resize->setChecked(true);
+ layout->addRow(tr("Resize root partition"), resize);
+ layout->addRow(tr("Boot partition"), new QCheckBox(tr("Create a separate 512 MiB partition")));
+ layout->addRow(tr("Encryption"), new QCheckBox(tr("Encrypt root partition with LUKS")));
+ layout->addRow(tr("Serial console"), new QCheckBox(tr("Add to boot arguments")));
+ auto *efi = new QComboBox;
+ efi->addItems({tr("None"), tr("EFI (GPT)"), tr("EFI (MBR)")});
+ layout->addRow(tr("EFI system partition"), efi);
+ auto *vnc = new QComboBox;
+ vnc->addItems({tr("Use image default"), tr("Enabled"), tr("Disabled")});
+ layout->addRow(tr("First-boot VNC"), vnc);
+ page->addWidget(group(tr("Options"), layout));
+ }
+
+ void updateReady() {
+ const bool destination = driveOutputButton_->isChecked()
+ ? driveBox_->isEnabled() && driveBox_->currentIndex() >= 0
+ : !outputPath_.isEmpty();
+ writeButton_->setEnabled(!sourcePath_.isEmpty() && !selectedBoard_.isEmpty() && destination);
+ }
+
+ void review() {
+ const QString destination = driveOutputButton_->isChecked() ? driveBox_->currentText() : outputPath_;
+ QMessageBox dialog(QMessageBox::Warning, tr("Ready to write?"),
+ tr("Board: %1\nPlatform: %2\nDestination: %3\n\n"
+ "This is an interface preview. No data will be written.")
+ .arg(selectedBoard_, selectedPlatform_, destination),
+ QMessageBox::Cancel | QMessageBox::Ok, this);
+ dialog.setDefaultButton(QMessageBox::Cancel);
+ dialog.button(QMessageBox::Ok)->setText(tr("Write"));
+ if (dialog.exec() == QMessageBox::Ok)
+ statusBar()->showMessage(tr("Flashing is not implemented in this preview"), 4000);
+ }
+
+ QVector<Board> boards_;
+ QString sourcePath_;
+ QString outputPath_;
+ QString selectedBoard_;
+ QString selectedPlatform_;
+ QRadioButton *rootfsButton_ = nullptr;
+ QRadioButton *imageButton_ = nullptr;
+ QRadioButton *driveOutputButton_ = nullptr;
+ QRadioButton *fileOutputButton_ = nullptr;
+ QLabel *sourceLabel_ = nullptr;
+ QLabel *sourceHint_ = nullptr;
+ QLabel *boardLabel_ = nullptr;
+ QLabel *boardHint_ = nullptr;
+ QLabel *outputLabel_ = nullptr;
+ QWidget *driveRow_ = nullptr;
+ QWidget *fileRow_ = nullptr;
+ QComboBox *driveBox_ = nullptr;
+ QPushButton *writeButton_ = nullptr;
+};
+
+} // namespace
+
+extern "C" int run_qt_app(int argc, char **argv, const char *boards, const char *drives) {
+ QApplication app(argc, argv);
+ QCoreApplication::setApplicationName(QStringLiteral("ALT Image Writer"));
+ QCoreApplication::setOrganizationName(QStringLiteral("ALT Linux"));
+ QApplication::setWindowIcon(QApplication::style()->standardIcon(QStyle::SP_DriveHDIcon));
+ MainWindow window(QString::fromUtf8(boards), QString::fromUtf8(drives));
+ window.show();
+ return app.exec();
+}