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 {{
* filepath: Path,
* device: DeviceName,
* device: Device,
* process?: ChildProcess,
* state: FileContainerState
* }} FileContainer
*
* @typedef {string} Path
* @typedef {string} DeviceName
*
* @typedef {{
* name: DeviceName
* path: string,
* physical: string,
* }} Device
**/
class Orchestrator {
@ -230,7 +339,7 @@ class Orchestrator {
*/
#fileMap
/**
* @type Set<DeviceName>
* @type Set<Device>
*/
#devices
@ -244,11 +353,15 @@ class Orchestrator {
/** @type {Logger} */
#logger
/** @type {HyprlandSupport} */
#support;
/** @type {'waiting'|'running'|'stopping'|'stopped'|'failed'} */
#state
constructor(
path,
logger
logger,
support
) {
this.#path = path
this.#fileMap = new Map();
@ -256,6 +369,7 @@ class Orchestrator {
this.#notifications = new NotificationHandler();
this.#logger = logger;
this.#state = 'waiting';
this.#support = support;
}
async run() {
@ -272,14 +386,15 @@ class Orchestrator {
this.#state = 'running';
do {
const prepareSteps = [
this.#updateFileMap(),
this.#updateDevices()
];
await Promise.all(prepareSteps);
await this.#updateDevices();
await this.#updateFileMap();
this.#actFiles()
if (this.#support.isValid) {
this.#support.update(this.#fileMap.values().toArray());
}
await new Promise(resolve => setTimeout(resolve, 1000));
} while(this.#state === 'running');
this.#state = 'stopped';
@ -347,18 +462,23 @@ class Orchestrator {
*/
#createFileContainer(path) {
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(
`Create file watcher for ${device} with '${path}'`,
`Create file watcher for ${deviceName} with '${path}'`,
"ORCHESTRATOR",
"DEBUG"
)
/**
* @type {Device}
*/
const selectedDevice = this.#devices.values().find((device) => device.name === deviceName)
return {
filepath: path,
state: "waiting",
device: device
device: selectedDevice
}
}
@ -372,11 +492,11 @@ class Orchestrator {
continue;
}
if (!deviceMap.has(value.device)) {
deviceMap.set(value.device, [])
if (!deviceMap.has(value.device.name)) {
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);
@ -385,7 +505,7 @@ class Orchestrator {
this.#notifications.sendNotification(
{
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...
Files:
@ -401,7 +521,7 @@ ${duplicate.map((file) => `- ${file.filepath}`).join('\n')}`,
});
})
this.#logger.log(
`Marking device ${container.device} invalid due to multiple files.
`Marking device ${container.device.name} invalid due to multiple files.
Files:
${duplicate.map((file) => `- ${file.filepath}`).join('\n')}\`
`,
@ -431,7 +551,7 @@ ${duplicate.map((file) => `- ${file.filepath}`).join('\n')}\`
* @var {string}
*/
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) {
reject(error);
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);
}
#actFiles() {
@ -495,7 +634,7 @@ s
if (data.includes("Going into read loop")) {
message.replace({
summary: "Device ready",
message: Orchestrator.NOTIFICATION_READY.replace("{device}", file.device),
message: Orchestrator.NOTIFICATION_READY.replace("{device}", file.device.name),
expire: 5_000,
urgency: 'normal'
})
@ -505,7 +644,7 @@ s
if (!dataMessage) {
this.#logger.log(
dataMetadata,
`EVREMAP[${file.device}]`,
`EVREMAP[${file.device.name}]`,
'WARN',
)
return;
@ -515,7 +654,7 @@ s
this.#logger.log(
dataMessage.trim(),
origin.toUpperCase().replace('EVREMAP', `EVREMAP[${file.device}]`),
origin.toUpperCase().replace('EVREMAP', `EVREMAP[${file.device.name}]`),
type,
new Date(Date.parse(time))
)
@ -524,8 +663,8 @@ s
const unexpected = code === null || code === 0;
this.#logger.log(
`Device '${file.device}' ${unexpected ? "unexpectedly" : ''} left with code '${code}'.`,
`EVREMAP[${file.device}]`,
`Device '${file.device.name}' ${unexpected ? "unexpectedly" : ''} left with code '${code}'.`,
`EVREMAP[${file.device.name}]`,
unexpected ? 'WARN' : 'INFO'
)
@ -535,7 +674,7 @@ s
message.replace({
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, {
@ -571,15 +710,15 @@ s
default:
case "GENERIC":
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;
case "REMOVED_FILE":
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;
case "INVALID_FILE":
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;
}
@ -627,7 +766,8 @@ function prepareProcess(orchestrator) {
const evremapPath = path.resolve(process.env.HOME, '.config/evremap');
const logger = new Logger("CHATTY");
const orchestrator = new Orchestrator(evremapPath, logger);
const support = new HyprlandSupport();
const orchestrator = new Orchestrator(evremapPath, logger, support);
prepareProcess(orchestrator);

View file

@ -36,14 +36,4 @@ device {
kb_variant = altgr-intl
}
device {
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
}
source = ~/.cache/evremap_hyprland_config.conf