Compare commits

..

51 Commits

Author SHA1 Message Date
b00e013752 only opens menu on one monitor and added background 2026-08-17 17:46:26 +01:00
3c7f2c2b75 keybinds working for logout menu, very cool 2026-08-17 17:05:09 +01:00
4a813121c7 removed duplicate state that was causing a warning 2026-08-17 17:04:43 +01:00
f90e62d25b renamed time to timemanager to be consistent 2026-08-17 16:19:19 +01:00
e33041b66c barebones logout screen implementation (too lazy to figure out wlr shell stuff rn) 2026-08-12 16:18:19 +01:00
88d867f1de ipc manager 2026-08-12 16:17:49 +01:00
b09039cabb fixed weird hovering thing for user experience 2026-08-12 12:28:54 +01:00
dc82f5a0ae refactored default state stuff to be in the container component instead of repeated 4 times 2026-08-12 12:27:12 +01:00
802b077362 long ass readme and prettier config to format said readme 2026-08-10 20:44:25 +01:00
9baa312d0e added private ip on network view 2026-08-10 20:12:04 +01:00
f246b7e658 Merge branch 'rewrite-again' 2026-08-10 13:26:09 +01:00
d7de61fac8 formatting and adding exclusivetoscreen to the right modules 2026-08-10 13:25:57 +01:00
006697b768 hover for mpris working 2026-08-09 17:06:09 +01:00
6cce500670 mpris popup working 2026-08-09 17:02:40 +01:00
0203236575 notifications working perfectly they look so cool 2026-08-09 15:38:02 +01:00
1c5a25c8fa null safety to remove warnings 2026-08-09 15:37:46 +01:00
9f72a8abad formatting 2026-08-09 15:37:30 +01:00
04e2ca54be consistent wifi display (looks good) 2026-08-09 13:01:24 +01:00
f2ed2e7a4e notifications working 2026-08-09 12:48:34 +01:00
3250231384 animation offsets to look nice 2026-08-09 12:48:27 +01:00
79d83cbe37 had opencode fix my weird animation issues, now the bar loads correctly from a coldstart 2026-08-09 12:23:50 +01:00
021ddae2d0 null safety so that it might load properly idk 2026-08-08 19:59:10 +01:00
b3c46e2df2 made it rely on laptop battery 2026-08-08 19:58:49 +01:00
e371c9cc7b fixed discharging vs charging thing because the wiki lies about the rate parameter 2026-08-08 19:46:45 +01:00
7858c5b90d fixed bluetooth menu margin 2026-08-08 19:46:17 +01:00
300cb0b2f8 removed duplicate animation 2026-08-08 17:11:17 +01:00
a90afb3154 hide battery icon when no laptop battery present 2026-08-08 17:11:02 +01:00
d97a6e0a48 sick ass animation for forceHidden 2026-08-08 17:08:45 +01:00
02e99cca74 battery done looks good to me 2026-08-08 15:22:53 +01:00
cf4db3b8df added fontweight property to styledtext 2026-08-08 15:22:46 +01:00
d0e1e2170c battery things 2026-08-07 20:32:01 +01:00
01fdddf0e8 made gradient rotatable 2026-08-07 20:31:56 +01:00
ce5bbb1dff fixed bluetooth height 2026-08-07 20:31:47 +01:00
f5623585fb network finished 2026-08-07 18:25:46 +01:00
942b179572 added closed state to stop weird sizing behavior 2026-08-07 18:25:37 +01:00
8267743fb5 bluetooth donesies i think 2026-08-07 17:14:38 +01:00
6f3e62bcf9 added networkmanager 2026-08-07 16:18:24 +01:00
d54487022f made panelwindow apply to the whole bar instead of individual containers 2026-08-07 16:18:17 +01:00
11a8f03215 bluetooth menu coming along 2026-08-07 14:49:49 +01:00
7cceb3a91a functional bluetooth module 2026-08-06 17:44:23 +01:00
4ef5f39f6d added propo option to StyledText 2026-08-06 17:44:10 +01:00
78f92f1dbf fixed "nothing to push error" 2026-08-06 17:43:55 +01:00
185e7618a4 fixed weird layout thing 2026-08-06 17:43:44 +01:00
bc2f308673 changed width 2026-08-06 17:43:27 +01:00
89ee2ec974 now can press the workspace to switch to it 2026-08-06 14:14:58 +01:00
abe1a1e12f functional workspace display 2026-08-06 14:09:14 +01:00
8e59d2dcec using the old functions to get some rectangles to draw 2026-08-06 10:33:54 +01:00
8ba73b061f have a clock again 2026-08-06 10:09:56 +01:00
9e0887c474 deleted everything lol 2026-08-06 10:09:32 +01:00
76c437da88 added battery thing but it doesnt work 2026-08-06 09:30:04 +01:00
afbe360b08 increased font weight due to update 2026-07-29 10:19:22 +01:00
31 changed files with 1878 additions and 1609 deletions

8
.prettierrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"tabWidth": 2,
"printWidth": 90,
"useTabs": false,
"proseWrap": "always",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto"
}

85
Bar.qml
View File

