Files
gnome-asus-kbd-rgb/extension/backend.js
2025-12-21 06:55:49 +01:00

286 lines
8.2 KiB
JavaScript

// backend.js - Interface sysfs et logique métier pour le rétroéclairage ASUS RGB
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
// Chemins sysfs pour le rétroéclairage ASUS
const SYSFS_BASE = '/sys/class/leds/asus::kbd_backlight';
const BRIGHTNESS_PATH = `${SYSFS_BASE}/brightness`;
const MAX_BRIGHTNESS_PATH = `${SYSFS_BASE}/max_brightness`;
const RGB_MODE_PATH = `${SYSFS_BASE}/kbd_rgb_mode`;
// Niveaux d'intensité (0=Off, 1=Faible, 2=Moyen, 3=Fort)
const BRIGHTNESS_LEVELS = [0, null, null, null]; // Sera rempli dynamiquement avec max_brightness
// Timer pour le debouncing
let debounceTimer = null;
const DEBOUNCE_DELAY = 75; // ms
/**
* Vérifie si le matériel ASUS RGB est présent sur le système
* @returns {boolean} true si le matériel est supporté
*/
export function checkHardwareSupport() {
try {
const brightnessFile = Gio.File.new_for_path(BRIGHTNESS_PATH);
const rgbFile = Gio.File.new_for_path(RGB_MODE_PATH);
return brightnessFile.query_exists(null) && rgbFile.query_exists(null);
} catch (e) {
console.error('Erreur lors de la vérification du matériel:', e);
return false;
}
}
/**
* Vérifie si l'utilisateur a les permissions d'écriture
* @returns {boolean} true si les permissions sont OK
*/
export function checkPermissions() {
try {
const brightnessFile = Gio.File.new_for_path(BRIGHTNESS_PATH);
const info = brightnessFile.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
// Vérifier si on peut écrire
return info.get_attribute_boolean('access::can-write');
} catch (e) {
console.error('Erreur lors de la vérification des permissions:', e);
return false;
}
}
/**
* Lit la valeur maximale de brightness
* @returns {number} Valeur max, ou 3 par défaut
*/
export function getMaxBrightness() {
try {
const file = Gio.File.new_for_path(MAX_BRIGHTNESS_PATH);
const [success, contents] = file.load_contents(null);
if (success) {
const maxValue = parseInt(new TextDecoder().decode(contents).trim());
// Initialiser les niveaux de brightness
BRIGHTNESS_LEVELS[1] = Math.floor(maxValue / 3);
BRIGHTNESS_LEVELS[2] = Math.floor(2 * maxValue / 3);
BRIGHTNESS_LEVELS[3] = maxValue;
return maxValue;
}
} catch (e) {
console.error('Erreur lors de la lecture de max_brightness:', e);
}
// Valeurs par défaut si échec
BRIGHTNESS_LEVELS[1] = 1;
BRIGHTNESS_LEVELS[2] = 2;
BRIGHTNESS_LEVELS[3] = 3;
return 3;
}
/**
* Lit la brightness actuelle
* @returns {number} Valeur actuelle, ou -1 en cas d'erreur
*/
export function readBrightness() {
try {
const file = Gio.File.new_for_path(BRIGHTNESS_PATH);
const [success, contents] = file.load_contents(null);
if (success) {
return parseInt(new TextDecoder().decode(contents).trim());
}
} catch (e) {
console.error('Erreur lors de la lecture de brightness:', e);
}
return -1;
}
/**
* Écrit la brightness (niveau 0-3)
* @param {number} level - Niveau d'intensité (0=Off, 1=Faible, 2=Moyen, 3=Fort)
* @returns {boolean} true si succès
*/
export function writeBrightness(level) {
try {
// Valider et clamper le niveau
level = Math.max(0, Math.min(3, Math.floor(level)));
const value = BRIGHTNESS_LEVELS[level];
if (value === null || value === undefined) {
console.error('Niveau de brightness invalide:', level);
return false;
}
const file = Gio.File.new_for_path(BRIGHTNESS_PATH);
const contents = `${value}\n`;
const [success] = file.replace_contents(
new TextEncoder().encode(contents),
null,
false,
Gio.FileCreateFlags.NONE,
null
);
if (success) {
console.log(`Brightness mise à ${level} (${value})`);
}
return success;
} catch (e) {
console.error('Erreur lors de l\'écriture de brightness:', e);
return false;
}
}
/**
* Clamp une valeur RGB entre 0 et 255
* @param {number} value - Valeur à clamper
* @returns {number} Valeur clampée
*/
function clampRGB(value) {
return Math.max(0, Math.min(255, Math.floor(value)));
}
/**
* Applique le master gain aux valeurs RGB
* @param {number} r - Rouge (0-255)
* @param {number} g - Vert (0-255)
* @param {number} b - Bleu (0-255)
* @param {number} masterGain - Gain master (0-100)
* @returns {Object} {r, g, b} avec gain appliqué
*/
function applyMasterGain(r, g, b, masterGain) {
const gain = masterGain / 100.0;
return {
r: clampRGB(r * gain),
g: clampRGB(g * gain),
b: clampRGB(b * gain)
};
}
/**
* Écrit les valeurs RGB dans kbd_rgb_mode
* @param {number} r - Rouge (0-255)
* @param {number} g - Vert (0-255)
* @param {number} b - Bleu (0-255)
* @param {number} masterGain - Gain master (0-100), défaut 100
* @returns {boolean} true si succès
*/
export function writeRGB(r, g, b, masterGain = 100) {
try {
// Clamper les valeurs
r = clampRGB(r);
g = clampRGB(g);
b = clampRGB(b);
// Appliquer le master gain
const adjusted = applyMasterGain(r, g, b, masterGain);
// Vérifier si brightness est > 0, sinon ne pas écrire
const currentBrightness = readBrightness();
if (currentBrightness === 0) {
console.log('Brightness est 0, RGB mémorisé mais non appliqué');
return true; // On considère cela comme un succès
}
// Format: "1 0 R G B 0\n"
const file = Gio.File.new_for_path(RGB_MODE_PATH);
const contents = `1 0 ${adjusted.r} ${adjusted.g} ${adjusted.b} 0\n`;
const [success] = file.replace_contents(
new TextEncoder().encode(contents),
null,
false,
Gio.FileCreateFlags.NONE,
null
);
if (success) {
console.log(`RGB mis à (${adjusted.r}, ${adjusted.g}, ${adjusted.b}) [master: ${masterGain}%]`);
}
return success;
} catch (e) {
console.error('Erreur lors de l\'écriture RGB:', e);
return false;
}
}
/**
* Applique RGB avec debouncing pour éviter de spammer sysfs
* @param {number} r - Rouge
* @param {number} g - Vert
* @param {number} b - Bleu
* @param {number} masterGain - Gain master
* @param {Function} callback - Callback optionnel appelé après l'écriture
*/
export function writeRGBDebounced(r, g, b, masterGain, callback = null) {
// Annuler le timer précédent
if (debounceTimer !== null) {
GLib.source_remove(debounceTimer);
debounceTimer = null;
}
// Créer un nouveau timer
debounceTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DEBOUNCE_DELAY, () => {
writeRGB(r, g, b, masterGain);
if (callback) {
callback();
}
debounceTimer = null;
return GLib.SOURCE_REMOVE;
});
}
/**
* Parse une chaîne preset "R,G,B" en objet
* @param {string} presetString - Format "R,G,B"
* @returns {Object} {r, g, b} ou null si invalide
*/
export function parsePreset(presetString) {
try {
const parts = presetString.split(',').map(s => parseInt(s.trim()));
if (parts.length === 3 && parts.every(n => !isNaN(n))) {
return {
r: clampRGB(parts[0]),
g: clampRGB(parts[1]),
b: clampRGB(parts[2])
};
}
} catch (e) {
console.error('Erreur lors du parsing du preset:', e);
}
return null;
}
/**
* Convertit RGB en hex
* @param {number} r - Rouge (0-255)
* @param {number} g - Vert (0-255)
* @param {number} b - Bleu (0-255)
* @returns {string} Format "#RRGGBB"
*/
export function rgbToHex(r, g, b) {
const toHex = (n) => {
const hex = clampRGB(n).toString(16).toUpperCase();
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* Nettoie les ressources (annule les timers en cours)
*/
export function cleanup() {
if (debounceTimer !== null) {
GLib.source_remove(debounceTimer);
debounceTimer = null;
}
}