Compare commits

...

5 Commits

20 changed files with 619 additions and 332 deletions

306
Bar.qml
View File

@@ -1,306 +0,0 @@
import Quickshell
import Quickshell.Io
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.UPower
import Quickshell.Services.SystemTray
import qs.config
Scope {
Variants {
model: Quickshell.screens
PanelWindow {
required property var modelData
screen: modelData
color: "transparent"
anchors {
top: true
}
implicitHeight: 50
implicitWidth: 1150
Rectangle {
anchors.fill: parent
anchors.topMargin: 10
radius: Appearance.radius
color: Appearance.colors.background
FlexboxLayout {
id: flexLayout
anchors.fill: parent
anchors.margins: Appearance.margin
wrap: FlexboxLayout.Wrap
direction: FlexboxLayout.Row
justifyContent: FlexboxLayout.JustifySpaceBetween
Rectangle {
color: 'transparent'
implicitHeight: parent.height
Layout.fillWidth: true
Row {
Layout.fillWidth: true
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Appearance.spacing
Button {
padding: Appearance.padding
display: AbstractButton.IconOnly
icon.color: Appearance.colors.foreground
icon.source: Quickshell.shellPath("assets") + "/icons/nix.svg"
onClicked: {
Quickshell.execDetached(["vicinae", "toggle"]);
}
hoverEnabled: true
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
Behavior on color {
ColorAnimation {
duration: Appearance.duration
}
}
}
}
Button {
property real percentage: Math.round(UPower.displayDevice.percentage * 100)
property real energyRate: UPower.displayDevice.changeRate
property string iconBase: Quickshell.shellPath("assets") + "/icons/"
property string iconName: parent.percentage < 30 ? "battery-low.svg" : parent.percentage < 70 ? "battery-medium.svg" : "battery-full.svg"
padding: Appearance.padding
text: `${percentage}% ${energyRate}W`
palette.buttonText: Appearance.colors.foreground
font.family: Appearance.font.family
font.pointSize: Appearance.font.pointSize
icon.color: Appearance.colors.foreground
icon.source: iconBase + iconName
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: Appearance.colors.inActive
}
}
}
}
Rectangle {
color: 'transparent'
implicitHeight: parent.height
Layout.fillWidth: true
Row {
Layout.fillWidth: true
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing
Repeater {
model: 7
Rectangle {
property var ws: Hyprland.workspaces.values.find(w => w.id === index + 1)
property bool isActive: Hyprland.focusedWorkspace?.id === (index + 1)
Layout.alignment: Qt.AlignVCenter
radius: 1000
implicitHeight: 15
implicitWidth: isActive ? this.implicitHeight * 2.3 : this.implicitHeight
color: {
if (handler.hovered) {
return Appearance.colors.accent;
} else if (isActive || ws) {
return Appearance.colors.accent;
} else {
return Appearance.colors.inActive;
}
}
MouseArea {
hoverEnabled: true
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Hyprland.dispatch("workspace " + (index + 1))
}
Behavior on color {
ColorAnimation {
duration: Appearance.duration
}
}
HoverHandler {
id: handler
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.duration
easing.type: Easing.OutQuad
}
}
}
}
}
}
Rectangle {
color: 'transparent'
implicitHeight: parent.height
Layout.fillWidth: true
Row {
Layout.fillWidth: true
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
spacing: Appearance.spacing
Button {
padding: Appearance.padding
display: AbstractButton.IconOnly
icon.color: Appearance.colors.foreground
icon.source: Quickshell.shellPath("assets") + "/icons/wifi.svg"
onClicked: {
Quickshell.execDetached(["vicinae", "vicinae://extensions/dagimg-dot/wifi-commander/scan-wifi"]);
}
hoverEnabled: true
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
Behavior on color {
ColorAnimation {
duration: Appearance.duration
}
}
}
}
Button {
padding: Appearance.padding
display: AbstractButton.IconOnly
icon.color: Appearance.colors.foreground
icon.source: Quickshell.shellPath("assets") + "/icons/bluetooth.svg"
onClicked: {
Quickshell.execDetached(["vicinae", "vicinae://extensions/Gelei/bluetooth/devices"]);
}
hoverEnabled: true
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
Behavior on color {
ColorAnimation {
duration: Appearance.duration
}
}
}
}
Rectangle {
implicitWidth: sysTrayRow.implicitWidth + 10
implicitHeight: parent.height
radius: Appearance.radius
color: Appearance.colors.inActive
Row {
id: sysTrayRow
anchors.centerIn: parent
spacing: Appearance.spacing
Repeater {
model: SystemTray.items.values.length
Image {
source: SystemTray.items.values[index].icon
height: parent.parent.height - 15
width: parent.parent.height - 15
}
}
}
}
Button {
padding: Appearance.padding
text: Time.time
palette.buttonText: Appearance.colors.foreground
font.family: Appearance.font.family
font.pointSize: Appearance.font.pointSize
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: Appearance.colors.inActive
}
}
Button {
padding: Appearance.padding
display: AbstractButton.IconOnly
icon.color: Appearance.colors.foreground
icon.source: Quickshell.shellPath("assets") + "/icons/ghost.svg"
hoverEnabled: true
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background
Behavior on color {
ColorAnimation {
duration: Appearance.duration
}
}
}
}
}
}
}
}
}
}
}

