diff options
| author | Andrew Guschin <guschin@altlinux.org> | 2026-07-25 20:00:30 +0400 |
|---|---|---|
| committer | Andrew Guschin <guschin@altlinux.org> | 2026-07-25 20:00:30 +0400 |
| commit | 902eec31a4b6f0403a379e1c242914174b87deb5 (patch) | |
| tree | 724c9b783bfde5e8beb78c0c482eafff22aa493e | |
| parent | 9808b79f2076026217dbd0027de62b12ad299304 (diff) | |
rewrite in qt5 instead of qtquick
| -rw-r--r-- | Cargo.lock | 160 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | crates/flasher-qt/Cargo.toml | 8 | ||||
| -rw-r--r-- | crates/flasher-qt/build.rs | 25 | ||||
| -rw-r--r-- | crates/flasher-qt/qml/Main.qml | 394 | ||||
| -rw-r--r-- | crates/flasher-qt/src/main.rs | 50 | ||||
| -rw-r--r-- | crates/flasher-qt/src/ui.cpp | 409 |
7 files changed, 480 insertions, 580 deletions
@@ -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" @@ -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(); +} |