#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct Board { QString name; QString platform; }; QVector parseBoards(const QString &text) { QVector 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 &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 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 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("

ALT Image Writer

Version 0.1.0

" "

Prepare ALT Linux installation media for supported " "single-board computers.

")); }); 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 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(); }