Adds hyprland support for evremap orchestrator
This commit is contained in:
parent
0f36495ddd
commit
17c1aae84b
2 changed files with 171 additions and 41 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -243,12 +352,16 @@ class Orchestrator {
|
||||||
#notifications
|
#notifications
|
||||||
/** @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,21 +551,40 @@ ${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;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(stdout);
|
resolve(stdout);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue