Compare commits

...

10 Commits

Author SHA1 Message Date
7d4d54c5b8 docs(readme): réécriture — rice compatible COSMIC + Hyprland, léger et cohérent 2026-03-16 01:34:07 +01:00
Tetardtek
f0adb2a291 Merge features/media-panel → main 2026-02-23 22:17:25 +01:00
Tetardtek
01133b78f0 feat(media-popup): intégrer contrôleur MPRIS avec album art et contrôles
Section LECTURE apparaît automatiquement si un lecteur est actif (playerctl).
- Album art chargé en arrière-plan (file:// et http/https), coins arrondis
  dessinés en Cairo avec fallback icône note de musique
- Contrôles prev / play-pause / next via playerctl
- Titre et artiste avec ellipsis, rafraîchis toutes les 2s
- Suppression de vc-brightness-popup.py et vc-volume-popup.py (fusionnés)
Dépendances apt ajoutées : gir1.2-gdkpixbuf-2.0, gir1.2-pango-1.0
2026-02-23 22:16:57 +01:00
Tetardtek
50f84afb66 fix(waybar/power-profile): masquer le module sur PC sans ACPI platform_profile
Retourne un JSON avec text vide et class 'unavailable' si
/sys/firmware/acpi/platform_profile est absent (PC fixe, VM),
évitant l'affichage d'une erreur dans la barre.
2026-02-23 22:16:47 +01:00
Tetardtek
59c39260d1 fix(waybar/network): détecter l'interface active via la route par défaut
Remplace la liste codée en dur d'interfaces (enp7s0, wlp8s0…) par
'ip route get 1.1.1.1' qui retourne l'interface réellement utilisée,
portable sur n'importe quelle machine sans configuration.
2026-02-23 22:16:41 +01:00
Tetardtek
0ba6bbd181 feat(waybar): modules cpu-temp et disks portables par scripts
Remplace les modules natifs temperature (thermal-zone codé en dur) et
disk (chemin fixe /) par des custom scripts auto-détectés.
- cpu-temp.sh : détecte x86_pkg_temp / k10temp / coretemp via thermal_zone
  et hwmon, émet warning à 65° et critical à 80°
- disks.sh : liste tous les FS montés réels, exclut snap/tmpfs/efi,
  affiche icône selon le point de montage, tooltip détaillé
CSS : styles warning + hover ajoutés pour les deux modules
2026-02-23 22:16:05 +01:00
Tetardtek
1690ec5eb4 fix(waybar/autostart): empêcher le double démarrage via flock
Sur Pop!_OS 24.04, systemd-xdg-autostart-generator et cosmic-session
traitent tous deux ~/.config/autostart/ → deux instances waybar.
Utilise flock sur un fichier verrou /tmp pour n'en démarrer qu'une.
2026-02-23 22:14:51 +01:00
Tetardtek
2f3fc71ab7 feat(media-panel): sélecteur sortie/entrée audio
- Sélecteur de sortie (SORTIE) : liste verticale des sinks disponibles,
  filtre les SUSPENDED (HDMI non branchés), actif surligné en rose
- Sélecteur d'entrée (ENTRÉE) : même logique, filtre les .monitor
  (loopbacks), garde les vrais micros même SUSPENDED
- Popup ancré à droite (plus jamais hors écran)
- LANG=C pour pactl (indépendant de la locale système)
2026-02-23 19:35:10 +01:00
Tetardtek
40850161a5 Merge features/waybar → main
Waybar island floating 3-pills, scripts GPU/réseau/profil énergie,
overlay wob-overlay.py, popup média vc-media-popup.py, fix install
GTK (adw-gtk3-dark + variables COSMIC exactes), protection root.
2026-02-23 19:02:31 +01:00
Tetardtek
8ee25d7853 fix(install): corriger le thème GTK sur fresh install
- Ajouter adw-gtk3 aux paquets apt (base du dark theme GTK3)
- Déployer violet-chaton-gtk.css sur gtk-3.0 ET gtk-4.0
- Appliquer gsettings gtk-theme=adw-gtk3-dark + color-scheme=prefer-dark
- Réécrire violet-chaton-gtk.css avec les variables exactes de COSMIC dark.css
  (les anciennes règles CSS explicites étaient ignorées par adw-gtk3-dark)
- Corriger deploy_file pour ne pas suivre les symlinks COSMIC (évite
  d'écraser cosmic/dark.css par erreur)
- Bloquer l'exécution en tant que root (causait des erreurs mkdir)
- Renommer CosmicTheme.Light/name en Violet-chaton pour cohérence
2026-02-23 18:56:47 +01:00
16 changed files with 793 additions and 1339 deletions

View File

@@ -2,7 +2,7 @@
Name=Waybar
Comment=violet-chaton status bar
Type=Application
Exec=waybar
Exec=bash -c "exec 9>/tmp/waybar-start.lock; flock -n 9 || exit 0; exec waybar"
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true

View File

@@ -20,10 +20,10 @@
"custom/launcher",
"custom/sep",
"cpu",
"temperature",
"custom/cpu-temp",
"custom/gpu",
"memory",
"disk",
"custom/disks",
"custom/sep",
"custom/network"
],
@@ -77,15 +77,13 @@
"interval": 2
},
// ── Température ─────────────────────────────────────────────────────────
// ── Température CPU (auto-détection) ────────────────────────────────────
"temperature": {
"thermal-zone": 9,
"format": " {temperatureC}°",
"format-critical": " {temperatureC}°",
"critical-threshold": 80,
"tooltip": false,
"interval": 2
"custom/cpu-temp": {
"exec": "~/.config/waybar/scripts/cpu-temp.sh",
"return-type": "json",
"interval": 2,
"format": "{}"
},
// ── GPU ─────────────────────────────────────────────────────────────────
@@ -109,12 +107,13 @@
"interval": 2
},
// ── Disque ──────────────────────────────────────────────────────────────
// ── Disques (auto-détection) ─────────────────────────────────────────────
"disk": {
"format": "󰋊 {used}",
"tooltip-format": "󰋊 Disque /\n{used} / {total}\n{percentage_used}% utilisé",
"interval": 30
"custom/disks": {
"exec": "~/.config/waybar/scripts/disks.sh",
"return-type": "json",
"interval": 30,
"format": "{}"
},
// ── Réseau ──────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# cpu-temp.sh — température CPU auto-détection → JSON waybar
# Priorité 1 : thermal zone x86_pkg_temp (Intel) ou k10temp (AMD)
# Priorité 2 : hwmon coretemp / k10temp / zenpower
# Retourne vide si aucune source trouvée
emit() {
local temp=$1
local cls="normal"
(( temp >= 80 )) && cls="critical"
(( temp >= 65 && temp < 80 )) && cls="warning"
printf '{"text":" %d°","tooltip":"CPU %d°C","class":"%s","percentage":%d}\n' \
"$temp" "$temp" "$cls" "$temp"
exit 0
}
# Priorité 1 — thermal_zone x86_pkg_temp (Intel) / TCPU / k10temp (AMD)
for zone in /sys/class/thermal/thermal_zone*/; do
zone_type=$(cat "${zone}type" 2>/dev/null) || continue
case "$zone_type" in
x86_pkg_temp|k10temp|TCPU|cpu_thermal)
temp_raw=$(cat "${zone}temp" 2>/dev/null) || continue
emit $(( temp_raw / 1000 ))
;;
esac
done
# Priorité 2 — hwmon coretemp (Intel desktop) ou k10temp (AMD)
for hw in /sys/class/hwmon/hwmon*/; do
hw_name=$(cat "${hw}name" 2>/dev/null) || continue
case "$hw_name" in
coretemp|k10temp|zenpower|amd_energy)
for f in "${hw}temp1_input" "${hw}temp2_input"; do
[[ -r "$f" ]] || continue
temp_raw=$(cat "$f" 2>/dev/null) || continue
emit $(( temp_raw / 1000 ))
done
;;
esac
done
# Aucune source — module masqué
printf '{"text":"","class":"unavailable"}\n'

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# disks.sh — liste les vrais systèmes de fichiers montés → JSON waybar
# Exclut tmpfs, devtmpfs, squashfs (snap), overlay, efi, etc.
TEXT=""
TOOLTIP="󰋊 Disques\n"
while IFS= read -r line; do
fs=$(awk '{print $1}' <<< "$line")
size=$(awk '{print $2}' <<< "$line")
used=$(awk '{print $3}' <<< "$line")
avail=$(awk '{print $4}' <<< "$line")
pct=$(awk '{print $5}' <<< "$line")
mnt=$(awk '{print $6}' <<< "$line")
# Exclure mounts sans intérêt
[[ "$mnt" == /snap/* ]] && continue
[[ "$mnt" == /boot/efi ]] && continue
[[ "$mnt" == /boot ]] && continue
[[ "$mnt" == /recovery ]] && continue
[[ "$mnt" == /run* ]] && continue
[[ "$mnt" == /sys* ]] && continue
[[ "$mnt" == /proc* ]] && continue
[[ "$mnt" == /dev* ]] && continue
# Icône selon le point de montage
case "$mnt" in
/) icon="󰋊" ;;
/home) icon="󱂵" ;;
/data*) icon="󱦡" ;;
/media*) icon="󰆼" ;;
/mnt*) icon="󱛟" ;;
*) icon="󰋊" ;;
esac
# Texte compact : icône + montage court + espace utilisé
label=$(basename "$mnt")
[[ "$mnt" == "/" ]] && label="/"
[[ -n "$TEXT" ]] && TEXT+=" "
TEXT+="${icon} ${label}: ${used}"
TOOLTIP+="${icon} ${mnt}\n Utilisé : ${used} / ${size} (${pct})\n Libre : ${avail}\n"
done < <(df -hP --exclude-type=tmpfs \
--exclude-type=devtmpfs \
--exclude-type=squashfs \
--exclude-type=overlay \
--exclude-type=fuse.portal \
--exclude-type=efivarfs \
2>/dev/null | tail -n +2 | sort -k6)
if [[ -z "$TEXT" ]]; then
printf '{"text":"󰋊 N/A","tooltip":"Aucun disque détecté","class":"unavailable"}\n'
else
# Échapper uniquement les guillemets pour JSON (\n reste tel quel = saut de ligne)
TOOLTIP_JSON=$(printf '%s' "$TOOLTIP" | sed 's/"/\\"/g')
printf '{"text":"%s","tooltip":"%s"}\n' "$TEXT" "$TOOLTIP_JSON"
fi

View File

@@ -3,19 +3,17 @@
STATE_FILE="/tmp/waybar_net_state"
# Détecter l'interface active
IFACE=""
for candidate in enp7s0 enp6s0 eth0; do
if [[ -d "/sys/class/net/$candidate" && "$(cat /sys/class/net/$candidate/operstate 2>/dev/null)" == "up" ]]; then
IFACE="$candidate"; TYPE="eth"; break
# Détecter l'interface active via la route par défaut (portable sur tous les PC)
IFACE=$(ip route get 1.1.1.1 2>/dev/null \
| awk '/dev/{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' \
| head -1)
if [[ -n "$IFACE" ]]; then
if [[ -d "/sys/class/net/$IFACE/wireless" || -d "/sys/class/net/$IFACE/phy80211" ]]; then
TYPE="wifi"
else
TYPE="eth"
fi
done
if [[ -z "$IFACE" ]]; then
for candidate in wlp8s0 wlp0s20f3 wlan0; do
if [[ -d "/sys/class/net/$candidate" && "$(cat /sys/class/net/$candidate/operstate 2>/dev/null)" == "up" ]]; then
IFACE="$candidate"; TYPE="wifi"; break
fi
done
fi
if [[ -z "$IFACE" ]]; then

View File

@@ -40,6 +40,12 @@ fi
# ── Affichage JSON ────────────────────────────────────────────────────────────
# PC fixe ou VM sans gestion de profil → module masqué
if [[ ! -f /sys/firmware/acpi/platform_profile ]]; then
printf '{"text":"","class":"unavailable"}\n'
exit 0
fi
PROFILE=$(cat /sys/firmware/acpi/platform_profile 2>/dev/null || echo "unknown")
case "$PROFILE" in

View File

@@ -1,245 +0,0 @@
#!/usr/bin/env python3
# vc-brightness-popup.py — Popup luminosité violet-chaton
# Lancé par le clic sur le module backlight de waybar
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkLayerShell', '0.1')
from gi.repository import Gtk, Gdk, GtkLayerShell, GLib
import subprocess
import os
import re
# ── CSS ───────────────────────────────────────────────────────────────────────
CSS = b"""
window {
background-color: rgba(52, 28, 74, 0.93);
border: 3px solid rgba(255, 121, 198, 0.78);
border-radius: 14px;
}
#container {
padding: 14px 20px 16px 20px;
}
#bright-icon {
color: #8be9fd;
font-family: "JetBrainsMono Nerd Font";
font-size: 18px;
min-width: 24px;
}
#bright-title {
color: rgba(248, 248, 242, 0.55);
font-family: "JetBrainsMono Nerd Font";
font-size: 11px;
}
#device-name {
color: #8be9fd;
font-family: "JetBrainsMono Nerd Font";
font-size: 11px;
font-weight: bold;
}
#bright-pct {
color: #f8f8f2;
font-family: "JetBrainsMono Nerd Font";
font-size: 13px;
font-weight: bold;
min-width: 44px;
}
#separator {
color: rgba(92, 73, 108, 0.60);
margin: 4px 0;
}
scale trough {
background-color: rgba(92, 73, 108, 0.55);
border-radius: 3px;
min-height: 6px;
border: none;
}
scale highlight {
background-color: #8be9fd;
border-radius: 3px;
border: none;
}
scale slider {
background-color: #f8f8f2;
border-radius: 50%;
min-width: 18px;
min-height: 18px;
border: 2px solid rgba(139, 233, 253, 0.80);
box-shadow: none;
transition: none;
}
scale slider:hover {
background-color: #8be9fd;
border-color: #8be9fd;
}
"""
POPUP_WIDTH = 300
# ── Brightness helpers ────────────────────────────────────────────────────────
def get_brightness():
"""Retourne (valeur 0-100, nom du device)."""
try:
r = subprocess.run(
['brightnessctl', 'info'],
capture_output=True, text=True, timeout=2
)
pct_match = re.search(r'\((\d+)%\)', r.stdout)
dev_match = re.search(r"Device '([^']+)'", r.stdout)
pct = int(pct_match.group(1)) if pct_match else 50
dev = dev_match.group(1) if dev_match else 'Écran'
# Rendre le nom plus lisible
dev = dev.replace('_', ' ').replace('backlight', '').strip().title()
return pct, dev
except Exception:
return 50, 'Écran'
def set_brightness(pct):
pct = max(1, min(100, pct)) # minimum 1% pour ne pas éteindre l'écran
subprocess.run(
['brightnessctl', 'set', f'{pct}%', '-q'],
capture_output=True
)
# Feedback wob
fifo = '/tmp/wob.fifo'
if os.path.exists(fifo):
try:
fd = os.open(fifo, os.O_WRONLY | os.O_NONBLOCK)
os.write(fd, f'{pct}\n'.encode())
os.close(fd)
except OSError:
pass
def bright_icon(pct):
if pct < 34:
return '󰃞'
if pct < 67:
return '󰃟'
return '󰃠'
# ── Popup ─────────────────────────────────────────────────────────────────────
class BrightnessPopup(Gtk.Window):
def __init__(self):
super().__init__()
self._blocked = False
# ── Position : centré sous le module backlight ────────────────────────
display = Gdk.Display.get_default()
monitor = display.get_primary_monitor() if display else None
screen_w = monitor.get_geometry().width if monitor else 1920
# Backlight est le 2e module de la pill droite (~250px depuis le bord)
module_center = screen_w - 16 - 250
margin_left = max(0, module_center - POPUP_WIDTH // 2)
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True)
GtkLayerShell.set_margin(self, GtkLayerShell.Edge.TOP, 66)
GtkLayerShell.set_margin(self, GtkLayerShell.Edge.LEFT, margin_left)
GtkLayerShell.set_keyboard_mode(self, GtkLayerShell.KeyboardMode.ON_DEMAND)
GtkLayerShell.set_exclusive_zone(self, -1)
self.set_decorated(False)
self.set_resizable(False)
self.set_default_size(POPUP_WIDTH, -1)
# ── CSS ───────────────────────────────────────────────────────────────
provider = Gtk.CssProvider()
provider.load_from_data(CSS)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
# ── État initial ──────────────────────────────────────────────────────
pct, dev = get_brightness()
# ── Layout ────────────────────────────────────────────────────────────
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
container.set_name('container')
self.add(container)
# Ligne device
dev_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
dev_icon = Gtk.Label(label='󰍹')
dev_icon.set_name('bright-title')
dev_row.pack_start(dev_icon, False, False, 0)
dev_label = Gtk.Label(label=dev)
dev_label.set_name('device-name')
dev_label.set_halign(Gtk.Align.START)
dev_label.set_ellipsize(3)
dev_row.pack_start(dev_label, True, True, 0)
container.pack_start(dev_row, False, False, 0)
# Séparateur
sep = Gtk.Label(label='' * 30)
sep.set_name('separator')
container.pack_start(sep, False, False, 4)
# En-tête : icône + "Luminosité" + %
header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
self.icon = Gtk.Label(label=bright_icon(pct))
self.icon.set_name('bright-icon')
header.pack_start(self.icon, False, False, 0)
title = Gtk.Label(label='Luminosité')
title.set_name('bright-title')
title.set_halign(Gtk.Align.START)
header.pack_start(title, True, True, 0)
self.pct = Gtk.Label(label=f'{pct}%')
self.pct.set_name('bright-pct')
self.pct.set_halign(Gtk.Align.END)
header.pack_end(self.pct, False, False, 0)
container.pack_start(header, False, False, 0)
# Slider (min 1% pour ne pas éteindre l'écran)
self.scale = Gtk.Scale.new_with_range(
Gtk.Orientation.HORIZONTAL, 1, 100, 5
)
self.scale.set_value(pct)
self.scale.set_draw_value(False)
self.scale.set_hexpand(True)
self.scale.connect('value-changed', self._on_changed)
container.pack_start(self.scale, False, False, 0)
# ── Fermeture ─────────────────────────────────────────────────────────
self.connect('key-press-event', self._on_key)
self.connect('focus-out-event', lambda *_: self.destroy())
self.show_all()
self.grab_focus()
def _on_changed(self, scale):
if self._blocked:
return
pct = int(scale.get_value())
self.pct.set_label(f'{pct}%')
self.icon.set_label(bright_icon(pct))
set_brightness(pct)
def _on_key(self, _widget, event):
if event.keyval == Gdk.KEY_Escape:
self.destroy()
if __name__ == '__main__':
win = BrightnessPopup()
win.connect('destroy', Gtk.main_quit)
Gtk.main()

View File

@@ -3,9 +3,16 @@
# Lancé depuis le clic sur wireplumber OU backlight
import gi
import math
import threading
import urllib.request
gi.require_version('Gtk', '3.0')
gi.require_version('GtkLayerShell', '0.1')
from gi.repository import Gtk, Gdk, GtkLayerShell, GLib
gi.require_version('GdkPixbuf', '2.0')
gi.require_version('Pango', '1.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Gtk, Gdk, GtkLayerShell, GLib, GdkPixbuf, Pango, PangoCairo
import subprocess
import os
import re
@@ -135,6 +142,31 @@ scale.audio.muted slider {
border-color: rgba(108, 112, 134, 0.60);
}
/* ── Sélecteur de périphérique de sortie ────────────────────────────────────── */
#device-btn {
background-color: transparent;
color: rgba(248, 248, 242, 0.65);
font-family: "JetBrainsMono Nerd Font";
font-size: 11px;
border: 1px solid transparent;
border-radius: 8px;
padding: 5px 10px;
min-width: 0;
}
#device-btn:hover {
background-color: rgba(91, 70, 113, 0.55);
color: #f8f8f2;
border-color: rgba(92, 73, 108, 0.60);
}
#device-btn.active {
background-color: rgba(255, 121, 198, 0.14);
color: #ff79c6;
border-color: rgba(255, 121, 198, 0.55);
}
/* ── Slider luminosité (cyan) ───────────────────────────────────────────────── */
scale.bright {
@@ -176,14 +208,55 @@ scale.bright slider:hover {
font-size: 17px;
min-width: 28px;
}
/* ── Section MPRIS ────────────────────────────────────────────────────────── */
#mpris-title {
color: #f8f8f2;
font-family: "JetBrainsMono Nerd Font";
font-size: 12px;
font-weight: bold;
}
#mpris-artist {
color: rgba(248, 248, 242, 0.55);
font-family: "JetBrainsMono Nerd Font";
font-size: 11px;
}
#mpris-btn {
background-color: transparent;
color: #e79cfe;
font-family: "JetBrainsMono Nerd Font";
font-size: 16px;
border: none;
border-radius: 6px;
padding: 4px 8px;
min-width: 0;
}
#mpris-btn:hover {
background-color: rgba(231, 156, 254, 0.15);
}
#mpris-btn.play {
font-size: 20px;
color: #ff79c6;
padding: 4px 12px;
}
#mpris-btn.play:hover {
background-color: rgba(255, 121, 198, 0.15);
}
"""
POPUP_WIDTH = 310
# ── Helpers ───────────────────────────────────────────────────────────────────
def run(cmd, **kw):
return subprocess.run(cmd, capture_output=True, text=True, timeout=2, **kw)
def run(cmd, env=None, **kw):
return subprocess.run(cmd, capture_output=True, text=True, timeout=2,
env=env, **kw)
def get_sink_volume():
r = run(['wpctl', 'get-volume', '@DEFAULT_AUDIO_SINK@'])
@@ -239,6 +312,93 @@ def _wob(msg):
except OSError:
pass
def get_sinks():
"""Retourne [(sink_name, description, is_default)] — exclut SUSPENDED."""
env = {**os.environ, 'LANG': 'C', 'LC_ALL': 'C'}
r_default = run(['pactl', 'get-default-sink'], env=env)
default_name = r_default.stdout.strip()
r_full = run(['pactl', 'list', 'sinks'], env=env)
sinks, state, name, desc = [], None, None, None
for line in r_full.stdout.splitlines():
st = re.search(r'^\s+State:\s+(\S+)', line)
nm = re.search(r'^\s+Name:\s+(.+)$', line)
ds = re.search(r'^\s+Description:\s+(.+)$', line)
if st: state = st.group(1)
elif nm: name = nm.group(1).strip()
elif ds and name:
desc = ds.group(1).strip()
if state != 'SUSPENDED':
sinks.append((name, desc, name == default_name))
state, name, desc = None, None, None
return sinks
def set_default_sink(name):
run(['pactl', 'set-default-sink', name])
def get_sources():
"""Retourne [(source_name, description, is_default)] — exclut les .monitor."""
env = {**os.environ, 'LANG': 'C', 'LC_ALL': 'C'}
r_default = run(['pactl', 'get-default-source'], env=env)
default_name = r_default.stdout.strip()
r_full = run(['pactl', 'list', 'sources'], env=env)
sources, name, desc = [], None, None
for line in r_full.stdout.splitlines():
nm = re.search(r'^\s+Name:\s+(.+)$', line)
ds = re.search(r'^\s+Description:\s+(.+)$', line)
if nm:
name = nm.group(1).strip()
elif ds and name:
desc = ds.group(1).strip()
if '.monitor' not in name:
sources.append((name, desc, name == default_name))
name, desc = None, None
return sources
def set_default_source(name):
run(['pactl', 'set-default-source', name])
def get_mpris_info():
"""Retourne dict ou None si pas de lecteur actif."""
try:
r = run(['playerctl', 'metadata', '--format',
'{{title}}||{{artist}}||{{mpris:artUrl}}||{{status}}'])
except (FileNotFoundError, subprocess.TimeoutExpired):
return None
if r.returncode != 0 or not r.stdout.strip():
return None
parts = r.stdout.strip().split('||', 3)
if len(parts) < 4:
return None
title, artist, art_url, status = [p.strip() for p in parts]
if not title:
return None
return {
'title': title,
'artist': artist,
'art_url': art_url,
'status': status.lower(), # 'playing' | 'paused' | 'stopped'
}
def source_icon(name, desc):
s = (name + desc).lower()
if 'bluetooth' in s or 'bluez' in s: return '󰥰'
if 'usb' in s: return '󱡬'
if 'headset' in s or 'headphone' in s: return '󰋎'
return '󰍬'
def sink_icon(name, desc):
s = (name + desc).lower()
if 'hdmi' in s or 'dp-' in s or 'displayport' in s: return '󰡁'
if 'bluetooth' in s or 'bluez' in s: return '󰥰'
if 'usb' in s: return '󱡬'
if 'headphone' in s or 'headset' in s: return '󰋋'
return '󰓃'
def short_desc(desc, maxlen=16):
return desc if len(desc) <= maxlen else desc[:maxlen - 1] + ''
def vol_icon(muted):
return '󰖁' if muted else '󰕾'
@@ -250,26 +410,109 @@ def bright_icon(pct):
if pct < 67: return '󰃟'
return '󰃠'
# ── Art widget (album art / miniature YouTube) ────────────────────────────────
class ArtWidget(Gtk.DrawingArea):
SIZE = 72
def __init__(self):
super().__init__()
self._pixbuf = None
self._url = None
self.set_size_request(self.SIZE, self.SIZE)
self.connect('draw', self._on_draw)
def load_url(self, url):
if url == self._url:
return
self._url = url
self._pixbuf = None
self.queue_draw()
if not url:
return
threading.Thread(target=self._fetch, args=(url,), daemon=True).start()
def _fetch(self, url):
try:
if url.startswith('file://'):
raw = GdkPixbuf.Pixbuf.new_from_file(url[7:])
else:
req = urllib.request.Request(
url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=5) as resp:
data = resp.read()
loader = GdkPixbuf.PixbufLoader()
loader.write(data)
loader.close()
raw = loader.get_pixbuf()
if raw:
s = self.SIZE
ow, oh = raw.get_width(), raw.get_height()
scale = min(s / ow, s / oh)
nw = max(1, int(ow * scale))
nh = max(1, int(oh * scale))
pixbuf = raw.scale_simple(nw, nh, GdkPixbuf.InterpType.BILINEAR)
else:
pixbuf = None
except Exception:
pixbuf = None
GLib.idle_add(self._set_pixbuf, pixbuf, url)
def _set_pixbuf(self, pixbuf, url):
if url == self._url:
self._pixbuf = pixbuf
self.queue_draw()
return False
def _on_draw(self, _widget, cr):
s, r = self.SIZE, 10
# Coins arrondis (clip)
cr.new_sub_path()
cr.arc(r, r, r, math.pi, 3 * math.pi / 2)
cr.arc(s - r, r, r, -math.pi / 2, 0)
cr.arc(s - r, s - r, r, 0, math.pi / 2)
cr.arc(r, s - r, r, math.pi / 2, math.pi)
cr.close_path()
cr.clip()
# Fond violet
cr.set_source_rgba(73/255, 49/255, 97/255, 0.85)
cr.paint()
if self._pixbuf:
pw = self._pixbuf.get_width()
ph = self._pixbuf.get_height()
Gdk.cairo_set_source_pixbuf(cr, self._pixbuf,
(s - pw) / 2, (s - ph) / 2)
cr.paint()
else:
# Icône note de musique (placeholder)
layout = PangoCairo.create_layout(cr)
layout.set_markup('<span font="JetBrainsMono Nerd Font 24">󰝚</span>')
lw, lh = layout.get_size()
cr.set_source_rgba(231/255, 156/255, 254/255, 0.45)
cr.move_to((s - lw / Pango.SCALE) / 2,
(s - lh / Pango.SCALE) / 2)
PangoCairo.show_layout(cr, layout)
# ── Popup ─────────────────────────────────────────────────────────────────────
class MediaPopup(Gtk.Window):
def __init__(self):
super().__init__()
self._blk = False
# ── Position ─────────────────────────────────────────────────────────
display = Gdk.Display.get_default()
monitor = display.get_primary_monitor() if display else None
screen_w = monitor.get_geometry().width if monitor else 1920
module_center = screen_w - 16 - 210
margin_left = max(0, module_center - POPUP_WIDTH // 2)
self._blk = False
self._has_mpris = False
# ── Position — ancré à droite, toujours dans l'écran ─────────────────
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_margin(self, GtkLayerShell.Edge.TOP, 66)
GtkLayerShell.set_margin(self, GtkLayerShell.Edge.LEFT, margin_left)
GtkLayerShell.set_margin(self, GtkLayerShell.Edge.RIGHT, 12)
GtkLayerShell.set_keyboard_mode(self, GtkLayerShell.KeyboardMode.ON_DEMAND)
GtkLayerShell.set_exclusive_zone(self, -1)
self.set_decorated(False)
@@ -295,10 +538,22 @@ class MediaPopup(Gtk.Window):
box.set_name('container')
self.add(box)
# ╔═══ LECTURE (MPRIS) — affiché seulement si lecteur actif ════════════╗
mpris_info = get_mpris_info()
if mpris_info:
self._build_mpris_section(box, mpris_info)
sep0 = Gtk.Label(label='' * 34)
sep0.set_name('separator')
box.pack_start(sep0, False, False, 0)
# ╔═══ SORTIE ══════════════════════════════════════════════════════════╗
sinks = get_sinks()
box.pack_start(self._section_header('SORTIE', '󰕾'), False, False, 0)
box.pack_start(self._device_label(
get_node_name('@DEFAULT_AUDIO_SINK@')), False, False, 2)
self.sink_device_lbl = self._device_label(
get_node_name('@DEFAULT_AUDIO_SINK@'))
box.pack_start(self.sink_device_lbl, False, False, 2)
if len(sinks) > 1:
box.pack_start(self._sink_selector(sinks), False, False, 4)
sink_row, self.sink_scale, self.sink_pct, self.sink_icon = \
self._slider_row(sink_vol, sink_muted, 'audio', vol_icon(sink_muted),
self._toggle_sink_mute, '@DEFAULT_AUDIO_SINK@')
@@ -309,9 +564,13 @@ class MediaPopup(Gtk.Window):
sep1.set_name('separator')
box.pack_start(sep1, False, False, 0)
sources = get_sources()
box.pack_start(self._section_header('ENTRÉE', '󰍬'), False, False, 0)
box.pack_start(self._device_label(
get_node_name('@DEFAULT_AUDIO_SOURCE@')), False, False, 2)
self.src_device_lbl = self._device_label(
get_node_name('@DEFAULT_AUDIO_SOURCE@'))
box.pack_start(self.src_device_lbl, False, False, 2)
if len(sources) > 1:
box.pack_start(self._source_selector(sources), False, False, 4)
src_row, self.src_scale, self.src_pct, self.src_icon = \
self._slider_row(src_vol, src_muted, 'audio', mic_icon(src_muted),
self._toggle_src_mute, '@DEFAULT_AUDIO_SOURCE@')
@@ -333,23 +592,167 @@ class MediaPopup(Gtk.Window):
self.connect('focus-out-event', lambda *_: self.destroy())
self.show_all()
# GTK3 bug : set_value() avant réalisation → highlight width=0 à max
# Forcer un re-calcul après que les widgets sont visibles
GLib.idle_add(self._redraw_scales)
self.grab_focus()
def _redraw_scales(self):
"""Force GTK3 à recalculer les highlights.
set_value(même_valeur) est un no-op — on oscille ±1 pour déclencher
un vrai recalcul de la position du highlight dans le trough."""
# ── MPRIS section builder ─────────────────────────────────────────────────
def _build_mpris_section(self, box, info):
box.pack_start(self._section_header('LECTURE', '󰝚'), False, False, 0)
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
content.set_margin_top(4)
content.set_margin_bottom(4)
# Album art
self.mpris_art = ArtWidget()
content.pack_start(self.mpris_art, False, False, 0)
# Infos + contrôles
info_col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
info_col.set_valign(Gtk.Align.CENTER)
self.mpris_title = Gtk.Label(label=info['title'])
self.mpris_title.set_name('mpris-title')
self.mpris_title.set_halign(Gtk.Align.START)
self.mpris_title.set_ellipsize(3)
self.mpris_title.set_max_width_chars(20)
info_col.pack_start(self.mpris_title, False, False, 0)
self.mpris_artist = Gtk.Label(label=info['artist'] or '')
self.mpris_artist.set_name('mpris-artist')
self.mpris_artist.set_halign(Gtk.Align.START)
self.mpris_artist.set_ellipsize(3)
self.mpris_artist.set_max_width_chars(20)
info_col.pack_start(self.mpris_artist, False, False, 0)
# Contrôles prev / play-pause / next
ctrl = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
ctrl.set_margin_top(4)
btn_prev = Gtk.Button(label='󰒮')
btn_prev.set_name('mpris-btn')
btn_prev.connect('clicked', lambda _: run(['playerctl', 'previous']))
self.btn_play = Gtk.Button(
label='󰏤' if info['status'] == 'playing' else '󰐊')
self.btn_play.set_name('mpris-btn')
self.btn_play.get_style_context().add_class('play')
self.btn_play.connect('clicked', self._on_play_pause)
btn_next = Gtk.Button(label='󰒭')
btn_next.set_name('mpris-btn')
btn_next.connect('clicked', lambda _: run(['playerctl', 'next']))
ctrl.pack_start(btn_prev, False, False, 0)
ctrl.pack_start(self.btn_play, False, False, 0)
ctrl.pack_start(btn_next, False, False, 0)
info_col.pack_start(ctrl, False, False, 0)
content.pack_start(info_col, True, True, 0)
box.pack_start(content, False, False, 4)
# Charger l'artwork en arrière-plan
if info.get('art_url'):
self.mpris_art.load_url(info['art_url'])
self._has_mpris = True
self._mpris_status = info['status']
def _on_play_pause(self, _btn):
run(['playerctl', 'play-pause'])
info = get_mpris_info()
if info and self._has_mpris:
self._mpris_status = info['status']
self.btn_play.set_label(
'󰏤' if info['status'] == 'playing' else '󰐊')
# ── Sélecteur de sortie ────────────────────────────────────────────────────
def _sink_selector(self, sinks):
self._sink_btns = {}
self._sink_descs = {}
col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
for name, desc, is_default in sinks:
self._sink_descs[name] = desc
btn = Gtk.Button()
btn.set_name('device-btn')
btn.set_hexpand(True)
lbl = Gtk.Label(label=self._sink_label(name, desc, is_default))
lbl.set_halign(Gtk.Align.START)
btn.add(lbl)
if is_default:
btn.get_style_context().add_class('active')
btn.connect('clicked', self._on_sink_selected, name)
col.pack_start(btn, False, True, 0)
self._sink_btns[name] = btn
return col
def _sink_label(self, name, desc, active):
check = ' ' if active else ''
return f'{sink_icon(name, desc)} {desc}{check}'
def _source_selector(self, sources):
self._src_btns = {}
self._src_descs = {}
col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
for name, desc, is_default in sources:
self._src_descs[name] = desc
btn = Gtk.Button()
btn.set_name('device-btn')
btn.set_hexpand(True)
lbl = Gtk.Label(label=self._src_label(name, desc, is_default))
lbl.set_halign(Gtk.Align.START)
btn.add(lbl)
if is_default:
btn.get_style_context().add_class('active')
btn.connect('clicked', self._on_source_selected, name)
col.pack_start(btn, False, True, 0)
self._src_btns[name] = btn
return col
def _src_label(self, name, desc, active):
check = ' ' if active else ''
return f'{source_icon(name, desc)} {desc}{check}'
def _on_source_selected(self, _btn, name):
for n, b in self._src_btns.items():
b.get_style_context().remove_class('active')
b.get_child().set_label(self._src_label(n, self._src_descs[n], False))
self._src_btns[name].get_style_context().add_class('active')
self._src_btns[name].get_child().set_label(
self._src_label(name, self._src_descs[name], True))
set_default_source(name)
self.src_device_lbl.set_label(self._src_descs[name])
vol, muted = get_source_volume()
self._blk = True
for scale in [self.sink_scale, self.src_scale, self.bright_scale]:
v = scale.get_value()
adj = scale.get_adjustment()
lo = adj.get_lower()
scale.set_value(max(lo, v - 1)) # valeur différente → GTK recalcule
scale.set_value(v) # retour à la valeur réelle
self.src_scale.set_value(vol)
self.src_pct.set_label(f'{vol}%')
self._blk = False
return False
self._src_muted = muted
self._apply_mute(self.src_scale, self.src_icon, muted, mic_icon)
subprocess.Popen(['pkill', '-RTMIN+1', 'waybar'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _on_sink_selected(self, _btn, name):
for n, b in self._sink_btns.items():
b.get_style_context().remove_class('active')
b.get_child().set_label(
self._sink_label(n, self._sink_descs[n], False))
self._sink_btns[name].get_style_context().add_class('active')
self._sink_btns[name].get_child().set_label(
self._sink_label(name, self._sink_descs[name], True))
set_default_sink(name)
self.sink_device_lbl.set_label(self._sink_descs[name])
vol, muted = get_sink_volume()
self._blk = True
self.sink_scale.set_value(vol)
self.sink_pct.set_label(f'{vol}%')
self._blk = False
self._sink_muted = muted
self._apply_mute(self.sink_scale, self.sink_icon, muted, vol_icon)
subprocess.Popen(['pkill', '-RTMIN+1', 'waybar'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# ── Builders UI ───────────────────────────────────────────────────────────
@@ -376,7 +779,6 @@ class MediaPopup(Gtk.Window):
"""Retourne (row, scale, pct_label, icon_btn)"""
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
# Icône = bouton mute
icon_btn = Gtk.Button(label=icon_char)
icon_btn.set_name('mute-icon' if target == '@DEFAULT_AUDIO_SINK@' else 'mic-icon')
if muted:
@@ -384,7 +786,6 @@ class MediaPopup(Gtk.Window):
icon_btn.connect('clicked', mute_cb)
row.pack_start(icon_btn, False, False, 0)
# Slider
scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 5)
scale.set_value(val)
scale.set_draw_value(False)
@@ -396,7 +797,6 @@ class MediaPopup(Gtk.Window):
self._on_audio_changed(s, pct_lbl, t))
row.pack_start(scale, True, True, 0)
# %
pct_lbl = Gtk.Label(label=f'{val}%')
pct_lbl.set_name('pct-label')
pct_lbl.set_halign(Gtk.Align.END)
@@ -469,7 +869,20 @@ class MediaPopup(Gtk.Window):
self._apply_mute(self.src_scale, self.src_icon,
self._src_muted, mic_icon)
def _redraw_scales(self):
"""Force GTK3 à recalculer les highlights."""
self._blk = True
for scale in [self.sink_scale, self.src_scale, self.bright_scale]:
v = scale.get_value()
adj = scale.get_adjustment()
lo = adj.get_lower()
scale.set_value(max(lo, v - 1))
scale.set_value(v)
self._blk = False
return False
def _refresh(self):
# ── Audio ──────────────────────────────────────────────────────────────
sink_vol, sink_muted = get_sink_volume()
src_vol, src_muted = get_source_volume()
@@ -489,6 +902,20 @@ class MediaPopup(Gtk.Window):
self.src_pct.set_label(f'{src_vol}%')
self._blk = False
# ── MPRIS ──────────────────────────────────────────────────────────────
if self._has_mpris:
info = get_mpris_info()
if info:
self.mpris_title.set_label(info['title'])
self.mpris_artist.set_label(info['artist'] or '')
if info['status'] != self._mpris_status:
self._mpris_status = info['status']
self.btn_play.set_label(
'󰏤' if info['status'] == 'playing' else '󰐊')
cur_url = info.get('art_url', '')
if cur_url != self.mpris_art._url:
self.mpris_art.load_url(cur_url)
return True

View File

@@ -1,422 +0,0 @@
#!/usr/bin/env python3
# vc-volume-popup.py — Popup volume slider violet-chaton
# Lancé par le clic sur le module wireplumber de waybar
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkLayerShell', '0.1')
from gi.repository import Gtk, Gdk, GtkLayerShell, GLib
import subprocess
import os
import re
# ── CSS ───────────────────────────────────────────────────────────────────────
CSS = b"""
window {
background-color: rgba(52, 28, 74, 0.93);
border: 3px solid rgba(255, 121, 198, 0.78);
border-radius: 14px;
}
#container {
padding: 14px 20px 16px 20px;
}
#vol-icon {
color: #ff79c6;
font-family: "JetBrainsMono Nerd Font";
font-size: 18px;
min-width: 24px;
}
#vol-title {
color: rgba(248, 248, 242, 0.55);
font-family: "JetBrainsMono Nerd Font";
font-size: 11px;
}
#sink-name {
color: #8be9fd;
font-family: "JetBrainsMono Nerd Font";
font-size: 11px;
font-weight: bold;
}
#vol-pct {
color: #f8f8f2;
font-family: "JetBrainsMono Nerd Font";
font-size: 13px;
font-weight: bold;
min-width: 44px;
}
#separator {
color: rgba(92, 73, 108, 0.60);
margin: 4px 0;
}
scale trough {
background-color: rgba(92, 73, 108, 0.55);
border-radius: 3px;
min-height: 6px;
border: none;
}
scale highlight {
background-color: #ff79c6;
border-radius: 3px;
border: none;
}
scale slider {
background-color: #f8f8f2;
border-radius: 50%;
min-width: 18px;
min-height: 18px;
border: 2px solid rgba(255, 121, 198, 0.80);
box-shadow: none;
transition: none;
}
scale slider:hover {
background-color: #e79cfe;
border-color: #ff79c6;
}
#mute-btn {
background: rgba(73, 49, 97, 0.50);
border: 1px solid rgba(92, 73, 108, 0.60);
border-radius: 8px;
color: rgba(248, 248, 242, 0.65);
font-family: "JetBrainsMono Nerd Font";
font-size: 12px;
padding: 5px 16px;
margin-top: 6px;
}
#mute-btn:hover {
background: rgba(255, 121, 198, 0.18);
border-color: rgba(255, 121, 198, 0.45);
color: #ff79c6;
}
#mute-btn.muted {
color: #f38ba8;
border-color: rgba(243, 139, 168, 0.45);
background: rgba(243, 139, 168, 0.10);
}
#mic-btn {
background: rgba(73, 49, 97, 0.50);
border: 1px solid rgba(139, 233, 253, 0.35);
border-radius: 8px;
color: #8be9fd;
font-family: "JetBrainsMono Nerd Font";
font-size: 12px;
padding: 5px 16px;
margin-top: 4px;
}
#mic-btn:hover {
background: rgba(139, 233, 253, 0.12);
border-color: rgba(139, 233, 253, 0.60);
color: #8be9fd;
}
#mic-btn.muted {
color: #f38ba8;
border-color: rgba(243, 139, 168, 0.45);
background: rgba(243, 139, 168, 0.10);
}
"""
POPUP_WIDTH = 300
# ── Audio helpers ─────────────────────────────────────────────────────────────
def get_volume():
"""Retourne (volume 0-100, is_muted)"""
try:
r = subprocess.run(
['wpctl', 'get-volume', '@DEFAULT_AUDIO_SINK@'],
capture_output=True, text=True, timeout=2
)
parts = r.stdout.strip().split()
vol = int(float(parts[1]) * 100)
muted = '[MUTED]' in r.stdout
return min(max(vol, 0), 100), muted
except Exception:
return 50, False
def get_sink_name():
"""Retourne le nom humain de la sortie audio active."""
try:
r = subprocess.run(
['wpctl', 'inspect', '@DEFAULT_AUDIO_SINK@'],
capture_output=True, text=True, timeout=2
)
# Chercher node.description en priorité, sinon node.nick
for field in ('node.description', 'node.nick'):
m = re.search(rf'{field}\s*=\s*"([^"]+)"', r.stdout)
if m:
return m.group(1)
except Exception:
pass
return 'Sortie audio'
def set_volume(vol):
subprocess.run(
['wpctl', 'set-volume', '-l', '1.0', '@DEFAULT_AUDIO_SINK@', f'{vol}%'],
capture_output=True
)
# Feedback wob (non-bloquant)
fifo = '/tmp/wob.fifo'
if os.path.exists(fifo):
try:
fd = os.open(fifo, os.O_WRONLY | os.O_NONBLOCK)
os.write(fd, f'{vol}\n'.encode())
os.close(fd)
except OSError:
pass
def toggle_mute():
subprocess.run(
['wpctl', 'set-mute', '@DEFAULT_AUDIO_SINK@', 'toggle'],
capture_output=True
)
def get_mic_muted():
"""Retourne True si le micro actif est muté."""
try:
r = subprocess.run(
['wpctl', 'get-volume', '@DEFAULT_AUDIO_SOURCE@'],
capture_output=True, text=True, timeout=2
)
return '[MUTED]' in r.stdout
except Exception:
return False
def toggle_mic_mute():
subprocess.run(
['wpctl', 'set-mute', '@DEFAULT_AUDIO_SOURCE@', 'toggle'],
capture_output=True
)
def vol_icon(vol, muted):
if muted or vol == 0:
return '󰝟'
if vol < 50:
return '󰕿'
return '󰕾'
# ── Popup ─────────────────────────────────────────────────────────────────────
class VolumePopup(Gtk.Window):
def __init__(self):
super().__init__()
self._blocked = False
# ── Position : centré sous le module wireplumber ──────────────────────
# Wireplumber = 1er module de la pill droite (côté droit de l'écran).
# On centre le popup horizontalement sous ce module.
display = Gdk.Display.get_default()
monitor = display.get_primary_monitor() if display else None
if monitor:
screen_w = monitor.get_geometry().width
else:
screen_w = 1920 # fallback
# La pill droite a ~16px de marge depuis le bord droit.
# Le module wireplumber est le 1er élément : ~180px depuis le bord droit.
module_center = screen_w - 16 - 180
margin_left = max(0, module_center - POPUP_WIDTH // 2)
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True)
GtkLayerShell.set_margin(self, GtkLayerShell.Edge.TOP, 66)
GtkLayerShell.set_margin(self, GtkLayerShell.Edge.LEFT, margin_left)
GtkLayerShell.set_keyboard_mode(self, GtkLayerShell.KeyboardMode.ON_DEMAND)
GtkLayerShell.set_exclusive_zone(self, -1)
self.set_decorated(False)
self.set_resizable(False)
self.set_default_size(POPUP_WIDTH, -1)
# ── CSS ───────────────────────────────────────────────────────────────
provider = Gtk.CssProvider()
provider.load_from_data(CSS)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
# ── État initial ──────────────────────────────────────────────────────
vol, muted = get_volume()
self._muted = muted
self._mic_muted = get_mic_muted()
sink = get_sink_name()
# ── Layout ────────────────────────────────────────────────────────────
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
container.set_name('container')
self.add(container)
# Ligne sink (sortie active)
sink_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
sink_icon = Gtk.Label(label='󰓃')
sink_icon.set_name('vol-title')
sink_row.pack_start(sink_icon, False, False, 0)
self.sink_label = Gtk.Label(label=sink)
self.sink_label.set_name('sink-name')
self.sink_label.set_halign(Gtk.Align.START)
self.sink_label.set_ellipsize(3) # PANGO_ELLIPSIZE_END
sink_row.pack_start(self.sink_label, True, True, 0)
container.pack_start(sink_row, False, False, 0)
# Séparateur
sep = Gtk.Label(label='' * 30)
sep.set_name('separator')
container.pack_start(sep, False, False, 4)
# En-tête volume : icône + "Volume" + %
header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
self.icon = Gtk.Label(label=vol_icon(vol, muted))
self.icon.set_name('vol-icon')
header.pack_start(self.icon, False, False, 0)
title = Gtk.Label(label='Volume')
title.set_name('vol-title')
title.set_halign(Gtk.Align.START)
header.pack_start(title, True, True, 0)
self.pct = Gtk.Label(label=f'{vol}%')
self.pct.set_name('vol-pct')
self.pct.set_halign(Gtk.Align.END)
header.pack_end(self.pct, False, False, 0)
container.pack_start(header, False, False, 0)
# Slider
self.scale = Gtk.Scale.new_with_range(
Gtk.Orientation.HORIZONTAL, 0, 100, 5
)
self.scale.set_value(vol)
self.scale.set_draw_value(False)
self.scale.set_hexpand(True)
self.scale.connect('value-changed', self._on_changed)
container.pack_start(self.scale, False, False, 0)
# Bouton mute
self.mute_btn = Gtk.Button(label=f'󰖁 {"Remettre le son" if muted else "Muet"}')
self.mute_btn.set_name('mute-btn')
self.mute_btn.set_halign(Gtk.Align.CENTER)
if muted:
self.mute_btn.get_style_context().add_class('muted')
self.mute_btn.connect('clicked', self._on_mute)
container.pack_start(self.mute_btn, False, False, 0)
# ── Section micro ─────────────────────────────────────────────────────
sep2 = Gtk.Label(label='' * 30)
sep2.set_name('separator')
container.pack_start(sep2, False, False, 4)
mic_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
mic_icon = Gtk.Label(label='󰍬')
mic_icon.set_name('vol-title')
mic_icon.set_markup('<span font_family="JetBrainsMono Nerd Font" size="large">󰍬</span>')
mic_row.pack_start(mic_icon, False, False, 0)
mic_title = Gtk.Label(label='Micro')
mic_title.set_name('vol-title')
mic_title.set_halign(Gtk.Align.START)
mic_row.pack_start(mic_title, True, True, 0)
mic_label = '󰍭 Coupé' if self._mic_muted else '󰍬 Actif'
self.mic_btn = Gtk.Button(label=mic_label)
self.mic_btn.set_name('mic-btn')
if self._mic_muted:
self.mic_btn.get_style_context().add_class('muted')
self.mic_btn.connect('clicked', self._on_mic_mute)
mic_row.pack_end(self.mic_btn, False, False, 0)
container.pack_start(mic_row, False, False, 0)
# ── Refresh périodique (détecte changement de sink/micro) ─────────────
GLib.timeout_add(2000, self._refresh_sink)
# ── Fermeture ─────────────────────────────────────────────────────────
self.connect('key-press-event', self._on_key)
self.connect('focus-out-event', lambda *_: self.destroy())
self.show_all()
self.grab_focus()
def _on_changed(self, scale):
if self._blocked:
return
vol = int(scale.get_value())
self.pct.set_label(f'{vol}%')
self.icon.set_label(vol_icon(vol, self._muted))
set_volume(vol)
def _on_mute(self, btn):
toggle_mute()
_, self._muted = get_volume()
vol = int(self.scale.get_value())
self.icon.set_label(vol_icon(vol, self._muted))
if self._muted:
btn.get_style_context().add_class('muted')
btn.set_label('󰕿 Remettre le son')
else:
btn.get_style_context().remove_class('muted')
btn.set_label('󰖁 Muet')
def _refresh_sink(self):
"""Met à jour la sortie et l'état du micro si changement détecté."""
# Sortie audio
sink = get_sink_name()
if self.sink_label.get_label() != sink:
self.sink_label.set_label(sink)
vol, muted = get_volume()
self._blocked = True
self.scale.set_value(vol)
self._blocked = False
self.pct.set_label(f'{vol}%')
self._muted = muted
self.icon.set_label(vol_icon(vol, muted))
# Micro
mic_muted = get_mic_muted()
if mic_muted != self._mic_muted:
self._mic_muted = mic_muted
if mic_muted:
self.mic_btn.get_style_context().add_class('muted')
self.mic_btn.set_label('󰍭 Coupé')
else:
self.mic_btn.get_style_context().remove_class('muted')
self.mic_btn.set_label('󰍬 Actif')
return True # continuer le timer
def _on_mic_mute(self, btn):
toggle_mic_mute()
self._mic_muted = get_mic_muted()
if self._mic_muted:
btn.get_style_context().add_class('muted')
btn.set_label('󰍭 Coupé')
else:
btn.get_style_context().remove_class('muted')
btn.set_label('󰍬 Actif')
def _on_key(self, _widget, event):
if event.keyval == Gdk.KEY_Escape:
self.destroy()
if __name__ == '__main__':
win = VolumePopup()
win.connect('destroy', Gtk.main_quit)
Gtk.main()

View File

@@ -12,6 +12,14 @@ export INSTALL_LOG="$HOME/violet-chaton-install-$(date +%Y%m%d-%H%M%S).log"
source "$SCRIPT_DIR/scripts/lib.sh"
# ── Refus root ────────────────────────────────────────────────────────────────
if [ "$EUID" -eq 0 ]; then
echo -e "${RED}${BOLD} ERREUR : Ne pas lancer ce script en tant que root !${RESET}"
echo -e " Lance-le en tant qu'utilisateur normal : ${CYAN}bash install.sh${RESET}"
echo -e " ${MUTED}(sudo sera demandé automatiquement quand nécessaire)${RESET}"
exit 1
fi
# ── Vérifications préalables ──────────────────────────────────────────────────
check_requirements() {
local ok=true

View File

@@ -19,6 +19,7 @@ PACKAGES=(
chafa
jq
libgtk-3-bin
adw-gtk3
nemo
nemo-fileroller
# fastfetch → installé via .deb GitHub (voir 02-packages-manual.sh)
@@ -36,6 +37,8 @@ PACKAGES=(
python3-gi
gir1.2-gtk-3.0
gir1.2-gtklayershell-0.1
gir1.2-gdkpixbuf-2.0
gir1.2-pango-1.0
# ── Fun & utils ──────────────────────────────────────────────────────────
cmatrix
toilet

View File

@@ -11,7 +11,14 @@ deploy_file() {
local src="$1"
local dst="$2"
ensure_dir "$(dirname "$dst")"
if [ -f "$dst" ]; then
if [ -L "$dst" ]; then
# Symlink géré par COSMIC : sauvegarder la cible réelle puis supprimer le lien
local real; real=$(readlink -f "$dst")
local rel="${dst#"$HOME/"}"
ensure_dir "$BACKUP_DIR/$(dirname "$rel")"
cp "$real" "$BACKUP_DIR/$rel" 2>/dev/null
rm "$dst"
elif [ -f "$dst" ]; then
local rel="${dst#"$HOME/"}"
ensure_dir "$BACKUP_DIR/$(dirname "$rel")"
cp "$dst" "$BACKUP_DIR/$rel" 2>/dev/null
@@ -118,11 +125,27 @@ else
fail "CosmicTerm"
fi
# ── GTK3 — thème violet-chaton ─────────────────────────────────────────────
section "GTK3 — thème violet-chaton"
# ── GTK3 / GTK4 — thème violet-chaton ─────────────────────────────────────
section "GTK — thème violet-chaton"
step "Thème GTK3 (adw-gtk3-dark + couleurs violet-chaton)..."
ensure_dir "$HOME/.config/gtk-3.0"
deploy_file "$THEMES/violet-chaton-gtk.css" "$HOME/.config/gtk-3.0/gtk.css"
step "Thème GTK4 / libadwaita (couleurs violet-chaton)..."
ensure_dir "$HOME/.config/gtk-4.0"
deploy_file "$THEMES/violet-chaton-gtk.css" "$HOME/.config/gtk-4.0/gtk.css"
step "Activation adw-gtk3-dark + dark mode (gsettings)..."
if has_cmd gsettings; then
gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3-dark' 2>/dev/null && \
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' 2>/dev/null && \
ok "gtk-theme=adw-gtk3-dark, color-scheme=prefer-dark" || \
warn "gsettings GTK échoué — thème à appliquer manuellement"
else
warn "gsettings non disponible — thème GTK à appliquer manuellement"
fi
# ── Nemo — gestionnaire de fichiers ────────────────────────────────────────
section "Nemo — configuration et thème"

View File

@@ -1 +1 @@
"cosmic-light"
"Violet-chaton"

View File

@@ -1,247 +1,122 @@
/* ── violet-chaton GTK3 theme — Nemo & GTK apps ───────────────────────────────
/* ── violet-chaton GTK theme (adw-gtk3-dark compatible) ────────────────────
*
* Couleurs extraites du thème COSMIC violet-chaton :
* bg #341C4A background.base
* surface #493161 background.component.base
* hover #5B4671 background.component.hover
* accent #E79CFE accent.base
* text #FCFCF6 background.on
* muted #7F849C neutral_7
* border #5C496C background.divider
* sidebar #2B1540 (bg légèrement plus sombre)
* Contenu identique au dark.css généré par COSMIC pour le thème violet-chaton.
* adw-gtk3-dark et libadwaita lisent ces variables @define-color.
* Sur le PC principal, COSMIC gère ce fichier via symlink — ce fichier
* sert de fallback lors de la première installation.
* ─────────────────────────────────────────────────────────────────────────── */
@define-color theme_bg_color #341C4A;
@define-color theme_fg_color #FCFCF6;
@define-color theme_base_color #493161;
@define-color theme_selected_bg_color #E79CFE;
@define-color theme_selected_fg_color #341C4A;
@define-color theme_text_color #FCFCF6;
@define-color borders #5C496C;
@define-color window_bg_color rgba(52, 28, 74, 1.00);
@define-color window_fg_color rgba(252, 252, 246, 1.00);
/* ── Fenêtre principale ────────────────────────────────────────────────────── */
window, .background {
background-color: #341C4A;
color: #FCFCF6;
}
@define-color view_bg_color rgba(56, 35, 75, 1.00);
@define-color view_fg_color rgba(193, 193, 187, 1.00);
/* ── Barre de titre / headerbar ───────────────────────────────────────────── */
headerbar, .titlebar {
background-color: #493161;
color: #FCFCF6;
border-bottom: 1px solid #5C496C;
}
@define-color headerbar_bg_color rgba(52, 28, 74, 1.00);
@define-color headerbar_fg_color rgba(252, 252, 246, 1.00);
@define-color headerbar_border_color_color rgba(92, 73, 108, 1.00);
@define-color headerbar_backdrop_color rgba(52, 28, 74, 1.00);
headerbar button, .titlebar button {
background-color: transparent;
color: #FCFCF6;
border: none;
border-radius: 6px;
}
@define-color sidebar_bg_color rgba(56, 35, 75, 1.00);
@define-color sidebar_fg_color rgba(193, 193, 187, 1.00);
@define-color sidebar_shade_color rgba(0, 0, 0, 0.08);
@define-color sidebar_backdrop_color rgba(72, 53, 89, 1.00);
headerbar button:hover, .titlebar button:hover {
background-color: #5B4671;
}
@define-color secondary_sidebar_bg_color rgba(69, 71, 90, 1.00);
@define-color secondary_sidebar_fg_color rgba(225, 225, 219, 1.00);
@define-color secondary_sidebar_shade_color rgba(0, 0, 0, 0.08);
@define-color secondary_sidebar_backdrop_color rgba(84, 86, 103, 1.00);
headerbar button:active, .titlebar button:active {
background-color: #E79CFE;
color: #341C4A;
}
@define-color card_bg_color rgba(73, 49, 97, 1.00);
@define-color card_fg_color rgba(212, 212, 206, 1.00);
/* ── Sidebar Nemo (paneau des emplacements) ───────────────────────────────── */
.sidebar, placessidebar {
background-color: #2B1540;
color: #FCFCF6;
border-right: 1px solid #5C496C;
}
@define-color thumbnail_bg_color rgba(73, 49, 97, 1.00);
@define-color thumbnail_fg_color rgba(212, 212, 206, 1.00);
.sidebar row, placessidebar row {
border-radius: 6px;
padding: 2px 4px;
}
@define-color dialog_bg_color rgba(56, 35, 75, 1.00);
@define-color dialog_fg_color rgba(193, 193, 187, 1.00);
.sidebar row:hover, placessidebar row:hover {
background-color: #5B4671;
}
@define-color popover_bg_color rgba(73, 49, 97, 1.00);
@define-color popover_fg_color rgba(212, 212, 206, 1.00);
.sidebar row:selected, placessidebar row:selected {
background-color: #E79CFE;
color: #341C4A;
}
@define-color shade_color rgba(0, 0, 0, 0.32);
@define-color scrollbar_outline_color rgba(52, 28, 74, 0.50);
.sidebar .sidebar-section-header, placessidebar .sidebar-section-header {
color: #7F849C;
font-size: smaller;
}
@define-color accent_color rgba(231, 156, 254, 1.00);
@define-color accent_bg_color rgba(231, 156, 254, 1.00);
@define-color accent_fg_color rgba(0, 0, 0, 1.00);
/* ── Vue fichiers (icônes + liste) ───────────────────────────────────────── */
.view, iconview, treeview {
background-color: #341C4A;
color: #FCFCF6;
}
@define-color destructive_color rgba(243, 139, 168, 1.00);
@define-color destructive_bg_color rgba(243, 139, 168, 1.00);
@define-color destructive_fg_color rgba(0, 0, 0, 1.00);
.view:selected, iconview:selected,
treeview:selected, .view:focus:selected {
background-color: #E79CFE;
color: #341C4A;
}
@define-color warning_color rgba(249, 226, 175, 1.00);
@define-color warning_bg_color rgba(249, 226, 175, 1.00);
@define-color warning_fg_color rgba(0, 0, 0, 1.00);
/* En-têtes de colonnes (vue liste) */
treeview header button {
background-color: #493161;
color: #FCFCF6;
border: none;
border-right: 1px solid #5C496C;
border-bottom: 1px solid #5C496C;
}
@define-color success_color rgba(166, 227, 161, 1.00);
@define-color success_bg_color rgba(166, 227, 161, 1.00);
@define-color success_fg_color rgba(0, 0, 0, 1.00);
treeview header button:hover {
background-color: #5B4671;
}
@define-color accent_color rgba(231, 156, 254, 1.00);
@define-color accent_bg_color rgba(231, 156, 254, 1.00);
@define-color accent_fg_color rgba(0, 0, 0, 1.00);
/* ── Barre d'outils / pathbar ─────────────────────────────────────────────── */
toolbar, .path-bar {
background-color: #493161;
border-bottom: 1px solid #5C496C;
}
@define-color error_color rgba(243, 139, 168, 1.00);
@define-color error_bg_color rgba(243, 139, 168, 1.00);
@define-color error_fg_color rgba(0, 0, 0, 1.00);
.path-bar button {
background-color: transparent;
color: #FCFCF6;
border: none;
border-radius: 6px;
}
@define-color blue_1 rgba(151, 195, 255, 1.00);
@define-color blue_2 rgba(144, 187, 255, 1.00);
@define-color blue_3 rgba(137, 180, 250, 1.00);
@define-color blue_4 rgba(114, 156, 224, 1.00);
@define-color blue_5 rgba(91, 132, 199, 1.00);
.path-bar button:hover {
background-color: #5B4671;
}
@define-color green_1 rgba(175, 236, 170, 1.00);
@define-color green_2 rgba(171, 232, 165, 1.00);
@define-color green_3 rgba(166, 227, 161, 1.00);
@define-color green_4 rgba(139, 199, 134, 1.00);
@define-color green_5 rgba(113, 171, 108, 1.00);
.path-bar button:checked {
background-color: #5B4671;
color: #E79CFE;
}
@define-color yellow_1 rgba(254, 231, 180, 1.00);
@define-color yellow_2 rgba(252, 229, 178, 1.00);
@define-color yellow_3 rgba(249, 226, 175, 1.00);
@define-color yellow_4 rgba(219, 196, 146, 1.00);
@define-color yellow_5 rgba(189, 167, 118, 1.00);
/* ── Barre de recherche / entrée texte ────────────────────────────────────── */
entry {
background-color: #5B4671;
color: #FCFCF6;
border: 1px solid #5C496C;
border-radius: 6px;
padding: 4px 8px;
}
@define-color red_1 rgba(255, 154, 183, 1.00);
@define-color red_2 rgba(252, 147, 176, 1.00);
@define-color red_3 rgba(243, 139, 168, 1.00);
@define-color red_4 rgba(217, 116, 145, 1.00);
@define-color red_5 rgba(191, 93, 122, 1.00);
entry:focus {
border-color: #E79CFE;
box-shadow: 0 0 0 2px alpha(#E79CFE, 0.3);
}
@define-color orange_1 rgba(255, 190, 146, 1.00);
@define-color orange_2 rgba(255, 185, 140, 1.00);
@define-color orange_3 rgba(250, 179, 135, 1.00);
@define-color orange_4 rgba(222, 153, 110, 1.00);
@define-color orange_5 rgba(195, 128, 85, 1.00);
entry placeholder {
color: #7F849C;
}
@define-color purple_1 rgba(192, 202, 255, 1.00);
@define-color purple_2 rgba(186, 196, 255, 1.00);
@define-color purple_3 rgba(180, 190, 254, 1.00);
@define-color purple_4 rgba(155, 164, 226, 1.00);
@define-color purple_5 rgba(130, 139, 200, 1.00);
@define-color light_0 rgba(0, 0, 0, 1.00);
@define-color light_1 rgba(3, 3, 16, 1.00);
@define-color light_2 rgba(24, 25, 43, 1.00);
@define-color light_3 rgba(50, 53, 72, 1.00);
@define-color light_4 rgba(79, 82, 103, 1.00);
@define-color dark_0 rgba(110, 114, 135, 1.00);
@define-color dark_1 rgba(143, 147, 169, 1.00);
@define-color dark_2 rgba(177, 181, 205, 1.00);
@define-color dark_3 rgba(213, 217, 241, 1.00);
@define-color dark_4 rgba(255, 255, 255, 1.00);
/* ── Scrollbar ────────────────────────────────────────────────────────────── */
scrollbar {
background-color: transparent;
}
scrollbar slider {
background-color: #5C496C;
border-radius: 10px;
min-width: 6px;
min-height: 6px;
}
scrollbar slider:hover {
background-color: #E79CFE;
}
/* ── Menu contextuel ──────────────────────────────────────────────────────── */
menu, .context-menu, .popup {
background-color: #493161;
color: #FCFCF6;
border: 1px solid #5C496C;
border-radius: 8px;
padding: 4px;
}
menuitem {
border-radius: 6px;
padding: 4px 8px;
}
menuitem:hover {
background-color: #E79CFE;
color: #341C4A;
}
menuitem accelerator {
color: #7F849C;
}
separator, menuitem separator {
background-color: #5C496C;
min-height: 1px;
}
/* ── Barre de statut ──────────────────────────────────────────────────────── */
.statusbar, statusbar {
background-color: #493161;
color: #7F849C;
border-top: 1px solid #5C496C;
font-size: smaller;
}
/* ── Boutons génériques ───────────────────────────────────────────────────── */
button {
background-color: #5B4671;
color: #FCFCF6;
border: 1px solid #5C496C;
border-radius: 6px;
padding: 4px 12px;
}
button:hover {
background-color: #E79CFE;
color: #341C4A;
border-color: #E79CFE;
}
button.suggested-action {
background-color: #E79CFE;
color: #341C4A;
border-color: #E79CFE;
}
button.destructive-action {
background-color: #F38BA8;
color: #341C4A;
}
/* ── Notebooks / onglets ─────────────────────────────────────────────────── */
notebook tab {
background-color: #493161;
color: #7F849C;
border-radius: 6px 6px 0 0;
padding: 4px 12px;
}
notebook tab:checked {
background-color: #5B4671;
color: #FCFCF6;
}
/* ── Popover ─────────────────────────────────────────────────────────────── */
popover {
background-color: #493161;
color: #FCFCF6;
border: 1px solid #5C496C;
border-radius: 8px;
}
/* ── Tooltip ─────────────────────────────────────────────────────────────── */
tooltip {
background-color: #5B4671;
color: #FCFCF6;
border-radius: 6px;
padding: 4px 8px;
}
/* ── Variables GTK3 classiques (compat apps legacy) ─────────────────────── */
@define-color theme_bg_color rgba(52, 28, 74, 1.00);
@define-color theme_fg_color rgba(252, 252, 246, 1.00);
@define-color theme_base_color rgba(73, 49, 97, 1.00);
@define-color theme_selected_bg_color rgba(231, 156, 254, 1.00);
@define-color theme_selected_fg_color rgba(52, 28, 74, 1.00);
@define-color theme_text_color rgba(252, 252, 246, 1.00);
@define-color borders rgba(92, 73, 108, 1.00);

View File

@@ -102,9 +102,11 @@ window#waybar {
#cpu,
#temperature,
#custom-cpu-temp,
#custom-gpu,
#memory,
#disk,
#custom-disks,
#custom-network,
#clock,
#custom-date,
@@ -140,7 +142,8 @@ window#waybar {
/* ── Température ──────────────────────────────────────────────────────────── */
#temperature {
#temperature,
#custom-cpu-temp {
color: rgba(139, 233, 253, 0.60);
font-size: 11px;
font-weight: normal;
@@ -148,12 +151,17 @@ window#waybar {
padding-right: 10px;
}
#temperature.critical {
#temperature.critical,
#custom-cpu-temp.critical {
color: #f38ba8;
font-weight: bold;
animation: pulse-critical 0.8s linear infinite;
}
#custom-cpu-temp.warning {
color: #f9e2af;
}
/* ── GPU ──────────────────────────────────────────────────────────────────── */
#custom-gpu {
@@ -186,7 +194,8 @@ window#waybar {
/* ── Disque ───────────────────────────────────────────────────────────────── */
#disk {
#disk,
#custom-disks {
color: rgba(255, 121, 198, 0.70);
font-size: 11px;
font-weight: normal;
@@ -405,6 +414,8 @@ tooltip label {
#cpu:hover,
#memory:hover,
#disk:hover,
#custom-disks:hover,
#custom-cpu-temp:hover,
#custom-network:hover,
#wireplumber:hover,
#backlight:hover,

436
README.md
View File

@@ -1,416 +1,86 @@
# violet-chaton — setup automatique
# violet-chaton
Environnement terminal complet aux couleurs violet-chaton, pensé pour Pop!_OS / Ubuntu avec COSMIC Desktop.
(Bonus thème Vesktop / Discord)
> Rice Pop!_OS complet aux couleurs violet-chaton — compatible **COSMIC** et **Hyprland**
![preview](assets/preview.png)
---
## Démarrage rapide
## Install
```bash
bash install.sh
git clone https://git.tetardtek.com/Tetardtek/dotfiles-violet-chaton.git
cd dotfiles-violet-chaton
bash INSTALL/install.sh
```
Un menu s'affiche. Choisis **1** pour une installation complète.
Choisir **1** (complète) ou **4** (configs uniquement si les outils sont déjà là).
Prérequis : `sudo apt install -y curl git unzip`
---
## Prérequis
## Ce que tu obtiens
- **Distribution :** Debian, Ubuntu, Pop!_OS (ou dérivé apt)
- **Droits :** compte avec `sudo`
- **Connexion internet** (téléchargement des binaires)
- **Outils de base :**
```bash
sudo apt install -y curl git unzip
```
> Pour COSMIC Desktop : Pop!_OS 24.04 ou supérieur.
**Shell** — zsh · starship · atuin · zinit · autosuggestions · syntax-highlighting
**CLI** — eza · bat · fd · fzf · zoxide · ripgrep · lazygit · delta · yazi · btop
**Terminal** — fastfetch avec sprite Pokémon via chafa · LS_COLORS patché depuis catppuccin-mocha via vivid · cava · pipes.sh
**Desktop** — Waybar 3-pills glassmorphism · Wofi · Rofi · wob (OSD volume/luminosité)
**Polices** — JetBrainsMono NL + 0xProto Nerd Fonts · icônes candy-icons
**Apps** — Vivaldi (thème injecté auto) · Vesktop/Discord · Nemo · GTK3/GTK4
---
## Ce que fait le script — étape par étape
## COSMIC
### Étape 1 — Paquets apt (`01-packages-apt.sh`)
Déploiement automatique complet :
Installe les outils via le gestionnaire de paquets système :
| Outil | Rôle |
|------------------|------|
| `zsh` | Shell principal (remplace bash) |
| `eza` | Remplacement de `ls` avec icônes et couleurs |
| `bat` | Remplacement de `cat` avec coloration syntaxique |
| `fd-find` | Remplacement de `find`, plus rapide et intuitif |
| `fzf` | Fuzzy finder — recherche floue de fichiers et dossiers |
| `zoxide` | Remplacement de `cd` avec mémoire des dossiers fréquents |
| `git-delta` | Pager git avec diff coloré côte à côte |
| `vivid` | Générateur de LS_COLORS |
| `ripgrep` | Remplacement de `grep`, très rapide |
| `ncdu` | Analyse d'espace disque en TUI |
| `thefuck` | Correction automatique de la dernière commande ratée |
| `lolcat` | Arc-en-ciel sur n'importe quel output |
| `cbonsai` | Bonsaï ASCII animé |
| `chafa` | Affichage d'images dans le terminal (logo fastfetch) |
| `cava` | Visualiseur audio animé |
| `btop` | Moniteur système en TUI |
| `nemo` | Explorateur de fichiers GUI |
| `jq` | Processeur JSON en ligne de commande |
| `vivaldi-stable` | Navigateur — dépôt officiel ajouté automatiquement |
| `gh` | CLI GitHub (auth, PR, issues) — dépôt officiel ajouté automatiquement |
| `cmatrix` | Pluie de caractères style Matrix |
| `toilet` | Texte en gros ASCII art coloré (bannières dans le terminal) |
| `w3m` | Navigateur web en mode texte dans le terminal |
| `jp2a` | Conversion d'images JPEG/PNG en ASCII art |
| `qalc` | Calculatrice CLI — unités, conversions, expressions complexes |
| `waybar` | Barre de statut Wayland 3-pills glassmorphism |
| `wob` | Overlay volume/luminosité animé |
| `wofi` | Launcher d'applications et menu power |
| `brightnessctl` | Contrôle de la luminosité rétroéclairage |
| `playerctl` | Contrôle MPRIS (lecture/pause, titre en cours) |
| `wireplumber` | Gestionnaire audio PipeWire (`wpctl`) |
| `python3-gi` | Bindings Python GTK3 (popups volume/luminosité) |
| `gir1.2-gtk-3.0` | Introspection GTK3 pour Python |
| `gir1.2-gtklayershell-0.1` | Layer-shell Wayland pour popups GTK flottants |
Définit aussi **zsh comme shell par défaut** via `chsh`.
- Thème COSMIC Dark/Light — palette violet-chaton
- CosmicTerm — police, couleurs, profil
- CosmicTk — polices UI + icônes candy-icons
- `cosmic-osd` désactivé, remplacé par `wob`
- Waybar + autostart configurés
---
### Étape 2 — Binaires manuels (`02-packages-manual.sh`)
## Hyprland
Télécharge les versions les plus récentes depuis GitHub et les installe dans `~/.local/bin/` :
Les configs shell, waybar, wofi, rofi, kitty et zsh fonctionnent **tels quels** sous Hyprland.
Ce qui reste à câbler côté Hyprland :
| Outil | Rôle |
|--------------|------|
| `lazygit` | Interface git complète en TUI (`lg`) |
| `yazi` | Gestionnaire de fichiers en TUI |
| `glow` | Rendu Markdown dans le terminal |
| `tldr` | Man pages simplifiées avec exemples (tealdeer) |
| `navi` | Cheatsheets interactives |
| `pipes.sh` | Animation de tuyaux dans le terminal |
| `fastfetch` | Infos système au démarrage du terminal — `.deb` depuis GitHub |
| `uv` / `uvx` | Gestionnaire de paquets Python ultra-rapide — script officiel astral.sh |
```
hyprland.conf — keybinds, moniteurs, règles fenêtres
hyprpaper / swww — fond d'écran
hyprlock — écran de verrouillage
mako — notifications
```
Installe également :
- **starship** et **atuin** via leurs scripts officiels
- **zinit** (gestionnaire de plugins zsh) via git clone
- **Nerd Fonts** — JetBrainsMono NL et 0xProto, vers `~/.local/share/fonts/NerdFonts/`
- **candy-icons** — thème d'icônes, vers `~/.local/share/icons/candy-icons-master/`
> **nomachine** — à installer manuellement depuis [nomachine.com](https://www.nomachine.com/download) (comme VSCode et Vesktop)
Met à jour le cache des pages tldr et le cache de polices (`fc-cache`).
> Ces configs ne sont pas (encore) dans le repo — les contributions sont les bienvenues.
---
### Étape 3 — Déploiement des configs et thèmes (`03-deploy-configs.sh`)
## Palette
Copie les fichiers de config et thèmes aux bons emplacements.
Avant chaque déploiement, les fichiers existants sont sauvegardés dans :
```
~/.config/violet-chaton-backups/YYYYMMDD-HHMMSS/
```
Le chemin exact est affiché à la fin du script si des fichiers ont été sauvegardés.
**Configs shell :**
- `~/.zshrc` — configuration zsh complète
- `~/.bashrc` — configuration bash minimale (PATH + `exec zsh`)
- `~/.gitconfig` — git avec delta comme pager
> `user.name` et `user.email` présents dans le gitconfig existant sont **automatiquement préservés** après le déploiement.
**Configs outils :**
- `~/.config/starship.toml` — prompt 2 lignes violet-chaton
- `~/.config/bat/config` — thème violet-chaton, style header
- `~/.config/btop/btop.conf` — moniteur avec thème violet-chaton
- `~/.config/fastfetch/config.jsonc` — modules système + logo chafa
- `~/.config/atuin/config.toml` — historique fuzzy, colonnes, thème
- `~/.config/lazygit/config.yml` — couleurs violet-chaton + delta
- `~/.config/yazi/yazi.toml` — config gestionnaire de fichiers
- `~/.config/glow/glow.yml` — style markdown dark
**Thèmes CLI :**
- `~/.config/bat/themes/violet-chaton.tmTheme`
- `~/.config/btop/themes/violet-chaton.theme`
- `~/.config/atuin/themes/violet-chaton.toml`
- `~/.config/cava/config`
- `~/.config/yazi/theme.toml`
- `~/.config/vesktop/themes/violet-chaton.theme.css` — Vesktop natif (toujours déployé)
- `~/.var/app/dev.vencord.Vesktop/config/vesktop/themes/` — Vesktop Flatpak (si installé)
**GTK3 et Nemo :**
- `~/.config/gtk-3.0/gtk.css` — thème GTK3 violet-chaton (Nemo et applications GTK)
- Nemo défini comme gestionnaire de fichiers par défaut (`xdg-mime`)
- Préférences Nemo appliquées via `gsettings` : vue icônes, miniatures, zoom standard
- Thème d'icônes **candy-icons** activé via `gsettings`
**COSMIC Desktop (entièrement automatique) :**
- `~/.config/cosmic/com.system76.CosmicTheme.Dark/v1/` — palette violet-chaton complète
- `~/.config/cosmic/com.system76.CosmicTheme.Light/v1/` — palette violet-chaton (mode clair)
- `~/.config/cosmic/com.system76.CosmicTheme.Mode/v1/is_dark` — mode sombre activé
- `~/.config/cosmic/com.system76.CosmicTerm/v1/` — police JetBrains Mono, couleurs, profil
- `~/.config/cosmic/com.system76.CosmicTk/v1/` — icônes candy-icons, polices UI 0xProto
**Waybar — island floating 3 pills :**
- `~/.config/waybar/config` — modules left/center/right
- `~/.config/waybar/style.css` — glassmorphism, bordures roses, animations
- `~/.config/waybar/cava-waybar.cfg` — config CAVA dédié waybar
- `~/.config/waybar/scripts/` — tous les scripts (gpu, network, power-profile, cava, wob, popups GTK)
- `~/.config/autostart/waybar.desktop` — démarrage automatique avec COSMIC
- `~/.config/autostart/wob.desktop` — démarrage automatique de l'overlay wob
**Wofi — launcher + menu power :**
- `~/.config/wofi/config` — configuration wofi
- `~/.config/wofi/style.css` — thème violet-chaton (launcher apps)
- `~/.config/wofi/power-style.css` — thème violet-chaton (menu power)
**wob — overlay volume/luminosité :**
- `~/.config/wob.ini` — couleurs violet-chaton, position bas de l'écran
**Système (avec sudo) :**
- `/etc/sudoers.d/waybar-power-profile` — changement de profil énergie sans mot de passe
- `/etc/udev/rules.d/90-platform-profile.rules` — permissions groupe `video` sur platform_profile
**Vivaldi (avec pause de sécurité) :**
- Si Vivaldi n'a pas encore été lancé, le script s'arrête et demande de le démarrer une fois
- Le thème **Rice Violet-Chaton** est ensuite injecté directement dans `~/.config/vivaldi/Default/Preferences`
Reconstruit également le **cache bat** pour activer le thème de coloration syntaxique.
| | Hex | Rôle |
|-|-----|------|
| ![#261537](https://placehold.co/12x12/261537/261537.png) | `#261537` | Background |
| ![#341c4a](https://placehold.co/12x12/341c4a/341c4a.png) | `#341c4a` | BG medium |
| ![#3d2454](https://placehold.co/12x12/3d2454/3d2454.png) | `#3d2454` | BG high · sélection |
| ![#ff79c6](https://placehold.co/12x12/ff79c6/ff79c6.png) | `#ff79c6` | Pink — accents · bordures |
| ![#e79cfe](https://placehold.co/12x12/e79cfe/e79cfe.png) | `#e79cfe` | Purple — accents secondaires |
| ![#8be9fd](https://placehold.co/12x12/8be9fd/8be9fd.png) | `#8be9fd` | Cyan — commandes · highlights |
| ![#f8f8f2](https://placehold.co/12x12/f8f8f2/f8f8f2.png) | `#f8f8f2` | Text |
---
## Étapes manuelles après installation
## Raccourcis
### zinit — premier démarrage
| Touche | Action |
|--------|--------|
| `Ctrl+R` | Historique atuin |
| `Ctrl+G` | Fichier (fzf) |
| `Ctrl+F` | Dossier (fzf) |
| `Ctrl+Space` | Accepter suggestion |
Au premier lancement de zsh, zinit télécharge automatiquement les plugins :
- `zsh-autosuggestions` — suggestions en gris au fil de la frappe
- `zsh-syntax-highlighting` — coloration des commandes en temps réel
- `zsh-completions` — complétion étendue
## Alias clés
Cela peut prendre quelques secondes lors du tout premier démarrage.
### Polices — vérification
Si les icônes ne s'affichent pas correctement dans le terminal, forcer la reconstruction du cache de polices :
```bash
fc-cache -f -v
```
Puis sélectionner **JetBrainsMono NL Nerd Font** dans les préférences du terminal.
### atuin — synchronisation (optionnel)
atuin peut synchroniser l'historique entre machines. Pour activer la sync :
```bash
atuin register # créer un compte
atuin sync # synchroniser
```
Sans compte, atuin fonctionne en local uniquement.
---
## Log d'installation
Chaque installation génère un fichier log horodaté :
```
~/violet-chaton-install-YYYYMMDD-HHMMSS.log
```
En cas d'erreur, consulter ce fichier pour identifier l'étape qui a échoué.
---
## Troubleshooting
### Icônes ne s'affichent pas
- Vérifier que la police **JetBrainsMono NL Nerd Font** est sélectionnée dans le terminal
- Relancer `fc-cache -f` puis rouvrir le terminal
### zinit ne se lance pas
- Vérifier que `~/.local/share/zinit/zinit.git/zinit.zsh` existe
- Relancer le script d'installation étape 3
### Injection Vivaldi échoue
- Lancer Vivaldi une première fois, le fermer complètement, puis relancer `bash install.sh` → option 4
### Thème bat ne s'applique pas
```bash
bat cache --build
```
### candy-icons ne s'affiche pas dans Nemo
```bash
gtk-update-icon-cache ~/.local/share/icons/candy-icons-master
gsettings set org.gnome.desktop.interface icon-theme 'candy-icons-master'
```
### Waybar ne démarre pas
```bash
waybar & # tester manuellement, lire les erreurs
pkill -SIGUSR2 waybar # recharger la config à chaud
```
### Popups volume/luminosité ne s'ouvrent pas
Vérifier que les dépendances Python sont installées :
```bash
python3 -c "import gi; gi.require_version('GtkLayerShell', '0.1'); print('OK')"
```
### Profil énergie ne change pas au clic
Vérifier que la règle sudoers et les permissions udev sont en place :
```bash
ls -la /sys/firmware/acpi/platform_profile # doit être g+w groupe video
cat /etc/sudoers.d/waybar-power-profile
```
---
## Raccourcis configurés
| Raccourci | Action |
|----------------|--------|
| `Ctrl+R` | Historique atuin (fuzzy search) |
| `Ctrl+G` | Rechercher un fichier (fzf) |
| `Ctrl+F` | Naviguer vers un dossier (fzf) |
| `Ctrl+Space` | Accepter la suggestion autosuggestions |
| `→` | Accepter la suggestion mot par mot |
---
## Alias configurés
| Alias | Commande réelle |
|----------|-----------------|
| `ls` | `eza --icons --git --group-directories-first` |
| `ll` | `eza -l --icons --git` |
| `lt` | `eza --tree --icons` |
| `cat` | `batcat --paging=never` |
| `bat` | `batcat` |
| `fd` | `fdfind` |
| `man` | `tldr` |
| `lg` | `lazygit` |
| `rg` | `rg --color=always` |
| `disk` | `ncdu` |
| `fetch` | `fastfetch` avec logo chafa |
| `pipes` | `pipes.sh` |
| `cd` | `zoxide` (avec mémoire) |
| `fuck` | correction auto thefuck |
| `grep` | `grep --color=auto` |
---
## Structure du dossier INSTALL/
```
INSTALL/
├── install.sh script principal — menu interactif
├── scripts/
│ ├── lib.sh couleurs et fonctions partagées
│ ├── 01-packages-apt.sh installation apt + Vivaldi
│ ├── 02-packages-manual.sh binaires GitHub + Nerd Fonts + candy-icons
│ └── 03-deploy-configs.sh configs, thèmes, COSMIC, Vivaldi
├── configs/ copies des fichiers de configuration
│ ├── zshrc
│ ├── bashrc
│ ├── gitconfig
│ ├── starship.toml
│ ├── bat.conf
│ ├── btop.conf
│ ├── fastfetch.jsonc
│ ├── atuin.toml
│ ├── lazygit.yml
│ ├── yazi.toml
│ ├── glow.yml
│ ├── autostart/
│ │ ├── waybar.desktop démarrage automatique waybar
│ │ └── wob.desktop démarrage automatique wob
│ ├── waybar/
│ │ ├── config modules 3-pills
│ │ ├── cava-waybar.cfg config CAVA dédiée waybar
│ │ └── scripts/ gpu, network, power-profile, cava, wob, popups GTK
│ ├── wofi/
│ │ └── config config wofi
│ └── wob/
│ └── wob.ini overlay volume/luminosité
├── assets/
│ └── violet-chaton-logo.png logo fastfetch (1024×1024)
└── themes/ tous les fichiers de thème violet-chaton
├── violet-chaton-bat.tmTheme
├── violet-chaton-btop.theme
├── violet-chaton-atuin.toml
├── violet-chaton-cava.conf
├── violet-chaton-yazi.toml
├── violet-chaton-gtk.css thème GTK3 (Nemo + applications GTK)
├── violet-chaton-ls-colors.sh
├── violet-chaton-vivaldi.json thème Rice Violet-Chaton pour Vivaldi
├── violet-chaton.theme.css thème Discord/Vesktop compilé
├── violet-chaton-waybar.css CSS 3-pills glassmorphism
├── violet-chaton-wofi.css thème wofi launcher
├── violet-chaton-wofi-power.css thème wofi menu power
├── cosmic/ configs COSMIC déployées automatiquement
│ ├── com.system76.CosmicTheme.Dark/v1/
│ ├── com.system76.CosmicTheme.Light/v1/
│ ├── com.system76.CosmicTheme.Mode/v1/
│ ├── com.system76.CosmicTerm/v1/
│ └── com.system76.CosmicTk/v1/
└── violet-chaton-discord-theme/ sources SCSS du thème (gitignored)
```
---
## Mettre à jour les configs
Après avoir modifié un fichier de config sur ta machine, re-copier vers INSTALL/ :
```bash
# Exemple : mettre à jour la config starship
cp ~/.config/starship.toml ~/Documents/config-violet-chaton/INSTALL/configs/starship.toml
# Mettre à jour les configs COSMIC
cp ~/.config/cosmic/com.system76.CosmicTerm/v1/* \
~/Documents/config-violet-chaton/INSTALL/themes/cosmic/com.system76.CosmicTerm/v1/
# Mettre à jour la config ou le CSS waybar
cp ~/.config/waybar/config \
~/Documents/config-violet-chaton/INSTALL/configs/waybar/config
cp ~/.config/waybar/style.css \
~/Documents/config-violet-chaton/INSTALL/themes/violet-chaton-waybar.css
# Mettre à jour un script waybar
cp ~/.config/waybar/scripts/power-profile.sh \
~/Documents/config-violet-chaton/INSTALL/configs/waybar/scripts/power-profile.sh
```
---
## Palette violet-chaton
| Rôle | Hex |
|------------|-----------|
| Background | `#261537` |
| BG medium | `#341c4a` |
| BG high | `#3d2454` |
| Pink | `#ff79c6` |
| Purple | `#e79cfe` |
| Cyan | `#8be9fd` |
| Text | `#f8f8f2` |
| Muted | `#6c7086` |
| Overlay | `#9399b2` |
| Success | `#a6e3a1` |
| Warning | `#f9e2af` |
| Danger | `#f38ba8` |
`ls` → eza · `cat` → bat · `fd` → fdfind · `man` → tldr · `lg` → lazygit · `disk` → ncdu · `fetch` → fastfetch+chafa