From 9274fbfd3a27595931f6ff2df9072eb02a7e0420 Mon Sep 17 00:00:00 2001 From: fy59 Date: Sun, 4 Oct 2026 20:47:26 +0200 Subject: [PATCH] feat(appearance): unify QuickShell and Sway theme engine --- .gitignore | 2 + bin/.local/bin/generate-appearance.py | 135 ++++++++++++++++++ .../.config/quickshell/labfy-sway/Bar.qml | 3 +- .../.config/quickshell/labfy-sway/README.md | 3 + .../labfy-sway/components/NerdIcon.qml | 3 +- .../labfy-sway/controlcenter/ActionButton.qml | 5 +- .../controlcenter/BatterySettings.qml | 45 +++--- .../controlcenter/BluetoothPage.qml | 57 ++++---- .../controlcenter/BrightnessSlider.qml | 13 +- .../controlcenter/ConfirmAction.qml | 11 +- .../controlcenter/ControlCenter.qml | 21 +-- .../controlcenter/ControlCenterButton.qml | 5 +- .../labfy-sway/controlcenter/PowerProfile.qml | 9 +- .../labfy-sway/controlcenter/QuickToggle.qml | 5 +- .../controlcenter/SessionAction.qml | 5 +- .../labfy-sway/controlcenter/SessionPage.qml | 3 +- .../labfy-sway/controlcenter/VolumeSlider.qml | 13 +- .../labfy-sway/controlcenter/WifiPage.qml | 49 +++---- .../labfy-sway/datecenter/DateCenter.qml | 7 +- .../labfy-sway/datecenter/MediaPlayer.qml | 15 +- .../datecenter/NotificationItem.qml | 19 +-- .../datecenter/NotificationList.qml | 7 +- .../quickshell/labfy-sway/modules/Clock.qml | 11 +- .../labfy-sway/modules/Workspaces.qml | 7 +- .../notifications/NotificationToast.qml | 19 +-- .../notifications/UnreadIndicator.qml | 9 +- .../.config/quickshell/labfy-sway/shell.qml | 16 +++ .../labfy-sway/status/BatteryIndicator.qml | 7 +- .../labfy-sway/status/BluetoothIndicator.qml | 7 +- .../labfy-sway/status/NetworkIndicator.qml | 7 +- .../labfy-sway/status/StatusTooltip.qml | 7 +- .../quickshell/labfy-sway/status/TrayItem.qml | 5 +- .../labfy-sway/status/UpdatesIndicator.qml | 7 +- .../labfy-sway/systemmonitor/MetricRow.qml | 9 +- .../systemmonitor/SystemMonitor.qml | 19 +-- .../systemmonitor/SystemMonitorButton.qml | 5 +- .../labfy-sway/theme/AppearanceController.qml | 60 ++++++++ .../quickshell/labfy-sway/theme/README.md | 68 +++++++++ .../quickshell/labfy-sway/theme/Theme.qml | 96 +++++++++++++ .../labfy-sway/theme/catppuccin.json | 114 +++++++++++++++ .../quickshell/labfy-sway/theme/qmldir | 2 + .../labfy-sway/windows/WindowButton.qml | 15 +- .../labfy-sway/windows/WindowMenu.qml | 19 +-- sway/.config/sway/README.md | 6 + sway/.config/sway/autostart | 2 +- sway/.config/sway/catppuccin-mocha | 26 ---- sway/.config/sway/style | 7 +- sway/.config/sway/swayfx | 4 +- sway/.config/sway/theme-default.conf | 12 ++ tests/test_generate_appearance.py | 46 ++++++ 50 files changed, 809 insertions(+), 238 deletions(-) create mode 100755 bin/.local/bin/generate-appearance.py create mode 100644 quickshell/.config/quickshell/labfy-sway/theme/AppearanceController.qml create mode 100644 quickshell/.config/quickshell/labfy-sway/theme/README.md create mode 100644 quickshell/.config/quickshell/labfy-sway/theme/Theme.qml create mode 100644 quickshell/.config/quickshell/labfy-sway/theme/catppuccin.json create mode 100644 quickshell/.config/quickshell/labfy-sway/theme/qmldir delete mode 100644 sway/.config/sway/catppuccin-mocha create mode 100644 sway/.config/sway/theme-default.conf create mode 100644 tests/test_generate_appearance.py diff --git a/.gitignore b/.gitignore index 63f05fa4d..a1297cb9a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ hyprwhspr/.local/share/ **/__pycache__/ *.py[cod] **/node_modules/ +sway/.config/sway/generated/theme.conf +sway/.config/sway/generated/theme.conf.tmp # Local editor/application backups *.bak diff --git a/bin/.local/bin/generate-appearance.py b/bin/.local/bin/generate-appearance.py new file mode 100755 index 000000000..71fa8a8a6 --- /dev/null +++ b/bin/.local/bin/generate-appearance.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Générer et appliquer les couleurs Sway depuis la palette Catppuccin unique.""" +import argparse +import json +import os +from pathlib import Path +import re +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[3] +PALETTE = ROOT / 'quickshell/.config/quickshell/labfy-sway/theme/catppuccin.json' +SWAY = Path(os.environ.get('XDG_CONFIG_HOME', Path.home() / '.config')) / 'sway' +STATE = Path(os.environ.get('XDG_STATE_HOME', Path.home() / '.local/state')) / 'labfy-appearance/effective.json' +ACCENTS = ('rosewater', 'flamingo', 'pink', 'mauve', 'red', 'maroon', 'peach', + 'yellow', 'green', 'teal', 'sky', 'sapphire', 'blue', 'lavender') +HEX = re.compile(r'#[0-9a-fA-F]{6}\Z') + + +def palette_data(): + data = json.loads(PALETTE.read_text(encoding='utf-8')) + if set(data) != {'latte', 'frappe', 'macchiato', 'mocha'}: + raise ValueError('Palette incomplète') + for colors in data.values(): + if len(colors) != 26 or not all(HEX.fullmatch(v) for v in colors.values()): + raise ValueError('Palette invalide') + return data + + +def render(flavor, accent, profile='default'): + data = palette_data() + if flavor not in data: + raise ValueError('flavor invalide: ' + flavor) + if accent not in ACCENTS: + raise ValueError('accent invalide: ' + accent) + if profile != 'default': + raise ValueError('profile invalide: ' + profile) + p = data[flavor] + # CONTRACT: toutes les valeurs interpolées proviennent de clés bornées ou + # de chaînes hex validées ; aucune entrée utilisateur ne devient une commande. + lines = ['# Généré depuis theme/catppuccin.json ; ne pas éditer.'] + lines += [f'set ${name} {value}' for name, value in p.items()] + lines += [f'set $accent {p[accent]}', + 'set $shadow_active #00000055', + 'set $shadow_inactive #00000035', + 'set $inactive_opacity 0.85'] + return '\n'.join(lines) + '\n' + + +def atomic_write(path, content): + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_name(path.name + '.tmp') + # CONTRACT: le fichier précédent reste intact jusqu'au rename sur le même FS. + try: + with open(tmp, 'wb') as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(tmp, path) + fd = os.open(path.parent, os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + finally: + tmp.unlink(missing_ok=True) + + +def checked(args): + return subprocess.run(args, check=True, capture_output=True, text=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('flavor', choices=('latte', 'frappe', 'macchiato', 'mocha')) + parser.add_argument('--accent', default='lavender', choices=ACCENTS) + parser.add_argument('--profile', default='default') + parser.add_argument('--output', type=Path, default=SWAY / 'generated/theme.conf') + parser.add_argument('--apply', action='store_true', help='valider puis recharger Sway et QuickShell') + args = parser.parse_args() + if args.apply and args.output != SWAY / 'generated/theme.conf': + parser.error('--apply exige la sortie runtime par défaut') + try: + content = render(args.flavor, args.accent, args.profile).encode('utf-8') + old = args.output.read_bytes() if args.output.exists() else None + old_state = STATE.read_bytes() if args.apply and STATE.exists() else None + previous_theme = None + if args.apply: + previous_theme = checked(['qs', '-c', 'labfy-sway', 'ipc', 'call', + 'appearance', 'currentTheme']).stdout.strip() + atomic_write(args.output, content) + if args.apply: + try: + checked(['sway', '--validate', '-c', str(SWAY / 'config')]) + checked(['swaymsg', 'reload']) + previous = json.loads(old_state) if old_state else {} + changed = (previous.get('effectiveFlavor') != args.flavor + or previous.get('effectiveAccent') != args.accent + or previous.get('effectiveMode') != 'normal') + revision = max(0, int(previous.get('revision', 0))) + int(changed) + state = {'version': 1, 'effectiveFlavor': args.flavor, + 'effectiveMode': 'normal', 'effectiveDark': args.flavor != 'latte', + 'effectiveHighContrast': False, 'effectiveAccent': args.accent, + 'revision': revision} + # CONTRACT: le backend persiste l'état après validation Sway ; + # le contrôleur l'expose aux consommateurs et le lit au restart. + atomic_write(STATE, (json.dumps(state, separators=(',', ':')) + '\n').encode('utf-8')) + result = checked(['qs', '-c', 'labfy-sway', 'ipc', 'call', 'appearance', + 'setTheme', args.flavor, args.accent]) + if result.stdout.strip() != 'true': + raise RuntimeError('QuickShell a refusé le thème') + except Exception: + if old is None: + args.output.unlink(missing_ok=True) + else: + atomic_write(args.output, old) + if old_state is None: + STATE.unlink(missing_ok=True) + else: + atomic_write(STATE, old_state) + checked(['swaymsg', 'reload']) + if previous_theme and '/' in previous_theme: + old_flavor, old_accent = previous_theme.split('/', 1) + checked(['qs', '-c', 'labfy-sway', 'ipc', 'call', 'appearance', + 'setTheme', old_flavor, old_accent]) + raise + print(f'{args.flavor}/{args.accent}: {args.output}') + except (OSError, ValueError, subprocess.CalledProcessError, RuntimeError) as exc: + print(f'Erreur : {exc}', file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/quickshell/.config/quickshell/labfy-sway/Bar.qml b/quickshell/.config/quickshell/labfy-sway/Bar.qml index cc8dfa220..e51a666ab 100644 --- a/quickshell/.config/quickshell/labfy-sway/Bar.qml +++ b/quickshell/.config/quickshell/labfy-sway/Bar.qml @@ -8,6 +8,7 @@ import "datecenter" import "notifications" import "status" import "systemmonitor" +import "theme" PanelWindow { id: bar @@ -37,7 +38,7 @@ PanelWindow { // layer-shell est focusable dès son ouverture. OnDemand n'accapare pas // le clavier hors interaction avec la barre ou son popup. focusable: true - color: "#eb1e1e2e" + color: Theme.panelBackground // CONTRACT: toute la capsule centrale partage le même popup et la même exclusion XOR. function toggleDateCenter() { diff --git a/quickshell/.config/quickshell/labfy-sway/README.md b/quickshell/.config/quickshell/labfy-sway/README.md index a299bed2d..af915af3c 100644 --- a/quickshell/.config/quickshell/labfy-sway/README.md +++ b/quickshell/.config/quickshell/labfy-sway/README.md @@ -1,5 +1,8 @@ # QuickShell labfy-sway +Le [moteur de thème commun](theme/README.md) fournit les couleurs effectives +à QuickShell et Sway/SwayFX depuis une seule palette Catppuccin versionnée. + Barre principale et serveur de notifications de la session SwayFX. ```text diff --git a/quickshell/.config/quickshell/labfy-sway/components/NerdIcon.qml b/quickshell/.config/quickshell/labfy-sway/components/NerdIcon.qml index 9e8c1433a..ead7d37a9 100644 --- a/quickshell/.config/quickshell/labfy-sway/components/NerdIcon.qml +++ b/quickshell/.config/quickshell/labfy-sway/components/NerdIcon.qml @@ -1,4 +1,5 @@ import QtQuick +import "../theme" Text { font.family: "JetBrainsMono Nerd Font Mono" @@ -6,5 +7,5 @@ Text { font.pixelSize: 18 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - color: "#cdd6f4" + color: Theme.foreground } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/ActionButton.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/ActionButton.qml index 01ca3508d..b368a3d69 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/ActionButton.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/ActionButton.qml @@ -1,4 +1,5 @@ import QtQuick +import "../theme" Rectangle { id: button @@ -8,13 +9,13 @@ Rectangle { implicitWidth: caption.implicitWidth + 20 implicitHeight: 30 radius: 4 - color: !enabled ? "#313244" : pointer.containsMouse ? "#585b70" : "#45475a" + color: !enabled ? Theme.buttonBackground : pointer.containsMouse ? Theme.strongBorder : Theme.border opacity: enabled ? 1 : 0.55 Text { id: caption anchors.centerIn: parent text: button.label - color: button.danger ? "#f38ba8" : "#cdd6f4" + color: button.danger ? Theme.danger : Theme.foreground font.pixelSize: 12 } MouseArea { diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/BatterySettings.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/BatterySettings.qml index ae12d1fb3..dcd0659a4 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/BatterySettings.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/BatterySettings.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import Quickshell.Io import Quickshell.Services.UPower import "../components" +import "../theme" Item { id: page @@ -211,7 +212,7 @@ Item { anchors.leftMargin: 8 anchors.verticalCenter: parent.verticalCenter text: "Batterie" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 16 font.bold: true } @@ -228,7 +229,7 @@ Item { width: parent.width height: 68 radius: 4 - color: "#313244" + color: Theme.buttonBackground NerdIcon { id: batteryIcon anchors.left: parent.left @@ -245,12 +246,12 @@ Item { spacing: 2 Text { text: page.available ? page.percent + " %" : "Batterie indisponible" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 16 } Text { text: page.batteryState - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 12 } } @@ -258,7 +259,7 @@ Item { Text { text: "Limite de charge" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 14 } @@ -267,8 +268,8 @@ Item { spacing: 8 Rectangle { width: 42; height: 38; radius: 4 - color: minusPointer.containsMouse ? "#45475a" : "#313244" - Text { anchors.centerIn: parent; text: "−"; color: "#cdd6f4"; font.pixelSize: 18 } + color: minusPointer.containsMouse ? Theme.border : Theme.buttonBackground + Text { anchors.centerIn: parent; text: "−"; color: Theme.foreground; font.pixelSize: 18 } MouseArea { id: minusPointer anchors.fill: parent @@ -283,7 +284,7 @@ Item { width: parent.width - 100 height: 38 horizontalAlignment: TextInput.AlignHCenter - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 16 maximumLength: 3 inputMethodHints: Qt.ImhDigitsOnly @@ -303,22 +304,22 @@ Item { } background: Rectangle { radius: 4 - color: "#313244" - border.color: page.validProposed ? "#45475a" : "#f38ba8" + color: Theme.buttonBackground + border.color: page.validProposed ? Theme.border : Theme.danger } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: "%" - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 13 } } Rectangle { width: 42; height: 38; radius: 4 - color: plusPointer.containsMouse ? "#45475a" : "#313244" - Text { anchors.centerIn: parent; text: "+"; color: "#cdd6f4"; font.pixelSize: 18 } + color: plusPointer.containsMouse ? Theme.border : Theme.buttonBackground + Text { anchors.centerIn: parent; text: "+"; color: Theme.foreground; font.pixelSize: 18 } MouseArea { id: plusPointer anchors.fill: parent @@ -344,11 +345,11 @@ Item { height: 32 radius: 4 opacity: modelData !== "" ? 1 : 0 - color: keyPointer.containsMouse ? "#45475a" : "#313244" + color: keyPointer.containsMouse ? Theme.border : Theme.buttonBackground Text { anchors.centerIn: parent text: modelData - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 13 } MouseArea { @@ -369,13 +370,13 @@ Item { delegate: Rectangle { required property int modelData width: 60; height: 32; radius: 4 - color: page.reportedThreshold === modelData ? "#585b70" : "#313244" + color: page.reportedThreshold === modelData ? Theme.strongBorder : Theme.buttonBackground border.color: page.validProposed && Number(valueField.text) === modelData - ? "#cba6f7" : "transparent" + ? Theme.accent : "transparent" Text { anchors.centerIn: parent text: modelData - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 13 } MouseArea { @@ -390,7 +391,7 @@ Item { Text { text: page.reportedThreshold >= 0 ? "Limite rapportée : " + page.reportedThreshold + " %" : "Limite indisponible" - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 12 } @@ -398,12 +399,12 @@ Item { width: parent.width height: 36 radius: 4 - color: applyPointer.containsMouse && applyPointer.enabled ? "#b4befe" : "#cba6f7" + color: Theme.accent opacity: applyPointer.enabled ? 1 : 0.5 Text { anchors.centerIn: parent text: page.phase === "Applying" ? "Application…" : "Appliquer" - color: "#1e1e2e" + color: Theme.onAccent font.pixelSize: 13 } MouseArea { @@ -423,7 +424,7 @@ Item { : !page.validProposed ? "Valeur comprise entre 40 et 100" : !page.helperReady ? "Service de charge non déployé" : "" color: page.phase === "Error" || !page.validProposed || !page.helperReady - ? "#f38ba8" : "#a6e3a1" + ? Theme.danger : Theme.success font.pixelSize: 12 wrapMode: Text.WordWrap } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/BluetoothPage.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/BluetoothPage.qml index ab7985c73..308f0b16b 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/BluetoothPage.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/BluetoothPage.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Bluetooth import "../components" +import "../theme" Item { id: page @@ -61,10 +62,10 @@ Item { Text { width: parent.width - 60; height: 30; verticalAlignment: Text.AlignVCenter text: page.stage === "main" ? "Bluetooth" : page.nameOf(page.selectedDevice) - color: "#cdd6f4"; font.pixelSize: 16; font.bold: true; elide: Text.ElideRight + color: Theme.foreground; font.pixelSize: 16; font.bold: true; elide: Text.ElideRight } } - Text { width: parent.width; visible: page.message.length > 0; text: page.message; wrapMode: Text.Wrap; color: "#f9e2af"; font.pixelSize: 12 } + Text { width: parent.width; visible: page.message.length > 0; text: page.message; wrapMode: Text.Wrap; color: Theme.warningForeground; font.pixelSize: 12 } Loader { width: parent.width; height: Math.max(0, parent.height - y) sourceComponent: page.stage === "main" ? mainComponent : page.stage === "details" @@ -80,22 +81,22 @@ Item { width: page.width; spacing: 10 Row { width: parent.width; spacing: 8 - Text { width: parent.width - 100; text: !page.adapter ? "Adaptateur indisponible" : page.adapter.enabled ? "Bluetooth activé" : "Bluetooth désactivé"; color: "#cdd6f4"; font.pixelSize: 13 } + Text { width: parent.width - 100; text: !page.adapter ? "Adaptateur indisponible" : page.adapter.enabled ? "Bluetooth activé" : "Bluetooth désactivé"; color: Theme.foreground; font.pixelSize: 13 } ActionButton { label: page.adapter && page.adapter.enabled ? "Désactiver" : "Activer"; enabled: !!page.adapter; onClicked: page.adapter.enabled = !page.adapter.enabled } } - Text { text: page.scanWanted ? "Recherche d'appareils…" : "Scan arrêté"; color: "#a6adc8"; font.pixelSize: 11 } - Text { visible: page.devices.length === 0; text: "Aucun appareil connu"; color: "#a6adc8"; font.pixelSize: 12 } - Text { visible: page.connectedDevices.length > 0; text: "Connectés"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 } + Text { text: page.scanWanted ? "Recherche d'appareils…" : "Scan arrêté"; color: Theme.secondaryForeground; font.pixelSize: 11 } + Text { visible: page.devices.length === 0; text: "Aucun appareil connu"; color: Theme.secondaryForeground; font.pixelSize: 12 } + Text { visible: page.connectedDevices.length > 0; text: "Connectés"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 } Repeater { model: ScriptModel { values: page.connectedDevices; objectProp: "address" } delegate: deviceRowComponent } - Text { visible: page.pairedDevices.length > 0; text: "Appairés"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 } + Text { visible: page.pairedDevices.length > 0; text: "Appairés"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 } Repeater { model: ScriptModel { values: page.pairedDevices; objectProp: "address" } delegate: deviceRowComponent } - Text { visible: page.newDevices.length > 0; text: "Nouveaux appareils"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 } + Text { visible: page.newDevices.length > 0; text: "Nouveaux appareils"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 } Repeater { model: ScriptModel { values: page.newDevices; objectProp: "address" } delegate: deviceRowComponent @@ -109,7 +110,7 @@ Item { required property var modelData readonly property var device: modelData width: page.width; height: 48; radius: 4 - color: pointer.containsMouse ? "#45475a" : "#313244" + color: pointer.containsMouse ? Theme.border : Theme.buttonBackground Image { id: iconImage; anchors.left: parent.left; anchors.leftMargin: 9; anchors.verticalCenter: parent.verticalCenter width: 19; height: 19; source: device.icon ? Quickshell.iconPath(device.icon, true) : "" @@ -117,13 +118,13 @@ Item { } NerdIcon { anchors.centerIn: iconImage; visible: !iconImage.visible - text: "󰂯"; color: "#cba6f7"; font.pixelSize: 17 + text: "󰂯"; color: Theme.accentForeground; font.pixelSize: 17 } Column { anchors.left: parent.left; anchors.leftMargin: 38; anchors.right: parent.right anchors.rightMargin: 10; anchors.verticalCenter: parent.verticalCenter; spacing: 2 - Text { width: parent.width; text: page.nameOf(device); color: "#cdd6f4"; font.pixelSize: 12; elide: Text.ElideRight } - Text { text: page.stateOf(device) + (device.batteryAvailable ? " • " + Math.round(device.battery * 100) + " %" : ""); color: "#a6adc8"; font.pixelSize: 11 } + Text { width: parent.width; text: page.nameOf(device); color: Theme.foreground; font.pixelSize: 12; elide: Text.ElideRight } + Text { text: page.stateOf(device) + (device.batteryAvailable ? " • " + Math.round(device.battery * 100) + " %" : ""); color: Theme.secondaryForeground; font.pixelSize: 11 } } MouseArea { id: pointer; anchors.fill: parent; hoverEnabled: true; onClicked: page.choose(device) } } @@ -134,10 +135,10 @@ Item { clip: true; ScrollBar.horizontal.policy: ScrollBar.AlwaysOff Column { width: page.width; spacing: 10 - Text { text: page.selectedDevice ? page.stateOf(page.selectedDevice) : "Appareil indisponible"; color: "#cdd6f4"; font.pixelSize: 13 } - Text { visible: !!page.selectedDevice && page.selectedDevice.deviceName !== page.selectedDevice.name; text: "Nom d'origine : " + (page.selectedDevice ? page.selectedDevice.deviceName : ""); color: "#a6adc8"; font.pixelSize: 11 } - Text { visible: !!page.selectedDevice && page.selectedDevice.batteryAvailable; text: "Batterie : " + (page.selectedDevice ? Math.round(page.selectedDevice.battery * 100) : 0) + " %"; color: "#cdd6f4"; font.pixelSize: 12 } - Text { text: page.selectedDevice ? "Appairé : " + (page.selectedDevice.paired ? "oui" : "non") + " • Lié : " + (page.selectedDevice.bonded ? "oui" : "non") : ""; color: "#a6adc8"; font.pixelSize: 11 } + Text { text: page.selectedDevice ? page.stateOf(page.selectedDevice) : "Appareil indisponible"; color: Theme.foreground; font.pixelSize: 13 } + Text { visible: !!page.selectedDevice && page.selectedDevice.deviceName !== page.selectedDevice.name; text: "Nom d'origine : " + (page.selectedDevice ? page.selectedDevice.deviceName : ""); color: Theme.secondaryForeground; font.pixelSize: 11 } + Text { visible: !!page.selectedDevice && page.selectedDevice.batteryAvailable; text: "Batterie : " + (page.selectedDevice ? Math.round(page.selectedDevice.battery * 100) : 0) + " %"; color: Theme.foreground; font.pixelSize: 12 } + Text { text: page.selectedDevice ? "Appairé : " + (page.selectedDevice.paired ? "oui" : "non") + " • Lié : " + (page.selectedDevice.bonded ? "oui" : "non") : ""; color: Theme.secondaryForeground; font.pixelSize: 11 } Row { spacing: 8 ActionButton { @@ -159,37 +160,37 @@ Item { } Row { spacing: 8 - Text { width: 165; text: "Appareil de confiance"; color: "#cdd6f4"; font.pixelSize: 12 } + Text { width: 165; text: "Appareil de confiance"; color: Theme.foreground; font.pixelSize: 12 } ActionButton { label: page.selectedDevice && page.selectedDevice.trusted ? "Activé" : "Désactivé"; enabled: !!page.selectedDevice && page.selectedDevice.paired; onClicked: page.selectedDevice.trusted = !page.selectedDevice.trusted } } Row { spacing: 8 - Text { width: 165; text: "Bloqué"; color: "#cdd6f4"; font.pixelSize: 12 } + Text { width: 165; text: "Bloqué"; color: Theme.foreground; font.pixelSize: 12 } ActionButton { label: page.selectedDevice && page.selectedDevice.blocked ? "Oui" : "Non"; enabled: !!page.selectedDevice; onClicked: { if (page.selectedDevice.blocked) page.selectedDevice.blocked = false; else page.stage = "block"; } } } - Text { width: parent.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: "#a6adc8"; font.pixelSize: 11 } + Text { width: parent.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: Theme.secondaryForeground; font.pixelSize: 11 } Row { visible: !!page.selectedDevice && page.selectedDevice.paired && page.adapter && page.adapter.enabled && page.selectedDevice.wakeAllowed !== undefined spacing: 8 - Text { width: 165; text: "Autoriser le réveil"; color: "#cdd6f4"; font.pixelSize: 12 } + Text { width: 165; text: "Autoriser le réveil"; color: Theme.foreground; font.pixelSize: 12 } ActionButton { label: page.selectedDevice && page.selectedDevice.wakeAllowed ? "Activé" : "Désactivé"; onClicked: page.selectedDevice.wakeAllowed = !page.selectedDevice.wakeAllowed } } - Text { text: "Nom local"; color: "#cba6f7"; font.pixelSize: 13; font.bold: true } + Text { text: "Nom local"; color: Theme.accentForeground; font.pixelSize: 13; font.bold: true } TextField { id: aliasInput; width: page.width text: page.selectedDevice ? page.selectedDevice.name : "" placeholderText: "Alias local"; selectByMouse: true - color: "#cdd6f4"; placeholderTextColor: "#6c7086" - background: Rectangle { color: "#313244"; border.color: aliasInput.activeFocus ? "#cba6f7" : "#585b70"; radius: 4 } + color: Theme.foreground; placeholderTextColor: Theme.disabledForeground + background: Rectangle { color: Theme.buttonBackground; border.color: aliasInput.activeFocus ? Theme.accent : Theme.strongBorder; radius: 4 } } Row { spacing: 8 ActionButton { label: "Réinitialiser"; enabled: !!page.selectedDevice; onClicked: { page.selectedDevice.name = ""; aliasInput.text = page.selectedDevice.name; } } ActionButton { label: "Enregistrer"; enabled: !!page.selectedDevice; onClicked: page.selectedDevice.name = aliasInput.text.trim() } } - Text { text: "Informations techniques : " + (page.selectedDevice ? page.selectedDevice.address : ""); color: "#6c7086"; font.pixelSize: 10 } + Text { text: "Informations techniques : " + (page.selectedDevice ? page.selectedDevice.address : ""); color: Theme.disabledForeground; font.pixelSize: 10 } } } } @@ -197,8 +198,8 @@ Item { id: forgetComponent Column { spacing: 10 - Text { width: page.width; wrapMode: Text.Wrap; text: "Oublier " + page.nameOf(page.selectedDevice) + " ?"; color: "#f38ba8"; font.pixelSize: 13 } - Text { width: page.width; wrapMode: Text.Wrap; text: "Cela supprimera les informations de pairage."; color: "#a6adc8"; font.pixelSize: 12 } + Text { width: page.width; wrapMode: Text.Wrap; text: "Oublier " + page.nameOf(page.selectedDevice) + " ?"; color: Theme.danger; font.pixelSize: 13 } + Text { width: page.width; wrapMode: Text.Wrap; text: "Cela supprimera les informations de pairage."; color: Theme.secondaryForeground; font.pixelSize: 12 } Row { spacing: 8 ActionButton { label: "Annuler"; onClicked: page.stage = "details" } @@ -210,8 +211,8 @@ Item { id: blockComponent Column { spacing: 10 - Text { width: page.width; wrapMode: Text.Wrap; text: "Bloquer " + page.nameOf(page.selectedDevice) + " ?"; color: "#f38ba8"; font.pixelSize: 13 } - Text { width: page.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: "#a6adc8"; font.pixelSize: 12 } + Text { width: page.width; wrapMode: Text.Wrap; text: "Bloquer " + page.nameOf(page.selectedDevice) + " ?"; color: Theme.danger; font.pixelSize: 13 } + Text { width: page.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: Theme.secondaryForeground; font.pixelSize: 12 } Row { spacing: 8 ActionButton { label: "Annuler"; onClicked: page.stage = "details" } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/BrightnessSlider.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/BrightnessSlider.qml index 279a5f7c7..3016b5671 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/BrightnessSlider.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/BrightnessSlider.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import Quickshell.Io import "../components" +import "../theme" Item { id: brightnessControl @@ -44,7 +45,7 @@ Item { anchors.verticalCenter: parent.verticalCenter text: "" font.pixelSize: 18 - color: "#cdd6f4" + color: Theme.foreground } Text { @@ -52,7 +53,7 @@ Item { anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter text: "Luminosité" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 14 } @@ -60,7 +61,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: brightnessControl.available ? brightnessControl.percent + "%" : "Indisponible" - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 14 } } @@ -88,13 +89,13 @@ Item { width: slider.availableWidth height: 8 radius: 4 - color: "#313244" + color: Theme.buttonBackground Rectangle { width: slider.visualPosition * parent.width height: parent.height radius: parent.radius - color: "#cba6f7" + color: Theme.accent } } @@ -104,7 +105,7 @@ Item { width: 16 height: 16 radius: 8 - color: "#cdd6f4" + color: Theme.foreground } } } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/ConfirmAction.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/ConfirmAction.qml index f6e06eeca..fce2cae39 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/ConfirmAction.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/ConfirmAction.qml @@ -1,4 +1,5 @@ import QtQuick +import "../theme" Item { id: confirmation @@ -19,7 +20,7 @@ Item { Text { width: parent.width text: confirmation.actionInfo.confirmTitle - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 16 font.bold: true wrapMode: Text.Wrap @@ -28,7 +29,7 @@ Item { Text { width: parent.width text: confirmation.actionInfo.description - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 13 wrapMode: Text.Wrap } @@ -42,12 +43,12 @@ Item { width: (parent.width - parent.spacing) / 2 height: parent.height radius: 4 - color: cancelPointer.containsMouse ? "#45475a" : "#313244" + color: cancelPointer.containsMouse ? Theme.border : Theme.buttonBackground Text { anchors.centerIn: parent text: "Annuler" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 13 } MouseArea { @@ -68,7 +69,7 @@ Item { Text { anchors.centerIn: parent text: confirmation.actionInfo.label - color: "#1e1e2e" + color: Theme.contrastingText(confirmation.actionInfo.accent) font.pixelSize: 13 font.bold: true } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenter.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenter.qml index 51210ac39..e26f8424d 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenter.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenter.qml @@ -5,6 +5,7 @@ import Quickshell.I3 import Quickshell.Networking import Quickshell.Bluetooth import "../components" +import "../theme" PopupWindow { id: popup @@ -15,19 +16,19 @@ PopupWindow { property var pendingAction: null // Une seule table associe les libellés, confirmations et commandes de session. readonly property var sessionActions: [ - { id: "lock", label: "Verrouiller", icon: "", accent: "#b4befe", command: ["swaylock"] }, - { id: "suspend", label: "Veille", icon: "", accent: "#b4befe", + { id: "lock", label: "Verrouiller", icon: "", accent: Theme.accent, command: ["swaylock"] }, + { id: "suspend", label: "Veille", icon: "", accent: Theme.accent, confirmTitle: "Mettre l'ordinateur en veille ?", description: "La session sera verrouillée avant la veille.", command: ["systemctl", "suspend"] }, - { id: "logout", label: "Déconnexion", icon: "", accent: "#fab387", + { id: "logout", label: "Déconnexion", icon: "", accent: Theme.urgent, confirmTitle: "Se déconnecter ?", description: "La session SwayFX en cours sera fermée.", command: null }, - { id: "reboot", label: "Redémarrer", icon: "", accent: "#f9e2af", + { id: "reboot", label: "Redémarrer", icon: "", accent: Theme.warning, confirmTitle: "Redémarrer l'ordinateur ?", description: "La session en cours sera fermée.", command: ["systemctl", "reboot"] }, - { id: "poweroff", label: "Éteindre", icon: "", accent: "#f38ba8", + { id: "poweroff", label: "Éteindre", icon: "", accent: Theme.danger, confirmTitle: "Éteindre l'ordinateur ?", description: "La session en cours sera fermée.", command: ["systemctl", "poweroff"] } @@ -82,8 +83,8 @@ PopupWindow { Rectangle { anchors.fill: parent radius: 4 - color: "#1e1e2e" - border.color: "#45475a" + color: Theme.popupBackground + border.color: Theme.border StackLayout { anchors.fill: parent @@ -96,7 +97,7 @@ PopupWindow { Text { text: "Réglages rapides" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 16 font.bold: true } @@ -130,7 +131,7 @@ PopupWindow { width: 40 height: 30 radius: 4 - color: powerPointer.containsMouse ? "#45475a" : "#313244" + color: powerPointer.containsMouse ? Theme.border : Theme.buttonBackground NerdIcon { anchors.centerIn: parent @@ -171,7 +172,7 @@ PopupWindow { ConfirmAction { id: confirmationPage actionInfo: popup.pendingAction || ({ id: "", label: "", confirmTitle: "", - description: "", accent: "#cdd6f4" }) + description: "", accent: Theme.foreground }) onCancelled: { popup.pendingAction = null; popup.currentPage = 3; diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenterButton.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenterButton.qml index 2292590e8..afae27e02 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenterButton.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/ControlCenterButton.qml @@ -1,5 +1,6 @@ import QtQuick import "../components" +import "../theme" Rectangle { id: controlButton @@ -10,14 +11,14 @@ Rectangle { width: 48 height: 26 radius: 4 - color: open ? "#cba6f7" : pointer.containsMouse ? "#45475a" : "#313244" + color: open ? Theme.accent : pointer.containsMouse ? Theme.border : Theme.buttonBackground NerdIcon { anchors.centerIn: parent text: "" // WHY: garder le même poids visuel que les statuts voisins dans la capsule fixe. font.pixelSize: 20 - color: controlButton.open ? "#1e1e2e" : "#cdd6f4" + color: controlButton.open ? Theme.onAccent : Theme.foreground } MouseArea { diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/PowerProfile.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/PowerProfile.qml index 7e1e1ef1d..9b0011c03 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/PowerProfile.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/PowerProfile.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell.Services.UPower import "../components" +import "../theme" Item { id: powerControl @@ -22,7 +23,7 @@ Item { anchors.verticalCenter: parent.verticalCenter text: "" font.pixelSize: 18 - color: "#cdd6f4" + color: Theme.foreground } Text { @@ -30,7 +31,7 @@ Item { anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter text: "Profil d'énergie" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 14 } } @@ -57,12 +58,12 @@ Item { width: (choices.width - 2 * choices.spacing) / 3 height: choices.height radius: 4 - color: !available ? "#313244" : selected ? "#cba6f7" : hover.hovered ? "#45475a" : "#313244" + color: !available ? Theme.buttonBackground : selected ? Theme.accent : hover.hovered ? Theme.border : Theme.buttonBackground Text { anchors.centerIn: parent text: parent.modelData.label - color: !parent.available ? "#6c7086" : parent.selected ? "#1e1e2e" : "#cdd6f4" + color: !parent.available ? Theme.disabledForeground : parent.selected ? Theme.onAccent : Theme.foreground font.pixelSize: 12 } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/QuickToggle.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/QuickToggle.qml index bb6f9d8dd..c1a06a756 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/QuickToggle.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/QuickToggle.qml @@ -1,5 +1,6 @@ import QtQuick import "../components" +import "../theme" Rectangle { id: tile @@ -14,8 +15,8 @@ Rectangle { height: 58 radius: 4 - color: active ? "#cba6f7" : "#313244" - readonly property color foreground: active ? "#1e1e2e" : "#cdd6f4" + color: active ? Theme.accent : Theme.buttonBackground + readonly property color foreground: active ? Theme.onAccent : Theme.foreground NerdIcon { id: symbol diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionAction.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionAction.qml index 3bacfa9f5..47aed2ea8 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionAction.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionAction.qml @@ -1,5 +1,6 @@ import QtQuick import "../components" +import "../theme" Rectangle { id: actionButton @@ -9,7 +10,7 @@ Rectangle { height: 48 radius: 4 - color: pointer.containsMouse ? "#45475a" : "#313244" + color: pointer.containsMouse ? Theme.border : Theme.buttonBackground NerdIcon { id: icon @@ -27,7 +28,7 @@ Rectangle { anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: actionButton.actionInfo.label - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 14 } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionPage.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionPage.qml index 14f3da794..a98df662b 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionPage.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/SessionPage.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "../components" +import "../theme" Item { id: page @@ -34,7 +35,7 @@ Item { anchors.leftMargin: 8 anchors.verticalCenter: parent.verticalCenter text: "Session" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 16 font.bold: true } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/VolumeSlider.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/VolumeSlider.qml index 73e57f378..14aac9b60 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/VolumeSlider.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/VolumeSlider.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import Quickshell.Services.Pipewire import "../components" +import "../theme" Item { id: volumeControl @@ -33,7 +34,7 @@ Item { anchors.verticalCenter: parent.verticalCenter text: volumeControl.sinkAudio ? (volumeControl.muted ? "" : "") : "" font.pixelSize: 18 - color: volumeControl.muted ? "#f38ba8" : "#cdd6f4" + color: volumeControl.muted ? Theme.danger : Theme.foreground MouseArea { anchors.fill: parent @@ -48,7 +49,7 @@ Item { anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter text: "Volume" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 14 } @@ -56,7 +57,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: volumeControl.sinkAudio ? volumeControl.percent + "%" : "Indisponible" - color: volumeControl.muted ? "#f38ba8" : "#a6adc8" + color: volumeControl.muted ? Theme.danger : Theme.secondaryForeground font.pixelSize: 14 } } @@ -82,13 +83,13 @@ Item { width: slider.availableWidth height: 8 radius: 4 - color: "#313244" + color: Theme.buttonBackground Rectangle { width: slider.visualPosition * parent.width height: parent.height radius: parent.radius - color: "#cba6f7" + color: Theme.accent } } @@ -98,7 +99,7 @@ Item { width: 16 height: 16 radius: 8 - color: "#cdd6f4" + color: Theme.foreground } } } diff --git a/quickshell/.config/quickshell/labfy-sway/controlcenter/WifiPage.qml b/quickshell/.config/quickshell/labfy-sway/controlcenter/WifiPage.qml index 0ed77c2d7..823524f21 100644 --- a/quickshell/.config/quickshell/labfy-sway/controlcenter/WifiPage.qml +++ b/quickshell/.config/quickshell/labfy-sway/controlcenter/WifiPage.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Networking import "../components" +import "../theme" Item { id: page @@ -122,12 +123,12 @@ Item { text: page.stage === "main" ? "Wi-Fi" : page.stage === "password" ? "Se connecter à " + page.selectedName : page.stage === "forget" ? "Oublier un réseau" : page.selectedName - color: "#cdd6f4"; font.pixelSize: 16; font.bold: true; elide: Text.ElideRight + color: Theme.foreground; font.pixelSize: 16; font.bold: true; elide: Text.ElideRight } } Text { width: parent.width; visible: page.message.length > 0 - text: page.message; wrapMode: Text.Wrap; color: "#f9e2af"; font.pixelSize: 12 + text: page.message; wrapMode: Text.Wrap; color: Theme.warningForeground; font.pixelSize: 12 } Loader { id: body @@ -147,27 +148,27 @@ Item { width: page.width; spacing: 9 Row { width: parent.width; spacing: 8 - Text { width: parent.width - 100; text: "Wi-Fi " + (Networking.wifiEnabled ? "activé" : "désactivé"); color: "#cdd6f4"; font.pixelSize: 13 } + Text { width: parent.width - 100; text: "Wi-Fi " + (Networking.wifiEnabled ? "activé" : "désactivé"); color: Theme.foreground; font.pixelSize: 13 } ActionButton { label: Networking.wifiEnabled ? "Désactiver" : "Activer" enabled: Networking.wifiHardwareEnabled onClicked: Networking.wifiEnabled = !Networking.wifiEnabled } } - Text { text: page.scanWanted ? "Recherche de réseaux…" : "Scan arrêté"; color: "#a6adc8"; font.pixelSize: 11 } - Text { text: "Réseau actuel"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 } + Text { text: page.scanWanted ? "Recherche de réseaux…" : "Scan arrêté"; color: Theme.secondaryForeground; font.pixelSize: 11 } + Text { text: "Réseau actuel"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 } Rectangle { - width: parent.width; height: 66; radius: 4; color: "#313244" + width: parent.width; height: 66; radius: 4; color: Theme.buttonBackground Text { anchors.left: parent.left; anchors.leftMargin: 10; anchors.top: parent.top; anchors.topMargin: 8 text: page.currentNetwork ? page.currentNetwork.name : "Aucun réseau connecté" - color: "#cdd6f4"; font.pixelSize: 13; font.bold: true + color: Theme.foreground; font.pixelSize: 13; font.bold: true } Text { anchors.left: parent.left; anchors.leftMargin: 10; anchors.bottom: parent.bottom; anchors.bottomMargin: 8 text: page.currentNetwork ? page.securityLabel(page.currentNetwork) + " • " + Math.round(page.currentNetwork.signalStrength * 100) + " %" : "" - color: "#a6adc8"; font.pixelSize: 11 + color: Theme.secondaryForeground; font.pixelSize: 11 } ActionButton { anchors.right: parent.right; anchors.rightMargin: 8; anchors.verticalCenter: parent.verticalCenter @@ -175,11 +176,11 @@ Item { onClicked: page.choose(page.currentNetwork) } } - Text { text: "Réseaux disponibles"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 } + Text { text: "Réseaux disponibles"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 } Text { visible: page.sortedNetworks.length === 0 text: !Networking.wifiHardwareEnabled ? "Wi-Fi bloqué matériellement" : "Aucun réseau visible" - color: "#a6adc8"; font.pixelSize: 12 + color: Theme.secondaryForeground; font.pixelSize: 12 } Repeater { model: ScriptModel { values: page.sortedNetworks; objectProp: "name" } @@ -187,23 +188,23 @@ Item { required property var modelData readonly property var network: modelData width: page.width; height: 44; radius: 4 - color: pointer.containsMouse ? "#45475a" : "#313244" + color: pointer.containsMouse ? Theme.border : Theme.buttonBackground NerdIcon { anchors.left: parent.left; anchors.leftMargin: 9; anchors.verticalCenter: parent.verticalCenter text: network.connected ? "" : network.security === WifiSecurityType.Open ? "󰖩" : "" - color: network.connected ? "#a6e3a1" : "#cdd6f4"; font.pixelSize: 16 + color: network.connected ? Theme.success : Theme.foreground; font.pixelSize: 16 } Text { anchors.left: parent.left; anchors.leftMargin: 37; anchors.right: strength.left anchors.rightMargin: 8; anchors.verticalCenter: parent.verticalCenter text: network.name + " • " + page.stateLabel(network) - color: "#cdd6f4"; font.pixelSize: 12; elide: Text.ElideRight + color: Theme.foreground; font.pixelSize: 12; elide: Text.ElideRight } Text { id: strength; anchors.right: parent.right; anchors.rightMargin: 9 anchors.verticalCenter: parent.verticalCenter text: Math.round(network.signalStrength * 100) + "%" - color: "#a6adc8"; font.pixelSize: 11 + color: Theme.secondaryForeground; font.pixelSize: 11 } MouseArea { id: pointer; anchors.fill: parent; hoverEnabled: true @@ -215,7 +216,7 @@ Item { } } } - Text { text: "Réseaux enregistrés visibles"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 } + Text { text: "Réseaux enregistrés visibles"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 } Repeater { model: ScriptModel { values: page.sortedNetworks.filter(n => n.known); objectProp: "name" } delegate: ActionButton { @@ -233,7 +234,7 @@ Item { clip: true; ScrollBar.horizontal.policy: ScrollBar.AlwaysOff Column { width: page.width; spacing: 10 - Text { text: page.selectedNetwork ? page.stateLabel(page.selectedNetwork) + " • " + page.securityLabel(page.selectedNetwork) + " • " + Math.round(page.selectedNetwork.signalStrength * 100) + " %" : "Réseau indisponible"; color: "#cdd6f4"; font.pixelSize: 13 } + Text { text: page.selectedNetwork ? page.stateLabel(page.selectedNetwork) + " • " + page.securityLabel(page.selectedNetwork) + " • " + Math.round(page.selectedNetwork.signalStrength * 100) + " %" : "Réseau indisponible"; color: Theme.foreground; font.pixelSize: 13 } Row { spacing: 8 ActionButton { @@ -250,15 +251,15 @@ Item { onClicked: { page.selectedProfile = -1; page.stage = "forget"; } } } - Text { text: "Profils NetworkManager"; color: "#cba6f7"; font.pixelSize: 13; font.bold: true } - Text { visible: page.profiles.length === 0; text: "Aucun profil enregistré"; color: "#a6adc8"; font.pixelSize: 12 } + Text { text: "Profils NetworkManager"; color: Theme.accentForeground; font.pixelSize: 13; font.bold: true } + Text { visible: page.profiles.length === 0; text: "Aucun profil enregistré"; color: Theme.secondaryForeground; font.pixelSize: 12 } Repeater { model: page.profiles delegate: Row { required property var modelData required property int index spacing: 6 - Text { width: 205; text: modelData.id + "\n" + modelData.uuid; color: "#cdd6f4"; font.pixelSize: 11; wrapMode: Text.WrapAnywhere } + Text { width: 205; text: modelData.id + "\n" + modelData.uuid; color: Theme.foreground; font.pixelSize: 11; wrapMode: Text.WrapAnywhere } ActionButton { label: "Utiliser"; onClicked: { page.message = ""; page.selectedNetwork.connectWithSettings(modelData); } } ActionButton { label: "Oublier"; danger: true; onClicked: { page.selectedProfile = index; page.stage = "forget"; } } } @@ -271,12 +272,12 @@ Item { Column { property alias passwordInput: input spacing: 10 - Text { text: "Mot de passe"; color: "#cdd6f4"; font.pixelSize: 13 } + Text { text: "Mot de passe"; color: Theme.foreground; font.pixelSize: 13 } TextField { id: input; width: page.width; echoMode: TextInput.Password placeholderText: "Mot de passe Wi-Fi"; selectByMouse: true - color: "#cdd6f4"; placeholderTextColor: "#6c7086" - background: Rectangle { color: "#313244"; border.color: input.activeFocus ? "#cba6f7" : "#585b70"; radius: 4 } + color: Theme.foreground; placeholderTextColor: Theme.disabledForeground + background: Rectangle { color: Theme.buttonBackground; border.color: input.activeFocus ? Theme.accent : Theme.strongBorder; radius: 4 } onAccepted: submit.clicked() } Row { @@ -303,8 +304,8 @@ Item { id: forgetComponent Column { spacing: 10 - Text { width: page.width; wrapMode: Text.Wrap; text: "Oublier " + page.selectedName + " ?"; color: "#f38ba8"; font.pixelSize: 13 } - Text { width: page.width; wrapMode: Text.Wrap; text: page.selectedProfile >= 0 ? "Seul le profil sélectionné sera supprimé." : "Tous les profils de ce réseau seront supprimés."; color: "#a6adc8"; font.pixelSize: 12 } + Text { width: page.width; wrapMode: Text.Wrap; text: "Oublier " + page.selectedName + " ?"; color: Theme.danger; font.pixelSize: 13 } + Text { width: page.width; wrapMode: Text.Wrap; text: page.selectedProfile >= 0 ? "Seul le profil sélectionné sera supprimé." : "Tous les profils de ce réseau seront supprimés."; color: Theme.secondaryForeground; font.pixelSize: 12 } Row { spacing: 8 ActionButton { label: "Annuler"; onClicked: page.stage = "details" } diff --git a/quickshell/.config/quickshell/labfy-sway/datecenter/DateCenter.qml b/quickshell/.config/quickshell/labfy-sway/datecenter/DateCenter.qml index ba7d1b282..aafbdc006 100644 --- a/quickshell/.config/quickshell/labfy-sway/datecenter/DateCenter.qml +++ b/quickshell/.config/quickshell/labfy-sway/datecenter/DateCenter.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import "../theme" PopupWindow { id: popup @@ -32,8 +33,8 @@ PopupWindow { Rectangle { anchors.fill: parent radius: 4 - color: "#1e1e2e" - border.color: "#45475a" + color: Theme.popupBackground + border.color: Theme.border Column { id: content anchors.left: parent.left @@ -43,7 +44,7 @@ PopupWindow { spacing: 14 Text { text: clock.date.toLocaleDateString(Qt.locale("fr_FR"), "dddd d MMMM") - color: "#cba6f7"; font.pixelSize: 17; font.bold: true + color: Theme.accentForeground; font.pixelSize: 17; font.bold: true } MediaPlayer { width: parent.width } NotificationList { width: parent.width; service: popup.notificationService } diff --git a/quickshell/.config/quickshell/labfy-sway/datecenter/MediaPlayer.qml b/quickshell/.config/quickshell/labfy-sway/datecenter/MediaPlayer.qml index ae0463fef..826ea2376 100644 --- a/quickshell/.config/quickshell/labfy-sway/datecenter/MediaPlayer.qml +++ b/quickshell/.config/quickshell/labfy-sway/datecenter/MediaPlayer.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell.Services.Mpris import "../components" +import "../theme" Rectangle { id: card @@ -22,7 +23,7 @@ Rectangle { implicitHeight: visible ? 116 : 0 height: implicitHeight radius: 4 - color: "#313244" + color: Theme.buttonBackground RowLayout { anchors.fill: parent @@ -33,7 +34,7 @@ Rectangle { Layout.preferredWidth: 72 Layout.preferredHeight: 72 radius: 4 - color: "#45475a" + color: Theme.border clip: true Image { @@ -48,7 +49,7 @@ Rectangle { anchors.centerIn: parent visible: !artwork.visible text: "" - color: "#cba6f7" + color: Theme.accent font.pixelSize: 27 } } @@ -60,7 +61,7 @@ Rectangle { Text { Layout.fillWidth: true text: card.player ? card.player.trackTitle : "" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 13 font.bold: true elide: Text.ElideRight @@ -69,7 +70,7 @@ Rectangle { Layout.fillWidth: true text: card.player ? card.player.trackArtist : "" visible: text.length > 0 - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 11 elide: Text.ElideRight } @@ -92,12 +93,12 @@ Rectangle { width: 32 height: 27 radius: 4 - color: controlPointer.containsMouse && available ? "#585b70" : "#45475a" + color: controlPointer.containsMouse && available ? Theme.strongBorder : Theme.border NerdIcon { anchors.centerIn: parent text: modelData.icon - color: parent.available ? "#cdd6f4" : "#6c7086" + color: parent.available ? Theme.foreground : Theme.disabledForeground font.pixelSize: 14 } MouseArea { diff --git a/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationItem.qml b/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationItem.qml index e977c9939..12cfdc1e0 100644 --- a/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationItem.qml +++ b/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationItem.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "../components" +import "../theme" Rectangle { id: card @@ -19,7 +20,7 @@ Rectangle { implicitHeight: bodyText.visible || image.visible || actions.visible ? 95 + image.height + actions.height : 64 height: implicitHeight radius: 4 - color: "#313244" + color: Theme.buttonBackground Column { anchors.fill: parent; anchors.margins: 10; spacing: 4 Row { @@ -27,21 +28,21 @@ Rectangle { Item { width: 14; height: 14 Image { id: appIcon; anchors.fill: parent; source: card.iconSource; fillMode: Image.PreserveAspectFit; visible: status === Image.Ready } - NerdIcon { anchors.centerIn: parent; visible: !appIcon.visible; text: ""; color: "#cba6f7"; font.pixelSize: 13 } + NerdIcon { anchors.centerIn: parent; visible: !appIcon.visible; text: ""; color: Theme.accent; font.pixelSize: 13 } } Text { width: parent.width - 48 text: card.notification.appName || card.notification.desktopEntry || "Notification" - textFormat: Text.PlainText; color: "#a6adc8"; font.pixelSize: 11; elide: Text.ElideRight + textFormat: Text.PlainText; color: Theme.secondaryForeground; font.pixelSize: 11; elide: Text.ElideRight } - Text { visible: !!card.live; text: "●"; color: "#a6e3a1"; font.pixelSize: 10 } + Text { visible: !!card.live; text: "●"; color: Theme.successForeground; font.pixelSize: 10 } NerdIcon { - text: ""; color: closePointer.containsMouse ? "#f38ba8" : "#a6adc8"; font.pixelSize: 12 + text: ""; color: closePointer.containsMouse ? Theme.danger : Theme.secondaryForeground; font.pixelSize: 12 MouseArea { id: closePointer; anchors.fill: parent; anchors.margins: -5; hoverEnabled: true; onClicked: card.service.dismiss(card.notification.internalId) } } } - Text { width: parent.width; text: card.notification.summary; textFormat: Text.PlainText; color: "#cdd6f4"; font.pixelSize: 12; font.bold: true; elide: Text.ElideRight } - Text { id: bodyText; width: parent.width; visible: text.length > 0; text: card.notification.body || ""; textFormat: Text.PlainText; color: "#bac2de"; font.pixelSize: 11; wrapMode: Text.Wrap; maximumLineCount: 2; elide: Text.ElideRight } + Text { width: parent.width; text: card.notification.summary; textFormat: Text.PlainText; color: Theme.foreground; font.pixelSize: 12; font.bold: true; elide: Text.ElideRight } + Text { id: bodyText; width: parent.width; visible: text.length > 0; text: card.notification.body || ""; textFormat: Text.PlainText; color: Theme.subtext1; font.pixelSize: 11; wrapMode: Text.Wrap; maximumLineCount: 2; elide: Text.ElideRight } Image { id: image; width: parent.width; height: status === Image.Ready ? Math.min(110, implicitHeight) : 0; visible: status === Image.Ready; source: card.notification.image || ""; fillMode: Image.PreserveAspectFit } Flow { id: actions @@ -53,8 +54,8 @@ Rectangle { model: card.live ? card.live.actions : [] delegate: Rectangle { required property var modelData - width: label.implicitWidth + 16; height: 24; radius: 4; color: "#45475a" - Text { id: label; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: "#cdd6f4"; font.pixelSize: 11 } + width: label.implicitWidth + 16; height: 24; radius: 4; color: Theme.border + Text { id: label; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: Theme.foreground; font.pixelSize: 11 } MouseArea { anchors.fill: parent; onClicked: modelData.invoke() } } } diff --git a/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationList.qml b/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationList.qml index 07827e6a0..a23649398 100644 --- a/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationList.qml +++ b/quickshell/.config/quickshell/labfy-sway/datecenter/NotificationList.qml @@ -1,4 +1,5 @@ import QtQuick +import "../theme" Item { id: list @@ -10,12 +11,12 @@ Item { implicitHeight: rows.length ? 28 + Math.min(307, rows.length * 94) : 80 height: implicitHeight Timer { interval: 5000; running: list.confirmClear; onTriggered: list.confirmClear = false } - Text { id: title; text: "Notifications"; color: "#cdd6f4"; font.pixelSize: 14; font.bold: true } + Text { id: title; text: "Notifications"; color: Theme.foreground; font.pixelSize: 14; font.bold: true } Text { anchors.right: parent.right text: list.confirmClear ? "Confirmer : tout effacer" : "Tout effacer" visible: list.rows.length > 0 - color: confirm.containsMouse ? "#f38ba8" : "#a6adc8" + color: confirm.containsMouse ? Theme.danger : Theme.secondaryForeground font.pixelSize: 11 MouseArea { id: confirm @@ -31,7 +32,7 @@ Item { visible: list.rows.length === 0 anchors.top: title.bottom; anchors.topMargin: 8 text: "Aucune notification récente" - color: "#a6adc8"; font.pixelSize: 12 + color: Theme.secondaryForeground; font.pixelSize: 12 } ListView { anchors.top: title.bottom; anchors.topMargin: 8 diff --git a/quickshell/.config/quickshell/labfy-sway/modules/Clock.qml b/quickshell/.config/quickshell/labfy-sway/modules/Clock.qml index aa216a79b..71c1ae21d 100644 --- a/quickshell/.config/quickshell/labfy-sway/modules/Clock.qml +++ b/quickshell/.config/quickshell/labfy-sway/modules/Clock.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "../notifications" +import "../theme" Rectangle { id: clockCapsule @@ -12,7 +13,7 @@ Rectangle { width: 184 height: 26 radius: 4 - color: open ? "#cba6f7" : pointer.containsMouse ? "#45475a" : "#313244" + color: open ? Theme.accent : pointer.containsMouse ? Theme.border : Theme.buttonBackground Accessible.role: Accessible.Button Accessible.name: unreadCount === 0 ? "Date et heure" : "Date et heure, " + unreadCount + (unreadCount === 1 @@ -28,7 +29,7 @@ Rectangle { // CONTRACT: le centre du texte coïncide avec celui de la barre, sans dépendre du badge. anchors.centerIn: parent text: Qt.formatDateTime(clock.date, "dd/MM HH:mm") - color: clockCapsule.open ? "#1e1e2e" : "#cdd6f4" + color: clockCapsule.open ? Theme.onAccent : Theme.foreground font.pixelSize: 13 font.weight: Font.Bold } @@ -63,15 +64,15 @@ Rectangle { Rectangle { anchors.fill: parent radius: 4 - color: "#313244" - border.color: "#45475a" + color: Theme.buttonBackground + border.color: Theme.border Text { id: tooltipText anchors.centerIn: parent text: clockCapsule.unreadCount === 0 ? "Date et notifications" : clockCapsule.unreadCount + (clockCapsule.unreadCount === 1 ? " notification non lue" : " notifications non lues") - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 11 } } diff --git a/quickshell/.config/quickshell/labfy-sway/modules/Workspaces.qml b/quickshell/.config/quickshell/labfy-sway/modules/Workspaces.qml index 8459148d1..af93f61d7 100644 --- a/quickshell/.config/quickshell/labfy-sway/modules/Workspaces.qml +++ b/quickshell/.config/quickshell/labfy-sway/modules/Workspaces.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.I3 import "../status" +import "../theme" Row { id: workspaces @@ -58,9 +59,9 @@ Row { width: button.active ? 24 : 16 height: button.active ? 16 : 11 radius: 4 - color: button.urgent ? "#fab387" - : button.active ? "#cba6f7" - : pointer.containsMouse ? "#6c7086" : "#585b70" + color: button.urgent ? Theme.urgent + : button.active ? Theme.accent + : pointer.containsMouse ? Theme.disabledForeground : Theme.strongBorder } MouseArea { diff --git a/quickshell/.config/quickshell/labfy-sway/notifications/NotificationToast.qml b/quickshell/.config/quickshell/labfy-sway/notifications/NotificationToast.qml index c1ff4fd1d..8f465efec 100644 --- a/quickshell/.config/quickshell/labfy-sway/notifications/NotificationToast.qml +++ b/quickshell/.config/quickshell/labfy-sway/notifications/NotificationToast.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Services.Notifications import "../components" +import "../theme" Rectangle { id: card @@ -23,8 +24,8 @@ Rectangle { width: 370 implicitHeight: content.implicitHeight + 20 radius: 4 - color: "#1e1e2e" - border.color: notification.urgency === NotificationUrgency.Critical ? "#f38ba8" : "#45475a" + color: Theme.popupBackground + border.color: notification.urgency === NotificationUrgency.Critical ? Theme.danger : Theme.border // CONTRACT: 0 interdit l'expiration automatique. Les durées sont en ms. Timer { interval: Math.max(1, card.timeout) @@ -50,23 +51,23 @@ Rectangle { fillMode: Image.PreserveAspectFit visible: status === Image.Ready } - NerdIcon { anchors.centerIn: parent; visible: !icon.visible; text: ""; color: "#cba6f7" } + NerdIcon { anchors.centerIn: parent; visible: !icon.visible; text: ""; color: Theme.accent } } Text { width: parent.width - 48 text: card.notification.appName || "Notification" textFormat: Text.PlainText - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 11 elide: Text.ElideRight } NerdIcon { - text: ""; color: "#a6adc8" + text: ""; color: Theme.secondaryForeground MouseArea { anchors.fill: parent; anchors.margins: -4; onClicked: card.service.dismiss(card.entry.internalId) } } } - Text { width: parent.width; text: card.notification.summary; textFormat: Text.PlainText; color: "#cdd6f4"; font.bold: true; wrapMode: Text.Wrap; maximumLineCount: 2 } - Text { width: parent.width; visible: text.length > 0; text: card.notification.body; textFormat: Text.PlainText; color: "#bac2de"; font.pixelSize: 12; wrapMode: Text.Wrap; maximumLineCount: 3; elide: Text.ElideRight } + Text { width: parent.width; text: card.notification.summary; textFormat: Text.PlainText; color: Theme.foreground; font.bold: true; wrapMode: Text.Wrap; maximumLineCount: 2 } + Text { width: parent.width; visible: text.length > 0; text: card.notification.body; textFormat: Text.PlainText; color: Theme.subtext1; font.pixelSize: 12; wrapMode: Text.Wrap; maximumLineCount: 3; elide: Text.ElideRight } Image { width: parent.width; height: status === Image.Ready ? Math.min(120, implicitHeight) : 0 visible: status === Image.Ready @@ -80,8 +81,8 @@ Rectangle { model: card.notification.actions delegate: Rectangle { required property var modelData - width: actionLabel.implicitWidth + 16; height: 25; radius: 4; color: "#45475a" - Text { id: actionLabel; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: "#cdd6f4"; font.pixelSize: 11 } + width: actionLabel.implicitWidth + 16; height: 25; radius: 4; color: Theme.border + Text { id: actionLabel; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: Theme.foreground; font.pixelSize: 11 } MouseArea { anchors.fill: parent; onClicked: modelData.invoke() } } } diff --git a/quickshell/.config/quickshell/labfy-sway/notifications/UnreadIndicator.qml b/quickshell/.config/quickshell/labfy-sway/notifications/UnreadIndicator.qml index 68878e9d7..e1f926be4 100644 --- a/quickshell/.config/quickshell/labfy-sway/notifications/UnreadIndicator.qml +++ b/quickshell/.config/quickshell/labfy-sway/notifications/UnreadIndicator.qml @@ -1,5 +1,6 @@ import QtQuick import "../components" +import "../theme" Item { id: indicator @@ -19,16 +20,16 @@ Item { id: bell // Le glyphe fa-bell est présent dans JetBrainsMono Nerd Font Mono. text: "" - color: indicator.inverted ? "#1e1e2e" - : indicator.criticalUnreadCount > 0 ? "#f38ba8" : "#cba6f7" + color: indicator.inverted ? Theme.onAccent + : indicator.criticalUnreadCount > 0 ? Theme.danger : Theme.accent // CONTRACT: la cloche gagne en lisibilité sans agrandir le badge ni déplacer la date. font.pixelSize: 16 } Text { id: countLabel text: indicator.unreadCount > 9 ? "9+" : String(indicator.unreadCount) - color: indicator.inverted ? "#1e1e2e" - : indicator.criticalUnreadCount > 0 ? "#f38ba8" : "#cdd6f4" + color: indicator.inverted ? Theme.onAccent + : indicator.criticalUnreadCount > 0 ? Theme.danger : Theme.foreground font.pixelSize: 10 font.bold: true } diff --git a/quickshell/.config/quickshell/labfy-sway/shell.qml b/quickshell/.config/quickshell/labfy-sway/shell.qml index 088f2a4a9..d99a70e88 100644 --- a/quickshell/.config/quickshell/labfy-sway/shell.qml +++ b/quickshell/.config/quickshell/labfy-sway/shell.qml @@ -1,8 +1,24 @@ //@ pragma UseQApplication import Quickshell +import Quickshell.Io import "notifications" +import "theme" ShellRoot { + // CONTRACT: seul ce point d'entrée reçoit les changements live du backend. + // Aucun état de thème n'est écrit par Theme.qml. + IpcHandler { + target: "appearance" + function setTheme(flavor: string, accent: string): bool { + return AppearanceController.applyTheme(flavor, accent); + } + function currentTheme(): string { + return AppearanceController.effectiveFlavor + "/" + AppearanceController.effectiveAccent; + } + function effectiveState(): string { + return AppearanceController.effectiveState(); + } + } // Une barre par écran, y compris si les sorties changent pendant la session. // CONTRACT: un seul serveur D-Bus pour toutes les sorties. NotificationService { id: rootNotificationService } diff --git a/quickshell/.config/quickshell/labfy-sway/status/BatteryIndicator.qml b/quickshell/.config/quickshell/labfy-sway/status/BatteryIndicator.qml index caa12b8ed..88d35165b 100644 --- a/quickshell/.config/quickshell/labfy-sway/status/BatteryIndicator.qml +++ b/quickshell/.config/quickshell/labfy-sway/status/BatteryIndicator.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell.Io import Quickshell.Services.UPower import "../components" +import "../theme" Item { id: indicator @@ -42,7 +43,7 @@ Item { onFileChanged: reload() } - Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? "#45475a" : "transparent" } + Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? Theme.border : "transparent" } NerdIcon { id: icon width: 18 @@ -57,8 +58,8 @@ Item { : indicator.percent <= 30 ? "󰁻" : indicator.percent <= 50 ? "󰁽" : indicator.percent <= 75 ? "󰁿" : "󰁹" - color: indicator.percent <= 15 ? "#f38ba8" - : indicator.percent <= 30 ? "#fab387" : "#cdd6f4" + color: indicator.percent <= 15 ? Theme.danger + : indicator.percent <= 30 ? Theme.urgentForeground : Theme.foreground } Text { id: value diff --git a/quickshell/.config/quickshell/labfy-sway/status/BluetoothIndicator.qml b/quickshell/.config/quickshell/labfy-sway/status/BluetoothIndicator.qml index 863253b74..93ca9a26f 100644 --- a/quickshell/.config/quickshell/labfy-sway/status/BluetoothIndicator.qml +++ b/quickshell/.config/quickshell/labfy-sway/status/BluetoothIndicator.qml @@ -1,5 +1,6 @@ import QtQuick import "../components" +import "../theme" Item { id: indicator @@ -16,14 +17,14 @@ Item { : "Bluetooth activé\n" + connectedDevices.length + " appareils connectés\n" + connectedDevices.map(device => device.name).join("\n") - Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? "#45475a" : "transparent" } + Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? Theme.border : "transparent" } NerdIcon { anchors.centerIn: parent // WHY: la silhouette Bluetooth est étroite ; seul le dessin grandit, pas la hitbox. font.pixelSize: 22 text: indicator.adapter && indicator.adapter.enabled ? "" : "󰂲" - color: !indicator.adapter || !indicator.adapter.enabled ? "#a6adc8" - : indicator.connectedDevices.length > 0 ? "#cba6f7" : "#cdd6f4" + color: !indicator.adapter || !indicator.adapter.enabled ? Theme.secondaryForeground + : indicator.connectedDevices.length > 0 ? Theme.accent : Theme.foreground } MouseArea { id: pointer diff --git a/quickshell/.config/quickshell/labfy-sway/status/NetworkIndicator.qml b/quickshell/.config/quickshell/labfy-sway/status/NetworkIndicator.qml index 3b2fa3685..25f599ecd 100644 --- a/quickshell/.config/quickshell/labfy-sway/status/NetworkIndicator.qml +++ b/quickshell/.config/quickshell/labfy-sway/status/NetworkIndicator.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell.Networking import "../components" +import "../theme" Item { id: indicator @@ -19,7 +20,7 @@ Item { : !connectedNetwork ? "Wi-Fi\nNon connecté" : "Wi-Fi\n" + connectedNetwork.name + "\nSignal : " + signalPercent + " %" - Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? "#45475a" : "transparent" } + Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? Theme.border : "transparent" } NerdIcon { anchors.centerIn: parent // WHY: md-wifi remplit mieux la hauteur visuelle que les quatre glyphes @@ -30,8 +31,8 @@ Item { text: !Networking.wifiHardwareEnabled ? "󰖪" : !Networking.wifiEnabled ? "󰖪" : !indicator.connectedNetwork ? "󰤯" : "󰖩" - color: !Networking.wifiHardwareEnabled ? "#f38ba8" - : !Networking.wifiEnabled ? "#a6adc8" : "#cdd6f4" + color: !Networking.wifiHardwareEnabled ? Theme.danger + : !Networking.wifiEnabled ? Theme.secondaryForeground : Theme.foreground } MouseArea { id: pointer diff --git a/quickshell/.config/quickshell/labfy-sway/status/StatusTooltip.qml b/quickshell/.config/quickshell/labfy-sway/status/StatusTooltip.qml index 97ca5bc60..ded82490d 100644 --- a/quickshell/.config/quickshell/labfy-sway/status/StatusTooltip.qml +++ b/quickshell/.config/quickshell/labfy-sway/status/StatusTooltip.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import "../theme" PopupWindow { id: tip @@ -25,15 +26,15 @@ PopupWindow { Rectangle { anchors.fill: parent radius: 4 - color: "#313244" - border.color: "#45475a" + color: Theme.buttonBackground + border.color: Theme.border Text { id: content anchors.centerIn: parent width: Math.min(304, implicitWidth) text: tip.message wrapMode: Text.Wrap - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 11 } } diff --git a/quickshell/.config/quickshell/labfy-sway/status/TrayItem.qml b/quickshell/.config/quickshell/labfy-sway/status/TrayItem.qml index 40d559f60..c7cc9e875 100644 --- a/quickshell/.config/quickshell/labfy-sway/status/TrayItem.qml +++ b/quickshell/.config/quickshell/labfy-sway/status/TrayItem.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Services.SystemTray import Quickshell.Widgets +import "../theme" Item { id: entry @@ -14,7 +15,7 @@ Item { return heading + (item.tooltipDescription ? "\n" + item.tooltipDescription : ""); } - Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? "#45475a" : "transparent" } + Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? Theme.border : "transparent" } IconImage { anchors.centerIn: parent width: 18 @@ -25,7 +26,7 @@ Item { Rectangle { visible: entry.item.status === Status.NeedsAttention width: 4; height: 4; radius: 2 - color: "#cba6f7" + color: Theme.accent anchors.right: parent.right anchors.top: parent.top } diff --git a/quickshell/.config/quickshell/labfy-sway/status/UpdatesIndicator.qml b/quickshell/.config/quickshell/labfy-sway/status/UpdatesIndicator.qml index 9711a1524..64e4f9f59 100644 --- a/quickshell/.config/quickshell/labfy-sway/status/UpdatesIndicator.qml +++ b/quickshell/.config/quickshell/labfy-sway/status/UpdatesIndicator.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Io import "../components" +import "../theme" Item { id: indicator @@ -46,7 +47,7 @@ Item { running: !snapshot.loaded onTriggered: snapshot.reload() } - Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? "#45475a" : "transparent" } + Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? Theme.border : "transparent" } NerdIcon { id: icon width: 18 @@ -56,7 +57,7 @@ Item { anchors.leftMargin: 3 anchors.verticalCenter: parent.verticalCenter text: indicator.hasError ? "" : "󰚰" - color: indicator.hasError ? "#fab387" : "#cba6f7" + color: indicator.hasError ? Theme.urgentForeground : Theme.accent } Text { id: count @@ -65,7 +66,7 @@ Item { anchors.verticalCenter: parent.verticalCenter visible: !indicator.hasError text: indicator.state.totalUpdates - color: "#cba6f7" + color: Theme.accent font.family: "JetBrainsMono Nerd Font Mono" font.pixelSize: 12 } diff --git a/quickshell/.config/quickshell/labfy-sway/systemmonitor/MetricRow.qml b/quickshell/.config/quickshell/labfy-sway/systemmonitor/MetricRow.qml index 2b4e5f250..9721d228d 100644 --- a/quickshell/.config/quickshell/labfy-sway/systemmonitor/MetricRow.qml +++ b/quickshell/.config/quickshell/labfy-sway/systemmonitor/MetricRow.qml @@ -1,25 +1,26 @@ import QtQuick +import "../theme" Column { id: row required property string label required property string value required property real percentage - property string accent: "#cba6f7" + property color accent: Theme.mauve width: parent.width spacing: 6 Row { width: parent.width - Text { id: labelText; text: row.label; color: "#cdd6f4"; font.pixelSize: 12 } + Text { id: labelText; text: row.label; color: Theme.foreground; font.pixelSize: 12 } Item { width: Math.max(0, parent.width - valueText.width - labelText.width); height: 1 } - Text { id: valueText; text: row.value; color: "#bac2de"; font.pixelSize: 12 } + Text { id: valueText; text: row.value; color: Theme.subtext1; font.pixelSize: 12 } } Rectangle { width: parent.width height: 5 radius: 3 - color: "#313244" + color: Theme.buttonBackground Rectangle { width: parent.width * Math.max(0, Math.min(100, row.percentage)) / 100 height: parent.height diff --git a/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitor.qml b/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitor.qml index 5252e4d63..a30701650 100644 --- a/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitor.qml +++ b/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitor.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import "../components" +import "../theme" PopupWindow { id: popup @@ -24,8 +25,8 @@ PopupWindow { Rectangle { anchors.fill: parent radius: 4 - color: "#1e1e2e" - border.color: "#45475a" + color: Theme.popupBackground + border.color: Theme.border Column { id: content @@ -38,7 +39,7 @@ PopupWindow { Row { spacing: 8 NerdIcon { text: "󰍛"; font.pixelSize: 18 } - Text { text: "Moniteur système"; color: "#cdd6f4"; font.pixelSize: 16; font.bold: true } + Text { text: "Moniteur système"; color: Theme.foreground; font.pixelSize: 16; font.bold: true } } MetricRow { @@ -46,7 +47,7 @@ PopupWindow { percentage: metrics.cpuPercent value: (metrics.cpuPercent >= 0 ? Math.round(metrics.cpuPercent) + " %" : "—") + (metrics.cpuTemp >= 0 ? " " + Math.round(metrics.cpuTemp) + " °C" : "") - accent: metrics.cpuPercent >= 90 ? "#f38ba8" : metrics.cpuPercent >= 70 ? "#fab387" : "#cba6f7" + accent: metrics.cpuPercent >= 90 ? Theme.danger : metrics.cpuPercent >= 70 ? Theme.urgent : Theme.mauve } MetricRow { @@ -55,7 +56,7 @@ PopupWindow { value: metrics.ramPercent >= 0 ? metrics.ramUsedGiB.toFixed(1) + " / " + metrics.ramTotalGiB.toFixed(1) + " GiB " + Math.round(metrics.ramPercent) + " %" : "—" - accent: metrics.ramPercent >= 90 ? "#f38ba8" : metrics.ramPercent >= 75 ? "#fab387" : "#cba6f7" + accent: metrics.ramPercent >= 90 ? Theme.danger : metrics.ramPercent >= 75 ? Theme.urgent : Theme.mauve } MetricRow { @@ -77,15 +78,15 @@ PopupWindow { Row { width: parent.width - Text { id: loadLabel; text: "Charge (1/5/15 min)"; color: "#cdd6f4"; font.pixelSize: 12 } + Text { id: loadLabel; text: "Charge (1/5/15 min)"; color: Theme.foreground; font.pixelSize: 12 } Item { width: Math.max(0, parent.width - loadValue.width - loadLabel.width); height: 1 } - Text { id: loadValue; text: metrics.loadAverage || "—"; color: "#bac2de"; font.pixelSize: 12 } + Text { id: loadValue; text: metrics.loadAverage || "—"; color: Theme.subtext1; font.pixelSize: 12 } } Row { width: parent.width - Text { id: uptimeLabel; text: "En marche"; color: "#cdd6f4"; font.pixelSize: 12 } + Text { id: uptimeLabel; text: "En marche"; color: Theme.foreground; font.pixelSize: 12 } Item { width: Math.max(0, parent.width - uptimeValue.width - uptimeLabel.width); height: 1 } - Text { id: uptimeValue; text: metrics.uptimeText || "—"; color: "#bac2de"; font.pixelSize: 12 } + Text { id: uptimeValue; text: metrics.uptimeText || "—"; color: Theme.subtext1; font.pixelSize: 12 } } } } diff --git a/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitorButton.qml b/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitorButton.qml index da8ce9203..ee6b827eb 100644 --- a/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitorButton.qml +++ b/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMonitorButton.qml @@ -1,6 +1,7 @@ import QtQuick import "../components" import "../status" +import "../theme" Rectangle { id: button @@ -10,14 +11,14 @@ Rectangle { width: 26 height: 26 radius: 4 - color: open ? "#cba6f7" : pointer.containsMouse ? "#45475a" : "#313244" + color: open ? Theme.accent : pointer.containsMouse ? Theme.border : Theme.buttonBackground NerdIcon { anchors.centerIn: parent text: "󰍛" // WHY: compenser la surface plus faible du glyphe sans agrandir le bouton. font.pixelSize: 20 - color: button.open ? "#1e1e2e" : "#cdd6f4" + color: button.open ? Theme.onAccent : Theme.foreground } MouseArea { id: pointer diff --git a/quickshell/.config/quickshell/labfy-sway/theme/AppearanceController.qml b/quickshell/.config/quickshell/labfy-sway/theme/AppearanceController.qml new file mode 100644 index 000000000..90225bd07 --- /dev/null +++ b/quickshell/.config/quickshell/labfy-sway/theme/AppearanceController.qml @@ -0,0 +1,60 @@ +pragma Singleton +import Quickshell +import Quickshell.Io + +Singleton { + id: appearance + // CONTRACT: état effectif unique du bureau. Les futurs adaptateurs ne + // déduisent jamais Light/Dark du nom de flavor ; ils lisent effectiveDark. + // 17B ne définit que le mode normal ; les décisions Sun arrivent après. + readonly property var flavors: ["latte", "frappe", "macchiato", "mocha"] + readonly property var accents: ["rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"] + // CONTRACT: le backend écrit cet état atomiquement après validation Sway. + // Le repli Mocha fonctionne avant la première génération ; aucune lecture + // par frame ni polling. Les futurs adaptateurs lisent cet état via l'IPC. + FileView { + id: stateFile + path: (Quickshell.env("XDG_STATE_HOME") || Quickshell.env("HOME") + "/.local/state") + + "/labfy-appearance/effective.json" + blockLoading: true + printErrors: false + } + function initialState() { + try { + const value = JSON.parse(stateFile.text()); + if (value.version === 1 && flavors.includes(value.effectiveFlavor) + && accents.includes(value.effectiveAccent) + && value.effectiveMode === "normal") return value; + } catch (_) {} + return { effectiveFlavor: "mocha", effectiveAccent: "lavender", revision: 0 }; + } + readonly property var initial: initialState() + property string effectiveFlavor: initial.effectiveFlavor + property string effectiveMode: "normal" + readonly property bool effectiveDark: effectiveFlavor !== "latte" + property bool effectiveHighContrast: false + property string effectiveAccent: initial.effectiveAccent + property int revision: Number.isSafeInteger(initial.revision) && initial.revision >= 0 + ? initial.revision : 0 + + // INVARIANT: une paire refusée ne change aucun champ ni la révision. + // La révision avance une seule fois après tout changement effectif. + function applyTheme(flavor, accent) { + if (!flavors.includes(flavor) || !accents.includes(accent)) return false; + if (effectiveFlavor === flavor && effectiveAccent === accent + && effectiveMode === "normal" && !effectiveHighContrast) return true; + effectiveFlavor = flavor; + effectiveAccent = accent; + effectiveMode = "normal"; + effectiveHighContrast = false; + revision++; + return true; + } + + function effectiveState() { + return JSON.stringify({ effectiveFlavor: effectiveFlavor, + effectiveMode: effectiveMode, effectiveDark: effectiveDark, + effectiveHighContrast: effectiveHighContrast, + effectiveAccent: effectiveAccent, revision: revision }); + } +} diff --git a/quickshell/.config/quickshell/labfy-sway/theme/README.md b/quickshell/.config/quickshell/labfy-sway/theme/README.md new file mode 100644 index 000000000..3f8f9375e --- /dev/null +++ b/quickshell/.config/quickshell/labfy-sway/theme/README.md @@ -0,0 +1,68 @@ +# Moteur d'apparence 17B + +`catppuccin.json` est la **seule source versionnée complète** des quatre +palettes. Les 26 valeurs de chaque flavor ont été importées sans modification +depuis [Catppuccin Palette v1.8.0](https://github.com/catppuccin/palette/blob/v1.8.0/palette.json), +révision `07d02aa110ef9eb7e7427afca5c73ba9cf7f8ebd`, le 4 octobre 2026. +Le JSON local conserve seulement les noms et valeurs hex, sans les métadonnées +RGB/HSL/OKLCH de l'amont. + +```text +catppuccin.json (versionné) + → AppearanceController.qml (API live, sans persistance) + → Theme.qml (singleton effectif, rôles UI) + → composants QuickShell + +catppuccin.json (versionné) + → generate-appearance.py (versionné) + → generated/theme.conf (runtime, ignoré par Git) + → Sway/SwayFX +``` + +`Theme.qml` lit le JSON une fois au chargement par `FileView` avec +`blockLoading: true`, sans processus externe, relecture par frame ou polling. +`AppearanceController.qml` détient l'état effectif du bureau : +`effectiveFlavor`, `effectiveMode`, `effectiveDark`, +`effectiveHighContrast`, `effectiveAccent`, `revision`. Le backend persiste +l'état effectif dans `$XDG_STATE_HOME/labfy-appearance/effective.json` avec +écriture atomique, puis le contrôleur le relit au démarrage. En 17B, +`effectiveMode` vaut `normal`, `effectiveHighContrast` vaut `false`, et la +révision augmente une fois par changement accepté. `effectiveDark` est +publié par le contrôleur pour les futurs consommateurs GTK, Qt, Kitty, +Neovim, Firefox et autres ; ces applications n'auront pas à déduire le mode +clair/sombre de la palette. Sun Light et Sun Dark viendront plus tard. +`Theme.qml` consomme cet état et expose les couleurs et rôles QuickShell. +Le flavor initial est Mocha et l'accent initial Lavender. `panelOpacity = 235/255` +recrée le fond de barre historique ; un profil ultérieur peut le rendre opaque. +`onAccent` choisit noir ou blanc technique par luminance WCAG. Lavender Latte +n'atteint pas 4,5:1 avec une couleur Catppuccin ; le noir atteint environ +6,6:1. Le texte nu accentué prend `text` en Latte pour rester lisible. + +Les 34 usages historiques de Mauve ont été classés par rôle : sélection, +focus, indicateur ou contrôle utilisent maintenant l'accent principal +Lavender. Les séries CPU/RAM du System Monitor restent intentionnellement +Mauve. Ce choix rend les contrôles historiques Mauve légèrement plus bleus +qu'avant. Les trois anciens Lavender restent l'accent principal. + +L'API pour 17C est `AppearanceController.applyTheme(flavor, accent)` ; l'IPC +`qs -c labfy-sway ipc call appearance effectiveState` expose l'état complet +et `qs -c labfy-sway ipc call appearance setTheme mocha lavender` change +QuickShell en direct. La commande unifiée depuis le dépôt est : + +```sh +python bin/.local/bin/generate-appearance.py latte --accent lavender --apply +``` + +Elle écrit atomiquement le fichier runtime Sway, valide la configuration, +recharge Sway, persiste l'état effectif, puis met à jour QuickShell. En cas +d'échec elle restaure la dernière génération valide, l'état persistant et +l'état QuickShell précédent. Aucune décision automatique de profil ni gestion +du wallpaper n'existe encore dans le QML. +Le repli versionné `sway/theme-default.conf` fournit Mocha même avant toute +génération. Ses quelques variables sont un artefact dérivé, pas une palette +complète indépendante. `sway/style` inclut le repli, puis un glob de fichier +runtime ; Sway accepte ce glob vide après un nouveau clone/Stow. + +Les ombres `#00000055` et `#00000035` sont des valeurs de profil structurelles, +pas des couleurs Catppuccin. La transparence inactive reste 0,85 ; le +générateur fournit une variable Sway pour le futur profil Sun Mode. diff --git a/quickshell/.config/quickshell/labfy-sway/theme/Theme.qml b/quickshell/.config/quickshell/labfy-sway/theme/Theme.qml new file mode 100644 index 000000000..e8a015c9b --- /dev/null +++ b/quickshell/.config/quickshell/labfy-sway/theme/Theme.qml @@ -0,0 +1,96 @@ +pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: theme + + // CONTRACT: la palette officielle versionnée est l'unique source des quatre flavors. + // Le chargement bloquant unique précède l'évaluation des couleurs des fenêtres. + FileView { + id: paletteFile + path: Qt.resolvedUrl("catppuccin.json") + blockLoading: true + } + readonly property var palettes: JSON.parse(paletteFile.text()) + readonly property string flavor: AppearanceController.effectiveFlavor + readonly property string accentName: AppearanceController.effectiveAccent + readonly property var palette: palettes[flavor] + + function setFlavor(value) { + return AppearanceController.applyTheme(value, accentName); + } + function setAccent(value) { + return AppearanceController.applyTheme(flavor, value); + } + readonly property var accentNames: AppearanceController.accents + + readonly property color rosewater: palette.rosewater + readonly property color flamingo: palette.flamingo + readonly property color pink: palette.pink + readonly property color mauve: palette.mauve + readonly property color red: palette.red + readonly property color maroon: palette.maroon + readonly property color peach: palette.peach + readonly property color yellow: palette.yellow + readonly property color green: palette.green + readonly property color teal: palette.teal + readonly property color sky: palette.sky + readonly property color sapphire: palette.sapphire + readonly property color blue: palette.blue + readonly property color lavender: palette.lavender + readonly property color text: palette.text + readonly property color subtext1: palette.subtext1 + readonly property color subtext0: palette.subtext0 + readonly property color overlay2: palette.overlay2 + readonly property color overlay1: palette.overlay1 + readonly property color overlay0: palette.overlay0 + readonly property color surface2: palette.surface2 + readonly property color surface1: palette.surface1 + readonly property color surface0: palette.surface0 + readonly property color base: palette.base + readonly property color mantle: palette.mantle + readonly property color crust: palette.crust + + // CONTRACT: l'alpha de la barre est un profil UI, jamais une entrée Catppuccin. + property real panelOpacity: 235 / 255 + readonly property color panelBackground: Qt.rgba(base.r, base.g, base.b, panelOpacity) + readonly property color popupBackground: base + readonly property color buttonBackground: surface0 + readonly property color buttonHover: surface1 + readonly property color buttonPressed: surface2 + readonly property color foreground: text + // Latte sur surface0 a besoin d'un texte plus sombre que subtext0. + readonly property color secondaryForeground: flavor === "latte" ? text : subtext0 + readonly property color disabledForeground: flavor === "latte" ? subtext1 : overlay0 + readonly property color border: surface1 + readonly property color strongBorder: surface2 + readonly property color accent: palette[accentName] + // WHY: Lavender Latte n'offre aucun texte Catppuccin à contraste 4.5:1. + // Choisir le noir ou le blanc technique par contraste WCAG pour chaque accent. + function luminance(c) { + const channel = x => x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); + return 0.2126 * channel(c.r) + 0.7152 * channel(c.g) + 0.0722 * channel(c.b); + } + function contrastingText(background) { + return luminance(background) > 0.179 ? "#000000" : "#ffffff"; + } + readonly property color onAccent: contrastingText(accent) + // Latte Lavender ne contraste pas assez avec base pour du texte nu. + readonly property color accentForeground: flavor === "latte" ? text : accent + readonly property color focus: accent + readonly property color urgent: peach + readonly property color warning: yellow + readonly property color warningForeground: flavor === "latte" ? text : yellow + readonly property color success: green + readonly property color danger: red + readonly property color urgentForeground: flavor === "latte" ? text : peach + readonly property color successForeground: flavor === "latte" ? text : green + readonly property color shadow: "#00000055" + readonly property color separator: surface1 + readonly property color inputBackground: surface0 + readonly property color inputBorder: surface2 + readonly property color selectedBackground: accent + readonly property color selectedForeground: onAccent +} diff --git a/quickshell/.config/quickshell/labfy-sway/theme/catppuccin.json b/quickshell/.config/quickshell/labfy-sway/theme/catppuccin.json new file mode 100644 index 000000000..86ede44ab --- /dev/null +++ b/quickshell/.config/quickshell/labfy-sway/theme/catppuccin.json @@ -0,0 +1,114 @@ +{ + "latte": { + "rosewater": "#dc8a78", + "flamingo": "#dd7878", + "pink": "#ea76cb", + "mauve": "#8839ef", + "red": "#d20f39", + "maroon": "#e64553", + "peach": "#fe640b", + "yellow": "#df8e1d", + "green": "#40a02b", + "teal": "#179299", + "sky": "#04a5e5", + "sapphire": "#209fb5", + "blue": "#1e66f5", + "lavender": "#7287fd", + "text": "#4c4f69", + "subtext1": "#5c5f77", + "subtext0": "#6c6f85", + "overlay2": "#7c7f93", + "overlay1": "#8c8fa1", + "overlay0": "#9ca0b0", + "surface2": "#acb0be", + "surface1": "#bcc0cc", + "surface0": "#ccd0da", + "base": "#eff1f5", + "mantle": "#e6e9ef", + "crust": "#dce0e8" + }, + "frappe": { + "rosewater": "#f2d5cf", + "flamingo": "#eebebe", + "pink": "#f4b8e4", + "mauve": "#ca9ee6", + "red": "#e78284", + "maroon": "#ea999c", + "peach": "#ef9f76", + "yellow": "#e5c890", + "green": "#a6d189", + "teal": "#81c8be", + "sky": "#99d1db", + "sapphire": "#85c1dc", + "blue": "#8caaee", + "lavender": "#babbf1", + "text": "#c6d0f5", + "subtext1": "#b5bfe2", + "subtext0": "#a5adce", + "overlay2": "#949cbb", + "overlay1": "#838ba7", + "overlay0": "#737994", + "surface2": "#626880", + "surface1": "#51576d", + "surface0": "#414559", + "base": "#303446", + "mantle": "#292c3c", + "crust": "#232634" + }, + "macchiato": { + "rosewater": "#f4dbd6", + "flamingo": "#f0c6c6", + "pink": "#f5bde6", + "mauve": "#c6a0f6", + "red": "#ed8796", + "maroon": "#ee99a0", + "peach": "#f5a97f", + "yellow": "#eed49f", + "green": "#a6da95", + "teal": "#8bd5ca", + "sky": "#91d7e3", + "sapphire": "#7dc4e4", + "blue": "#8aadf4", + "lavender": "#b7bdf8", + "text": "#cad3f5", + "subtext1": "#b8c0e0", + "subtext0": "#a5adcb", + "overlay2": "#939ab7", + "overlay1": "#8087a2", + "overlay0": "#6e738d", + "surface2": "#5b6078", + "surface1": "#494d64", + "surface0": "#363a4f", + "base": "#24273a", + "mantle": "#1e2030", + "crust": "#181926" + }, + "mocha": { + "rosewater": "#f5e0dc", + "flamingo": "#f2cdcd", + "pink": "#f5c2e7", + "mauve": "#cba6f7", + "red": "#f38ba8", + "maroon": "#eba0ac", + "peach": "#fab387", + "yellow": "#f9e2af", + "green": "#a6e3a1", + "teal": "#94e2d5", + "sky": "#89dceb", + "sapphire": "#74c7ec", + "blue": "#89b4fa", + "lavender": "#b4befe", + "text": "#cdd6f4", + "subtext1": "#bac2de", + "subtext0": "#a6adc8", + "overlay2": "#9399b2", + "overlay1": "#7f849c", + "overlay0": "#6c7086", + "surface2": "#585b70", + "surface1": "#45475a", + "surface0": "#313244", + "base": "#1e1e2e", + "mantle": "#181825", + "crust": "#11111b" + } +} diff --git a/quickshell/.config/quickshell/labfy-sway/theme/qmldir b/quickshell/.config/quickshell/labfy-sway/theme/qmldir new file mode 100644 index 000000000..8a40da2dc --- /dev/null +++ b/quickshell/.config/quickshell/labfy-sway/theme/qmldir @@ -0,0 +1,2 @@ +singleton Theme 1.0 Theme.qml +singleton AppearanceController 1.0 AppearanceController.qml diff --git a/quickshell/.config/quickshell/labfy-sway/windows/WindowButton.qml b/quickshell/.config/quickshell/labfy-sway/windows/WindowButton.qml index 71226d8f3..233ca9d75 100644 --- a/quickshell/.config/quickshell/labfy-sway/windows/WindowButton.qml +++ b/quickshell/.config/quickshell/labfy-sway/windows/WindowButton.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.I3 import "../components" +import "../theme" Rectangle { id: button @@ -27,9 +28,9 @@ Rectangle { : 26 height: 26 radius: 4 - color: primary ? "#45475a" : pointer.containsMouse ? "#45475a" : "#313244" + color: primary ? Theme.border : pointer.containsMouse ? Theme.border : Theme.buttonBackground border.width: primary ? 1 : 0 - border.color: "#cba6f7" + border.color: Theme.accent NerdIcon { id: icon @@ -51,7 +52,7 @@ Rectangle { anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter text: button.fullTitle - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 13 elide: Text.ElideRight maximumLineCount: 1 @@ -105,8 +106,8 @@ Rectangle { Rectangle { anchors.fill: parent radius: 4 - color: "#313244" - border.color: "#45475a" + color: Theme.buttonBackground + border.color: Theme.border Column { id: tooltipContent @@ -118,14 +119,14 @@ Rectangle { Text { text: button.appName - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 12 font.bold: true } Text { width: parent.width text: button.fullTitle - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 12 wrapMode: Text.Wrap maximumLineCount: 3 diff --git a/quickshell/.config/quickshell/labfy-sway/windows/WindowMenu.qml b/quickshell/.config/quickshell/labfy-sway/windows/WindowMenu.qml index 02a87344a..3d010c1f7 100644 --- a/quickshell/.config/quickshell/labfy-sway/windows/WindowMenu.qml +++ b/quickshell/.config/quickshell/labfy-sway/windows/WindowMenu.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.I3 +import "../theme" PopupWindow { id: menu @@ -30,8 +31,8 @@ PopupWindow { Rectangle { anchors.fill: parent radius: 4 - color: "#1e1e2e" - border.color: "#45475a" + color: Theme.popupBackground + border.color: Theme.border Column { id: content @@ -45,7 +46,7 @@ PopupWindow { width: parent.width text: menu.entry ? menu.entry.name : menu.selectedWindow ? menu.selectedWindow.appId : "" - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 15 font.bold: true elide: Text.ElideRight @@ -53,7 +54,7 @@ PopupWindow { Text { text: "Fenêtres ouvertes" - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 11 } @@ -65,7 +66,7 @@ PopupWindow { width: content.width height: 30 radius: 4 - color: pointer.containsMouse ? "#45475a" : "#313244" + color: pointer.containsMouse ? Theme.border : Theme.buttonBackground Text { anchors.left: parent.left @@ -74,7 +75,7 @@ PopupWindow { anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter text: modelData.title - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 12 elide: Text.ElideRight } @@ -94,7 +95,7 @@ PopupWindow { Text { visible: menu.actions.length > 0 text: "Actions" - color: "#a6adc8" + color: Theme.secondaryForeground font.pixelSize: 11 } @@ -106,7 +107,7 @@ PopupWindow { width: content.width height: 30 radius: 4 - color: actionPointer.containsMouse ? "#45475a" : "#313244" + color: actionPointer.containsMouse ? Theme.border : Theme.buttonBackground Text { anchors.left: parent.left @@ -115,7 +116,7 @@ PopupWindow { anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter text: modelData.name - color: "#cdd6f4" + color: Theme.foreground font.pixelSize: 12 elide: Text.ElideRight } diff --git a/sway/.config/sway/README.md b/sway/.config/sway/README.md index ef0deceab..09cbbc55d 100644 --- a/sway/.config/sway/README.md +++ b/sway/.config/sway/README.md @@ -1,5 +1,11 @@ # Configuration SwayFX +Les couleurs et variables d'apparence viennent du +[moteur commun](../../../../quickshell/.config/quickshell/labfy-sway/theme/README.md). +`theme-default.conf` assure le démarrage Mocha avant génération ; +`generated/theme.conf` est l'état runtime créé atomiquement par +`bin/.local/bin/generate-appearance.py` depuis la palette QuickShell. + Le compositeur actif est **SwayFX 0.6** avec `scenefx`. La session UWSM sélectionne `sway.desktop` et charge `config` : `bind`, `input`, `rules`, `style`, `swayfx` et `autostart`. `eDP-1` est l'unique écran configuré, à diff --git a/sway/.config/sway/autostart b/sway/.config/sway/autostart index debe8a4e8..9077452ec 100644 --- a/sway/.config/sway/autostart +++ b/sway/.config/sway/autostart @@ -7,7 +7,7 @@ # ================================================= # Listener IPC démarré une fois par session ; exec ne se rejoue pas au reload. -exec python3 ~/.config/sway/scripts/inactive-windows-transparency.py +exec python3 ~/.config/sway/scripts/inactive-windows-transparency.py --opacity $inactive_opacity # ================================================= # Règles d'assignation automatique diff --git a/sway/.config/sway/catppuccin-mocha b/sway/.config/sway/catppuccin-mocha deleted file mode 100644 index 77749f635..000000000 --- a/sway/.config/sway/catppuccin-mocha +++ /dev/null @@ -1,26 +0,0 @@ -set $rosewater #f5e0dc -set $flamingo #f2cdcd -set $pink #f5c2e7 -set $mauve #cba6f7 -set $red #f38ba8 -set $maroon #eba0ac -set $peach #fab387 -set $yellow #f9e2af -set $green #a6e3a1 -set $teal #94e2d5 -set $sky #89dceb -set $sapphire #74c7ec -set $blue #89b4fa -set $lavender #b4befe -set $text #cdd6f4 -set $subtext1 #bac2de -set $subtext0 #a6adc8 -set $overlay2 #9399b2 -set $overlay1 #7f849c -set $overlay0 #6c7086 -set $surface2 #585b70 -set $surface1 #45475a -set $surface0 #313244 -set $base #1e1e2e -set $mantle #181825 -set $crust #11111b diff --git a/sway/.config/sway/style b/sway/.config/sway/style index d94beff35..448412494 100644 --- a/sway/.config/sway/style +++ b/sway/.config/sway/style @@ -10,10 +10,13 @@ # # Style # - include ~/.config/sway/catppuccin-mocha + # CONTRACT: le glob vide est valide dans Sway ; le repli fonctionne dès Stow. + # Le fichier runtime, s'il existe, remplace les variables sans palette parallèle. + include ~/.config/sway/theme-default.conf + include ~/.config/sway/generated/*.conf # target title bg text indicator border - client.focused $lavender $base $text $rosewater $lavender + client.focused $accent $base $text $rosewater $accent client.focused_inactive $overlay0 $base $text $rosewater $overlay0 client.unfocused $overlay0 $base $text $rosewater $overlay0 client.urgent $peach $base $peach $overlay0 $peach diff --git a/sway/.config/sway/swayfx b/sway/.config/sway/swayfx index 76cae6667..01b3dd39d 100644 --- a/sway/.config/sway/swayfx +++ b/sway/.config/sway/swayfx @@ -5,8 +5,8 @@ corner_radius 4 shadows enable shadows_on_csd disable shadow_blur_radius 10 -shadow_color #00000055 -shadow_inactive_color #00000035 +shadow_color $shadow_active +shadow_inactive_color $shadow_inactive shadow_offset 0 2 default_dim_inactive 0 diff --git a/sway/.config/sway/theme-default.conf b/sway/.config/sway/theme-default.conf new file mode 100644 index 000000000..c2a911828 --- /dev/null +++ b/sway/.config/sway/theme-default.conf @@ -0,0 +1,12 @@ +# Repli Mocha avant la première génération runtime. Valeurs dérivées de +# theme/catppuccin.json ; seules les variables nécessaires à Sway sont ici. +set $lavender #b4befe +set $base #1e1e2e +set $text #cdd6f4 +set $rosewater #f5e0dc +set $overlay0 #6c7086 +set $peach #fab387 +set $accent #b4befe +set $shadow_active #00000055 +set $shadow_inactive #00000035 +set $inactive_opacity 0.85 diff --git a/tests/test_generate_appearance.py b/tests/test_generate_appearance.py new file mode 100644 index 000000000..392db1db4 --- /dev/null +++ b/tests/test_generate_appearance.py @@ -0,0 +1,46 @@ +"""Contrats du générateur : validation et remplacement non destructif.""" +import importlib.util +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +SCRIPT = Path(__file__).resolve().parents[1] / 'bin/.local/bin/generate-appearance.py' +spec = importlib.util.spec_from_file_location('appearance_generator', SCRIPT) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +class AppearanceGeneratorTest(unittest.TestCase): + def test_all_flavors_and_valid_accent(self): + palette = module.palette_data() + for flavor in palette: + result = module.render(flavor, 'lavender') + self.assertIn(f'set $accent {palette[flavor]["lavender"]}', result) + self.assertIn(f'set $base {palette[flavor]["base"]}', result) + self.assertIn('set $accent ' + palette['mocha']['mauve'], + module.render('mocha', 'mauve')) + + def test_invalid_inputs(self): + for flavor, accent, profile in [('invalid', 'lavender', 'default'), + ('mocha', 'invalid', 'default'), + ('mocha', 'lavender', 'invalid')]: + with self.subTest(flavor=flavor, accent=accent, profile=profile): + with self.assertRaises(ValueError): + module.render(flavor, accent, profile) + + def test_atomic_replace_and_failed_replace(self): + with tempfile.TemporaryDirectory() as folder: + output = Path(folder) / 'theme.conf' + output.write_bytes(b'old\n') + module.atomic_write(output, b'new\n') + self.assertEqual(output.read_bytes(), b'new\n') + with patch.object(module.os, 'replace', side_effect=OSError('échec')): + with self.assertRaises(OSError): + module.atomic_write(output, b'broken\n') + self.assertEqual(output.read_bytes(), b'new\n') + self.assertFalse((Path(folder) / 'theme.conf.tmp').exists()) + + +if __name__ == '__main__': + unittest.main()