@@ -1,16 +1,25 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "Bar"
import Quickshell.Wayland
import "./Bar"
import "./Services/"
import "Color.js" as Colors
Scope {
Variants {
model: Quickshell.screens
id: panelDelegate
delegate: panelWindow
model: Quickshell.screens
}
Component {
id: panelWindow
delegate: Component {
PanelWindow {
id: win
@@ -19,13 +28,33 @@ Scope {
required property var modelData
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
WlrLayershell.keyboardFocus: monitor.focused == true ? IpcManager.focusState :
WlrKeyboardFocus.None
color: "transparent"
exclusionMode: ExclusionMode.Normal
exclusiveZone: barExclusionZone
implicitHeight: barMaxHeight
mask: maskRegion
screen: modelData
mask: Region {
Rectangle {
id: blurOverlay
color: Colors.base
implicitHeight: win.modelData.height
implicitWidth: win.modelData.width
opacity: IpcManager.logoutMenuOpen == true ? 0.8 : 0
Behavior on opacity {
NumberAnimation {
duration: 200
}
}
}
Region {
id: maskRegion
Region {
item: left
}
@@ -45,23 +74,42 @@ Scope {
top: true
}
MultiEffect {
anchors.fill: rowContainer
shadowBlur: 1.5
shadowColor: Colors.mantle
shadowEnabled: true
shadowHorizontalOffset: 5
shadowOpacity: 1
shadowVerticalOffset: 3
source: rowContainer
}
Item {
id: rowContainer
anchors {
fill: parent
topMargin: 3
}
anchors {
left: parent.left
leftMargin: 3
right: parent.right
rightMargin: 3
top: parent.top
}
Row {
id: left
height: childrenRect.height
anchors {
left: parent.left
leftMargin: 7
top: parent.top
}
Left {
WorkspaceSelector {
monitor: win.monitor
}
}
@@ -69,13 +117,12 @@ Scope {
Row {
id: center
height: childrenRect.height
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
}
Center {
Time {
monitor: win.monitor
}
}
@@ -83,19 +130,27 @@ Scope {
Row {
id: right
height: childrenRect.height
Layout.alignment: Qt.AlignTop
spacing: 5
anchors {
right: parent.right
rightMargin: 7
top: parent.top
}
Right {
Network {
monitor: win.monitor
}
Bluetooth {
monitor: win.monitor
}
Battery {
monitor: win.monitor
}
}
}
}
}
}
}

144
Bar/Battery.qml Normal file
View File

@@ -0,0 +1,144 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
Container {
id: root
property double batteryDisplayHeight: 100
required property HyprlandMonitor monitor
property bool opened: false
property double radius: 9
animOffset: 150
boxColor: Colors.green
boxHeight: 25
boxHeightOpened: root.batteryDisplayHeight
boxRadius: radius
boxWidth: 60
boxWidthOpened: 410
defaultItem: icon
exclusiveMonitor: root.monitor
exclusiveToScreen: true
forceHidden: !BatteryManager.hasLaptopBattery
openedItem: batteryDisplay
Component {
id: icon
Item {
BatteryGradient {}
CenteredText {
property string component: "icon"
propo: true
text: BatteryManager.getBatteryText()
}
}
}
Component {
id: batteryDisplay
Item {
id: batteryDisplayRoot
property double innerMargin: 5
property double outerMargin: 6
BatteryGradient {}
Rectangle {
id: batteryBackground
anchors.fill: parent
anchors.margins: batteryDisplayRoot.outerMargin
color: Colors.surface1
radius: 8
RowLayout {
anchors.fill: parent
Rectangle {
id: percentBox
property double percentBoxRadius: 5
Layout.fillHeight: true
Layout.margins: batteryDisplayRoot.innerMargin
color: Colors.text
implicitWidth: root.batteryDisplayHeight - (batteryDisplayRoot.innerMargin * 2) - (
batteryDisplayRoot.outerMargin * 2)
radius: 5
Rectangle {
color: Colors.green
implicitHeight: (root.batteryDisplayHeight - (batteryDisplayRoot.innerMargin * 2) - (
batteryDisplayRoot.outerMargin * 2)) * BatteryManager.mainBattery.percentage
radius: percentBox.percentBoxRadius
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
}
CenteredText {
fontSize: 15
text: BatteryManager.getBatteryPercentage(BatteryManager.mainBattery) + "%"
}
}
Column {
id: batColumn
property var batInfo: BatteryManager.getMainBatteryInfo()
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
color: Colors.text
fontSize: 14
text: "Battery - " + batColumn.batInfo["name"]
}
StyledText {
color: Colors.subtext1
fontSize: 12
fontWeight: 5
text: batColumn.batInfo["energy"] + "/" + batColumn.batInfo["capacity"] + "Wh - "
+ BatteryManager.getChargingRateFormat()
}
StyledText {
property var timeInfo: batColumn.batInfo["time"]
color: Colors.subtext1
fontSize: 12
fontWeight: 5
text: BatteryManager.getTimeFormat()
}
}
}
}
}
}
component BatteryGradient: MidpointGradient {
anchors.fill: parent
angle: -45
blur: 0.6
color: BatteryManager.getBatteryColor()
midpoint: 0.55
radius: radius
}
}

165
Bar/Bluetooth.qml Normal file
View File

@@ -0,0 +1,165 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
Container {
id: root
property double devicePlaqueHeight: 60
property double devicePlaqueSpacing: 4
required property HyprlandMonitor monitor
animOffset: 200
boxColor: Colors.peach
boxHeight: 25
boxHeightOpened: (root.devicePlaqueHeight * BluetoothManager.getConnectedDevicesList().length) + (
root.devicePlaqueSpacing * (BluetoothManager.getConnectedDevicesList().length + 1)) + 2
boxWidth: 33
boxWidthOpened: 350
defaultItem: icon
exclusiveMonitor: root.monitor
exclusiveToScreen: true
forceHidden: !BluetoothManager.anyConnected
openedItem: deviceList
Component {
id: icon
StyledText {
property string component: "icon"
horizontalAlignment: Qt.AlignCenter
propo: true
text: "󰂱"
verticalAlignment: Qt.AlignVCenter
}
}
Component {
id: deviceList
Column {
id: deviceListRoot
property string component: "deviceList"
property double plaqueHeight: root.devicePlaqueHeight
property double plaqueSpacing: root.devicePlaqueSpacing
spacing: plaqueSpacing
Item {
implicitHeight: 1
implicitWidth: 10
}
Repeater {
delegate: devicePlaque
model: BluetoothManager.getConnectedDevicesList()
}
Component {
id: devicePlaque
Rectangle {
id: devicePlaqueRoot
readonly property variant deviceText: BluetoothManager.getDeviceText(modelData)
readonly property double margin: 5
required property var modelData
color: Colors.surface1
implicitHeight: deviceListRoot.plaqueHeight
radius: 10
anchors {
left: parent.left
leftMargin: devicePlaqueRoot.margin
right: parent.right
rightMargin: devicePlaqueRoot.margin
}
RowLayout {
id: deviceInfoRow
property double margin: 7
spacing: margin * 2
anchors {
fill: parent
leftMargin: margin
}
Rectangle {
id: iconContainer
property double boxRadius: 4
color: Colors.text
implicitHeight: deviceListRoot.plaqueHeight - (deviceInfoRow.margin * 2)
implicitWidth: deviceListRoot.plaqueHeight - (deviceInfoRow.margin * 2)
radius: boxRadius
Rectangle {
id: batteryIndicator
color: Colors.green
implicitHeight: (deviceListRoot.plaqueHeight - (deviceInfoRow.margin * 2))
* devicePlaqueRoot.deviceText["batteryRaw"]
radius: iconContainer.boxRadius
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
}
HoverHandler {
id: batteryHoverHandler
onHoveredChanged: {
if (batteryHoverHandler.hovered == true) {
batteryText.text = devicePlaqueRoot.deviceText["battery"];
} else {
batteryText.text = devicePlaqueRoot.deviceText["icon"];
}
}
}
CenteredText {
id: batteryText
fontSize: 17
text: devicePlaqueRoot.deviceText["icon"]
}
}
Column {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
color: Colors.text
fontSize: 14
text: devicePlaqueRoot.deviceText["name"]
}
StyledText {
color: Colors.subtext1
fontSize: 10
text: devicePlaqueRoot.deviceText["mac"]
}
}
}
}
}
}
}
}

View File

@@ -1,263 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Mpris
import Quickshell.Services.Notifications
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
import "./Center"
Container {
id: root
property Notification latestNotification: NotificationManager.getLatestNotification() || null
required property HyprlandMonitor monitor
property NotificationServer notifServer: NotificationManager.notif
property string previousState
boxColor: Colors.mauve
defaultItem: time
exclusiveMonitor: root.monitor
exclusiveToScreen: true
states: [
State {
name: ""
PropertyChanges {
root.boxHeight: 28
root.boxRadius: 9
root.boxWidth: 100
root.visibleTopMargin: 0
}
StateChangeScript {
script: {
if (root.previousState != "hovered") {
root.stack.replace(time);
}
root.previousState = "";
}
}
},
State {
name: "hovered"
PropertyChanges {
root.boxHeight: 35
root.boxRadius: 15
root.boxWidth: 110
root.visibleTopMargin: 0
}
StateChangeScript {
script: {
root.previousState = "hovered";
}
}
},
State {
name: "mprisToast"
PropertyChanges {
root.boxHeight: 35
root.boxRadius: 15
root.boxWidth: 350
root.visibleTopMargin: 0
}
StateChangeScript {
script: {
root.previousState = "mpris";
root.stack.replace(mpris);
}
}
},
State {
name: "expanded"
PropertyChanges {
root.boxHeight: 140
root.boxRadius: 30
root.boxWidth: 300
root.visibleTopMargin: 0
}
StateChangeScript {
script: {
if (root.state != "notifications") {
root.stack.replace(fullTime);
root.previousState = "expanded";
}
}
}
},
State {
extend: "expanded"
name: "notifications"
PropertyChanges {
root.boxHeight: 320
}
},
State {
name: "notified"
PropertyChanges {
root.boxHeight: 90
root.boxRadius: 30
root.boxWidth: 330
root.visibleTopMargin: 20
}
StateChangeScript {
script: {
root.stack.replace(notification);
root.previousState = "notified";
}
}
}
]
hover.onHoveredChanged: {
if (hover.hovered == true) {
if (root.state == "notified") {
root.state = "notified";
notificationViewTimer.stop();
} else if (MprisManager.getPlaying() == true) {
root.state = "mprisToast";
} else {
root.state = "hovered";
}
} else {
root.state = "";
}
}
onLatestNotificationChanged: {
if (latestNotification != null && Hyprland.focusedMonitor == monitor) {
if (root.latestNotification.lastGeneration == false) {
root.state = "notified";
notificationViewTimer.restart();
}
}
}
tap.onTapped: {
root.state == "hovered" ? root.state = "expanded" : undefined;
}
Connections {
function onTrackChanged() {
if (Hyprland.focusedMonitor == root.monitor) {
musicToastTimer.restart();
if (root.state == "") {
root.state = "mprisToast";
}
}
}
target: MprisManager.defaultPlayer
}
Timer {
id: notificationViewTimer
interval: 3000
repeat: false
running: false
onTriggered: {
root.state = "";
}
}
Timer {
id: musicToastTimer
interval: 3000
repeat: false
running: false
onTriggered: {
if (!root.hover.hovered) {
root.state = "";
}
}
}
Component {
id: time
Item {
CenteredText {
text: Time.time
}
}
}
Component {
id: mpris
Mpris {
TapHandler {
onTapped: root.state = "expanded"
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: MprisManager.skip()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: MprisManager.prev()
}
}
}
Component {
id: fullTime
Expanded {
NotificationList {
id: notifList
animEnabled: false
radius: root.boxRadius
rootState: root.state
server: root.notifServer
state: ""
TapHandler {
onTapped: {
notifList.animEnabled = true;
root.state = "notifications";
Qt.callLater(() => {
notifList.animEnabled = false;
});
}
}
}
}
}
Component {
id: notification
NotificationDisplay {
body: root.latestNotification.body
summary: root.latestNotification.summary
TapHandler {
onTapped: {
root.state = "notifications";
root.stack.replace(fullTime);
}
}
}
}
}

View File

@@ -1,55 +0,0 @@
import QtQuick
import Quickshell
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: fullTimeRoot
Rectangle {
anchors.fill: parent
radius: 30
gradient: MidpointGradient {
color: Colors.red
}
}
Item {
id: timeColumnContainer
anchors.fill: parent
Column {
id: timeColumn
spacing: 5
anchors {
centerIn: parent
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.surface0
text: Time.time
font {
pointSize: 30
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.surface1
text: Time.date
font {
pointSize: 9
weight: 400
}
}
}
}
}

View File

@@ -1,62 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: root
property MprisPlayer player: MprisManager.defaultPlayer
Rectangle {
color: Colors.red
radius: 5
anchors {
bottomMargin: 15
fill: parent
leftMargin: 10
rightMargin: parent.width - ((parent.width * MprisManager.pos) - 10)
topMargin: 15
}
}
RowLayout {
anchors {
bottomMargin: 2
fill: parent
leftMargin: 7
rightMargin: 7
topMargin: 2
}
Item {
Layout.fillHeight: true
Layout.preferredWidth: height
Image {
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: MprisManager.defaultPlayer.trackArtUrl
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
StyledText {
elide: Qt.ElideRight
horizontalAlignment: Qt.AlignHCenter
text: MprisManager.defaultPlayer.trackArtist + " - " + MprisManager.defaultPlayer.trackTitle
verticalAlignment: Qt.AlignVCenter
anchors {
fill: parent
}
}
}
}
}

View File

@@ -1,49 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: root
required property string body
required property string summary
Rectangle {
anchors.fill: parent
radius: 30
gradient: MidpointGradient {
color: Colors.green
midpoint: 0.7
}
}
Column {
anchors.centerIn: parent
spacing: 5
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 11
text: root.summary
}
StyledText {
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
text: root.body
verticalAlignment: Text.AlignVCenter
width: root.width - 20
wrapMode: Text.WordWrap
font {
pointSize: 10
weight: 500
}
}
}
}

View File

@@ -1,307 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Notifications
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: root
required property bool animEnabled
required property double radius
required property string rootState
required property NotificationServer server
Behavior on anchors.topMargin {
enabled: root.animEnabled
SpringAnimation {
damping: 0.35
spring: 4
}
}
states: [
State {
name: ""
when: root.rootState != "notifications"
PropertyChanges {
root.anchors.topMargin: root.parent.height - 21
}
},
State {
name: "expanded"
when: root.rootState == "notifications"
PropertyChanges {
root.anchors.topMargin: 5
}
}
]
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
top: parent.top
topMargin: parent.height - 21
}
Rectangle {
id: background
anchors.fill: parent
color: Colors.surface0
radius: root.radius - 3
states: [
State {
name: ""
when: root.rootState != "notifications"
PropertyChanges {}
},
State {
name: "expanded"
when: root.rootState == "notifications"
PropertyChanges {
background.anchors.bottomMargin: 5
background.anchors.leftMargin: 5
background.anchors.rightMargin: 5
}
}
]
anchors {
bottomMargin: 15
leftMargin: 20
rightMargin: 20
}
}
Item {
id: notifListContainer
anchors.fill: parent
clip: true
opacity: 0
anchors {
bottomMargin: 5
leftMargin: 5
rightMargin: 5
topMargin: 5
}
CenteredText {
color: Colors.overlay0
text: "No Notifications"
visible: root.server.trackedNotifications.values.length <= 0
}
ListView {
id: notifScrollingList
anchors.fill: parent
boundsBehavior: Flickable.StopAtBounds
height: Math.min(contentHeight, parent.height)
implicitHeight: background.height
model: root.server.trackedNotifications.values.length
spacing: 7
delegate: Item {
id: notifRoot
property Notification currentNotif: root.server.trackedNotifications.values[(notifRoot.notifLen
- 1) - index]
required property real index
property real notifLen: root.server.trackedNotifications.values.length
height: 50
width: ListView.view.width
states: [
State {
name: ""
when: notifHover.hovered == false
},
State {
name: "hovered"
when: notifHover.hovered == false
PropertyChanges {}
}
]
HoverHandler {
id: notifHover
}
Rectangle {
id: dismissButton
color: Colors.red
implicitWidth: 100
radius: 10
Behavior on anchors.rightMargin {
NumberAnimation {
duration: 100
}
}
states: [
State {
name: ""
when: notifHover.hovered == false
},
State {
name: "hovered"
when: notifHover.hovered == true
PropertyChanges {
dismissButton.anchors.rightMargin: 5
}
}
]
StyledText {
color: Colors.surface0
font.pointSize: 15
horizontalAlignment: Qt.AlignRight
text: "󰩹"
verticalAlignment: Qt.AlignVCenter
anchors {
fill: parent
rightMargin: 14
}
}
TapHandler {
onTapped: {
notifRoot.currentNotif.dismiss();
}
}
anchors {
bottom: parent.bottom
bottomMargin: 3
right: parent.right
rightMargin: 10
top: parent.top
topMargin: 3
}
}
Rectangle {
id: textContainer
anchors.fill: parent
clip: true
color: Colors.surface2
radius: 9
Behavior on anchors.rightMargin {
NumberAnimation {
duration: 100
}
}
states: [
State {
name: ""
when: notifHover.hovered == false
},
State {
name: "hovered"
when: notifHover.hovered == true
PropertyChanges {
textContainer.anchors.rightMargin: 40
}
}
]
Column {
id: textColumn
anchors {
centerIn: parent
left: parent.left
right: parent.right
}
StyledText {
color: Colors.text
horizontalAlignment: Text.AlignHCenter
text: notifRoot.currentNotif.summary
verticalAlignment: Text.AlignVCenter
width: notifRoot.width - 10
}
StyledText {
color: Colors.subtext1
horizontalAlignment: Text.AlignHCenter
text: notifRoot.currentNotif.body
verticalAlignment: Text.AlignVCenter
width: notifRoot.width - 10
}
}
}
}
anchors {
bottomMargin: 5
leftMargin: 10
rightMargin: 10
topMargin: 5
}
}
}
MultiEffect {
id: notifListClipper
anchors.fill: notifListContainer
maskEnabled: true
maskSource: clipsource
opacity: 0
source: notifListContainer
Behavior on opacity {
NumberAnimation {
duration: 100
}
}
states: [
State {
name: ""
when: root.rootState != "notifications"
PropertyChanges {
notifListClipper.opacity: 0
}
},
State {
name: "expanded"
when: root.rootState == "notifications"
PropertyChanges {
notifListClipper.opacity: 1
}
}
]
}
Rectangle {
id: clipsource
anchors.fill: background
color: "black"
layer.enabled: true
radius: background.radius
visible: false
}
}

View File

@@ -1,70 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Notifications
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
import "./Center"
import "./Left/"
Container {
id: root
property double extraHoveredWidth: 0
required property HyprlandMonitor monitor
property double rowLeftMargin: 4
property double rowSpacing: 3
property double rowYmargin: 3
property double selectorFocusedExtra: 15
property double selectorHeight: 20
property double selectorWidth: 20
property double spacing: 3
property double workspaceNumber: WorkspaceManager.getNumberOfWorkspaces(monitor)
boxHeight: 25
boxWidth: (selectorWidth * workspaceNumber) + ((2 * rowLeftMargin) + (rowSpacing * (
workspaceNumber - 1))) + extraHoveredWidth + (hoverHandler.hovered == true
? selectorFocusedExtra : 0)
exclusiveMonitor: monitor
defaultItem: WorkspaceContainer {
extraHoveredWidth: root.extraHoveredWidth
monitor: root.monitor
rowLeftMargin: root.rowLeftMargin
rowSpacing: root.rowSpacing
rowYmargin: root.rowYmargin
selectorFocusedExtra: root.selectorFocusedExtra
selectorHeight: root.selectorHeight
selectorWidth: root.selectorWidth
}
Behavior on rowYmargin {
SpringAnimation {
damping: 0.3
spring: 4
}
}
states: [
State {
name: ""
when: !hoverHandler.hovered
},
State {
name: "hovered"
when: hoverHandler.hovered
PropertyChanges {
root.boxHeight: 40
root.extraHoveredWidth: 25
root.rowYmargin: 5
root.selectorWidth: 40
}
}
]
HoverHandler {
id: hoverHandler
}
}

View File

@@ -1,86 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: root
required property double extraHoveredWidth
required property HyprlandMonitor monitor
required property double rowLeftMargin
required property double rowSpacing
required property double rowYmargin
required property double selectorFocusedExtra
required property double selectorHeight
required property double selectorWidth
Item {
id: newWorkspaceButton
implicitWidth: 20
opacity: root.extraHoveredWidth > 0 ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 100
}
}
TapHandler {
id: tapHandler
onTapped: WorkspaceManager.activateNextFreeWorkspace()
}
anchors {
bottom: parent.bottom
bottomMargin: 5 + root.rowYmargin
right: parent.right
rightMargin: 4
top: parent.top
topMargin: 5 + root.rowYmargin
}
Rectangle {
anchors.fill: parent
color: Colors.green
radius: 4
StyledText {
anchors.centerIn: parent
text: "+"
}
}
}
Row {
spacing: root.rowSpacing
anchors {
bottom: parent.bottom
bottomMargin: root.rowYmargin
left: parent.left
leftMargin: root.rowLeftMargin
top: parent.top
topMargin: root.rowYmargin
}
Repeater {
model: WorkspaceManager.getWorkspacesForMonitor(root.monitor)
WorkspaceSelector {
required property HyprlandWorkspace modelData
extraHoveredWidth: root.extraHoveredWidth
selectorFocusedExtra: root.selectorFocusedExtra
selectorHeight: root.selectorHeight
selectorWidth: root.selectorWidth
workspace: modelData
}
}
}
}

View File

@@ -1,87 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: root
required property double extraHoveredWidth
property bool hovered: extraHoveredWidth > 0 ? true : false
property color selectorColor: Colors.surface2
required property double selectorFocusedExtra
required property double selectorHeight
required property double selectorWidth
required property HyprlandWorkspace workspace
property string workspaceState: WorkspaceManager.getWorkspaceState(workspace)
implicitWidth: selectorWidth + (root.hovered == true ? (root.workspaceState == "focused"
? selectorFocusedExtra : 0) : 0)
Behavior on implicitWidth {
SpringAnimation {
damping: 0.3
spring: 4
}
}
states: [
State {
name: ""
when: root.workspaceState == "inactive"
PropertyChanges {
root.selectorColor: Colors.surface2
}
},
State {
name: "active"
when: root.workspaceState == "active"
PropertyChanges {
root.selectorColor: Colors.overlay2
}
},
State {
name: "focused"
when: root.workspaceState == "focused"
PropertyChanges {
root.selectorColor: Colors.lavender
}
}
]
TapHandler {
id: tapHandler
onTapped: root.workspace.activate()
}
anchors {
bottom: parent.bottom
top: parent.top
}
Rectangle {
color: root.selectorColor
radius: 6
Behavior on color {
ColorAnimation {
duration: 100
}
}
anchors {
fill: parent
}
CenteredText {
font.pointSize: 10.5
text: root.workspace.id
}
}
}

114
Bar/Network.qml Normal file
View File

@@ -0,0 +1,114 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
Container {
id: root
required property HyprlandMonitor monitor
property double networkDisplayHeight: 80
animOffset: 250
boxColor: Colors.red
boxHeight: 25
boxHeightOpened: root.networkDisplayHeight
boxWidth: 33
boxWidthOpened: 350
defaultItem: icon
exclusiveMonitor: root.monitor
exclusiveToScreen: true
forceHidden: !NetworkManager.connectedWifi
openedItem: networkDisplay
Component {
id: icon
StyledText {
horizontalAlignment: Qt.AlignCenter
propo: true
text: NetworkManager.getNetworkDetails(NetworkManager.defaultAdapter)["icon"] || "󰤭"
verticalAlignment: Qt.AlignVCenter
}
}
Component {
id: networkDisplay
Item {
id: networkDisplayRoot
property double innerMargin: 5
property double outerMargin: 6
Rectangle {
id: networkDisplayBg
anchors.fill: parent
anchors.margins: networkDisplayRoot.outerMargin
color: Colors.surface1
radius: 8
RowLayout {
anchors.fill: parent
Rectangle {
id: iconBox
property double iconBoxRadius: 5
Layout.fillHeight: true
Layout.margins: networkDisplayRoot.innerMargin
color: Colors.red
implicitWidth: root.networkDisplayHeight - (networkDisplayRoot.innerMargin * 2) - (
networkDisplayRoot.outerMargin * 2)
radius: 5
CenteredText {
fontSize: 23
propo: true
text: NetworkManager.getNetworkDetails(NetworkManager.defaultAdapter)["icon"]
}
}
Column {
id: networkColumn
property var networkInfo: NetworkManager.getNetworkDetails(NetworkManager.defaultAdapter)
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
color: Colors.text
fontSize: 14
text: networkColumn.networkInfo["networkName"]
}
StyledText {
color: Colors.subtext1
fontSize: 12
fontWeight: 5
text: networkColumn.networkInfo["ipAddress"] + " on "
+ networkColumn.networkInfo["adapterName"]
}
StyledText {
property var timeInfo: networkColumn.networkInfo["time"]
color: Colors.subtext1
fontSize: 12
fontWeight: 5
text: "Network Details " + (networkColumn.networkInfo["saved"] == true ? "Saved" : "Not Saved")
}
}
}
}
}
}
}

View File

@@ -1,21 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Notifications
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
import "./Center"
import "./Right/"
Row {
id: root
required property HyprlandMonitor monitor
Bluetooth {
exclusiveMonitor: root.monitor
}
}

View File

@@ -1,98 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Bluetooth
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
import "Bluetooth"
Container {
id: root
property bool shown: false
animOffset: 200
boxColor: Colors.peach
boxHeight: 25
boxWidth: 30
defaultItem: icon
exclusiveToScreen: true
forceHidden: !(root.shown || BluetoothManager.getConnected())
hoverableWhenHidden: true
state: ""
states: [
State {
name: ""
PropertyChanges {
root.boxHeight: 25
root.boxWidth: 30
}
StateChangeScript {
script: {
root.stack.replace(icon);
}
}
},
State {
name: "expanded"
PropertyChanges {
root.boxHeight: 250
root.boxRadius: 20
root.boxWidth: 350
}
StateChangeScript {
script: {
root.stack.replace(expanded);
root.shown = true;
}
}
}
]
hover.onHoveredChanged: {
if (hover.hovered == false) {
root.state = "";
hoverIntervalTimer.start();
} else {
root.shown = true;
hoverIntervalTimer.start();
}
}
tap.onTapped: state = "expanded"
Timer {
id: hoverIntervalTimer
interval: 500
repeat: false
running: false
onTriggered: {
if (root.hover.hovered == false) {
root.shown = false;
}
}
}
Component {
id: icon
Item {
CenteredText {
text: BluetoothManager.getConnected() == true ? "󰂱" : "󰂲"
}
}
}
Component {
id: expanded
Expanded {}
}
}

View File

@@ -1,384 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Bluetooth
import "../../../Color.js" as Colors
import "../../../Components/"
import "../../../Services/"
Item {
id: root
property double topBarHeight: 35
Rectangle {
color: Colors.surface0
radius: 17
anchors {
bottomMargin: 5
fill: parent
leftMargin: 5
rightMargin: 5
topMargin: 5
}
Rectangle {
id: topBar
property double topBarHeight: root.topBarHeight
color: Colors.peach
implicitHeight: topBarHeight
radius: 9
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
top: parent.top
topMargin: 5
}
RowLayout {
id: topBarRow
property double topBarRowPadding: 4
spacing: 3
anchors {
left: parent.left
leftMargin: topBarRowPadding
right: parent.right
rightMargin: topBarRowPadding
top: parent.top
topMargin: topBarRowPadding
}
Rectangle {
Layout.fillWidth: true
// color: Qt.lighter(Colors.peach, 1.2)
color: Colors.surface1
implicitHeight: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2)
radius: 6
StyledText {
anchors.verticalCenter: parent.verticalCenter
color: Colors.text
horizontalAlignment: Qt.AlignLeft
text: " " + BluetoothManager.defaultAdapterName
}
}
Rectangle {
id: scanButton
color: Qt.lighter(Colors.peach, 1.15)
implicitHeight: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2)
implicitWidth: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2)
radius: 6
Behavior on color {
ColorAnimation {
duration: 100
}
}
states: [
State {
name: "hovered"
PropertyChanges {
scanButton.color: Colors.green
}
}
]
border {
color: Colors.surface0
width: 2
}
HoverHandler {
onHoveredChanged: hovered == true ? scanButton.state = "hovered" : scanButton.state = ""
}
TapHandler {
onTapped: {
BluetoothManager.toggleDiscover();
}
}
CenteredText {
id: scanButtonText
text: ""
NumberAnimation {
id: animateRotation
duration: 1000
from: 0
loops: Animation.Infinite
properties: "rotation"
running: BluetoothManager.defaultAdapter.discovering
target: scanButtonText
to: 360
onStopped: {
scanButton.rotation = 0;
}
}
}
}
}
}
ListView {
boundsBehavior: Flickable.StopAtBounds
clip: true
model: BluetoothManager.getDevicesList()
spacing: 5
delegate: Item {
id: deviceRoot
required property real index
required property BluetoothDevice modelData
height: 55
width: ListView.view.width
Rectangle {
id: containerBox
anchors.fill: parent
clip: true
color: Colors.peach
radius: 7
Rectangle {
anchors.centerIn: parent
color: Colors.green
implicitHeight: parent.height - 10
implicitWidth: parent.width * (deviceRoot.modelData.batteryAvailable == true
? deviceRoot.modelData.battery : 0) - 10
opacity: 0.7
radius: 7
visible: deviceRoot.modelData.batteryAvailable == true
}
RowLayout {
anchors {
bottomMargin: 4
fill: parent
leftMargin: 4
rightMargin: 4
topMargin: 4
}
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: height
color: deviceRoot.modelData.connected ? Colors.green : Qt.lighter(Colors.peach, 1.25)
radius: 5
CenteredText {
color: Colors.surface0
font.pointSize: 30
text: BluetoothManager.getIcon(deviceRoot.modelData.icon)
}
}
ColumnLayout {
Layout.fillWidth: true
Rectangle {
Layout.fillHeight: true
Layout.fillWidth: true
color: Qt.lighter(Colors.peach, 1.15)
radius: 4
StyledText {
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
elide: Qt.ElideRight
text: deviceRoot.modelData.name
}
}
Rectangle {
Layout.fillHeight: true
Layout.fillWidth: true
color: "transparent"
StyledText {
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
color: Colors.surface2
font.weight: 300
text: deviceRoot.modelData.address
}
}
}
Item {
Layout.fillHeight: true
Layout.preferredWidth: height
RowLayout {
anchors.fill: parent
Rectangle {
id: itemEntry
Layout.fillHeight: true
Layout.fillWidth: true
color: Colors.red
radius: 3
Loader {
id: deleteButtonLoader
anchors.fill: parent
sourceComponent: forgetButton
states: [
State {
name: ""
when: deviceRoot.modelData.connected == false
PropertyChanges {
deleteButtonLoader.sourceComponent: forgetButton
}
},
State {
name: "connected"
when: deviceRoot.modelData.connected == true
PropertyChanges {
deleteButtonLoader.sourceComponent: disconnectButton
}
}
]
}
Component {
id: disconnectButton
Item {
anchors.fill: parent
TapHandler {
onTapped: deviceRoot.modelData.disconnect()
}
CenteredText {
text: ""
}
}
}
Component {
id: forgetButton
Item {
anchors.fill: parent
TapHandler {
onTapped: deviceRoot.modelData.forget()
}
CenteredText {
text: "󰩹"
}
}
}
}
Rectangle {
id: greenButton
property Component loadedItem: connectButton
Layout.fillHeight: true
Layout.fillWidth: true
color: Colors.green
radius: 3
states: [
State {
name: ""
when: deviceRoot.modelData.paired == true
PropertyChanges {
greenButton.color: Colors.green
greenButton.loadedItem: connectButton
}
},
State {
name: "unpaired"
when: deviceRoot.modelData.paired == false
PropertyChanges {
greenButton.color: Colors.blue
greenButton.loadedItem: pairButton
}
}
]
Loader {
anchors.fill: parent
sourceComponent: greenButton.loadedItem
}
Component {
id: connectButton
Item {
anchors.fill: parent
CenteredText {
text: "󱘖"
}
TapHandler {
onTapped: deviceRoot.modelData.connect()
}
}
}
Component {
id: pairButton
Item {
anchors.fill: parent
CenteredText {
text: ""
}
TapHandler {
onTapped: {
deviceRoot.modelData.pair();
}
}
}
}
}
}
}
}
}
}
anchors {
bottomMargin: 5
fill: parent
leftMargin: 5
rightMargin: 5
topMargin: 10 + root.topBarHeight
}
}
}
}

437
Bar/Time.qml Normal file
View File

@@ -0,0 +1,437 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Notifications
import Quickshell.Widgets
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
Container {
id: root
property Notification latestNotif
property bool logout: false
property double logoutHeight: 100
property double logoutMargin: 7
property double logoutSpacing: 5
required property HyprlandMonitor monitor
property bool mpris: false
property double mprisHeight: 60
property double mprisWidth: 350
property double notificationHeight: 100
property double notificationWidth: 350
property bool notified: false
boxColor: Colors.mauve
defaultItem: time
exclusiveMonitor: root.monitor
exclusiveToScreen: true
states: [
State {
name: "notified"
when: root.notified == true
PropertyChanges {
root.boxHeight: root.notificationHeight
root.boxRadius: 12
root.boxWidth: root.notificationWidth
root.visibleTopMargin: 5
}
StateChangeScript {
script: {
root.stack.replace(notification);
notificationTimer.restart();
}
}
},
State {
name: "mpris"
when: root.mpris == true
PropertyChanges {
root.boxHeight: root.mprisHeight
root.boxRadius: 12
root.boxWidth: root.mprisWidth
root.visibleTopMargin: root.hovered == true ? 0 : 5
}
StateChangeScript {
script: {
root.stack.replace(mprisToast);
mprisTimer.restart();
}
}
},
State {
name: "logout"
when: root.logout == true
PropertyChanges {
root.boxHeight: root.logoutHeight
root.boxRadius: 13
root.boxWidth: ((root.logoutHeight - (root.logoutMargin * 2)) * 5) + (root.logoutMargin * 2) + (
root.logoutSpacing * 4)
root.visibleTopMargin: 10
}
StateChangeScript {
script: {
root.stack.replace(logoutMenu);
}
}
}
]
onHoveredChanged: {
if (root.hovered == true && MprisManager.getPlaying() == true) {
root.overriden = true;
root.mpris = true;
} else {
root.overriden = false;
root.mpris = true;
}
}
Connections {
function onCloseLogoutMenu() {
root.overriden = false;
root.logout = false;
}
function onLogoutMenu() {
root.overriden = true;
root.logout = true;
}
target: IpcManager
}
Connections {
function onNewNotification() {
notification => {
if (notification.lastGeneration == false) {
root.latestNotif = notification;
root.overriden = true;
root.notified = true;
}
};
}
target: NotificationManager
}
Connections {
function onTrackChanged() {
root.overriden = true;
root.mpris = true;
console.log("track changed");
}
target: MprisManager.defaultPlayer
}
Timer {
id: notificationTimer
interval: 2000
repeat: false
running: false
onTriggered: {
root.notified = false;
root.overriden = false;
}
}
Timer {
id: mprisTimer
interval: 3000
repeat: false
running: false
onTriggered: {
root.mpris = false;
root.overriden = false;
}
}
Component {
id: logoutMenu
Item {
id: logoutMenuRoot
property var logoutModel: [
{
color: Colors.red,
text: "s",
key: Qt.Key_S
},
{
color: Colors.peach,
text: "e",
key: Qt.Key_E
},
{
color: Colors.yellow,
text: "r",
key: Qt.Key_R
},
{
color: Colors.green,
text: "l",
key: Qt.Key_L
},
{
color: Colors.sky,
text: "u",
key: Qt.Key_U
},
]
Component.onCompleted: {
forceActiveFocus();
}
Keys.onPressed: event => {
if (event.key == Qt.Key_Escape) {
IpcManager.closeLogoutMenu();
} else {
for (let button of logoutMenuRoot.logoutModel) {
if (button.key == event.key) {
event.accepted = true;
}
}
}
}
RowLayout {
spacing: root.logoutSpacing
anchors {
fill: parent
margins: root.logoutMargin
}
Repeater {
delegate: logoutButton
model: logoutMenuRoot.logoutModel
}
Component {
id: logoutButton
Item {
id: logoutButtonRoot
// Key yen isn't on my keyboard, acts as an errorless analog for no bind
required property var modelData
Layout.fillHeight: true
Layout.fillWidth: true
Rectangle {
anchors.fill: parent
color: logoutButtonRoot.modelData.color
radius: 7
StyledText {
anchors.fill: parent
horizontalAlignment: Qt.AlignHCenter
text: logoutButtonRoot.modelData.text
verticalAlignment: Qt.AlignVCenter
}
}
}
}
}
}
}
Component {
id: time
StyledText {
property string component: "time"
horizontalAlignment: Qt.AlignCenter
text: TimeManager.time
verticalAlignment: Qt.AlignVCenter
}
}
Component {
id: notification
Item {
id: notificationRoot
property double innerMargin: 5
property double outerMargin: 6
function getInnerHeight() {
let fullHeight = root.notificationHeight;
let fullMargin = notificationRoot.innerMargin + notificationRoot.outerMargin;
return fullHeight - (fullMargin * 2);
}
Rectangle {
color: Colors.surface1
radius: 9
anchors {
fill: parent
margins: notificationRoot.outerMargin
}
RowLayout {
Rectangle {
Layout.margins: notificationRoot.innerMargin
color: Colors.mauve
implicitHeight: notificationRoot.getInnerHeight()
implicitWidth: notificationRoot.getInnerHeight()
radius: 5
IconImage {
anchors.fill: parent
anchors.margins: 10
source: root.latestNotif.image
}
}
ColumnLayout {
property double textMargin: 4
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.leftMargin: 0
Layout.margins: notificationRoot.innerMargin + textMargin
StyledText {
color: Colors.text
fontSize: 14
text: root.latestNotif.summary
}
StyledText {
Layout.fillHeight: true
Layout.maximumWidth: root.notificationWidth - ((notificationRoot.outerMargin * 2) + (
notificationRoot.innerMargin * 3) + notificationRoot.getInnerHeight())
color: Colors.subtext0
elide: Qt.ElideRight
fontWeight: 5
maximumLineCount: 2
text: root.latestNotif.body
wrapMode: Text.WordWrap
}
}
}
}
}
}
Component {
id: mprisToast
Item {
id: mprisToastRoot
property double innerMargin: 4
property double outerMargin: 4
property var trackInfo: MprisManager.getTrackInfo()
function getInnerHeight() {
let fullHeight = root.mprisHeight;
let fullMargin = mprisToastRoot.innerMargin + mprisToastRoot.outerMargin;
return fullHeight - (fullMargin * 2);
}
Rectangle {
color: Colors.surface1
radius: 9
anchors {
fill: parent
margins: mprisToastRoot.outerMargin
}
RowLayout {
Rectangle {
Layout.margins: mprisToastRoot.innerMargin
color: Colors.mauve
implicitHeight: mprisToastRoot.getInnerHeight()
implicitWidth: mprisToastRoot.getInnerHeight()
radius: 5
IconImage {
anchors.fill: parent
source: mprisToastRoot.trackInfo["albumArt"]
}
}
ColumnLayout {
property double textMargin: 1
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.leftMargin: 0
Layout.margins: mprisToastRoot.innerMargin + textMargin
StyledText {
Layout.maximumWidth: root.mprisWidth - ((mprisToastRoot.outerMargin * 2) + (
mprisToastRoot.innerMargin * 5) + mprisToastRoot.getInnerHeight())
color: Colors.text
fontSize: 14
text: mprisToastRoot.trackInfo["name"] + " - " + mprisToastRoot.trackInfo["artist"]
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: root.mprisWidth - ((mprisToastRoot.outerMargin * 2) + (
mprisToastRoot.innerMargin * 5) + mprisToastRoot.getInnerHeight())
Layout.preferredWidth: root.mprisWidth - ((mprisToastRoot.outerMargin * 2) + (
mprisToastRoot.innerMargin * 5) + mprisToastRoot.getInnerHeight())
Rectangle {
anchors.fill: parent
color: Colors.overlay0
radius: 100
Rectangle {
color: Colors.mauve
implicitWidth: parent.width * mprisToastRoot.trackInfo["lengthPercent"]
radius: 100
anchors {
bottom: parent.bottom
left: parent.left
top: parent.top
}
}
StyledText {
text: MprisManager.getTimeString()
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
}

96
Bar/WorkspaceSelector.qml Normal file
View File

@@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import "../Color.js" as Colors
import "../Components/"
import "../Services/"
Container {
id: root
property double barHeight: 25
required property HyprlandMonitor monitor
property double rowLeftMargin: 5
property double workspaceButtonSpacing: 4
property double workspaceCount: WorkspaceManager.getNumberOfWorkspaces(monitor)
property double workspaceWidth: 17
boxHeight: barHeight
boxRadius: 8
boxWidth: (2 * rowLeftMargin) + (workspaceCount * workspaceWidth) + ((workspaceCount - 1)
* workspaceButtonSpacing)
defaultItem: placeholder
exclusiveMonitor: monitor
Component {
id: placeholder
Item {}
}
RowLayout {
spacing: root.workspaceButtonSpacing
anchors {
bottom: parent.bottom
left: parent.left
leftMargin: root.rowLeftMargin
top: parent.top
}
Repeater {
delegate: workspaceButton
delegateModelAccess: DelegateModel.ReadOnly
model: WorkspaceManager.getWorkspacesForMonitor(root.exclusiveMonitor)
}
Component {
id: workspaceButton
Item {
id: buttonRoot
required property var modelData
property double stateInt: WorkspaceManager.getWorkspaceStateIndex(modelData)
property variant workspaceColors: [Colors.lavender, Colors.overlay2, Colors.surface2]
property string workspaceId: modelData.id
Layout.alignment: Qt.AlignLeft
implicitHeight: root.barHeight - 7
implicitWidth: root.workspaceWidth
Behavior on implicitHeight {
SpringAnimation {
damping: 0.3
spring: 4
}
}
Behavior on implicitWidth {
SpringAnimation {
damping: 0.3
spring: 4
}
}
TapHandler {
id: tapHandler
onTapped: WorkspaceManager.activateWorkspaceById(buttonRoot.modelData.id)
}
Rectangle {
anchors.fill: parent
color: buttonRoot.workspaceColors[buttonRoot.stateInt]
radius: 4
CenteredText {
text: buttonRoot.modelData.id
}
}
}
}
}
}

View File

@@ -9,11 +9,17 @@ import "../Color.js" as Colors
Item {
id: root
property bool _fullyHidden: false
property bool _widthCollapsed: false
property double animOffset: 0
property color boxColor: Colors.surface0
property double boxHeight: 28
property double boxHeightOpened: 150
property bool boxOpened: false
property double boxRadius: 9
property double boxRadiusOpened: 12
property double boxWidth: 100
property double boxWidthOpened: 200
readonly property double calculatedTopMargin: {
if (!root.exclusiveToScreen) {
return visibleTopMargin;
@@ -35,35 +41,125 @@ Item {
property bool forceHidden: false
property alias hover: hoverHandler
property bool hoverableWhenHidden: false
property bool hovered: false
property Component openedItem
property bool overriden: false
property alias rect: container
property alias stack: containerContent
property alias tap: tapHandler
property double visibleTopMargin: 0
implicitHeight: boxHeight
implicitWidth: boxWidth
implicitWidth: root._widthCollapsed ? 0 : root.boxWidth
visible: !root._fullyHidden
Behavior on anchors.topMargin {
animation: defaultCurve
}
Behavior on boxHeight {
SpringAnimation {
damping: 0.3
spring: 4
}
}
Behavior on boxRadius {
SpringAnimation {
damping: 0.3
spring: 4
}
}
Behavior on boxWidth {
Behavior on implicitHeight {
SpringAnimation {
damping: 0.3
spring: 4
}
}
Behavior on implicitWidth {
SpringAnimation {
damping: 0.3
spring: 4
}
}
states: [
State {
name: "closed"
when: root.hovered == false && root.boxOpened == false && root.overriden == false
PropertyChanges {
container.radius: root.boxRadius
root.implicitHeight: root.boxHeight
root.implicitWidth: root.boxWidth
}
StateChangeScript {
script: {
stack.replace(root.defaultItem);
}
}
},
State {
name: "hovered"
when: root.hovered == true && root.boxOpened == false && root.overriden == false
PropertyChanges {
container.radius: root.boxRadius + 3
root.implicitHeight: root.boxHeight + 2
root.implicitWidth: root.boxWidth + 2
}
StateChangeScript {
script: {
stack.replace(root.defaultItem);
}
}
},
State {
name: "opened"
when: root.boxOpened == true && root.hovered == true && root.overriden == false
PropertyChanges {
container.radius: root.boxRadiusOpened
root.implicitHeight: root.boxHeightOpened
root.implicitWidth: root.boxWidthOpened
}
StateChangeScript {
script: {
stack.replace(root.openedItem);
}
}
}
]
onForceHiddenChanged: {
if (root.forceHidden == true) {
hideTimer.restart();
} else {
hideTimer.stop();
subHideTimer.stop();
root._fullyHidden = false;
root._widthCollapsed = false;
}
}
Timer {
id: hideTimer
interval: 250
repeat: false
running: false
onTriggered: {
root._widthCollapsed = true;
subHideTimer.restart();
}
}
Timer {
id: subHideTimer
interval: 200
repeat: false
running: false
onTriggered: {
root._fullyHidden = true;
}
}
anchors {
top: parent.top
@@ -79,20 +175,28 @@ Item {
HoverHandler {
id: hoverHandler
onHoveredChanged: {
if (hoverHandler.hovered == true) {
root.hovered = true;
} else {
root.hovered = false;
root.boxOpened = false;
if (root.state == "closed") {
root.stack.replace(root.defaultItem);
}
}
}
}
TapHandler {
id: tapHandler
}
RectangularShadow {
anchors.fill: container
blur: 30
color: Colors.mantle
offset.x: 7
offset.y: 3
radius: container.radius
spread: 10
onTapped: {
if (root.openedItem) {
root.boxOpened = true;
}
}
}
Rectangle {
@@ -103,6 +207,12 @@ Item {
color: root.boxColor
radius: root.boxRadius
Behavior on radius {
NumberAnimation {
duration: 300
}
}
StackView {
id: containerContent

View File

@@ -1,23 +1,75 @@
import QtQuick
import QtQuick.Shapes
Gradient {
Item {
id: root
required property color color
property double midpoint: 0.6
property real angle: 0 // Degrees; 0 = transparent left, colour right
readonly property real angleRadians: angle * Math.PI / 180.0
property real blur: 0.0 // 0 = sharp, 1 = very soft
// Public API
property color color: "#4da3ffff"
// Half the diagonal ensures the gradient covers the entire item.
readonly property real gradientRadius: Math.sqrt(width * width + height * height) / 2.0
property real midpoint: 0.5 // 0..1
property double radius: 10
readonly property real safeBlur: Math.max(blur, 0.00001)
readonly property real safeMidpoint: clamp01(midpoint)
readonly property real transitionEnd: clamp01(safeMidpoint + safeBlur / 2.0)
readonly property real transitionStart: clamp01(safeMidpoint - safeBlur / 2.0)
readonly property color transparentColor: Qt.rgba(color.r, color.g, color.b, 0.0)
function clamp01(value) {
return Math.max(0.0, Math.min(1.0, value));
}
Behavior on color {
ColorAnimation {
duration: 100
}
}
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: -1
fillGradient: LinearGradient {
x1: root.width / 2.0 - root.gradientRadius * Math.cos(root.angleRadians)
x2: root.width / 2.0 + root.gradientRadius * Math.cos(root.angleRadians)
y1: root.height / 2.0 - root.gradientRadius * Math.sin(root.angleRadians)
y2: root.height / 2.0 + root.gradientRadius * Math.sin(root.angleRadians)
GradientStop {
color: "transparent"
color: root.transparentColor
position: 0.0
}
GradientStop {
color: "transparent"
position: root.midpoint
color: root.transparentColor
position: root.transitionStart
}
GradientStop {
color: root.color
position: root.transitionEnd
}
GradientStop {
color: root.color
position: 1.0
}
}
PathRectangle {
height: root.height
radius: Math.min(root.radius, Math.min(width, height) / 2.0)
width: root.width
}
}
}
}

View File

@@ -2,12 +2,18 @@ import QtQuick
import "../Color.js" as Colors
Text {
id: root
property double fontSize: 12
property double fontWeight: 8
property bool propo: false
color: Colors.base
elide: Text.ElideRight
font {
family: "FiraMono Nerd Font"
pointSize: 12
weight: 600
family: root.propo == true ? "FiraMono Nerd Font Propo" : "FiraMono Nerd Font"
pointSize: fontSize
weight: root.fontWeight * 100
}
}

169
README.md Normal file
View File

@@ -0,0 +1,169 @@
# VoidBar
_Minimal, Verbose, Clean_
## Ethos
This bar is a retort to all of the "dynamic island" style bars that have been popular
recently. I'm sure you know the ones, with a cramped, forced "minimalistic" aesthetic,
that, even though marketed toward window manager users such as myself, usually end up
being mouse driven spring animation slop.
Nobody wants that.
If you use something like Caelestia or Noctalia, sure, whatever. I'm not going to be some
elitist and say they aren't without their uses. If you want a cohesive shell with theme
switching and everything working perfectly out of the box, then they're good enough
solutions for new users, or even experienced ones that just claim they don't want to waste
their time writing from scratch.
However, everything outside of those shells is bullshit. Lately, all the shells I've seen
have been "dynamic islands", bars that only show the time when dormant and have a shitload
of features that you're never going to use effectively. They have no concept of workspace
identifiers, instead opting for nondescript nerd icon blobs rather than the tried and true
numbers that seem to always be more useful. Not that it matters, they're hidden anyway,
until you change workspaces and they get shown for a second or two after the point that
you actually needed to see them.
Then come the theme switchers, a sorry excuse for a feature that just ends up being a
party trick at the end of the day. Changing wallpapers every so often I can get behind,
but the whole theme? Really? The prophecy is borderline self fulfilling, you're going to
pick a favorite eventually and end up never touching the switcher again, no matter how
many variations of gruvbox you have configured. On top of that, are you really going to be
switching wallpapers at runtime that often? I for one never even considered implementing a
feature so distracting, opting instead to actually getting shit done, to the behest of
everyone on r/unixporn.
The final gripe I have with these so called "minimal bars" is, ironically, the
over-abundance of features. No, you don't need an android-style pull down menu to adjust
your brightness, no, you don't need a button to change your screen scaling on the fly, and
no, you don't need to be able to authenticate with your fucking desktop shell. Just use
sudo like the rest of us. Many such features are jammed into bars like these, it's as if
they need some standout feature in order to actually get used (oh wait). Bars shouldn't
need a whole custom settings app. Bars shouldn't need tutorials for what keybinds to use
to access some obscure feature. Bars shouldn't implement entire bluetooth management
interfaces. Bars should show the time, what workspace you're on, and maybe, at a stretch,
how much battery you have left. However, none of the geniuses over in the quickshell
discussions tab can figure out how to put numbers where the workspace blobs are.
So, I had no choice to fix the problem.
### The problem
I like minimalistic apps, and minimalism in general. But there comes a point where being
"minimal" either wraps around to being over the top and useless, or practically unusable.
Theoretically, the most minimal bar is no bar at all, but that suffers from not existing
by definition, so I did my best to come up with something that looks good, has utility,
and, most of the time, stays out of your way when you don't care about what it says.
The first thing I did was to expand from a magical single pill to the more classical
layout, a left, center and right section, ripped straight from waybar. If you only have a
left or right section, then your screen looks lopsided, and if you only have a center
section, then your bar either looks weirdly wide, or becomes just another useless capsule.
As I have ordained, the left section is solely for the workspace switcher, the center is
for the clock, and the right is for optional information, like bluetooth connection and
battery level, which you wouldn't need on a desktop machine.
The second innovation I came up with (fr fr no cap) was removing useless information from
the bar entirely. When wifi is disconnected, what good is it to have a "no wifi" icon
taking up space on the screen? None at all, in fact. Instead, I have opted to hide unused
modules entirely. For example, if no bluetooth devices are connected, instead of leaving a
useless logo in the top right, the module slides up off of the screen, telling you that
there is nothing connected simply through its absence. Plus, it looks better. That means
that on a system with no battery, no wifi connection (ethernet hides the module), and no
bluetooth devices connected, like my main pc most of the time, there are no needless icons
cluttering the top right of the screen.
The third and final main feature that I implemented is monitor-exclusive modules. If you
have multiple monitors, you most likely won't be looking at the second if you're doing
work on the first. If that's the case, then why would you need a ticking clock on your
second monitor? Why would you need an entire shell over there taking up space and being a
potential distraction (yes I know I'm being melodramatic but stfu)? This was one of my
main gripes with waybar, the fact that the same bar with the same information was
plastered across all of your screens, regardless of whether the information was needed or
not. A simple check can be done within an instance of the bar in quickshell, as to whether
that instance of the bar is on the currently focused monitor. If it is? Display the
modules. If not? Hide them away to keep the desktop clean. It's as simple as that.
No repeated information, no unnecesarry "disconnected" icons, and, most importantly, no
useless features. Speaking of...
## Features
### Tl;Dr
- _Workspace switcher:_ Displaying only the workspaces on that monitor. When on a
different monitor, highlights the focused workspace as to distinguish it from unfocused
workspaces
- _Clock:_ It's a clock. When a notification comes in, the clock is substituted for a
notification popup. If music is playing (any playerctld compatible player), when the
track changes a toast will appear, and when hovered over, the clock will be replaced
with a music information screen.
- _Network:_ Displays whether the machine is connected to wifi or not. Ethernet is assumed
a permanent feature, and the module is thus hidden even though a network is connected.
When clicked, shows the network name, local ip address, interface that is being used to
connect, and whether the network has saved login information.
- _Bluetooth:_ Displays whether there are any connected bluetooth devices. Does NOT
function as a full bluetooth interface because I have bluetui for that. When clicked,
displays connected devices, appropriate icons, and their mac addresses. The box
containing the icon doubles as a battery indicator, and when the icon is hovered over it
is replaced with a numerical percentage referring to the device's battery level
- _Battery:_ Displays battery level, and state through coloring and icons. When unplugged,
battery icon is reactive to every 10%. When plugged in but not recieving charge, the
module is highlighted orange. when plugged in and recieving charge, the module is
highlighted purple (the best color). When plugged in and full, the module is highlighted
blue. Icons also change to reflect the battery state, using the `nf-md-battery` class of
icons, as labeled on the nerd font website. When clicked on, The module shows the
battery's UPower reference (eg `BAT0`), reported charge in Wh, change rate in watts for
both charging and discharging, the time remaining until the battery is either fully
charged or empty, and a numerical percentage alongside another vertical battery meter
(same component as the bluetooth devices)
- Planned features (when I get around to it)
- Logout menu (wlogout is still broken lol)
- Notification menu/management
- Lock screen
- Volume control? (probably not, way-edges clears)
- Interactible mpris player (skip and rewind)
- Music indicator on the left of the clock when music is playing
- Hover to show mpris thing when music is paused
- Mpris firefox exclusion (I had to disable it in about:config lol)
### More rambling
If you read the above section, you will notice there is no mention of a theme switcher or
launcher functionality. That is because I made a bar, not a theme switcher or a launcher.
Other, more knowledgeable people have made those things already, and they are almost
definately more feature rich and polished than anything you or I could make in qml. Once
again, this bar has NO BULLSHIT!!! NONE!!!
One thing I found cool while making this bar is all the interesting information that most
other bars will hide from you. Instead of what my cpu temperature is, I can see how fast
my battery is charging down to 8 decimal places of precision. On top of this, because qml
is just fancy javascript under the hood, you can do surprisingly complex data manipulation
if needs be. Of course, you can pull out an old trusty `toFixed(2)` so that there aren't
so many decimals, but beyond that, for example, the mpris api gives you track info in
miliseconds, which means you have to make a utility function to convert from that format
into a readable one, and then manipulate those values again, because they don't have
leading zeros by default.
Because of how qml works, I worried less about how inefficient my polling code was, and
how things were going to react with eachother, and update order, and instead focused on
how I wanted the bar to act. I'll tell you a little secret, qml is a really really easy
language to learn. That's how it's supposed to be, and why there's a quick in the name.
Qml is to desktop apps what tailwind is to html, almost. You can throw together something
that looks cohesive in a few minutes, and because of how state management works, adding
reactivity like a hover animation is stupidly easy. Everything is predictable, and because
you don't have to worry about how to update components, you can just throw in a few js
functions here and there regardless of whether you need them. I have to tell you, having a
bunch of heights and margins and then using a bunch of maths in a function to dynamically
calculate the height of a window or a textbox is the coolest shit ever.
I will actually use this bar on my main machine. There might be some weird shit going on
with the battery module, because the only machine I had while making this was a laptop,
but once I get home everything will be resolved. This was not made to post on reddit. This
wasn't made on a whim. I hate the state of places like r/unixporn, where, for every decent
and original rice, there are 300 copycats or people just running a prefab shell with a
custom wallpaper. And don't even get me going on the damn islands again. If I can make
something like this in a few days, then so can you. Go make something, and thanks for
reading <3

162
Services/BatteryManager.qml Normal file
View File

@@ -0,0 +1,162 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import "../Color.js" as Colors
Singleton {
id: root
property double batteryPercentage: root.mainBattery.percentage || 0
property bool batteryPresent: root.mainBattery.ready || false
property UPowerDevice mainBattery: UPower.displayDevice || undefined
property bool hasLaptopBattery: root.mainBattery && root.mainBattery.isLaptopBattery
function getBatteryColor() {
let criticalLevel = 20;
let batteryPercentage = getBatteryPercentage(mainBattery);
if (mainBattery.state == UPowerDeviceState.FullyCharged) {
return Colors.sky;
} else if (mainBattery.state == UPowerDeviceState.Charging) {
return Colors.mauve;
} else if (mainBattery.state == UPowerDeviceState.PendingCharge | mainBattery.state
== UPowerDeviceState.PendingDischarge) {
return Colors.peach;
} else if (batteryPercentage <= criticalLevel) {
return Colors.red;
} else {
return Colors.green;
}
}
function getBatteryIcon(battery) {
if (battery.state == UPowerDeviceState.Charging) {
return "󰂄";
} else if (battery.state == UPowerDeviceState.PendingCharge | battery.state
== UPowerDeviceState.PendingDischarge) {
return "󰚥";
} else if (battery.state == UPowerDeviceState.Discharging | battery.state
== UPowerDeviceState.FullyCharged) {
let iconArr = ["󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"];
let batteryCharge = battery.percentage;
let chargeIndex = Math.ceil(batteryCharge * iconArr.length) - 1;
return iconArr[chargeIndex];
}
}
function getBatteryName(battery) {
let pathsplit = battery.nativePath.split("/");
let batteryName = pathsplit[pathsplit.length];
return pathsplit;
}
function getBatteryPercentage(battery) {
return battery.percentage * 100;
}
function getBatteryText() {
let icon = getBatteryIcon(mainBattery);
let percentage = getBatteryPercentage(mainBattery);
return icon + "" + percentage + "%";
}
function getChargingRateFormat() {
let rate = mainBattery.changeRate.toFixed(1);
let finalString = "harging at " + rate + "W";
if (rate == 0) {
finalString = "Battery Idle";
} else if (rate <= 0 || mainBattery.state == UPowerDeviceState.Charging) {
finalString = "C" + finalString;
} else {
finalString = "Disc" + finalString;
}
return finalString;
}
function getMainBatteryInfo() {
let battery = UPower.devices.values[0];
let batteryState = battery.state;
let batteryName = getBatteryName(battery);
let time = getTimeFormat();
return {
"name": batteryName,
"charge": battery.percentage,
"energy": battery.energy.toFixed(1),
"capacity": battery.energyCapacity.toFixed(1),
"health": battery.healthPercentage
};
}
function getTimeFormat() {
let state = mainBattery.state;
let finalString = "";
if (state == UPowerDeviceState.FullyCharged) {
finalString += "Battery Full";
} else if (state == UPowerDeviceState.Charging) {
let time = mainBattery.timeToFull;
if (time == 0) {
return "Getting Time...";
} else {
let timeArr = secondsToTime(time);
// If more than 1 hour
if (timeArr.hours >= 1 || timeArr.hourNeedsIncrement == 1) {
finalString += timeArr.Hours + " Hours ";
// If minutes not 0
if (timeArr.minutesRounded > 1) {
finalString += timeArr.minutesRounded + " Minutes";
}
} else {
finalString += timeArr.minutes + " Minutes ";
finalString += timeArr.seconds + " Seconds";
}
}
finalString += " Until Full";
} else if (state == UPowerDeviceState.Discharging) {
let time = mainBattery.timeToEmpty;
if (time == 0) {
return "Getting Time...";
} else {
let timeArr = secondsToTime(time);
// If more than 1 hour
if (timeArr.hours >= 1 || timeArr.hourNeedsIncrement == 1) {
finalString += timeArr.hours + " Hours ";
// If minutes not 0
if (timeArr.minutesRounded > 1) {
finalString += timeArr.minutesRounded + " Minutes";
}
} else {
finalString += timeArr.minutes + " Minutes ";
finalString += timeArr.seconds + " Seconds";
}
}
finalString += " Remaining";
} else {
finalString += "Getting Time...";
}
return finalString;
}
function secondsToTime(totalSeconds) {
totalSeconds = Math.max(0, Number(totalSeconds));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
const roundedMinutes = minutes + (seconds >= 30 ? 1 : 0);
const hourNeedsIncrement = roundedMinutes === 60;
return {
hours,
minutes,
seconds,
minutesRounded: hourNeedsIncrement ? 0 : roundedMinutes,
hourNeedsIncrement
};
}
}

View File

@@ -6,21 +6,59 @@ import Quickshell.Bluetooth
Singleton {
id: root
property BluetoothAdapter defaultAdapter: Bluetooth.defaultAdapter
property string defaultAdapterName: Bluetooth.defaultAdapter.adapterId
property BluetoothAdapter defaultAdapter: Bluetooth.defaultAdapter || null
property string defaultAdapterName: defaultAdapter ? defaultAdapter.adapterId : ""
property bool anyConnected: root.defaultAdapter != null
&& root.defaultAdapter.devices.values.some(device => device.connected)
function getConnected() {
let deviceList = getDevicesList();
for (let device in deviceList) {
let currentDevice = deviceList[device];
if (currentDevice.connected == true) {
return true;
return root.anyConnected;
}
function getConnectedDevicesList() {
let devicesList = getDevicesList();
if (!devicesList) {
return [];
}
let connectedList = [];
for (let device of devicesList) {
if (device.connected == true) {
connectedList.push(device);
}
}
return false;
return connectedList;
}
function getDeviceText(device) {
if (!device) {
return undefined;
}
let deviceName = device.deviceName;
let deviceIcon = getIcon(device.icon);
let deviceMac = device.address;
let hasBattery = device.batteryAvailable;
let deviceBattery = deviceIcon;
let deviceBatteryRaw = 0;
if (hasBattery) {
deviceBattery = device.battery * 100;
deviceBatteryRaw = device.battery;
}
return {
"icon": deviceIcon,
"name": deviceName,
"mac": deviceMac,
"hasBattery": hasBattery,
"battery": deviceBattery,
"batteryRaw": deviceBatteryRaw
};
}
function getDevicesList() {
if (!root.defaultAdapter) {
return [];
}
return root.defaultAdapter.devices.values;
}
@@ -41,29 +79,4 @@ Singleton {
return icons[name] || "󰾰";
}
function pairTrustConnect(device) {
device.pair();
device.trusted = true;
device.connect();
}
function toggleDiscover() {
if (defaultAdapter.discovering == true) {
defaultAdapter.discovering = false;
} else {
defaultAdapter.discovering = true;
discoveringTimeout.running = true;
}
}
Timer {
id: discoveringTimeout
interval: 15000
repeat: false
running: false
onTriggered: defaultAdapter.discovering = false
}
}

31
Services/IpcManager.qml Normal file
View File

@@ -0,0 +1,31 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Singleton {
id: root
property var focusState: WlrKeyboardFocus.None
property bool logoutMenuOpen: false
signal closeLogoutMenu
signal logoutMenu
onCloseLogoutMenu: {
root.focusState = WlrKeyboardFocus.None;
root.logoutMenuOpen = false;
}
IpcHandler {
function openLogoutMenu(): void {
root.logoutMenu();
root.focusState = WlrKeyboardFocus.Exclusive;
root.logoutMenuOpen = true;
}
target: "root"
}
}

View File

@@ -6,8 +6,7 @@ import Quickshell.Services.Mpris
Singleton {
id: root
property MprisPlayer defaultPlayer: Mpris.players.values[0]
property real pos: defaultPlayer.position / defaultPlayer.length
property MprisPlayer defaultPlayer: Mpris.players.values[0] || null
function getPlaying() {
if (!defaultPlayer) {
@@ -17,6 +16,55 @@ Singleton {
}
}
function getTimeString() {
let trackInfo = getTrackInfo();
let position = trackInfo["position"];
let length = trackInfo["length"];
if (length.hours > 0) {
return position.hours + ":" + position.minutes + ":" + position.seconds + "/" + length.hours
+ ":" + length.minutes + ":" + length.seconds;
} else {
return position.minutes + ":" + position.seconds + "/" + length.minutes + ":" + length.seconds;
}
}
function getTrackInfo() {
let trackName = root.defaultPlayer.trackTitle;
let trackArtist = root.defaultPlayer.trackArtist;
let trackAlbum = root.defaultPlayer.trackAlbum;
let trackPosition = root.defaultPlayer.position;
let trackLength = root.defaultPlayer.length;
let trackArt = root.defaultPlayer.trackArtUrl;
return {
"name": trackName,
"artist": trackArtist,
"album": trackAlbum,
"position": msToArray(trackPosition),
"length": msToArray(trackLength),
"lengthRemaining": msToArray(trackLength - trackPosition),
"lengthPercent": trackPosition / trackLength,
"albumArt": trackArt
};
}
function msToArray(time) {
time = Math.max(0, Number(time));
const inthours = Math.floor(time / 3600000);
const intminutes = Math.floor((time % 60000) / 60);
const intseconds = Math.floor((time % 1000) % 60);
const hours = String(inthours).padStart(2, "0");
const minutes = String(intminutes).padStart(2, "0");
const seconds = String(intseconds).padStart(2, "0");
return {
hours,
minutes,
seconds
};
}
function prev() {
defaultPlayer.previous();
}

122
Services/NetworkManager.qml Normal file
View File

@@ -0,0 +1,122 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Networking
Singleton {
id: root
property bool connectedWifi: root.defaultAdapter != null && root.defaultAdapter.connected
&& root.defaultAdapter.type == DeviceType.Wifi
property NetworkDevice defaultAdapter: Networking.devices.values[0] || null
property string ipAddress: "0.0.0.0"
function getConnectedNetworks(networksArr) {
let connectedNetworks = [];
for (let network of networksArr) {
if (network.state == ConnectionState.Connected) {
connectedNetworks.push(network);
}
}
return connectedNetworks;
}
function getNetworkDetails(adapter) {
if (!adapter) {
return {
"adapterName": "",
"adapterConnected": false,
"adapterIsWifi": false,
"networkName": "",
"networkStrength": 0,
"icon": "󰤭"
};
}
let networksArr = adapter.networks.values;
let connectedNetworks = getConnectedNetworks(networksArr);
let deviceWifi = isDeviceWifi(adapter);
let primaryNetwork = connectedNetworks[0];
if (!primaryNetwork) {
return {
"adapterName": adapter.name,
"adapterConnected": adapter.connected,
"adapterIsWifi": deviceWifi,
"networkName": "No Network",
"networkStrength": 0,
"icon": "󰤭"
};
}
let networkName = primaryNetwork.name;
let networkStrength = primaryNetwork.signalStrength;
let networkIcon = getWifiIcon(networkStrength);
let outputDict = {
"adapterName": adapter.name,
"adapterConnected": adapter.connected,
"adapterIsWifi": deviceWifi,
"networkName": networkName,
"networkStrength": networkStrength,
"icon": networkIcon,
"saved": primaryNetwork.known,
"ipAddress": root.ipAddress
};
return outputDict;
}
function getWifiIcon(strength) {
let iconArr = ["󰤟", "󰤢", "󰤥", "󰤨"];
let strengthIndex = Math.ceil(strength * iconArr.length);
return iconArr[strengthIndex - 1];
}
function getWifiText() {
let info = getNetworkDetails(defaultAdapter);
let adapter = info["adapterName"];
let name = info["networkName"];
let finalString = adapter + " - " + name;
return finalString;
}
function isConnectedAndWifi() {
return root.connectedWifi;
}
function isDeviceWifi(adapter) {
if (adapter.type == DeviceType.Wifi) {
return true;
} else if (adapter.type == DeviceType.Wired) {
return false;
} else {
console.log(adapter.type);
return false;
}
}
function refreshIpAddress() {
ipProcess.running = true;
}
onDefaultAdapterChanged: {
refreshIpAddress();
}
Process {
id: ipProcess
command: ["sh", "-c", "ip -4 -o addr show dev " + root.defaultAdapter.name
+ " scope global | awk '{sub(\"/.*\", \"\", $4); printf $4}'"]
running: true
stdout: StdioCollector {
onStreamFinished: root.ipAddress = this.text
}
}
}

View File

@@ -2,12 +2,15 @@ pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "../Color.js" as Colors
Singleton {
id: root
property alias notif: server
signal newNotification(notificiation: Notification)
function getLatestNotification() {
let notificationList = server.trackedNotifications.values;
let len = notificationList.length;
@@ -24,9 +27,11 @@ Singleton {
actionsSupported: true
bodyMarkupSupported: true
bodySupported: true
imageSupported: true
onNotification: notification => {
notification.tracked = true;
root.newNotification(notification);
}
}
}

View File

@@ -62,6 +62,17 @@ Singleton {
}
}
function getWorkspaceStateIndex(workspace) {
let state = getWorkspaceState(workspace);
if (state == "focused") {
return 0;
} else if (state == "active") {
return 1;
} else if (state == "inactive") {
return 2;
}
}
function getWorkspacesForMonitor(monitor) {
let allWorkspaces = getAllNumberedWorkspaces();
let monitorWorkspaces = [];

View File

@@ -1,6 +1,9 @@
module Services
singleton Time 1.0 Time.qml
singleton TimeManager 1.0 TimeManager.qml
singleton NotificationManager 1.0 NotificationManager.qml
singleton WorkspaceManager 1.0 WorkspaceManager.qml
singleton BluetoothManager 1.0 BluetoothManager.qml
singleton MprisManager 1.0 MprisManager.qml
singleton BatteryManager 1.0 BatteryManager.qml
singleton NetworkManager 1.0 NetworkManager.qml
singleton IpcManager 1.0 IpcManager.qml