feat: implement keyvibe daemon with sound selection and configuration reloading

This commit is contained in:
sankalp committed 2025-10-07 22:55:19 +05:45
1 parent 791093ba8f
commit 02c63fae2c
10 files changed
+817 -1046

No files matched your search

+546
View File
@@ -0,0 +1,546 @@
#define _GNU_SOURCE
#include <json-c/json.h>
#include <libgen.h>
#include <pthread.h>
#include <pulse/error.h>
#include <pulse/simple.h>
#include <sndfile.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#define MAX_LINE_LENGTH 1024
#define MAX_CONCURRENT_SOUNDS 10
typedef struct {
int start_ms;
int duration_ms;
} SoundMapping;
typedef struct {
char press_file[256];
char release_file[256];
char generic_press_files[5][256];
int num_generic_press_files;
char sound_file[256];
SoundMapping key_mappings[256];
struct {
char *press;
char *release;
} multi_key_mappings[256];
int is_multi;
SF_INFO sf_info;
} SoundPack;
typedef struct {
int key_code;
SoundPack *sound_pack;
int thread_id;
int is_pressed;
} PlaybackData;
SoundPack g_sound_pack = {0};
float g_volume = 1.0f;
int g_verbose = 0;
pthread_t sound_threads[MAX_CONCURRENT_SOUNDS];
volatile int thread_active[MAX_CONCURRENT_SOUNDS] = {0};
pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER;
static void get_full_path(char *buffer, size_t buffer_size,
const char *base_dir, const char *filename) {
if (!filename || !base_dir) {
buffer[0] = '\0';
return;
}
if (filename[0] == '/') {
strncpy(buffer, filename, buffer_size - 1);
buffer[buffer_size - 1] = '\0';
} else {
snprintf(buffer, buffer_size, "%s/%s", base_dir, filename);
}
}
int load_sound_config(const char *config_path) {
FILE *file = fopen(config_path, "r");
if (!file) {
fprintf(stderr, "Error: Cannot open config file: %s\n", config_path);
perror("fopen");
return -1;
}
char config_path_copy[MAX_LINE_LENGTH];
strncpy(config_path_copy, config_path, sizeof(config_path_copy) - 1);
config_path_copy[sizeof(config_path_copy) - 1] = '\0';
char *config_dir = dirname(config_path_copy);
fseek(file, 0, SEEK_END);
long size = ftell(file);
rewind(file);
char *json_string = malloc(size + 1);
if (!json_string) {
fclose(file);
fprintf(stderr, "Error: Memory allocation failed\n");
return -1;
}
fread(json_string, 1, size, file);
json_string[size] = '\0';
fclose(file);
json_object *root = json_tokener_parse(json_string);
free(json_string);
if (!root) {
fprintf(stderr, "Error: Invalid JSON in config file\n");
return -1;
}
const char *key_type = "single";
json_object *obj;
if (json_object_object_get_ex(root, "key_define_type", &obj))
key_type = json_object_get_string(obj);
g_sound_pack.is_multi = strcmp(key_type, "multi") == 0;
printf("Config loaded: Using %s mode\n",
g_sound_pack.is_multi ? "multi" : "single");
if (g_sound_pack.is_multi) {
g_sound_pack.num_generic_press_files = 0;
if (json_object_object_get_ex(root, "sound", &obj)) {
const char *pattern = json_object_get_string(obj);
printf("Sound pattern: %s\n", pattern);
if (strstr(pattern, "%d") || strstr(pattern, "{")) {
for (int i = 0; i <= 4; i++) {
char temp_filename[256];
if (strstr(pattern, "{")) {
char temp_pattern[256];
strncpy(temp_pattern, pattern, sizeof(temp_pattern) - 1);
temp_pattern[sizeof(temp_pattern) - 1] = '\0';
char *brace_start = strstr(temp_pattern, "{");
char *brace_end = strstr(temp_pattern, "}");
if (brace_start && brace_end) {
*brace_start = '%';
*(brace_start + 1) = 'd';
memmove(brace_start + 2, brace_end + 1,
strlen(brace_end + 1) + 1);
}
snprintf(temp_filename, sizeof(temp_filename), temp_pattern, i);
} else {
snprintf(temp_filename, sizeof(temp_filename), pattern, i);
}
get_full_path(g_sound_pack.generic_press_files[i],
sizeof(g_sound_pack.generic_press_files[i]), config_dir,
temp_filename);
if (access(g_sound_pack.generic_press_files[i], R_OK) == 0) {
g_sound_pack.num_generic_press_files = i + 1;
} else {
printf("Generic sound file not found: %s\n",
g_sound_pack.generic_press_files[i]);
break;
}
}
} else {
get_full_path(g_sound_pack.generic_press_files[0],
sizeof(g_sound_pack.generic_press_files[0]), config_dir,
pattern);
if (access(g_sound_pack.generic_press_files[0], R_OK) == 0) {
g_sound_pack.num_generic_press_files = 1;
printf("Found single generic sound file: %s\n",
g_sound_pack.generic_press_files[0]);
}
}
printf("Total generic press sound files: %d\n",
g_sound_pack.num_generic_press_files);
}
if (json_object_object_get_ex(root, "soundup", &obj)) {
char temp_release_file[256];
strncpy(temp_release_file, json_object_get_string(obj),
sizeof(temp_release_file) - 1);
temp_release_file[sizeof(temp_release_file) - 1] = '\0';
get_full_path(g_sound_pack.release_file,
sizeof(g_sound_pack.release_file), config_dir,
temp_release_file);
printf("Release sound file: %s\n", g_sound_pack.release_file);
}
if (json_object_object_get_ex(root, "defines", &obj)) {
json_object_object_foreach(obj, key, val) {
int key_code;
int is_release = 0;
char *dash = strstr(key, "-up");
if (dash) {
char key_copy[16];
strncpy(key_copy, key, dash - key);
key_copy[dash - key] = '\0';
key_code = atoi(key_copy);
is_release = 1;
} else {
key_code = atoi(key);
}
if (key_code >= 0 && key_code < 256) {
const char *filename_relative = json_object_get_string(val);
char full_filename[MAX_LINE_LENGTH];
get_full_path(full_filename, sizeof(full_filename), config_dir,
filename_relative);
if (is_release) {
if (g_sound_pack.multi_key_mappings[key_code].release)
free(g_sound_pack.multi_key_mappings[key_code].release);
g_sound_pack.multi_key_mappings[key_code].release =
strdup(full_filename);
} else {
if (g_sound_pack.multi_key_mappings[key_code].press)
free(g_sound_pack.multi_key_mappings[key_code].press);
g_sound_pack.multi_key_mappings[key_code].press =
strdup(full_filename);
}
}
}
}
} else {
if (json_object_object_get_ex(root, "sound", &obj)) {
char temp_sound_file[256];
strncpy(temp_sound_file, json_object_get_string(obj),
sizeof(temp_sound_file) - 1);
temp_sound_file[sizeof(temp_sound_file) - 1] = '\0';
get_full_path(g_sound_pack.sound_file, sizeof(g_sound_pack.sound_file),
config_dir, temp_sound_file);
printf("Single mode sound file: %s\n", g_sound_pack.sound_file);
}
if (json_object_object_get_ex(root, "defines", &obj)) {
json_object_object_foreach(obj, key, val) {
int key_code = atoi(key);
if (key_code >= 0 && key_code < 256 &&
json_object_is_type(val, json_type_array) &&
json_object_array_length(val) >= 2) {
g_sound_pack.key_mappings[key_code].start_ms =
json_object_get_int(json_object_array_get_idx(val, 0));
g_sound_pack.key_mappings[key_code].duration_ms =
json_object_get_int(json_object_array_get_idx(val, 1));
}
}
}
}
json_object_put(root);
return 0;
}
int init_audio() {
if (g_sound_pack.is_multi)
return 0;
if (strlen(g_sound_pack.sound_file) == 0) {
fprintf(stderr, "Error: No sound file specified in config\n");
return -1;
}
if (access(g_sound_pack.sound_file, R_OK) != 0) {
fprintf(stderr, "Sound file not accessible: %s\n", g_sound_pack.sound_file);
perror("access");
return -1;
}
SNDFILE *test_sf =
sf_open(g_sound_pack.sound_file, SFM_READ, &g_sound_pack.sf_info);
if (!test_sf) {
fprintf(stderr, "Could not open sound file: %s\n", g_sound_pack.sound_file);
fprintf(stderr, "libsndfile error: %s\n", sf_strerror(NULL));
return -1;
}
sf_close(test_sf);
printf("Sound file info: %ld frames, %d channels, %d Hz\n",
g_sound_pack.sf_info.frames, g_sound_pack.sf_info.channels,
g_sound_pack.sf_info.samplerate);
return 0;
}
void *play_sound_thread(void *arg) {
PlaybackData *data = (PlaybackData *)arg;
int key_code = data->key_code;
int thread_id = data->thread_id;
int is_pressed = data->is_pressed;
const char *file_to_play = NULL;
if (g_verbose) {
printf("Thread %d: Playing sound for key %d (%s)\n", thread_id, key_code,
is_pressed ? "press" : "release");
}
if (g_sound_pack.is_multi) {
if (is_pressed && g_sound_pack.multi_key_mappings[key_code].press)
file_to_play = g_sound_pack.multi_key_mappings[key_code].press;
else if (!is_pressed && g_sound_pack.multi_key_mappings[key_code].release)
file_to_play = g_sound_pack.multi_key_mappings[key_code].release;
else if (is_pressed && g_sound_pack.num_generic_press_files > 0) {
int idx = rand() % g_sound_pack.num_generic_press_files;
file_to_play = g_sound_pack.generic_press_files[idx];
} else if (!is_pressed && strlen(g_sound_pack.release_file) > 0)
file_to_play = g_sound_pack.release_file;
if (!file_to_play) {
if (g_verbose) {
printf("Thread %d: No sound file found for key %d (%s)\n", thread_id,
key_code, is_pressed ? "press" : "release");
}
goto exit_cleanup;
}
if (g_verbose) {
printf("Thread %d: Using sound file: %s\n", thread_id, file_to_play);
}
SF_INFO sf_info = {0};
SNDFILE *sf = sf_open(file_to_play, SFM_READ, &sf_info);
if (!sf) {
fprintf(stderr, "Could not open sound file: %s (Error: %s)\n",
file_to_play, sf_strerror(NULL));
goto exit_cleanup;
}
pa_sample_spec ss = {.format = PA_SAMPLE_S16LE,
.rate = sf_info.samplerate,
.channels = sf_info.channels};
int pa_error;
pa_simple *pa_handle =
pa_simple_new(NULL, "KeyboardSounds", PA_STREAM_PLAYBACK, NULL,
"playback", &ss, NULL, NULL, &pa_error);
if (!pa_handle) {
fprintf(stderr, "Could not initialize PulseAudio: %s\n",
pa_strerror(pa_error));
sf_close(sf);
goto exit_cleanup;
}
int frames = 2048;
short *buffer = malloc(frames * sf_info.channels * sizeof(short));
if (!buffer) {
pa_simple_free(pa_handle);
sf_close(sf);
goto exit_cleanup;
}
sf_count_t read;
while ((read = sf_readf_short(sf, buffer, frames)) > 0) {
for (sf_count_t i = 0; i < read * sf_info.channels; i++) {
buffer[i] = (short)(buffer[i] * g_volume);
}
int pa_write_error;
if (pa_simple_write(pa_handle, buffer,
read * sf_info.channels * sizeof(short),
&pa_write_error) < 0) {
fprintf(stderr, "PulseAudio write error: %s\n",
pa_strerror(pa_write_error));
break;
}
}
int pa_drain_error;
pa_simple_drain(pa_handle, &pa_drain_error);
pa_simple_free(pa_handle);
sf_close(sf);
free(buffer);
} else {
if (key_code >= 256 ||
g_sound_pack.key_mappings[key_code].duration_ms == 0) {
if (g_verbose) {
printf("Thread %d: No mapping for key %d\n", thread_id, key_code);
}
goto exit_cleanup;
}
SoundMapping *mapping = &g_sound_pack.key_mappings[key_code];
SF_INFO sf_info = g_sound_pack.sf_info;
SNDFILE *sf = sf_open(g_sound_pack.sound_file, SFM_READ, &sf_info);
if (!sf) {
fprintf(stderr, "Thread: Could not open sound file\n");
goto exit_cleanup;
}
pa_sample_spec ss = {.format = PA_SAMPLE_S16LE,
.rate = sf_info.samplerate,
.channels = sf_info.channels};
int pa_error;
pa_simple *pa_handle =
pa_simple_new(NULL, "KeyboardSounds", PA_STREAM_PLAYBACK, NULL,
"playback", &ss, NULL, NULL, &pa_error);
if (!pa_handle) {
sf_close(sf);
fprintf(stderr, "Thread: Could not initialize PulseAudio: %s\n",
pa_strerror(pa_error));
goto exit_cleanup;
}
sf_count_t start_frame = (mapping->start_ms * sf_info.samplerate) / 1000;
sf_count_t duration_frames =
(mapping->duration_ms * sf_info.samplerate) / 1000;
sf_seek(sf, start_frame, SEEK_SET);
short *buffer = malloc(duration_frames * sf_info.channels * sizeof(short));
if (!buffer) {
pa_simple_free(pa_handle);
sf_close(sf);
goto exit_cleanup;
}
sf_count_t frames_read = sf_readf_short(sf, buffer, duration_frames);
for (sf_count_t i = 0; i < frames_read * sf_info.channels; i++) {
buffer[i] = (short)(buffer[i] * g_volume);
}
int pa_write_error, pa_drain_error;
pa_simple_write(pa_handle, buffer,
frames_read * sf_info.channels * sizeof(short),
&pa_write_error);
pa_simple_drain(pa_handle, &pa_drain_error);
free(buffer);
pa_simple_free(pa_handle);
sf_close(sf);
}
exit_cleanup:
pthread_mutex_lock(&thread_mutex);
thread_active[thread_id] = 0;
pthread_mutex_unlock(&thread_mutex);
free(data);
return NULL;
}
int find_available_thread_slot() {
pthread_mutex_lock(&thread_mutex);
for (int i = 0; i < MAX_CONCURRENT_SOUNDS; i++) {
if (!thread_active[i]) {
pthread_mutex_unlock(&thread_mutex);
return i;
}
}
pthread_mutex_unlock(&thread_mutex);
return -1;
}
void play_sound_segment(int key_code, int is_pressed) {
if (!g_sound_pack.is_multi && !is_pressed) {
if (g_verbose) {
printf("Single mode: Ignoring key release for key %d\n", key_code);
}
return;
}
int slot = find_available_thread_slot();
if (slot == -1) {
if (g_verbose) {
printf("Warning: No available thread slots\n");
}
return;
}
PlaybackData *data = malloc(sizeof(PlaybackData));
if (!data)
return;
data->key_code = key_code;
data->sound_pack = &g_sound_pack;
data->thread_id = slot;
data->is_pressed = is_pressed;
pthread_mutex_lock(&thread_mutex);
thread_active[slot] = 1;
if (pthread_create(&sound_threads[slot], NULL, play_sound_thread, data) !=
0) {
thread_active[slot] = 0;
free(data);
fprintf(stderr, "Failed to create sound thread\n");
} else {
pthread_detach(sound_threads[slot]);
}
pthread_mutex_unlock(&thread_mutex);
}
int parse_keyboard_event(const char *json_line, int *key_code,
int *is_pressed) {
char *line_copy = strdup(json_line);
if (!line_copy)
return -1;
char *newline = strchr(line_copy, '\n');
if (newline)
*newline = '\0';
if (g_verbose) {
printf("Parsing JSON: %s\n", line_copy);
}
json_object *root = json_tokener_parse(line_copy);
free(line_copy);
if (!root) {
fprintf(stderr, "Failed to parse JSON: %s\n", json_line);
return -1;
}
json_object *key_code_obj, *state_code_obj;
if (json_object_object_get_ex(root, "key_code", &key_code_obj) &&
json_object_object_get_ex(root, "state_code", &state_code_obj)) {
*key_code = json_object_get_int(key_code_obj);
*is_pressed = json_object_get_int(state_code_obj);
if (g_verbose) {
printf("Parsed key event: key_code=%d, is_pressed=%d\n", *key_code,
*is_pressed);
}
json_object_put(root);
return 0;
}
json_object_put(root);
return -1;
}
void cleanup() {
printf("Cleaning up...\n");
usleep(500000);
for (int i = 0; i < 256; i++) {
if (g_sound_pack.multi_key_mappings[i].press) {
free(g_sound_pack.multi_key_mappings[i].press);
g_sound_pack.multi_key_mappings[i].press = NULL;
}
if (g_sound_pack.multi_key_mappings[i].release) {
free(g_sound_pack.multi_key_mappings[i].release);
g_sound_pack.multi_key_mappings[i].release = NULL;
}
}
pthread_mutex_lock(&thread_mutex);
for (int i = 0; i < MAX_CONCURRENT_SOUNDS; i++) {
thread_active[i] = 0;
}
pthread_mutex_unlock(&thread_mutex);
}
int main(int argc, char *argv[]) {
if (argc < 2 || argc > 4) {
fprintf(stderr, "Usage: %s <config.json> [volume] [verbose]\n", argv[0]);
fprintf(stderr, " volume: 0-100 (default: 50)\n");
fprintf(stderr, " verbose: 1 to enable verbose output (default: 0)\n");
return 1;
}
if (argc >= 3) {
int volume_percent = atoi(argv[2]);
if (volume_percent < 0)
volume_percent = 0;
if (volume_percent > 100)
volume_percent = 100;
g_volume = volume_percent / 100.0f;
printf("Volume set to: %d%%\n", volume_percent);
} else {
printf("Volume set to: 50%% (default)\n");
}
if (argc >= 4) {
g_verbose = atoi(argv[3]);
if (g_verbose) {
printf("Verbose mode enabled\n");
}
}
if (load_sound_config(argv[1]) != 0) {
fprintf(stderr, "Failed to load sound configuration\n");
return 1;
}
if (init_audio() != 0) {
fprintf(stderr, "Failed to initialize audio\n");
return 1;
}
fd_set readfds;
struct timeval timeout;
char line[MAX_LINE_LENGTH];
while (1) {
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int ready = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
if (ready == -1) {
perror("select");
break;
} else if (ready == 0) {
if (g_verbose) {
printf("Waiting for input...\n");
}
continue;
}
if (FD_ISSET(STDIN_FILENO, &readfds)) {
if (fgets(line, sizeof(line), stdin) == NULL) {
if (feof(stdin)) {
printf("EOF reached on stdin\n");
} else {
perror("fgets");
}
break;
}
int key_code, is_pressed;
if (parse_keyboard_event(line, &key_code, &is_pressed) == 0) {
play_sound_segment(key_code, is_pressed);
}
}
}
cleanup();
return 0;
}
+202
View File
@@ -0,0 +1,202 @@
// Renamed from get_key_presses.c to keyvibe_input.c (inlined)
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <getopt.h>
#include <poll.h>
#include <pthread.h>
#include <unistd.h>
#include <libevdev/libevdev.h>
#include <libinput.h>
#include <libudev.h>
#include "config.h"
#define MAX_BUFFER_LENGTH 512
enum error_code {
NO_ERROR,
UDEV_FAILED,
LIBINPUT_FAILED,
SEAT_FAILED,
PERMISSION_FAILED
};
struct input_handler_data {
struct udev *udev;
struct libinput *libinput;
};
static void *handle_input(void *user_data) {
struct input_handler_data *input_handler_data = user_data;
char line[MAX_BUFFER_LENGTH];
while (fgets(line, MAX_BUFFER_LENGTH, stdin) != NULL) {
if (strcmp(line, "stop\n") == 0) {
libinput_unref(input_handler_data->libinput);
udev_unref(input_handler_data->udev);
exit(EXIT_SUCCESS);
}
}
return NULL;
}
static int open_restricted(const char *path, int flags, void *user_data) {
(void)user_data;
int fd = open(path, flags);
if (fd < 0)
fprintf(stderr, "Failed to open %s because of %s.\n", path,
strerror(errno));
return fd < 0 ? -errno : fd;
}
static void close_restricted(int fd, void *user_data) {
(void)user_data;
close(fd);
}
static const struct libinput_interface interface = {
.open_restricted = open_restricted, .close_restricted = close_restricted};
static int print_key_event(struct libinput_event *event) {
struct libinput_event_keyboard *keyboard =
libinput_event_get_keyboard_event(event);
enum libinput_event_type event_type = libinput_event_get_type(event);
uint32_t time_stamp = libinput_event_keyboard_get_time(keyboard);
uint32_t key_code = libinput_event_keyboard_get_key(keyboard);
const char *key_name = libevdev_event_code_get_name(EV_KEY, key_code);
key_name = key_name ? key_name : "null";
enum libinput_key_state state_code =
libinput_event_keyboard_get_key_state(keyboard);
const char *state_name =
state_code == LIBINPUT_KEY_STATE_PRESSED ? "PRESSED" : "RELEASED";
return printf("{\"event_name\": \"KEYBOARD_KEY\", \"event_type\": %d, "
"\"time_stamp\": %d, \"key_name\": \"%s\", \"key_code\": %d, "
"\"state_name\": \"%s\", \"state_code\": %d}\n",
event_type, time_stamp, key_name, key_code, state_name,
state_code);
}
static int print_button_event(struct libinput_event *event) {
struct libinput_event_pointer *pointer =
libinput_event_get_pointer_event(event);
enum libinput_event_type event_type = libinput_event_get_type(event);
uint32_t time_stamp = libinput_event_pointer_get_time(pointer);
uint32_t button_code = libinput_event_pointer_get_button(pointer);
const char *button_name = libevdev_event_code_get_name(EV_KEY, button_code);
enum libinput_button_state state_code =
libinput_event_pointer_get_button_state(pointer);
const char *state_name =
state_code == LIBINPUT_BUTTON_STATE_PRESSED ? "PRESSED" : "RELEASED";
return printf("{\"event_name\": \"POINTER_BUTTON\", \"event_type\": %d, "
"\"time_stamp\": %d, \"key_name\": \"%s\", \"key_code\": %d, "
"\"state_name\": \"%s\", \"state_code\": %d}\n",
event_type, time_stamp, button_name, button_code, state_name,
state_code);
}
static int handle_events(struct libinput *libinput) {
int result = -1;
struct libinput_event *event;
if (libinput_dispatch(libinput) < 0)
return result;
while ((event = libinput_get_event(libinput)) != NULL) {
switch (libinput_event_get_type(event)) {
case LIBINPUT_EVENT_KEYBOARD_KEY:
print_key_event(event);
break;
case LIBINPUT_EVENT_POINTER_BUTTON:
print_button_event(event);
break;
default:
break;
}
fflush(stdout);
libinput_event_destroy(event);
result = 0;
}
return result;
}
static int run_mainloop(struct libinput *libinput) {
struct pollfd fd;
fd.fd = libinput_get_fd(libinput);
fd.events = POLLIN;
fd.revents = 0;
if (handle_events(libinput) != 0) {
fprintf(stderr, "Expected device added events on startup but got none. "
"Maybe you don't have the right permissions?\n");
return -1;
}
while (poll(&fd, 1, -1) > -1)
handle_events(libinput);
return 0;
}
void print_help(char *program_name) {
printf("The backend of Show Me The Key.\n");
printf("Version " PROJECT_VERSION ".\n");
printf("Usage: %s [OPTION…]\n", program_name);
printf("Options:\n");
printf("\t-h, --help\tDisplay help then exit.\n");
printf("\t-v, --version\tDisplay version then exit.\n");
printf("Warning: This is the backend and is not designed to run by users. "
"You should run the frontend of Show Me The Key, and the frontend "
"will run this.\n");
}
int main(int argc, char *argv[]) {
const struct option long_options[] = {{"version", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{NULL, 0, NULL, 0}};
int option_index = 0;
int opt = 0;
while ((opt = getopt_long(argc, argv, "vh", long_options, &option_index)) !=
-1) {
switch (opt) {
case 0:
break;
case 'v':
printf(PROJECT_VERSION "\n");
return 0;
case 'h':
print_help(argv[0]);
return 0;
case '?':
break;
default:
fprintf(stderr, "%s: Invalid option `-%c`.\n", argv[0], opt);
break;
}
}
struct udev *udev = udev_new();
if (udev == NULL) {
fprintf(stderr, "Failed to initialize udev.\n");
return UDEV_FAILED;
}
struct libinput *libinput =
libinput_udev_create_context(&interface, NULL, udev);
if (!libinput) {
fprintf(stderr, "Failed to initialize libinput from udev.\n");
return LIBINPUT_FAILED;
}
if (libinput_udev_assign_seat(libinput, "seat0") != 0) {
fprintf(stderr, "Failed to set seat.\n");
libinput_unref(libinput);
udev_unref(udev);
return SEAT_FAILED;
}
pthread_t input_handler;
struct input_handler_data input_handler_data = {udev, libinput};
pthread_create(&input_handler, NULL, handle_input, &input_handler_data);
if (run_mainloop(libinput) < 0)
return PERMISSION_FAILED;
libinput_unref(libinput);
udev_unref(udev);
return NO_ERROR;
}
+688
View File
@@ -0,0 +1,688 @@
#define _POSIX_C_SOURCE 200809L
#include "config.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <json-c/json.h>
#include <pthread.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define MAX_PATH_LENGTH 512
#define AUDIO_BASE_DIR KeyVibe_DATA_DIR "/audio"
#define USER_CONFIG_FILENAME ".keyvibe.json"
// Global variables for cleanup
pid_t keyboard_pid = 0;
pid_t sound_pid = 0;
static char pidfile_path[MAX_PATH_LENGTH] = {0};
static int is_daemon = 0;
static volatile sig_atomic_t reload_requested = 0;
static int current_volume = 50;
static char current_sound_name[MAX_PATH_LENGTH] = {0};
static int current_verbose = 0;
static char current_config_path[MAX_PATH_LENGTH] = {0};
static char current_sound_dir[MAX_PATH_LENGTH] = {0};
void print_usage(const char *program_name) {
printf("KeyVibe - Mechanical Keyboard Sound Simulator\n\n");
printf("Usage: %s [OPTIONS]\n\n", program_name);
printf("Options:\n");
printf(" -s, --sound SOUND_NAME Select sound pack (default: eg-oreo)\n");
printf(" -V, --volume VOLUME Set volume [0-100] (default: 50)\n");
printf(" -c, --override-config Apply -s/-V to override loaded config\n");
printf(" -l, --list List available sound packs\n");
printf(" --daemon Run in background (write PID file)\n");
printf(" --stop Stop background daemon\n");
printf(" --reload Send reload signal to running daemon\n");
printf(" -h, --help Show this help message\n");
printf(" -v, --verbose Enable verbose output\n");
printf("\nConfiguration:\n");
printf(" Reads ~/.keyvibe.json if present. On first run, prompts to create "
"it.\n");
printf(" In daemon mode, editing ~/.keyvibe.json will auto-reload.\n");
printf("\nExamples:\n");
printf(" %s # Use default sound (eg-oreo)\n",
program_name);
printf(" %s -s cherrymx-blue-abs # Use Cherry MX Blue ABS sound\n",
program_name);
printf(" %s -l # List all available sounds\n",
program_name);
printf("\nPress Ctrl+C to exit.\n");
}
int list_sound_packs() {
DIR *dir;
struct dirent *entry;
char path[MAX_PATH_LENGTH];
struct stat st;
dir = opendir(AUDIO_BASE_DIR);
if (dir == NULL) {
fprintf(stderr, "Error: Cannot open audio directory '%s'\n",
AUDIO_BASE_DIR);
fprintf(stderr, "Make sure you're running from the correct directory.\n");
return 1;
}
printf("Available sound packs:\n");
printf("======================\n");
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
snprintf(path, sizeof(path), "%s/%s", AUDIO_BASE_DIR, entry->d_name);
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
printf(" %s\n", entry->d_name);
}
}
closedir(dir);
return 0;
}
int validate_sound_pack(const char *sound_name) {
char config_path[MAX_PATH_LENGTH];
char sound_path[MAX_PATH_LENGTH];
snprintf(config_path, sizeof(config_path), "%s/%s/config.json",
AUDIO_BASE_DIR, sound_name);
snprintf(sound_path, sizeof(sound_path), "%s/%s", AUDIO_BASE_DIR, sound_name);
if (access(sound_path, F_OK) != 0) {
fprintf(stderr, "Error: Sound pack '%s' not found in %s/\n", sound_name,
AUDIO_BASE_DIR);
fprintf(stderr, "Use --list to see available sound packs.\n");
return 0;
}
if (access(config_path, R_OK) != 0) {
fprintf(stderr, "Error: Config file not found: %s\n", config_path);
return 0;
}
return 1;
}
void cleanup_processes(int sig) {
(void)sig;
if (!is_daemon) {
printf("\nShutting down KeyVibe...\n");
}
if (sound_pid > 0) {
kill(sound_pid, SIGTERM);
waitpid(sound_pid, NULL, 0);
}
if (keyboard_pid > 0) {
kill(keyboard_pid, SIGTERM);
waitpid(keyboard_pid, NULL, 0);
}
if (pidfile_path[0] != '\0') {
unlink(pidfile_path);
}
exit(0);
}
int check_sudo_cached() {
int status = system("sudo -n true 2>/dev/null");
return WEXITSTATUS(status) == 0;
}
int ensure_sudo_access() {
if (check_sudo_cached()) {
return 1;
}
printf("This program requires sudo access to monitor keyboard events.\n");
printf("Please enter your password when prompted:\n");
int status = system("sudo -v");
if (WEXITSTATUS(status) != 0) {
fprintf(stderr, "Error: Failed to obtain sudo access\n");
return 0;
}
return 1;
}
static char *get_user_config_path(char *buffer, size_t buflen) {
const char *home = getenv("HOME");
if (!home || strlen(home) == 0) {
struct passwd *pw = getpwuid(getuid());
if (pw && pw->pw_dir)
home = pw->pw_dir;
}
if (!home)
return NULL;
snprintf(buffer, buflen, "%s/%s", home, USER_CONFIG_FILENAME);
return buffer;
}
static const char *get_runtime_dir() {
const char *rd = getenv("XDG_RUNTIME_DIR");
if (rd && strlen(rd) > 0)
return rd;
return "/tmp";
}
static int build_paths_for_sound(const char *sound_name, char *out_config_path,
size_t out_config_sz, char *out_sound_dir,
size_t out_sound_sz) {
size_t base_len = strlen(AUDIO_BASE_DIR);
size_t name_len = strlen(sound_name);
const char *cfg_suffix = "/config.json";
size_t cfg_suffix_len = strlen(cfg_suffix);
if (base_len + 1 + name_len + cfg_suffix_len + 1 > out_config_sz) {
fprintf(stderr, "Error: sound pack name too long for path buffer.\n");
return 0;
}
if (base_len + 1 + name_len + 1 > out_sound_sz) {
fprintf(stderr, "Error: sound pack name too long for directory buffer.\n");
return 0;
}
snprintf(out_config_path, out_config_sz, "%s/%s%s", AUDIO_BASE_DIR,
sound_name, cfg_suffix);
snprintf(out_sound_dir, out_sound_sz, "%s/%s", AUDIO_BASE_DIR, sound_name);
return 1;
}
static void build_pidfile_path(char *buffer, size_t buflen) {
const char *rd = get_runtime_dir();
snprintf(buffer, buflen, "%s/keyvibe-%d.pid", rd, (int)getuid());
}
static int read_pidfile(const char *path, pid_t *out_pid) {
FILE *f = fopen(path, "r");
if (!f)
return 0;
long val = -1;
if (fscanf(f, "%ld", &val) == 1 && val > 0) {
*out_pid = (pid_t)val;
fclose(f);
return 1;
}
fclose(f);
return 0;
}
static int write_pidfile(const char *path, pid_t pid) {
FILE *f = fopen(path, "w");
if (!f)
return 0;
fprintf(f, "%ld\n", (long)pid);
fclose(f);
return 1;
}
static int process_is_running(pid_t pid) {
if (pid <= 0)
return 0;
if (kill(pid, 0) == 0)
return 1;
return 0;
}
static void daemonize_self() {
pid_t pid = fork();
if (pid < 0)
exit(1);
if (pid > 0)
exit(0);
if (setsid() < 0)
exit(1);
pid = fork();
if (pid < 0)
exit(1);
if (pid > 0)
exit(0);
umask(0);
chdir("/");
int fd = open("/dev/null", O_RDWR);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2)
close(fd);
}
}
static void stop_children() {
if (sound_pid > 0) {
kill(sound_pid, SIGTERM);
waitpid(sound_pid, NULL, 0);
sound_pid = 0;
}
if (keyboard_pid > 0) {
kill(keyboard_pid, SIGTERM);
waitpid(keyboard_pid, NULL, 0);
keyboard_pid = 0;
}
}
static int start_children(const char *sound_dir, const char *config_path,
int volume, int verbose) {
(void)config_path;
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
return 0;
}
sound_pid = fork();
if (sound_pid == -1) {
perror("fork");
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
char sound_player_path[MAX_PATH_LENGTH];
snprintf(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio",
KeyVibe_BIN_DIR);
if (sound_pid == 0) {
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
if (chdir(sound_dir) != 0) {
perror("chdir");
exit(1);
}
char volume_str[32];
snprintf(volume_str, sizeof(volume_str), "%d", volume);
char verbose_str[8];
snprintf(verbose_str, sizeof(verbose_str), "%d", verbose);
execl(sound_player_path, "keyvibe-audio", "config.json", volume_str,
verbose_str, (char *)NULL);
perror("execl keyvibe-audio");
exit(1);
}
keyboard_pid = fork();
if (keyboard_pid == -1) {
perror("fork");
kill(sound_pid, SIGTERM);
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
char get_key_presses_path[MAX_PATH_LENGTH];
snprintf(get_key_presses_path, sizeof(get_key_presses_path),
"%s/keyvibe-input", KeyVibe_BIN_DIR);
if (keyboard_pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execl(get_key_presses_path, "keyvibe-input", (char *)NULL);
perror("execl keyvibe-input");
exit(1);
}
close(pipefd[0]);
close(pipefd[1]);
return 1;
}
static void handle_sighup(int sig) {
(void)sig;
reload_requested = 1;
}
struct inotify_thread_args {
char path[MAX_PATH_LENGTH];
};
static void *inotify_thread_fn(void *arg) {
struct inotify_thread_args *a = (struct inotify_thread_args *)arg;
int fd = inotify_init1(IN_NONBLOCK);
if (fd < 0)
return NULL;
int wd =
inotify_add_watch(fd, a->path, IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB);
if (wd < 0) {
close(fd);
return NULL;
}
char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
while (1) {
ssize_t len = read(fd, buf, sizeof(buf));
if (len <= 0) {
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = 200000000L;
nanosleep(&ts, NULL);
continue;
}
reload_requested = 1;
}
inotify_rm_watch(fd, wd);
close(fd);
return NULL;
}
static int write_user_config(const char *path, const char *sound, int volume) {
json_object *root = json_object_new_object();
if (!root)
return 0;
json_object_object_add(root, "sound", json_object_new_string(sound));
json_object_object_add(root, "volume", json_object_new_int(volume));
const char *json_str =
json_object_to_json_string_ext(root, JSON_C_TO_STRING_PRETTY);
FILE *f = fopen(path, "w");
if (!f) {
json_object_put(root);
return 0;
}
fprintf(f, "%s\n", json_str);
fclose(f);
json_object_put(root);
return 1;
}
static int read_user_config(const char *path, char **out_sound,
int *out_volume) {
FILE *f = fopen(path, "r");
if (!f)
return 0;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
char *buf = malloc(sz + 1);
if (!buf) {
fclose(f);
return 0;
}
fread(buf, 1, sz, f);
buf[sz] = '\0';
fclose(f);
json_object *root = json_tokener_parse(buf);
free(buf);
if (!root)
return 0;
json_object *o;
if (json_object_object_get_ex(root, "sound", &o)) {
const char *s = json_object_get_string(o);
if (s)
*out_sound = strdup(s);
}
if (json_object_object_get_ex(root, "volume", &o)) {
*out_volume = json_object_get_int(o);
}
json_object_put(root);
return 1;
}
int main(int argc, char *argv[]) {
char *sound_name = "eg-oreo";
int verbose = 0;
int list_sounds = 0;
int flag_daemon = 0;
int flag_stop = 0;
static struct option long_options[] = {
{"sound", required_argument, 0, 's'},
{"volume", required_argument, 0, 'V'},
{"override-config", no_argument, 0, 'c'},
{"list", no_argument, 0, 'l'},
{"daemon", no_argument, 0, 1000},
{"stop", no_argument, 0, 1001},
{"reload", no_argument, 0, 1002},
{"help", no_argument, 0, 'h'},
{"verbose", no_argument, 0, 'v'},
{0, 0, 0, 0}};
int volume = 50;
int override_config = 0;
char *cli_sound = NULL;
int cli_volume = -1;
char user_cfg_path[MAX_PATH_LENGTH];
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
if (access(user_cfg_path, R_OK) == 0) {
char *cfg_sound = NULL;
int cfg_volume = volume;
if (read_user_config(user_cfg_path, &cfg_sound, &cfg_volume)) {
if (cfg_sound) {
sound_name = cfg_sound;
}
volume = cfg_volume;
}
}
}
int opt;
while ((opt = getopt_long(argc, argv, "s:V:clhv", long_options, NULL)) !=
-1) {
switch (opt) {
case 's':
cli_sound = optarg;
break;
case 'V':
cli_volume = atoi(optarg);
if (cli_volume < 0)
cli_volume = 0;
if (cli_volume > 100)
cli_volume = 100;
break;
case 'c':
override_config = 1;
break;
case 'l':
list_sounds = 1;
break;
case 1000:
flag_daemon = 1;
break;
case 1001:
flag_stop = 1;
break;
case 1002: {
build_pidfile_path(pidfile_path, sizeof(pidfile_path));
pid_t running_pid = 0;
if (!read_pidfile(pidfile_path, &running_pid) ||
!process_is_running(running_pid)) {
fprintf(stderr, "KeyVibe: not running.\n");
return 1;
}
if (kill(running_pid, SIGHUP) != 0) {
perror("kill");
return 1;
}
printf("KeyVibe reload signal sent.\n");
return 0;
}
case 'h':
print_usage(argv[0]);
return 0;
case 'v':
verbose = 1;
break;
default:
print_usage(argv[0]);
return 1;
}
}
if (list_sounds) {
return list_sound_packs();
}
build_pidfile_path(pidfile_path, sizeof(pidfile_path));
if (flag_stop) {
pid_t running_pid = 0;
if (!read_pidfile(pidfile_path, &running_pid)) {
fprintf(stderr, "KeyVibe: no pidfile found at %s\n", pidfile_path);
return 1;
}
if (!process_is_running(running_pid)) {
fprintf(stderr,
"KeyVibe: process %ld not running, removing stale pidfile\n",
(long)running_pid);
unlink(pidfile_path);
return 1;
}
if (kill(running_pid, SIGTERM) != 0) {
perror("kill");
return 1;
}
for (int i = 0; i < 30; i++) {
if (!process_is_running(running_pid)) {
unlink(pidfile_path);
printf("KeyVibe stopped.\n");
return 0;
}
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = 100000000L;
nanosleep(&ts, NULL);
}
fprintf(stderr, "KeyVibe: process did not stop in time\n");
return 1;
}
if (override_config) {
if (cli_sound != NULL)
sound_name = cli_sound;
if (cli_volume >= 0)
volume = cli_volume;
}
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
if (access(user_cfg_path, F_OK) != 0) {
if (!write_user_config(user_cfg_path, sound_name, volume)) {
fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path);
} else if (verbose) {
fprintf(stderr, "Created default config %s\n", user_cfg_path);
}
}
}
if (!validate_sound_pack(sound_name)) {
return 1;
}
char get_key_presses_path[MAX_PATH_LENGTH];
char sound_player_path[MAX_PATH_LENGTH];
snprintf(get_key_presses_path, sizeof(get_key_presses_path),
"%s/keyvibe-input", KeyVibe_BIN_DIR);
snprintf(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio",
KeyVibe_BIN_DIR);
if (access(get_key_presses_path, X_OK) != 0) {
fprintf(stderr, "Error: Cannot find or execute %s\n", get_key_presses_path);
return 1;
}
if (access(sound_player_path, X_OK) != 0) {
fprintf(stderr, "Error: Cannot find or execute %s\n", sound_player_path);
return 1;
}
signal(SIGINT, cleanup_processes);
signal(SIGTERM, cleanup_processes);
if (flag_daemon) {
pid_t existing = 0;
if (read_pidfile(pidfile_path, &existing) && process_is_running(existing)) {
fprintf(stderr,
"KeyVibe already running (pid %ld). Use --stop to stop it.\n",
(long)existing);
return 1;
}
daemonize_self();
is_daemon = 1;
write_pidfile(pidfile_path, getpid());
}
char config_path[MAX_PATH_LENGTH];
char sound_dir[MAX_PATH_LENGTH];
if (!build_paths_for_sound(sound_name, config_path, sizeof(config_path),
sound_dir, sizeof(sound_dir))) {
return 1;
}
current_volume = volume;
current_verbose = verbose;
strncpy(current_sound_name, sound_name, sizeof(current_sound_name) - 1);
current_sound_name[sizeof(current_sound_name) - 1] = '\0';
strncpy(current_config_path, config_path, sizeof(current_config_path) - 1);
current_config_path[sizeof(current_config_path) - 1] = '\0';
strncpy(current_sound_dir, sound_dir, sizeof(current_sound_dir) - 1);
current_sound_dir[sizeof(current_sound_dir) - 1] = '\0';
if (verbose && !is_daemon) {
printf("KeyVibe starting...\n");
printf("Sound pack: %s\n", sound_name);
printf("Config file: %s\n", config_path);
printf("Working directory: %s\n", sound_dir);
printf("Press Ctrl+C to exit.\n\n");
} else {
if (!is_daemon) {
printf("KeyVibe started with sound pack: %s\n", sound_name);
printf("Press Ctrl+C to exit.\n");
}
}
if (!start_children(sound_dir, config_path, volume, verbose)) {
return 1;
}
int status;
pid_t finished_pid;
signal(SIGHUP, handle_sighup);
pthread_t inotify_thread;
struct inotify_thread_args in_args;
if (is_daemon) {
char user_cfg_path2[MAX_PATH_LENGTH];
if (get_user_config_path(user_cfg_path2, sizeof(user_cfg_path2))) {
strncpy(in_args.path, user_cfg_path2, sizeof(in_args.path) - 1);
in_args.path[sizeof(in_args.path) - 1] = '\0';
pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args);
pthread_detach(inotify_thread);
}
}
while (1) {
if (reload_requested) {
reload_requested = 0;
char user_cfg_path3[MAX_PATH_LENGTH];
if (get_user_config_path(user_cfg_path3, sizeof(user_cfg_path3))) {
char *cfg_sound = NULL;
int cfg_volume = current_volume;
if (read_user_config(user_cfg_path3, &cfg_sound, &cfg_volume)) {
if (cfg_sound) {
strncpy(current_sound_name, cfg_sound,
sizeof(current_sound_name) - 1);
free(cfg_sound);
}
current_volume = cfg_volume;
}
}
if (validate_sound_pack(current_sound_name)) {
if (!build_paths_for_sound(current_sound_name, current_config_path,
sizeof(current_config_path),
current_sound_dir,
sizeof(current_sound_dir))) {
continue;
}
stop_children();
start_children(current_sound_dir, current_config_path, current_volume,
current_verbose);
}
continue;
}
finished_pid = waitpid(-1, &status, WNOHANG);
if (finished_pid == 0) {
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = 100000000L;
nanosleep(&ts, NULL);
continue;
} else if (finished_pid > 0) {
if (finished_pid == keyboard_pid) {
if (!is_daemon)
printf("Keyboard listener exited with status %d\n",
WEXITSTATUS(status));
keyboard_pid = 0;
if (sound_pid > 0)
kill(sound_pid, SIGTERM);
} else if (finished_pid == sound_pid) {
if (!is_daemon)
printf("Sound player exited with status %d\n", WEXITSTATUS(status));
sound_pid = 0;
if (keyboard_pid > 0)
kill(keyboard_pid, SIGTERM);
}
break;
} else {
if (errno == ECHILD)
break;
}
}
if (!is_daemon) {
printf("KeyVibe exited.\n");
}
return 0;
}