From 971854bfefad4c644ac3d5f5d6003f24d551bf1d Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Mon, 10 Mar 2025 22:04:24 +0530 Subject: Add completer + simplify command parsing + class generator --- TODO.org | 8 ++++-- include/CommandParser.hpp | 9 +++---- include/LuaRuntime.hpp | 2 +- include/completion/CommandsModel.hpp | 32 ++++++++++++++++++++++ include/completion/Completer.hpp | 14 ++++++++++ include/completion/CompleterDelegate.hpp | 15 +++++++++++ include/widgets/CommandInput.hpp | 3 ++- src/CommandParser.cpp | 10 ++++--- src/LuaRuntime.cpp | 4 ++- src/completion/CommandsModel.cpp | 42 +++++++++++++++++++++++++++++ src/completion/Completer.cpp | 46 ++++++++++++++++++++++++++++++++ src/completion/CompleterDelegate.cpp | 40 +++++++++++++++++++++++++++ src/widgets/CommandInput.cpp | 40 ++++++++++++++++++++++++--- src/widgets/MainWindow.cpp | 11 ++++---- templates/qclass.sh | 34 +++++++++++++++++++++++ 15 files changed, 287 insertions(+), 23 deletions(-) create mode 100644 include/completion/CommandsModel.hpp create mode 100644 include/completion/Completer.hpp create mode 100644 include/completion/CompleterDelegate.hpp create mode 100644 src/completion/CommandsModel.cpp create mode 100644 src/completion/Completer.cpp create mode 100644 src/completion/CompleterDelegate.cpp create mode 100755 templates/qclass.sh diff --git a/TODO.org b/TODO.org index d101052..7c0ff36 100644 --- a/TODO.org +++ b/TODO.org @@ -1,14 +1,18 @@ ** Current - [X] Multi tabs - [X] Open in new tab/new window -- [ ] Lua for command input +- [X] Lua for command input +- [X] Simpler shell-ey/vimscript-ey language for command input +- [X] Command completion +- [ ] Buffer list/search - [ ] Modal keys - [ ] Tab history navigation +- [ ] Refactor MainWindow ** Next - [ ] History persistance +- [ ] History completion - [ ] Sketch lua api for managing ui -- [ ] Simpler shell-ey/vimscript-ey language for command input ** Later - [ ] Fullscreen diff --git a/include/CommandParser.hpp b/include/CommandParser.hpp index 128b5f4..dea04c5 100644 --- a/include/CommandParser.hpp +++ b/include/CommandParser.hpp @@ -12,13 +12,10 @@ enum CommandType { TabPrev, }; -class Cmd { -public: +struct Cmd { CommandType command; - QStringList args; - - Cmd(CommandType command = Noop, QStringList args = QStringList()) - : command(command), args(args) {} + QString argsString; + QString rawInput; }; class CommandParser { diff --git a/include/LuaRuntime.hpp b/include/LuaRuntime.hpp index 85bb1ae..96c0004 100644 --- a/include/LuaRuntime.hpp +++ b/include/LuaRuntime.hpp @@ -13,7 +13,7 @@ public: return &inst; } - void evaluate(const char *code); + void evaluate(QString code); protected: LuaRuntime(); diff --git a/include/completion/CommandsModel.hpp b/include/completion/CommandsModel.hpp new file mode 100644 index 0000000..6cb25f9 --- /dev/null +++ b/include/completion/CommandsModel.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +struct Command { + QString name; + QString description; +}; + +const QList commands = { + {.name = "open", .description = "Open a url in the current tab"}, + {.name = "tabopen", .description = "Open a url in a new tab"}, + {.name = "bnext", .description = "Go to next tab"}, + {.name = "bprev", .description = "Go to previous tab"}, +}; + +class CommandsModel : public QAbstractListModel { + Q_OBJECT + +public: + CommandsModel(QObject *parent = nullptr); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + QVariant data(const QModelIndex &index, + int role = Qt::DisplayRole) const override; + +private: + QList items; +}; diff --git a/include/completion/Completer.hpp b/include/completion/Completer.hpp new file mode 100644 index 0000000..b67de61 --- /dev/null +++ b/include/completion/Completer.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include + +class Completer : public QCompleter { + Q_OBJECT + +public: + Completer(QObject *parent = nullptr); + + // bool eventFilter(QObject *watched, QEvent *event) override; +}; diff --git a/include/completion/CompleterDelegate.hpp b/include/completion/CompleterDelegate.hpp new file mode 100644 index 0000000..fc7a321 --- /dev/null +++ b/include/completion/CompleterDelegate.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + +class CompleterDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + explicit CompleterDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; diff --git a/include/widgets/CommandInput.hpp b/include/widgets/CommandInput.hpp index a6ca0ed..3ce9500 100644 --- a/include/widgets/CommandInput.hpp +++ b/include/widgets/CommandInput.hpp @@ -1,8 +1,9 @@ #pragma once #include -#include #include +#include +#include class CommandInput : public QWidget { Q_OBJECT diff --git a/src/CommandParser.cpp b/src/CommandParser.cpp index ccb84bc..7a8eb00 100644 --- a/src/CommandParser.cpp +++ b/src/CommandParser.cpp @@ -9,11 +9,15 @@ Cmd CommandParser::parse(QString input) { auto parts = input.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); if (parts.isEmpty()) - return Cmd(); + return {.command = Noop, .argsString = "", .rawInput = input}; + + auto cmdStr = parts.first(); + auto cmd = toCommandType(cmdStr); + auto rawArgs = input.slice(cmdStr.length()); - auto cmd = toCommandType(parts.first()); parts.removeFirst(); - return Cmd(cmd, parts); + + return {.command = cmd, .argsString = rawArgs, .rawInput = input}; } CommandType CommandParser::toCommandType(QString cmd) { diff --git a/src/LuaRuntime.cpp b/src/LuaRuntime.cpp index 37845d3..6867b61 100644 --- a/src/LuaRuntime.cpp +++ b/src/LuaRuntime.cpp @@ -15,7 +15,9 @@ LuaRuntime::LuaRuntime() { lua_setglobal(state, "web"); } -void LuaRuntime::evaluate(const char *code) { luaL_dostring(state, code); } +void LuaRuntime::evaluate(QString code) { + luaL_dostring(state, code.toStdString().c_str()); +} int LuaRuntime::lua_onUrlOpen(lua_State *state) { const char *url = luaL_optstring(state, 1, ""); diff --git a/src/completion/CommandsModel.cpp b/src/completion/CommandsModel.cpp new file mode 100644 index 0000000..0f66233 --- /dev/null +++ b/src/completion/CommandsModel.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "completion/CommandsModel.hpp" + +CommandsModel::CommandsModel(QObject *parent) : QAbstractListModel(parent) { + items = commands; +} + +QVariant CommandsModel::data(const QModelIndex &index, int role) const { + if (!index.isValid() || index.row() >= items.size()) + return QVariant(); + + const Command &item = items.at(index.row()); + + if (role == Qt::DisplayRole) + return item.name; + else if (role == Qt::ToolTipRole) + return item.description; + else if (role == Qt::UserRole) + return item.description; + + return QVariant(); +} + +QHash CommandsModel::roleNames() const { + QHash roles; + roles[Qt::DisplayRole] = "name"; + roles[Qt::UserRole] = "description"; + return roles; +} + +int CommandsModel::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return items.size(); +} + +int CommandsModel::columnCount(const QModelIndex &parent) const { + Q_UNUSED(parent); + return 2; // name + description +} diff --git a/src/completion/Completer.cpp b/src/completion/Completer.cpp new file mode 100644 index 0000000..96a363e --- /dev/null +++ b/src/completion/Completer.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include +#include + +#include "completion/Completer.hpp" +#include "completion/CompleterDelegate.hpp" + +const char *completerStyles = R"( + background-color: #111; + color: #fff; + border-radius: 0; +)"; + +Completer::Completer(QObject *parent) : QCompleter(parent) { + setFilterMode(Qt::MatchContains); + setCaseSensitivity(Qt::CaseInsensitive); + setCompletionRole(Qt::DisplayRole); + setCompletionMode(QCompleter::PopupCompletion); + + auto treeView = new QTreeView(); + setPopup(treeView); + treeView->setStyleSheet(completerStyles); + treeView->setItemDelegate(new CompleterDelegate(treeView)); + treeView->setRootIsDecorated(false); + treeView->setUniformRowHeights(true); + treeView->header()->hide(); + treeView->setColumnWidth(1, 160); + + complete(); +} + +// bool Completer::eventFilter(QObject *watched, QEvent *event) { +// auto isFocusIn = event->type() == QEvent::FocusIn; +// auto isKeyRelease = event->type() == QEvent::KeyRelease; +// +// QLineEdit *lineEdit = qobject_cast(watched); +// bool isInputEmpty = lineEdit && lineEdit->text().isEmpty(); +// +// if ((isFocusIn || isKeyRelease) && isInputEmpty) +// complete(); +// +// return QCompleter::eventFilter(watched, event); +// } diff --git a/src/completion/CompleterDelegate.cpp b/src/completion/CompleterDelegate.cpp new file mode 100644 index 0000000..a35305f --- /dev/null +++ b/src/completion/CompleterDelegate.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include + +#include "completion/CompleterDelegate.hpp" + +CompleterDelegate::CompleterDelegate(QObject *parent) + : QStyledItemDelegate(parent) {} + +void CompleterDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const { + painter->save(); + + // Style + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, QColor("#aaa")); + painter->setPen(Qt::black); + } else { + painter->fillRect(option.rect, QColor("#111")); + painter->setPen(Qt::white); + } + + // Draw text + QString text; + QRect rect; + if (index.column() == 0) { + text = index.data(Qt::DisplayRole).toString(); + rect = option.rect.adjusted(5, 0, 0, 0); + painter->drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, text); + } else if (index.column() == 1) { + text = index.data(Qt::UserRole).toString(); + rect = option.rect.adjusted(0, 0, -5, 0); + painter->drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, text); + } + + painter->restore(); +} diff --git a/src/widgets/CommandInput.cpp b/src/widgets/CommandInput.cpp index 464065a..3ea0f38 100644 --- a/src/widgets/CommandInput.cpp +++ b/src/widgets/CommandInput.cpp @@ -1,23 +1,57 @@ +#include #include #include +#include #include +#include #include #include +#include #include +#include +#include +#include +#include "completion/CommandsModel.hpp" +#include "completion/Completer.hpp" #include "widgets/CommandInput.hpp" +const char *rootStyles = R"( + background-color: #000; + color: #fff; + border-radius: 0; +)"; + CommandInput::CommandInput(QString defaultInput, QWidget *parentNode) : QWidget(parentNode) { setContentsMargins(0, 0, 0, 0); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - setStyleSheet("background-color: #000; color: #fff; border-radius: 0;"); + setStyleSheet(rootStyles); auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); - input = new QLineEdit(defaultInput, this); + + auto listModel = new CommandsModel(); + auto completer = new Completer(this); + completer->setModel(listModel); + + auto cmdLineBox = new QWidget(); + layout->addWidget(cmdLineBox); + auto cmdLineLayout = new QHBoxLayout(); + cmdLineBox->setLayout(cmdLineLayout); + cmdLineBox->layout()->setContentsMargins(0, 0, 0, 0); + cmdLineLayout->setSpacing(0); + + auto promptPrefix = new QLabel(tr(":")); + promptPrefix->setContentsMargins(0, 0, 0, 0); + input = new QLineEdit(defaultInput); input->setFocusPolicy(Qt::StrongFocus); - layout->addWidget(input); + input->setCompleter(completer); + // input->installEventFilter(completer); + + cmdLineLayout->addWidget(promptPrefix); + cmdLineLayout->addWidget(input); + setFixedHeight(input->sizeHint().height()); } diff --git a/src/widgets/MainWindow.cpp b/src/widgets/MainWindow.cpp index 9ac5f12..3191719 100644 --- a/src/widgets/MainWindow.cpp +++ b/src/widgets/MainWindow.cpp @@ -37,7 +37,7 @@ MainWindow::MainWindow() { luaRuntime = LuaRuntime::instance(); connect(luaRuntime, &LuaRuntime::urlOpenned, this, [this](QString url, OpenType openType) { - this->browserManager->openUrl(QUrl(url), openType); + browserManager->openUrl(QUrl(url), openType); }); } @@ -56,19 +56,18 @@ void MainWindow::showURLInput() { void MainWindow::evaluateCommand(QString command) { hideURLInput(); - // TODO: Temporary hack CommandParser parser; auto cmd = parser.parse(command); switch (cmd.command) { case CommandType::LuaEval: - luaRuntime->evaluate(cmd.args.join(" ").toStdString().c_str()); + luaRuntime->evaluate(cmd.argsString); break; case CommandType::Open: - browserManager->openUrl(cmd.args.first(), OpenType::OpenUrl); + browserManager->openUrl(cmd.argsString, OpenType::OpenUrl); break; case CommandType::TabOpen: - browserManager->openUrl(cmd.args.at(1), OpenType::OpenUrlInTab); + browserManager->openUrl(cmd.argsString, OpenType::OpenUrlInTab); break; case CommandType::TabNext: browserManager->nextWebView(); @@ -88,7 +87,7 @@ void MainWindow::keyPressEvent(QKeyEvent *event) { toggleURLInput(); } else if (combo.key() == Qt::Key_T && combo.keyboardModifiers().testFlag(Qt::ControlModifier)) { - browserManager->createNewWebView(QUrl("https://duckduckgo.com"), true); + browserManager->createNewWebView(QUrl("https://lite.duckduckgo.com"), true); } else if (combo.key() == Qt::Key_J && combo.keyboardModifiers().testFlag(Qt::ControlModifier)) { browserManager->nextWebView(); diff --git a/templates/qclass.sh b/templates/qclass.sh new file mode 100755 index 0000000..8ad085a --- /dev/null +++ b/templates/qclass.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env sh + +set -e -o pipefail + +class_name="$1" +path="$2" + +[ -z "$class_name" ] && exit 1 + +mkdir -p "./include/$path"; +mkdir -p "./src/$path"; + +# Header +echo "#pragma once + +#include +#include + +class $class_name : public QWidget { + Q_OBJECT + + public: + $class_name(); +}; +" > "./include/$path/$class_name.hpp" + +# Implementation +echo "#include +#include + +#include \"$path/$class_name.hpp\" + +$class_name::$class_name(): QWidget() {} +" > "./src/$path/$class_name.cpp" -- cgit v1.3.1