add quickshell

This commit is contained in:
Zoty 2026-04-15 10:38:34 -03:00
parent 3d48e35de5
commit 10960f620d
Signed by: Zoty
SSH key fingerprint: SHA256:WsGEGivgl37t3Mth6uugXMOgMCTR7QolIepNbC+j/tA
32 changed files with 1415 additions and 11 deletions

View file

@ -0,0 +1,18 @@
import QtQuick
Text {
readonly property string title: niri.focusedWindow?.title ?? ""
text: title
color: Theme.subtle
font.pixelSize: Theme.fontSize
font.family: Theme.font
elide: Text.ElideRight
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
opacity: title.length > 0 ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 120 }
}
}

57
quickshell/Bar.qml Normal file
View file

@ -0,0 +1,57 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
PanelWindow {
anchors { top: true; left: true; right: true }
visible: !root.zenMode
height: Theme.barHeight
color: "transparent"
exclusiveZone: height
Rectangle {
anchors.fill: parent
color: Theme.bg
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: Theme.bg2
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padding
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacing
Workspaces {}
}
ActiveWindow {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: 480
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.padding
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacing
NetWidget {}
CpuWidget {}
RamWidget {}
GpuWidget {}
DateWidget {}
Clock {}
KeyboardLayout {}
NotificationWidget {}
}
}
}

20
quickshell/BarPill.qml Normal file
View file

@ -0,0 +1,20 @@
import QtQuick
import QtQuick.Layouts
Rectangle {
default property alias content: inner.data
property int minWidth: 0
color: Theme.bg1
radius: Theme.radius
height: Theme.barHeight - 8
width: Math.max(inner.implicitWidth + Theme.spacing * 2, minWidth)
clip: true
RowLayout {
id: inner
anchors.centerIn: parent
spacing: 5
}
}

19
quickshell/Clock.qml Normal file
View file

@ -0,0 +1,19 @@
import Quickshell
import QtQuick
BarPill {
SystemClock { id: clock; precision: SystemClock.Seconds }
Text {
text: "󰥔"
color: Theme.yellow
font.pixelSize: Theme.fontSize
font.family: Theme.font
}
Text {
text: Qt.formatDateTime(clock.date, "hh:mm")
color: Theme.text
font.pixelSize: Theme.fontSize
font.family: Theme.font
}
}

57
quickshell/CpuMonitor.qml Normal file
View file

@ -0,0 +1,57 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property int usage: 0
property int temp: 0
property var _prevTotal: null
property var _prevIdle: null
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
_statProc.running = false
_statProc.running = true
_tempProc.running = false
_tempProc.running = true
}
}
Process {
id: _statProc
command: ["bash", "-c", "grep '^cpu ' /proc/stat"]
stdout: SplitParser {
onRead: line => {
const p = line.trim().split(/\s+/)
const idle = parseInt(p[4]) + parseInt(p[5])
const total = p.slice(1, 8).reduce((a, v) => a + parseInt(v), 0)
if (root._prevTotal !== null) {
const dt = total - root._prevTotal
const di = idle - root._prevIdle
root.usage = dt > 0 ? Math.round((1 - di / dt) * 100) : 0
}
root._prevTotal = total
root._prevIdle = idle
}
}
}
Process {
id: _tempProc
command: ["bash", "-c", "for d in /sys/class/hwmon/hwmon*/; do [ \"$(cat ${d}name 2>/dev/null)\" = 'coretemp' ] && awk '{print int($1/1000)}' \"${d}temp1_input\" 2>/dev/null && break; done"]
stdout: SplitParser {
onRead: line => {
const v = parseInt(line.trim())
if (!isNaN(v) && v > 0) root.temp = v
}
}
}
}

10
quickshell/CpuWidget.qml Normal file
View file

@ -0,0 +1,10 @@
import QtQuick
BarPill {
minWidth: 115
Text { text: "󰻠"; color: Theme.green; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: CpuMonitor.usage + "%"; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: "|"; color: Theme.muted; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: CpuMonitor.temp + "°"; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: "󰔄"; color: Theme.orange; font.pixelSize: Theme.fontSize; font.family: Theme.font }
}

19
quickshell/DateWidget.qml Normal file
View file

@ -0,0 +1,19 @@
import Quickshell
import QtQuick
BarPill {
SystemClock { id: clock; precision: SystemClock.Minutes }
Text {
text: "󰃭"
color: Theme.aqua
font.pixelSize: Theme.fontSize
font.family: Theme.font
}
Text {
text: Qt.formatDateTime(clock.date, "ddd dd/MM")
color: Theme.text
font.pixelSize: Theme.fontSize
font.family: Theme.font
}
}

37
quickshell/GpuMonitor.qml Normal file
View file

@ -0,0 +1,37 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property int usage: 0
property int temp: 0
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
_proc.running = false
_proc.running = true
}
}
Process {
id: _proc
command: ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"]
stdout: SplitParser {
onRead: line => {
const parts = line.trim().split(/\s*,\s*/)
if (parts.length < 2) return
const u = parseInt(parts[0])
const t = parseInt(parts[1])
if (!isNaN(u)) root.usage = u
if (!isNaN(t)) root.temp = t
}
}
}
}

10
quickshell/GpuWidget.qml Normal file
View file

@ -0,0 +1,10 @@
import QtQuick
BarPill {
minWidth: 115
Text { text: "󰾲"; color: Theme.blue; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: GpuMonitor.usage + "%"; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: "|"; color: Theme.muted; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: GpuMonitor.temp + "°"; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: "󰔄"; color: Theme.orange; font.pixelSize: Theme.fontSize; font.family: Theme.font }
}

View file

@ -0,0 +1,69 @@
import Quickshell
import Quickshell.Io
import QtQuick
BarPill {
id: root
property int currentIdx: 0
property var names: []
readonly property string shortName: {
if (names.length === 0) return "??"
const n = names[currentIdx] ?? ""
if (n.includes("Portuguese")) return "PT"
if (n.includes("English")) return "EN"
// fallback: first two chars of first word
return n.split(" ")[0].substring(0, 2).toUpperCase()
}
Process {
command: ["niri", "msg", "--json", "keyboard-layouts"]
running: true
stdout: SplitParser {
splitMarker: ""
onRead: data => {
try {
const kl = JSON.parse(data)
root.currentIdx = kl.current_idx
root.names = kl.names
} catch (e) {}
}
}
}
Connections {
target: niri
function onRawEventReceived(event) {
try {
const ev = JSON.parse(event)
if (!ev.KeyboardLayoutsChanged) return
const kl = ev.KeyboardLayoutsChanged.keyboard_layouts
root.currentIdx = kl.current_idx
root.names = kl.names
} catch (e) {}
}
}
Text {
text: "󰌌"
color: Theme.purple
font.pixelSize: Theme.fontSize
font.family: Theme.font
}
Text {
text: root.shortName
color: Theme.text
font.pixelSize: Theme.fontSize
font.family: Theme.font
Behavior on text {
// Flash the label on layout change
SequentialAnimation {
PropertyAnimation { target: parent; property: "opacity"; to: 0; duration: 80 }
PropertyAction { target: parent; property: "text" }
PropertyAnimation { target: parent; property: "opacity"; to: 1; duration: 80 }
}
}
}
}

53
quickshell/NetMonitor.qml Normal file
View file

@ -0,0 +1,53 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property real rxSpeed: 0
property real txSpeed: 0
property var _prevRx: null
property var _prevTx: null
function _fmt(bytes) {
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + " M"
if (bytes >= 1024) return (bytes / 1024).toFixed(0) + " K"
return bytes + " B"
}
readonly property string rxStr: _fmt(rxSpeed)
readonly property string txStr: _fmt(txSpeed)
Timer {
interval: 1000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
_proc.running = false
_proc.running = true
}
}
Process {
id: _proc
command: ["bash", "-c", "iface=$(ip route show default | awk '{print $5; exit}'); awk -v i=\"$iface:\" '$1==i {print $2, $10}' /proc/net/dev"]
stdout: SplitParser {
onRead: line => {
const parts = line.trim().split(/\s+/)
if (parts.length < 2) return
const rx = parseInt(parts[0])
const tx = parseInt(parts[1])
if (root._prevRx !== null) {
root.rxSpeed = Math.max(0, rx - root._prevRx)
root.txSpeed = Math.max(0, tx - root._prevTx)
}
root._prevRx = rx
root._prevTx = tx
}
}
}
}

9
quickshell/NetWidget.qml Normal file
View file

@ -0,0 +1,9 @@
import QtQuick
BarPill {
minWidth: 140
Text { text: "󰁇"; color: Theme.aqua; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: NetMonitor.rxStr; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: "󰁞"; color: Theme.yellow; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: NetMonitor.txStr; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
}

View file

@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property int nid
required property string appName
required property string summary
required property string body
required property string appIcon
required property int urgency
required property var timestamp
signal dismissed()
implicitHeight: inner.implicitHeight + 16
height: implicitHeight
Rectangle {
id: bg
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
anchors.topMargin: 2
anchors.bottomMargin: 2
color: itemClick.containsMouse ? Theme.bg1 : "transparent"
radius: Theme.radius
Behavior on color { ColorAnimation { duration: 100 } }
RowLayout {
id: inner
// Right margin reserves space so content does not overlap the close button
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
anchors.rightMargin: 32
spacing: 8
Rectangle {
width: 34
height: 34
radius: 17
color: root.urgency === 2 ? Theme.redN
: root.urgency === 0 ? Theme.bg3
: Theme.blueN
Layout.alignment: Qt.AlignVCenter
Image {
id: iconImg
anchors.fill: parent
anchors.margins: 5
source: root.appIcon
fillMode: Image.PreserveAspectFit
visible: status === Image.Ready
smooth: true
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: root.appName.charAt(0).toUpperCase()
color: Theme.fg
font.pixelSize: Theme.fontSize
font.family: Theme.font
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: 6
Text {
text: root.summary.length > 0 ? root.summary : root.appName
color: Theme.text
font.pixelSize: Theme.fontSize
font.family: Theme.font
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: NotificationsState.formatTime(root.timestamp)
color: Theme.subtle
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.font
}
}
Text {
visible: root.body.length > 0
text: root.body
color: Theme.fg2
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.font
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
visible: root.summary.length > 0
text: root.appName
color: Theme.muted
font.pixelSize: Theme.fontSizeSmall - 1
font.family: Theme.font
}
}
}
Rectangle {
id: closeArea
width: 20
height: 20
radius: 10
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 5
anchors.rightMargin: 5
color: closeBtn.containsMouse ? Theme.bg3 : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: "✕"
color: closeBtn.containsMouse ? Theme.fg : Theme.subtle
font.pixelSize: 10
opacity: closeBtn.containsMouse ? 1.0 : 0.5
Behavior on color { ColorAnimation { duration: 100 } }
Behavior on opacity { NumberAnimation { duration: 100 } }
}
MouseArea {
id: closeBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.dismissed()
}
}
// Does not extend to the right edge to avoid overlapping the close button
MouseArea {
id: itemClick
anchors.fill: parent
anchors.rightMargin: 28
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NotificationsState.invokeDefault(root.nid)
root.dismissed()
}
}
}
}

View file

