Files
dotfiles/fedora/quickshell/control-center/shell.qml
2026-07-25 12:38:39 +03:00

749 lines
31 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 mediaOpen: false
property bool notificationsOpen: false
property bool osdOpen: false
property string osdKind: "volume"
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
}
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
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 "#806d95";
if (temperature >= root.settings.temperatureCritical) return "#c94f6d";
if (temperature >= root.settings.temperatureWarning) return "#996315";
return "#4d7c38";
}
function openPanel() {
closePanelTimer.stop();
root.panelVisible = true;
openPanelTimer.restart();
}
function closePanel() {
openPanelTimer.stop();
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
}
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(470, parent.width - 32)
height: parent.height
anchors.top: parent.top
anchors.topMargin: 0
x: root.open ? parent.width - width - 16 : parent.width + 24
radius: 20
color: "#f5f2ea"
border.color: "#d8d0c2"
border.width: 1
z: 1
Behavior on x {
NumberAnimation {
duration: 220
easing.type: Easing.OutCubic
}
}
MouseArea {
anchors.fill: parent
onClicked: mouse.accepted = true
}
ColumnLayout {
id: content
anchors.fill: parent
anchors.margins: 18
spacing: 12
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Label {
text: "Control center"
color: "#252321"
font.pixelSize: 22
font.bold: true
}
Label {
text: "Niri / QuickShell"
color: "#7c756c"
font.pixelSize: 12
}
}
Button {
text: "✕"
ToolTip.visible: hovered
ToolTip.text: "Close"
onClicked: root.closePanel()
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ColumnLayout {
width: parent.width
spacing: 10
Rectangle {
Layout.fillWidth: true
implicitHeight: wifiColumn.implicitHeight + 24
radius: 14
color: "#e5f0ef"
ColumnLayout {
id: wifiColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Wi-Fi"
color: "#1c666d"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: root.wifiDevice ? (root.wifiDevice.name || "Available") : "Unavailable"
color: "#52777a"
elide: Text.ElideRight
}
Switch {
checked: Networking.wifiEnabled
enabled: Networking.wifiHardwareEnabled
onToggled: Networking.wifiEnabled = checked
}
}
RowLayout {
visible: root.wifiDevice !== null
Layout.fillWidth: true
Label {
text: root.wifiDevice && root.wifiDevice.networks.values.find(network => network.connected)
? root.wifiDevice.networks.values.find(network => network.connected).name
: "Not connected"
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Button {
text: "Scan"
onClicked: {
if (root.wifiDevice) root.wifiDevice.scannerEnabled = true;
}
}
}
Repeater {
model: root.wifiDevice ? root.wifiDevice.networks : null
delegate: RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: modelData.name
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Label {
text: modelData.connected ? "Connected" : Math.round(modelData.signalStrength) + "%"
color: modelData.connected ? "#1c666d" : "#7c756c"
}
Button {
text: modelData.connected ? "Disconnect" : "Connect"
enabled: !modelData.stateChanging
onClicked: modelData.connected ? modelData.disconnect() : root.connectWifi(modelData)
}
}
}
RowLayout {
visible: root.pendingNetwork !== null
Layout.fillWidth: true
TextField {
Layout.fillWidth: true
placeholderText: "Password for " + (root.pendingNetwork ? root.pendingNetwork.name : "network")
echoMode: TextInput.Password
text: root.wifiPassword
onTextChanged: root.wifiPassword = text
onAccepted: root.submitWifiPassword()
}
Button {
text: "Join"
enabled: root.wifiPassword.length > 0
onClicked: root.submitWifiPassword()
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: bluetoothColumn.implicitHeight + 24
radius: 14
color: "#eceafd"
ColumnLayout {
id: bluetoothColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Bluetooth"
color: "#55499c"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Switch {
checked: root.bluetoothAdapter ? root.bluetoothAdapter.enabled : false
enabled: root.bluetoothAdapter !== null
onToggled: {
if (root.bluetoothAdapter) root.bluetoothAdapter.enabled = checked;
}
}
Button {
text: root.bluetoothAdapter && root.bluetoothAdapter.discovering ? "Stop scan" : "Scan"
enabled: root.bluetoothAdapter && root.bluetoothAdapter.enabled
onClicked: {
if (root.bluetoothAdapter) root.bluetoothAdapter.discovering = !root.bluetoothAdapter.discovering;
}
}
}
Repeater {
model: root.bluetoothAdapter ? root.bluetoothAdapter.devices : null
delegate: RowLayout {
Layout.fillWidth: true
spacing: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Label {
text: modelData.name || modelData.deviceName || "Unknown device"
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Label {
text: modelData.connected ? "Connected" : (modelData.paired ? "Paired" : "Not paired")
color: "#7c756c"
font.pixelSize: 11
}
}
Label {
visible: modelData.batteryAvailable
text: Math.round(modelData.battery * 100) + "%"
color: "#7c756c"
}
Button {
text: modelData.connected ? "Disconnect" : (modelData.paired ? "Connect" : "Pair")
onClicked: {
if (modelData.connected) modelData.disconnect();
else if (modelData.paired) modelData.connect();
else modelData.pair();
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: audioColumn.implicitHeight + 24
radius: 14
color: "#fff1d7"
ColumnLayout {
id: audioColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Audio"
color: "#996315"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: root.audioSink ? (root.audioSink.description || "Default output") : "Unavailable"
color: "#806a48"
elide: Text.ElideRight
Layout.maximumWidth: 230
}
}
RowLayout {
Layout.fillWidth: true
Button {
text: root.audioSink && root.audioSink.audio && root.audioSink.audio.muted ? "Unmute" : "Mute"
enabled: root.audioSink && root.audioSink.audio
onClicked: {
if (root.audioSink && root.audioSink.audio) root.audioSink.audio.muted = !root.audioSink.audio.muted;
}
}
Slider {
Layout.fillWidth: true
from: 0
to: 1
value: root.audioSink && root.audioSink.audio ? root.audioSink.audio.volume : 0
enabled: root.audioSink && root.audioSink.audio
onMoved: {
if (root.audioSink && root.audioSink.audio) root.audioSink.audio.volume = value;
}
}
Label {
text: root.audioSink && root.audioSink.audio ? Math.round(root.audioSink.audio.volume * 100) + "%" : "--"
color: "#806a48"
}
}
Repeater {
model: Pipewire.nodes
delegate: RowLayout {
visible: modelData.isSink && !modelData.isStream
Layout.fillWidth: true
Label {
text: modelData.description || modelData.name
color: "#252321"
elide: Text.ElideRight
Layout.fillWidth: true
}
Button {
text: modelData === root.audioSink ? "Default" : "Use"
enabled: modelData !== root.audioSink
onClicked: Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: displayColumn.implicitHeight + 24
radius: 14
color: "#f8e4dc"
ColumnLayout {
id: displayColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Display brightness"
color: "#9e4d39"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: root.brightness > 0 ? root.brightness + "%" : "Unavailable"
color: "#8b665b"
}
}
Slider {
Layout.fillWidth: true
from: 1
to: 100
value: root.brightness
enabled: root.brightness > 0
onPressedChanged: {
if (!pressed) root.setBrightness(value);
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: statusColumn.implicitHeight + 24
radius: 14
color: "#eee8f7"
ColumnLayout {
id: statusColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Status bar"
color: "#684b8f"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: "Temperature"
color: "#806d95"
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "CPU temperature"
color: "#252321"
Layout.fillWidth: true
}
Label {
text: root.cpuTemperature
color: root.temperatureColor(root.cpuTemperature)
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "GPU temperature"
color: "#252321"
Layout.fillWidth: true
}
Label {
text: root.gpuTemperature
color: root.temperatureColor(root.gpuTemperature)
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Refresh interval"
color: "#252321"
Layout.fillWidth: true
}
ComboBox {
model: ["1 second", "2 seconds", "5 seconds"]
currentIndex: [1000, 2000, 5000].indexOf(root.settings.temperatureInterval)
onActivated: root.settings.temperatureInterval = [1000, 2000, 5000][currentIndex]
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Warning / critical"
color: "#252321"
Layout.fillWidth: true
}
SpinBox {
from: 40
to: 99
value: root.settings.temperatureWarning
editable: true
onValueModified: root.settings.setTemperatureWarning(value)
}
Label {
text: "/"
color: "#7c756c"
}
SpinBox {
from: 41
to: 110
value: root.settings.temperatureCritical
editable: true
onValueModified: root.settings.setTemperatureCritical(value)
}
Label {
text: "°C"
color: "#7c756c"
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: powerColumn.implicitHeight + 24
radius: 14
color: "#e8f1df"
ColumnLayout {
id: powerColumn
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
Label {
text: "Power profile"
color: "#4d7c38"
font.pixelSize: 16
font.bold: true
Layout.fillWidth: true
}
Label {
text: PowerProfile.toString(PowerProfiles.profile)
color: "#66825a"
}
}
RowLayout {
Layout.fillWidth: true
Button {
text: "Saver"
Layout.fillWidth: true
onClicked: PowerProfiles.profile = PowerProfile.PowerSaver
}
Button {
text: "Balanced"
Layout.fillWidth: true
onClicked: PowerProfiles.profile = PowerProfile.Balanced
}
Button {
text: "Performance"
enabled: PowerProfiles.hasPerformanceProfile
Layout.fillWidth: true
onClicked: PowerProfiles.profile = PowerProfile.Performance
}
}
}
}
}
}
}
}
}
}