14
assets/cava.conf Normal file
View File

@@ -0,0 +1,14 @@
[general]
bars = 12
framerate = 60
[input]
method = pulse
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
ascii_max_range = 100
bar_delimiter = 59
frame_delimiter = 10

29
config/BarLayout.qml Normal file
View File

@@ -0,0 +1,29 @@
pragma Singleton
import Quickshell
import qs.ui
import qs.services
Singleton {
id: root
function widget(name: string): url {
return `${Quickshell.shellPath("modules/bar")}/${name}.qml`;
}
property var left: [
root.widget("LauncherButton"),
root.widget("BatteryIndicator"),
root.widget("CavaVisualizer")
]
property var center: [
root.widget("Workspaces")
]
property var right: [
root.widget("SystemTrayWidget"),
root.widget("ClockWidget"),
root.widget("GhostButton")
]
}

41
flake.lock generated
View File

@@ -2,15 +2,15 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1771848320, "lastModified": 1787360063,
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", "narHash": "sha256-dt4WdcvsA8/RCe+VZZwqU0X+XMM3wBbGCWA0/sFWzGo=",
"owner": "nixos", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2fc6539b481e1d2569f25f8799236694180c0993", "rev": "2c423e03bbafcff28bfadc6781a4a8257f205cb5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "NixOS",
"ref": "nixos-unstable", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
@@ -23,11 +23,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1771926182, "lastModified": 1787279335,
"narHash": "sha256-QbXuSLhiSxOq6ydBL3+KGe1aiYWBW+e3J6qjJZaRMq0=", "narHash": "sha256-CLX2Zp5i5BuLbOxNOkwRd9YY84IOrACNxBV79o9/F9Y=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "cddb4f061bab495f4473ca5f2c571b6c710efef7", "rev": "1a4716cde794a59928d9d9fc15f2afc7a95de360",
"revCount": 744, "revCount": 846,
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/outfoxxed/quickshell" "url": "https://git.outfoxxed.me/outfoxxed/quickshell"
}, },
@@ -39,7 +39,28 @@
"root": { "root": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"quickshell": "quickshell" "quickshell": "quickshell",
"treefmt-nix": "treefmt-nix"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1786901030,
"narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
} }
} }
}, },

View File

@@ -2,30 +2,54 @@
description = "tux's widgets for wayland"; description = "tux's widgets for wayland";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
quickshell = { quickshell = {
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell"; url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { outputs =
self, {
nixpkgs, self,
quickshell, nixpkgs,
}: let quickshell,
system = "x86_64-linux"; treefmt-nix,
pkgs = nixpkgs.legacyPackages.${system}; }:
in { let
devShells.${system} = { system = "x86_64-linux";
default = pkgs.mkShell { pkgs = nixpkgs.legacyPackages.${system};
buildInputs = [
quickshell.packages.${system}.default quickshellWithModules = quickshell.packages.${system}.default.withModules [
pkgs.kdePackages.qtdeclarative pkgs.qt6.qtbase
pkgs.kdePackages.qt5compat pkgs.qt6.qtdeclarative
pkgs.qt6.qtmultimedia
pkgs.qt6.qttranslations
];
treefmtEval = treefmt-nix.lib.evalModule pkgs {
projectRootFile = "flake.nix";
programs = {
nixfmt.enable = true;
};
};
in
{
formatter.${system} = treefmtEval.config.build.wrapper;
devShells.${system}.default = pkgs.mkShell {
packages = [
quickshellWithModules
pkgs.cava
pkgs.wallust
]; ];
}; };
}; };
};
} }

View File

@@ -0,0 +1,11 @@
pragma ComponentBehavior: Bound
import qs.ui
import qs.services
BarButton {
hoverHighlight: false
pointerCursor: false
label: `${Battery.percentage}% ${Battery.energyRate}W`
iconSource: Battery.icon
}

View File

