3 Commits

Author SHA1 Message Date
forust eabf420ac3 ci: fix GUI smoke binary name
ci-release / verify (push) Failing after 2m20s
ci-release / publish-linux-amd64 (push) Has been skipped
2026-07-26 19:03:54 +02:00
forust 9f7908b275 chore: release v0.3.0
ci-release / publish-linux-amd64 (push) Has been skipped
ci-release / verify (push) Failing after 1m46s
2026-07-26 16:46:25 +02:00
forust 010bd0f374 feat(ui): rename app and add input history
Import legacy SearchMyFiles settings once so the FolderScope identity change does not discard user profiles and preferences.
2026-07-26 16:46:08 +02:00
12 changed files with 173 additions and 54 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
shell: bash
run: |
set +e
QT_QPA_PLATFORM=offscreen timeout 3s ./build/searchmyfiles
QT_QPA_PLATFORM=offscreen timeout 3s ./build/folderscope
status="$?"
test "$status" = 0 || test "$status" = 124
+17
View File
@@ -5,6 +5,22 @@ All notable changes to FolderScope are documented here.
The project uses [Semantic Versioning](https://semver.org/). Release tags are
formatted as `vMAJOR.MINOR.PATCH`.
## [0.3.0] - 2026-07-26
### Added
- Added persistent input history for folder and wildcard fields in Search
Options.
### Changed
- Renamed the executable, desktop entry, icon, and application identity from
SearchMyFiles to FolderScope.
- Preserved existing settings and search profiles when migrating to the new
application identity.
- Kept the base-folder browser button compact so the input field uses the
available width.
## [0.2.5] - 2026-07-26
### Added
@@ -82,3 +98,4 @@ formatted as `vMAJOR.MINOR.PATCH`.
[0.2.3]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.2.3
[0.2.4]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.2.4
[0.2.5]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.2.5
[0.3.0]: https://gitea.forust.xyz/forust/folderscope/releases/tag/v0.3.0
+7 -7
View File
@@ -22,7 +22,7 @@ target_link_libraries(folderscope_core PUBLIC Qt6::Core)
target_compile_options(folderscope_core PRIVATE -Wall -Wextra -Wpedantic)
target_include_directories(folderscope_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src")
qt_add_executable(searchmyfiles
qt_add_executable(folderscope
src/long_long_spin_box.cpp
src/long_long_spin_box.h
src/main.cpp
@@ -38,14 +38,14 @@ qt_add_executable(searchmyfiles
src/options_dialog.cpp
)
target_link_libraries(searchmyfiles PRIVATE folderscope_core Qt6::Widgets Qt6::Concurrent Qt6::DBus)
target_compile_options(searchmyfiles PRIVATE -Wall -Wextra -Wpedantic)
target_compile_definitions(searchmyfiles PRIVATE FOLDERSCOPE_VERSION="${PROJECT_VERSION}")
target_link_libraries(folderscope PRIVATE folderscope_core Qt6::Widgets Qt6::Concurrent Qt6::DBus)
target_compile_options(folderscope PRIVATE -Wall -Wextra -Wpedantic)
target_compile_definitions(folderscope PRIVATE FOLDERSCOPE_VERSION="${PROJECT_VERSION}")
install(TARGETS searchmyfiles RUNTIME DESTINATION bin)
install(FILES packaging/searchmyfiles.desktop
install(TARGETS folderscope RUNTIME DESTINATION bin)
install(FILES packaging/folderscope.desktop
DESTINATION share/applications)
install(FILES packaging/searchmyfiles.svg
install(FILES packaging/folderscope.svg
DESTINATION share/icons/hicolor/scalable/apps)
include(CTest)
+1 -1
View File
@@ -34,7 +34,7 @@ Requirements:
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/searchmyfiles
./build/folderscope
```
Run the automated tests:
+1 -1
View File
@@ -1 +1 @@
0.2.5
0.3.0
@@ -1,8 +1,8 @@
[Desktop Entry]
Type=Application
Name=SearchMyFiles
Name=FolderScope
Comment=Fast advanced file search
Exec=searchmyfiles
Icon=searchmyfiles
Exec=folderscope
Icon=folderscope
Terminal=false
Categories=Utility;FileTools;

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 324 B

+25 -2
View File
@@ -1,15 +1,38 @@
#include <QApplication>
#include <QMetaType>
#include <QSettings>
#include <QTimer>
#include "main_window.h"
#include "search_core.h"
namespace {
void migrateLegacySettings()
{
QSettings settings;
const QString migrationKey =
QStringLiteral("migration/searchMyFilesSettingsImported");
if (settings.value(migrationKey, false).toBool())
return;
QSettings legacy(QStringLiteral("SearchMyFilesLinux"),
QStringLiteral("SearchMyFiles"));
for (const QString &key : legacy.allKeys()) {
if (!settings.contains(key))
settings.setValue(key, legacy.value(key));
}
settings.setValue(migrationKey, true);
}
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QCoreApplication::setOrganizationName(QStringLiteral("SearchMyFilesLinux"));
QCoreApplication::setApplicationName(QStringLiteral("SearchMyFiles"));
QCoreApplication::setOrganizationName(QStringLiteral("FolderScope"));
QCoreApplication::setApplicationName(QStringLiteral("FolderScope"));
migrateLegacySettings();
QApplication::setStyle(QStringLiteral("Fusion"));
qRegisterMetaType<SearchOptions>();
qRegisterMetaType<QVector<FileRecord>>();
+2 -2
View File
@@ -23,7 +23,7 @@
MainWindow::MainWindow()
{
loadOptions();
setWindowTitle(tr("SearchMyFiles for Linux"));
setWindowTitle(tr("FolderScope"));
resize(1280, 760);
model_ = new ResultsModel(this);
model_->setDisplayOptions(sizeUnit_, showGmt_, markDuplicates_, duplicateColorSet_);
@@ -507,7 +507,7 @@ void MainWindow::startCurrentSearch()
progress_->show();
progress_->setRange(0, 0);
progress_->setFormat(tr("Scanning…"));
setWindowTitle(tr("%1 — %2 — SearchMyFiles for Linux")
setWindowTitle(tr("%1 — %2 — FolderScope")
.arg(options_.roots, options_.mode));
if (focusOnSearchStart_)
table_->setFocus();
+75 -30
View File
@@ -8,6 +8,7 @@
#include <QMessageBox>
#include <QMimeData>
#include <QSettings>
#include <QSizePolicy>
#include "long_long_spin_box.h"
@@ -87,22 +88,29 @@ void OptionsDialog::setupUi(const SearchOptions &o)
auto *filesPage = new QWidget;
auto *filesForm = new QFormLayout(filesPage);
roots_ = new QLineEdit(o.roots);
roots_->setAcceptDrops(true);
roots_ = historyCombo(QStringLiteral("roots"), o.roots);
roots_->lineEdit()->setAcceptDrops(true);
roots_->setToolTip(tr("Drop folders here from your file manager"));
roots_->installEventFilter(this);
roots_->lineEdit()->installEventFilter(this);
auto *rootRow = new QWidget;
auto *rootLayout = new QHBoxLayout(rootRow);
rootLayout->setContentsMargins(0, 0, 0, 0);
rootLayout->addWidget(roots_);
auto *browse = new QPushButton(tr("Browse…"));
browse->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
rootLayout->addWidget(browse);
rootLayout->setStretch(0, 1);
filesForm->addRow(tr("Base folders (separate with ;):"), rootRow);
fileMasks_ = addLine(filesForm, tr("Files wildcard:"), o.fileWildcards);
subfolderMasks_ = addLine(filesForm, tr("Subfolders wildcard:"), o.subfolderWildcards);
excludeFiles_ = addLine(filesForm, tr("Exclude files:"), o.excludeFiles);
excludeFolders_ = addLine(filesForm, tr("Exclude folders:"), o.excludeFolders);
includeFolders_ = addLine(filesForm, tr("Include only folders:"), o.includeFolders);
fileMasks_ = addHistoryCombo(filesForm, tr("Files wildcard:"),
QStringLiteral("fileWildcards"), o.fileWildcards);
subfolderMasks_ = addHistoryCombo(filesForm, tr("Subfolders wildcard:"),
QStringLiteral("subfolderWildcards"), o.subfolderWildcards);
excludeFiles_ = addHistoryCombo(filesForm, tr("Exclude files:"),
QStringLiteral("excludeFiles"), o.excludeFiles);
excludeFolders_ = addHistoryCombo(filesForm, tr("Exclude folders:"),
QStringLiteral("excludeFolders"), o.excludeFolders);
includeFolders_ = addHistoryCombo(filesForm, tr("Include only folders:"),
QStringLiteral("includeFolders"), o.includeFolders);
recursive_ = new QCheckBox(tr("Search subfolders")); recursive_->setChecked(o.recursive);
findFiles_ = new QCheckBox(tr("Find files")); findFiles_->setChecked(o.findFiles);
findFolders_ = new QCheckBox(tr("Find folders")); findFolders_->setChecked(o.findFolders);
@@ -183,7 +191,7 @@ void OptionsDialog::setupUi(const SearchOptions &o)
connect(buttons, &QDialogButtonBox::accepted, this, &OptionsDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(browse, &QPushButton::clicked, this, [this] {
const QStringList existingFolders = patterns(roots_->text());
const QStringList existingFolders = patterns(roots_->currentText());
const QString startFolder = existingFolders.isEmpty()
? QDir::homePath() : existingFolders.constLast();
const QString folder = QFileDialog::getExistingDirectory(
@@ -195,7 +203,7 @@ void OptionsDialog::setupUi(const SearchOptions &o)
const QString cleanPath = QDir::cleanPath(folder);
if (!folders.contains(cleanPath))
folders.push_back(cleanPath);
roots_->setText(folders.join(u';'));
roots_->setEditText(folders.join(u';'));
});
layout->addWidget(buttons);
}
@@ -203,12 +211,12 @@ void OptionsDialog::setupUi(const SearchOptions &o)
SearchOptions OptionsDialog::options() const
{
SearchOptions o;
o.roots = roots_->text();
o.fileWildcards = fileMasks_->text();
o.subfolderWildcards = subfolderMasks_->text();
o.excludeFiles = excludeFiles_->text();
o.excludeFolders = excludeFolders_->text();
o.includeFolders = includeFolders_->text();
o.roots = roots_->currentText();
o.fileWildcards = fileMasks_->currentText();
o.subfolderWildcards = subfolderMasks_->currentText();
o.excludeFiles = excludeFiles_->currentText();
o.excludeFolders = excludeFolders_->currentText();
o.includeFolders = includeFolders_->currentText();
o.contains = contains_->toPlainText();
o.binary = binary_->isChecked();
o.multipleValues = multiple_->isChecked();
@@ -244,14 +252,35 @@ void OptionsDialog::accept()
if (!validateDate(minTime_, tr("From time"))
|| !validateDate(maxTime_, tr("To time")))
return;
rememberHistory(QStringLiteral("roots"), roots_->currentText());
rememberHistory(QStringLiteral("fileWildcards"), fileMasks_->currentText());
rememberHistory(QStringLiteral("subfolderWildcards"), subfolderMasks_->currentText());
rememberHistory(QStringLiteral("excludeFiles"), excludeFiles_->currentText());
rememberHistory(QStringLiteral("excludeFolders"), excludeFolders_->currentText());
rememberHistory(QStringLiteral("includeFolders"), includeFolders_->currentText());
QDialog::accept();
}
QLineEdit *OptionsDialog::addLine(QFormLayout *layout, const QString &label, const QString &value)
QComboBox *OptionsDialog::historyCombo(const QString &key, const QString &value)
{
auto *edit = new QLineEdit(value);
layout->addRow(label, edit);
return edit;
auto *combo = new QComboBox;
combo->setEditable(true);
combo->setInsertPolicy(QComboBox::NoInsert);
combo->setObjectName(QStringLiteral("history%1").arg(key));
QSettings settings;
const QStringList history = settings.value(QStringLiteral("inputHistory/%1").arg(key)).toStringList();
combo->addItems(history);
combo->setEditText(value);
return combo;
}
QComboBox *OptionsDialog::addHistoryCombo(QFormLayout *layout, const QString &label,
const QString &key, const QString &value)
{
auto *combo = historyCombo(key, value);
layout->addRow(label, combo);
return combo;
}
LongLongSpinBox *OptionsDialog::sizeSpin(qint64 value)
@@ -284,6 +313,22 @@ bool OptionsDialog::validateDate(QLineEdit *edit, const QString &label)
return false;
}
void OptionsDialog::rememberHistory(const QString &key, const QString &value)
{
if (value.isEmpty())
return;
QSettings settings;
const QString settingsKey = QStringLiteral("inputHistory/%1").arg(key);
QStringList history = settings.value(settingsKey).toStringList();
history.removeAll(value);
history.prepend(value);
constexpr qsizetype maxHistoryEntries = 20;
if (history.size() > maxHistoryEntries)
history.erase(history.begin() + maxHistoryEntries, history.end());
settings.setValue(settingsKey, history);
}
QComboBox *OptionsDialog::attrCombo(const QString &value)
{
auto *combo = new QComboBox;
@@ -294,7 +339,7 @@ QComboBox *OptionsDialog::attrCombo(const QString &value)
bool OptionsDialog::eventFilter(QObject *obj, QEvent *event)
{
if (obj == roots_ && event->type() == QEvent::DragEnter) {
if (obj == roots_->lineEdit() && event->type() == QEvent::DragEnter) {
auto *dragEnter = static_cast<QDragEnterEvent *>(event);
if (dragEnter->mimeData()->hasUrls()) {
dragEnter->acceptProposedAction();
@@ -302,11 +347,11 @@ bool OptionsDialog::eventFilter(QObject *obj, QEvent *event)
}
return false;
}
if (obj == roots_ && event->type() == QEvent::Drop) {
if (obj == roots_->lineEdit() && event->type() == QEvent::Drop) {
auto *drop = static_cast<QDropEvent *>(event);
if (!drop->mimeData()->hasUrls())
return false;
QStringList folders = patterns(roots_->text());
QStringList folders = patterns(roots_->currentText());
for (const QUrl &url : drop->mimeData()->urls()) {
if (!url.isLocalFile())
continue;
@@ -314,7 +359,7 @@ bool OptionsDialog::eventFilter(QObject *obj, QEvent *event)
if (QFileInfo::exists(cleanPath) && !folders.contains(cleanPath))
folders.append(cleanPath);
}
roots_->setText(folders.join(u';'));
roots_->setEditText(folders.join(u';'));
drop->acceptProposedAction();
return true;
}
@@ -401,12 +446,12 @@ void OptionsDialog::loadProfile()
auto loadInt = [&](const char *key, int def) { return settings.value(QString::fromLatin1(key), def).toInt(); };
auto loadLong = [&](const char *key, qint64 def) { return settings.value(QString::fromLatin1(key), def).toLongLong(); };
roots_->setText(load("roots"));
fileMasks_->setText(load("fileWildcards"));
subfolderMasks_->setText(load("subfolderWildcards"));
excludeFiles_->setText(load("excludeFiles"));
excludeFolders_->setText(load("excludeFolders"));
includeFolders_->setText(load("includeFolders"));
roots_->setEditText(load("roots"));
fileMasks_->setEditText(load("fileWildcards"));
subfolderMasks_->setEditText(load("subfolderWildcards"));
excludeFiles_->setEditText(load("excludeFiles"));
excludeFolders_->setEditText(load("excludeFolders"));
includeFolders_->setEditText(load("includeFolders"));
contains_->setText(load("contains"));
binary_->setChecked(loadBool("binary", false));
multiple_->setChecked(loadBool("multipleValues", false));
+10 -7
View File
@@ -29,10 +29,13 @@ protected:
private:
bool eventFilter(QObject *obj, QEvent *event) override;
static QLineEdit *addLine(QFormLayout *layout, const QString &label, const QString &value);
static QComboBox *historyCombo(const QString &key, const QString &value);
static QComboBox *addHistoryCombo(QFormLayout *layout, const QString &label,
const QString &key, const QString &value);
static LongLongSpinBox *sizeSpin(qint64 value);
static qint64 dateValue(const QLineEdit *edit);
bool validateDate(QLineEdit *edit, const QString &label);
void rememberHistory(const QString &key, const QString &value);
static QComboBox *attrCombo(const QString &value);
void setupUi(const SearchOptions &o);
@@ -42,12 +45,12 @@ private:
QComboBox *mode_{};
QComboBox *duplicateMode_{};
QCheckBox *duplicateWithoutExtension_{};
QLineEdit *roots_{};
QLineEdit *fileMasks_{};
QLineEdit *subfolderMasks_{};
QLineEdit *excludeFiles_{};
QLineEdit *excludeFolders_{};
QLineEdit *includeFolders_{};
QComboBox *roots_{};
QComboBox *fileMasks_{};
QComboBox *subfolderMasks_{};
QComboBox *excludeFiles_{};
QComboBox *excludeFolders_{};
QComboBox *includeFolders_{};
QTextEdit *contains_{};
QCheckBox *binary_{};
QCheckBox *multiple_{};
+31
View File
@@ -1,6 +1,8 @@
#include "options_dialog.h"
#include <QCheckBox>
#include <QComboBox>
#include <QSettings>
#include <QtTest>
class OptionsDialogTest final : public QObject {
@@ -23,6 +25,35 @@ private slots:
strict->setChecked(false);
QVERIFY(!dialog.options().strictDuplicateComparison);
}
void remembersInputHistory()
{
QSettings settings;
settings.remove(QStringLiteral("inputHistory"));
SearchOptions options;
OptionsDialog first(options);
auto *roots = first.findChild<QComboBox *>(QStringLiteral("historyroots"));
QVERIFY(roots);
roots->setEditText(QStringLiteral("/tmp/first;/tmp/second"));
QVERIFY(QMetaObject::invokeMethod(&first, "accept"));
QCOMPARE(settings.value(QStringLiteral("inputHistory/roots")).toStringList(),
QStringList{QStringLiteral("/tmp/first;/tmp/second")});
OptionsDialog second(options);
auto *history = second.findChild<QComboBox *>(QStringLiteral("historyroots"));
QVERIFY(history);
QCOMPARE(history->itemText(0), QStringLiteral("/tmp/first;/tmp/second"));
history->setEditText(QStringLiteral("/tmp/new"));
QVERIFY(QMetaObject::invokeMethod(&second, "accept"));
QCOMPARE(settings.value(QStringLiteral("inputHistory/roots")).toStringList(),
(QStringList{QStringLiteral("/tmp/new"),
QStringLiteral("/tmp/first;/tmp/second")}));
settings.remove(QStringLiteral("inputHistory"));
}
};
QTEST_MAIN(OptionsDialogTest)