diff --git a/flake.lock b/flake.lock index 4d215a5..49e555e 100644 --- a/flake.lock +++ b/flake.lock @@ -49,6 +49,24 @@ "type": "github" } }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1762040540, + "narHash": "sha256-z5PlZ47j50VNF3R+IMS9LmzI5fYRGY/Z5O5tol1c9I4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "0010412d62a25d959151790968765a70c436598b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -175,6 +193,21 @@ "type": "github" } }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1761765539, + "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, "nixpkgs-stable": { "locked": { "lastModified": 1775811116, @@ -239,6 +272,67 @@ "type": "github" } }, + "nixpkgs_4": { + "locked": { + "lastModified": 1761236834, + "narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d5faa84122bc0a1fd5d378492efce4e289f8eac1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "qml-niri": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "quickshell": "quickshell", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1775384967, + "narHash": "sha256-5JuFsu0tPEZrHhj/d5C7eP0+FSLNi9hTW5h/17N3Mbk=", + "owner": "imiric", + "repo": "qml-niri", + "rev": "2d60eb204aa7a9704b7f5cf228b85fb19acbbcee", + "type": "github" + }, + "original": { + "owner": "imiric", + "ref": "main", + "repo": "qml-niri", + "type": "github" + } + }, + "quickshell": { + "inputs": { + "nixpkgs": [ + "qml-niri", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1761897390, + "narHash": "sha256-er4gYrIoThYLjlsOMTysoRfn67d1Gci+ZpqDrtQxrA0=", + "ref": "refs/heads/master", + "rev": "fc704e6b5d445899a1565955268c91942a4f263f", + "revCount": 700, + "type": "git", + "url": "https://git.outfoxxed.me/outfoxxed/quickshell" + }, + "original": { + "type": "git", + "url": "https://git.outfoxxed.me/outfoxxed/quickshell" + } + }, "root": { "inputs": { "claude-code": "claude-code", @@ -249,6 +343,7 @@ "niri-flake": "niri-flake", "nixpkgs": "nixpkgs_3", "nixpkgs-unstable": "nixpkgs-unstable", + "qml-niri": "qml-niri", "sops-nix": "sops-nix" } }, @@ -287,6 +382,24 @@ "type": "github" } }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_4" + }, + "locked": { + "lastModified": 1762410071, + "narHash": "sha256-aF5fvoZeoXNPxT0bejFUBXeUjXfHLSL7g+mjR/p5TEg=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "97a30861b13c3731a84e09405414398fbf3e109f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, "xwayland-satellite-stable": { "flake": false, "locked": { diff --git a/flake.nix b/flake.nix index 441de1b..7b5c6e5 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,10 @@ inputs.nixpkgs.follows = "nixpkgs"; }; claude-code.url = "github:sadjow/claude-code-nix"; + qml-niri = { + url = "github:imiric/qml-niri/main"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs: diff --git a/modules/_home/default.nix b/modules/_home/default.nix index aa6d5db..32cfbe0 100644 --- a/modules/_home/default.nix +++ b/modules/_home/default.nix @@ -35,6 +35,7 @@ in { ]; initContent = '' source ${pkgs.zsh-powerlevel10k}/share/zsh-powerlevel10k/powerlevel10k.zsh-theme + [[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh ''; oh-my-zsh = { enable = true; diff --git a/modules/_home/mako.nix b/modules/_home/mako.nix deleted file mode 100644 index a5a2c00..0000000 --- a/modules/_home/mako.nix +++ /dev/null @@ -1,5 +0,0 @@ -{ ... }: { - services.mako = { - enable = true; - }; -} diff --git a/modules/_home/niri.nix b/modules/_home/niri.nix index c72b6b2..455f762 100644 --- a/modules/_home/niri.nix +++ b/modules/_home/niri.nix @@ -1,4 +1,7 @@ -{ pkgs, ... }: { +{ config, pkgs, ... }: +let + wallpaper = ../../wallpapers/miku.png; +in { home.sessionVariables = { XDG_SESSION_TYPE = "wayland"; XDG_CURRENT_DESKTOP = "niri"; @@ -42,12 +45,16 @@ ''; }; + xdg.configFile."quickshell".source = + config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/repos/nixos/quickshell"; + xdg.configFile."niri/config.kdl".text = '' // Modificadores: Mod = Super (tecla Windows) input { keyboard { xkb { - layout "en" + layout "us,pt" + variant ",br" } } touchpad { @@ -67,6 +74,10 @@ prefer-no-csd + spawn-at-startup "swww-daemon" + spawn-at-startup "swww" "img" "${wallpaper}" "--transition-type" "none" + spawn-at-startup "quickshell" + screenshot-path "~/Imagens/screenshots/%Y-%m-%d %H-%M-%S.png" binds { @@ -101,13 +112,19 @@ // Sistema Mod+Shift+E { quit; } - Mod+L { spawn "swaylock"; } + Mod+L { spawn "swaylock" "-i" "${wallpaper}" "--scaling" "fill"; } // Volume XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%+"; } XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%-"; } XF86AudioMute { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } + // Zen mode (toggle quickshell bar) + Mod+Z { spawn "quickshell" "ipc" "call" "zen" "toggle"; } + + // Input layout + Mod+Space { switch-layout "next"; } + // Screenshot Mod+Shift+S { spawn "bash" "-c" "$HOME/.local/bin/screenshot region"; } Mod+Shift+Alt+S { spawn "bash" "-c" "$HOME/.local/bin/screenshot screen"; } diff --git a/modules/core.nix b/modules/core.nix index b5c2eaf..7363d12 100644 --- a/modules/core.nix +++ b/modules/core.nix @@ -33,7 +33,7 @@ wget pkgs-unstable.neovim btop - nixd + ncdu unrar p7zip ]; diff --git a/modules/den.nix b/modules/den.nix index 4743162..ee91c7a 100644 --- a/modules/den.nix +++ b/modules/den.nix @@ -71,7 +71,6 @@ ./_home/niri.nix ./_home/cursor.nix ./_home/git.nix - ./_home/mako.nix ./_home/dolphin.nix ./_home/theme.nix ]; diff --git a/modules/dev.nix b/modules/dev.nix index 870b136..44c49d2 100644 --- a/modules/dev.nix +++ b/modules/dev.nix @@ -27,6 +27,7 @@ angular-language-server # Angular vue-language-server # Vue tailwindcss-language-server # Tailwind + qt6Packages.qtdeclarative # qmlls (QML/Quickshell LSP) # Extra chafa ripgrep diff --git a/modules/niri.nix b/modules/niri.nix index 6d991e7..52b7fa8 100644 --- a/modules/niri.nix +++ b/modules/niri.nix @@ -1,7 +1,12 @@ { inputs, den, pkgs, ... }: { - den.aspects.niri.nixos = { pkgs, ... }: { + den.aspects.niri.nixos = { pkgs, ... }: + let + quickshell-niri = inputs.qml-niri.packages.${pkgs.system}.quickshell; + in { imports = [ inputs.niri-flake.nixosModules.niri ]; + fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ]; + programs.niri = { enable = true; package = pkgs.niri; @@ -10,10 +15,12 @@ programs.gpu-screen-recorder.enable = true; environment.systemPackages = with pkgs; [ + quickshell-niri waybar fuzzel swaylock swayidle + swww grim slurp wl-clipboard @@ -21,6 +28,7 @@ kdePackages.xdg-desktop-portal-kde xwayland-satellite gpu-screen-recorder-gtk + libnotify ]; xdg.portal = { diff --git a/quickshell/ActiveWindow.qml b/quickshell/ActiveWindow.qml new file mode 100644 index 0000000..e437035 --- /dev/null +++ b/quickshell/ActiveWindow.qml @@ -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 } + } +} diff --git a/quickshell/Bar.qml b/quickshell/Bar.qml new file mode 100644 index 0000000..69dc6e1 --- /dev/null +++ b/quickshell/Bar.qml @@ -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 {} + } + } +} diff --git a/quickshell/BarPill.qml b/quickshell/BarPill.qml new file mode 100644 index 0000000..de0cb2e --- /dev/null +++ b/quickshell/BarPill.qml @@ -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 + } +} diff --git a/quickshell/Clock.qml b/quickshell/Clock.qml new file mode 100644 index 0000000..5bfabea --- /dev/null +++ b/quickshell/Clock.qml @@ -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 + } +} diff --git a/quickshell/CpuMonitor.qml b/quickshell/CpuMonitor.qml new file mode 100644 index 0000000..2f1d186 --- /dev/null +++ b/quickshell/CpuMonitor.qml @@ -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 + } + } + } +} diff --git a/quickshell/CpuWidget.qml b/quickshell/CpuWidget.qml new file mode 100644 index 0000000..50397b1 --- /dev/null +++ b/quickshell/CpuWidget.qml @@ -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 } +} diff --git a/quickshell/DateWidget.qml b/quickshell/DateWidget.qml new file mode 100644 index 0000000..c6acbbb --- /dev/null +++ b/quickshell/DateWidget.qml @@ -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 + } +} diff --git a/quickshell/GpuMonitor.qml b/quickshell/GpuMonitor.qml new file mode 100644 index 0000000..cf42345 --- /dev/null +++ b/quickshell/GpuMonitor.qml @@ -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 + } + } + } +} diff --git a/quickshell/GpuWidget.qml b/quickshell/GpuWidget.qml new file mode 100644 index 0000000..7420c38 --- /dev/null +++ b/quickshell/GpuWidget.qml @@ -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 } +} diff --git a/quickshell/KeyboardLayout.qml b/quickshell/KeyboardLayout.qml new file mode 100644 index 0000000..39bfa2f --- /dev/null +++ b/quickshell/KeyboardLayout.qml @@ -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 } + } + } + } +} diff --git a/quickshell/NetMonitor.qml b/quickshell/NetMonitor.qml new file mode 100644 index 0000000..e3caa20 --- /dev/null +++ b/quickshell/NetMonitor.qml @@ -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 + } + } + } +} diff --git a/quickshell/NetWidget.qml b/quickshell/NetWidget.qml new file mode 100644 index 0000000..c0e951c --- /dev/null +++ b/quickshell/NetWidget.qml @@ -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 } +} diff --git a/quickshell/NotificationItem.qml b/quickshell/NotificationItem.qml new file mode 100644 index 0000000..082b87d --- /dev/null +++ b/quickshell/NotificationItem.qml @@ -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() + } + } + } +} diff --git a/quickshell/NotificationPanel.qml b/quickshell/NotificationPanel.qml new file mode 100644 index 0000000..50dafb4 --- /dev/null +++ b/quickshell/NotificationPanel.qml @@ -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 + } + } + } +} diff --git a/quickshell/NotificationToast.qml b/quickshell/NotificationToast.qml new file mode 100644 index 0000000..068a02d --- /dev/null +++ b/quickshell/NotificationToast.qml @@ -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) + } + } + } + } + } +} diff --git a/quickshell/NotificationWidget.qml b/quickshell/NotificationWidget.qml new file mode 100644 index 0000000..6cc14e6 --- /dev/null +++ b/quickshell/NotificationWidget.qml @@ -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() + } +} diff --git a/quickshell/NotificationsState.qml b/quickshell/NotificationsState.qml new file mode 100644 index 0000000..2efa017 --- /dev/null +++ b/quickshell/NotificationsState.qml @@ -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") + } +} diff --git a/quickshell/RamMonitor.qml b/quickshell/RamMonitor.qml new file mode 100644 index 0000000..dac8dc2 --- /dev/null +++ b/quickshell/RamMonitor.qml @@ -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 + } + } + } + } +} diff --git a/quickshell/RamWidget.qml b/quickshell/RamWidget.qml new file mode 100644 index 0000000..2a616de --- /dev/null +++ b/quickshell/RamWidget.qml @@ -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 } +} diff --git a/quickshell/Theme.qml b/quickshell/Theme.qml new file mode 100644 index 0000000..aeea9e3 --- /dev/null +++ b/quickshell/Theme.qml @@ -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 +} diff --git a/quickshell/Workspaces.qml b/quickshell/Workspaces.qml new file mode 100644 index 0000000..6ac9225 --- /dev/null +++ b/quickshell/Workspaces.qml @@ -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) + } + } + } +} diff --git a/quickshell/shell.qml b/quickshell/shell.qml new file mode 100644 index 0000000..89f8e11 --- /dev/null +++ b/quickshell/shell.qml @@ -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 {} +} diff --git a/wallpapers/miku.png b/wallpapers/miku.png new file mode 100644 index 0000000..a25347d Binary files /dev/null and b/wallpapers/miku.png differ