548 lines
25 KiB
Plaintext
548 lines
25 KiB
Plaintext
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Wayland
|
|
import Quickshell.Widgets
|
|
import Quickshell.Services.SystemTray
|
|
import Quickshell.Services.Pipewire
|
|
import Quickshell.Services.UPower
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
|
|
ShellRoot {
|
|
id: root
|
|
|
|
// ───────────── SETTINGS & THEMING ─────────────
|
|
readonly property string mono: "JetBrainsMono Nerd Font"
|
|
readonly property int barWidth: 42
|
|
readonly property int wsCount: 5
|
|
|
|
readonly property color cBar: "#0a0a0a"
|
|
readonly property color cPanel: "#0d0d0d"
|
|
readonly property color cBorder: "#333333"
|
|
readonly property color cText: "#c8c8c8"
|
|
readonly property color cDim: "#555555"
|
|
readonly property color cBright: "#ffffff"
|
|
readonly property color cAccent: "#ffffff"
|
|
readonly property color cWarn: "#e05555"
|
|
|
|
// ───────────── STATE ─────────────
|
|
property bool launcherOpen: false
|
|
property bool netUp: false
|
|
property bool btOn: false
|
|
property string activeApplet: ""
|
|
|
|
PwObjectTracker { objects: [Pipewire] }
|
|
|
|
IpcHandler {
|
|
target: "launcher"
|
|
function toggle(): void { root.launcherOpen = !root.launcherOpen }
|
|
function open(): void { root.launcherOpen = true }
|
|
function close(): void { root.launcherOpen = false }
|
|
}
|
|
|
|
SystemClock { id: clock; precision: SystemClock.Minutes }
|
|
|
|
Process {
|
|
id: statusProc
|
|
command: ["sh", "-c",
|
|
"nmcli -t -f STATE general 2>/dev/null | grep -q '^connected' && echo net=1 || echo net=0; " +
|
|
"nmcli -t -f WIRELESS radio 2>/dev/null | grep -q 'enabled' && echo bt=1 || echo bt=0"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
root.netUp = text.includes("net=1")
|
|
root.btOn = text.includes("bt=1")
|
|
}
|
|
}
|
|
}
|
|
Timer { interval: 5000; running: true; repeat: true; triggeredOnStart: true; onTriggered: statusProc.running = true }
|
|
|
|
// ───────────── VERTICAL BAR ─────────────
|
|
Variants {
|
|
model: Quickshell.screens
|
|
|
|
PanelWindow {
|
|
id: bar
|
|
required property var modelData
|
|
screen: modelData
|
|
|
|
anchors { top: true; bottom: true; left: true }
|
|
implicitWidth: root.barWidth
|
|
color: root.cBar
|
|
|
|
Rectangle {
|
|
anchors { top: parent.top; bottom: parent.bottom; right: parent.right }
|
|
width: 1
|
|
color: root.cBorder
|
|
}
|
|
|
|
// Launcher Button & Workspaces (Top)
|
|
ColumnLayout {
|
|
anchors { top: parent.top; topMargin: 12; horizontalCenter: parent.horizontalCenter }
|
|
spacing: 6
|
|
|
|
// App Launcher Icon
|
|
Item {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
implicitWidth: 32; implicitHeight: 32
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: ""
|
|
font.family: root.mono; font.pixelSize: 14
|
|
color: root.launcherOpen ? root.cBright : root.cDim
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.launcherOpen = !root.launcherOpen
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder; Layout.bottomMargin: 4 }
|
|
|
|
Repeater {
|
|
model: root.wsCount
|
|
delegate: Rectangle {
|
|
required property int index
|
|
readonly property int wsId: index + 1
|
|
readonly property bool active: Hyprland.focusedWorkspace?.id === wsId
|
|
readonly property bool occupied: Hyprland.workspaces.values.some(w => w.id === wsId)
|
|
|
|
Layout.alignment: Qt.AlignHCenter
|
|
implicitWidth: 32; implicitHeight: 32
|
|
color: "transparent"
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: parent.wsId
|
|
font.family: root.mono
|
|
font.pixelSize: 12
|
|
font.bold: parent.active
|
|
color: parent.active ? root.cBright : (parent.occupied ? root.cText : root.cDim)
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: Quickshell.execDetached(["hyprctl", "dispatch", "workspace " + parent.wsId])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clock & Applet Triggers (Bottom) - NOW WITH BLUETOOTH & NETWORK INDICATORS
|
|
ColumnLayout {
|
|
anchors { bottom: parent.bottom; bottomMargin: 12; horizontalCenter: parent.horizontalCenter }
|
|
spacing: 12
|
|
|
|
Text { Layout.alignment: Qt.AlignHCenter; text: Qt.formatDateTime(clock.date, "HH"); font.family: root.mono; font.pixelSize: 12; font.bold: true; color: root.cBright }
|
|
Text { Layout.alignment: Qt.AlignHCenter; text: Qt.formatDateTime(clock.date, "mm"); font.family: root.mono; font.pixelSize: 12; color: root.cDim; Layout.topMargin: -8 }
|
|
|
|
Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
|
|
|
// NETWORK INDICATOR (ADDED)
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: root.netUp ? "" : "" // Wi-Fi when connected, Ethernet when not (or vice versa based on preference)
|
|
font.family: root.mono; font.pixelSize: 14
|
|
color: root.netUp ? root.cAccent : root.cDim
|
|
MouseArea {
|
|
anchors.fill: parent;
|
|
anchors.margins: -8;
|
|
onClicked: root.activeApplet = root.activeApplet === "wifi" ? "" : "wifi"
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
|
|
|
// BLUETOOTH INDICATOR (ADDED)
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: root.btOn ? "" : "" // Bluetooth on/off
|
|
font.family: root.mono; font.pixelSize: 14
|
|
color: root.btOn ? root.cAccent : root.cDim
|
|
MouseArea {
|
|
anchors.fill: parent;
|
|
anchors.margins: -8;
|
|
onClicked: root.activeApplet = root.activeApplet === "bluetooth" ? "" : "bluetooth"
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.alignment: Qt.AlignHCenter; width: 16; height: 1; color: root.cBorder; Layout.topMargin: 4; Layout.bottomMargin: 4 }
|
|
|
|
Text {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: Pipewire.defaultAudioSink?.audio?.muted ? "" : ""
|
|
font.family: root.mono; font.pixelSize: 14
|
|
color: root.activeApplet === "sound" ? root.cAccent : root.cText
|
|
MouseArea { anchors.fill: parent; anchors.margins: -8; onClicked: root.activeApplet = root.activeApplet === "sound" ? "" : "sound" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ───────────── APP LAUNCHER OVERLAY ─────────────
|
|
PanelWindow {
|
|
id: launcherWindow
|
|
visible: root.launcherOpen
|
|
|
|
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0]
|
|
|
|
// Centered overlay matching the design in screenshots
|
|
implicitWidth: 320
|
|
implicitHeight: 520
|
|
color: "transparent"
|
|
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.namespace: "launcher"
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: root.cPanel
|
|
border.color: root.cBorder
|
|
border.width: 1
|
|
|
|
FocusScope {
|
|
id: launcherScope
|
|
anchors.fill: parent
|
|
focus: launcherWindow.visible
|
|
|
|
Keys.onEscapePressed: root.launcherOpen = false
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 10
|
|
|
|
// Search Input Box ("Apps...")
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
implicitHeight: 34
|
|
color: "#141414"
|
|
border.color: searchInput.activeFocus ? root.cBright : root.cBorder
|
|
border.width: 1
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: ""
|
|
font.family: root.mono
|
|
font.pixelSize: 12
|
|
color: root.cDim
|
|
}
|
|
|
|
TextInput {
|
|
id: searchInput
|
|
Layout.fillWidth: true
|
|
font.family: root.mono
|
|
font.pixelSize: 12
|
|
color: root.cBright
|
|
focus: true
|
|
selectByMouse: true
|
|
|
|
Text {
|
|
text: "Apps..."
|
|
font.family: root.mono
|
|
font.pixelSize: 12
|
|
color: root.cDim
|
|
visible: searchInput.text === ""
|
|
}
|
|
|
|
Keys.onDownPressed: appListView.incrementCurrentIndex()
|
|
Keys.onUpPressed: appListView.decrementCurrentIndex()
|
|
Keys.onReturnPressed: {
|
|
if (appListView.currentItem) {
|
|
appListView.currentItem.launch()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Application List
|
|
ListView {
|
|
id: appListView
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
spacing: 2
|
|
currentIndex: 0
|
|
|
|
model: appListProc.apps
|
|
|
|
delegate: Rectangle {
|
|
id: appItem
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: appListView.width
|
|
height: 36
|
|
color: ListView.isCurrentItem ? "#1a1a1a" : (itemArea.containsMouse ? "#141414" : "transparent")
|
|
border.color: ListView.isCurrentItem ? root.cBorder : "transparent"
|
|
|
|
function launch() {
|
|
Quickshell.execDetached(["sh", "-c", modelData.exec])
|
|
root.launcherOpen = false
|
|
searchInput.text = ""
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
spacing: 10
|
|
|
|
Text {
|
|
text: ""
|
|
font.family: root.mono
|
|
font.pixelSize: 13
|
|
color: ListView.isCurrentItem ? root.cBright : root.cDim
|
|
}
|
|
|
|
Text {
|
|
text: modelData.name
|
|
font.family: root.mono
|
|
font.pixelSize: 12
|
|
color: ListView.isCurrentItem ? root.cBright : root.cText
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: itemArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: appItem.launch()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process to fetch installed desktop applications
|
|
Process {
|
|
id: appListProc
|
|
property var rawApps: []
|
|
property var apps: {
|
|
const query = searchInput.text.toLowerCase().trim()
|
|
if (!query) return rawApps
|
|
return rawApps.filter(a => a.name.toLowerCase().includes(query))
|
|
}
|
|
|
|
command: ["python3", "-c", `
|
|
import os, glob
|
|
|
|
apps = []
|
|
dirs = ['/usr/share/applications', os.path.expanduser('~/.local/share/applications')]
|
|
for d in dirs:
|
|
for path in glob.glob(os.path.join(d, '*.desktop')):
|
|
try:
|
|
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
name, exec_cmd, nodisplay = "", "", False
|
|
for line in f:
|
|
if line.startswith('Name=') and not name:
|
|
name = line.split('=', 1)[1].strip()
|
|
elif line.startswith('Exec=') and not exec_cmd:
|
|
exec_cmd = line.split('=', 1)[1].strip().split('%')[0].strip()
|
|
elif line.startswith('NoDisplay=true'):
|
|
nodisplay = True
|
|
if name and exec_cmd and not nodisplay:
|
|
apps.append(f"{name}|||{exec_cmd}")
|
|
except:
|
|
pass
|
|
|
|
apps = sorted(list(set(apps)), key=lambda x: x.lower())
|
|
for a in apps:
|
|
print(a)
|
|
`]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const lines = text.trim().split("\n")
|
|
let parsed = []
|
|
for (let line of lines) {
|
|
const parts = line.split("|||")
|
|
if (parts.length === 2) {
|
|
parsed.push({ name: parts[0], exec: parts[1] })
|
|
}
|
|
}
|
|
appListProc.rawApps = parsed
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: appListProc.running = true
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
searchInput.text = ""
|
|
searchInput.forceActiveFocus()
|
|
}
|
|
}
|
|
}
|
|
|
|
// ───────────── APPLETS POPUP WINDOW ─────────────
|
|
PanelWindow {
|
|
id: appletWindow
|
|
visible: root.activeApplet !== ""
|
|
|
|
screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? Quickshell.screens[0]
|
|
anchors { left: true; bottom: true }
|
|
|
|
margins.left: root.barWidth + 12
|
|
margins.bottom: 12
|
|
|
|
implicitWidth: 340
|
|
implicitHeight: root.activeApplet === "sound" ? 420 : 380
|
|
color: "transparent"
|
|
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.namespace: "applet"
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: root.cPanel
|
|
border.color: root.cBorder
|
|
border.width: 1
|
|
|
|
FocusScope {
|
|
anchors.fill: parent
|
|
focus: appletWindow.visible
|
|
Keys.onEscapePressed: root.activeApplet = ""
|
|
}
|
|
|
|
// ─ NETWORK APPLET ─
|
|
ColumnLayout {
|
|
anchors.fill: parent; anchors.margins: 16
|
|
visible: root.activeApplet === "wifi"
|
|
spacing: 16
|
|
|
|
property var wifiList: []
|
|
|
|
Process {
|
|
id: wifiScanProc
|
|
command: ["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE", "dev", "wifi"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const lines = text.trim().split("\n")
|
|
let results = []
|
|
for (let l of lines) {
|
|
if (!l) continue
|
|
const parts = l.split(":")
|
|
if (parts[0] && !results.some(r => r.ssid === parts[0])) {
|
|
results.push({ ssid: parts[0], signal: parseInt(parts[1]), active: parts[3] === "*" })
|
|
}
|
|
}
|
|
results.sort((a, b) => b.active - a.active || b.signal - a.signal)
|
|
parent.wifiList = results
|
|
}
|
|
}
|
|
}
|
|
onVisibleChanged: if(visible) wifiScanProc.running = true
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
ColumnLayout {
|
|
spacing: 2
|
|
Text { text: "Network"; font.family: root.mono; font.pixelSize: 14; font.bold: true; color: root.cBright }
|
|
Text { text: "WI-FI & ETHERNET"; font.family: root.mono; font.pixelSize: 10; color: root.cDim }
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Rectangle {
|
|
width: 32; height: 18; color: root.netUp ? root.cBright : root.cBorder
|
|
Rectangle { x: root.netUp ? 15 : 1; y: 1; width: 16; height: 16; color: root.cPanel; Behavior on x { NumberAnimation { duration: 150 } } }
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder }
|
|
|
|
Text { text: "ZNANE SIECI"; font.family: root.mono; font.pixelSize: 10; color: root.cDim }
|
|
|
|
ListView {
|
|
Layout.fillWidth: true; Layout.fillHeight: true; clip: true; spacing: 8
|
|
model: parent.wifiList
|
|
delegate: Rectangle {
|
|
width: ListView.view.width; height: 32; color: "transparent"
|
|
border.color: modelData.active ? root.cBorder : "transparent"
|
|
|
|
RowLayout {
|
|
anchors.fill: parent; anchors.leftMargin: 12; anchors.rightMargin: 12; spacing: 12
|
|
Text { text: ""; font.family: root.mono; font.pixelSize: 12; color: modelData.active ? root.cAccent : root.cDim }
|
|
Text { text: modelData.ssid; font.family: root.mono; font.pixelSize: 12; color: modelData.active ? root.cBright : root.cText; Layout.fillWidth: true; elide: Text.ElideRight }
|
|
}
|
|
MouseArea { anchors.fill: parent; onClicked: { Quickshell.execDetached(["foot", "-e", "nmtui-connect", modelData.ssid]); root.activeApplet = "" } }
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─ AUDIO APPLET ─
|
|
ColumnLayout {
|
|
anchors.fill: parent; anchors.margins: 16
|
|
visible: root.activeApplet === "sound"
|
|
spacing: 12
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
ColumnLayout {
|
|
spacing: 2
|
|
Text { text: "Audio"; font.family: root.mono; font.pixelSize: 14; font.bold: true; color: root.cBright }
|
|
Text { text: "POKRYJ HAŁAS"; font.family: root.mono; font.pixelSize: 10; color: root.cDim }
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Rectangle {
|
|
width: 32; height: 18; color: !Pipewire.defaultAudioSink?.audio?.muted ? root.cBright : root.cBorder
|
|
Rectangle { x: !Pipewire.defaultAudioSink?.audio?.muted ? 15 : 1; y: 1; width: 16; height: 16; color: root.cPanel; Behavior on x { NumberAnimation { duration: 150 } } }
|
|
MouseArea { anchors.fill: parent; onClicked: if(Pipewire.defaultAudioSink?.audio) Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted }
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: root.cBorder; Layout.bottomMargin: 8 }
|
|
|
|
// Input Section
|
|
Text { text: "INPUT"; font.family: root.mono; font.pixelSize: 10; color: root.cDim }
|
|
Repeater {
|
|
model: Pipewire.nodes.values.filter(n => n.mediaClass === "Audio/Source" && !n.description.includes("Monitor"))
|
|
delegate: ColumnLayout {
|
|
Layout.fillWidth: true; spacing: 4
|
|
RowLayout {
|
|
Text { text: ""; font.family: root.mono; font.pixelSize: 12; color: root.cDim }
|
|
Text { text: modelData.description; font.family: root.mono; font.pixelSize: 11; color: root.cText; Layout.fillWidth: true; elide: Text.ElideRight }
|
|
}
|
|
Slider {
|
|
Layout.fillWidth: true; from: 0; to: 1; value: modelData.audio?.volume ?? 0
|
|
onValueChanged: if (modelData.audio && Math.abs(modelData.audio.volume - value) > 0.01) modelData.audio.volume = value
|
|
background: Rectangle { width: parent.width; height: 2; color: root.cBorder; y: parent.height/2; Rectangle { width: parent.visualPosition * parent.width; height: 2; color: root.cBright } }
|
|
handle: Rectangle { x: parent.visualPosition * (parent.width - width); y: parent.height/2 - height/2; width: 10; height: 10; color: root.cBright }
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 8 }
|
|
|
|
// Output Section
|
|
Text { text: "OUTPUT"; font.family: root.mono; font.pixelSize: 10; color: root.cDim }
|
|
Repeater {
|
|
model: Pipewire.nodes.values.filter(n => n.mediaClass === "Audio/Sink")
|
|
delegate: ColumnLayout {
|
|
Layout.fillWidth: true; spacing: 4
|
|
RowLayout {
|
|
Text { text: ""; font.family: root.mono; font.pixelSize: 12; color: root.cDim }
|
|
Text { text: modelData.description; font.family: root.mono; font.pixelSize: 11; color: root.cText; Layout.fillWidth: true; elide: Text.ElideRight }
|
|
}
|
|
Slider {
|
|
Layout.fillWidth: true; from: 0; to: 1; value: modelData.audio?.volume ?? 0
|
|
onValueChanged: if (modelData.audio && Math.abs(modelData.audio.volume - value) > 0.01) modelData.audio.volume = value
|
|
background: Rectangle { width: parent.width; height: 2; color: root.cBorder; y: parent.height/2; Rectangle { width: parent.visualPosition * parent.width; height: 2; color: root.cBright } }
|
|
handle: Rectangle { x: parent.visualPosition * (parent.width - width); y: parent.height/2 - height/2; width: 10; height: 10; color: root.cBright }
|
|
}
|
|
}
|
|
}
|
|
Item { Layout.fillHeight: true }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |