initial commit
This commit is contained in:
commit
9fde6d4fc6
27 changed files with 1110 additions and 0 deletions
69
flake.lock
generated
Normal file
69
flake.lock
generated
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775423009,
|
||||
"narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"quickshell": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775720097,
|
||||
"narHash": "sha256-p+vqkCuFfVNyQBo370wr6MebNUvz55RZiC0m8YKUhvQ=",
|
||||
"ref": "refs/heads/master",
|
||||
"rev": "d4c92973b53d9fa34cc110d3b974eb6bde5b3027",
|
||||
"revCount": 800,
|
||||
"type": "git",
|
||||
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"quickshell": "quickshell",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775636079,
|
||||
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
72
flake.nix
Normal file
72
flake.nix
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
description = "nova-shell - minimal Quickshell bar for niri";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
quickshell = {
|
||||
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
treefmt-nix = {
|
||||
url = "github:numtide/treefmt-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
quickshell,
|
||||
treefmt-nix,
|
||||
...
|
||||
}:
|
||||
let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
treefmt-config = {
|
||||
projectRootFile = "flake.nix";
|
||||
programs.nixfmt.enable = true;
|
||||
};
|
||||
forAllSystems =
|
||||
fn:
|
||||
nixpkgs.lib.genAttrs systems (
|
||||
system:
|
||||
fn rec {
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
treefmt-eval = treefmt-nix.lib.evalModule pkgs treefmt-config;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
formatter = forAllSystems ({ treefmt-eval, ... }: treefmt-eval.config.build.wrapper);
|
||||
|
||||
packages = forAllSystems (
|
||||
{ pkgs, ... }:
|
||||
rec {
|
||||
nova-shell = pkgs.callPackage ./nix/package.nix {
|
||||
quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default.override {
|
||||
withX11 = false;
|
||||
withI3 = false;
|
||||
};
|
||||
};
|
||||
default = nova-shell;
|
||||
}
|
||||
);
|
||||
|
||||
checks = forAllSystems (
|
||||
{ pkgs, treefmt-eval }:
|
||||
{
|
||||
formatting = treefmt-eval.config.build.check self;
|
||||
build = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
}
|
||||
);
|
||||
|
||||
homeModules.default = import ./nix/hm-module.nix self;
|
||||
homeManagerModules.default = self.homeModules.default;
|
||||
};
|
||||
}
|
||||
38
modules/Backlight.qml
Normal file
38
modules/Backlight.qml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: 4
|
||||
visible: percent > 0
|
||||
|
||||
property int percent: 0
|
||||
|
||||
FileView {
|
||||
id: current
|
||||
path: "/sys/class/backlight/intel_backlight/brightness"
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: root._update()
|
||||
}
|
||||
FileView {
|
||||
id: max
|
||||
path: "/sys/class/backlight/intel_backlight/max_brightness"
|
||||
onLoaded: root._update()
|
||||
}
|
||||
|
||||
function _update() {
|
||||
const c = parseInt(current.text());
|
||||
const m = parseInt(max.text());
|
||||
if (m > 0) root.percent = Math.round((c / m) * 100);
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.percent + "% "
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
79
modules/Bar.qml
Normal file
79
modules/Bar.qml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "." as M
|
||||
|
||||
PanelWindow {
|
||||
id: bar
|
||||
|
||||
required property var screen
|
||||
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
implicitHeight: M.Theme.barHeight
|
||||
exclusiveZone: implicitHeight
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: M.Theme.base00
|
||||
opacity: M.Theme.barOpacity
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 8
|
||||
|
||||
// ---- left ----
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
spacing: 8
|
||||
|
||||
M.Workspaces {}
|
||||
M.Tray { bar: bar }
|
||||
M.WindowTitle { Layout.maximumWidth: 400 }
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// ---- center ----
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 8
|
||||
|
||||
M.Clock {}
|
||||
M.Notifications {}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// ---- right ----
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: 12
|
||||
|
||||
M.Mpris {}
|
||||
M.Volume {}
|
||||
M.Bluetooth {}
|
||||
M.Backlight {}
|
||||
M.Network {}
|
||||
M.PowerProfile {}
|
||||
M.IdleInhibitor {}
|
||||
M.Weather {}
|
||||
M.Temperature {}
|
||||
M.Cpu {}
|
||||
M.Memory {}
|
||||
M.Disk {}
|
||||
M.Battery {}
|
||||
M.Wlogout {}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
modules/Battery.qml
Normal file
32
modules/Battery.qml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import "." as M
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: 4
|
||||
visible: UPower.displayDevice?.isLaptopBattery ?? false
|
||||
|
||||
readonly property var dev: UPower.displayDevice
|
||||
readonly property real pct: (dev?.percentage ?? 0) * 100
|
||||
readonly property bool charging: dev?.state === UPowerDeviceState.Charging
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (root.charging) return "";
|
||||
const icons = ["","","","","","","","","","",""];
|
||||
return icons[Math.min(10, Math.floor(root.pct / 10))];
|
||||
}
|
||||
color: root.pct < 15 ? M.Theme.base08 : M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 2
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: Math.round(root.pct) + "%"
|
||||
color: root.pct < 15 ? M.Theme.base08 : M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
38
modules/Bluetooth.qml
Normal file
38
modules/Bluetooth.qml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: 4
|
||||
|
||||
property string status: "off"
|
||||
property string device: ""
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: true
|
||||
command: ["sh", "-c", "bluetoothctl info 2>/dev/null | awk -F': ' '/Name/ {n=$2} /Connected: yes/ {c=1} END {if (c) print n; else print \"\"}'"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const t = text.trim();
|
||||
root.device = t;
|
||||
root.status = t ? "connected" : "on";
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.status === "connected" ? (" " + root.device) : ""
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 1
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
16
modules/Clock.qml
Normal file
16
modules/Clock.qml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
text: Qt.formatDateTime(clock.date, "ddd, dd. MMM HH:mm")
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 1
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
51
modules/Cpu.qml
Normal file
51
modules/Cpu.qml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property int usage: 0
|
||||
property real freqGhz: 0
|
||||
|
||||
property var _prev: null
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
path: "/proc/stat"
|
||||
onLoaded: {
|
||||
const line = text().split("\n")[0];
|
||||
const f = line.trim().split(/\s+/).slice(1).map(Number);
|
||||
const idle = f[3] + f[4];
|
||||
const total = f.reduce((a, b) => a + b, 0);
|
||||
if (root._prev) {
|
||||
const dIdle = idle - root._prev.idle;
|
||||
const dTotal = total - root._prev.total;
|
||||
if (dTotal > 0) root.usage = Math.round((1 - dIdle / dTotal) * 100);
|
||||
}
|
||||
root._prev = { idle, total };
|
||||
}
|
||||
}
|
||||
FileView {
|
||||
id: cpuinfo
|
||||
path: "/proc/cpuinfo"
|
||||
onLoaded: {
|
||||
const lines = text().split("\n").filter(l => l.startsWith("cpu MHz"));
|
||||
if (lines.length === 0) return;
|
||||
const sum = lines.reduce((a, l) => a + parseFloat(l.split(":")[1]), 0);
|
||||
root.freqGhz = sum / lines.length / 1000;
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: { stat.reload(); cpuinfo.reload(); }
|
||||
}
|
||||
|
||||
text: " " + root.usage.toString().padStart(2) + "%@" + root.freqGhz.toFixed(2)
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
38
modules/Disk.qml
Normal file
38
modules/Disk.qml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property int freePct: 0
|
||||
property real totalTb: 0
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: true
|
||||
command: ["sh", "-c", "df -B1 --output=size,avail / | tail -1"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const parts = text.trim().split(/\s+/).map(Number);
|
||||
const size = parts[0], avail = parts[1];
|
||||
if (size > 0) {
|
||||
root.freePct = Math.round((avail / size) * 100);
|
||||
root.totalTb = size / 1e12;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 30000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
text: " " + root.freePct + "% " + root.totalTb.toFixed(1)
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
31
modules/IdleInhibitor.qml
Normal file
31
modules/IdleInhibitor.qml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
|
||||
text: root.active ? "" : ""
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 1
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Process {
|
||||
id: toggle
|
||||
command: ["sh", "-c", root.active
|
||||
? "pkill -x systemd-inhibit || true"
|
||||
: "systemd-inhibit --what=idle --who=nova-shell --why=user sleep infinity &"]
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.active = !root.active;
|
||||
toggle.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
modules/Memory.qml
Normal file
36
modules/Memory.qml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property int percent: 0
|
||||
|
||||
FileView {
|
||||
id: meminfo
|
||||
path: "/proc/meminfo"
|
||||
onLoaded: {
|
||||
const m = {};
|
||||
text().split("\n").forEach(l => {
|
||||
const [k, v] = l.split(":");
|
||||
if (v) m[k.trim()] = parseInt(v.trim());
|
||||
});
|
||||
const total = m.MemTotal;
|
||||
const avail = m.MemAvailable;
|
||||
if (total > 0) root.percent = Math.round(((total - avail) / total) * 100);
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: meminfo.reload()
|
||||
}
|
||||
|
||||
text: " " + root.percent + "%"
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
32
modules/Mpris.qml
Normal file
32
modules/Mpris.qml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import QtQuick
|
||||
import Quickshell.Services.Mpris
|
||||
import "." as M
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: 4
|
||||
visible: player !== null
|
||||
|
||||
readonly property MprisPlayer player: Mpris.players.values[0] ?? null
|
||||
readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
Text {
|
||||
text: root.playing ? "" : (root.player?.playbackState === MprisPlaybackState.Paused ? "" : "")
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 1
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: root.player?.identity ?? ""
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.player?.togglePlaying()
|
||||
}
|
||||
}
|
||||
51
modules/Network.qml
Normal file
51
modules/Network.qml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: 4
|
||||
|
||||
property string ifname: ""
|
||||
property string essid: ""
|
||||
property string state: "disconnected"
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: true
|
||||
command: ["sh", "-c", "nmcli -t -f NAME,TYPE,DEVICE connection show --active | head -1"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const line = text.trim();
|
||||
if (!line) {
|
||||
root.state = "disconnected";
|
||||
root.essid = "";
|
||||
root.ifname = "";
|
||||
return;
|
||||
}
|
||||
const parts = line.split(":");
|
||||
root.essid = parts[0] || "";
|
||||
root.ifname = parts[2] || "";
|
||||
root.state = (parts[1] || "").includes("wireless") ? "wifi" : "eth";
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (root.state === "wifi") return " " + root.essid;
|
||||
if (root.state === "eth") return "";
|
||||
return "";
|
||||
}
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 1
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
58
modules/Notifications.qml
Normal file
58
modules/Notifications.qml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: 4
|
||||
|
||||
property int count: 0
|
||||
property bool dnd: false
|
||||
|
||||
Process {
|
||||
id: sub
|
||||
running: true
|
||||
command: ["swaync-client", "--subscribe-waybar"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (line) => {
|
||||
try {
|
||||
const d = JSON.parse(line);
|
||||
root.count = d.count ?? 0;
|
||||
root.dnd = (d.class ?? "").includes("dnd");
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (root.dnd) return root.count > 0 ? "" : "";
|
||||
return root.count > 0 ? "" : "";
|
||||
}
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 2
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Text {
|
||||
text: root.count > 0 ? String(root.count) : ""
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (m) => {
|
||||
const cmd = m.button === Qt.RightButton
|
||||
? ["swaync-client", "--toggle-dnd", "--skip-wait"]
|
||||
: ["swaync-client", "--toggle-panel", "--skip-wait"];
|
||||
clicker.command = cmd;
|
||||
clicker.running = true;
|
||||
}
|
||||
}
|
||||
Process { id: clicker }
|
||||
}
|
||||
35
modules/PowerProfile.qml
Normal file
35
modules/PowerProfile.qml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property string profile: ""
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: true
|
||||
command: ["powerprofilesctl", "get"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.profile = text.trim()
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
text: {
|
||||
if (root.profile === "performance") return "";
|
||||
if (root.profile === "power-saver") return "";
|
||||
if (root.profile === "balanced") return "";
|
||||
return "";
|
||||
}
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 1
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
27
modules/Temperature.qml
Normal file
27
modules/Temperature.qml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property int celsius: 0
|
||||
|
||||
FileView {
|
||||
id: thermal
|
||||
path: "/sys/class/thermal/thermal_zone0/temp"
|
||||
onLoaded: root.celsius = Math.round(parseInt(text()) / 1000)
|
||||
}
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: thermal.reload()
|
||||
}
|
||||
|
||||
text: " " + root.celsius + "°C"
|
||||
color: root.celsius > 80 ? M.Theme.base08 : M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
53
modules/Theme.qml
Normal file
53
modules/Theme.qml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// base16 palette, overwritten from ~/.config/nova-shell/theme.json
|
||||
property color base00: "#1e1e2e"
|
||||
property color base01: "#181825"
|
||||
property color base02: "#313244"
|
||||
property color base03: "#45475a"
|
||||
property color base04: "#585b70"
|
||||
property color base05: "#cdd6f4"
|
||||
property color base06: "#f5e0dc"
|
||||
property color base07: "#b4befe"
|
||||
property color base08: "#f38ba8"
|
||||
property color base09: "#fab387"
|
||||
property color base0A: "#f9e2af"
|
||||
property color base0B: "#a6e3a1"
|
||||
property color base0C: "#94e2d5"
|
||||
property color base0D: "#89b4fa"
|
||||
property color base0E: "#cba6f7"
|
||||
property color base0F: "#f2cdcd"
|
||||
|
||||
property string fontFamily: "sans-serif"
|
||||
property int fontSize: 12
|
||||
property real barOpacity: 0.9
|
||||
property int barHeight: 32
|
||||
|
||||
property FileView _themeFile: FileView {
|
||||
path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config"))
|
||||
+ "/nova-shell/theme.json"
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: root._apply(text())
|
||||
}
|
||||
|
||||
function _apply(raw) {
|
||||
let data;
|
||||
try { data = JSON.parse(raw); } catch (e) { return; }
|
||||
const c = data.colors || {};
|
||||
for (const k of Object.keys(c)) {
|
||||
if (k in root) root[k] = c[k];
|
||||
}
|
||||
if (data.fontFamily) root.fontFamily = data.fontFamily;
|
||||
if (data.fontSize) root.fontSize = data.fontSize;
|
||||
if (data.barOpacity !== undefined) root.barOpacity = data.barOpacity;
|
||||
if (data.barHeight !== undefined) root.barHeight = data.barHeight;
|
||||
}
|
||||
}
|
||||
40
modules/Tray.qml
Normal file
40
modules/Tray.qml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
spacing: 6
|
||||
|
||||
required property var bar
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Item {
|
||||
id: iconItem
|
||||
required property SystemTrayItem modelData
|
||||
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: iconItem.modelData.icon
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
iconItem.modelData.activate();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
iconItem.modelData.display(root.bar, mouse.x, mouse.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
modules/Volume.qml
Normal file
37
modules/Volume.qml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import QtQuick
|
||||
import Quickshell.Services.Pipewire
|
||||
import "." as M
|
||||
|
||||
Row {
|
||||
id: root
|
||||
spacing: 4
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink]
|
||||
}
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property real volume: sink?.audio?.volume ?? 0
|
||||
readonly property bool muted: sink?.audio?.muted ?? false
|
||||
|
||||
Text {
|
||||
text: root.muted ? "" : (root.volume > 0.5 ? "" : (root.volume > 0 ? "" : ""))
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 1
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round(root.volume * 100) + "%"
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: if (root.sink?.audio) root.sink.audio.muted = !root.sink.audio.muted
|
||||
}
|
||||
}
|
||||
37
modules/Weather.qml
Normal file
37
modules/Weather.qml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: true
|
||||
command: ["wttrbar", "--nerd"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
root.label = data.text ?? "";
|
||||
} catch (e) {
|
||||
root.label = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 3600000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
|
||||
text: root.label
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
12
modules/WindowTitle.qml
Normal file
12
modules/WindowTitle.qml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import QtQuick
|
||||
import Quickshell.Services.Niri
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
text: Niri.focusedWindow?.title ?? ""
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
19
modules/Wlogout.qml
Normal file
19
modules/Wlogout.qml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize + 2
|
||||
font.family: M.Theme.fontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Process { id: proc; command: ["wlogout"] }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: proc.running = true
|
||||
}
|
||||
}
|
||||
36
modules/Workspaces.qml
Normal file
36
modules/Workspaces.qml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Niri
|
||||
import "." as M
|
||||
|
||||
RowLayout {
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: Niri.workspaces
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
implicitWidth: 24
|
||||
implicitHeight: 20
|
||||
radius: 4
|
||||
color: modelData.isFocused
|
||||
? M.Theme.base0D
|
||||
: (modelData.isActive ? M.Theme.base03 : M.Theme.base02)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.idx ?? modelData.id
|
||||
color: modelData.isFocused ? M.Theme.base00 : M.Theme.base05
|
||||
font.pixelSize: M.Theme.fontSize
|
||||
font.family: M.Theme.fontFamily
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Niri.dispatch(["action", "focus-workspace", String(modelData.id)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
modules/qmldir
Normal file
22
modules/qmldir
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
module modules
|
||||
singleton Theme 1.0 Theme.qml
|
||||
Bar 1.0 Bar.qml
|
||||
Workspaces 1.0 Workspaces.qml
|
||||
WindowTitle 1.0 WindowTitle.qml
|
||||
Clock 1.0 Clock.qml
|
||||
Volume 1.0 Volume.qml
|
||||
Tray 1.0 Tray.qml
|
||||
Battery 1.0 Battery.qml
|
||||
Mpris 1.0 Mpris.qml
|
||||
Network 1.0 Network.qml
|
||||
Bluetooth 1.0 Bluetooth.qml
|
||||
Backlight 1.0 Backlight.qml
|
||||
Cpu 1.0 Cpu.qml
|
||||
Memory 1.0 Memory.qml
|
||||
Disk 1.0 Disk.qml
|
||||
Temperature 1.0 Temperature.qml
|
||||
Weather 1.0 Weather.qml
|
||||
PowerProfile 1.0 PowerProfile.qml
|
||||
IdleInhibitor 1.0 IdleInhibitor.qml
|
||||
Notifications 1.0 Notifications.qml
|
||||
Wlogout 1.0 Wlogout.qml
|
||||
101
nix/hm-module.nix
Normal file
101
nix/hm-module.nix
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
self:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.programs.nova-shell;
|
||||
|
||||
stylixAvailable = config ? lib && config.lib ? stylix;
|
||||
|
||||
stylixTheme = lib.mkIf stylixAvailable (
|
||||
let
|
||||
c = config.lib.stylix.colors.withHashtag;
|
||||
f = config.stylix.fonts;
|
||||
in
|
||||
{
|
||||
colors = {
|
||||
inherit (c)
|
||||
base00
|
||||
base01
|
||||
base02
|
||||
base03
|
||||
base04
|
||||
base05
|
||||
base06
|
||||
base07
|
||||
base08
|
||||
base09
|
||||
base0A
|
||||
base0B
|
||||
base0C
|
||||
base0D
|
||||
base0E
|
||||
base0F
|
||||
;
|
||||
};
|
||||
fontFamily = f.sansSerif.name;
|
||||
fontSize = f.sizes.desktop;
|
||||
barOpacity = 1.0 - config.stylix.opacity.desktop;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
options.programs.nova-shell = {
|
||||
enable = lib.mkEnableOption "nova-shell Quickshell bar";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
description = "nova-shell package to use.";
|
||||
};
|
||||
|
||||
theme = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
default = { };
|
||||
description = ''
|
||||
Theme overrides written to `$XDG_CONFIG_HOME/nova-shell/theme.json`.
|
||||
Keys: colors (base00-base0F), fontFamily, fontSize, barOpacity, barHeight.
|
||||
Automatically populated from stylix when it is available.
|
||||
'';
|
||||
};
|
||||
|
||||
systemd = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Run nova-shell as a systemd user service.";
|
||||
};
|
||||
target = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "graphical-session.target";
|
||||
description = "Systemd target to bind the service to.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
programs.nova-shell.theme = lib.mkIf stylixAvailable (lib.mkDefault stylixTheme);
|
||||
|
||||
home.packages = [ cfg.package ];
|
||||
|
||||
xdg.configFile."nova-shell/theme.json".source =
|
||||
(pkgs.formats.json { }).generate "nova-shell-theme.json" cfg.theme;
|
||||
|
||||
systemd.user.services.nova-shell = lib.mkIf cfg.systemd.enable {
|
||||
Unit = {
|
||||
Description = "nova-shell Quickshell bar";
|
||||
PartOf = [ cfg.systemd.target ];
|
||||
After = [ cfg.systemd.target ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = lib.getExe cfg.package;
|
||||
Restart = "on-failure";
|
||||
Slice = "session.slice";
|
||||
};
|
||||
Install.WantedBy = [ cfg.systemd.target ];
|
||||
};
|
||||
};
|
||||
}
|
||||
35
nix/package.nix
Normal file
35
nix/package.nix
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
makeWrapper,
|
||||
quickshell,
|
||||
}:
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "nova-shell";
|
||||
version = "0.1.0";
|
||||
|
||||
src = lib.cleanSource ../.;
|
||||
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/share/nova-shell
|
||||
cp -r shell.qml modules $out/share/nova-shell/
|
||||
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \
|
||||
--add-flags "-p $out/share/nova-shell/shell.qml"
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Minimal Quickshell bar for niri";
|
||||
mainProgram = "nova-shell";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
15
shell.qml
Normal file
15
shell.qml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||||
|
||||
import "modules"
|
||||
import Quickshell
|
||||
|
||||
ShellRoot {
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Bar {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue