Files
dotfiles/fedora/quickshell/control-center/shell.qml
2026-07-26 16:51:25 +03:00

337 lines
11 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Networking
import Quickshell.Services.Pipewire
import Quickshell.Services.UPower
import Quickshell.Wayland
Scope {
id: root
property bool open: false
property bool panelVisible: false
property bool contentReady: false
property bool mediaOpen: false
property bool notificationsOpen: false
property bool osdOpen: false
property string osdKind: "volume"
property int pageIndex: 0
property int brightness: 0
property var pendingNetwork: null
property string wifiPassword: ""
property var wifiDevice: Networking.devices.values.find(device => device.type === DeviceType.Wifi)
property var bluetoothAdapter: Bluetooth.defaultAdapter
property var audioSink: Pipewire.defaultAudioSink
property var settings: statusSettings
property alias cpuTemperature: statusBar.temperature
property alias gpuTemperature: statusBar.gpuTemperature
Settings { id: statusSettings }
UiTokens { id: uiTheme }
Bar {
id: statusBar
controlCenter: root
settings: root.settings
mediaPopup: mediaPopup
toggleMedia: function() { root.mediaOpen = !root.mediaOpen }
notificationCenter: notificationCenter
}
MediaPopup {
id: mediaPopup
open: root.mediaOpen
onDismissed: root.mediaOpen = false
}
Notifications {
id: notificationCenter
settings: root.settings
open: root.notificationsOpen
onDismissed: root.notificationsOpen = false
}
Osd {
open: root.osdOpen
kind: root.osdKind
value: root.osdKind === "brightness"
? root.brightness / 100
: (root.audioSink && root.audioSink.audio ? root.audioSink.audio.volume : 0)
muted: root.audioSink && root.audioSink.audio ? root.audioSink.audio.muted : false
}
function setBrightness(value) {
const level = Math.max(0, Math.min(100, Math.round(value)));
Quickshell.execDetached(["ddcutil", "setvcp", "10", String(level)]);
root.brightness = level;
}
function connectWifi(network) {
if (network.security === WifiSecurityType.Open) {
network.connect();
return;
}
root.pendingNetwork = network;
root.wifiPassword = "";
}
function submitWifiPassword() {
if (root.pendingNetwork && root.wifiPassword.length > 0) {
root.pendingNetwork.connectWithPsk(root.wifiPassword);
root.pendingNetwork = null;
root.wifiPassword = "";
}
}
function showOsd(kind) {
root.osdKind = kind;
root.osdOpen = true;
osdTimer.restart();
}
function adjustBrightness(delta) {
const current = root.brightness > 0 ? root.brightness : 50;
root.setBrightness(current + delta);
root.showOsd("brightness");
}
function temperatureColor(value) {
const match = String(value).match(/(\d+)C\s*$/);
const temperature = match ? Number(match[1]) : -1;
if (temperature < 0) return uiTheme.mutedText;
if (temperature >= root.settings.temperatureCritical) return uiTheme.critical;
if (temperature >= root.settings.temperatureWarning) return uiTheme.warning;
return uiTheme.success;
}
function openPanel() {
closePanelTimer.stop();
root.panelVisible = true;
root.contentReady = false;
openPanelTimer.restart();
}
function closePanel() {
openPanelTimer.stop();
contentReadyTimer.stop();
root.contentReady = false;
root.open = false;
closePanelTimer.restart();
}
function togglePanel() {
if (root.open || root.panelVisible) root.closePanel();
else root.openPanel();
}
Timer {
id: openPanelTimer
interval: 1
repeat: false
onTriggered: {
root.open = true;
contentReadyTimer.restart();
}
}
Timer {
id: contentReadyTimer
interval: 220
repeat: false
onTriggered: root.contentReady = true
}
Timer {
id: closePanelTimer
interval: 240
repeat: false
onTriggered: root.panelVisible = false
}
IpcHandler {
target: "controlcenter"
function toggle() { root.togglePanel(); }
function open() { root.openPanel(); }
function close() { root.closePanel(); }
}
IpcHandler {
target: "osd"
function volume_up() { Quickshell.execDetached(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%+"]); root.showOsd("volume"); }
function volume_down() { Quickshell.execDetached(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%-"]); root.showOsd("volume"); }
function volume_mute() { Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"]); root.showOsd("volume"); }
function mic_mute() { Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", "toggle"]); root.showOsd("mic"); }
function brightness_up() { root.adjustBrightness(5); }
function brightness_down() { root.adjustBrightness(-5); }
}
Timer { id: osdTimer; interval: 1400; repeat: false; onTriggered: root.osdOpen = false }
PwObjectTracker { objects: [root.audioSink] }
Process {
id: brightnessReader
command: ["sh", "-c", "ddcutil getvcp 10 2>/dev/null | sed -n 's/.*current value = *\\([0-9]*\\).*/\\1/p'"]
stdout: StdioCollector {
onStreamFinished: {
const value = parseInt(text.trim());
if (!isNaN(value)) root.brightness = value;
}
}
}
Timer {
id: brightnessPoll
interval: 3000
repeat: true
running: root.open
onTriggered: if (!brightnessReader.running) brightnessReader.running = true
}
PanelWindow {
visible: root.panelVisible
color: "transparent"
anchors { top: true; bottom: true; left: true; right: true }
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: root.panelVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-control-center"
MouseArea {
anchors.fill: parent
onClicked: root.closePanel()
}
Rectangle {
id: card
width: Math.min(root.settings.panelWidth, parent.width - 24)
height: parent.height
anchors.top: parent.top
x: root.open ? parent.width - width - 16 : parent.width + 24
radius: root.settings.panelRadius
color: Qt.rgba(uiTheme.background.r, uiTheme.background.g, uiTheme.background.b, root.settings.panelOpacity)
border.color: uiTheme.border
border.width: 1
z: 1
Behavior on x {
NumberAnimation { duration: 220; easing.type: Easing.OutCubic }
}
MouseArea { anchors.fill: parent; onClicked: mouse.accepted = true }
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
NavigationRail {
id: navigation
theme: uiTheme
currentIndex: root.pageIndex
Layout.fillHeight: true
onSelected: root.pageIndex = index
}
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: 1
color: uiTheme.border
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Label {
text: navigation.names[root.pageIndex]
color: uiTheme.text
font.pixelSize: 22
font.bold: true
}
Label {
text: "Control center / Niri"
color: uiTheme.mutedText
font.pixelSize: 11
}
}
CCButton { theme: uiTheme; text: "Close"; onClicked: root.closePanel() }
}
ScrollView {
id: pageScroll
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
contentWidth: availableWidth
Loader {
id: pageLoader
width: pageScroll.availableWidth
active: root.contentReady
sourceComponent: [quickPage, barPage, panelPage, notificationsPage, statusPage][root.pageIndex]
opacity: root.contentReady ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 140 } }
}
}
}
}
}
}
PanelWindow {
visible: root.settings.panelOpenOnCorner && !root.panelVisible
implicitWidth: 30
implicitHeight: 30
anchors { top: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-control-center-hot-corner"
Rectangle {
anchors.fill: parent
anchors.margins: 4
radius: 8
color: uiTheme.primary
opacity: 0.35
MouseArea { anchors.fill: parent; onClicked: root.openPanel() }
}
}
Component {
id: quickPage
QuickPage { width: pageLoader.width; controlCenter: root; settings: root.settings; theme: uiTheme }
}
Component {
id: barPage
BarPage { width: pageLoader.width; settings: root.settings; theme: uiTheme; availableModules: root.settings.defaultBarModules }
}
Component {
id: panelPage
PanelPage { width: pageLoader.width; settings: root.settings; theme: uiTheme }
}
Component {
id: notificationsPage
NotificationsPage { width: pageLoader.width; settings: root.settings; theme: uiTheme; notificationCenter: notificationCenter }
}
Component {
id: statusPage
StatusPage {
width: pageLoader.width
settings: root.settings
theme: uiTheme
cpuTemperature: root.cpuTemperature
gpuTemperature: root.gpuTemperature
temperatureColorCpu: root.temperatureColor(root.cpuTemperature)
temperatureColorGpu: root.temperatureColor(root.gpuTemperature)
}
}
}