Compare commits

...

7 commits

Author SHA1 Message Date
e45f412930
feat!: support exponential brightness curve
I don't need most of the fluff for handling other types of displays, and getting rid of it lets me do something a lot nicer: add an exponential-gamma brightness display.
2026-01-28 13:59:09 -05:00
bc5d256073
fix: more strictly close bar popouts 2026-01-25 04:44:39 -05:00
2bdcf4d504
tweak(dashboard): adjust weather panel spacing 2026-01-25 04:44:16 -05:00
730434c481
fix(dashboard): prevent hidden windows from being active 2026-01-25 04:43:12 -05:00
2f8b877c8e
fix: fix window panel transition 2026-01-25 04:41:22 -05:00
dba7ff6c97
fix: add proper app icon fallback 2026-01-25 04:41:00 -05:00
984a840a8d
refactor: make battery indicator more consistent 2026-01-25 04:39:31 -05:00
20 changed files with 63 additions and 146 deletions

BIN
assets/icon-missing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

4
assets/icon-missing.svg Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" height="512px" viewBox="0 -960 960 960" width="512px" fill="#787c99">
<path d="M560-360q17 0 29.5-12.5T602-402q0-17-12.5-29.5T560-444q-17 0-29.5 12.5T518-402q0 17 12.5 29.5T560-360Zm-30-128h60q0-29 6-42.5t28-35.5q30-30 40-48.5t10-43.5q0-45-31.5-73.5T560-760q-41 0-71.5 23T446-676l54 22q9-25 24.5-37.5T560-704q24 0 39 13.5t15 36.5q0 14-8 26.5T578-596q-33 29-40.5 45.5T530-488ZM320-240q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

View file

@ -161,6 +161,8 @@ Singleton {
} }
readonly property QtObject services: QtObject { readonly property QtObject services: QtObject {
readonly property real batteryWarning: 0.15
readonly property string weatherLocation: "" readonly property string weatherLocation: ""
readonly property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) readonly property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem)
@ -169,6 +171,8 @@ Singleton {
readonly property string sunsetFrom: "21:00" readonly property string sunsetFrom: "21:00"
readonly property string sunsetTo: "9:00" readonly property string sunsetTo: "9:00"
readonly property int sunsetTemperature: 4500 readonly property int sunsetTemperature: 4500
readonly property real brightnessExp: 4.0
} }
readonly property QtObject session: QtObject { readonly property QtObject session: QtObject {

View file

@ -95,7 +95,7 @@ Container {
readonly property bool hasBattery: UPower.displayDevice.isLaptopBattery readonly property bool hasBattery: UPower.displayDevice.isLaptopBattery
readonly property real percentage: UPower.displayDevice.percentage readonly property real percentage: UPower.displayDevice.percentage
readonly property bool charging: !UPower.onBattery && batteryText.text !== "100" readonly property bool charging: !UPower.onBattery && batteryText.text !== "100"
readonly property bool warning: UPower.onBattery && percentage < 0.15 readonly property bool warning: UPower.onBattery && percentage < Config.services.batteryWarning + 0.01
text: { text: {
if (!hasBattery) { if (!hasBattery) {
@ -142,7 +142,7 @@ Container {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 0.5 anchors.verticalCenterOffset: 0.5
text: Math.round(battery.percentage * 100) text: Math.floor(battery.percentage * 100)
color: battery.warning ? Config.colors.batteryWarning : Config.colors.bg color: battery.warning ? Config.colors.batteryWarning : Config.colors.bg
font.family: Config.font.family.mono font.family: Config.font.family.mono
font.pointSize: 6 font.pointSize: 6

View file

@ -73,6 +73,13 @@ Item {
property: "scale" property: "scale"
to: 1 to: 1
} }
// Fixes really weird transition bug
NumberAnimation {
targets: header
property: "height"
to: Config.font.size.normal
duration: 20
}
} }
// Reveal on window title change // Reveal on window title change
@ -111,6 +118,7 @@ Item {
anchors.right: infobox.right anchors.right: infobox.right
anchors.top: infobox.top anchors.top: infobox.top
anchors.margins: 12 anchors.margins: 12
anchors.topMargin: 11
} }
} }

View file

@ -13,14 +13,15 @@ ColumnLayout {
spacing: 4 spacing: 4
readonly property color color: UPower.onBattery && UPower.displayDevice.percentage < 0.15 ? readonly property bool hasBattery: UPower.displayDevice.isLaptopBattery
Config.colors.batteryWarning : readonly property real percentage: UPower.displayDevice.percentage
Config.colors.battery readonly property bool warning: UPower.onBattery && percentage < Config.services.batteryWarning + 0.01
readonly property color color: warning ? Config.colors.batteryWarning : Config.colors.battery
Loader { Loader {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
active: UPower.displayDevice.isLaptopBattery active: root.hasBattery
asynchronous: true asynchronous: true
height: active ? (item?.implicitHeight ?? 0) : 0 height: active ? (item?.implicitHeight ?? 0) : 0
@ -76,7 +77,7 @@ ColumnLayout {
radiusX: (meter.size + meter.thickness) / 2 + meter.padding radiusX: (meter.size + meter.thickness) / 2 + meter.padding
radiusY: radiusX radiusY: radiusX
startAngle: -90 - meter.angle / 2 startAngle: -90 - meter.angle / 2
sweepAngle: meter.angle * UPower.displayDevice.percentage sweepAngle: meter.angle * root.percentage
} }
Behavior on strokeColor { Behavior on strokeColor {
@ -98,7 +99,7 @@ ColumnLayout {
CustomText { CustomText {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: Math.round(UPower.displayDevice.percentage * 100) + "%" text: Math.floor(root.percentage * 100) + "%"
font.pointSize: Config.font.size.largest font.pointSize: Config.font.size.largest
} }

View file

@ -67,7 +67,7 @@ Item {
name: "battery" name: "battery"
source: "Battery.qml" source: "Battery.qml"
color: UPower.displayDevice.isLaptopBattery && color: UPower.displayDevice.isLaptopBattery &&
UPower.onBattery && UPower.displayDevice.percentage < 0.15 ? UPower.onBattery && UPower.displayDevice.percentage < Config.services.batteryWarning + 0.01 ?
Config.colors.batteryWarning : Config.colors.batteryWarning :
Config.colors.battery Config.colors.battery
} }

View file

@ -13,7 +13,6 @@ Item {
id: root id: root
required property PersistentProperties uiState required property PersistentProperties uiState
required property ShellScreen screen
readonly property real nonAnimWidth: content.implicitWidth readonly property real nonAnimWidth: content.implicitWidth
readonly property real nonAnimHeight: y > 0 || hasCurrent ? content.implicitHeight : 0 readonly property real nonAnimHeight: y > 0 || hasCurrent ? content.implicitHeight : 0

View file

@ -110,8 +110,8 @@ Item {
source: { source: {
const icon = entry.modelData.properties["application.icon-name"]; const icon = entry.modelData.properties["application.icon-name"];
if (icon) if (icon)
return Icons.getAppIcon(icon, "image-missing"); return Icons.getAppIcon(icon, "icon-missing");
Icons.getAppIcon(entry.modelData.name, "image-missing") Icons.getAppIcon(entry.modelData.name, "icon-missing")
} }
} }

View file

@ -290,6 +290,7 @@ Item {
property var ipc: modelData.lastIpcObject property var ipc: modelData.lastIpcObject
opacity: ipc && ipc.at && !ipc.hidden ? 1 : 0 opacity: ipc && ipc.at && !ipc.hidden ? 1 : 0
visible: opacity > 0
property real nonAnimX: ipc?.at ? (ipc.at[0] - preview.monX) * preview.sizeRatio : 0 property real nonAnimX: ipc?.at ? (ipc.at[0] - preview.monX) * preview.sizeRatio : 0
property real nonAnimY: ipc?.at ? (ipc.at[1] - preview.monY) * preview.sizeRatio : 0 property real nonAnimY: ipc?.at ? (ipc.at[1] - preview.monY) * preview.sizeRatio : 0

View file

@ -83,7 +83,6 @@ Item {
anchors.left: parent.right anchors.left: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.bottomMargin: 1
animate: true animate: true
text: Weather.feelsLike text: Weather.feelsLike

View file

@ -37,7 +37,7 @@ Item {
IconImage { IconImage {
id: icon id: icon
source: Quickshell.iconPath(root.modelData?.icon, "image-missing") source: Quickshell.iconPath(root.modelData?.icon, "icon-missing")
implicitSize: parent.height * 0.9 implicitSize: parent.height * 0.9
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter

View file

@ -11,7 +11,6 @@ Item {
id: root id: root
required property var uiState required property var uiState
required property Brightness.Monitor monitor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
@ -95,13 +94,10 @@ Item {
implicitHeight: Config.osd.sliderLength implicitHeight: Config.osd.sliderLength
function onWheel(event: WheelEvent) { function onWheel(event: WheelEvent) {
const monitor = root.monitor;
if (!monitor)
return;
if (event.angleDelta.y > 0) if (event.angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + 0.1); Brightness.setBrightness(Brightness.brightness + 0.1);
else if (event.angleDelta.y < 0) else if (event.angleDelta.y < 0)
monitor.setBrightness(monitor.brightness - 0.1); Brightness.setBrightness(Brightness.brightness - 0.1);
} }
CustomFilledSlider { CustomFilledSlider {
@ -109,8 +105,8 @@ Item {
color: Config.colors.brightness color: Config.colors.brightness
icon: Icons.getBrightnessIcon(value) icon: Icons.getBrightnessIcon(value)
value: root.monitor?.brightness ?? 0 value: Brightness.brightness
onMoved: root.monitor?.setBrightness(value) onMoved: Brightness.setBrightness(value)
} }
} }
} }

View file

@ -7,10 +7,8 @@ Scope {
id: root id: root
required property PersistentProperties uiState required property PersistentProperties uiState
required property ShellScreen screen
required property bool hovered required property bool hovered
required property bool suppressed required property bool suppressed
readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(screen)
function show(): void { function show(): void {
if (!root.suppressed) { if (!root.suppressed) {
@ -34,7 +32,7 @@ Scope {
} }
Connections { Connections {
target: root.monitor target: Brightness
function onBrightnessChanged(): void { function onBrightnessChanged(): void {
if (root.uiState.osdBrightnessReact) if (root.uiState.osdBrightnessReact)

View file

@ -8,7 +8,6 @@ Item {
id: root id: root
required property var uiState required property var uiState
required property ShellScreen screen
visible: width > 0 visible: width > 0
implicitWidth: 0 implicitWidth: 0
@ -61,6 +60,5 @@ Item {
id: content id: content
uiState: root.uiState uiState: root.uiState
monitor: Brightness.getMonitorForScreen(root.screen)
} }
} }

View file

@ -62,6 +62,10 @@ CustomMouseArea {
if (y < Config.bar.height && !popoutsSuppressed && !popouts.persistent) { if (y < Config.bar.height && !popoutsSuppressed && !popouts.persistent) {
bar.checkPopout(x); bar.checkPopout(x);
} }
// Hide bar popouts (if user moves mouse along edge)
if (y > popouts.nonAnimHeight + Config.bar.height) {
popouts.hasCurrent = false;
}
// Show osd on hover // Show osd on hover
const showOsd = inRightPanel(panels.osd, x, y); const showOsd = inRightPanel(panels.osd, x, y);
@ -116,7 +120,6 @@ CustomMouseArea {
Osd.Interactions { Osd.Interactions {
uiState: root.uiState uiState: root.uiState
screen: root.screen
hovered: root.osdHovered hovered: root.osdHovered
suppressed: root.osdSuppressed suppressed: root.osdSuppressed
} }

View file

@ -13,7 +13,6 @@ Item {
id: root id: root
required property PersistentProperties uiState required property PersistentProperties uiState
required property ShellScreen screen
required property Item bar required property Item bar
readonly property alias popouts: popouts readonly property alias popouts: popouts
@ -31,14 +30,12 @@ Item {
id: popouts id: popouts
uiState: root.uiState uiState: root.uiState
screen: root.screen
} }
Osd.Wrapper { Osd.Wrapper {
id: osd id: osd
uiState: root.uiState uiState: root.uiState
screen: root.screen
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right anchors.right: parent.right

View file

@ -110,7 +110,6 @@ Variants {
visible: !uiState.uiState.hidden visible: !uiState.uiState.hidden
uiState: uiState.uiState uiState: uiState.uiState
screen: scope.modelData
bar: bar bar: bar
} }
} }

View file

@ -55,6 +55,9 @@ in stdenv.mkDerivation {
cp -R . $out/share/${pname} cp -R . $out/share/${pname}
mkdir -p $out/share/icons/hicolor/512x512/apps/
cp assets/icon-missing.png $out/share/icons/hicolor/512x512/apps/
makeWrapper ${quickshell}/bin/qs $out/bin/${pname} \ makeWrapper ${quickshell}/bin/qs $out/bin/${pname} \
--prefix PATH : "${lib.makeBinPath runtimeDeps}" \ --prefix PATH : "${lib.makeBinPath runtimeDeps}" \
--set FONTCONFIG_FILE "${fontconfig}" \ --set FONTCONFIG_FILE "${fontconfig}" \

View file

@ -12,59 +12,39 @@ Singleton {
reloadableId: "brightness" reloadableId: "brightness"
property list<var> ddcMonitors: [] property real brightness: 1.0
readonly property list<Monitor> monitors: variants.instances property real maxBrightness: 0.0
property bool appleDisplayPresent: false
function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(m => m.modelData === screen);
}
function increaseBrightness(): void { function increaseBrightness(): void {
const focusedName = Hypr.focusedMonitor.name; setBrightness(brightness + Config.osd.brightnessIncrement);
const monitor = monitors.find(m => focusedName === m.modelData.name);
if (monitor)
monitor.setBrightness(monitor.brightness + Config.osd.brightnessIncrement);
} }
function decreaseBrightness(): void { function decreaseBrightness(): void {
const focusedName = Hypr.focusedMonitor.name; setBrightness(brightness - Config.osd.brightnessIncrement);
const monitor = monitors.find(m => focusedName === m.modelData.name);
if (monitor)
monitor.setBrightness(monitor.brightness - Config.osd.brightnessIncrement);
} }
onMonitorsChanged: { function setBrightness(value: real): void {
ddcMonitors = []; value = Math.max(0, Math.min(1, value));
ddcProc.running = true; if (Math.abs(brightness - value) < 0.01) return;
} brightness = value;
Variants { const exp = Config.services.brightnessExp;
id: variants const raw = Math.round((value ** exp) * maxBrightness);
Quickshell.execDetached(["brightnessctl", "s", `${raw}`]);
model: Quickshell.screens
Monitor {}
} }
Component.onCompleted: initProc.running = true
Process { Process {
running: true id: initProc
command: ["sh", "-c", "asdbctl get"] // To avoid warnings if asdbctl is not installed command: ["sh", "-c", "echo $(brightnessctl g) $(brightnessctl m)"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 onStreamFinished: {
const exp = Config.services.brightnessExp;
const [cur, max] = text.split(" ");
root.maxBrightness = parseInt(max);
root.brightness = (parseInt(cur) / root.maxBrightness) ** (1 / exp);
} }
} }
Process {
id: ddcProc
command: ["ddcutil", "detect", "--brief"]
stdout: StdioCollector {
onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({
model: d.match(/Monitor:.*:(.*):.*/)[1],
busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1]
}))
}
} }
CustomShortcut { CustomShortcut {
@ -110,77 +90,4 @@ Singleton {
root.decreaseBrightness(); root.decreaseBrightness();
} }
} }
component Monitor: QtObject {
id: monitor
required property ShellScreen modelData
readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model)
readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? ""
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
property real brightness
property real queuedBrightness: NaN
readonly property Process initProc: Process {
stdout: StdioCollector {
onStreamFinished: {
if (monitor.isAppleDisplay) {
const val = parseInt(text.trim());
monitor.brightness = val / 101;
} else {
const [, , , cur, max] = text.split(" ");
monitor.brightness = parseInt(cur) / parseInt(max);
}
}
}
}
readonly property Timer timer: Timer {
interval: 500
onTriggered: {
if (!isNaN(monitor.queuedBrightness)) {
monitor.setBrightness(monitor.queuedBrightness);
monitor.queuedBrightness = NaN;
}
}
}
function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value));
const rounded = Math.round(value * 100);
if (Math.round(brightness * 100) === rounded)
return;
if (isDdc && timer.running) {
queuedBrightness = value;
return;
}
brightness = value;
if (isAppleDisplay)
Quickshell.execDetached(["asdbctl", "set", rounded]);
else if (isDdc)
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]);
else
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
if (isDdc)
timer.restart();
}
function initBrightness(): void {
if (isAppleDisplay)
initProc.command = ["asdbctl", "get"];
else if (isDdc)
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
else
initProc.command = ["sh", "-c", "echo a b c $(brightnessctl g) $(brightnessctl m)"];
initProc.running = true;
}
onBusNumChanged: initBrightness()
Component.onCompleted: initBrightness()
}
} }