initial commit

This commit is contained in:
Damocles 2026-04-10 10:49:48 +02:00
commit 9fde6d4fc6
27 changed files with 1110 additions and 0 deletions

69
flake.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}
}