feat(appearance): unify QuickShell and Sway theme engine

This commit is contained in:
fy59 2026-10-04 20:47:26 +02:00
parent aa140b0de5
commit 9274fbfd3a
50 changed files with 809 additions and 238 deletions

2
.gitignore vendored
View file

@ -5,6 +5,8 @@ hyprwhspr/.local/share/
**/__pycache__/ **/__pycache__/
*.py[cod] *.py[cod]
**/node_modules/ **/node_modules/
sway/.config/sway/generated/theme.conf
sway/.config/sway/generated/theme.conf.tmp
# Local editor/application backups # Local editor/application backups
*.bak *.bak

View file

@ -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())

View file

@ -8,6 +8,7 @@ import "datecenter"
import "notifications" import "notifications"
import "status" import "status"
import "systemmonitor" import "systemmonitor"
import "theme"
PanelWindow { PanelWindow {
id: bar id: bar
@ -37,7 +38,7 @@ PanelWindow {
// layer-shell est focusable dès son ouverture. OnDemand n'accapare pas // layer-shell est focusable dès son ouverture. OnDemand n'accapare pas
// le clavier hors interaction avec la barre ou son popup. // le clavier hors interaction avec la barre ou son popup.
focusable: true focusable: true
color: "#eb1e1e2e" color: Theme.panelBackground
// CONTRACT: toute la capsule centrale partage le même popup et la même exclusion XOR. // CONTRACT: toute la capsule centrale partage le même popup et la même exclusion XOR.
function toggleDateCenter() { function toggleDateCenter() {

View file

@ -1,5 +1,8 @@
# QuickShell labfy-sway # 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. Barre principale et serveur de notifications de la session SwayFX.
```text ```text

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import "../theme"
Text { Text {
font.family: "JetBrainsMono Nerd Font Mono" font.family: "JetBrainsMono Nerd Font Mono"
@ -6,5 +7,5 @@ Text {
font.pixelSize: 18 font.pixelSize: 18
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: "#cdd6f4" color: Theme.foreground
} }

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import "../theme"
Rectangle { Rectangle {
id: button id: button
@ -8,13 +9,13 @@ Rectangle {
implicitWidth: caption.implicitWidth + 20 implicitWidth: caption.implicitWidth + 20
implicitHeight: 30 implicitHeight: 30
radius: 4 radius: 4
color: !enabled ? "#313244" : pointer.containsMouse ? "#585b70" : "#45475a" color: !enabled ? Theme.buttonBackground : pointer.containsMouse ? Theme.strongBorder : Theme.border
opacity: enabled ? 1 : 0.55 opacity: enabled ? 1 : 0.55
Text { Text {
id: caption id: caption
anchors.centerIn: parent anchors.centerIn: parent
text: button.label text: button.label
color: button.danger ? "#f38ba8" : "#cdd6f4" color: button.danger ? Theme.danger : Theme.foreground
font.pixelSize: 12 font.pixelSize: 12
} }
MouseArea { MouseArea {

View file

@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.UPower import Quickshell.Services.UPower
import "../components" import "../components"
import "../theme"
Item { Item {
id: page id: page
@ -211,7 +212,7 @@ Item {
anchors.leftMargin: 8 anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Batterie" text: "Batterie"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 16 font.pixelSize: 16
font.bold: true font.bold: true
} }
@ -228,7 +229,7 @@ Item {
width: parent.width width: parent.width
height: 68 height: 68
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
NerdIcon { NerdIcon {
id: batteryIcon id: batteryIcon
anchors.left: parent.left anchors.left: parent.left
@ -245,12 +246,12 @@ Item {
spacing: 2 spacing: 2
Text { Text {
text: page.available ? page.percent + " %" : "Batterie indisponible" text: page.available ? page.percent + " %" : "Batterie indisponible"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 16 font.pixelSize: 16
} }
Text { Text {
text: page.batteryState text: page.batteryState
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 12 font.pixelSize: 12
} }
} }
@ -258,7 +259,7 @@ Item {
Text { Text {
text: "Limite de charge" text: "Limite de charge"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 14 font.pixelSize: 14
} }
@ -267,8 +268,8 @@ Item {
spacing: 8 spacing: 8
Rectangle { Rectangle {
width: 42; height: 38; radius: 4 width: 42; height: 38; radius: 4
color: minusPointer.containsMouse ? "#45475a" : "#313244" color: minusPointer.containsMouse ? Theme.border : Theme.buttonBackground
Text { anchors.centerIn: parent; text: "−"; color: "#cdd6f4"; font.pixelSize: 18 } Text { anchors.centerIn: parent; text: "−"; color: Theme.foreground; font.pixelSize: 18 }
MouseArea { MouseArea {
id: minusPointer id: minusPointer
anchors.fill: parent anchors.fill: parent
@ -283,7 +284,7 @@ Item {
width: parent.width - 100 width: parent.width - 100
height: 38 height: 38
horizontalAlignment: TextInput.AlignHCenter horizontalAlignment: TextInput.AlignHCenter
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 16 font.pixelSize: 16
maximumLength: 3 maximumLength: 3
inputMethodHints: Qt.ImhDigitsOnly inputMethodHints: Qt.ImhDigitsOnly
@ -303,22 +304,22 @@ Item {
} }
background: Rectangle { background: Rectangle {
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
border.color: page.validProposed ? "#45475a" : "#f38ba8" border.color: page.validProposed ? Theme.border : Theme.danger
} }
Text { Text {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "%" text: "%"
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 13 font.pixelSize: 13
} }
} }
Rectangle { Rectangle {
width: 42; height: 38; radius: 4 width: 42; height: 38; radius: 4
color: plusPointer.containsMouse ? "#45475a" : "#313244" color: plusPointer.containsMouse ? Theme.border : Theme.buttonBackground
Text { anchors.centerIn: parent; text: "+"; color: "#cdd6f4"; font.pixelSize: 18 } Text { anchors.centerIn: parent; text: "+"; color: Theme.foreground; font.pixelSize: 18 }
MouseArea { MouseArea {
id: plusPointer id: plusPointer
anchors.fill: parent anchors.fill: parent
@ -344,11 +345,11 @@ Item {
height: 32 height: 32
radius: 4 radius: 4
opacity: modelData !== "" ? 1 : 0 opacity: modelData !== "" ? 1 : 0
color: keyPointer.containsMouse ? "#45475a" : "#313244" color: keyPointer.containsMouse ? Theme.border : Theme.buttonBackground
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: modelData text: modelData
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 13 font.pixelSize: 13
} }
MouseArea { MouseArea {
@ -369,13 +370,13 @@ Item {
delegate: Rectangle { delegate: Rectangle {
required property int modelData required property int modelData
width: 60; height: 32; radius: 4 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 border.color: page.validProposed && Number(valueField.text) === modelData
? "#cba6f7" : "transparent" ? Theme.accent : "transparent"
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: modelData text: modelData
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 13 font.pixelSize: 13
} }
MouseArea { MouseArea {
@ -390,7 +391,7 @@ Item {
Text { Text {
text: page.reportedThreshold >= 0 text: page.reportedThreshold >= 0
? "Limite rapportée : " + page.reportedThreshold + " %" : "Limite indisponible" ? "Limite rapportée : " + page.reportedThreshold + " %" : "Limite indisponible"
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 12 font.pixelSize: 12
} }
@ -398,12 +399,12 @@ Item {
width: parent.width width: parent.width
height: 36 height: 36
radius: 4 radius: 4
color: applyPointer.containsMouse && applyPointer.enabled ? "#b4befe" : "#cba6f7" color: Theme.accent
opacity: applyPointer.enabled ? 1 : 0.5 opacity: applyPointer.enabled ? 1 : 0.5
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: page.phase === "Applying" ? "Application…" : "Appliquer" text: page.phase === "Applying" ? "Application…" : "Appliquer"
color: "#1e1e2e" color: Theme.onAccent
font.pixelSize: 13 font.pixelSize: 13
} }
MouseArea { MouseArea {
@ -423,7 +424,7 @@ Item {
: !page.validProposed ? "Valeur comprise entre 40 et 100" : !page.validProposed ? "Valeur comprise entre 40 et 100"
: !page.helperReady ? "Service de charge non déployé" : "" : !page.helperReady ? "Service de charge non déployé" : ""
color: page.phase === "Error" || !page.validProposed || !page.helperReady color: page.phase === "Error" || !page.validProposed || !page.helperReady
? "#f38ba8" : "#a6e3a1" ? Theme.danger : Theme.success
font.pixelSize: 12 font.pixelSize: 12
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }

View file

@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import "../components" import "../components"
import "../theme"
Item { Item {
id: page id: page
@ -61,10 +62,10 @@ Item {
Text { Text {
width: parent.width - 60; height: 30; verticalAlignment: Text.AlignVCenter width: parent.width - 60; height: 30; verticalAlignment: Text.AlignVCenter
text: page.stage === "main" ? "Bluetooth" : page.nameOf(page.selectedDevice) 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 { Loader {
width: parent.width; height: Math.max(0, parent.height - y) width: parent.width; height: Math.max(0, parent.height - y)
sourceComponent: page.stage === "main" ? mainComponent : page.stage === "details" sourceComponent: page.stage === "main" ? mainComponent : page.stage === "details"
@ -80,22 +81,22 @@ Item {
width: page.width; spacing: 10 width: page.width; spacing: 10
Row { Row {
width: parent.width; spacing: 8 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 } 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 { 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: "#a6adc8"; font.pixelSize: 12 } 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: "#cba6f7"; font.bold: true; font.pixelSize: 13 } Text { visible: page.connectedDevices.length > 0; text: "Connectés"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 }
Repeater { Repeater {
model: ScriptModel { values: page.connectedDevices; objectProp: "address" } model: ScriptModel { values: page.connectedDevices; objectProp: "address" }
delegate: deviceRowComponent 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 { Repeater {
model: ScriptModel { values: page.pairedDevices; objectProp: "address" } model: ScriptModel { values: page.pairedDevices; objectProp: "address" }
delegate: deviceRowComponent 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 { Repeater {
model: ScriptModel { values: page.newDevices; objectProp: "address" } model: ScriptModel { values: page.newDevices; objectProp: "address" }
delegate: deviceRowComponent delegate: deviceRowComponent
@ -109,7 +110,7 @@ Item {
required property var modelData required property var modelData
readonly property var device: modelData readonly property var device: modelData
width: page.width; height: 48; radius: 4 width: page.width; height: 48; radius: 4
color: pointer.containsMouse ? "#45475a" : "#313244" color: pointer.containsMouse ? Theme.border : Theme.buttonBackground
Image { Image {
id: iconImage; anchors.left: parent.left; anchors.leftMargin: 9; anchors.verticalCenter: parent.verticalCenter 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) : "" width: 19; height: 19; source: device.icon ? Quickshell.iconPath(device.icon, true) : ""
@ -117,13 +118,13 @@ Item {
} }
NerdIcon { NerdIcon {
anchors.centerIn: iconImage; visible: !iconImage.visible anchors.centerIn: iconImage; visible: !iconImage.visible
text: "󰂯"; color: "#cba6f7"; font.pixelSize: 17 text: "󰂯"; color: Theme.accentForeground; font.pixelSize: 17
} }
Column { Column {
anchors.left: parent.left; anchors.leftMargin: 38; anchors.right: parent.right anchors.left: parent.left; anchors.leftMargin: 38; anchors.right: parent.right
anchors.rightMargin: 10; anchors.verticalCenter: parent.verticalCenter; spacing: 2 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 { 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: "#a6adc8"; font.pixelSize: 11 } 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) } MouseArea { id: pointer; anchors.fill: parent; hoverEnabled: true; onClicked: page.choose(device) }
} }
@ -134,10 +135,10 @@ Item {
clip: true; ScrollBar.horizontal.policy: ScrollBar.AlwaysOff clip: true; ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column { Column {
width: page.width; spacing: 10 width: page.width; spacing: 10
Text { text: page.selectedDevice ? page.stateOf(page.selectedDevice) : "Appareil indisponible"; color: "#cdd6f4"; font.pixelSize: 13 } 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: "#a6adc8"; font.pixelSize: 11 } 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: "#cdd6f4"; font.pixelSize: 12 } 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: "#a6adc8"; font.pixelSize: 11 } Text { text: page.selectedDevice ? "Appairé : " + (page.selectedDevice.paired ? "oui" : "non") + " • Lié : " + (page.selectedDevice.bonded ? "oui" : "non") : ""; color: Theme.secondaryForeground; font.pixelSize: 11 }
Row { Row {
spacing: 8 spacing: 8
ActionButton { ActionButton {
@ -159,37 +160,37 @@ Item {
} }
Row { Row {
spacing: 8 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 } ActionButton { label: page.selectedDevice && page.selectedDevice.trusted ? "Activé" : "Désactivé"; enabled: !!page.selectedDevice && page.selectedDevice.paired; onClicked: page.selectedDevice.trusted = !page.selectedDevice.trusted }
} }
Row { Row {
spacing: 8 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"; } } 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 { Row {
visible: !!page.selectedDevice && page.selectedDevice.paired visible: !!page.selectedDevice && page.selectedDevice.paired
&& page.adapter && page.adapter.enabled && page.adapter && page.adapter.enabled
&& page.selectedDevice.wakeAllowed !== undefined && page.selectedDevice.wakeAllowed !== undefined
spacing: 8 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 } 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 { TextField {
id: aliasInput; width: page.width id: aliasInput; width: page.width
text: page.selectedDevice ? page.selectedDevice.name : "" text: page.selectedDevice ? page.selectedDevice.name : ""
placeholderText: "Alias local"; selectByMouse: true placeholderText: "Alias local"; selectByMouse: true
color: "#cdd6f4"; placeholderTextColor: "#6c7086" color: Theme.foreground; placeholderTextColor: Theme.disabledForeground
background: Rectangle { color: "#313244"; border.color: aliasInput.activeFocus ? "#cba6f7" : "#585b70"; radius: 4 } background: Rectangle { color: Theme.buttonBackground; border.color: aliasInput.activeFocus ? Theme.accent : Theme.strongBorder; radius: 4 }
} }
Row { Row {
spacing: 8 spacing: 8
ActionButton { label: "Réinitialiser"; enabled: !!page.selectedDevice; onClicked: { page.selectedDevice.name = ""; aliasInput.text = page.selectedDevice.name; } } 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() } 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 id: forgetComponent
Column { Column {
spacing: 10 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: "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: "#a6adc8"; font.pixelSize: 12 } Text { width: page.width; wrapMode: Text.Wrap; text: "Cela supprimera les informations de pairage."; color: Theme.secondaryForeground; font.pixelSize: 12 }
Row { Row {
spacing: 8 spacing: 8
ActionButton { label: "Annuler"; onClicked: page.stage = "details" } ActionButton { label: "Annuler"; onClicked: page.stage = "details" }
@ -210,8 +211,8 @@ Item {
id: blockComponent id: blockComponent
Column { Column {
spacing: 10 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: "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: "#a6adc8"; font.pixelSize: 12 } Text { width: page.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: Theme.secondaryForeground; font.pixelSize: 12 }
Row { Row {
spacing: 8 spacing: 8
ActionButton { label: "Annuler"; onClicked: page.stage = "details" } ActionButton { label: "Annuler"; onClicked: page.stage = "details" }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell.Io import Quickshell.Io
import "../components" import "../components"
import "../theme"
Item { Item {
id: brightnessControl id: brightnessControl
@ -44,7 +45,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "" text: ""
font.pixelSize: 18 font.pixelSize: 18
color: "#cdd6f4" color: Theme.foreground
} }
Text { Text {
@ -52,7 +53,7 @@ Item {
anchors.leftMargin: 10 anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Luminosité" text: "Luminosité"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 14 font.pixelSize: 14
} }
@ -60,7 +61,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: brightnessControl.available ? brightnessControl.percent + "%" : "Indisponible" text: brightnessControl.available ? brightnessControl.percent + "%" : "Indisponible"
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 14 font.pixelSize: 14
} }
} }
@ -88,13 +89,13 @@ Item {
width: slider.availableWidth width: slider.availableWidth
height: 8 height: 8
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
Rectangle { Rectangle {
width: slider.visualPosition * parent.width width: slider.visualPosition * parent.width
height: parent.height height: parent.height
radius: parent.radius radius: parent.radius
color: "#cba6f7" color: Theme.accent
} }
} }
@ -104,7 +105,7 @@ Item {
width: 16 width: 16
height: 16 height: 16
radius: 8 radius: 8
color: "#cdd6f4" color: Theme.foreground
} }
} }
} }

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import "../theme"
Item { Item {
id: confirmation id: confirmation
@ -19,7 +20,7 @@ Item {
Text { Text {
width: parent.width width: parent.width
text: confirmation.actionInfo.confirmTitle text: confirmation.actionInfo.confirmTitle
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 16 font.pixelSize: 16
font.bold: true font.bold: true
wrapMode: Text.Wrap wrapMode: Text.Wrap
@ -28,7 +29,7 @@ Item {
Text { Text {
width: parent.width width: parent.width
text: confirmation.actionInfo.description text: confirmation.actionInfo.description
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 13 font.pixelSize: 13
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
@ -42,12 +43,12 @@ Item {
width: (parent.width - parent.spacing) / 2 width: (parent.width - parent.spacing) / 2
height: parent.height height: parent.height
radius: 4 radius: 4
color: cancelPointer.containsMouse ? "#45475a" : "#313244" color: cancelPointer.containsMouse ? Theme.border : Theme.buttonBackground
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "Annuler" text: "Annuler"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 13 font.pixelSize: 13
} }
MouseArea { MouseArea {
@ -68,7 +69,7 @@ Item {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: confirmation.actionInfo.label text: confirmation.actionInfo.label
color: "#1e1e2e" color: Theme.contrastingText(confirmation.actionInfo.accent)
font.pixelSize: 13 font.pixelSize: 13
font.bold: true font.bold: true
} }

View file

@ -5,6 +5,7 @@ import Quickshell.I3
import Quickshell.Networking import Quickshell.Networking
import Quickshell.Bluetooth import Quickshell.Bluetooth
import "../components" import "../components"
import "../theme"
PopupWindow { PopupWindow {
id: popup id: popup
@ -15,19 +16,19 @@ PopupWindow {
property var pendingAction: null property var pendingAction: null
// Une seule table associe les libellés, confirmations et commandes de session. // Une seule table associe les libellés, confirmations et commandes de session.
readonly property var sessionActions: [ readonly property var sessionActions: [
{ id: "lock", label: "Verrouiller", icon: "", accent: "#b4befe", command: ["swaylock"] }, { id: "lock", label: "Verrouiller", icon: "", accent: Theme.accent, command: ["swaylock"] },
{ id: "suspend", label: "Veille", icon: "", accent: "#b4befe", { id: "suspend", label: "Veille", icon: "", accent: Theme.accent,
confirmTitle: "Mettre l'ordinateur en veille ?", confirmTitle: "Mettre l'ordinateur en veille ?",
description: "La session sera verrouillée avant la veille.", description: "La session sera verrouillée avant la veille.",
command: ["systemctl", "suspend"] }, command: ["systemctl", "suspend"] },
{ id: "logout", label: "Déconnexion", icon: "", accent: "#fab387", { id: "logout", label: "Déconnexion", icon: "", accent: Theme.urgent,
confirmTitle: "Se déconnecter ?", confirmTitle: "Se déconnecter ?",
description: "La session SwayFX en cours sera fermée.", command: null }, 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 ?", confirmTitle: "Redémarrer l'ordinateur ?",
description: "La session en cours sera fermée.", description: "La session en cours sera fermée.",
command: ["systemctl", "reboot"] }, command: ["systemctl", "reboot"] },
{ id: "poweroff", label: "Éteindre", icon: "", accent: "#f38ba8", { id: "poweroff", label: "Éteindre", icon: "", accent: Theme.danger,
confirmTitle: "Éteindre l'ordinateur ?", confirmTitle: "Éteindre l'ordinateur ?",
description: "La session en cours sera fermée.", description: "La session en cours sera fermée.",
command: ["systemctl", "poweroff"] } command: ["systemctl", "poweroff"] }
@ -82,8 +83,8 @@ PopupWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 4 radius: 4
color: "#1e1e2e" color: Theme.popupBackground
border.color: "#45475a" border.color: Theme.border
StackLayout { StackLayout {
anchors.fill: parent anchors.fill: parent
@ -96,7 +97,7 @@ PopupWindow {
Text { Text {
text: "Réglages rapides" text: "Réglages rapides"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 16 font.pixelSize: 16
font.bold: true font.bold: true
} }
@ -130,7 +131,7 @@ PopupWindow {
width: 40 width: 40
height: 30 height: 30
radius: 4 radius: 4
color: powerPointer.containsMouse ? "#45475a" : "#313244" color: powerPointer.containsMouse ? Theme.border : Theme.buttonBackground
NerdIcon { NerdIcon {
anchors.centerIn: parent anchors.centerIn: parent
@ -171,7 +172,7 @@ PopupWindow {
ConfirmAction { ConfirmAction {
id: confirmationPage id: confirmationPage
actionInfo: popup.pendingAction || ({ id: "", label: "", confirmTitle: "", actionInfo: popup.pendingAction || ({ id: "", label: "", confirmTitle: "",
description: "", accent: "#cdd6f4" }) description: "", accent: Theme.foreground })
onCancelled: { onCancelled: {
popup.pendingAction = null; popup.pendingAction = null;
popup.currentPage = 3; popup.currentPage = 3;

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import "../components" import "../components"
import "../theme"
Rectangle { Rectangle {
id: controlButton id: controlButton
@ -10,14 +11,14 @@ Rectangle {
width: 48 width: 48
height: 26 height: 26
radius: 4 radius: 4
color: open ? "#cba6f7" : pointer.containsMouse ? "#45475a" : "#313244" color: open ? Theme.accent : pointer.containsMouse ? Theme.border : Theme.buttonBackground
NerdIcon { NerdIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "" text: ""
// WHY: garder le même poids visuel que les statuts voisins dans la capsule fixe. // WHY: garder le même poids visuel que les statuts voisins dans la capsule fixe.
font.pixelSize: 20 font.pixelSize: 20
color: controlButton.open ? "#1e1e2e" : "#cdd6f4" color: controlButton.open ? Theme.onAccent : Theme.foreground
} }
MouseArea { MouseArea {

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell.Services.UPower import Quickshell.Services.UPower
import "../components" import "../components"
import "../theme"
Item { Item {
id: powerControl id: powerControl
@ -22,7 +23,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "" text: ""
font.pixelSize: 18 font.pixelSize: 18
color: "#cdd6f4" color: Theme.foreground
} }
Text { Text {
@ -30,7 +31,7 @@ Item {
anchors.leftMargin: 10 anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Profil d'énergie" text: "Profil d'énergie"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 14 font.pixelSize: 14
} }
} }
@ -57,12 +58,12 @@ Item {
width: (choices.width - 2 * choices.spacing) / 3 width: (choices.width - 2 * choices.spacing) / 3
height: choices.height height: choices.height
radius: 4 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 { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: parent.modelData.label 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 font.pixelSize: 12
} }

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import "../components" import "../components"
import "../theme"
Rectangle { Rectangle {
id: tile id: tile
@ -14,8 +15,8 @@ Rectangle {
height: 58 height: 58
radius: 4 radius: 4
color: active ? "#cba6f7" : "#313244" color: active ? Theme.accent : Theme.buttonBackground
readonly property color foreground: active ? "#1e1e2e" : "#cdd6f4" readonly property color foreground: active ? Theme.onAccent : Theme.foreground
NerdIcon { NerdIcon {
id: symbol id: symbol

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import "../components" import "../components"
import "../theme"
Rectangle { Rectangle {
id: actionButton id: actionButton
@ -9,7 +10,7 @@ Rectangle {
height: 48 height: 48
radius: 4 radius: 4
color: pointer.containsMouse ? "#45475a" : "#313244" color: pointer.containsMouse ? Theme.border : Theme.buttonBackground
NerdIcon { NerdIcon {
id: icon id: icon
@ -27,7 +28,7 @@ Rectangle {
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: actionButton.actionInfo.label text: actionButton.actionInfo.label
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 14 font.pixelSize: 14
} }

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "../components" import "../components"
import "../theme"
Item { Item {
id: page id: page
@ -34,7 +35,7 @@ Item {
anchors.leftMargin: 8 anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Session" text: "Session"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 16 font.pixelSize: 16
font.bold: true font.bold: true
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import "../components" import "../components"
import "../theme"
Item { Item {
id: volumeControl id: volumeControl
@ -33,7 +34,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: volumeControl.sinkAudio ? (volumeControl.muted ? "" : "") : "" text: volumeControl.sinkAudio ? (volumeControl.muted ? "" : "") : ""
font.pixelSize: 18 font.pixelSize: 18
color: volumeControl.muted ? "#f38ba8" : "#cdd6f4" color: volumeControl.muted ? Theme.danger : Theme.foreground
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@ -48,7 +49,7 @@ Item {
anchors.leftMargin: 10 anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Volume" text: "Volume"
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 14 font.pixelSize: 14
} }
@ -56,7 +57,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: volumeControl.sinkAudio ? volumeControl.percent + "%" : "Indisponible" text: volumeControl.sinkAudio ? volumeControl.percent + "%" : "Indisponible"
color: volumeControl.muted ? "#f38ba8" : "#a6adc8" color: volumeControl.muted ? Theme.danger : Theme.secondaryForeground
font.pixelSize: 14 font.pixelSize: 14
} }
} }
@ -82,13 +83,13 @@ Item {
width: slider.availableWidth width: slider.availableWidth
height: 8 height: 8
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
Rectangle { Rectangle {
width: slider.visualPosition * parent.width width: slider.visualPosition * parent.width
height: parent.height height: parent.height
radius: parent.radius radius: parent.radius
color: "#cba6f7" color: Theme.accent
} }
} }
@ -98,7 +99,7 @@ Item {
width: 16 width: 16
height: 16 height: 16
radius: 8 radius: 8
color: "#cdd6f4" color: Theme.foreground
} }
} }
} }

View file

@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Networking import Quickshell.Networking
import "../components" import "../components"
import "../theme"
Item { Item {
id: page id: page
@ -122,12 +123,12 @@ Item {
text: page.stage === "main" ? "Wi-Fi" : page.stage === "password" text: page.stage === "main" ? "Wi-Fi" : page.stage === "password"
? "Se connecter à " + page.selectedName : page.stage === "forget" ? "Se connecter à " + page.selectedName : page.stage === "forget"
? "Oublier un réseau" : page.selectedName ? "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 { Text {
width: parent.width; visible: page.message.length > 0 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 { Loader {
id: body id: body
@ -147,27 +148,27 @@ Item {
width: page.width; spacing: 9 width: page.width; spacing: 9
Row { Row {
width: parent.width; spacing: 8 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 { ActionButton {
label: Networking.wifiEnabled ? "Désactiver" : "Activer" label: Networking.wifiEnabled ? "Désactiver" : "Activer"
enabled: Networking.wifiHardwareEnabled enabled: Networking.wifiHardwareEnabled
onClicked: Networking.wifiEnabled = !Networking.wifiEnabled onClicked: Networking.wifiEnabled = !Networking.wifiEnabled
} }
} }
Text { text: page.scanWanted ? "Recherche de réseaux…" : "Scan arrêté"; color: "#a6adc8"; font.pixelSize: 11 } Text { text: page.scanWanted ? "Recherche de réseaux…" : "Scan arrêté"; color: Theme.secondaryForeground; font.pixelSize: 11 }
Text { text: "Réseau actuel"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 } Text { text: "Réseau actuel"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 }
Rectangle { Rectangle {
width: parent.width; height: 66; radius: 4; color: "#313244" width: parent.width; height: 66; radius: 4; color: Theme.buttonBackground
Text { Text {
anchors.left: parent.left; anchors.leftMargin: 10; anchors.top: parent.top; anchors.topMargin: 8 anchors.left: parent.left; anchors.leftMargin: 10; anchors.top: parent.top; anchors.topMargin: 8
text: page.currentNetwork ? page.currentNetwork.name : "Aucun réseau connecté" 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 { Text {
anchors.left: parent.left; anchors.leftMargin: 10; anchors.bottom: parent.bottom; anchors.bottomMargin: 8 anchors.left: parent.left; anchors.leftMargin: 10; anchors.bottom: parent.bottom; anchors.bottomMargin: 8
text: page.currentNetwork ? page.securityLabel(page.currentNetwork) + " • " text: page.currentNetwork ? page.securityLabel(page.currentNetwork) + " • "
+ Math.round(page.currentNetwork.signalStrength * 100) + " %" : "" + Math.round(page.currentNetwork.signalStrength * 100) + " %" : ""
color: "#a6adc8"; font.pixelSize: 11 color: Theme.secondaryForeground; font.pixelSize: 11
} }
ActionButton { ActionButton {
anchors.right: parent.right; anchors.rightMargin: 8; anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right; anchors.rightMargin: 8; anchors.verticalCenter: parent.verticalCenter
@ -175,11 +176,11 @@ Item {
onClicked: page.choose(page.currentNetwork) 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 { Text {
visible: page.sortedNetworks.length === 0 visible: page.sortedNetworks.length === 0
text: !Networking.wifiHardwareEnabled ? "Wi-Fi bloqué matériellement" : "Aucun réseau visible" text: !Networking.wifiHardwareEnabled ? "Wi-Fi bloqué matériellement" : "Aucun réseau visible"
color: "#a6adc8"; font.pixelSize: 12 color: Theme.secondaryForeground; font.pixelSize: 12
} }
Repeater { Repeater {
model: ScriptModel { values: page.sortedNetworks; objectProp: "name" } model: ScriptModel { values: page.sortedNetworks; objectProp: "name" }
@ -187,23 +188,23 @@ Item {
required property var modelData required property var modelData
readonly property var network: modelData readonly property var network: modelData
width: page.width; height: 44; radius: 4 width: page.width; height: 44; radius: 4
color: pointer.containsMouse ? "#45475a" : "#313244" color: pointer.containsMouse ? Theme.border : Theme.buttonBackground
NerdIcon { NerdIcon {
anchors.left: parent.left; anchors.leftMargin: 9; anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left; anchors.leftMargin: 9; anchors.verticalCenter: parent.verticalCenter
text: network.connected ? "" : network.security === WifiSecurityType.Open ? "󰖩" : "" 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 { Text {
anchors.left: parent.left; anchors.leftMargin: 37; anchors.right: strength.left anchors.left: parent.left; anchors.leftMargin: 37; anchors.right: strength.left
anchors.rightMargin: 8; anchors.verticalCenter: parent.verticalCenter anchors.rightMargin: 8; anchors.verticalCenter: parent.verticalCenter
text: network.name + " • " + page.stateLabel(network) text: network.name + " • " + page.stateLabel(network)
color: "#cdd6f4"; font.pixelSize: 12; elide: Text.ElideRight color: Theme.foreground; font.pixelSize: 12; elide: Text.ElideRight
} }
Text { Text {
id: strength; anchors.right: parent.right; anchors.rightMargin: 9 id: strength; anchors.right: parent.right; anchors.rightMargin: 9
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Math.round(network.signalStrength * 100) + "%" text: Math.round(network.signalStrength * 100) + "%"
color: "#a6adc8"; font.pixelSize: 11 color: Theme.secondaryForeground; font.pixelSize: 11
} }
MouseArea { MouseArea {
id: pointer; anchors.fill: parent; hoverEnabled: true 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 { Repeater {
model: ScriptModel { values: page.sortedNetworks.filter(n => n.known); objectProp: "name" } model: ScriptModel { values: page.sortedNetworks.filter(n => n.known); objectProp: "name" }
delegate: ActionButton { delegate: ActionButton {
@ -233,7 +234,7 @@ Item {
clip: true; ScrollBar.horizontal.policy: ScrollBar.AlwaysOff clip: true; ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column { Column {
width: page.width; spacing: 10 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 { Row {
spacing: 8 spacing: 8
ActionButton { ActionButton {
@ -250,15 +251,15 @@ Item {
onClicked: { page.selectedProfile = -1; page.stage = "forget"; } onClicked: { page.selectedProfile = -1; page.stage = "forget"; }
} }
} }
Text { text: "Profils NetworkManager"; color: "#cba6f7"; font.pixelSize: 13; font.bold: true } Text { text: "Profils NetworkManager"; color: Theme.accentForeground; font.pixelSize: 13; font.bold: true }
Text { visible: page.profiles.length === 0; text: "Aucun profil enregistré"; color: "#a6adc8"; font.pixelSize: 12 } Text { visible: page.profiles.length === 0; text: "Aucun profil enregistré"; color: Theme.secondaryForeground; font.pixelSize: 12 }
Repeater { Repeater {
model: page.profiles model: page.profiles
delegate: Row { delegate: Row {
required property var modelData required property var modelData
required property int index required property int index
spacing: 6 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: "Utiliser"; onClicked: { page.message = ""; page.selectedNetwork.connectWithSettings(modelData); } }
ActionButton { label: "Oublier"; danger: true; onClicked: { page.selectedProfile = index; page.stage = "forget"; } } ActionButton { label: "Oublier"; danger: true; onClicked: { page.selectedProfile = index; page.stage = "forget"; } }
} }
@ -271,12 +272,12 @@ Item {
Column { Column {
property alias passwordInput: input property alias passwordInput: input
spacing: 10 spacing: 10
Text { text: "Mot de passe"; color: "#cdd6f4"; font.pixelSize: 13 } Text { text: "Mot de passe"; color: Theme.foreground; font.pixelSize: 13 }
TextField { TextField {
id: input; width: page.width; echoMode: TextInput.Password id: input; width: page.width; echoMode: TextInput.Password
placeholderText: "Mot de passe Wi-Fi"; selectByMouse: true placeholderText: "Mot de passe Wi-Fi"; selectByMouse: true
color: "#cdd6f4"; placeholderTextColor: "#6c7086" color: Theme.foreground; placeholderTextColor: Theme.disabledForeground
background: Rectangle { color: "#313244"; border.color: input.activeFocus ? "#cba6f7" : "#585b70"; radius: 4 } background: Rectangle { color: Theme.buttonBackground; border.color: input.activeFocus ? Theme.accent : Theme.strongBorder; radius: 4 }
onAccepted: submit.clicked() onAccepted: submit.clicked()
} }
Row { Row {
@ -303,8 +304,8 @@ Item {
id: forgetComponent id: forgetComponent
Column { Column {
spacing: 10 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: "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: "#a6adc8"; font.pixelSize: 12 } 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 { Row {
spacing: 8 spacing: 8
ActionButton { label: "Annuler"; onClicked: page.stage = "details" } ActionButton { label: "Annuler"; onClicked: page.stage = "details" }

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "../theme"
PopupWindow { PopupWindow {
id: popup id: popup
@ -32,8 +33,8 @@ PopupWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 4 radius: 4
color: "#1e1e2e" color: Theme.popupBackground
border.color: "#45475a" border.color: Theme.border
Column { Column {
id: content id: content
anchors.left: parent.left anchors.left: parent.left
@ -43,7 +44,7 @@ PopupWindow {
spacing: 14 spacing: 14
Text { Text {
text: clock.date.toLocaleDateString(Qt.locale("fr_FR"), "dddd d MMMM") 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 } MediaPlayer { width: parent.width }
NotificationList { width: parent.width; service: popup.notificationService } NotificationList { width: parent.width; service: popup.notificationService }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import "../components" import "../components"
import "../theme"
Rectangle { Rectangle {
id: card id: card
@ -22,7 +23,7 @@ Rectangle {
implicitHeight: visible ? 116 : 0 implicitHeight: visible ? 116 : 0
height: implicitHeight height: implicitHeight
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -33,7 +34,7 @@ Rectangle {
Layout.preferredWidth: 72 Layout.preferredWidth: 72
Layout.preferredHeight: 72 Layout.preferredHeight: 72
radius: 4 radius: 4
color: "#45475a" color: Theme.border
clip: true clip: true
Image { Image {
@ -48,7 +49,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
visible: !artwork.visible visible: !artwork.visible
text: "" text: ""
color: "#cba6f7" color: Theme.accent
font.pixelSize: 27 font.pixelSize: 27
} }
} }
@ -60,7 +61,7 @@ Rectangle {
Text { Text {
Layout.fillWidth: true Layout.fillWidth: true
text: card.player ? card.player.trackTitle : "" text: card.player ? card.player.trackTitle : ""
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 13 font.pixelSize: 13
font.bold: true font.bold: true
elide: Text.ElideRight elide: Text.ElideRight
@ -69,7 +70,7 @@ Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
text: card.player ? card.player.trackArtist : "" text: card.player ? card.player.trackArtist : ""
visible: text.length > 0 visible: text.length > 0
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 11 font.pixelSize: 11
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -92,12 +93,12 @@ Rectangle {
width: 32 width: 32
height: 27 height: 27
radius: 4 radius: 4
color: controlPointer.containsMouse && available ? "#585b70" : "#45475a" color: controlPointer.containsMouse && available ? Theme.strongBorder : Theme.border
NerdIcon { NerdIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: modelData.icon text: modelData.icon
color: parent.available ? "#cdd6f4" : "#6c7086" color: parent.available ? Theme.foreground : Theme.disabledForeground
font.pixelSize: 14 font.pixelSize: 14
} }
MouseArea { MouseArea {

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "../components" import "../components"
import "../theme"
Rectangle { Rectangle {
id: card id: card
@ -19,7 +20,7 @@ Rectangle {
implicitHeight: bodyText.visible || image.visible || actions.visible ? 95 + image.height + actions.height : 64 implicitHeight: bodyText.visible || image.visible || actions.visible ? 95 + image.height + actions.height : 64
height: implicitHeight height: implicitHeight
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
Column { Column {
anchors.fill: parent; anchors.margins: 10; spacing: 4 anchors.fill: parent; anchors.margins: 10; spacing: 4
Row { Row {
@ -27,21 +28,21 @@ Rectangle {
Item { Item {
width: 14; height: 14 width: 14; height: 14
Image { id: appIcon; anchors.fill: parent; source: card.iconSource; fillMode: Image.PreserveAspectFit; visible: status === Image.Ready } 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 { Text {
width: parent.width - 48 width: parent.width - 48
text: card.notification.appName || card.notification.desktopEntry || "Notification" 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 { 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) } 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 { 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: "#bac2de"; font.pixelSize: 11; wrapMode: Text.Wrap; maximumLineCount: 2; 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 } 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 { Flow {
id: actions id: actions
@ -53,8 +54,8 @@ Rectangle {
model: card.live ? card.live.actions : [] model: card.live ? card.live.actions : []
delegate: Rectangle { delegate: Rectangle {
required property var modelData required property var modelData
width: label.implicitWidth + 16; height: 24; radius: 4; color: "#45475a" width: label.implicitWidth + 16; height: 24; radius: 4; color: Theme.border
Text { id: label; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: "#cdd6f4"; font.pixelSize: 11 } 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() } MouseArea { anchors.fill: parent; onClicked: modelData.invoke() }
} }
} }

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import "../theme"
Item { Item {
id: list id: list
@ -10,12 +11,12 @@ Item {
implicitHeight: rows.length ? 28 + Math.min(307, rows.length * 94) : 80 implicitHeight: rows.length ? 28 + Math.min(307, rows.length * 94) : 80
height: implicitHeight height: implicitHeight
Timer { interval: 5000; running: list.confirmClear; onTriggered: list.confirmClear = false } 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 { Text {
anchors.right: parent.right anchors.right: parent.right
text: list.confirmClear ? "Confirmer : tout effacer" : "Tout effacer" text: list.confirmClear ? "Confirmer : tout effacer" : "Tout effacer"
visible: list.rows.length > 0 visible: list.rows.length > 0
color: confirm.containsMouse ? "#f38ba8" : "#a6adc8" color: confirm.containsMouse ? Theme.danger : Theme.secondaryForeground
font.pixelSize: 11 font.pixelSize: 11
MouseArea { MouseArea {
id: confirm id: confirm
@ -31,7 +32,7 @@ Item {
visible: list.rows.length === 0 visible: list.rows.length === 0
anchors.top: title.bottom; anchors.topMargin: 8 anchors.top: title.bottom; anchors.topMargin: 8
text: "Aucune notification récente" text: "Aucune notification récente"
color: "#a6adc8"; font.pixelSize: 12 color: Theme.secondaryForeground; font.pixelSize: 12
} }
ListView { ListView {
anchors.top: title.bottom; anchors.topMargin: 8 anchors.top: title.bottom; anchors.topMargin: 8

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "../notifications" import "../notifications"
import "../theme"
Rectangle { Rectangle {
id: clockCapsule id: clockCapsule
@ -12,7 +13,7 @@ Rectangle {
width: 184 width: 184
height: 26 height: 26
radius: 4 radius: 4
color: open ? "#cba6f7" : pointer.containsMouse ? "#45475a" : "#313244" color: open ? Theme.accent : pointer.containsMouse ? Theme.border : Theme.buttonBackground
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: unreadCount === 0 ? "Date et heure" Accessible.name: unreadCount === 0 ? "Date et heure"
: "Date et heure, " + unreadCount + (unreadCount === 1 : "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. // CONTRACT: le centre du texte coïncide avec celui de la barre, sans dépendre du badge.
anchors.centerIn: parent anchors.centerIn: parent
text: Qt.formatDateTime(clock.date, "dd/MM HH:mm") 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.pixelSize: 13
font.weight: Font.Bold font.weight: Font.Bold
} }
@ -63,15 +64,15 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
border.color: "#45475a" border.color: Theme.border
Text { Text {
id: tooltipText id: tooltipText
anchors.centerIn: parent anchors.centerIn: parent
text: clockCapsule.unreadCount === 0 ? "Date et notifications" text: clockCapsule.unreadCount === 0 ? "Date et notifications"
: clockCapsule.unreadCount + (clockCapsule.unreadCount === 1 : clockCapsule.unreadCount + (clockCapsule.unreadCount === 1
? " notification non lue" : " notifications non lues") ? " notification non lue" : " notifications non lues")
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 11 font.pixelSize: 11
} }
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.I3 import Quickshell.I3
import "../status" import "../status"
import "../theme"
Row { Row {
id: workspaces id: workspaces
@ -58,9 +59,9 @@ Row {
width: button.active ? 24 : 16 width: button.active ? 24 : 16
height: button.active ? 16 : 11 height: button.active ? 16 : 11
radius: 4 radius: 4
color: button.urgent ? "#fab387" color: button.urgent ? Theme.urgent
: button.active ? "#cba6f7" : button.active ? Theme.accent
: pointer.containsMouse ? "#6c7086" : "#585b70" : pointer.containsMouse ? Theme.disabledForeground : Theme.strongBorder
} }
MouseArea { MouseArea {

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import "../components" import "../components"
import "../theme"
Rectangle { Rectangle {
id: card id: card
@ -23,8 +24,8 @@ Rectangle {
width: 370 width: 370
implicitHeight: content.implicitHeight + 20 implicitHeight: content.implicitHeight + 20
radius: 4 radius: 4
color: "#1e1e2e" color: Theme.popupBackground
border.color: notification.urgency === NotificationUrgency.Critical ? "#f38ba8" : "#45475a" border.color: notification.urgency === NotificationUrgency.Critical ? Theme.danger : Theme.border
// CONTRACT: 0 interdit l'expiration automatique. Les durées sont en ms. // CONTRACT: 0 interdit l'expiration automatique. Les durées sont en ms.
Timer { Timer {
interval: Math.max(1, card.timeout) interval: Math.max(1, card.timeout)
@ -50,23 +51,23 @@ Rectangle {
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
visible: status === Image.Ready 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 { Text {
width: parent.width - 48 width: parent.width - 48
text: card.notification.appName || "Notification" text: card.notification.appName || "Notification"
textFormat: Text.PlainText textFormat: Text.PlainText
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 11 font.pixelSize: 11
elide: Text.ElideRight elide: Text.ElideRight
} }
NerdIcon { NerdIcon {
text: ""; color: "#a6adc8" text: ""; color: Theme.secondaryForeground
MouseArea { anchors.fill: parent; anchors.margins: -4; onClicked: card.service.dismiss(card.entry.internalId) } 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; 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: "#bac2de"; font.pixelSize: 12; wrapMode: Text.Wrap; maximumLineCount: 3; elide: Text.ElideRight } 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 { Image {
width: parent.width; height: status === Image.Ready ? Math.min(120, implicitHeight) : 0 width: parent.width; height: status === Image.Ready ? Math.min(120, implicitHeight) : 0
visible: status === Image.Ready visible: status === Image.Ready
@ -80,8 +81,8 @@ Rectangle {
model: card.notification.actions model: card.notification.actions
delegate: Rectangle { delegate: Rectangle {
required property var modelData required property var modelData
width: actionLabel.implicitWidth + 16; height: 25; radius: 4; color: "#45475a" width: actionLabel.implicitWidth + 16; height: 25; radius: 4; color: Theme.border
Text { id: actionLabel; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: "#cdd6f4"; font.pixelSize: 11 } 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() } MouseArea { anchors.fill: parent; onClicked: modelData.invoke() }
} }
} }

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import "../components" import "../components"
import "../theme"
Item { Item {
id: indicator id: indicator
@ -19,16 +20,16 @@ Item {
id: bell id: bell
// Le glyphe fa-bell est présent dans JetBrainsMono Nerd Font Mono. // Le glyphe fa-bell est présent dans JetBrainsMono Nerd Font Mono.
text: "" text: ""
color: indicator.inverted ? "#1e1e2e" color: indicator.inverted ? Theme.onAccent
: indicator.criticalUnreadCount > 0 ? "#f38ba8" : "#cba6f7" : indicator.criticalUnreadCount > 0 ? Theme.danger : Theme.accent
// CONTRACT: la cloche gagne en lisibilité sans agrandir le badge ni déplacer la date. // CONTRACT: la cloche gagne en lisibilité sans agrandir le badge ni déplacer la date.
font.pixelSize: 16 font.pixelSize: 16
} }
Text { Text {
id: countLabel id: countLabel
text: indicator.unreadCount > 9 ? "9+" : String(indicator.unreadCount) text: indicator.unreadCount > 9 ? "9+" : String(indicator.unreadCount)
color: indicator.inverted ? "#1e1e2e" color: indicator.inverted ? Theme.onAccent
: indicator.criticalUnreadCount > 0 ? "#f38ba8" : "#cdd6f4" : indicator.criticalUnreadCount > 0 ? Theme.danger : Theme.foreground
font.pixelSize: 10 font.pixelSize: 10
font.bold: true font.bold: true
} }

View file

@ -1,8 +1,24 @@
//@ pragma UseQApplication //@ pragma UseQApplication
import Quickshell import Quickshell
import Quickshell.Io
import "notifications" import "notifications"
import "theme"
ShellRoot { 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. // Une barre par écran, y compris si les sorties changent pendant la session.
// CONTRACT: un seul serveur D-Bus pour toutes les sorties. // CONTRACT: un seul serveur D-Bus pour toutes les sorties.
NotificationService { id: rootNotificationService } NotificationService { id: rootNotificationService }

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.UPower import Quickshell.Services.UPower
import "../components" import "../components"
import "../theme"
Item { Item {
id: indicator id: indicator
@ -42,7 +43,7 @@ Item {
onFileChanged: reload() 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 { NerdIcon {
id: icon id: icon
width: 18 width: 18
@ -57,8 +58,8 @@ Item {
: indicator.percent <= 30 ? "󰁻" : indicator.percent <= 30 ? "󰁻"
: indicator.percent <= 50 ? "󰁽" : indicator.percent <= 50 ? "󰁽"
: indicator.percent <= 75 ? "󰁿" : "󰁹" : indicator.percent <= 75 ? "󰁿" : "󰁹"
color: indicator.percent <= 15 ? "#f38ba8" color: indicator.percent <= 15 ? Theme.danger
: indicator.percent <= 30 ? "#fab387" : "#cdd6f4" : indicator.percent <= 30 ? Theme.urgentForeground : Theme.foreground
} }
Text { Text {
id: value id: value

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import "../components" import "../components"
import "../theme"
Item { Item {
id: indicator id: indicator
@ -16,14 +17,14 @@ Item {
: "Bluetooth activé\n" + connectedDevices.length + " appareils connectés\n" : "Bluetooth activé\n" + connectedDevices.length + " appareils connectés\n"
+ connectedDevices.map(device => device.name).join("\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 { NerdIcon {
anchors.centerIn: parent anchors.centerIn: parent
// WHY: la silhouette Bluetooth est étroite ; seul le dessin grandit, pas la hitbox. // WHY: la silhouette Bluetooth est étroite ; seul le dessin grandit, pas la hitbox.
font.pixelSize: 22 font.pixelSize: 22
text: indicator.adapter && indicator.adapter.enabled ? "" : "󰂲" text: indicator.adapter && indicator.adapter.enabled ? "" : "󰂲"
color: !indicator.adapter || !indicator.adapter.enabled ? "#a6adc8" color: !indicator.adapter || !indicator.adapter.enabled ? Theme.secondaryForeground
: indicator.connectedDevices.length > 0 ? "#cba6f7" : "#cdd6f4" : indicator.connectedDevices.length > 0 ? Theme.accent : Theme.foreground
} }
MouseArea { MouseArea {
id: pointer id: pointer

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell.Networking import Quickshell.Networking
import "../components" import "../components"
import "../theme"
Item { Item {
id: indicator id: indicator
@ -19,7 +20,7 @@ Item {
: !connectedNetwork ? "Wi-Fi\nNon connecté" : !connectedNetwork ? "Wi-Fi\nNon connecté"
: "Wi-Fi\n" + connectedNetwork.name + "\nSignal : " + signalPercent + " %" : "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 { NerdIcon {
anchors.centerIn: parent anchors.centerIn: parent
// WHY: md-wifi remplit mieux la hauteur visuelle que les quatre glyphes // WHY: md-wifi remplit mieux la hauteur visuelle que les quatre glyphes
@ -30,8 +31,8 @@ Item {
text: !Networking.wifiHardwareEnabled ? "󰖪" text: !Networking.wifiHardwareEnabled ? "󰖪"
: !Networking.wifiEnabled ? "󰖪" : !Networking.wifiEnabled ? "󰖪"
: !indicator.connectedNetwork ? "󰤯" : "󰖩" : !indicator.connectedNetwork ? "󰤯" : "󰖩"
color: !Networking.wifiHardwareEnabled ? "#f38ba8" color: !Networking.wifiHardwareEnabled ? Theme.danger
: !Networking.wifiEnabled ? "#a6adc8" : "#cdd6f4" : !Networking.wifiEnabled ? Theme.secondaryForeground : Theme.foreground
} }
MouseArea { MouseArea {
id: pointer id: pointer

View file

@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "../theme"
PopupWindow { PopupWindow {
id: tip id: tip
@ -25,15 +26,15 @@ PopupWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
border.color: "#45475a" border.color: Theme.border
Text { Text {
id: content id: content
anchors.centerIn: parent anchors.centerIn: parent
width: Math.min(304, implicitWidth) width: Math.min(304, implicitWidth)
text: tip.message text: tip.message
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 11 font.pixelSize: 11
} }
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
import "../theme"
Item { Item {
id: entry id: entry
@ -14,7 +15,7 @@ Item {
return heading + (item.tooltipDescription ? "\n" + item.tooltipDescription : ""); 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 { IconImage {
anchors.centerIn: parent anchors.centerIn: parent
width: 18 width: 18
@ -25,7 +26,7 @@ Item {
Rectangle { Rectangle {
visible: entry.item.status === Status.NeedsAttention visible: entry.item.status === Status.NeedsAttention
width: 4; height: 4; radius: 2 width: 4; height: 4; radius: 2
color: "#cba6f7" color: Theme.accent
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import "../components" import "../components"
import "../theme"
Item { Item {
id: indicator id: indicator
@ -46,7 +47,7 @@ Item {
running: !snapshot.loaded running: !snapshot.loaded
onTriggered: snapshot.reload() 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 { NerdIcon {
id: icon id: icon
width: 18 width: 18
@ -56,7 +57,7 @@ Item {
anchors.leftMargin: 3 anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: indicator.hasError ? "" : "󰚰" text: indicator.hasError ? "" : "󰚰"
color: indicator.hasError ? "#fab387" : "#cba6f7" color: indicator.hasError ? Theme.urgentForeground : Theme.accent
} }
Text { Text {
id: count id: count
@ -65,7 +66,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: !indicator.hasError visible: !indicator.hasError
text: indicator.state.totalUpdates text: indicator.state.totalUpdates
color: "#cba6f7" color: Theme.accent
font.family: "JetBrainsMono Nerd Font Mono" font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: 12 font.pixelSize: 12
} }

View file

@ -1,25 +1,26 @@
import QtQuick import QtQuick
import "../theme"
Column { Column {
id: row id: row
required property string label required property string label
required property string value required property string value
required property real percentage required property real percentage
property string accent: "#cba6f7" property color accent: Theme.mauve
width: parent.width width: parent.width
spacing: 6 spacing: 6
Row { Row {
width: parent.width 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 } 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 { Rectangle {
width: parent.width width: parent.width
height: 5 height: 5
radius: 3 radius: 3
color: "#313244" color: Theme.buttonBackground
Rectangle { Rectangle {
width: parent.width * Math.max(0, Math.min(100, row.percentage)) / 100 width: parent.width * Math.max(0, Math.min(100, row.percentage)) / 100
height: parent.height height: parent.height

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import "../components" import "../components"
import "../theme"
PopupWindow { PopupWindow {
id: popup id: popup
@ -24,8 +25,8 @@ PopupWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 4 radius: 4
color: "#1e1e2e" color: Theme.popupBackground
border.color: "#45475a" border.color: Theme.border
Column { Column {
id: content id: content
@ -38,7 +39,7 @@ PopupWindow {
Row { Row {
spacing: 8 spacing: 8
NerdIcon { text: "󰍛"; font.pixelSize: 18 } 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 { MetricRow {
@ -46,7 +47,7 @@ PopupWindow {
percentage: metrics.cpuPercent percentage: metrics.cpuPercent
value: (metrics.cpuPercent >= 0 ? Math.round(metrics.cpuPercent) + " %" : "—") value: (metrics.cpuPercent >= 0 ? Math.round(metrics.cpuPercent) + " %" : "—")
+ (metrics.cpuTemp >= 0 ? " " + Math.round(metrics.cpuTemp) + " °C" : "") + (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 { MetricRow {
@ -55,7 +56,7 @@ PopupWindow {
value: metrics.ramPercent >= 0 value: metrics.ramPercent >= 0
? metrics.ramUsedGiB.toFixed(1) + " / " + metrics.ramTotalGiB.toFixed(1) ? metrics.ramUsedGiB.toFixed(1) + " / " + metrics.ramTotalGiB.toFixed(1)
+ " GiB " + Math.round(metrics.ramPercent) + " %" : "—" + " 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 { MetricRow {
@ -77,15 +78,15 @@ PopupWindow {
Row { Row {
width: parent.width 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 } 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 { Row {
width: parent.width 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 } 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 }
} }
} }
} }

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import "../components" import "../components"
import "../status" import "../status"
import "../theme"
Rectangle { Rectangle {
id: button id: button
@ -10,14 +11,14 @@ Rectangle {
width: 26 width: 26
height: 26 height: 26
radius: 4 radius: 4
color: open ? "#cba6f7" : pointer.containsMouse ? "#45475a" : "#313244" color: open ? Theme.accent : pointer.containsMouse ? Theme.border : Theme.buttonBackground
NerdIcon { NerdIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "󰍛" text: "󰍛"
// WHY: compenser la surface plus faible du glyphe sans agrandir le bouton. // WHY: compenser la surface plus faible du glyphe sans agrandir le bouton.
font.pixelSize: 20 font.pixelSize: 20
color: button.open ? "#1e1e2e" : "#cdd6f4" color: button.open ? Theme.onAccent : Theme.foreground
} }
MouseArea { MouseArea {
id: pointer id: pointer

View file

@ -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 });
}
}

View file

@ -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.

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -0,0 +1,2 @@
singleton Theme 1.0 Theme.qml
singleton AppearanceController 1.0 AppearanceController.qml

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.I3 import Quickshell.I3
import "../components" import "../components"
import "../theme"
Rectangle { Rectangle {
id: button id: button
@ -27,9 +28,9 @@ Rectangle {
: 26 : 26
height: 26 height: 26
radius: 4 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.width: primary ? 1 : 0
border.color: "#cba6f7" border.color: Theme.accent
NerdIcon { NerdIcon {
id: icon id: icon
@ -51,7 +52,7 @@ Rectangle {
anchors.rightMargin: 8 anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: button.fullTitle text: button.fullTitle
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 13 font.pixelSize: 13
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
@ -105,8 +106,8 @@ Rectangle {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 4 radius: 4
color: "#313244" color: Theme.buttonBackground
border.color: "#45475a" border.color: Theme.border
Column { Column {
id: tooltipContent id: tooltipContent
@ -118,14 +119,14 @@ Rectangle {
Text { Text {
text: button.appName text: button.appName
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 12 font.pixelSize: 12
font.bold: true font.bold: true
} }
Text { Text {
width: parent.width width: parent.width
text: button.fullTitle text: button.fullTitle
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 12 font.pixelSize: 12
wrapMode: Text.Wrap wrapMode: Text.Wrap
maximumLineCount: 3 maximumLineCount: 3

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.I3 import Quickshell.I3
import "../theme"
PopupWindow { PopupWindow {
id: menu id: menu
@ -30,8 +31,8 @@ PopupWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: 4 radius: 4
color: "#1e1e2e" color: Theme.popupBackground
border.color: "#45475a" border.color: Theme.border
Column { Column {
id: content id: content
@ -45,7 +46,7 @@ PopupWindow {
width: parent.width width: parent.width
text: menu.entry ? menu.entry.name text: menu.entry ? menu.entry.name
: menu.selectedWindow ? menu.selectedWindow.appId : "" : menu.selectedWindow ? menu.selectedWindow.appId : ""
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 15 font.pixelSize: 15
font.bold: true font.bold: true
elide: Text.ElideRight elide: Text.ElideRight
@ -53,7 +54,7 @@ PopupWindow {
Text { Text {
text: "Fenêtres ouvertes" text: "Fenêtres ouvertes"
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 11 font.pixelSize: 11
} }
@ -65,7 +66,7 @@ PopupWindow {
width: content.width width: content.width
height: 30 height: 30
radius: 4 radius: 4
color: pointer.containsMouse ? "#45475a" : "#313244" color: pointer.containsMouse ? Theme.border : Theme.buttonBackground
Text { Text {
anchors.left: parent.left anchors.left: parent.left
@ -74,7 +75,7 @@ PopupWindow {
anchors.rightMargin: 8 anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData.title text: modelData.title
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 12 font.pixelSize: 12
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -94,7 +95,7 @@ PopupWindow {
Text { Text {
visible: menu.actions.length > 0 visible: menu.actions.length > 0
text: "Actions" text: "Actions"
color: "#a6adc8" color: Theme.secondaryForeground
font.pixelSize: 11 font.pixelSize: 11
} }
@ -106,7 +107,7 @@ PopupWindow {
width: content.width width: content.width
height: 30 height: 30
radius: 4 radius: 4
color: actionPointer.containsMouse ? "#45475a" : "#313244" color: actionPointer.containsMouse ? Theme.border : Theme.buttonBackground
Text { Text {
anchors.left: parent.left anchors.left: parent.left
@ -115,7 +116,7 @@ PopupWindow {
anchors.rightMargin: 8 anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData.name text: modelData.name
color: "#cdd6f4" color: Theme.foreground
font.pixelSize: 12 font.pixelSize: 12
elide: Text.ElideRight elide: Text.ElideRight
} }

View file

@ -1,5 +1,11 @@
# Configuration SwayFX # 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 Le compositeur actif est **SwayFX 0.6** avec `scenefx`. La session UWSM
sélectionne `sway.desktop` et charge `config` : `bind`, `input`, `rules`, sélectionne `sway.desktop` et charge `config` : `bind`, `input`, `rules`,
`style`, `swayfx` et `autostart`. `eDP-1` est l'unique écran configuré, à `style`, `swayfx` et `autostart`. `eDP-1` est l'unique écran configuré, à

View file

@ -7,7 +7,7 @@
# ================================================= # =================================================
# Listener IPC démarré une fois par session ; exec ne se rejoue pas au reload. # 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 # Règles d'assignation automatique

View file

@ -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

View file

@ -10,10 +10,13 @@
# #
# Style # 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 # 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.focused_inactive $overlay0 $base $text $rosewater $overlay0
client.unfocused $overlay0 $base $text $rosewater $overlay0 client.unfocused $overlay0 $base $text $rosewater $overlay0
client.urgent $peach $base $peach $overlay0 $peach client.urgent $peach $base $peach $overlay0 $peach

View file

@ -5,8 +5,8 @@ corner_radius 4
shadows enable shadows enable
shadows_on_csd disable shadows_on_csd disable
shadow_blur_radius 10 shadow_blur_radius 10
shadow_color #00000055 shadow_color $shadow_active
shadow_inactive_color #00000035 shadow_inactive_color $shadow_inactive
shadow_offset 0 2 shadow_offset 0 2
default_dim_inactive 0 default_dim_inactive 0

View file

@ -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

View file

@ -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()