Files
vbx/keyvibe.c
T

786 lines
21 KiB
C

#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) {
// Skip . and .. directories
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
// Build full path
snprintf(path, sizeof(path), "%s/%s", AUDIO_BASE_DIR, entry->d_name);
// Use stat to check if it's a directory
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);
// Check if directory exists
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;
}
// Check if config.json exists
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; // Suppress unused parameter warning
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);
}
// Function to check if sudo credentials are cached
int check_sudo_cached() {
int status = system("sudo -n true 2>/dev/null");
return WEXITSTATUS(status) == 0;
}
// Function to prompt for sudo password if needed
int ensure_sudo_access() {
if (check_sudo_cached()) {
return 1; // Already have sudo access
}
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);
// Need: base + '/' + name + cfg_suffix + null
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;
}
// Need: base + '/' + name + null
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); // Parent exits
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;
}
// Fork sound player first
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/keyboard_sound_player", 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, "keyboard_sound_player", "config.json", volume_str,
verbose_str, (char *)NULL);
perror("execl keyboard_sound_player");
exit(1);
}
// Keyboard listener
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/get_key_presses", KeyVibe_BIN_DIR);
if (keyboard_pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execl(get_key_presses_path, "get_key_presses", (char *)NULL);
perror("execl get_key_presses");
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"; // Default sound pack
int verbose = 0;
int list_sounds = 0;
int flag_daemon = 0;
int flag_stop = 0;
// Parse command line arguments
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;
// Load user config if present (pre-parse so CLI can override)
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:
// --reload acts like sending SIGHUP to daemon
{
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;
}
}
// Handle list command
if (list_sounds) {
return list_sound_packs();
}
// PID file path
build_pidfile_path(pidfile_path, sizeof(pidfile_path));
// Handle stop command early
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;
}
// Wait up to ~3s for it to exit
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; // 100ms
nanosleep(&ts, NULL);
}
fprintf(stderr, "KeyVibe: process did not stop in time\n");
return 1;
}
// Apply CLI overrides only if -c/--override-config was provided
if (override_config) {
if (cli_sound != NULL)
sound_name = cli_sound;
if (cli_volume >= 0)
volume = cli_volume;
}
// If no config file existed, create one with defaults
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
if (access(user_cfg_path, F_OK) != 0) {
// Use current values (defaults or CLI overrides) for initial file
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);
}
}
}
// Validate sound pack
if (!validate_sound_pack(sound_name)) {
return 1;
}
// No sudo pre-check; we'll run without sudo by default
// Check if required executables exist
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/get_key_presses", KeyVibe_BIN_DIR);
snprintf(sound_player_path, sizeof(sound_player_path),
"%s/keyboard_sound_player", 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;
}
// Setup signal handler for cleanup
signal(SIGINT, cleanup_processes);
signal(SIGTERM, cleanup_processes);
// If daemon mode requested, ensure not already running, then daemonize
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 our pidfile (this is the supervising process)
if (!write_pidfile(pidfile_path, getpid())) {
// If we cannot write pidfile, still continue but warn and run in
// foreground to avoid orphaning However we're already daemonized; best we
// can do is proceed.
}
}
// Build paths
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;
}
// Store current settings for reloads
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");
}
}
// Start children (sound + keyboard)
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) {
// Monitor user config for changes
char user_cfg_path[MAX_PATH_LENGTH];
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
strncpy(in_args.path, user_cfg_path, 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);
}
}
// Main supervise loop: wait for child exits or reload requests
while (1) {
if (reload_requested) {
reload_requested = 0;
// Re-read user config and optionally apply overrides only if
// override_config was set initially
char user_cfg_path[MAX_PATH_LENGTH];
if (get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
char *cfg_sound = NULL;
int cfg_volume = current_volume;
if (read_user_config(user_cfg_path, &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;
}
}
// Validate new sound pack and paths
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))) {
// Skip reload if paths invalid
continue;
}
// Restart children with new settings
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;
}