Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/vicinae-ipc/include/vicinae-ipc/ipc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ struct DMenu {
std::optional<int> width;
std::optional<int> height;
bool noFooter = false;
bool preview = false;
};

struct Response {
Expand Down
2 changes: 2 additions & 0 deletions vicinae/src/cli/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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: label<TAB>preview_path)");
}

void run(CLI::App *app) override {
Expand Down
39 changes: 26 additions & 13 deletions vicinae/src/ui/dmenu-view/dmenu-model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,56 @@
#include "common/scored.hpp"
#include "utils.hpp"

class DMenuModel : public vicinae::ui::VerticalListModel<std::string_view> {
struct DMenuEntry {
std::string_view label;
std::string_view quickLookPath;
};

class DMenuModel : public vicinae::ui::VerticalListModel<DMenuEntry> {
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; }

int sectionItemCount(int id) const override { return m_entries.size(); }

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<Scored<std::string_view>> entries) {
void setEntries(std::span<Scored<DMenuEntry>> 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<Scored<std::string_view>> m_entries;
std::span<Scored<DMenuEntry>> m_entries;
};
44 changes: 32 additions & 12 deletions vicinae/src/ui/dmenu-view/dmenu-view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,39 @@
#include <ranges>

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<std::vector>();
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 (label<TAB>preview_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;
}

Expand Down Expand Up @@ -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<std::string_view>{.data = s, .score = score};
auto toScore = [&](const DMenuEntry &entry) {
int score = fzf::defaultMatcher.fuzzy_match_v2_score_query(entry.label, query);
return Scored<DMenuEntry>{.data = entry, .score = score};
};

auto filtered = m_entries | std::views::transform(toScore) |
Expand All @@ -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<ListActionPanelState>();
auto main = panel->createSection();
auto select = new StaticAction("Select entry", ImageURL::builtin("save-document"),
Expand Down
4 changes: 2 additions & 2 deletions vicinae/src/ui/dmenu-view/dmenu-view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ class View : public TypedListView<DMenuModel> {
void selectEntry(const QString &text);
void initialize() override;

std::vector<std::string_view> m_entries;
std::vector<Scored<std::string_view>> m_filteredEntries;
std::vector<DMenuEntry> m_entries;
std::vector<Scored<DMenuEntry>> m_filteredEntries;
std::string_view m_sectionNameTemplate;
std::string m_sectionName;
bool m_selected = false;
Expand Down
Loading