diff --git a/lib/vicinae-ipc/include/vicinae-ipc/ipc.hpp b/lib/vicinae-ipc/include/vicinae-ipc/ipc.hpp index e600665d7..f0371c921 100644 --- a/lib/vicinae-ipc/include/vicinae-ipc/ipc.hpp +++ b/lib/vicinae-ipc/include/vicinae-ipc/ipc.hpp @@ -87,6 +87,7 @@ struct DMenu { std::optional width; std::optional height; bool noFooter = false; + bool preview = false; }; struct Response { diff --git a/vicinae/src/cli/cli.cpp b/vicinae/src/cli/cli.cpp index b822da899..7b96ef645 100644 --- a/vicinae/src/cli/cli.cpp +++ b/vicinae/src/cli/cli.cpp @@ -264,6 +264,8 @@ class DMenuCommand : public AbstractCommandLineCommand { "Do not show quick look if available for a given entry"); app->add_flag("--no-metadata", m_req.noMetadata, "Do not show metadata section in quick look"); app->add_flag("--no-footer", m_req.noFooter, "Hide the status bar footer"); + app->add_flag("--preview", m_req.preview, + "Enable tab-separated preview mode (format: labelpreview_path)"); } void run(CLI::App *app) override { diff --git a/vicinae/src/ui/dmenu-view/dmenu-model.hpp b/vicinae/src/ui/dmenu-view/dmenu-model.hpp index 51e26afa7..2325d090c 100644 --- a/vicinae/src/ui/dmenu-view/dmenu-model.hpp +++ b/vicinae/src/ui/dmenu-view/dmenu-model.hpp @@ -4,22 +4,33 @@ #include "common/scored.hpp" #include "utils.hpp" -class DMenuModel : public vicinae::ui::VerticalListModel { +struct DMenuEntry { + std::string_view label; + std::string_view quickLookPath; +}; + +class DMenuModel : public vicinae::ui::VerticalListModel { public: - DMenuModel(QObject *parent = nullptr) { setParent(parent); } + DMenuModel(QObject *parent = nullptr, bool noIcon = false) : m_noIcon(noIcon) { setParent(parent); } void setSectionName(std::string_view name) { m_sectionName = name; } - ItemData createItemData(const std::string_view &item) const override { - if (isFile(item)) { - return ItemData{.title = getLastPathComponent(item).c_str(), .icon = ImageURL::fileIcon(item)}; + ItemData createItemData(const DMenuEntry &item) const override { + if (!m_noIcon && isFile(item)) { + auto path = item.quickLookPath.empty() ? item.label : item.quickLookPath; + return ItemData{.title = QString::fromUtf8(item.label.data(), item.label.size()), + .icon = ImageURL::fileIcon(path)}; } - return ItemData{.title = QString::fromUtf8(item.data(), item.size())}; + return ItemData{.title = QString::fromUtf8(item.label.data(), item.label.size())}; } int sectionCount() const override { return 1; } - VListModel::StableID stableId(const std::string_view &item) const override { return hash(item); } + VListModel::StableID stableId(const DMenuEntry &item) const override { + auto h1 = hash(item.label); + auto h2 = hash(item.quickLookPath); + return h1 ^ (h2 << 1); + } int sectionIdFromIndex(int idx) const override { return idx; } @@ -27,20 +38,22 @@ class DMenuModel : public vicinae::ui::VerticalListModel { std::string_view sectionName(int id) const override { return m_sectionName; } - std::string_view sectionItemAt(int id, int itemIdx) const override { return m_entries[itemIdx].data; } + DMenuEntry sectionItemAt(int id, int itemIdx) const override { return m_entries[itemIdx].data; } - void setEntries(std::span> entries) { + void setEntries(std::span> entries) { m_entries = entries; emit dataChanged(); } private: - static bool isFile(std::string_view entry) { - if (!entry.starts_with('/')) return false; // avoid unnecessary stat + static bool isFile(const DMenuEntry &entry) { + std::string_view path = entry.quickLookPath.empty() ? entry.label : entry.quickLookPath; + if (!path.starts_with('/')) return false; // avoid unnecessary stat std::error_code ec; - return std::filesystem::exists(entry, ec); + return std::filesystem::exists(path, ec); } + bool m_noIcon = false; std::string_view m_sectionName; - std::span> m_entries; + std::span> m_entries; }; diff --git a/vicinae/src/ui/dmenu-view/dmenu-view.cpp b/vicinae/src/ui/dmenu-view/dmenu-view.cpp index 063b0b9be..1da0b58f9 100644 --- a/vicinae/src/ui/dmenu-view/dmenu-view.cpp +++ b/vicinae/src/ui/dmenu-view/dmenu-view.cpp @@ -9,19 +9,39 @@ #include namespace DMenu { -View::View(ipc::DMenu::Request data) : m_data(data), m_model(new DMenuModel(this)) { - m_entries = std::views::split(m_data.rawContent, std::string_view("\n")) | - std::views::transform([](auto &&s) { return std::string_view(s); }) | - std::views::filter([](auto &&s) { return !s.empty(); }) | std::ranges::to(); +View::View(ipc::DMenu::Request data) + : m_data(data), m_model(new DMenuModel(this, m_data.preview || m_data.noIcon)) { + auto lines = std::views::split(m_data.rawContent, std::string_view("\n")) | + std::views::transform([](auto &&s) { return std::string_view(s); }) | + std::views::filter([](auto &&s) { return !s.empty(); }); + + m_entries.reserve(std::ranges::distance(lines)); + + for (auto line : lines) { + if (m_data.preview) { + // Preview mode: parse as tab-separated (labelpreview_path) + size_t tabPos = line.find('\t'); + if (tabPos != std::string_view::npos) { + m_entries.emplace_back(DMenuEntry{line.substr(0, tabPos), line.substr(tabPos + 1)}); + } else { + m_entries.emplace_back(DMenuEntry{line, ""}); + } + } else { + // Default mode: use line as-is, no delimiter parsing + m_entries.emplace_back(DMenuEntry{line, ""}); + } + } } -QWidget *View::generateDetail(const std::string_view &text) const { +QWidget *View::generateDetail(const DMenuEntry &item) const { if (m_data.noQuickLook) return nullptr; + + std::string_view path = item.quickLookPath.empty() ? item.label : item.quickLookPath; std::error_code ec; - if (text.starts_with('/') && std::filesystem::exists(text, ec)) { + if (path.starts_with('/') && std::filesystem::exists(path, ec)) { auto detail = new FileDetail; - detail->setPath(text, !m_data.noMetadata); + detail->setPath(path, !m_data.noMetadata); return detail; } @@ -70,9 +90,9 @@ void View::textChanged(const QString &text) { } void View::setFilter(std::string_view query) { - auto toScore = [&](std::string_view s) { - int score = fzf::defaultMatcher.fuzzy_match_v2_score_query(s, query); - return Scored{.data = s, .score = score}; + auto toScore = [&](const DMenuEntry &entry) { + int score = fzf::defaultMatcher.fuzzy_match_v2_score_query(entry.label, query); + return Scored{.data = entry, .score = score}; }; auto filtered = m_entries | std::views::transform(toScore) | @@ -98,8 +118,8 @@ void View::updateSectionName(std::string_view name) { m_model->setSectionName(m_sectionName); } -void View::itemSelected(const std::string_view &view) { - auto text = QString::fromUtf8(view.data(), view.size()); +void View::itemSelected(const DMenuEntry &item) { + auto text = QString::fromUtf8(item.label.data(), item.label.size()); auto panel = std::make_unique(); auto main = panel->createSection(); auto select = new StaticAction("Select entry", ImageURL::builtin("save-document"), diff --git a/vicinae/src/ui/dmenu-view/dmenu-view.hpp b/vicinae/src/ui/dmenu-view/dmenu-view.hpp index 2dddf4e32..3f17d19dd 100644 --- a/vicinae/src/ui/dmenu-view/dmenu-view.hpp +++ b/vicinae/src/ui/dmenu-view/dmenu-view.hpp @@ -34,8 +34,8 @@ class View : public TypedListView { void selectEntry(const QString &text); void initialize() override; - std::vector m_entries; - std::vector> m_filteredEntries; + std::vector m_entries; + std::vector> m_filteredEntries; std::string_view m_sectionNameTemplate; std::string m_sectionName; bool m_selected = false;