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__/
*.py[cod]
**/node_modules/
sway/.config/sway/generated/theme.conf
sway/.config/sway/generated/theme.conf.tmp
# Local editor/application backups
*.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 "status"
import "systemmonitor"
import "theme"
PanelWindow {
id: bar
@ -37,7 +38,7 @@ PanelWindow {
// layer-shell est focusable dès son ouverture. OnDemand n'accapare pas
// le clavier hors interaction avec la barre ou son popup.
focusable: true
color: "#eb1e1e2e"
color: Theme.panelBackground
// CONTRACT: toute la capsule centrale partage le même popup et la même exclusion XOR.
function toggleDateCenter() {

View file

@ -1,5 +1,8 @@
# QuickShell labfy-sway
Le [moteur de thème commun](theme/README.md) fournit les couleurs effectives
à QuickShell et Sway/SwayFX depuis une seule palette Catppuccin versionnée.
Barre principale et serveur de notifications de la session SwayFX.
```text

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import "../components"
import "../theme"
Item {
id: page
@ -61,10 +62,10 @@ Item {
Text {
width: parent.width - 60; height: 30; verticalAlignment: Text.AlignVCenter
text: page.stage === "main" ? "Bluetooth" : page.nameOf(page.selectedDevice)
color: "#cdd6f4"; font.pixelSize: 16; font.bold: true; elide: Text.ElideRight
color: Theme.foreground; font.pixelSize: 16; font.bold: true; elide: Text.ElideRight
}
}
Text { width: parent.width; visible: page.message.length > 0; text: page.message; wrapMode: Text.Wrap; color: "#f9e2af"; font.pixelSize: 12 }
Text { width: parent.width; visible: page.message.length > 0; text: page.message; wrapMode: Text.Wrap; color: Theme.warningForeground; font.pixelSize: 12 }
Loader {
width: parent.width; height: Math.max(0, parent.height - y)
sourceComponent: page.stage === "main" ? mainComponent : page.stage === "details"
@ -80,22 +81,22 @@ Item {
width: page.width; spacing: 10
Row {
width: parent.width; spacing: 8
Text { width: parent.width - 100; text: !page.adapter ? "Adaptateur indisponible" : page.adapter.enabled ? "Bluetooth activé" : "Bluetooth désactivé"; color: "#cdd6f4"; font.pixelSize: 13 }
Text { width: parent.width - 100; text: !page.adapter ? "Adaptateur indisponible" : page.adapter.enabled ? "Bluetooth activé" : "Bluetooth désactivé"; color: Theme.foreground; font.pixelSize: 13 }
ActionButton { label: page.adapter && page.adapter.enabled ? "Désactiver" : "Activer"; enabled: !!page.adapter; onClicked: page.adapter.enabled = !page.adapter.enabled }
}
Text { text: page.scanWanted ? "Recherche d'appareils…" : "Scan arrêté"; color: "#a6adc8"; font.pixelSize: 11 }
Text { visible: page.devices.length === 0; text: "Aucun appareil connu"; color: "#a6adc8"; font.pixelSize: 12 }
Text { visible: page.connectedDevices.length > 0; text: "Connectés"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 }
Text { text: page.scanWanted ? "Recherche d'appareils…" : "Scan arrêté"; color: Theme.secondaryForeground; font.pixelSize: 11 }
Text { visible: page.devices.length === 0; text: "Aucun appareil connu"; color: Theme.secondaryForeground; font.pixelSize: 12 }
Text { visible: page.connectedDevices.length > 0; text: "Connectés"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 }
Repeater {
model: ScriptModel { values: page.connectedDevices; objectProp: "address" }
delegate: deviceRowComponent
}
Text { visible: page.pairedDevices.length > 0; text: "Appairés"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 }
Text { visible: page.pairedDevices.length > 0; text: "Appairés"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 }
Repeater {
model: ScriptModel { values: page.pairedDevices; objectProp: "address" }
delegate: deviceRowComponent
}
Text { visible: page.newDevices.length > 0; text: "Nouveaux appareils"; color: "#cba6f7"; font.bold: true; font.pixelSize: 13 }
Text { visible: page.newDevices.length > 0; text: "Nouveaux appareils"; color: Theme.accentForeground; font.bold: true; font.pixelSize: 13 }
Repeater {
model: ScriptModel { values: page.newDevices; objectProp: "address" }
delegate: deviceRowComponent
@ -109,7 +110,7 @@ Item {
required property var modelData
readonly property var device: modelData
width: page.width; height: 48; radius: 4
color: pointer.containsMouse ? "#45475a" : "#313244"
color: pointer.containsMouse ? Theme.border : Theme.buttonBackground
Image {
id: iconImage; anchors.left: parent.left; anchors.leftMargin: 9; anchors.verticalCenter: parent.verticalCenter
width: 19; height: 19; source: device.icon ? Quickshell.iconPath(device.icon, true) : ""
@ -117,13 +118,13 @@ Item {
}
NerdIcon {
anchors.centerIn: iconImage; visible: !iconImage.visible
text: "󰂯"; color: "#cba6f7"; font.pixelSize: 17
text: "󰂯"; color: Theme.accentForeground; font.pixelSize: 17
}
Column {
anchors.left: parent.left; anchors.leftMargin: 38; anchors.right: parent.right
anchors.rightMargin: 10; anchors.verticalCenter: parent.verticalCenter; spacing: 2
Text { width: parent.width; text: page.nameOf(device); color: "#cdd6f4"; font.pixelSize: 12; elide: Text.ElideRight }
Text { text: page.stateOf(device) + (device.batteryAvailable ? " • " + Math.round(device.battery * 100) + " %" : ""); color: "#a6adc8"; font.pixelSize: 11 }
Text { width: parent.width; text: page.nameOf(device); color: Theme.foreground; font.pixelSize: 12; elide: Text.ElideRight }
Text { text: page.stateOf(device) + (device.batteryAvailable ? " • " + Math.round(device.battery * 100) + " %" : ""); color: Theme.secondaryForeground; font.pixelSize: 11 }
}
MouseArea { id: pointer; anchors.fill: parent; hoverEnabled: true; onClicked: page.choose(device) }
}
@ -134,10 +135,10 @@ Item {
clip: true; ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: page.width; spacing: 10
Text { text: page.selectedDevice ? page.stateOf(page.selectedDevice) : "Appareil indisponible"; color: "#cdd6f4"; font.pixelSize: 13 }
Text { visible: !!page.selectedDevice && page.selectedDevice.deviceName !== page.selectedDevice.name; text: "Nom d'origine : " + (page.selectedDevice ? page.selectedDevice.deviceName : ""); color: "#a6adc8"; font.pixelSize: 11 }
Text { visible: !!page.selectedDevice && page.selectedDevice.batteryAvailable; text: "Batterie : " + (page.selectedDevice ? Math.round(page.selectedDevice.battery * 100) : 0) + " %"; color: "#cdd6f4"; font.pixelSize: 12 }
Text { text: page.selectedDevice ? "Appairé : " + (page.selectedDevice.paired ? "oui" : "non") + " • Lié : " + (page.selectedDevice.bonded ? "oui" : "non") : ""; color: "#a6adc8"; font.pixelSize: 11 }
Text { text: page.selectedDevice ? page.stateOf(page.selectedDevice) : "Appareil indisponible"; color: Theme.foreground; font.pixelSize: 13 }
Text { visible: !!page.selectedDevice && page.selectedDevice.deviceName !== page.selectedDevice.name; text: "Nom d'origine : " + (page.selectedDevice ? page.selectedDevice.deviceName : ""); color: Theme.secondaryForeground; font.pixelSize: 11 }
Text { visible: !!page.selectedDevice && page.selectedDevice.batteryAvailable; text: "Batterie : " + (page.selectedDevice ? Math.round(page.selectedDevice.battery * 100) : 0) + " %"; color: Theme.foreground; font.pixelSize: 12 }
Text { text: page.selectedDevice ? "Appairé : " + (page.selectedDevice.paired ? "oui" : "non") + " • Lié : " + (page.selectedDevice.bonded ? "oui" : "non") : ""; color: Theme.secondaryForeground; font.pixelSize: 11 }
Row {
spacing: 8
ActionButton {
@ -159,37 +160,37 @@ Item {
}
Row {
spacing: 8
Text { width: 165; text: "Appareil de confiance"; color: "#cdd6f4"; font.pixelSize: 12 }
Text { width: 165; text: "Appareil de confiance"; color: Theme.foreground; font.pixelSize: 12 }
ActionButton { label: page.selectedDevice && page.selectedDevice.trusted ? "Activé" : "Désactivé"; enabled: !!page.selectedDevice && page.selectedDevice.paired; onClicked: page.selectedDevice.trusted = !page.selectedDevice.trusted }
}
Row {
spacing: 8
Text { width: 165; text: "Bloqué"; color: "#cdd6f4"; font.pixelSize: 12 }
Text { width: 165; text: "Bloqué"; color: Theme.foreground; font.pixelSize: 12 }
ActionButton { label: page.selectedDevice && page.selectedDevice.blocked ? "Oui" : "Non"; enabled: !!page.selectedDevice; onClicked: { if (page.selectedDevice.blocked) page.selectedDevice.blocked = false; else page.stage = "block"; } }
}
Text { width: parent.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: "#a6adc8"; font.pixelSize: 11 }
Text { width: parent.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: Theme.secondaryForeground; font.pixelSize: 11 }
Row {
visible: !!page.selectedDevice && page.selectedDevice.paired
&& page.adapter && page.adapter.enabled
&& page.selectedDevice.wakeAllowed !== undefined
spacing: 8
Text { width: 165; text: "Autoriser le réveil"; color: "#cdd6f4"; font.pixelSize: 12 }
Text { width: 165; text: "Autoriser le réveil"; color: Theme.foreground; font.pixelSize: 12 }
ActionButton { label: page.selectedDevice && page.selectedDevice.wakeAllowed ? "Activé" : "Désactivé"; onClicked: page.selectedDevice.wakeAllowed = !page.selectedDevice.wakeAllowed }
}
Text { text: "Nom local"; color: "#cba6f7"; font.pixelSize: 13; font.bold: true }
Text { text: "Nom local"; color: Theme.accentForeground; font.pixelSize: 13; font.bold: true }
TextField {
id: aliasInput; width: page.width
text: page.selectedDevice ? page.selectedDevice.name : ""
placeholderText: "Alias local"; selectByMouse: true
color: "#cdd6f4"; placeholderTextColor: "#6c7086"
background: Rectangle { color: "#313244"; border.color: aliasInput.activeFocus ? "#cba6f7" : "#585b70"; radius: 4 }
color: Theme.foreground; placeholderTextColor: Theme.disabledForeground
background: Rectangle { color: Theme.buttonBackground; border.color: aliasInput.activeFocus ? Theme.accent : Theme.strongBorder; radius: 4 }
}
Row {
spacing: 8
ActionButton { label: "Réinitialiser"; enabled: !!page.selectedDevice; onClicked: { page.selectedDevice.name = ""; aliasInput.text = page.selectedDevice.name; } }
ActionButton { label: "Enregistrer"; enabled: !!page.selectedDevice; onClicked: page.selectedDevice.name = aliasInput.text.trim() }
}
Text { text: "Informations techniques : " + (page.selectedDevice ? page.selectedDevice.address : ""); color: "#6c7086"; font.pixelSize: 10 }
Text { text: "Informations techniques : " + (page.selectedDevice ? page.selectedDevice.address : ""); color: Theme.disabledForeground; font.pixelSize: 10 }
}
}
}
@ -197,8 +198,8 @@ Item {
id: forgetComponent
Column {
spacing: 10
Text { width: page.width; wrapMode: Text.Wrap; text: "Oublier " + page.nameOf(page.selectedDevice) + " ?"; color: "#f38ba8"; font.pixelSize: 13 }
Text { width: page.width; wrapMode: Text.Wrap; text: "Cela supprimera les informations de pairage."; color: "#a6adc8"; font.pixelSize: 12 }
Text { width: page.width; wrapMode: Text.Wrap; text: "Oublier " + page.nameOf(page.selectedDevice) + " ?"; color: Theme.danger; font.pixelSize: 13 }
Text { width: page.width; wrapMode: Text.Wrap; text: "Cela supprimera les informations de pairage."; color: Theme.secondaryForeground; font.pixelSize: 12 }
Row {
spacing: 8
ActionButton { label: "Annuler"; onClicked: page.stage = "details" }
@ -210,8 +211,8 @@ Item {
id: blockComponent
Column {
spacing: 10
Text { width: page.width; wrapMode: Text.Wrap; text: "Bloquer " + page.nameOf(page.selectedDevice) + " ?"; color: "#f38ba8"; font.pixelSize: 13 }
Text { width: page.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: "#a6adc8"; font.pixelSize: 12 }
Text { width: page.width; wrapMode: Text.Wrap; text: "Bloquer " + page.nameOf(page.selectedDevice) + " ?"; color: Theme.danger; font.pixelSize: 13 }
Text { width: page.width; wrapMode: Text.Wrap; text: "Un appareil bloqué ne peut pas se connecter."; color: Theme.secondaryForeground; font.pixelSize: 12 }
Row {
spacing: 8
ActionButton { label: "Annuler"; onClicked: page.stage = "details" }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import QtQuick
import Quickshell.Services.UPower
import "../components"
import "../theme"
Item {
id: powerControl
@ -22,7 +23,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
text: ""
font.pixelSize: 18
color: "#cdd6f4"
color: Theme.foreground
}
Text {
@ -30,7 +31,7 @@ Item {
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: "Profil d'énergie"
color: "#cdd6f4"
color: Theme.foreground
font.pixelSize: 14
}
}
@ -57,12 +58,12 @@ Item {
width: (choices.width - 2 * choices.spacing) / 3
height: choices.height
radius: 4
color: !available ? "#313244" : selected ? "#cba6f7" : hover.hovered ? "#45475a" : "#313244"
color: !available ? Theme.buttonBackground : selected ? Theme.accent : hover.hovered ? Theme.border : Theme.buttonBackground
Text {
anchors.centerIn: parent
text: parent.modelData.label
color: !parent.available ? "#6c7086" : parent.selected ? "#1e1e2e" : "#cdd6f4"
color: !parent.available ? Theme.disabledForeground : parent.selected ? Theme.onAccent : Theme.foreground
font.pixelSize: 12
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import "../components"
import "../theme"
Rectangle {
id: card
@ -19,7 +20,7 @@ Rectangle {
implicitHeight: bodyText.visible || image.visible || actions.visible ? 95 + image.height + actions.height : 64
height: implicitHeight
radius: 4
color: "#313244"
color: Theme.buttonBackground
Column {
anchors.fill: parent; anchors.margins: 10; spacing: 4
Row {
@ -27,21 +28,21 @@ Rectangle {
Item {
width: 14; height: 14
Image { id: appIcon; anchors.fill: parent; source: card.iconSource; fillMode: Image.PreserveAspectFit; visible: status === Image.Ready }
NerdIcon { anchors.centerIn: parent; visible: !appIcon.visible; text: ""; color: "#cba6f7"; font.pixelSize: 13 }
NerdIcon { anchors.centerIn: parent; visible: !appIcon.visible; text: ""; color: Theme.accent; font.pixelSize: 13 }
}
Text {
width: parent.width - 48
text: card.notification.appName || card.notification.desktopEntry || "Notification"
textFormat: Text.PlainText; color: "#a6adc8"; font.pixelSize: 11; elide: Text.ElideRight
textFormat: Text.PlainText; color: Theme.secondaryForeground; font.pixelSize: 11; elide: Text.ElideRight
}
Text { visible: !!card.live; text: "●"; color: "#a6e3a1"; font.pixelSize: 10 }
Text { visible: !!card.live; text: "●"; color: Theme.successForeground; font.pixelSize: 10 }
NerdIcon {
text: ""; color: closePointer.containsMouse ? "#f38ba8" : "#a6adc8"; font.pixelSize: 12
text: ""; color: closePointer.containsMouse ? Theme.danger : Theme.secondaryForeground; font.pixelSize: 12
MouseArea { id: closePointer; anchors.fill: parent; anchors.margins: -5; hoverEnabled: true; onClicked: card.service.dismiss(card.notification.internalId) }
}
}
Text { width: parent.width; text: card.notification.summary; textFormat: Text.PlainText; color: "#cdd6f4"; font.pixelSize: 12; font.bold: true; elide: Text.ElideRight }
Text { id: bodyText; width: parent.width; visible: text.length > 0; text: card.notification.body || ""; textFormat: Text.PlainText; color: "#bac2de"; font.pixelSize: 11; wrapMode: Text.Wrap; maximumLineCount: 2; elide: Text.ElideRight }
Text { width: parent.width; text: card.notification.summary; textFormat: Text.PlainText; color: Theme.foreground; font.pixelSize: 12; font.bold: true; elide: Text.ElideRight }
Text { id: bodyText; width: parent.width; visible: text.length > 0; text: card.notification.body || ""; textFormat: Text.PlainText; color: Theme.subtext1; font.pixelSize: 11; wrapMode: Text.Wrap; maximumLineCount: 2; elide: Text.ElideRight }
Image { id: image; width: parent.width; height: status === Image.Ready ? Math.min(110, implicitHeight) : 0; visible: status === Image.Ready; source: card.notification.image || ""; fillMode: Image.PreserveAspectFit }
Flow {
id: actions
@ -53,8 +54,8 @@ Rectangle {
model: card.live ? card.live.actions : []
delegate: Rectangle {
required property var modelData
width: label.implicitWidth + 16; height: 24; radius: 4; color: "#45475a"
Text { id: label; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: "#cdd6f4"; font.pixelSize: 11 }
width: label.implicitWidth + 16; height: 24; radius: 4; color: Theme.border
Text { id: label; anchors.centerIn: parent; text: modelData.text; textFormat: Text.PlainText; color: Theme.foreground; font.pixelSize: 11 }
MouseArea { anchors.fill: parent; onClicked: modelData.invoke() }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,24 @@
//@ pragma UseQApplication
import Quickshell
import Quickshell.Io
import "notifications"
import "theme"
ShellRoot {
// CONTRACT: seul ce point d'entrée reçoit les changements live du backend.
// Aucun état de thème n'est écrit par Theme.qml.
IpcHandler {
target: "appearance"
function setTheme(flavor: string, accent: string): bool {
return AppearanceController.applyTheme(flavor, accent);
}
function currentTheme(): string {
return AppearanceController.effectiveFlavor + "/" + AppearanceController.effectiveAccent;
}
function effectiveState(): string {
return AppearanceController.effectiveState();
}
}
// Une barre par écran, y compris si les sorties changent pendant la session.
// CONTRACT: un seul serveur D-Bus pour toutes les sorties.
NotificationService { id: rootNotificationService }

View file

@ -2,6 +2,7 @@ import QtQuick
import Quickshell.Io
import Quickshell.Services.UPower
import "../components"
import "../theme"
Item {
id: indicator
@ -42,7 +43,7 @@ Item {
onFileChanged: reload()
}
Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? "#45475a" : "transparent" }
Rectangle { anchors.fill: parent; radius: 4; color: pointer.containsMouse ? Theme.border : "transparent" }
NerdIcon {
id: icon
width: 18
@ -57,8 +58,8 @@ Item {
: indicator.percent <= 30 ? "󰁻"
: indicator.percent <= 50 ? "󰁽"
: indicator.percent <= 75 ? "󰁿" : "󰁹"
color: indicator.percent <= 15 ? "#f38ba8"
: indicator.percent <= 30 ? "#fab387" : "#cdd6f4"
color: indicator.percent <= 15 ? Theme.danger
: indicator.percent <= 30 ? Theme.urgentForeground : Theme.foreground
}
Text {
id: value

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,25 +1,26 @@
import QtQuick
import "../theme"
Column {
id: row
required property string label
required property string value
required property real percentage
property string accent: "#cba6f7"
property color accent: Theme.mauve
width: parent.width
spacing: 6
Row {
width: parent.width
Text { id: labelText; text: row.label; color: "#cdd6f4"; font.pixelSize: 12 }
Text { id: labelText; text: row.label; color: Theme.foreground; font.pixelSize: 12 }
Item { width: Math.max(0, parent.width - valueText.width - labelText.width); height: 1 }
Text { id: valueText; text: row.value; color: "#bac2de"; font.pixelSize: 12 }
Text { id: valueText; text: row.value; color: Theme.subtext1; font.pixelSize: 12 }
}
Rectangle {
width: parent.width
height: 5
radius: 3
color: "#313244"
color: Theme.buttonBackground
Rectangle {
width: parent.width * Math.max(0, Math.min(100, row.percentage)) / 100
height: parent.height

View file

@ -1,6 +1,7 @@
import QtQuick
import Quickshell
import "../components"
import "../theme"
PopupWindow {
id: popup
@ -24,8 +25,8 @@ PopupWindow {
Rectangle {
anchors.fill: parent
radius: 4
color: "#1e1e2e"
border.color: "#45475a"
color: Theme.popupBackground
border.color: Theme.border
Column {
id: content
@ -38,7 +39,7 @@ PopupWindow {
Row {
spacing: 8
NerdIcon { text: "󰍛"; font.pixelSize: 18 }
Text { text: "Moniteur système"; color: "#cdd6f4"; font.pixelSize: 16; font.bold: true }
Text { text: "Moniteur système"; color: Theme.foreground; font.pixelSize: 16; font.bold: true }
}
MetricRow {
@ -46,7 +47,7 @@ PopupWindow {
percentage: metrics.cpuPercent
value: (metrics.cpuPercent >= 0 ? Math.round(metrics.cpuPercent) + " %" : "—")
+ (metrics.cpuTemp >= 0 ? " " + Math.round(metrics.cpuTemp) + " °C" : "")
accent: metrics.cpuPercent >= 90 ? "#f38ba8" : metrics.cpuPercent >= 70 ? "#fab387" : "#cba6f7"
accent: metrics.cpuPercent >= 90 ? Theme.danger : metrics.cpuPercent >= 70 ? Theme.urgent : Theme.mauve
}
MetricRow {
@ -55,7 +56,7 @@ PopupWindow {
value: metrics.ramPercent >= 0
? metrics.ramUsedGiB.toFixed(1) + " / " + metrics.ramTotalGiB.toFixed(1)
+ " GiB " + Math.round(metrics.ramPercent) + " %" : "—"
accent: metrics.ramPercent >= 90 ? "#f38ba8" : metrics.ramPercent >= 75 ? "#fab387" : "#cba6f7"
accent: metrics.ramPercent >= 90 ? Theme.danger : metrics.ramPercent >= 75 ? Theme.urgent : Theme.mauve
}
MetricRow {
@ -77,15 +78,15 @@ PopupWindow {
Row {
width: parent.width
Text { id: loadLabel; text: "Charge (1/5/15 min)"; color: "#cdd6f4"; font.pixelSize: 12 }
Text { id: loadLabel; text: "Charge (1/5/15 min)"; color: Theme.foreground; font.pixelSize: 12 }
Item { width: Math.max(0, parent.width - loadValue.width - loadLabel.width); height: 1 }
Text { id: loadValue; text: metrics.loadAverage || "—"; color: "#bac2de"; font.pixelSize: 12 }
Text { id: loadValue; text: metrics.loadAverage || "—"; color: Theme.subtext1; font.pixelSize: 12 }
}
Row {
width: parent.width
Text { id: uptimeLabel; text: "En marche"; color: "#cdd6f4"; font.pixelSize: 12 }
Text { id: uptimeLabel; text: "En marche"; color: Theme.foreground; font.pixelSize: 12 }
Item { width: Math.max(0, parent.width - uptimeValue.width - uptimeLabel.width); height: 1 }
Text { id: uptimeValue; text: metrics.uptimeText || "—"; color: "#bac2de"; font.pixelSize: 12 }
Text { id: uptimeValue; text: metrics.uptimeText || "—"; color: Theme.subtext1; font.pixelSize: 12 }
}
}
}

View file

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

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

View file

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

View file

@ -1,5 +1,11 @@
# Configuration SwayFX
Les couleurs et variables d'apparence viennent du
[moteur commun](../../../../quickshell/.config/quickshell/labfy-sway/theme/README.md).
`theme-default.conf` assure le démarrage Mocha avant génération ;
`generated/theme.conf` est l'état runtime créé atomiquement par
`bin/.local/bin/generate-appearance.py` depuis la palette QuickShell.
Le compositeur actif est **SwayFX 0.6** avec `scenefx`. La session UWSM
sélectionne `sway.desktop` et charge `config` : `bind`, `input`, `rules`,
`style`, `swayfx` et `autostart`. `eDP-1` est l'unique écran configuré, à

View file

@ -7,7 +7,7 @@
# =================================================
# Listener IPC démarré une fois par session ; exec ne se rejoue pas au reload.
exec python3 ~/.config/sway/scripts/inactive-windows-transparency.py
exec python3 ~/.config/sway/scripts/inactive-windows-transparency.py --opacity $inactive_opacity
# =================================================
# Règles d'assignation automatique

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
#
include ~/.config/sway/catppuccin-mocha
# CONTRACT: le glob vide est valide dans Sway ; le repli fonctionne dès Stow.
# Le fichier runtime, s'il existe, remplace les variables sans palette parallèle.
include ~/.config/sway/theme-default.conf
include ~/.config/sway/generated/*.conf
# target title bg text indicator border
client.focused $lavender $base $text $rosewater $lavender
client.focused $accent $base $text $rosewater $accent
client.focused_inactive $overlay0 $base $text $rosewater $overlay0
client.unfocused $overlay0 $base $text $rosewater $overlay0
client.urgent $peach $base $peach $overlay0 $peach

View file

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

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