#define _POSIX_C_SOURCE 200809L #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; }