@ -0,0 +1,161 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
PanelWindow {
id: panel
anchors.top: true
anchors.right: true
margins.top: Theme.padding
margins.right: Theme.padding
exclusiveZone: 0
WlrLayershell.layer: WlrLayer.Overlay
visible: NotificationsState.panelOpen && !root.zenMode
width: 360
height: NotificationsState.notifications.count === 0
? 120
: Math.min(41 + list.contentHeight + 8, 480)
color: "transparent"
Rectangle {
anchors.fill: parent
color: Theme.bg
radius: Theme.radius
border.color: Theme.bg2
border.width: 1
Item {
id: header
height: 40
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
Text {
anchors.left: parent.left
anchors.leftMargin: Theme.padding
anchors.verticalCenter: parent.verticalCenter
text: "Notifications"
color: Theme.text
font.pixelSize: Theme.fontSize
font.family: Theme.font
font.bold: true
}
Rectangle {
visible: NotificationsState.notifications.count > 0
anchors.right: parent.right
anchors.rightMargin: Theme.padding
anchors.verticalCenter: parent.verticalCenter
height: 22
width: clearLabel.implicitWidth + 12
radius: Theme.radius
color: clearBtn.containsMouse ? Theme.bg2 : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
id: clearLabel
anchors.centerIn: parent
text: "Clear all"
color: clearBtn.containsMouse ? Theme.fg2 : Theme.subtle
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.font
}
MouseArea {
id: clearBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationsState.dismissAll()
}
}
}
Rectangle {
id: sep
height: 1
color: Theme.bg2
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
}
Item {
visible: NotificationsState.notifications.count === 0
anchors.top: sep.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 79
Text {
anchors.centerIn: parent
text: "No notifications"
color: Theme.subtle
font.pixelSize: Theme.fontSize
font.family: Theme.font
}
}
ListView {
id: list
visible: NotificationsState.notifications.count > 0
anchors.top: sep.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: 4
anchors.bottomMargin: 4
clip: true
spacing: 0
model: NotificationsState.notifications
section.property: "day"
section.delegate: Item {
width: list.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: section
color: Theme.accent
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.font
font.bold: true
}
}
delegate: NotificationItem {
required property var model
required property int index
width: list.width
nid: model.nid
appName: model.appName
summary: model.summary
body: model.body
appIcon: model.appIcon
urgency: model.urgency
timestamp: model.timestamp
onDismissed: NotificationsState.dismiss(index)
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
}

View file

@ -0,0 +1,171 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
PanelWindow {
anchors.top: true
anchors.right: true
margins.top: Theme.padding
margins.right: Theme.padding
exclusiveZone: 0
visible: NotificationsState.toasts.count > 0 && !root.zenMode
width: 320
height: toastCol.implicitHeight
color: "transparent"
Column {
id: toastCol
width: parent.width
spacing: 4
Repeater {
model: NotificationsState.toasts
delegate: Rectangle {
required property var model
required property int index
width: toastCol.width
height: toastInner.implicitHeight + Theme.padding * 2 + 3
radius: Theme.radius
color: Theme.bg
border.color: model.urgency === 2 ? Theme.red : Theme.bg2
border.width: 1
RowLayout {
id: toastInner
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Theme.padding
anchors.leftMargin: Theme.padding
anchors.rightMargin: Theme.padding
spacing: Theme.spacing
Rectangle {
width: 34
height: 34
radius: 17
color: model.urgency === 2 ? Theme.redN
: model.urgency === 0 ? Theme.bg3
: Theme.blueN
Layout.alignment: Qt.AlignTop
Image {
id: tIcon
anchors.fill: parent
anchors.margins: 5
source: model.appIcon
fillMode: Image.PreserveAspectFit
visible: status === Image.Ready
smooth: true
}
Text {
anchors.centerIn: parent
visible: !tIcon.visible
text: model.appName.charAt(0).toUpperCase()
color: Theme.fg
font.pixelSize: Theme.fontSize
font.family: Theme.font
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
RowLayout {
Layout.fillWidth: true
Text {
text: model.summary.length > 0 ? model.summary : model.appName
color: Theme.text
font.pixelSize: Theme.fontSize
font.family: Theme.font
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
visible: model.summary.length > 0
text: model.appName
color: Theme.subtle
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.font
}
}
Text {
visible: model.body.length > 0
text: model.body
color: Theme.fg2
font.pixelSize: Theme.fontSizeSmall
font.family: Theme.font
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Rectangle {
width: 22
height: 22
radius: 11
color: closeBtn.containsMouse ? Theme.bg3 : "transparent"
Layout.alignment: Qt.AlignTop
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: "✕"
color: closeBtn.containsMouse ? Theme.fg : Theme.subtle
font.pixelSize: 11
opacity: closeBtn.containsMouse ? 1.0 : 0.5
Behavior on color { ColorAnimation { duration: 100 } }
Behavior on opacity { NumberAnimation { duration: 100 } }
}
MouseArea {
id: closeBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationsState.dismissToast(model.tid)
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
height: 3
radius: 2
width: parent.width * model.progress
color: model.urgency === 2 ? Theme.red : Theme.accent
}
// Does not extend to the right edge to avoid overlapping the close button
MouseArea {
anchors.fill: parent
anchors.rightMargin: 34
cursorShape: Qt.PointingHandCursor
onClicked: {
NotificationsState.invokeDefault(model.tid)
NotificationsState.dismissToast(model.tid)
}
}
}
}
}
}

View file

@ -0,0 +1,52 @@
import QtQuick
Item {
id: root
width: pill.width
height: pill.height
BarPill {
id: pill
Text {
text: NotificationsState.unreadCount > 0 ? "󰂞" : "󰂜"
color: NotificationsState.unreadCount > 0 ? Theme.yellow : Theme.subtle
font.pixelSize: Theme.fontSize
font.family: Theme.font
Behavior on color { ColorAnimation { duration: 150 } }
}
}
// Overlaps the pill corner, so z and sibling anchors are used
Rectangle {
visible: NotificationsState.unreadCount > 0
width: Math.max(15, badgeLabel.implicitWidth + 5)
height: 13
radius: 7
color: Theme.red
z: 10
anchors.top: pill.top
anchors.right: pill.right
anchors.topMargin: -3
anchors.rightMargin: -3
Text {
id: badgeLabel
anchors.centerIn: parent
text: NotificationsState.unreadCount > 9
? "9+" : NotificationsState.unreadCount.toString()
color: "white"
font.pixelSize: 8
font.family: Theme.font
font.bold: true
}
}
MouseArea {
anchors.fill: pill
cursorShape: Qt.PointingHandCursor
onClicked: NotificationsState.togglePanel()
}
}

View file

@ -0,0 +1,142 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
Singleton {
id: root
property bool panelOpen: false
property int unreadCount: 0
property ListModel notifications: ListModel {}
property var _refs: ({}) // maps nid to the live Notification object
property ListModel toasts: ListModel {}
// Runs every 100ms while toasts are active, decrementing progress (1.0 to 0.0 over 5s)
// and removing any toast that has expired
Timer {
id: toastTick
interval: 100
repeat: true
running: root.toasts.count > 0
onTriggered: {
let i = 0
while (i < root.toasts.count) {
const p = root.toasts.get(i).progress - 0.02
if (p <= 0) {
root.toasts.remove(i, 1)
} else {
root.toasts.setProperty(i, "progress", p)
i++
}
}
}
}
NotificationServer {
keepOnReload: true
onNotification: notif => {
const ts = Date.now()
const day = root._dayLabel(ts)
root.notifications.insert(0, {
nid: notif.id,
appName: notif.appName || "Unknown",
summary: notif.summary || "",
body: notif.body || "",
appIcon: notif.appIcon || "",
urgency: notif.urgency || 1,
timestamp: ts,
day: day
})
const r = root._refs
r[notif.id] = notif
root._refs = r
if (!root.panelOpen) root.unreadCount++
root.toasts.insert(0, {
tid: notif.id,
appName: notif.appName || "Unknown",
summary: notif.summary || "",
body: notif.body || "",
appIcon: notif.appIcon || "",
urgency: notif.urgency || 1,
progress: 1.0
})
}
}
function dismiss(index) {
if (index < 0 || index >= notifications.count) return
const entry = notifications.get(index)
const ref = _refs[entry.nid]
if (ref) {
try { ref.close() } catch (_) {}
const r = _refs
delete r[entry.nid]
_refs = r
}
notifications.remove(index, 1)
}
function dismissAll() {
for (let i = 0; i < notifications.count; i++) {
const ref = _refs[notifications.get(i).nid]
if (ref) { try { ref.close() } catch (_) {} }
}
notifications.clear()
_refs = {}
unreadCount = 0
}
// Invokes the "default" action on a notification, falling back to the first action
function invokeDefault(nid) {
const ref = _refs[nid]
if (!ref) return
const actions = ref.actions
if (!actions || actions.length === 0) return
for (let i = 0; i < actions.length; i++) {
if (actions[i].identifier === "default") {
try { actions[i].invoke() } catch (_) {}
return
}
}
try { actions[0].invoke() } catch (_) {}
}
function dismissToast(tid) {
for (let i = 0; i < toasts.count; i++) {
if (toasts.get(i).tid === tid) {
toasts.remove(i, 1)
return
}
}
}
function togglePanel() {
panelOpen = !panelOpen
if (panelOpen) unreadCount = 0
}
function formatTime(ts) {
return Qt.formatDateTime(new Date(ts), "hh:mm")
}
function _dayLabel(ts) {
const now = new Date()
const date = new Date(ts)
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterday = new Date(today.getTime() - 86400000)
const day = new Date(date.getFullYear(), date.getMonth(), date.getDate())
if (day.getTime() === today.getTime()) return "Today"
if (day.getTime() === yesterday.getTime()) return "Yesterday"
return Qt.formatDateTime(date, "dd/MM/yyyy")
}
}

47
quickshell/RamMonitor.qml Normal file
View file

@ -0,0 +1,47 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property int usedMb: 0
property int totalMb: 0
property int usage: 0
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
_proc.running = false
_proc.running = true
}
}
Process {
id: _proc
command: ["bash", "-c", "grep -E '^(MemTotal|MemAvailable):' /proc/meminfo | awk '{print $2}'"]
stdout: SplitParser {
property int _total: 0
property int _avail: 0
property int _lines: 0
onRead: line => {
const v = parseInt(line.trim())
if (isNaN(v)) return
if (_lines === 0) _total = v
else _avail = v
_lines++
if (_lines === 2) {
root.totalMb = Math.round(_total / 1024)
root.usedMb = Math.round((_total - _avail) / 1024)
root.usage = Math.round((1 - _avail / _total) * 100)
_lines = 0
}
}
}
}
}

10
quickshell/RamWidget.qml Normal file
View file

@ -0,0 +1,10 @@
import QtQuick
BarPill {
minWidth: 115
Text { text: "󰍛"; color: Theme.purple; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: (RamMonitor.usedMb / 1024).toFixed(1) + " GB"; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: "|"; color: Theme.muted; font.pixelSize: Theme.fontSize; font.family: Theme.font }
Text { text: RamMonitor.usage + "%"; color: Theme.text; font.pixelSize: Theme.fontSize; font.family: Theme.font }
}

52
quickshell/Theme.qml Normal file
View file

@ -0,0 +1,52 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
// Gruvbox Dark
readonly property color bg: "#282828"
readonly property color bgDark: "#1d2021"
readonly property color bg1: "#3c3836"
readonly property color bg2: "#504945"
readonly property color bg3: "#665c54"
readonly property color bg4: "#7c6f64"
readonly property color fg: "#ebdbb2"
readonly property color fg1: "#d5c4a1"
readonly property color fg2: "#bdae93"
readonly property color fg3: "#a89984"
readonly property color red: "#fb4934"
readonly property color green: "#b8bb26"
readonly property color yellow: "#fabd2f"
readonly property color blue: "#83a598"
readonly property color purple: "#d3869b"
readonly property color aqua: "#8ec07c"
readonly property color orange: "#fe8019"
// Neutral (softer) variants
readonly property color redN: "#cc241d"
readonly property color greenN: "#98971a"
readonly property color yellowN: "#d79921"
readonly property color blueN: "#458588"
readonly property color purpleN: "#b16286"
readonly property color aquaN: "#689d6a"
readonly property color orangeN: "#d65d0e"
// Semantic aliases
readonly property color accent: yellow
readonly property color urgent: red
readonly property color text: fg
readonly property color subtle: fg3
readonly property color muted: bg4
readonly property int barHeight: 32
readonly property int padding: 12
readonly property int spacing: 8
readonly property int radius: 6
readonly property string font: "JetBrainsMono Nerd Font"
readonly property string fontFallback: "monospace"
readonly property int fontSize: 13
readonly property int fontSizeSmall: 11
}

47
quickshell/Workspaces.qml Normal file
View file

@ -0,0 +1,47 @@
import QtQuick
import QtQuick.Layouts
RowLayout {
spacing: Theme.spacing
Repeater {
model: niri.workspaces
// Container com largura fixa o layout nunca muda
Item {
required property var modelData
readonly property bool focused: modelData.isFocused
readonly property bool occupied: modelData.activeWindowId !== null
width: 24
height: 8
Rectangle {
anchors.centerIn: parent
height: parent.height
radius: height / 2
width: parent.focused ? 20 : 8
color: {
if (parent.focused) return Theme.accent
if (parent.occupied) return Theme.subtle
return Theme.muted
}
Behavior on width {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
Behavior on color {
ColorAnimation { duration: 150 }
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: niri.focusWorkspaceById(modelData.id)
}
}
}
}

40
quickshell/shell.qml Normal file
View file

@ -0,0 +1,40 @@
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import Niri 0.1
ShellRoot {
id: root
property bool zenMode: false
IpcHandler {
target: "zen"
function toggle() { root.zenMode = !root.zenMode }
}
Niri {
id: niri
Component.onCompleted: connect()
onErrorOccurred: error => console.error("[Niri]", error)
}
Bar {}
// Fullscreen overlay to help closing open windows
PanelWindow {
anchors { top: true; bottom: true; left: true; right: true }
exclusiveZone: 0
visible: NotificationsState.panelOpen && !root.zenMode
color: "transparent"
MouseArea {
anchors.fill: parent
onClicked: NotificationsState.togglePanel()
}
}
NotificationPanel {}
NotificationToast {}
}