summaryrefslogtreecommitdiff
path: root/crates/flasher-qt
diff options
context:
space:
mode:
authorAndrew Guschin <guschin@altlinux.org>2026-08-03 17:52:58 +0400
committerAndrew Guschin <guschin@altlinux.org>2026-08-03 17:52:58 +0400
commit1da6636ef354b77857a57059e47c3801304361cf (patch)
treea8f07c59cd01ff275283d35dacc452c3730fe4a7 /crates/flasher-qt
parent04c1d3d8751ae55b0dc963fa32359875f58a3fe1 (diff)
remake ui with kirigamimain
Diffstat (limited to 'crates/flasher-qt')
-rw-r--r--crates/flasher-qt/Cargo.toml10
-rw-r--r--crates/flasher-qt/build.rs27
-rw-r--r--crates/flasher-qt/qml/Main.qml437
-rw-r--r--crates/flasher-qt/src/main.rs59
-rw-r--r--crates/flasher-qt/src/ui.cpp404
5 files changed, 469 insertions, 468 deletions
diff --git a/crates/flasher-qt/Cargo.toml b/crates/flasher-qt/Cargo.toml
index d964edc..a4f0642 100644
--- a/crates/flasher-qt/Cargo.toml
+++ b/crates/flasher-qt/Cargo.toml
@@ -2,10 +2,12 @@
name = "flasher-qt"
version = "0.1.0"
edition = "2024"
-description = "A native Qt Widgets interface for preparing ALT Linux SBC images"
+description = "A Qt 6/KDE Kirigami interface for preparing ALT Linux SBC images"
license = "GPL-2.0-or-later"
-build = "build.rs"
+
+[dependencies]
+cxx-qt-lib = { version = "0.9.1", features = ["qt_gui", "qt_qml", "link_qt_object_files"] }
+serde_json = "1"
[build-dependencies]
-cc = "1.4"
-pkg-config = "0.3"
+cxx-qt-build = { version = "0.9.1", features = ["link_qt_object_files"] }
diff --git a/crates/flasher-qt/build.rs b/crates/flasher-qt/build.rs
index e4bdfc2..d57bff5 100644
--- a/crates/flasher-qt/build.rs
+++ b/crates/flasher-qt/build.rs
@@ -1,25 +1,6 @@
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");
+ cxx_qt_build::CxxQtBuilder::new()
+ .qt_module("Qml")
+ .qt_module("Quick")
+ .build();
}
diff --git a/crates/flasher-qt/qml/Main.qml b/crates/flasher-qt/qml/Main.qml
new file mode 100644
index 0000000..805c6ed
--- /dev/null
+++ b/crates/flasher-qt/qml/Main.qml
@@ -0,0 +1,437 @@
+import QtQuick
+import QtQuick.Controls as Controls
+import QtQuick.Layouts
+import QtQuick.Dialogs
+import org.kde.kirigami as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: root
+ visible: true
+ width: 760
+ height: 820
+ minimumWidth: 600
+ minimumHeight: 580
+ title: qsTr("ALT Image Writer")
+
+ readonly property string supportedBoardsText: @SUPPORTED_BOARDS@
+ readonly property string availableDrivesText: @AVAILABLE_DRIVES@
+ 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.startsWith("file://") ? 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 && driveNames.length > 0)
+ }
+
+ globalDrawer: Kirigami.GlobalDrawer {
+ isMenu: true
+ actions: [
+ Kirigami.Action {
+ text: qsTr("About ALT Image Writer")
+ icon.name: "help-about"
+ onTriggered: aboutDialog.open()
+ }
+ ]
+ }
+
+ pageStack.initialPage: Kirigami.ScrollablePage {
+ title: qsTr("Create Installation Media")
+ actions: [
+ Kirigami.Action {
+ text: qsTr("About")
+ icon.name: "help-about"
+ displayHint: Kirigami.DisplayHint.IconOnly
+ onTriggered: aboutDialog.open()
+ }
+ ]
+
+ ColumnLayout {
+ width: Math.min(parent.width, Kirigami.Units.gridUnit * 42)
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Kirigami.Units.largeSpacing
+
+ Controls.Label {
+ text: qsTr("Prepare an ALT Linux root filesystem or disk image for your board.")
+ color: Kirigami.Theme.disabledTextColor
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+
+ Kirigami.AbstractCard {
+ Layout.fillWidth: true
+ contentItem: ColumnLayout {
+ spacing: Kirigami.Units.smallSpacing
+ Kirigami.Heading { text: qsTr("Installation Source"); level: 3 }
+ Controls.Label {
+ text: qsTr("Choose a root filesystem archive or a ready-made disk image.")
+ color: Kirigami.Theme.disabledTextColor
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ Kirigami.FormLayout {
+ Layout.fillWidth: true
+ RowLayout {
+ Kirigami.FormData.label: qsTr("Source type:")
+ Controls.RadioButton {
+ text: qsTr("Root filesystem")
+ checked: true
+ onCheckedChanged: if (checked) sourceIsImage = false
+ }
+ Controls.RadioButton {
+ text: qsTr("Disk image")
+ onCheckedChanged: if (checked) {
+ sourceIsImage = true
+ driveOutput.checked = true
+ }
+ }
+ }
+ RowLayout {
+ Kirigami.FormData.label: qsTr("Source:")
+ Controls.Label {
+ text: sourcePath.length ? fileName(sourcePath) : qsTr("No source selected")
+ elide: Text.ElideMiddle
+ Layout.fillWidth: true
+ }
+ Controls.Button {
+ text: qsTr("Choose…")
+ icon.name: "document-open"
+ onClicked: sourceDialog.open()
+ }
+ }
+ }
+ Controls.Label {
+ text: sourcePath.length ? sourcePath
+ : sourceIsImage ? qsTr("Supported: .img, .img.xz")
+ : qsTr("Supported: .tar, .tar.gz, .tar.xz")
+ color: Kirigami.Theme.disabledTextColor
+ elide: Text.ElideMiddle
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ Kirigami.AbstractCard {
+ Layout.fillWidth: true
+ contentItem: ColumnLayout {
+ Kirigami.Heading { text: qsTr("Target Hardware"); level: 3 }
+ Controls.Label {
+ text: qsTr("Boards are grouped by platform, SoC family, and architecture.")
+ color: Kirigami.Theme.disabledTextColor
+ wrapMode: Text.WordWrap
+ Layout.fillWidth: true
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Kirigami.Icon {
+ source: "computer-symbolic"
+ implicitWidth: Kirigami.Units.iconSizes.medium
+ implicitHeight: implicitWidth
+ }
+ ColumnLayout {
+ spacing: 0
+ Layout.fillWidth: true
+ Controls.Label {
+ text: selectedBoard.length ? selectedBoard : qsTr("No board selected")
+ font.bold: selectedBoard.length > 0
+ }
+ Controls.Label {
+ text: selectedBoard.length ? selectedPlatform : qsTr("Choose a supported target board")
+ color: Kirigami.Theme.disabledTextColor
+ }
+ }
+ Controls.Button {
+ text: qsTr("Select Board…")
+ onClicked: boardDialog.open()
+ }
+ }
+ }
+ }
+
+ Kirigami.AbstractCard {
+ Layout.fillWidth: true
+ contentItem: ColumnLayout {
+ Kirigami.Heading { text: qsTr("Destination"); level: 3 }
+ Controls.Label {
+ text: qsTr("Write to removable media or create an image file.")
+ color: Kirigami.Theme.disabledTextColor
+ }
+ Kirigami.FormLayout {
+ Layout.fillWidth: true
+ RowLayout {
+ Kirigami.FormData.label: qsTr("Output type:")
+ Controls.RadioButton {
+ id: driveOutput
+ text: qsTr("Drive")
+ checked: true
+ onCheckedChanged: if (checked) outputIsFile = false
+ }
+ Controls.RadioButton {
+ text: qsTr("Image file")
+ enabled: !sourceIsImage
+ onCheckedChanged: if (checked) outputIsFile = true
+ }
+ }
+ Controls.ComboBox {
+ id: driveBox
+ visible: !outputIsFile
+ Kirigami.FormData.label: qsTr("Storage device:")
+ Layout.fillWidth: true
+ model: driveNames
+ displayText: currentIndex < 0 || driveNames.length === 0
+ ? qsTr("No removable drives detected") : currentText
+ enabled: driveNames.length > 0
+ }
+ RowLayout {
+ visible: outputIsFile
+ Kirigami.FormData.label: qsTr("Image file:")
+ Controls.Label {
+ text: outputPath.length ? outputPath : qsTr("Choose where to save the image")
+ elide: Text.ElideMiddle
+ Layout.fillWidth: true
+ }
+ Controls.Button {
+ text: qsTr("Choose…")
+ icon.name: "document-save"
+ onClicked: outputDialog.open()
+ }
+ }
+ }
+ }
+ }
+
+ Kirigami.AbstractCard {
+ Layout.fillWidth: true
+ contentItem: ColumnLayout {
+ Kirigami.Heading { text: qsTr("Options"); level: 3 }
+ Kirigami.FormLayout {
+ Layout.fillWidth: true
+ Controls.ComboBox {
+ Kirigami.FormData.label: qsTr("Root filesystem:")
+ model: ["ext4", "f2fs"]
+ }
+ Controls.CheckBox {
+ Kirigami.FormData.label: qsTr("Resize root partition:")
+ text: qsTr("Grow to fill the destination")
+ checked: true
+ }
+ Controls.CheckBox {
+ Kirigami.FormData.label: qsTr("Boot partition:")
+ text: qsTr("Create a separate 512 MiB partition")
+ }
+ Controls.CheckBox {
+ Kirigami.FormData.label: qsTr("Encryption:")
+ text: qsTr("Encrypt root partition with LUKS")
+ }
+ Controls.CheckBox {
+ Kirigami.FormData.label: qsTr("Serial console:")
+ text: qsTr("Add to boot arguments")
+ }
+ Controls.ComboBox {
+ Kirigami.FormData.label: qsTr("EFI system partition:")
+ model: [qsTr("None"), qsTr("EFI (GPT)"), qsTr("EFI (MBR)")]
+ }
+ Controls.ComboBox {
+ Kirigami.FormData.label: qsTr("First-boot VNC:")
+ model: [qsTr("Use image default"), qsTr("Enabled"), qsTr("Disabled")]
+ }
+ }
+ }
+ }
+
+ Kirigami.InlineMessage {
+ id: previewMessage
+ Layout.fillWidth: true
+ type: Kirigami.MessageType.Information
+ text: qsTr("Flashing is not implemented in this interface preview.")
+ showCloseButton: true
+ }
+ }
+ }
+
+ footer: Controls.ToolBar {
+ contentItem: RowLayout {
+ Controls.Label {
+ text: qsTr("Nothing will be written until you confirm.")
+ color: Kirigami.Theme.disabledTextColor
+ Layout.leftMargin: Kirigami.Units.largeSpacing
+ Layout.fillWidth: true
+ }
+ Controls.Button {
+ text: qsTr("Review and Write")
+ icon.name: "media-flash-sd-mmc"
+ highlighted: true
+ enabled: root.ready()
+ Layout.rightMargin: Kirigami.Units.largeSpacing
+ onClicked: reviewDialog.open()
+ }
+ }
+ }
+
+ Controls.Dialog {
+ id: boardDialog
+ parent: Controls.Overlay.overlay
+ anchors.centerIn: parent
+ width: Math.min(root.width - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 34)
+ height: Math.min(root.height - Kirigami.Units.gridUnit * 4, Kirigami.Units.gridUnit * 34)
+ modal: true
+ title: qsTr("Select Target Board")
+ standardButtons: Controls.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.endsWith(":")) {
+ 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().includes(needle)
+ || item.platformName.toLowerCase().includes(needle))
+ filteredBoards.append({ boardName: item.boardName, platformName: item.platformName })
+ }
+ }
+
+ onOpened: {
+ populate()
+ searchField.forceActiveFocus()
+ }
+
+ contentItem: ColumnLayout {
+ Controls.TextField {
+ id: searchField
+ Layout.fillWidth: true
+ placeholderText: qsTr("Search boards or platforms…")
+ onTextChanged: boardDialog.filter(text)
+ }
+ Controls.Label {
+ text: qsTr("%1 matching boards").arg(filteredBoards.count)
+ color: Kirigami.Theme.disabledTextColor
+ }
+ ListView {
+ id: boardList
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ model: filteredBoards
+ section.property: "platformName"
+ section.criteria: ViewSection.FullString
+ section.delegate: Kirigami.ListSectionHeader { text: section }
+ delegate: Controls.ItemDelegate {
+ required property string boardName
+ required property string platformName
+ width: boardList.width
+ text: boardName
+ icon.name: "computer-symbolic"
+ onClicked: {
+ selectedBoard = boardName
+ selectedPlatform = platformName
+ boardDialog.close()
+ }
+ }
+ Controls.ScrollBar.vertical: Controls.ScrollBar {}
+ }
+ }
+ }
+
+ Controls.Dialog {
+ id: reviewDialog
+ parent: Controls.Overlay.overlay
+ anchors.centerIn: parent
+ modal: true
+ title: qsTr("Ready to write?")
+ standardButtons: Controls.Dialog.Cancel | Controls.Dialog.Ok
+ onAccepted: previewMessage.visible = true
+ contentItem: Kirigami.FormLayout {
+ Controls.Label { Kirigami.FormData.label: qsTr("Board:"); text: selectedBoard }
+ Controls.Label { Kirigami.FormData.label: qsTr("Platform:"); text: selectedPlatform }
+ Controls.Label {
+ Kirigami.FormData.label: qsTr("Destination:")
+ text: outputIsFile ? outputPath : driveBox.currentText
+ elide: Text.ElideMiddle
+ }
+ Controls.Label {
+ text: qsTr("This is an interface preview. No data will be written.")
+ color: Kirigami.Theme.disabledTextColor
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+
+ Controls.Dialog {
+ id: aboutDialog
+ parent: Controls.Overlay.overlay
+ anchors.centerIn: parent
+ modal: true
+ title: qsTr("About ALT Image Writer")
+ standardButtons: Controls.Dialog.Ok
+ contentItem: ColumnLayout {
+ Kirigami.Icon {
+ source: "media-flash-sd-mmc"
+ implicitWidth: Kirigami.Units.iconSizes.huge
+ implicitHeight: implicitWidth
+ Layout.alignment: Qt.AlignHCenter
+ }
+ Kirigami.Heading { text: qsTr("ALT Image Writer"); level: 2; Layout.alignment: Qt.AlignHCenter }
+ Controls.Label { text: qsTr("Version 0.1.0"); Layout.alignment: Qt.AlignHCenter }
+ Controls.Label {
+ text: qsTr("Prepare ALT Linux installation media for supported single-board computers.")
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ Layout.preferredWidth: Kirigami.Units.gridUnit * 22
+ }
+ }
+ }
+
+ FileDialog {
+ id: sourceDialog
+ title: qsTr("Choose Installation Source")
+ fileMode: 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(selectedFile)
+ }
+
+ FileDialog {
+ id: outputDialog
+ title: qsTr("Create Disk Image")
+ fileMode: FileDialog.SaveFile
+ defaultSuffix: "img"
+ nameFilters: [qsTr("Raw disk images (*.img)"), qsTr("All files (*)")]
+ onAccepted: outputPath = localPath(selectedFile)
+ }
+}
diff --git a/crates/flasher-qt/src/main.rs b/crates/flasher-qt/src/main.rs
index e304418..243406b 100644
--- a/crates/flasher-qt/src/main.rs
+++ b/crates/flasher-qt/src/main.rs
@@ -1,43 +1,30 @@
-use std::{
- env,
- ffi::CString,
- fs,
- os::raw::{c_char, c_int},
-};
+use cxx_qt_lib::{QByteArray, QGuiApplication, QQmlApplicationEngine, QString, QUrl};
+use std::fs;
const BOARDS: &str = include_str!("../../../alt-rootfs-installer/SUPPORTED-BOARDS");
-
-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;
-}
+const QML: &str = include_str!("../qml/Main.qml");
fn main() {
- 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");
+ let mut app = QGuiApplication::new();
+ app.pin_mut()
+ .set_application_name(&QString::from("ALT Image Writer"));
+ app.pin_mut()
+ .set_organization_name(&QString::from("ALT Linux"));
+ app.pin_mut()
+ .set_application_version(&QString::from(env!("CARGO_PKG_VERSION")));
- // 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(),
- );
- }
+ let boards = serde_json::to_string(BOARDS).expect("serialize supported boards");
+ let drives = serde_json::to_string(&available_drives().join("\n"))
+ .expect("serialize available drives");
+ let qml = QML
+ .replace("@SUPPORTED_BOARDS@", &boards)
+ .replace("@AVAILABLE_DRIVES@", &drives);
+
+ let mut engine = QQmlApplicationEngine::new();
+ engine
+ .pin_mut()
+ .load_data(&QByteArray::from(qml.as_str()), &QUrl::default());
+ app.pin_mut().exec();
}
fn available_drives() -> Vec<String> {
@@ -45,7 +32,6 @@ fn available_drives() -> Vec<String> {
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") {
@@ -57,7 +43,6 @@ fn available_drives() -> Vec<String> {
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())
diff --git a/crates/flasher-qt/src/ui.cpp b/crates/flasher-qt/src/ui.cpp
deleted file mode 100644
index 0af1987..0000000
--- a/crates/flasher-qt/src/ui.cpp
+++ /dev/null
@@ -1,404 +0,0 @@
-#include <QApplication>
-#include <QButtonGroup>
-#include <QCheckBox>
-#include <QComboBox>
-#include <QDialog>
-#include <QDialogButtonBox>
-#include <QFileDialog>
-#include <QFormLayout>
-#include <QFrame>
-#include <QGroupBox>
-#include <QHBoxLayout>
-#include <QLabel>
-#include <QLineEdit>
-#include <QMainWindow>
-#include <QMenuBar>
-#include <QMessageBox>
-#include <QPushButton>
-#include <QRadioButton>
-#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();
-}