colour -> color, behaviour -> behavior, centre -> center, favourite -> favorite, and about twenty other pairs, applied consistently across comments, docs, error/UI copy, and a handful of QML identifiers that used the British spelling as their actual name: SystemSettings' serialiseValue/serialiseTable/normaliseGradient, Displays' normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's _normalise helper, and ShortcutCapture's cancelled signal (with its onCancelled handler in ShortcutsPage.qml). Every call site and the two tests that assert on the literal source text (settings-ownership and settings-backup-live contracts) were updated in lockstep. Left untouched: config/dot/espanso/match/packages/misspell-en/ is a vendored third-party autocorrect dictionary -- its entries are typo corrections, not our prose, and rewriting them would fight the package's own purpose (and any future re-sync from upstream). The already-American `favorites` property (Home page pinned accessories) was never actually misspelled -- only nearby comments and error strings said "favourites" -- so no data migration was needed there. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
159 lines
5.4 KiB
QML
159 lines
5.4 KiB
QML
// Brightness, from whichever source this machine actually has.
|
|
//
|
|
// Two exist and they are not interchangeable:
|
|
//
|
|
// The kernel backlight class, driven by brightnessctl. Laptop panels have it;
|
|
// this desktop does not -- brightnessctl reports only keyboard and NIC LEDs.
|
|
//
|
|
// DDC/CI, the channel the buttons on a monitor's bezel drive. That is the
|
|
// only brightness an external display has, and it is per-monitor.
|
|
//
|
|
// A machine may have neither, either, or both, so this renders a row per source
|
|
// found and removes itself entirely when there are none, rather than sitting
|
|
// there as a dead control.
|
|
//
|
|
// Connector labels appear only when there is more than one row. A single
|
|
// slider needs no explanation of which screen it dims.
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.widgets
|
|
import qs.config
|
|
import qs.services
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property bool backlightAvailable: false
|
|
property real backlightValue: 0
|
|
|
|
readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length
|
|
readonly property bool labeled: root.rowCount > 1
|
|
|
|
visible: root.rowCount > 0
|
|
implicitHeight: rows.implicitHeight
|
|
|
|
// Probing I2C takes on the order of a second, so it waits until the panel
|
|
// is actually on screen rather than running at shell startup. Monitors do
|
|
// not come and go, so once is enough.
|
|
onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
|
|
Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
|
|
|
|
// `-m` is the machine-readable form: name,class,current,percent,max
|
|
Process {
|
|
running: true
|
|
command: ["brightnessctl", "-m", "-c", "backlight", "-l"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.parseDevices(this.text)
|
|
}
|
|
}
|
|
|
|
function parseDevices(text: string): void {
|
|
for (const line of text.trim().split("\n")) {
|
|
const fields = line.split(",");
|
|
if (fields.length < 5 || fields[1] !== "backlight")
|
|
continue;
|
|
root.backlightAvailable = true;
|
|
root.backlightValue = parseInt(fields[3]) / 100;
|
|
return;
|
|
}
|
|
}
|
|
|
|
function applyBacklight(v: real): void {
|
|
root.backlightValue = v;
|
|
// Never go fully dark: a 0% backlight looks like a broken shell.
|
|
Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]);
|
|
}
|
|
|
|
Column {
|
|
id: rows
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
spacing: 4
|
|
|
|
BrightnessRow {
|
|
width: rows.width
|
|
visible: root.backlightAvailable
|
|
label: "Built-in"
|
|
value: root.backlightValue
|
|
onMoved: v => root.applyBacklight(v)
|
|
}
|
|
|
|
Repeater {
|
|
model: Brightness.displays
|
|
|
|
BrightnessRow {
|
|
required property var modelData
|
|
width: rows.width
|
|
// Hyprland already knows what each output is called, so the
|
|
// name comes from there rather than from a second source that
|
|
// could disagree with the Displays page. The connector is the
|
|
// fallback, so a display is never an unlabeled slider.
|
|
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
|
value: modelData.value / 100
|
|
onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
|
|
}
|
|
}
|
|
}
|
|
|
|
component BrightnessRow: Item {
|
|
id: row
|
|
|
|
property string label: ""
|
|
property real value: 0
|
|
signal moved(real value)
|
|
|
|
implicitHeight: caption.height + control.height
|
|
|
|
Text {
|
|
id: caption
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: 6
|
|
anchors.rightMargin: 6
|
|
anchors.top: parent.top
|
|
visible: root.labeled
|
|
height: visible ? implicitHeight + 2 : 0
|
|
text: row.label
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// The slider and its glyph share one strip so the two stay aligned
|
|
// whether or not a caption sits above them. Anchoring the glyph to
|
|
// both a caption and a center line instead would conflict, and an
|
|
// anchor set to undefined is not released.
|
|
Item {
|
|
id: control
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: caption.bottom
|
|
height: 32
|
|
|
|
// ValueSlider draws its own leading icon, but symbolic icons need
|
|
// recoloring to be visible — see ThemedIcon.
|
|
ThemedIcon {
|
|
id: glyph
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
size: 17
|
|
icon: "display-brightness-symbolic"
|
|
}
|
|
|
|
ValueSlider {
|
|
anchors.left: glyph.right
|
|
anchors.leftMargin: 12
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 32
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
value: row.value
|
|
onMoved: v => row.moved(v)
|
|
}
|
|
}
|
|
}
|
|
}
|