@@ -0,0 +1,51 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.config
import qs.services
Rectangle {
id: root
property int barCount: 14
property real maxValue: 100
readonly property bool shouldVisualize: barCount > 0 && Cava.values.some(v => v >= 0.001)
implicitWidth: 96
implicitHeight: 32
radius: Appearance.radius
color: Appearance.colors.inActive
visible: shouldVisualize
Row {
id: barRow
anchors.fill: parent
anchors.margins: 8
spacing: 3
Repeater {
model: root.barCount
Rectangle {
id: bar
required property int index
readonly property real value: Cava.values[index] ?? 0
width: (barRow.width - barRow.spacing * (root.barCount - 1)) / root.barCount
height: Math.max(2, (bar.value / root.maxValue) * barRow.height)
anchors.bottom: parent.bottom
radius: bar.width / 2
color: Appearance.colors.accent
antialiasing: true
Behavior on height {
NumberAnimation {
duration: Appearance.duration
}
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
pragma ComponentBehavior: Bound
import qs.ui
import qs.services
BarButton {
hoverHighlight: false
pointerCursor: false
label: Time.time
}

View File

@@ -0,0 +1,9 @@
pragma ComponentBehavior: Bound
import Quickshell
import qs.ui
BarButton {
iconOnly: true
iconSource: `${Quickshell.shellPath("assets")}/icons/ghost.svg`
}

View File

@@ -0,0 +1,10 @@
pragma ComponentBehavior: Bound
import Quickshell
import qs.ui
BarButton {
iconOnly: true
iconSource: `${Quickshell.shellPath("assets")}/icons/nix.svg`
onClicked: Quickshell.execDetached(["vicinae", "toggle"])
}

View File

@@ -0,0 +1,32 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Services.SystemTray
import qs.config
import qs.ui
BarButton {
id: root
property int iconSize: 16
hoverHighlight: false
pointerCursor: false
visible: SystemTray.items.values.length > 0
contentItem: Row {
spacing: Appearance.spacing
Repeater {
model: SystemTray.items.values
Image {
required property var modelData
source: modelData.icon
width: root.iconSize
height: root.iconSize
}
}
}
}

View File

@@ -0,0 +1,60 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Hyprland
import qs.config
Row {
id: root
property int count: 7
spacing: Appearance.spacing
Repeater {
model: root.count
Rectangle {
id: pill
required property int index
property var ws: Hyprland.workspaces.values.find(w => w.id === pill.index + 1)
property bool isActive: Hyprland.focusedWorkspace?.id === (pill.index + 1)
radius: 1000
implicitHeight: 15
implicitWidth: pill.isActive ? pill.implicitHeight * 2.3 : pill.implicitHeight
color: {
if (handler.hovered)
return Appearance.colors.accent;
if (pill.isActive || pill.ws)
return Appearance.colors.accent;
return Appearance.colors.inActive;
}
MouseArea {
hoverEnabled: true
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Hyprland.dispatch("workspace " + (pill.index + 1))
}
HoverHandler {
id: handler
}
Behavior on color {
ColorAnimation {
duration: Appearance.duration
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.duration
easing.type: Easing.OutQuad
}
}
}
}
}

13
services/Battery.qml Normal file
View File

@@ -0,0 +1,13 @@
pragma Singleton
import Quickshell
import QtQuick
import Quickshell.Services.UPower
Singleton {
id: root
readonly property int percentage: Math.round(UPower.displayDevice.percentage * 100)
readonly property real energyRate: UPower.displayDevice.changeRate
readonly property string iconName: root.percentage < 30 ? "battery-low.svg" : root.percentage < 70 ? "battery-medium.svg" : "battery-full.svg"
readonly property url icon: `${Quickshell.shellPath("assets")}/icons/${root.iconName}`
}

26
services/Cava.qml Normal file
View File

@@ -0,0 +1,26 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
readonly property string configPath: {
const url = Quickshell.shellPath("assets/cava.conf").toString();
return url.startsWith("file://") ? decodeURIComponent(url.substring(7)) : url;
}
property var values: []
Process {
running: true
command: ["cava", "-p", root.configPath]
stdout: SplitParser {
onRead: line => {
root.values = line.split(";").map(Number).filter(v => !Number.isNaN(v));
}
}
}
}

View File

@@ -1,4 +1,5 @@
import Quickshell import Quickshell
import qs.windows
Scope { Scope {
Bar {} Bar {}

46
ui/BarButton.qml Normal file
View File

@@ -0,0 +1,46 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import qs.config
Button {
id: root
property string label: ""
property url iconSource: ""
property bool iconOnly: false
property bool hoverHighlight: true
property bool pointerCursor: true
padding: Appearance.padding
display: iconOnly ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
text: label
font.family: Appearance.font.family
font.pointSize: Appearance.font.pointSize
palette.buttonText: Appearance.colors.foreground
icon.color: Appearance.colors.foreground
icon.source: iconSource
HoverHandler {
cursorShape: Qt.PointingHandCursor
enabled: root.pointerCursor
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.radius
color: {
if (!root.hoverHighlight)
return Appearance.colors.inActive;
return root.hovered ? Appearance.colors.inActive : Appearance.colors.background;
}
Behavior on color {
ColorAnimation {
duration: Appearance.duration
}
}
}
}

12
ui/ExclusionZone.qml Normal file
View File

@@ -0,0 +1,12 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
PanelWindow {
property string name
implicitWidth: 0
implicitHeight: 0
WlrLayershell.namespace: `quickshell:${name}ExclusionZone`
}

66
ui/RoundedCorner.qml Normal file
View File

@@ -0,0 +1,66 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import Quickshell.Widgets
import qs.config
WrapperItem {
id: root
property int corner
property real radius: Appearance.radius
property color color
Component.onCompleted: {
switch (corner) {
case 0:
anchors.left = parent.left;
anchors.top = parent.top;
break;
case 1:
anchors.top = parent.top;
anchors.right = parent.right;
rotation = 90;
break;
case 2:
anchors.right = parent.right;
anchors.bottom = parent.bottom;
rotation = 180;
break;
case 3:
anchors.left = parent.left;
anchors.bottom = parent.bottom;
rotation = -90;
break;
}
}
Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 0
fillColor: root.color
startX: root.radius
PathArc {
relativeX: -root.radius
relativeY: root.radius
radiusX: root.radius
radiusY: radiusX
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -root.radius
}
PathLine {
relativeX: root.radius
relativeY: 0
}
}
}
}

158
windows/Bar.qml Normal file
View File

@@ -0,0 +1,158 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
import qs.config
import qs.ui
PanelWindow {
id: window
property color barColor: Appearance.colors.background
color: "transparent"
exclusionMode: ExclusionMode.Ignore
mask: Region {
item: cornersArea
intersection: Intersection.Subtract
}
anchors {
left: true
top: true
right: true
bottom: true
}
component BarSection: Rectangle {
id: section
property var widgets: []
property int alignment: Qt.AlignLeft
color: "transparent"
implicitHeight: parent.height
Layout.fillWidth: true
Row {
x: section.alignment === Qt.AlignLeft ? 0 : section.alignment === Qt.AlignRight ? parent.width - width : (parent.width - width) / 2
y: (section.height - height) / 2
spacing: Appearance.spacing
Repeater {
model: section.widgets
Loader {
required property var modelData
source: modelData
}
}
}
}
// Exclusions
Scope {
ExclusionZone {
name: "left"
exclusiveZone: leftBar.implicitWidth
anchors.left: true
}
ExclusionZone {
name: "top"
exclusiveZone: topBar.implicitHeight
anchors.top: true
}
ExclusionZone {
name: "right"
exclusiveZone: rightBar.implicitWidth
anchors.right: true
}
ExclusionZone {
name: "bottom"
exclusiveZone: bottomBar.implicitHeight
anchors.bottom: true
}
}
// Bars
Rectangle {
id: leftBar
implicitWidth: 10
implicitHeight: QsWindow.window?.height ?? 0
color: window.barColor
anchors.left: parent.left
}
Rectangle {
id: rightBar
implicitWidth: 10
implicitHeight: QsWindow.window?.height ?? 0
color: window.barColor
anchors.right: parent.right
}
Rectangle {
id: bottomBar
implicitWidth: QsWindow.window?.width ?? 0
implicitHeight: 10
color: window.barColor
anchors.bottom: parent.bottom
}
Rectangle {
id: topBar
implicitWidth: QsWindow.window?.width ?? 0
implicitHeight: 50
color: window.barColor
anchors.top: parent.top
FlexboxLayout {
id: flexLayout
anchors.fill: parent
anchors.margins: Appearance.margin
wrap: FlexboxLayout.Wrap
direction: FlexboxLayout.Row
justifyContent: FlexboxLayout.JustifySpaceBetween
BarSection {
widgets: BarLayout.left
alignment: Qt.AlignLeft
}
BarSection {
widgets: BarLayout.center
alignment: Qt.AlignHCenter
}
BarSection {
widgets: BarLayout.right
alignment: Qt.AlignRight
}
}
}
Rectangle {
id: cornersArea
implicitWidth: QsWindow.window?.width - (leftBar.implicitWidth + rightBar.implicitWidth)
implicitHeight: QsWindow.window?.height - (topBar.implicitHeight + bottomBar.implicitHeight)
color: "transparent"
x: leftBar.implicitWidth
y: topBar.implicitHeight
Repeater {
model: 4
RoundedCorner {
required property int modelData
corner: modelData
color: window.barColor
}
}
}
}