diff --git a/CMakeLists.txt b/CMakeLists.txt index d57e322b..6dfead37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES boption(X11 "X11" ON) boption(I3 "I3/Sway" ON) boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) +boption(DWL "DWL" ON) boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_MPRIS "Mpris" ON) diff --git a/default.nix b/default.nix index 59e68b05..95a229d5 100644 --- a/default.nix +++ b/default.nix @@ -48,6 +48,7 @@ withPam ? true, withHyprland ? true, withI3 ? true, + withDWL ? true, withPolkit ? true, withNetworkManager ? true, }: let @@ -109,6 +110,7 @@ (lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager) (lib.cmakeBool "SERVICE_POLKIT" withPolkit) (lib.cmakeBool "HYPRLAND" withHyprland) + (lib.cmakeBool "DWL" withDWL) (lib.cmakeBool "I3" withI3) ]; diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ca49c8f7..6a263ebf 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -114,6 +114,10 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() +if (DWL) + add_subdirectory(dwl) +endif() + add_subdirectory(idle_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) diff --git a/src/wayland/dwl/CMakeLists.txt b/src/wayland/dwl/CMakeLists.txt new file mode 100644 index 00000000..3424997f --- /dev/null +++ b/src/wayland/dwl/CMakeLists.txt @@ -0,0 +1,44 @@ +qt_add_library(quickshell-dwl STATIC + output.cpp + tag.cpp + manager.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-dwl + URI Quickshell.DWL + VERSION 0.1 +) + +install_qml_module(quickshell-dwl) +wl_proto(wlp-dwl-ipc dwl-ipc-unstable-v2 "${CMAKE_CURRENT_SOURCE_DIR}") + +target_precompile_headers(quickshell-dwl PRIVATE + + + + + + + + + + + + +) + +target_link_libraries(quickshell-dwl PRIVATE + Qt6::Core + Qt6::Gui + Qt6::GuiPrivate + Qt6::Widgets + Qt6::WaylandClient + Qt6::WaylandClientPrivate + Wayland::Client + wlp-dwl-ipc +) + +target_link_libraries(quickshell PRIVATE + quickshell-dwl +) diff --git a/src/wayland/dwl/dwl-ipc-unstable-v2.xml b/src/wayland/dwl/dwl-ipc-unstable-v2.xml new file mode 100644 index 00000000..cf655172 --- /dev/null +++ b/src/wayland/dwl/dwl-ipc-unstable-v2.xml @@ -0,0 +1,292 @@ + + + + + This protocol allows clients to update and get updates from dwl. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible + changes may be added together with the corresponding interface + version bump. + Backward incompatible changes are done by bumping the version + number in the protocol and interface names and resetting the + interface version. Once the protocol is to be declared stable, + the 'z' prefix and the version number in the protocol and + interface names are removed and the interface version number is + reset. + + + + + This interface is exposed as a global in wl_registry. + + Clients can use this interface to get a dwl_ipc_output. + After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events. + The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client. + + + + + Indicates that the client will not the dwl_ipc_manager object anymore. + Objects created through this instance are not affected. + + + + + + Get a dwl_ipc_outout for the specified wl_output. + + + + + + + + This event is sent after binding. + A roundtrip after binding guarantees the client recieved all tags. + + + + + + + This event is sent after binding. + A roundtrip after binding guarantees the client recieved all layouts. + + + + + + + + Observe and control a dwl output. + + Events are double-buffered: + Clients should cache events and redraw when a dwl_ipc_output.frame event is sent. + + Request are not double-buffered: + The compositor will update immediately upon request. + + + + + + + + + + + Indicates to that the client no longer needs this dwl_ipc_output. + + + + + + Indicates the client should hide or show themselves. + If the client is visible then hide, if hidden then show. + + + + + + Indicates if the output is active. Zero is invalid, nonzero is valid. + + + + + + + Indicates that a tag has been updated. + + + + + + + + + + Indicates a new layout is selected. + + + + + + + Indicates the title has changed. + + + + + + + Indicates the appid has changed. + + + + + + + Indicates the layout has changed. Since layout symbols are dynamic. + As opposed to the zdwl_ipc_manager.layout event, this should take precendence when displaying. + You can ignore the zdwl_ipc_output.layout event. + + + + + + + Indicates that a sequence of status updates have finished and the client should redraw. + + + + + + + + + + + + The tags are updated as follows: + new_tags = (current_tags AND and_tags) XOR xor_tags + + + + + + + + + + + + This request allows clients to instruct the compositor to quit mango. + + + + + + + + + + + + + + + + Indicates if the selected client on this output is fullscreen. + + + + + + + Indicates if the selected client on this output is floating. + + + + + + + Indicates if x coordinates of the selected client. + + + + + + + Indicates if y coordinates of the selected client. + + + + + + + Indicates if width of the selected client. + + + + + + + Indicates if height of the selected client. + + + + + + + last map layer. + + + + + + + current keyboard layout. + + + + + + + current keybind mode. + + + + + + + scale factor of monitor. + + + + + + + diff --git a/src/wayland/dwl/manager.cpp b/src/wayland/dwl/manager.cpp new file mode 100644 index 00000000..8651ac4e --- /dev/null +++ b/src/wayland/dwl/manager.cpp @@ -0,0 +1,99 @@ +#include "manager.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "output.hpp" + +namespace { +QS_LOGGING_CATEGORY(logDwlIpc, "quickshell.dwl", QtWarningMsg); +} + +namespace qs::dwl { + +DwlIpcManager::DwlIpcManager(): QWaylandClientExtensionTemplate(2) { + QObject::connect(this, &QWaylandClientExtension::activeChanged, this, [this]() { + if (!this->isActive()) { + qCWarning(logDwlIpc) << "DWL is not available"; + return; + } + + const auto screens = QGuiApplication::screens(); + for (QScreen* screen: screens) this->onScreenAdded(screen); + + QObject::connect(qApp, &QGuiApplication::screenAdded, this, &DwlIpcManager::onScreenAdded); + QObject::connect(qApp, &QGuiApplication::screenRemoved, this, &DwlIpcManager::onScreenRemoved); + }); +} + +DwlIpcManager* DwlIpcManager::instance() { + static auto* instance = new DwlIpcManager(); + return instance; +} + +quint32 DwlIpcManager::tagCount() const { return this->mTagCount; } +QStringList DwlIpcManager::layouts() const { return this->mLayouts; } +QList DwlIpcManager::outputs() const { return this->mOutputs; } + +void DwlIpcManager::onScreenAdded(QScreen* screen) { + auto* waylandScreen = dynamic_cast(screen->handle()); + if (!waylandScreen) return; + this->bindOutput(waylandScreen->output(), screen->name()); +} + +void DwlIpcManager::onScreenRemoved(QScreen* screen) { + auto* waylandScreen = dynamic_cast(screen->handle()); + if (!waylandScreen) return; + this->removeOutput(waylandScreen->output()); +} + +DwlIpcOutput* DwlIpcManager::bindOutput(struct wl_output* wlOutput, const QString& name) { + if (auto* existing = this->mOutputMap.value(wlOutput, nullptr)) return existing; + + auto* output = new DwlIpcOutput(this->get_output(wlOutput), name, this); + output->initTags(this->mTagCount); + this->mOutputs.append(output); + this->mOutputMap.insert(wlOutput, output); + emit this->outputAdded(output); + + return output; +} + +void DwlIpcManager::removeOutput(struct wl_output* wlOutput) { + auto* output = this->mOutputMap.take(wlOutput); + if (!output) return; + + this->mOutputs.removeOne(output); + emit this->outputRemoved(output); + output->deleteLater(); +} + +void DwlIpcManager::zdwl_ipc_manager_v2_tags(uint32_t amount) { + if (amount == this->mTagCount) return; + this->mTagCount = amount; + for (DwlIpcOutput* o: this->mOutputs) o->initTags(amount); + emit this->tagCountChanged(); +} + +void DwlIpcManager::zdwl_ipc_manager_v2_layout(const QString& name) { + this->mLayouts.append(name); + emit this->layoutsChanged(); +} + +} // namespace qs::dwl diff --git a/src/wayland/dwl/manager.hpp b/src/wayland/dwl/manager.hpp new file mode 100644 index 00000000..c06ef5af --- /dev/null +++ b/src/wayland/dwl/manager.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "output.hpp" + +namespace qs::dwl { + +class DwlIpcManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zdwl_ipc_manager_v2 { + Q_OBJECT; + +public: + explicit DwlIpcManager(); + + [[nodiscard]] quint32 tagCount() const; + [[nodiscard]] QStringList layouts() const; + [[nodiscard]] QList outputs() const; + + DwlIpcOutput* bindOutput(struct wl_output* wlOutput, const QString& name); + void removeOutput(struct wl_output* wlOutput); + + static DwlIpcManager* instance(); + +signals: + void tagCountChanged(); + void layoutsChanged(); + void outputAdded(DwlIpcOutput* output); + void outputRemoved(DwlIpcOutput* output); + +protected: + void zdwl_ipc_manager_v2_tags(uint32_t amount) override; + void zdwl_ipc_manager_v2_layout(const QString& name) override; + +private slots: + void onScreenAdded(QScreen* screen); + void onScreenRemoved(QScreen* screen); + +private: + quint32 mTagCount = 0; + QStringList mLayouts; + QList mOutputs; + QHash mOutputMap; +}; + +} // namespace qs::dwl diff --git a/src/wayland/dwl/module.md b/src/wayland/dwl/module.md new file mode 100644 index 00000000..3b6c08ff --- /dev/null +++ b/src/wayland/dwl/module.md @@ -0,0 +1,8 @@ +name = "Quickshell.DWL" +description = "DWL (+Mango) unstable-v2" +headers = [ + "output.hpp", + "tag.hpp", + "qml.hpp", +] +----- diff --git a/src/wayland/dwl/output.cpp b/src/wayland/dwl/output.cpp new file mode 100644 index 00000000..a346528c --- /dev/null +++ b/src/wayland/dwl/output.cpp @@ -0,0 +1,122 @@ +#include "output.hpp" +#include +#include + +#include +#include +#include +#include +#include + +#include "qwayland-dwl-ipc-unstable-v2.h" +#include "tag.hpp" +#include "wayland-dwl-ipc-unstable-v2-client-protocol.h" + +namespace qs::dwl { + +DwlIpcOutput::DwlIpcOutput(::zdwl_ipc_output_v2* handle, QString name, QObject* parent) + : QObject(parent) + , QtWayland::zdwl_ipc_output_v2(handle) + , mOutputName(std::move(name)) {} + +DwlIpcOutput::~DwlIpcOutput() { + if (this->isInitialized()) this->release(); +} + +bool DwlIpcOutput::active() const { return this->mActive; } +quint32 DwlIpcOutput::layoutIndex() const { return this->mLayoutIndex; } +QString DwlIpcOutput::layoutSymbol() const { return this->mLayoutSymbol; } +bool DwlIpcOutput::floating() const { return this->mFloating; } +QList DwlIpcOutput::tags() const { return this->mTags; } +QString DwlIpcOutput::kbLayout() const { return this->mKbLayout; } +const QString& DwlIpcOutput::outputName() const { return this->mOutputName; } + +void DwlIpcOutput::setTags(quint32 tagmask, quint32 toggleTagset) { + this->set_tags(tagmask, toggleTagset); +} + +void DwlIpcOutput::setClientTags(quint32 andTags, quint32 xorTags) { + this->set_client_tags(andTags, xorTags); +} + +void DwlIpcOutput::setLayout(quint32 index) { this->set_layout(index); } + +void DwlIpcOutput::initTags(quint32 count) { + for (DwlTag* t: this->mTags) t->deleteLater(); + this->mTags.clear(); + this->mTags.reserve(static_cast(count)); + + for (quint32 i = 0; i < count; ++i) this->mTags.append(new DwlTag(i, this)); + emit this->tagsChanged(); +} + +void DwlIpcOutput::zdwl_ipc_output_v2_toggle_visibility() { emit this->toggleVisibility(); } +void DwlIpcOutput::zdwl_ipc_output_v2_active(uint32_t active) { + this->mPending.hasActive = true; + this->mPending.active = active != 0; +} + +void DwlIpcOutput::zdwl_ipc_output_v2_tag( + uint32_t tag, + uint32_t state, + uint32_t clients, + uint32_t focused +) { + if (std::cmp_less(tag, this->mTags.size())) + this->mTags[static_cast(tag)]->updateState(state, clients, focused); +} + +void DwlIpcOutput::zdwl_ipc_output_v2_layout(uint32_t layout) { + this->mPending.hasLayoutIndex = true; + this->mPending.layoutIndex = layout; +} + +void DwlIpcOutput::zdwl_ipc_output_v2_layout_symbol(const QString& layout) { + this->mPending.hasLayoutSymbol = true; + this->mPending.layoutSymbol = layout; +} + +void DwlIpcOutput::zdwl_ipc_output_v2_floating(uint32_t isFloating) { + this->mPending.hasFloating = true; + this->mPending.floating = isFloating != 0; +} + +void DwlIpcOutput::zdwl_ipc_output_v2_kb_layout(const QString& kbLayout) { + this->mPending.hasKbLayout = true; + this->mPending.kbLayout = kbLayout; +} + +void DwlIpcOutput::zdwl_ipc_output_v2_frame() { + auto& p = this->mPending; + + if (p.hasActive && p.active != this->mActive) { + this->mActive = p.active; + emit this->activeChanged(); + } + + if (p.hasLayoutIndex && p.layoutIndex != this->mLayoutIndex) { + this->mLayoutIndex = p.layoutIndex; + emit this->layoutIndexChanged(); + } + + if (p.hasLayoutSymbol && p.layoutSymbol != this->mLayoutSymbol) { + this->mLayoutSymbol = p.layoutSymbol; + emit this->layoutSymbolChanged(); + } + + if (p.hasFloating && p.floating != this->mFloating) { + this->mFloating = p.floating; + emit this->floatingChanged(); + } + + if (p.hasKbLayout && p.kbLayout != this->mKbLayout) { + this->mKbLayout = p.kbLayout; + emit this->kbLayoutChanged(); + } + + p = {}; + + emit this->frame(); +} + +} // namespace qs::dwl diff --git a/src/wayland/dwl/output.hpp b/src/wayland/dwl/output.hpp new file mode 100644 index 00000000..bcd51917 --- /dev/null +++ b/src/wayland/dwl/output.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "tag.hpp" + +namespace qs::dwl { + +///! A DWL monitor/output with IPC state. +/// Exposes per-monitor compositor state: tag list, active layout, focused +/// window title, app ID, and fullscreen/floating flags. +/// +/// Obtain instances via @@DwlIpc.outputs. +class DwlIpcOutput + : public QObject + , public QtWayland::zdwl_ipc_output_v2 { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("DwlIpcOutput instances are created by DwlIpc."); + + /// Whether this output is currently focused. + Q_PROPERTY(bool active READ active NOTIFY activeChanged); + /// Index into @@DwlIpc.layouts of the active layout. + Q_PROPERTY(quint32 layoutIndex READ layoutIndex NOTIFY layoutIndexChanged); + /// Current layout symbol string (e.g. "[]="). Use this for display. + Q_PROPERTY(QString layoutSymbol READ layoutSymbol NOTIFY layoutSymbolChanged); + /// Whether the focused client is floating. + Q_PROPERTY(bool floating READ floating NOTIFY floatingChanged); + /// Tag state list. Length equals @@DwlIpc.tagCount. + Q_PROPERTY(QList tags READ tags NOTIFY tagsChanged); + /// Keyboard layout. + Q_PROPERTY(QString kbLayout READ kbLayout NOTIFY kbLayoutChanged); + +public: + explicit DwlIpcOutput(::zdwl_ipc_output_v2* handle, QString name, QObject* parent = nullptr); + + ~DwlIpcOutput() override; + + DwlIpcOutput(const DwlIpcOutput&) = delete; + DwlIpcOutput& operator=(const DwlIpcOutput&) = delete; + DwlIpcOutput(DwlIpcOutput&&) = delete; + DwlIpcOutput& operator=(DwlIpcOutput&&) = delete; + + [[nodiscard]] bool active() const; + [[nodiscard]] quint32 layoutIndex() const; + [[nodiscard]] QString layoutSymbol() const; + [[nodiscard]] bool floating() const; + [[nodiscard]] QString kbLayout() const; + [[nodiscard]] QList tags() const; + [[nodiscard]] const QString& outputName() const; + + /// Set active tags. + Q_INVOKABLE void setTags(quint32 tagmask, quint32 toggleTagset = 0); + /// Set focused client tags. + Q_INVOKABLE void setClientTags(quint32 andTags, quint32 xorTags); + /// Select layout by index (from @@DwlIpc.layouts). + Q_INVOKABLE void setLayout(quint32 index); + + void initTags(quint32 count); + +signals: + void activeChanged(); + void layoutIndexChanged(); + void layoutSymbolChanged(); + void fullscreenChanged(); + void floatingChanged(); + void toggleVisibility(); + void kbLayoutChanged(); + void tagsChanged(); + /// Emitted when all double-buffered state for this frame has been committed. + void frame(); + +protected: + void zdwl_ipc_output_v2_toggle_visibility() override; + void zdwl_ipc_output_v2_active(uint32_t active) override; + void zdwl_ipc_output_v2_layout(uint32_t layout) override; + void zdwl_ipc_output_v2_layout_symbol(const QString& layout) override; + void zdwl_ipc_output_v2_frame() override; + void zdwl_ipc_output_v2_floating(uint32_t isFloating) override; + void zdwl_ipc_output_v2_kb_layout(const QString& kbLayout) override; + + void + zdwl_ipc_output_v2_tag(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused) override; + +private: + QString mOutputName; + + // Committed state, updated atomically on frame. + bool mActive = false; + QList mTags; + quint32 mLayoutIndex = 0; + QString mLayoutSymbol; + QString mKbLayout; + bool mFloating = false; + + // Pending state accumulated between frame events. + struct { + bool active = false; + quint32 layoutIndex = 0; + QString layoutSymbol; + QString kbLayout; + bool floating = false; + bool hasActive : 1 = false; + bool hasLayoutIndex : 1 = false; + bool hasLayoutSymbol : 1 = false; + bool hasFloating : 1 = false; + bool hasKbLayout : 1 = false; + } mPending; +}; + +} // namespace qs::dwl diff --git a/src/wayland/dwl/qml.cpp b/src/wayland/dwl/qml.cpp new file mode 100644 index 00000000..b8c9b9c4 --- /dev/null +++ b/src/wayland/dwl/qml.cpp @@ -0,0 +1,44 @@ +#include "qml.hpp" + +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "output.hpp" + +namespace qs::dwl { + +DwlIpcQml::DwlIpcQml(QObject* parent): QObject(parent) { + auto* manager = DwlIpcManager::instance(); + this->manager = manager; + + QObject::connect(manager, &DwlIpcManager::tagCountChanged, this, &DwlIpcQml::tagCountChanged); + QObject::connect(manager, &DwlIpcManager::layoutsChanged, this, &DwlIpcQml::layoutsChanged); + QObject::connect(manager, &DwlIpcManager::outputAdded, this, &DwlIpcQml::outputsChanged); + QObject::connect(manager, &DwlIpcManager::outputRemoved, this, &DwlIpcQml::outputsChanged); + QObject::connect( + manager, + &QWaylandClientExtension::activeChanged, + this, + &DwlIpcQml::availableChanged + ); +} + +quint32 DwlIpcQml::tagCount() const { return this->manager->tagCount(); } +// NOLINTNEXTLINE(misc-include-cleaner) +QStringList DwlIpcQml::layouts() const { return this->manager->layouts(); } +QList DwlIpcQml::outputs() const { return this->manager->outputs(); } +bool DwlIpcQml::available() const { return this->manager->isActive(); } + +DwlIpcOutput* DwlIpcQml::outputForName(const QString& name) const { + for (DwlIpcOutput* o: this->manager->outputs()) + if (o->outputName() == name) return o; + + return nullptr; +} + +} // namespace qs::dwl diff --git a/src/wayland/dwl/qml.hpp b/src/wayland/dwl/qml.hpp new file mode 100644 index 00000000..a13ab462 --- /dev/null +++ b/src/wayland/dwl/qml.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "output.hpp" + +namespace qs::dwl { + +class DwlIpcQml: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + QML_NAMED_ELEMENT(DwlIpc); + + Q_PROPERTY(quint32 tagCount READ tagCount NOTIFY tagCountChanged); + Q_PROPERTY(QStringList layouts READ layouts NOTIFY layoutsChanged); + Q_PROPERTY(QList outputs READ outputs NOTIFY outputsChanged); + Q_PROPERTY(bool available READ available NOTIFY availableChanged); + +public: + explicit DwlIpcQml(QObject* parent = nullptr); + + [[nodiscard]] quint32 tagCount() const; + [[nodiscard]] QStringList layouts() const; + [[nodiscard]] QList outputs() const; + [[nodiscard]] bool available() const; + + [[nodiscard]] Q_INVOKABLE qs::dwl::DwlIpcOutput* outputForName(const QString& name) const; + +private: + DwlIpcManager* manager = nullptr; + +signals: + void tagCountChanged(); + void layoutsChanged(); + void outputsChanged(); + void availableChanged(); +}; + +} // namespace qs::dwl diff --git a/src/wayland/dwl/tag.cpp b/src/wayland/dwl/tag.cpp new file mode 100644 index 00000000..a1af280d --- /dev/null +++ b/src/wayland/dwl/tag.cpp @@ -0,0 +1,44 @@ +#include "tag.hpp" + +#include +#include +#include + +#include "wayland-dwl-ipc-unstable-v2-client-protocol.h" + +namespace qs::dwl { + +DwlTag::DwlTag(quint32 index, QObject* parent): QObject(parent), mIndex(index) {} + +quint32 DwlTag::index() const { return this->mIndex; } +bool DwlTag::active() const { return this->mActive; } +bool DwlTag::urgent() const { return this->mUrgent; } +quint32 DwlTag::clientCount() const { return this->mClientCount; } +quint32 DwlTag::focusedClient() const { return this->mFocusedClient; } + +void DwlTag::updateState(quint32 state, quint32 clients, quint32 focused) { + const bool newActive = (state & ZDWL_IPC_OUTPUT_V2_TAG_STATE_ACTIVE) != 0; + const bool newUrgent = (state & ZDWL_IPC_OUTPUT_V2_TAG_STATE_URGENT) != 0; + + if (newActive != this->mActive) { + this->mActive = newActive; + emit this->activeChanged(); + } + + if (newUrgent != this->mUrgent) { + this->mUrgent = newUrgent; + emit this->urgentChanged(); + } + + if (clients != this->mClientCount) { + this->mClientCount = clients; + emit this->clientCountChanged(); + } + + if (focused != this->mFocusedClient) { + this->mFocusedClient = focused; + emit this->focusedClientChanged(); + } +} + +} // namespace qs::dwl diff --git a/src/wayland/dwl/tag.hpp b/src/wayland/dwl/tag.hpp new file mode 100644 index 00000000..0547aa74 --- /dev/null +++ b/src/wayland/dwl/tag.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +#include "wayland-dwl-ipc-unstable-v2-client-protocol.h" + +namespace qs::dwl { + +///! State of a single DWL tag. +/// Represents one tag slot on @@DwlIpcOutput. +class DwlTag: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("DwlTag instances are created by DwlIpcOutput."); + + /// Zero-based index of this tag. + Q_PROPERTY(quint32 index READ index CONSTANT); + /// Whether this tag is currently active on its output. + Q_PROPERTY(bool active READ active NOTIFY activeChanged); + /// Whether any client on this tag is urgent. + Q_PROPERTY(bool urgent READ urgent NOTIFY urgentChanged); + /// Number of clients assigned to this tag. + Q_PROPERTY(quint32 clientCount READ clientCount NOTIFY clientCountChanged); + /// Nonzero index of the focused client within this tag, 0 if none. + Q_PROPERTY(quint32 focusedClient READ focusedClient NOTIFY focusedClientChanged); + +public: + explicit DwlTag(quint32 index, QObject* parent = nullptr); + + [[nodiscard]] quint32 index() const; + [[nodiscard]] bool active() const; + [[nodiscard]] bool urgent() const; + [[nodiscard]] quint32 clientCount() const; + [[nodiscard]] quint32 focusedClient() const; + + void updateState(quint32 state, quint32 clients, quint32 focused); + +signals: + void activeChanged(); + void urgentChanged(); + void clientCountChanged(); + void focusedClientChanged(); + +private: + quint32 mIndex; + bool mActive = false; + bool mUrgent = false; + quint32 mClientCount = 0; + quint32 mFocusedClient = 0; +}; + +} // namespace qs::dwl diff --git a/src/wayland/dwl/test/manual/bar.qml b/src/wayland/dwl/test/manual/bar.qml new file mode 100644 index 00000000..95f9d989 --- /dev/null +++ b/src/wayland/dwl/test/manual/bar.qml @@ -0,0 +1,135 @@ +import Quickshell +import Quickshell.Wayland +import Quickshell.DWL +import QtQuick +import QtQuick.Layouts + +ShellRoot { + Scope { + Variants { + model: Quickshell.screens + PanelWindow { + required property var modelData + + screen: modelData + anchors.top: true + anchors.left: true + anchors.right: true + implicitHeight: 30 + color: "#1e1e2e" + + property DwlIpcOutput dwlOutput: DwlIpc.outputs.length > 0 ? DwlIpc.outputForName(modelData.name) : null + property string currentLayout: dwlOutput ? dwlOutput.kbLayout : "" + + RowLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 2 + + // Tag indicators + Repeater { + model: dwlOutput ? dwlOutput.tags : [] + delegate: Rectangle { + required property DwlTag modelData + width: 22 + height: 22 + radius: 4 + color: modelData.active ? "#89b4fa" : (modelData.clientCount > 0 ? "#313244" : "transparent") + border.color: modelData.urgent ? "#f38ba8" : "transparent" + border.width: 2 + + Text { + anchors.centerIn: parent + text: modelData.index + 1 + color: modelData.active ? "#1e1e2e" : "#cdd6f4" + font.pixelSize: 12 + font.bold: modelData.active + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + // Left click: switch to tag + // Right click: toggle tag on focused client + onClicked: mouse => { + if (mouse.button === Qt.RightButton) + dwlOutput.setClientTags(0xFFFFFFFF, 1 << modelData.index); + else + dwlOutput.setTags(1 << modelData.index); + } + } + } + } + + // Layout symbol + Text { + text: dwlOutput ? dwlOutput.layoutSymbol : "" + color: "#a6e3a1" + font.pixelSize: 12 + font.family: "monospace" + visible: dwlOutput !== null + + MouseArea { + anchors.fill: parent + + onClicked: { + if (dwlOutput) { + const next = (dwlOutput.layoutIndex + 1) % DwlIpc.layouts.length; + dwlOutput.setLayout(next); + } + } + } + } + + Item { + Layout.fillWidth: true + } + + Text { + id: layoutText + + text: { + if (!currentLayout) + return "XX"; + if (currentLayout.includes('(') && currentLayout.includes(')')) { + const match = currentLayout.match(/\(([^)]+)\)/); + return match ? match[1] : currentLayout.substring(0, 2).toUpperCase(); + } + + const firstWord = currentLayout.split(' ')[0]; + return firstWord.length <= 3 ? firstWord : firstWord.substring(0, 2).toUpperCase(); + } + + font.pixelSize: 12 + font.bold: true + color: "#f38ba8" + } + + // Focused window title + Text { + text: ToplevelManager.activeToplevel ? ToplevelManager.activeToplevel.title : "" + color: "#cdd6f4" + font.pixelSize: 12 + elide: Text.ElideRight + Layout.maximumWidth: 300 + } + + Text { + visible: dwlOutput ? dwlOutput.floating : false + text: "[~]" + color: "#f9e2af" + font.pixelSize: 12 + } + } + + Text { + visible: !DwlIpc.available + anchors.centerIn: parent + text: "DWL not available" + color: "#f38ba8" + font.pixelSize: 12 + } + } + } + } +}