Adds hyprland support for evremap orchestrator

This commit is contained in:
Michel Fedde 2025-05-10 18:44:27 +02:00
parent 0f36495ddd
commit 17c1aae84b
2 changed files with 171 additions and 41 deletions

View file

@ -200,18 +200,127 @@ class Logger {
} }
} }
class HyprlandSupport {
static HYPR_INPUT_CONFIG_FILE = path.join(process.env.HOME, '.config/hypr/config/settings/Input.conf')
static TARGET_CONFIG_FILE = path.join(process.env.HOME, '.cache/evremap_hyprland_config.conf')
#lastFile = '';
/**
* @returns {boolean}
*/
get isValid() {
return process.env.DESKTOP_SESSION === 'hyprland';
}
/**
* @param {FileContainer[]} activeFiles
*/
update(files) {
const configurations = this.#getConfigurations();
const activeFiles = files.filter((file) => file.state === 'running');
const activeConfigurations =
configurations.filter((config) =>
activeFiles.some((file) => {
const normalisedDeviceName = this.#getNormalizedDeviceName(file.device.name)
return config.name === normalisedDeviceName
})
)
this.#createFile(activeFiles, activeConfigurations)
}
#getConfigurations() {
const config = fs.readFileSync(HyprlandSupport.HYPR_INPUT_CONFIG_FILE, 'utf-8');
const lines = config.split('\n').filter(line => line);
const configurations = [];
let currentConfiguration;
let inDevice = false;
for (const line of lines) {
if (!inDevice) {
if (line === 'device {') {
inDevice = true;
currentConfiguration = {};
}
continue;
}
if (line === '}') {
inDevice = false;
configurations.push(currentConfiguration);
currentConfiguration = {};
continue;
}
const [ name, value ] = line.split('=');
currentConfiguration[name.trim()] = value.trim()
}
return configurations;
}
#createFile(files, configurations) {
let configFileContent = "";
for (const file of files) {
const configuration = configurations.find(
(config) => config.name === this.#getNormalizedDeviceName(file.device.name)
)
if (!configuration) {
continue
}
configuration.name = `evremap-virtual-input-for-${file.device.path}`;
const entry = Object.entries(configuration)
.map(([name, value]) => ` ${name} = ${value}`)
.join('\n');
configFileContent += `device {\n${entry}\n}\n`;
}
if (this.#lastFile === configFileContent) {
return;
}
this.#lastFile = configFileContent
fs.writeFileSync(HyprlandSupport.TARGET_CONFIG_FILE, configFileContent);
console.log("Writing new config")
}
/**
* @param {string} deviceName
* @returns {string}
*/
#getNormalizedDeviceName(deviceName) {
return deviceName.replaceAll(' ', '-').toLowerCase();
}
}
/** /**
* @typedef {"waiting"|"running"|"failed"|"destroy"|"invalid"} FileContainerState * @typedef {"waiting"|"running"|"failed"|"destroy"|"invalid"} FileContainerState
* *
* @typedef {{ * @typedef {{
* filepath: Path, * filepath: Path,
* device: DeviceName, * device: Device,
* process?: ChildProcess, * process?: ChildProcess,
* state: FileContainerState * state: FileContainerState
* }} FileContainer * }} FileContainer
* *
* @typedef {string} Path * @typedef {string} Path
* @typedef {string} DeviceName * @typedef {string} DeviceName
*
* @typedef {{
* name: DeviceName
* path: string,
* physical: string,
* }} Device
**/ **/
class Orchestrator { class Orchestrator {
@ -230,7 +339,7 @@ class Orchestrator {
*/ */
#fileMap #fileMap
/** /**
* @type Set<DeviceName> * @type Set<Device>
*/ */
#devices #devices
@ -244,11 +353,15 @@ class Orchestrator {
/** @type {Logger} */ /** @type {Logger} */
#logger #logger
/** @type {HyprlandSupport} */
#support;
/** @type {'waiting'|'running'|'stopping'|'stopped'|'failed'} */ /** @type {'waiting'|'running'|'stopping'|'stopped'|'failed'} */
#state #state
constructor( constructor(
path, path,
logger logger,
support
) { ) {
this.#path = path this.#path = path
this.#fileMap = new Map(); this.#fileMap = new Map();
@ -256,6 +369,7 @@ class Orchestrator {
this.#notifications = new NotificationHandler(); this.#notifications = new NotificationHandler();
this.#logger = logger; this.#logger = logger;
this.#state = 'waiting'; this.#state = 'waiting';
this.#support = support;
} }
async run() { async run() {
@ -272,14 +386,15 @@ class Orchestrator {
this.#state = 'running'; this.#state = 'running';
do { do {
const prepareSteps = [ await this.#updateDevices();
this.#updateFileMap(), await this.#updateFileMap();
this.#updateDevices()
];
await Promise.all(prepareSteps);
this.#actFiles() this.#actFiles()
if (this.#support.isValid) {
this.#support.update(this.#fileMap.values().toArray());
}
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
} while(this.#state === 'running'); } while(this.#state === 'running');
this.#state = 'stopped'; this.#state = 'stopped';
@ -347,18 +462,23 @@ class Orchestrator {
*/ */
#createFileContainer(path) { #createFileContainer(path) {
const grepResult = child_process.execSync(`grep device_name "${path}"`).toString(); const grepResult = child_process.execSync(`grep device_name "${path}"`).toString();
const device = grepResult.split('=')[1].replaceAll('"', '').trim(); const deviceName = grepResult.split('=')[1].replaceAll('"', '').trim();
this.#logger.log( this.#logger.log(
`Create file watcher for ${device} with '${path}'`, `Create file watcher for ${deviceName} with '${path}'`,
"ORCHESTRATOR", "ORCHESTRATOR",
"DEBUG" "DEBUG"
) )
/**
* @type {Device}
*/
const selectedDevice = this.#devices.values().find((device) => device.name === deviceName)
return { return {
filepath: path, filepath: path,
state: "waiting", state: "waiting",
device: device device: selectedDevice
} }
} }
@ -372,11 +492,11 @@ class Orchestrator {
continue; continue;
} }
if (!deviceMap.has(value.device)) { if (!deviceMap.has(value.device.name)) {
deviceMap.set(value.device, []) deviceMap.set(value.device.name, [])
} }
deviceMap.get(value.device).push(value); deviceMap.get(value.device.name).push(value);
} }
const duplicates = deviceMap.values().filter(devices => devices.some(device => device.state !== 'invalid') && devices.length > 1); const duplicates = deviceMap.values().filter(devices => devices.some(device => device.state !== 'invalid') && devices.length > 1);
@ -385,7 +505,7 @@ class Orchestrator {
this.#notifications.sendNotification( this.#notifications.sendNotification(
{ {
summary: "Duplicate remap files for device", summary: "Duplicate remap files for device",
message: `The following device ${container.device} has multiple remap files. This is not possible. Please make sure only one is avaliable. message: `The following device ${container.device.name} has multiple remap files. This is not possible. Please make sure only one is avaliable.
Marking all files as invalid and unloading currently loaded files... Marking all files as invalid and unloading currently loaded files...
Files: Files:
@ -401,7 +521,7 @@ ${duplicate.map((file) => `- ${file.filepath}`).join('\n')}`,
}); });
}) })
this.#logger.log( this.#logger.log(
`Marking device ${container.device} invalid due to multiple files. `Marking device ${container.device.name} invalid due to multiple files.
Files: Files:
${duplicate.map((file) => `- ${file.filepath}`).join('\n')}\` ${duplicate.map((file) => `- ${file.filepath}`).join('\n')}\`
`, `,
@ -431,7 +551,7 @@ ${duplicate.map((file) => `- ${file.filepath}`).join('\n')}\`
* @var {string} * @var {string}
*/ */
const evremapResult = await new Promise((resolve, reject) => { const evremapResult = await new Promise((resolve, reject) => {
child_process.exec('evremap list-devices | grep Name:', (error, stdout, stderr) => { child_process.exec('evremap list-devices', (error, stdout, stderr) => {
if (error) { if (error) {
reject(error); reject(error);
return; return;
@ -441,11 +561,30 @@ ${duplicate.map((file) => `- ${file.filepath}`).join('\n')}\`
}) })
}) })
const lines = evremapResult.split('\n').filter(line => line);
/** /**
* @type {DeviceName[]} * @type {Device[]}
*/ */
const devices = evremapResult.split('\n').map((result) => result.replace('Name: ', '')) const devices = [];
for (let i = 0; i < lines.length; i += 3) {
const nameLine = lines[i];
const pathLine = lines[i + 1];
const physLine = lines[i + 2];
/**
* @type {Device}
*/
const device = {
name: nameLine.split(':')[1]?.trim() ?? '',
path: pathLine.split(':')[1]?.trim() ?? '',
physical: physLine.split(':')[1]?.trim() ?? ''
}
devices.push(device);
}
this.#devices = new Set(devices); this.#devices = new Set(devices);
} }
#actFiles() { #actFiles() {
@ -495,7 +634,7 @@ s
if (data.includes("Going into read loop")) { if (data.includes("Going into read loop")) {
message.replace({ message.replace({
summary: "Device ready", summary: "Device ready",
message: Orchestrator.NOTIFICATION_READY.replace("{device}", file.device), message: Orchestrator.NOTIFICATION_READY.replace("{device}", file.device.name),
expire: 5_000, expire: 5_000,
urgency: 'normal' urgency: 'normal'
}) })
@ -505,7 +644,7 @@ s
if (!dataMessage) { if (!dataMessage) {
this.#logger.log( this.#logger.log(
dataMetadata, dataMetadata,
`EVREMAP[${file.device}]`, `EVREMAP[${file.device.name}]`,
'WARN', 'WARN',
) )
return; return;
@ -515,7 +654,7 @@ s
this.#logger.log( this.#logger.log(
dataMessage.trim(), dataMessage.trim(),
origin.toUpperCase().replace('EVREMAP', `EVREMAP[${file.device}]`), origin.toUpperCase().replace('EVREMAP', `EVREMAP[${file.device.name}]`),
type, type,
new Date(Date.parse(time)) new Date(Date.parse(time))
) )
@ -524,8 +663,8 @@ s
const unexpected = code === null || code === 0; const unexpected = code === null || code === 0;
this.#logger.log( this.#logger.log(
`Device '${file.device}' ${unexpected ? "unexpectedly" : ''} left with code '${code}'.`, `Device '${file.device.name}' ${unexpected ? "unexpectedly" : ''} left with code '${code}'.`,
`EVREMAP[${file.device}]`, `EVREMAP[${file.device.name}]`,
unexpected ? 'WARN' : 'INFO' unexpected ? 'WARN' : 'INFO'
) )
@ -535,7 +674,7 @@ s
message.replace({ message.replace({
summary: "Device left unexpected...", summary: "Device left unexpected...",
message: Orchestrator.NOTIFICATION_CONNECTION_LOST.replace('{device}', file.device) message: Orchestrator.NOTIFICATION_CONNECTION_LOST.replace('{device}', file.device.name)
}) })
this.#fileMap.set(file.filepath, { this.#fileMap.set(file.filepath, {
@ -571,15 +710,15 @@ s
default: default:
case "GENERIC": case "GENERIC":
notificationMessage = Orchestrator.NOTIFICATION_GENERIC_PROCESS_STOP.replace('{device}', file.device) notificationMessage = Orchestrator.NOTIFICATION_GENERIC_PROCESS_STOP.replace('{device}', file.device)
logMessage = `Removing process for device '${file.device}' from file '${file.filepath}', due to unknown reason` logMessage = `Removing process for device '${file.device.name}' from file '${file.filepath}', due to unknown reason`
break; break;
case "REMOVED_FILE": case "REMOVED_FILE":
notificationMessage = Orchestrator.NOTIFICATION_MISSING_FILE_PROCESS_STOP.replace('{device}', file.device) notificationMessage = Orchestrator.NOTIFICATION_MISSING_FILE_PROCESS_STOP.replace('{device}', file.device)
logMessage = `Removing process for device '${file.device}' from file '${file.filepath}', due to a removed file` logMessage = `Removing process for device '${file.device.name}' from file '${file.filepath}', due to a removed file`
break; break;
case "INVALID_FILE": case "INVALID_FILE":
notificationMessage = Orchestrator.NOTIFICATION_INVALID_FILE_PROCESS_STOP.replace('{device}', file.device) notificationMessage = Orchestrator.NOTIFICATION_INVALID_FILE_PROCESS_STOP.replace('{device}', file.device)
logMessage = `Removing process for device '${file.device}' from file '${file.filepath}', due to a invalid file` logMessage = `Removing process for device '${file.device.name}' from file '${file.filepath}', due to a invalid file`
break; break;
} }
@ -627,7 +766,8 @@ function prepareProcess(orchestrator) {
const evremapPath = path.resolve(process.env.HOME, '.config/evremap'); const evremapPath = path.resolve(process.env.HOME, '.config/evremap');
const logger = new Logger("CHATTY"); const logger = new Logger("CHATTY");
const orchestrator = new Orchestrator(evremapPath, logger); const support = new HyprlandSupport();
const orchestrator = new Orchestrator(evremapPath, logger, support);
prepareProcess(orchestrator); prepareProcess(orchestrator);

View file

@ -36,14 +36,4 @@ device {
kb_variant = altgr-intl kb_variant = altgr-intl
} }
device { source = ~/.cache/evremap_hyprland_config.conf
name = evremap-virtual-input-for-/dev/input/event17
kb_layout = us
kb_variant = altgr-intl
}
device {
name = evremap-virtual-input-for-/dev/input/event4
kb_layout = us
kb_variant = altgr-intl
}