diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 97010b7..5701b4b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,3 +1,4 @@
+
name: build-and-test
concurrency:
@@ -35,27 +36,27 @@ jobs:
- name: Prepare data dir for runtime
run: |
- mkdir -p "$PWD/share/keyvibe"
- cp -r audio "$PWD/share/keyvibe/"
+ mkdir -p "$PWD/share/vbx"
+ cp -r audio "$PWD/share/vbx/"
- name: List sound packs
- run: ./keyvibe --list
+ run: ./vbx --list
- name: Smoke test help
- run: ./keyvibe --help || true
+ run: ./vbx --help || true
- name: Extra smoke tests (non-fatal)
run: |
- ./keyvibe --reload || true
- ./keyvibe --stop || true
+ ./vbx --reload || true
+ ./vbx --stop || true
- name: Upload binaries
uses: actions/upload-artifact@v4
with:
- name: keyvibe-binaries
+ name: vbx-binaries
path: |
- keyvibe
- keyvibe-audio
- keyvibe-input
+ vbx
+ vbx-audio
+ vbx-input
diff --git a/Makefile b/Makefile
index ba124e8..c91aec2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
-
CC = gcc
-CFLAGS = -Wall -Wextra -std=c99 -Iinclude
+# Updated include paths for modular headers
+CFLAGS = -Wall -Wextra -std=c99 -Iinclude -Iinclude/app -Iinclude/audio -Iinclude/common -Iinclude/sound -Iinclude/config
PREFIX ?= /usr
# Pass PACKAGE_PREFIX macro for config.h
@@ -10,25 +10,25 @@ LDFLAGS_SOUND = -ljson-c -lpulse -lpulse-simple -lsndfile -lpthread
LDFLAGS_KEYBOARD = $(shell pkg-config --libs libevdev libinput libudev) -lpthread
# Targets
-KeyVibe_TARGET = keyvibe
-SOUND_TARGET = keyvibe-audio
-KEYBOARD_TARGET = keyvibe-input
+VBX_TARGET = vbx
+SOUND_TARGET = audio
+KEYBOARD_TARGET = input
-# Sources
-KeyVibe_SOURCE = src/keyvibe_main.c src/utils.c
-SOUND_SOURCE = src/keyvibe_audio.c src/utils.c
-KEYBOARD_SOURCE = src/keyvibe_input.c src/utils.c
+# Sources (reorganized)
+VBX_SOURCE = src/main.c src/common/utils.c src/config.c src/soundpacks.c src/app/process.c src/app/watch.c src/cli.c src/app/reload.c
+SOUND_SOURCE = src/audio/main.c src/audio/config.c src/audio/playback.c src/common/utils.c
+KEYBOARD_SOURCE = src/input.c src/common/utils.c
# Install paths
BINDIR = $(PREFIX)/bin
-SHAREDIR = $(PREFIX)/share/keyvibe
-UDEV_RULE = /etc/udev/rules.d/99-keyvibe-allow-keyboard.rules
+SHAREDIR = $(PREFIX)/share/vbx
+UDEV_RULE = /etc/udev/rules.d/99-vbx-allow-keyboard.rules
-all: $(KeyVibe_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET)
+all: $(VBX_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET)
# Build main launcher (needs json-c)
-$(KeyVibe_TARGET): $(KeyVibe_SOURCE)
+$(VBX_TARGET): $(VBX_SOURCE)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ -ljson-c -lpthread
$(SOUND_TARGET): $(SOUND_SOURCE)
@@ -38,24 +38,24 @@ $(KEYBOARD_TARGET): $(KEYBOARD_SOURCE)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS_KEYBOARD)
clean:
- rm -f $(KeyVibe_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET)
+ rm -f $(VBX_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET)
test: all
@echo "Testing sound packs:"
- ./$(KeyVibe_TARGET) --list
+ ./$(VBX_TARGET) --list
@echo ""
- @echo "To run KeyVibe:"
- @echo " sudo ./$(KeyVibe_TARGET) # Default sound"
- @echo " sudo ./$(KeyVibe_TARGET) -s cherrymx-blue-abs # Specific sound"
- @echo " sudo ./$(KeyVibe_TARGET) --help # Show help"
+ @echo "To run VBX:"
+ @echo " sudo ./$(VBX_TARGET) # Default sound"
+ @echo " sudo ./$(VBX_TARGET) -s cherrymx-blue-abs # Specific sound"
+ @echo " sudo ./$(VBX_TARGET) --help # Show help"
install:
- @echo "Installing KeyVibe to $(DESTDIR)$(BINDIR) and $(DESTDIR)$(SHAREDIR)..."
- install -Dm755 $(KeyVibe_TARGET) $(DESTDIR)$(BINDIR)/$(KeyVibe_TARGET)
- install -Dm755 $(SOUND_TARGET) $(DESTDIR)$(BINDIR)/$(SOUND_TARGET)
- install -Dm755 $(KEYBOARD_TARGET) $(DESTDIR)$(BINDIR)/$(KEYBOARD_TARGET)
+ @echo "Installing VBX to $(DESTDIR)$(BINDIR) and $(DESTDIR)$(SHAREDIR)..."
+ install -Dm755 $(VBX_TARGET) $(DESTDIR)$(BINDIR)/$(VBX_TARGET)
+ install -Dm755 $(SOUND_TARGET) $(DESTDIR)$(BINDIR)/vbx-audio
+ install -Dm755 $(KEYBOARD_TARGET) $(DESTDIR)$(BINDIR)/vbx-input
install -d $(DESTDIR)$(SHAREDIR)
- cp -r audio $(DESTDIR)$(SHAREDIR)/
+ cp -r soundpacks $(DESTDIR)$(SHAREDIR)/
@echo "Installing udev rule for non-root keyboard access..."
@echo '# Allow non-root access to input event devices for active seat users and input group' | sudo tee $(UDEV_RULE) >/dev/null
@echo 'SUBSYSTEM=="input", KERNEL=="event*", TAG+="uaccess", GROUP="input", MODE="0660"' | sudo tee -a $(UDEV_RULE) >/dev/null
@@ -70,10 +70,10 @@ install:
@echo "Installation complete."
uninstall:
- @echo "Uninstalling KeyVibe from $(DESTDIR)$(BINDIR) and $(DESTDIR)$(SHAREDIR)..."
- rm -f $(DESTDIR)$(BINDIR)/$(KeyVibe_TARGET)
- rm -f $(DESTDIR)$(BINDIR)/$(SOUND_TARGET)
- rm -f $(DESTDIR)$(BINDIR)/$(KEYBOARD_TARGET)
+ @echo "Uninstalling VBX from $(DESTDIR)$(BINDIR) and $(DESTDIR)$(SHAREDIR)..."
+ rm -f $(DESTDIR)$(BINDIR)/$(VBX_TARGET)
+ rm -f $(DESTDIR)$(BINDIR)/vbx-audio
+ rm -f $(DESTDIR)$(BINDIR)/vbx-input
rm -rf $(DESTDIR)$(SHAREDIR)
@echo "Removing udev rule..."
@sudo rm -f $(UDEV_RULE) || true
diff --git a/README.md b/README.md
index 37a6636..9172a65 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,22 @@
-

-> A CLI tool that brings realistic mechanical keyboard sounds to every keystroke.
+# vbx
-KeyVibe listens to your keyboard events and plays high‑quality switch sounds with low latency. It ships with multiple sound packs and supports your own custom packs. Run it interactively or as a background daemon with live reload of your preferences.
+> A CLI tool that brings realistic mechanical keyboard and mouse sounds to every keystroke.
+
+vbx listens to your keyboard and mouse events and plays high‑quality switch sounds with low latency. It ships with multiple sound packs for both devices and supports your own custom packs. Run it interactively or as a background daemon with live reload of your references.
-
-[](https://github.com/S4NKALP/KeyVibe/graphs/contributors)
-[](https://github.com/S4NKALP/KeyVibe/stargazers)
-[](https://github.com/S4NKALP/KeyVibe/network/members)
-[](LICENSE)
-[](https://github.com/S4NKALP/KeyVibe/issues)
-[](https://github.com/S4NKALP/KeyVibe/pulse)
+[](https://github.com/S4NKALP/vbx/graphs/contributors)
+[](https://github.com/S4NKALP/vbx/stargazers)
+[](https://github.com/S4NKALP/vbx/network/members)
+[](LICENSE)
+[](https://github.com/S4NKALP/vbx/issues)
+[](https://github.com/S4NKALP/vbx/pulse)
-[**Report Bug**](https://github.com/S4NKALP/KeyVibe/issues) • [**Request Feature**](https://github.com/S4NKALP/KeyVibe/discussions)
+[**Report Bug**](https://github.com/S4NKALP/vbx/issues) • [**Request Feature**](https://github.com/S4NKALP/vbx/discussions)
@@ -24,11 +24,12 @@ KeyVibe listens to your keyboard events and plays high‑quality switch sounds w
## ✨ Features
-- Real-time key press/release sound playback
-- Multiple bundled sound packs
-- User sound packs under `~/.local/share/keyvibe/audio/`
+- Real-time keyboard key press/release sound playback
+- Real-time mouse click sound playback
+- Multiple bundled sound packs for both keyboard and mouse
+- User sound packs under `~/.local/share/vbx/audio/`
- Background daemon mode with PID file and safe shutdown
-- Live config reload on `~/.keyvibe.json` changes (daemon mode)
+- Live config reload on `~/.vbx.json` changes (daemon mode)
## 📋 Dependencies
@@ -46,21 +47,21 @@ KeyVibe listens to your keyboard events and plays high‑quality switch sounds w
### Arch Linux
```bash
-paru -S keyvibe
+paru -S vbx-git
```
### 🛠️ Build From Source
```bash
-git clone https://github.com/S4NKALP/KeyVibe.git
-cd KeyVibe
+git clone https://github.com/S4NKALP/vbx.git
+cd vbx
make
sudo make install
```
## 💡 Usage
- Usage: keyvibe [OPTIONS]
+ Usage: vbx [OPTIONS]
Options:
-S, --sound SOUND_NAME Select sound pack (default: eg-oreo)
@@ -69,20 +70,21 @@ sudo make install
-s --stop Stop the background daemon
-m --mute Mute sound
-u --unmute Unmute sound
+ -- enbale/disable DEVICE_NAME Enable/disable a device
-l, --list List available sound packs
-h, --help Show this help message
-v, --verbose Enable verbose output
Notes:
-- First run creates `~/.keyvibe.json`. Subsequent runs use it unless `-c` is supplied.
-- In daemon mode, editing `~/.keyvibe.json` will automatically reload.
+- First run creates `~/.vbx.json`. Subsequent runs use it unless `-c` is supplied.
+- In daemon mode, editing `~/.vbx.json` will automatically reload.
## 🎵 Sound Packs
-System packs are installed under: `/usr/share/keyvibe/audio/`
+System packs are installed under: `/usr/share/vbx/soundpacks/`
-User packs go under: `~/.local/share/keyvibe/audio//`
+User packs go under: `~/.local/share/vbx/soundpacks///`
Required files inside each `/` directory:
@@ -91,7 +93,7 @@ Required files inside each `/` directory:
## 🤝 Contributing
-PRs welcome. Please keep code modular and readable, and prefer small, focused changes.
+Contributions are welcome! Feel free to open issues or submit pull requests.
## 📄 License
@@ -100,4 +102,5 @@ MIT — see [LICENSE](LICENSE).
## 🙏 Credits
- [MechVibes](https://github.com/hainguyents13/mechvibes) for sound packs
+- [mechavibes-dx](https://github.com/hainguyents13/mechvibes-dx) for sound packs
- [showmethekey](https://github.com/AlynxZhou/showmethekey) for code
diff --git a/include/app/cli.h b/include/app/cli.h
new file mode 100644
index 0000000..69a1e91
--- /dev/null
+++ b/include/app/cli.h
@@ -0,0 +1,25 @@
+#ifndef VBX_CLI_H
+#define VBX_CLI_H
+
+typedef struct {
+ char *sound;
+ char *mouse_sound;
+ int volume;
+ int keyboard_volume;
+ int mouse_volume;
+ int daemon_flag;
+ int stop_flag;
+ int list_flag;
+ int verbose;
+ int keyboard_mute; // -1 unchanged, 0 unmute, 1 mute
+ int mouse_mute; // -1 unchanged, 0 unmute, 1 mute
+ int keyboard_enabled; // -1 unchanged, 0 disabled, 1 enabled
+ int mouse_enabled; // -1 unchanged, 0 disabled, 1 enabled
+} CliOptions;
+
+void print_usage(const char *program_name);
+int parse_cli(int argc, char **argv, CliOptions *out);
+
+#endif // VBX_CLI_H
+
+
diff --git a/include/app/process.h b/include/app/process.h
new file mode 100644
index 0000000..3696b2a
--- /dev/null
+++ b/include/app/process.h
@@ -0,0 +1,23 @@
+#ifndef VBX_PROCESS_H
+#define VBX_PROCESS_H
+
+#include
+#include
+
+extern pid_t keyboard_pid;
+extern pid_t sound_pid;
+extern char pidfile_path[];
+extern int is_daemon;
+extern volatile sig_atomic_t reload_requested;
+
+void cleanup_processes(int sig);
+int require_running_pid(pid_t *out_pid);
+void daemonize_self(void);
+void stop_children(void);
+int start_children(const char *sound_dir, const char *config_path, int volume,
+ int verbose, int mute, const char *mouse_sound_dir,
+ const char *mouse_config_path, int mouse_volume,
+ int keyboard_mute, int mouse_mute, int keyboard_enabled, int mouse_enabled);
+void handle_sighup(int sig);
+
+#endif // VBX_PROCESS_H
diff --git a/include/app/reload.h b/include/app/reload.h
new file mode 100644
index 0000000..a213bcd
--- /dev/null
+++ b/include/app/reload.h
@@ -0,0 +1,15 @@
+#ifndef VBX_RELOAD_H
+#define VBX_RELOAD_H
+
+int handle_reload(const char *user_cfg_path,
+ char *current_sound_name, char *current_mouse_sound_name,
+ int *current_keyboard_volume, int *current_mouse_volume,
+ char *current_config_path, char *current_sound_dir,
+ char *current_mouse_config_path, char *current_mouse_sound_dir,
+ int current_mute, int current_verbose,
+ int current_mouse_volume_arg, int current_keyboard_mute,
+ int current_mouse_mute, int *current_keyboard_enabled, int *current_mouse_enabled);
+
+#endif // VBX_RELOAD_H
+
+
diff --git a/include/app/watch.h b/include/app/watch.h
new file mode 100644
index 0000000..df483c3
--- /dev/null
+++ b/include/app/watch.h
@@ -0,0 +1,6 @@
+#ifndef VBX_WATCH_H
+#define VBX_WATCH_H
+
+int start_config_watcher(const char *path);
+
+#endif // VBX_WATCH_H
diff --git a/include/audio/playback.h b/include/audio/playback.h
new file mode 100644
index 0000000..325a7a2
--- /dev/null
+++ b/include/audio/playback.h
@@ -0,0 +1,10 @@
+#ifndef VBX_AUDIO_PLAYBACK_H
+#define VBX_AUDIO_PLAYBACK_H
+
+int init_audio(void);
+int parse_keyboard_event(const char *json_line, int *key_code, int *is_pressed);
+void play_sound_segment(int key_code, int is_pressed);
+
+#endif // VBX_AUDIO_PLAYBACK_H
+
+
diff --git a/include/audio/types.h b/include/audio/types.h
new file mode 100644
index 0000000..11f4861
--- /dev/null
+++ b/include/audio/types.h
@@ -0,0 +1,45 @@
+#ifndef VBX_AUDIO_TYPES_H
+#define VBX_AUDIO_TYPES_H
+
+#include
+
+#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[512];
+ struct {
+ char *press;
+ char *release;
+ } multi_key_mappings[512];
+ int is_multi;
+ SF_INFO sf_info;
+} SoundPack;
+
+typedef struct {
+ int key_code;
+ int thread_id;
+ int is_pressed;
+} PlaybackData;
+
+extern SoundPack g_sound_pack;
+extern SoundPack g_mouse_sound_pack;
+extern float g_volume;
+extern float g_mouse_volume;
+extern int g_verbose;
+extern int g_mute;
+extern int g_keyboard_mute;
+extern int g_mouse_mute;
+extern int g_keyboard_enabled;
+extern int g_mouse_enabled;
+
+#endif // VBX_AUDIO_TYPES_H
diff --git a/include/common/mute.h b/include/common/mute.h
new file mode 100644
index 0000000..f1fae62
--- /dev/null
+++ b/include/common/mute.h
@@ -0,0 +1,9 @@
+#ifndef VBX_MUTE_H
+#define VBX_MUTE_H
+
+int read_runtime_mute_file(void);
+void write_runtime_mute_file(int mute);
+void write_runtime_keyboard_mute_file(int mute);
+void write_runtime_mouse_mute_file(int mute);
+
+#endif // VBX_MUTE_H
diff --git a/include/common/utils.h b/include/common/utils.h
new file mode 100644
index 0000000..eb808d7
--- /dev/null
+++ b/include/common/utils.h
@@ -0,0 +1,48 @@
+#ifndef VBX_UTILS_H
+#define VBX_UTILS_H
+
+#include
+#include
+#include
+
+// Returns 1 on success, 0 on truncation/error
+int safe_snprintf(char *dst, size_t dst_sz, const char *fmt, ...);
+
+// fprintf to stderr with printf-style formatting. Returns 1 for convenient
+// `return errorf(...)` usage.
+int errorf(const char *fmt, ...);
+
+// Common runtime helpers
+const char *get_runtime_dir(void);
+void build_pidfile_path(char *buffer, size_t buflen);
+int read_pidfile(const char *path, pid_t *out_pid);
+int write_pidfile(const char *path, pid_t pid);
+int process_is_running(pid_t pid);
+
+// Runtime mute helpers
+int read_runtime_mute_file(void);
+void write_runtime_mute_file(int mute);
+void write_runtime_keyboard_mute_file(int mute);
+void write_runtime_mouse_mute_file(int mute);
+
+// Runtime enabled helpers
+int write_runtime_keyboard_enabled_file(int enabled);
+int write_runtime_mouse_enabled_file(int enabled);
+
+// Helper functions
+void int_to_str(char *buffer, size_t size, int value);
+char *xstrdup(const char *s);
+int validate_volume(int volume);
+const char *get_home_dir(void);
+void safe_strncpy(char *dest, const char *src, size_t dest_size);
+void print_error_and_close_file(FILE *file, const char *error_msg, const char *filename);
+
+// Safe wrapper functions for clang-tidy compliance
+int safe_fscanf(FILE *stream, const char *format, void *ptr);
+int safe_fprintf(FILE *stream, const char *format, ...);
+int safe_snprintf_wrapper(char *str, size_t size, const char *format, ...);
+void *safe_memcpy(void *dest, const void *src, size_t n);
+void *safe_memmove(void *dest, const void *src, size_t n);
+void *safe_memset(void *s, int c, size_t n);
+
+#endif // VBX_UTILS_H
diff --git a/include/config.h b/include/config.h
index 8b2bbe4..68adb25 100644
--- a/include/config.h
+++ b/include/config.h
@@ -1,8 +1,8 @@
#ifndef __CONFIG_H__
#define __CONFIG_H__
-#define PROJECT_VERSION "0.1.0"
-#define GETTEXT_PACKAGE "keyvibe"
+#define PROJECT_VERSION "1.1.0"
+#define GETTEXT_PACKAGE "vbx"
#define PKEXEC_PATH "/usr/bin/pkexec"
// Prefix macro should be defined by compiler flags, fallback to /usr
@@ -13,8 +13,8 @@
#define PACKAGE_BINDIR PACKAGE_PREFIX "/bin"
#define PACKAGE_LOCALEDIR PACKAGE_PREFIX "/share/locale"
-// KeyVibe specific paths relative to prefix
-#define KeyVibe_DATA_DIR PACKAGE_PREFIX "/share/keyvibe"
-#define KeyVibe_BIN_DIR PACKAGE_PREFIX "/bin"
+// VBX specific paths relative to prefix
+#define VBX_DATA_DIR PACKAGE_PREFIX "/share/vbx"
+#define VBX_BIN_DIR PACKAGE_PREFIX "/bin"
#endif
diff --git a/include/soundpacks.h b/include/soundpacks.h
new file mode 100644
index 0000000..3083b7d
--- /dev/null
+++ b/include/soundpacks.h
@@ -0,0 +1,19 @@
+#ifndef VBX_SOUNDPACKS_H
+#define VBX_SOUNDPACKS_H
+#include
+
+int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir,
+ size_t out_sz);
+int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir,
+ size_t out_sz);
+int build_paths_for_keyboard_sound(const char *sound_name,
+ char *out_config_path, size_t out_config_sz,
+ char *out_sound_dir, size_t out_sound_sz);
+int build_paths_for_mouse_sound(const char *sound_name, char *out_config_path,
+ size_t out_config_sz, char *out_sound_dir,
+ size_t out_sound_sz);
+int validate_keyboard_sound_pack(const char *sound_name);
+int validate_mouse_sound_pack(const char *sound_name);
+int list_sound_packs(void);
+
+#endif // VBX_SOUNDPACKS_H
diff --git a/include/user_config.h b/include/user_config.h
new file mode 100644
index 0000000..f063805
--- /dev/null
+++ b/include/user_config.h
@@ -0,0 +1,18 @@
+#ifndef VBX_CONFIG_IO_H
+#define VBX_CONFIG_IO_H
+#include
+
+// Resolve user config path: returns buffer on success, NULL on failure
+char *get_user_config_path(char *buffer, size_t buflen);
+
+// Write ~/.vbx.json in new format
+int write_user_config(const char *path, const char *keyboard_sound,
+ const char *mouse_sound, int keyboard_volume,
+ int mouse_volume, int keyboard_enabled, int mouse_enabled);
+
+// Read ~/.vbx.json supporting new and legacy formats
+int read_user_config(const char *path, char **out_keyboard_sound,
+ char **out_mouse_sound, int *out_keyboard_volume,
+ int *out_mouse_volume, int *out_keyboard_enabled, int *out_mouse_enabled);
+
+#endif // VBX_CONFIG_IO_H
diff --git a/include/utils.h b/include/utils.h
deleted file mode 100644
index 03cd5bf..0000000
--- a/include/utils.h
+++ /dev/null
@@ -1,22 +0,0 @@
-
-#ifndef KEYVIBE_UTILS_H
-#define KEYVIBE_UTILS_H
-
-#include
-#include
-
-// Returns 1 on success, 0 on truncation/error
-int safe_snprintf(char *dst, size_t dst_sz, const char *fmt, ...);
-
-// fprintf to stderr with printf-style formatting. Returns 1 for convenient
-// `return errorf(...)` usage.
-int errorf(const char *fmt, ...);
-
-// Common runtime helpers
-const char *get_runtime_dir(void);
-void build_pidfile_path(char *buffer, size_t buflen);
-int read_pidfile(const char *path, pid_t *out_pid);
-int write_pidfile(const char *path, pid_t pid);
-int process_is_running(pid_t pid);
-
-#endif // KEYVIBE_UTILS_H
diff --git a/audio/banana-split-lubed/LICENSE.txt b/soundpacks/keyboard/banana-split-lubed/LICENSE.txt
similarity index 100%
rename from audio/banana-split-lubed/LICENSE.txt
rename to soundpacks/keyboard/banana-split-lubed/LICENSE.txt
diff --git a/audio/banana-split-lubed/banana-l-1.wav b/soundpacks/keyboard/banana-split-lubed/banana-l-1.wav
similarity index 100%
rename from audio/banana-split-lubed/banana-l-1.wav
rename to soundpacks/keyboard/banana-split-lubed/banana-l-1.wav
diff --git a/audio/banana-split-lubed/banana-l-2.wav b/soundpacks/keyboard/banana-split-lubed/banana-l-2.wav
similarity index 100%
rename from audio/banana-split-lubed/banana-l-2.wav
rename to soundpacks/keyboard/banana-split-lubed/banana-l-2.wav
diff --git a/audio/banana-split-lubed/banana-l-3.wav b/soundpacks/keyboard/banana-split-lubed/banana-l-3.wav
similarity index 100%
rename from audio/banana-split-lubed/banana-l-3.wav
rename to soundpacks/keyboard/banana-split-lubed/banana-l-3.wav
diff --git a/audio/banana-split-lubed/banana-l-4.wav b/soundpacks/keyboard/banana-split-lubed/banana-l-4.wav
similarity index 100%
rename from audio/banana-split-lubed/banana-l-4.wav
rename to soundpacks/keyboard/banana-split-lubed/banana-l-4.wav
diff --git a/audio/banana-split-lubed/banana-l-5.wav b/soundpacks/keyboard/banana-split-lubed/banana-l-5.wav
similarity index 100%
rename from audio/banana-split-lubed/banana-l-5.wav
rename to soundpacks/keyboard/banana-split-lubed/banana-l-5.wav
diff --git a/audio/banana-split-lubed/banana-l-6.wav b/soundpacks/keyboard/banana-split-lubed/banana-l-6.wav
similarity index 100%
rename from audio/banana-split-lubed/banana-l-6.wav
rename to soundpacks/keyboard/banana-split-lubed/banana-l-6.wav
diff --git a/audio/banana-split-lubed/banana-l-7.wav b/soundpacks/keyboard/banana-split-lubed/banana-l-7.wav
similarity index 100%
rename from audio/banana-split-lubed/banana-l-7.wav
rename to soundpacks/keyboard/banana-split-lubed/banana-l-7.wav
diff --git a/audio/banana-split-lubed/config.json b/soundpacks/keyboard/banana-split-lubed/config.json
similarity index 100%
rename from audio/banana-split-lubed/config.json
rename to soundpacks/keyboard/banana-split-lubed/config.json
diff --git a/audio/cherrymx-black-abs/config.json b/soundpacks/keyboard/cherrymx-black-abs/config.json
similarity index 100%
rename from audio/cherrymx-black-abs/config.json
rename to soundpacks/keyboard/cherrymx-black-abs/config.json
diff --git a/audio/cherrymx-black-abs/sound.ogg b/soundpacks/keyboard/cherrymx-black-abs/sound.ogg
similarity index 100%
rename from audio/cherrymx-black-abs/sound.ogg
rename to soundpacks/keyboard/cherrymx-black-abs/sound.ogg
diff --git a/audio/cherrymx-black-pbt/config.json b/soundpacks/keyboard/cherrymx-black-pbt/config.json
similarity index 100%
rename from audio/cherrymx-black-pbt/config.json
rename to soundpacks/keyboard/cherrymx-black-pbt/config.json
diff --git a/audio/cherrymx-black-pbt/sound.ogg b/soundpacks/keyboard/cherrymx-black-pbt/sound.ogg
similarity index 100%
rename from audio/cherrymx-black-pbt/sound.ogg
rename to soundpacks/keyboard/cherrymx-black-pbt/sound.ogg
diff --git a/audio/cherrymx-blue-abs/config.json b/soundpacks/keyboard/cherrymx-blue-abs/config.json
similarity index 100%
rename from audio/cherrymx-blue-abs/config.json
rename to soundpacks/keyboard/cherrymx-blue-abs/config.json
diff --git a/audio/cherrymx-blue-abs/sound.ogg b/soundpacks/keyboard/cherrymx-blue-abs/sound.ogg
similarity index 100%
rename from audio/cherrymx-blue-abs/sound.ogg
rename to soundpacks/keyboard/cherrymx-blue-abs/sound.ogg
diff --git a/audio/cherrymx-blue-pbt/config.json b/soundpacks/keyboard/cherrymx-blue-pbt/config.json
similarity index 100%
rename from audio/cherrymx-blue-pbt/config.json
rename to soundpacks/keyboard/cherrymx-blue-pbt/config.json
diff --git a/audio/cherrymx-blue-pbt/sound.ogg b/soundpacks/keyboard/cherrymx-blue-pbt/sound.ogg
similarity index 100%
rename from audio/cherrymx-blue-pbt/sound.ogg
rename to soundpacks/keyboard/cherrymx-blue-pbt/sound.ogg
diff --git a/audio/cherrymx-brown-abs/config.json b/soundpacks/keyboard/cherrymx-brown-abs/config.json
similarity index 100%
rename from audio/cherrymx-brown-abs/config.json
rename to soundpacks/keyboard/cherrymx-brown-abs/config.json
diff --git a/audio/cherrymx-brown-abs/sound.ogg b/soundpacks/keyboard/cherrymx-brown-abs/sound.ogg
similarity index 100%
rename from audio/cherrymx-brown-abs/sound.ogg
rename to soundpacks/keyboard/cherrymx-brown-abs/sound.ogg
diff --git a/audio/cherrymx-brown-pbt/config.json b/soundpacks/keyboard/cherrymx-brown-pbt/config.json
similarity index 100%
rename from audio/cherrymx-brown-pbt/config.json
rename to soundpacks/keyboard/cherrymx-brown-pbt/config.json
diff --git a/audio/cherrymx-brown-pbt/sound.ogg b/soundpacks/keyboard/cherrymx-brown-pbt/sound.ogg
similarity index 100%
rename from audio/cherrymx-brown-pbt/sound.ogg
rename to soundpacks/keyboard/cherrymx-brown-pbt/sound.ogg
diff --git a/audio/cherrymx-red-abs/config.json b/soundpacks/keyboard/cherrymx-red-abs/config.json
similarity index 100%
rename from audio/cherrymx-red-abs/config.json
rename to soundpacks/keyboard/cherrymx-red-abs/config.json
diff --git a/audio/cherrymx-red-abs/sound.ogg b/soundpacks/keyboard/cherrymx-red-abs/sound.ogg
similarity index 100%
rename from audio/cherrymx-red-abs/sound.ogg
rename to soundpacks/keyboard/cherrymx-red-abs/sound.ogg
diff --git a/audio/cherrymx-red-pbt/config.json b/soundpacks/keyboard/cherrymx-red-pbt/config.json
similarity index 100%
rename from audio/cherrymx-red-pbt/config.json
rename to soundpacks/keyboard/cherrymx-red-pbt/config.json
diff --git a/audio/cherrymx-red-pbt/sound.ogg b/soundpacks/keyboard/cherrymx-red-pbt/sound.ogg
similarity index 100%
rename from audio/cherrymx-red-pbt/sound.ogg
rename to soundpacks/keyboard/cherrymx-red-pbt/sound.ogg
diff --git a/audio/cream-travel/config.json b/soundpacks/keyboard/cream-travel/config.json
similarity index 100%
rename from audio/cream-travel/config.json
rename to soundpacks/keyboard/cream-travel/config.json
diff --git a/audio/cream-travel/press/BACKSPACE.mp3 b/soundpacks/keyboard/cream-travel/press/BACKSPACE.mp3
similarity index 100%
rename from audio/cream-travel/press/BACKSPACE.mp3
rename to soundpacks/keyboard/cream-travel/press/BACKSPACE.mp3
diff --git a/audio/cream-travel/press/ENTER.mp3 b/soundpacks/keyboard/cream-travel/press/ENTER.mp3
similarity index 100%
rename from audio/cream-travel/press/ENTER.mp3
rename to soundpacks/keyboard/cream-travel/press/ENTER.mp3
diff --git a/audio/cream-travel/press/GENERIC_R0.mp3 b/soundpacks/keyboard/cream-travel/press/GENERIC_R0.mp3
similarity index 100%
rename from audio/cream-travel/press/GENERIC_R0.mp3
rename to soundpacks/keyboard/cream-travel/press/GENERIC_R0.mp3
diff --git a/audio/cream-travel/press/GENERIC_R1.mp3 b/soundpacks/keyboard/cream-travel/press/GENERIC_R1.mp3
similarity index 100%
rename from audio/cream-travel/press/GENERIC_R1.mp3
rename to soundpacks/keyboard/cream-travel/press/GENERIC_R1.mp3
diff --git a/audio/cream-travel/press/GENERIC_R2.mp3 b/soundpacks/keyboard/cream-travel/press/GENERIC_R2.mp3
similarity index 100%
rename from audio/cream-travel/press/GENERIC_R2.mp3
rename to soundpacks/keyboard/cream-travel/press/GENERIC_R2.mp3
diff --git a/audio/cream-travel/press/GENERIC_R3.mp3 b/soundpacks/keyboard/cream-travel/press/GENERIC_R3.mp3
similarity index 100%
rename from audio/cream-travel/press/GENERIC_R3.mp3
rename to soundpacks/keyboard/cream-travel/press/GENERIC_R3.mp3
diff --git a/audio/cream-travel/press/GENERIC_R4.mp3 b/soundpacks/keyboard/cream-travel/press/GENERIC_R4.mp3
similarity index 100%
rename from audio/cream-travel/press/GENERIC_R4.mp3
rename to soundpacks/keyboard/cream-travel/press/GENERIC_R4.mp3
diff --git a/audio/cream-travel/press/SPACE.mp3 b/soundpacks/keyboard/cream-travel/press/SPACE.mp3
similarity index 100%
rename from audio/cream-travel/press/SPACE.mp3
rename to soundpacks/keyboard/cream-travel/press/SPACE.mp3
diff --git a/audio/cream-travel/release/BACKSPACE.mp3 b/soundpacks/keyboard/cream-travel/release/BACKSPACE.mp3
similarity index 100%
rename from audio/cream-travel/release/BACKSPACE.mp3
rename to soundpacks/keyboard/cream-travel/release/BACKSPACE.mp3
diff --git a/audio/cream-travel/release/ENTER.mp3 b/soundpacks/keyboard/cream-travel/release/ENTER.mp3
similarity index 100%
rename from audio/cream-travel/release/ENTER.mp3
rename to soundpacks/keyboard/cream-travel/release/ENTER.mp3
diff --git a/audio/cream-travel/release/GENERIC.mp3 b/soundpacks/keyboard/cream-travel/release/GENERIC.mp3
similarity index 100%
rename from audio/cream-travel/release/GENERIC.mp3
rename to soundpacks/keyboard/cream-travel/release/GENERIC.mp3
diff --git a/audio/cream-travel/release/SPACE.mp3 b/soundpacks/keyboard/cream-travel/release/SPACE.mp3
similarity index 100%
rename from audio/cream-travel/release/SPACE.mp3
rename to soundpacks/keyboard/cream-travel/release/SPACE.mp3
diff --git a/audio/eg-crystal-purple/config.json b/soundpacks/keyboard/eg-crystal-purple/config.json
similarity index 100%
rename from audio/eg-crystal-purple/config.json
rename to soundpacks/keyboard/eg-crystal-purple/config.json
diff --git a/audio/eg-crystal-purple/purple.ogg b/soundpacks/keyboard/eg-crystal-purple/purple.ogg
similarity index 100%
rename from audio/eg-crystal-purple/purple.ogg
rename to soundpacks/keyboard/eg-crystal-purple/purple.ogg
diff --git a/audio/eg-oreo/config.json b/soundpacks/keyboard/eg-oreo/config.json
similarity index 97%
rename from audio/eg-oreo/config.json
rename to soundpacks/keyboard/eg-oreo/config.json
index ddaebb1..5aac47f 100644
--- a/audio/eg-oreo/config.json
+++ b/soundpacks/keyboard/eg-oreo/config.json
@@ -464,6 +464,18 @@
"61011": [
13964,
113
+ ],
+ "272": [
+ 971,
+ 142
+ ],
+ "273": [
+ 1276,
+ 115
+ ],
+ "274": [
+ 1604,
+ 122
]
}
}
diff --git a/audio/eg-oreo/oreo.ogg b/soundpacks/keyboard/eg-oreo/oreo.ogg
similarity index 100%
rename from audio/eg-oreo/oreo.ogg
rename to soundpacks/keyboard/eg-oreo/oreo.ogg
diff --git a/audio/holy-pandas/BACKSPACE.mp3 b/soundpacks/keyboard/holy-pandas/BACKSPACE.mp3
similarity index 100%
rename from audio/holy-pandas/BACKSPACE.mp3
rename to soundpacks/keyboard/holy-pandas/BACKSPACE.mp3
diff --git a/audio/holy-pandas/ENTER.mp3 b/soundpacks/keyboard/holy-pandas/ENTER.mp3
similarity index 100%
rename from audio/holy-pandas/ENTER.mp3
rename to soundpacks/keyboard/holy-pandas/ENTER.mp3
diff --git a/audio/holy-pandas/GENERIC_R0.mp3 b/soundpacks/keyboard/holy-pandas/GENERIC_R0.mp3
similarity index 100%
rename from audio/holy-pandas/GENERIC_R0.mp3
rename to soundpacks/keyboard/holy-pandas/GENERIC_R0.mp3
diff --git a/audio/holy-pandas/GENERIC_R1.mp3 b/soundpacks/keyboard/holy-pandas/GENERIC_R1.mp3
similarity index 100%
rename from audio/holy-pandas/GENERIC_R1.mp3
rename to soundpacks/keyboard/holy-pandas/GENERIC_R1.mp3
diff --git a/audio/holy-pandas/GENERIC_R2.mp3 b/soundpacks/keyboard/holy-pandas/GENERIC_R2.mp3
similarity index 100%
rename from audio/holy-pandas/GENERIC_R2.mp3
rename to soundpacks/keyboard/holy-pandas/GENERIC_R2.mp3
diff --git a/audio/holy-pandas/GENERIC_R3.mp3 b/soundpacks/keyboard/holy-pandas/GENERIC_R3.mp3
similarity index 100%
rename from audio/holy-pandas/GENERIC_R3.mp3
rename to soundpacks/keyboard/holy-pandas/GENERIC_R3.mp3
diff --git a/audio/holy-pandas/GENERIC_R4.mp3 b/soundpacks/keyboard/holy-pandas/GENERIC_R4.mp3
similarity index 100%
rename from audio/holy-pandas/GENERIC_R4.mp3
rename to soundpacks/keyboard/holy-pandas/GENERIC_R4.mp3
diff --git a/audio/holy-pandas/LICENSE b/soundpacks/keyboard/holy-pandas/LICENSE
similarity index 100%
rename from audio/holy-pandas/LICENSE
rename to soundpacks/keyboard/holy-pandas/LICENSE
diff --git a/audio/holy-pandas/README.md b/soundpacks/keyboard/holy-pandas/README.md
similarity index 100%
rename from audio/holy-pandas/README.md
rename to soundpacks/keyboard/holy-pandas/README.md
diff --git a/audio/holy-pandas/SPACE.mp3 b/soundpacks/keyboard/holy-pandas/SPACE.mp3
similarity index 100%
rename from audio/holy-pandas/SPACE.mp3
rename to soundpacks/keyboard/holy-pandas/SPACE.mp3
diff --git a/audio/holy-pandas/config.json b/soundpacks/keyboard/holy-pandas/config.json
similarity index 100%
rename from audio/holy-pandas/config.json
rename to soundpacks/keyboard/holy-pandas/config.json
diff --git a/audio/holy-pandas/release/BACKSPACE.mp3 b/soundpacks/keyboard/holy-pandas/release/BACKSPACE.mp3
similarity index 100%
rename from audio/holy-pandas/release/BACKSPACE.mp3
rename to soundpacks/keyboard/holy-pandas/release/BACKSPACE.mp3
diff --git a/audio/holy-pandas/release/ENTER.mp3 b/soundpacks/keyboard/holy-pandas/release/ENTER.mp3
similarity index 100%
rename from audio/holy-pandas/release/ENTER.mp3
rename to soundpacks/keyboard/holy-pandas/release/ENTER.mp3
diff --git a/audio/holy-pandas/release/GENERIC.mp3 b/soundpacks/keyboard/holy-pandas/release/GENERIC.mp3
similarity index 100%
rename from audio/holy-pandas/release/GENERIC.mp3
rename to soundpacks/keyboard/holy-pandas/release/GENERIC.mp3
diff --git a/audio/holy-pandas/release/SPACE.mp3 b/soundpacks/keyboard/holy-pandas/release/SPACE.mp3
similarity index 100%
rename from audio/holy-pandas/release/SPACE.mp3
rename to soundpacks/keyboard/holy-pandas/release/SPACE.mp3
diff --git a/soundpacks/keyboard/kalih-box-white/Untitled Clip(3).mp3 b/soundpacks/keyboard/kalih-box-white/Untitled Clip(3).mp3
new file mode 100644
index 0000000..9bbdc66
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/Untitled Clip(3).mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/Untitled Clip(4).mp3 b/soundpacks/keyboard/kalih-box-white/Untitled Clip(4).mp3
new file mode 100644
index 0000000..9e03f93
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/Untitled Clip(4).mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/Untitled Clip(5).mp3 b/soundpacks/keyboard/kalih-box-white/Untitled Clip(5).mp3
new file mode 100644
index 0000000..2789cfe
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/Untitled Clip(5).mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/Untitled Clip(6).mp3 b/soundpacks/keyboard/kalih-box-white/Untitled Clip(6).mp3
new file mode 100644
index 0000000..f3ac560
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/Untitled Clip(6).mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/Untitled Clip(7).mp3 b/soundpacks/keyboard/kalih-box-white/Untitled Clip(7).mp3
new file mode 100644
index 0000000..4a4b64b
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/Untitled Clip(7).mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/Untitled Clip.mp3 b/soundpacks/keyboard/kalih-box-white/Untitled Clip.mp3
new file mode 100644
index 0000000..dda4af7
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/Untitled Clip.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/alt.mp3 b/soundpacks/keyboard/kalih-box-white/alt.mp3
new file mode 100644
index 0000000..7ba5dc1
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/alt.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/config.json b/soundpacks/keyboard/kalih-box-white/config.json
new file mode 100644
index 0000000..d590dd6
--- /dev/null
+++ b/soundpacks/keyboard/kalih-box-white/config.json
@@ -0,0 +1,135 @@
+{
+ "id": "custom-sound-pack-1203000000053",
+ "name": "Kalih Box White",
+ "key_define_type": "multi",
+ "includes_numpad": false,
+ "sound": "sound.ogg",
+ "defines": {
+ "1": "kboxwh - 08.mp3",
+ "2": "kboxwh - 10.mp3",
+ "3": "kboxwh - 06.mp3",
+ "4": "kboxwh - 03.mp3",
+ "5": "kboxwh - 02.mp3",
+ "6": "kboxwh - 08.mp3",
+ "7": "kboxwh - 07.mp3",
+ "8": "kboxwh - 04.mp3",
+ "9": "kboxwh - 01.mp3",
+ "10": "kboxwh - 04.mp3",
+ "11": "kboxwh - 02.mp3",
+ "12": "Untitled Clip(6).mp3",
+ "13": "Untitled Clip(5).mp3",
+ "14": "Untitled Clip(3).mp3",
+ "15": "Untitled Clip(4).mp3",
+ "16": "kboxwh - 05.mp3",
+ "17": "kboxwh - 01.mp3",
+ "18": "kboxwh - 06.mp3",
+ "19": "kboxwh - 07.mp3",
+ "20": "kboxwh - 05.mp3",
+ "21": "kboxwh - 03.mp3",
+ "22": "kboxwh - 10.mp3",
+ "23": "kboxwh - 01.mp3",
+ "24": "kboxwh - 03.mp3",
+ "25": "kboxwh - 06.mp3",
+ "26": "Untitled Clip(5).mp3",
+ "27": "Untitled Clip(6).mp3",
+ "28": "enter.mp3",
+ "29": "Untitled Clip(3).mp3",
+ "30": "kboxwh - 02.mp3",
+ "31": "kboxwh - 03.mp3",
+ "32": "kboxwh - 04.mp3",
+ "33": "kboxwh - 08.mp3",
+ "34": "kboxwh - 06.mp3",
+ "35": "kboxwh - 07.mp3",
+ "36": "kboxwh - 01.mp3",
+ "37": "Untitled Clip(3).mp3",
+ "38": "kboxwh - 06.mp3",
+ "39": "Untitled Clip(5).mp3",
+ "40": "Untitled Clip(6).mp3",
+ "41": "kboxwh - 09.mp3",
+ "42": "Untitled Clip(5).mp3",
+ "43": "enter.mp3",
+ "44": "kboxwh - 06.mp3",
+ "45": "kboxwh - 04.mp3",
+ "46": "kboxwh - 09.mp3",
+ "47": "kboxwh - 10.mp3",
+ "48": "kboxwh - 02.mp3",
+ "49": "kboxwh - 09.mp3",
+ "50": "kboxwh - 08.mp3",
+ "51": "Untitled Clip(7).mp3",
+ "52": "Untitled Clip(7).mp3",
+ "53": "kboxwh - 04.mp3",
+ "54": "Untitled Clip(5).mp3",
+ "55": "Untitled Clip(6).mp3",
+ "56": "alt.mp3",
+ "57": "Untitled Clip.mp3",
+ "58": "Untitled Clip(6).mp3",
+ "59": "kboxwh - 10.mp3",
+ "60": "kboxwh - 09.mp3",
+ "61": "kboxwh - 08.mp3",
+ "62": "kboxwh - 07.mp3",
+ "63": "kboxwh - 06.mp3",
+ "64": "kboxwh - 05.mp3",
+ "65": "kboxwh - 04.mp3",
+ "66": "kboxwh - 03.mp3",
+ "67": "kboxwh - 01.mp3",
+ "68": "kboxwh - 02.mp3",
+ "69": "Untitled Clip(5).mp3",
+ "70": null,
+ "71": "kboxwh - 08.mp3",
+ "72": "kboxwh - 07.mp3",
+ "73": "kboxwh - 03.mp3",
+ "74": "Untitled Clip(6).mp3",
+ "75": "kboxwh - 09.mp3",
+ "76": "kboxwh - 01.mp3",
+ "77": "kboxwh - 09.mp3",
+ "78": "Untitled Clip(6).mp3",
+ "79": "kboxwh - 09.mp3",
+ "80": "kboxwh - 03.mp3",
+ "81": "kboxwh - 04.mp3",
+ "82": "kboxwh - 02.mp3",
+ "83": "Untitled Clip(7).mp3",
+ "87": "kboxwh - 06.mp3",
+ "88": "kboxwh - 04.mp3",
+ "91": null,
+ "92": null,
+ "93": null,
+ "3597": null,
+ "3612": "enter.mp3",
+ "3613": "Untitled Clip(3).mp3",
+ "3637": "Untitled Clip(6).mp3",
+ "3639": null,
+ "3640": "alt.mp3",
+ "3653": null,
+ "3655": null,
+ "3657": null,
+ "3663": null,
+ "3665": null,
+ "3666": null,
+ "3667": null,
+ "3675": "Untitled Clip(7).mp3",
+ "3676": "Untitled Clip(7).mp3",
+ "3677": "Untitled Clip(3).mp3",
+ "57416": "kboxwh - 01.mp3",
+ "57419": "kboxwh - 02.mp3",
+ "57421": "kboxwh - 04.mp3",
+ "57424": "kboxwh - 03.mp3",
+ "60999": null,
+ "61000": "kboxwh - 01.mp3",
+ "61001": null,
+ "61003": "kboxwh - 02.mp3",
+ "61005": "kboxwh - 04.mp3",
+ "61007": null,
+ "61008": "kboxwh - 03.mp3",
+ "61009": null,
+ "61010": null,
+ "61011": null
+ },
+ "_zip": "kailh-box-white.zip",
+ "group": "1fMFAp - sOcaAT - MuxCD - GlHmN3",
+ "description": "Kailh Box White switches with individual sounds per keypress and no background noises.",
+ "m_author": "edy.",
+ "tags": [
+ "keyboard",
+ "clicky"
+ ]
+}
\ No newline at end of file
diff --git a/soundpacks/keyboard/kalih-box-white/enter.mp3 b/soundpacks/keyboard/kalih-box-white/enter.mp3
new file mode 100644
index 0000000..7a54bdb
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/enter.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 01.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 01.mp3
new file mode 100644
index 0000000..2a3efea
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 01.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 02.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 02.mp3
new file mode 100644
index 0000000..94e7daf
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 02.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 03.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 03.mp3
new file mode 100644
index 0000000..770b929
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 03.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 04.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 04.mp3
new file mode 100644
index 0000000..2490ac3
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 04.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 05.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 05.mp3
new file mode 100644
index 0000000..daac682
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 05.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 06.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 06.mp3
new file mode 100644
index 0000000..c3d3444
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 06.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 07.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 07.mp3
new file mode 100644
index 0000000..d05eebf
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 07.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 08.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 08.mp3
new file mode 100644
index 0000000..16b4115
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 08.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 09.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 09.mp3
new file mode 100644
index 0000000..485ed83
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 09.mp3 differ
diff --git a/soundpacks/keyboard/kalih-box-white/kboxwh - 10.mp3 b/soundpacks/keyboard/kalih-box-white/kboxwh - 10.mp3
new file mode 100644
index 0000000..18a064d
Binary files /dev/null and b/soundpacks/keyboard/kalih-box-white/kboxwh - 10.mp3 differ
diff --git a/audio/mxblack-travel/config.json b/soundpacks/keyboard/mxblack-travel/config.json
similarity index 100%
rename from audio/mxblack-travel/config.json
rename to soundpacks/keyboard/mxblack-travel/config.json
diff --git a/audio/mxblack-travel/press/BACKSPACE.mp3 b/soundpacks/keyboard/mxblack-travel/press/BACKSPACE.mp3
similarity index 100%
rename from audio/mxblack-travel/press/BACKSPACE.mp3
rename to soundpacks/keyboard/mxblack-travel/press/BACKSPACE.mp3
diff --git a/audio/mxblack-travel/press/ENTER.mp3 b/soundpacks/keyboard/mxblack-travel/press/ENTER.mp3
similarity index 100%
rename from audio/mxblack-travel/press/ENTER.mp3
rename to soundpacks/keyboard/mxblack-travel/press/ENTER.mp3
diff --git a/audio/mxblack-travel/press/GENERIC_R0.mp3 b/soundpacks/keyboard/mxblack-travel/press/GENERIC_R0.mp3
similarity index 100%
rename from audio/mxblack-travel/press/GENERIC_R0.mp3
rename to soundpacks/keyboard/mxblack-travel/press/GENERIC_R0.mp3
diff --git a/audio/mxblack-travel/press/GENERIC_R1.mp3 b/soundpacks/keyboard/mxblack-travel/press/GENERIC_R1.mp3
similarity index 100%
rename from audio/mxblack-travel/press/GENERIC_R1.mp3
rename to soundpacks/keyboard/mxblack-travel/press/GENERIC_R1.mp3
diff --git a/audio/mxblack-travel/press/GENERIC_R2.mp3 b/soundpacks/keyboard/mxblack-travel/press/GENERIC_R2.mp3
similarity index 100%
rename from audio/mxblack-travel/press/GENERIC_R2.mp3
rename to soundpacks/keyboard/mxblack-travel/press/GENERIC_R2.mp3
diff --git a/audio/mxblack-travel/press/GENERIC_R3.mp3 b/soundpacks/keyboard/mxblack-travel/press/GENERIC_R3.mp3
similarity index 100%
rename from audio/mxblack-travel/press/GENERIC_R3.mp3
rename to soundpacks/keyboard/mxblack-travel/press/GENERIC_R3.mp3
diff --git a/audio/mxblack-travel/press/GENERIC_R4.mp3 b/soundpacks/keyboard/mxblack-travel/press/GENERIC_R4.mp3
similarity index 100%
rename from audio/mxblack-travel/press/GENERIC_R4.mp3
rename to soundpacks/keyboard/mxblack-travel/press/GENERIC_R4.mp3
diff --git a/audio/mxblack-travel/press/SPACE.mp3 b/soundpacks/keyboard/mxblack-travel/press/SPACE.mp3
similarity index 100%
rename from audio/mxblack-travel/press/SPACE.mp3
rename to soundpacks/keyboard/mxblack-travel/press/SPACE.mp3
diff --git a/audio/mxblack-travel/release/BACKSPACE.mp3 b/soundpacks/keyboard/mxblack-travel/release/BACKSPACE.mp3
similarity index 100%
rename from audio/mxblack-travel/release/BACKSPACE.mp3
rename to soundpacks/keyboard/mxblack-travel/release/BACKSPACE.mp3
diff --git a/audio/mxblack-travel/release/ENTER.mp3 b/soundpacks/keyboard/mxblack-travel/release/ENTER.mp3
similarity index 100%
rename from audio/mxblack-travel/release/ENTER.mp3
rename to soundpacks/keyboard/mxblack-travel/release/ENTER.mp3
diff --git a/audio/mxblack-travel/release/GENERIC.mp3 b/soundpacks/keyboard/mxblack-travel/release/GENERIC.mp3
similarity index 100%
rename from audio/mxblack-travel/release/GENERIC.mp3
rename to soundpacks/keyboard/mxblack-travel/release/GENERIC.mp3
diff --git a/audio/mxblack-travel/release/SPACE.mp3 b/soundpacks/keyboard/mxblack-travel/release/SPACE.mp3
similarity index 100%
rename from audio/mxblack-travel/release/SPACE.mp3
rename to soundpacks/keyboard/mxblack-travel/release/SPACE.mp3
diff --git a/audio/mxblue-travel/config.json b/soundpacks/keyboard/mxblue-travel/config.json
similarity index 100%
rename from audio/mxblue-travel/config.json
rename to soundpacks/keyboard/mxblue-travel/config.json
diff --git a/audio/mxblue-travel/press/GENERIC_R0.mp3 b/soundpacks/keyboard/mxblue-travel/press/GENERIC_R0.mp3
similarity index 100%
rename from audio/mxblue-travel/press/GENERIC_R0.mp3
rename to soundpacks/keyboard/mxblue-travel/press/GENERIC_R0.mp3
diff --git a/audio/mxblue-travel/press/GENERIC_R1.mp3 b/soundpacks/keyboard/mxblue-travel/press/GENERIC_R1.mp3
similarity index 100%
rename from audio/mxblue-travel/press/GENERIC_R1.mp3
rename to soundpacks/keyboard/mxblue-travel/press/GENERIC_R1.mp3
diff --git a/audio/mxblue-travel/press/GENERIC_R2.mp3 b/soundpacks/keyboard/mxblue-travel/press/GENERIC_R2.mp3
similarity index 100%
rename from audio/mxblue-travel/press/GENERIC_R2.mp3
rename to soundpacks/keyboard/mxblue-travel/press/GENERIC_R2.mp3
diff --git a/audio/mxblue-travel/press/GENERIC_R3.mp3 b/soundpacks/keyboard/mxblue-travel/press/GENERIC_R3.mp3
similarity index 100%
rename from audio/mxblue-travel/press/GENERIC_R3.mp3
rename to soundpacks/keyboard/mxblue-travel/press/GENERIC_R3.mp3
diff --git a/audio/mxblue-travel/press/GENERIC_R4.mp3 b/soundpacks/keyboard/mxblue-travel/press/GENERIC_R4.mp3
similarity index 100%
rename from audio/mxblue-travel/press/GENERIC_R4.mp3
rename to soundpacks/keyboard/mxblue-travel/press/GENERIC_R4.mp3
diff --git a/audio/mxblue-travel/release/GENERIC.mp3 b/soundpacks/keyboard/mxblue-travel/release/GENERIC.mp3
similarity index 100%
rename from audio/mxblue-travel/release/GENERIC.mp3
rename to soundpacks/keyboard/mxblue-travel/release/GENERIC.mp3
diff --git a/audio/mxbrown-travel/config.json b/soundpacks/keyboard/mxbrown-travel/config.json
similarity index 100%
rename from audio/mxbrown-travel/config.json
rename to soundpacks/keyboard/mxbrown-travel/config.json
diff --git a/audio/mxbrown-travel/press/BACKSPACE.mp3 b/soundpacks/keyboard/mxbrown-travel/press/BACKSPACE.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/BACKSPACE.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/BACKSPACE.mp3
diff --git a/audio/mxbrown-travel/press/ENTER.mp3 b/soundpacks/keyboard/mxbrown-travel/press/ENTER.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/ENTER.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/ENTER.mp3
diff --git a/audio/mxbrown-travel/press/GENERIC_R0.mp3 b/soundpacks/keyboard/mxbrown-travel/press/GENERIC_R0.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/GENERIC_R0.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/GENERIC_R0.mp3
diff --git a/audio/mxbrown-travel/press/GENERIC_R1.mp3 b/soundpacks/keyboard/mxbrown-travel/press/GENERIC_R1.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/GENERIC_R1.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/GENERIC_R1.mp3
diff --git a/audio/mxbrown-travel/press/GENERIC_R2.mp3 b/soundpacks/keyboard/mxbrown-travel/press/GENERIC_R2.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/GENERIC_R2.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/GENERIC_R2.mp3
diff --git a/audio/mxbrown-travel/press/GENERIC_R3.mp3 b/soundpacks/keyboard/mxbrown-travel/press/GENERIC_R3.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/GENERIC_R3.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/GENERIC_R3.mp3
diff --git a/audio/mxbrown-travel/press/GENERIC_R4.mp3 b/soundpacks/keyboard/mxbrown-travel/press/GENERIC_R4.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/GENERIC_R4.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/GENERIC_R4.mp3
diff --git a/audio/mxbrown-travel/press/SPACE.mp3 b/soundpacks/keyboard/mxbrown-travel/press/SPACE.mp3
similarity index 100%
rename from audio/mxbrown-travel/press/SPACE.mp3
rename to soundpacks/keyboard/mxbrown-travel/press/SPACE.mp3
diff --git a/audio/mxbrown-travel/release/BACKSPACE.mp3 b/soundpacks/keyboard/mxbrown-travel/release/BACKSPACE.mp3
similarity index 100%
rename from audio/mxbrown-travel/release/BACKSPACE.mp3
rename to soundpacks/keyboard/mxbrown-travel/release/BACKSPACE.mp3
diff --git a/audio/mxbrown-travel/release/ENTER.mp3 b/soundpacks/keyboard/mxbrown-travel/release/ENTER.mp3
similarity index 100%
rename from audio/mxbrown-travel/release/ENTER.mp3
rename to soundpacks/keyboard/mxbrown-travel/release/ENTER.mp3
diff --git a/audio/mxbrown-travel/release/GENERIC.mp3 b/soundpacks/keyboard/mxbrown-travel/release/GENERIC.mp3
similarity index 100%
rename from audio/mxbrown-travel/release/GENERIC.mp3
rename to soundpacks/keyboard/mxbrown-travel/release/GENERIC.mp3
diff --git a/audio/mxbrown-travel/release/SPACE.mp3 b/soundpacks/keyboard/mxbrown-travel/release/SPACE.mp3
similarity index 100%
rename from audio/mxbrown-travel/release/SPACE.mp3
rename to soundpacks/keyboard/mxbrown-travel/release/SPACE.mp3
diff --git a/audio/nk-cream/[.wav b/soundpacks/keyboard/nk-cream/[.wav
similarity index 100%
rename from audio/nk-cream/[.wav
rename to soundpacks/keyboard/nk-cream/[.wav
diff --git a/audio/nk-cream/].wav b/soundpacks/keyboard/nk-cream/].wav
similarity index 100%
rename from audio/nk-cream/].wav
rename to soundpacks/keyboard/nk-cream/].wav
diff --git a/audio/nk-cream/a.wav b/soundpacks/keyboard/nk-cream/a.wav
similarity index 100%
rename from audio/nk-cream/a.wav
rename to soundpacks/keyboard/nk-cream/a.wav
diff --git a/audio/nk-cream/b.wav b/soundpacks/keyboard/nk-cream/b.wav
similarity index 100%
rename from audio/nk-cream/b.wav
rename to soundpacks/keyboard/nk-cream/b.wav
diff --git a/audio/nk-cream/backspace.wav b/soundpacks/keyboard/nk-cream/backspace.wav
similarity index 100%
rename from audio/nk-cream/backspace.wav
rename to soundpacks/keyboard/nk-cream/backspace.wav
diff --git a/audio/nk-cream/c.wav b/soundpacks/keyboard/nk-cream/c.wav
similarity index 100%
rename from audio/nk-cream/c.wav
rename to soundpacks/keyboard/nk-cream/c.wav
diff --git a/audio/nk-cream/caps lock.wav b/soundpacks/keyboard/nk-cream/caps lock.wav
similarity index 100%
rename from audio/nk-cream/caps lock.wav
rename to soundpacks/keyboard/nk-cream/caps lock.wav
diff --git a/audio/nk-cream/config.json b/soundpacks/keyboard/nk-cream/config.json
similarity index 100%
rename from audio/nk-cream/config.json
rename to soundpacks/keyboard/nk-cream/config.json
diff --git a/audio/nk-cream/d.wav b/soundpacks/keyboard/nk-cream/d.wav
similarity index 100%
rename from audio/nk-cream/d.wav
rename to soundpacks/keyboard/nk-cream/d.wav
diff --git a/audio/nk-cream/e.wav b/soundpacks/keyboard/nk-cream/e.wav
similarity index 100%
rename from audio/nk-cream/e.wav
rename to soundpacks/keyboard/nk-cream/e.wav
diff --git a/audio/nk-cream/enter.wav b/soundpacks/keyboard/nk-cream/enter.wav
similarity index 100%
rename from audio/nk-cream/enter.wav
rename to soundpacks/keyboard/nk-cream/enter.wav
diff --git a/audio/nk-cream/f.wav b/soundpacks/keyboard/nk-cream/f.wav
similarity index 100%
rename from audio/nk-cream/f.wav
rename to soundpacks/keyboard/nk-cream/f.wav
diff --git a/audio/nk-cream/g.wav b/soundpacks/keyboard/nk-cream/g.wav
similarity index 100%
rename from audio/nk-cream/g.wav
rename to soundpacks/keyboard/nk-cream/g.wav
diff --git a/audio/nk-cream/h.wav b/soundpacks/keyboard/nk-cream/h.wav
similarity index 100%
rename from audio/nk-cream/h.wav
rename to soundpacks/keyboard/nk-cream/h.wav
diff --git a/audio/nk-cream/i.wav b/soundpacks/keyboard/nk-cream/i.wav
similarity index 100%
rename from audio/nk-cream/i.wav
rename to soundpacks/keyboard/nk-cream/i.wav
diff --git a/audio/nk-cream/j.wav b/soundpacks/keyboard/nk-cream/j.wav
similarity index 100%
rename from audio/nk-cream/j.wav
rename to soundpacks/keyboard/nk-cream/j.wav
diff --git a/audio/nk-cream/k.wav b/soundpacks/keyboard/nk-cream/k.wav
similarity index 100%
rename from audio/nk-cream/k.wav
rename to soundpacks/keyboard/nk-cream/k.wav
diff --git a/audio/nk-cream/l.wav b/soundpacks/keyboard/nk-cream/l.wav
similarity index 100%
rename from audio/nk-cream/l.wav
rename to soundpacks/keyboard/nk-cream/l.wav
diff --git a/audio/nk-cream/m.wav b/soundpacks/keyboard/nk-cream/m.wav
similarity index 100%
rename from audio/nk-cream/m.wav
rename to soundpacks/keyboard/nk-cream/m.wav
diff --git a/audio/nk-cream/n.wav b/soundpacks/keyboard/nk-cream/n.wav
similarity index 100%
rename from audio/nk-cream/n.wav
rename to soundpacks/keyboard/nk-cream/n.wav
diff --git a/audio/nk-cream/o.wav b/soundpacks/keyboard/nk-cream/o.wav
similarity index 100%
rename from audio/nk-cream/o.wav
rename to soundpacks/keyboard/nk-cream/o.wav
diff --git a/audio/nk-cream/p.wav b/soundpacks/keyboard/nk-cream/p.wav
similarity index 100%
rename from audio/nk-cream/p.wav
rename to soundpacks/keyboard/nk-cream/p.wav
diff --git a/audio/nk-cream/q.wav b/soundpacks/keyboard/nk-cream/q.wav
similarity index 100%
rename from audio/nk-cream/q.wav
rename to soundpacks/keyboard/nk-cream/q.wav
diff --git a/audio/nk-cream/r.wav b/soundpacks/keyboard/nk-cream/r.wav
similarity index 100%
rename from audio/nk-cream/r.wav
rename to soundpacks/keyboard/nk-cream/r.wav
diff --git a/audio/nk-cream/s.wav b/soundpacks/keyboard/nk-cream/s.wav
similarity index 100%
rename from audio/nk-cream/s.wav
rename to soundpacks/keyboard/nk-cream/s.wav
diff --git a/audio/nk-cream/shift.wav b/soundpacks/keyboard/nk-cream/shift.wav
similarity index 100%
rename from audio/nk-cream/shift.wav
rename to soundpacks/keyboard/nk-cream/shift.wav
diff --git a/audio/nk-cream/space.wav b/soundpacks/keyboard/nk-cream/space.wav
similarity index 100%
rename from audio/nk-cream/space.wav
rename to soundpacks/keyboard/nk-cream/space.wav
diff --git a/audio/nk-cream/t.wav b/soundpacks/keyboard/nk-cream/t.wav
similarity index 100%
rename from audio/nk-cream/t.wav
rename to soundpacks/keyboard/nk-cream/t.wav
diff --git a/audio/nk-cream/tab.wav b/soundpacks/keyboard/nk-cream/tab.wav
similarity index 100%
rename from audio/nk-cream/tab.wav
rename to soundpacks/keyboard/nk-cream/tab.wav
diff --git a/audio/nk-cream/u.wav b/soundpacks/keyboard/nk-cream/u.wav
similarity index 100%
rename from audio/nk-cream/u.wav
rename to soundpacks/keyboard/nk-cream/u.wav
diff --git a/audio/nk-cream/v.wav b/soundpacks/keyboard/nk-cream/v.wav
similarity index 100%
rename from audio/nk-cream/v.wav
rename to soundpacks/keyboard/nk-cream/v.wav
diff --git a/audio/nk-cream/w.wav b/soundpacks/keyboard/nk-cream/w.wav
similarity index 100%
rename from audio/nk-cream/w.wav
rename to soundpacks/keyboard/nk-cream/w.wav
diff --git a/audio/nk-cream/x.wav b/soundpacks/keyboard/nk-cream/x.wav
similarity index 100%
rename from audio/nk-cream/x.wav
rename to soundpacks/keyboard/nk-cream/x.wav
diff --git a/audio/nk-cream/y.wav b/soundpacks/keyboard/nk-cream/y.wav
similarity index 100%
rename from audio/nk-cream/y.wav
rename to soundpacks/keyboard/nk-cream/y.wav
diff --git a/audio/nk-cream/z.wav b/soundpacks/keyboard/nk-cream/z.wav
similarity index 100%
rename from audio/nk-cream/z.wav
rename to soundpacks/keyboard/nk-cream/z.wav
diff --git a/audio/topre-purple-hybrid-pbt/config.json b/soundpacks/keyboard/topre-purple-hybrid-pbt/config.json
similarity index 100%
rename from audio/topre-purple-hybrid-pbt/config.json
rename to soundpacks/keyboard/topre-purple-hybrid-pbt/config.json
diff --git a/audio/topre-purple-hybrid-pbt/sound.ogg b/soundpacks/keyboard/topre-purple-hybrid-pbt/sound.ogg
similarity index 100%
rename from audio/topre-purple-hybrid-pbt/sound.ogg
rename to soundpacks/keyboard/topre-purple-hybrid-pbt/sound.ogg
diff --git a/audio/turquoise/config.json b/soundpacks/keyboard/turquoise/config.json
similarity index 100%
rename from audio/turquoise/config.json
rename to soundpacks/keyboard/turquoise/config.json
diff --git a/audio/turquoise/press/BACKSPACE.mp3 b/soundpacks/keyboard/turquoise/press/BACKSPACE.mp3
similarity index 100%
rename from audio/turquoise/press/BACKSPACE.mp3
rename to soundpacks/keyboard/turquoise/press/BACKSPACE.mp3
diff --git a/audio/turquoise/press/ENTER.mp3 b/soundpacks/keyboard/turquoise/press/ENTER.mp3
similarity index 100%
rename from audio/turquoise/press/ENTER.mp3
rename to soundpacks/keyboard/turquoise/press/ENTER.mp3
diff --git a/audio/turquoise/press/GENERIC_R0.mp3 b/soundpacks/keyboard/turquoise/press/GENERIC_R0.mp3
similarity index 100%
rename from audio/turquoise/press/GENERIC_R0.mp3
rename to soundpacks/keyboard/turquoise/press/GENERIC_R0.mp3
diff --git a/audio/turquoise/press/GENERIC_R1.mp3 b/soundpacks/keyboard/turquoise/press/GENERIC_R1.mp3
similarity index 100%
rename from audio/turquoise/press/GENERIC_R1.mp3
rename to soundpacks/keyboard/turquoise/press/GENERIC_R1.mp3
diff --git a/audio/turquoise/press/GENERIC_R2.mp3 b/soundpacks/keyboard/turquoise/press/GENERIC_R2.mp3
similarity index 100%
rename from audio/turquoise/press/GENERIC_R2.mp3
rename to soundpacks/keyboard/turquoise/press/GENERIC_R2.mp3
diff --git a/audio/turquoise/press/GENERIC_R3.mp3 b/soundpacks/keyboard/turquoise/press/GENERIC_R3.mp3
similarity index 100%
rename from audio/turquoise/press/GENERIC_R3.mp3
rename to soundpacks/keyboard/turquoise/press/GENERIC_R3.mp3
diff --git a/audio/turquoise/press/GENERIC_R4.mp3 b/soundpacks/keyboard/turquoise/press/GENERIC_R4.mp3
similarity index 100%
rename from audio/turquoise/press/GENERIC_R4.mp3
rename to soundpacks/keyboard/turquoise/press/GENERIC_R4.mp3
diff --git a/audio/turquoise/press/SPACE.mp3 b/soundpacks/keyboard/turquoise/press/SPACE.mp3
similarity index 100%
rename from audio/turquoise/press/SPACE.mp3
rename to soundpacks/keyboard/turquoise/press/SPACE.mp3
diff --git a/audio/turquoise/release/BACKSPACE.mp3 b/soundpacks/keyboard/turquoise/release/BACKSPACE.mp3
similarity index 100%
rename from audio/turquoise/release/BACKSPACE.mp3
rename to soundpacks/keyboard/turquoise/release/BACKSPACE.mp3
diff --git a/audio/turquoise/release/ENTER.mp3 b/soundpacks/keyboard/turquoise/release/ENTER.mp3
similarity index 100%
rename from audio/turquoise/release/ENTER.mp3
rename to soundpacks/keyboard/turquoise/release/ENTER.mp3
diff --git a/audio/turquoise/release/GENERIC.mp3 b/soundpacks/keyboard/turquoise/release/GENERIC.mp3
similarity index 100%
rename from audio/turquoise/release/GENERIC.mp3
rename to soundpacks/keyboard/turquoise/release/GENERIC.mp3
diff --git a/audio/turquoise/release/SPACE.mp3 b/soundpacks/keyboard/turquoise/release/SPACE.mp3
similarity index 100%
rename from audio/turquoise/release/SPACE.mp3
rename to soundpacks/keyboard/turquoise/release/SPACE.mp3
diff --git a/soundpacks/mouse/chat/chat.mp3 b/soundpacks/mouse/chat/chat.mp3
new file mode 100644
index 0000000..883b63a
Binary files /dev/null and b/soundpacks/mouse/chat/chat.mp3 differ
diff --git a/soundpacks/mouse/chat/config.json b/soundpacks/mouse/chat/config.json
new file mode 100644
index 0000000..47627a8
--- /dev/null
+++ b/soundpacks/mouse/chat/config.json
@@ -0,0 +1,28 @@
+{
+ "audio_file": "chat.mp3",
+ "config_version": "2",
+ "created_at": "2025-06-17T12:23:39.537516300+00:00",
+ "definition_method": "single",
+ "author": "Mechvibes",
+ "definitions": {
+ "MouseLeft": {
+ "timing": [
+ [26.0, 156.0],
+ [176.0, 545.0]
+ ]
+ },
+ "MouseRight": {
+ "timing": [
+ [26.0, 156.0],
+ [176.0, 545.0]
+ ]
+ }
+ },
+ "id": "mouse-chat",
+ "name": "Chat",
+ "options": {
+ "random_pitch": false,
+ "recommended_volume": 1.0
+ },
+ "tags": []
+}
diff --git a/soundpacks/mouse/ping/config.json b/soundpacks/mouse/ping/config.json
new file mode 100644
index 0000000..9d13ff4
--- /dev/null
+++ b/soundpacks/mouse/ping/config.json
@@ -0,0 +1,28 @@
+{
+ "audio_file": "ping.mp3",
+ "config_version": "2",
+ "created_at": "2025-06-17T12:23:39.537516300+00:00",
+ "definition_method": "single",
+ "author": "Mechvibes",
+ "definitions": {
+ "MouseLeft": {
+ "timing": [
+ [47.0, 84.0],
+ [105.0, 177.0]
+ ]
+ },
+ "MouseRight": {
+ "timing": [
+ [261.0, 295.0],
+ [373.0, 402.0]
+ ]
+ }
+ },
+ "id": "mouse-ping",
+ "name": "Ping",
+ "options": {
+ "random_pitch": false,
+ "recommended_volume": 1.0
+ },
+ "tags": []
+}
diff --git a/soundpacks/mouse/ping/ping.mp3 b/soundpacks/mouse/ping/ping.mp3
new file mode 100644
index 0000000..945eac5
Binary files /dev/null and b/soundpacks/mouse/ping/ping.mp3 differ
diff --git a/soundpacks/mouse/vibrate/config.json b/soundpacks/mouse/vibrate/config.json
new file mode 100644
index 0000000..103c375
--- /dev/null
+++ b/soundpacks/mouse/vibrate/config.json
@@ -0,0 +1,28 @@
+{
+ "audio_file": "vibrate.mp3",
+ "config_version": "2",
+ "created_at": "2025-06-17T12:23:39.537516300+00:00",
+ "definition_method": "single",
+ "author": "Mechvibes",
+ "definitions": {
+ "MouseLeft": {
+ "timing": [
+ [99.0, 321.0],
+ [368.0, 633.0]
+ ]
+ },
+ "MouseRight": {
+ "timing": [
+ [99.0, 321.0],
+ [368.0, 633.0]
+ ]
+ }
+ },
+ "id": "mouse-vibrate",
+ "name": "Vibrate",
+ "options": {
+ "random_pitch": false,
+ "recommended_volume": 1.0
+ },
+ "tags": []
+}
diff --git a/soundpacks/mouse/vibrate/vibrate.mp3 b/soundpacks/mouse/vibrate/vibrate.mp3
new file mode 100644
index 0000000..4bd2435
Binary files /dev/null and b/soundpacks/mouse/vibrate/vibrate.mp3 differ
diff --git a/soundpacks/mouse/wooden/config.json b/soundpacks/mouse/wooden/config.json
new file mode 100644
index 0000000..a1f10f3
--- /dev/null
+++ b/soundpacks/mouse/wooden/config.json
@@ -0,0 +1,28 @@
+{
+ "audio_file": "wooden.mp3",
+ "config_version": "2",
+ "created_at": "2025-06-17T12:23:39.537516300+00:00",
+ "definition_method": "single",
+ "author": "Mechvibes",
+ "definitions": {
+ "MouseLeft": {
+ "timing": [
+ [528.0, 570.0],
+ [782.0, 806.0]
+ ]
+ },
+ "MouseRight": {
+ "timing": [
+ [528.0, 570.0],
+ [782.0, 806.0]
+ ]
+ }
+ },
+ "id": "mouse-wooden",
+ "name": "Wooden",
+ "options": {
+ "random_pitch": false,
+ "recommended_volume": 1.0
+ },
+ "tags": []
+}
diff --git a/soundpacks/mouse/wooden/wooden.mp3 b/soundpacks/mouse/wooden/wooden.mp3
new file mode 100644
index 0000000..1c648e9
Binary files /dev/null and b/soundpacks/mouse/wooden/wooden.mp3 differ
diff --git a/src/app/process.c b/src/app/process.c
new file mode 100644
index 0000000..125cbed
--- /dev/null
+++ b/src/app/process.c
@@ -0,0 +1,168 @@
+#define _POSIX_C_SOURCE 200809L
+#include "app/process.h"
+#include "common/utils.h"
+#include "config.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define MAX_PATH_LENGTH 1024
+
+pid_t keyboard_pid = 0;
+pid_t sound_pid = 0;
+char pidfile_path[MAX_PATH_LENGTH] = {0};
+int is_daemon = 0;
+volatile sig_atomic_t reload_requested = 0;
+
+void cleanup_processes(int sig) {
+ (void)sig;
+ if (!is_daemon) {
+ printf("\nShutting down VBX daemon...\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 require_running_pid(pid_t *out_pid) {
+ 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, "VBX daemon is not running.\n");
+ return 0;
+ }
+ *out_pid = running_pid;
+ return 1;
+}
+
+void daemonize_self(void) {
+ 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);
+ }
+}
+
+void stop_children(void) {
+ 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;
+ }
+}
+
+
+int start_children(const char *sound_dir, const char *config_path, int volume,
+ int verbose, int mute, const char *mouse_sound_dir,
+ const char *mouse_config_path, int mouse_volume,
+ int keyboard_mute, int mouse_mute, int keyboard_enabled,
+ int mouse_enabled) {
+ (void)config_path;
+ (void)mouse_sound_dir;
+ 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/vbx-audio",
+ VBX_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], mouse_volume_str[32], verbose_str[8], mute_str[8];
+ char keyboard_mute_str[8], mouse_mute_str[8];
+ char keyboard_enabled_str[8], mouse_enabled_str[8];
+
+ int_to_str(volume_str, sizeof(volume_str), volume);
+ int_to_str(mouse_volume_str, sizeof(mouse_volume_str), mouse_volume);
+ int_to_str(verbose_str, sizeof(verbose_str), verbose);
+ int_to_str(mute_str, sizeof(mute_str), mute);
+ int_to_str(keyboard_mute_str, sizeof(keyboard_mute_str), keyboard_mute);
+ int_to_str(mouse_mute_str, sizeof(mouse_mute_str), mouse_mute);
+ int_to_str(keyboard_enabled_str, sizeof(keyboard_enabled_str), keyboard_enabled);
+ int_to_str(mouse_enabled_str, sizeof(mouse_enabled_str), mouse_enabled);
+
+ execl(sound_player_path, "vbx-audio", "config.json", volume_str,
+ verbose_str, mute_str, mouse_config_path, mouse_volume_str,
+ keyboard_mute_str, mouse_mute_str, keyboard_enabled_str,
+ mouse_enabled_str, (char *)NULL);
+ perror("execl vbx-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/vbx-input", VBX_BIN_DIR);
+ if (keyboard_pid == 0) {
+ close(pipefd[0]);
+ dup2(pipefd[1], STDOUT_FILENO);
+ close(pipefd[1]);
+ execl(get_key_presses_path, "vbx-input", (char *)NULL);
+ perror("execl vbx-input");
+ exit(1);
+ }
+ close(pipefd[0]);
+ close(pipefd[1]);
+ return 1;
+}
+
+void handle_sighup(int sig) {
+ (void)sig;
+ reload_requested = 1;
+}
diff --git a/src/app/reload.c b/src/app/reload.c
new file mode 100644
index 0000000..7435646
--- /dev/null
+++ b/src/app/reload.c
@@ -0,0 +1,117 @@
+#include "app/reload.h"
+#include "app/process.h"
+#include "common/utils.h"
+#include "soundpacks.h"
+#include "user_config.h"
+#include
+#include
+#include
+
+int handle_reload(const char *user_cfg_path, char *current_sound_name,
+ char *current_mouse_sound_name, int *current_keyboard_volume,
+ int *current_mouse_volume, char *current_config_path,
+ char *current_sound_dir, char *current_mouse_config_path,
+ char *current_mouse_sound_dir, int current_mute,
+ int current_verbose, int current_mouse_volume_arg,
+ int current_keyboard_mute, int current_mouse_mute,
+ int *current_keyboard_enabled, int *current_mouse_enabled) {
+ char *cfg_keyboard_sound = NULL;
+ char *cfg_mouse_sound = NULL;
+ int cfg_keyboard_volume = *current_keyboard_volume;
+ int cfg_mouse_volume = *current_mouse_volume;
+ int cfg_keyboard_enabled = *current_keyboard_enabled;
+ int cfg_mouse_enabled = *current_mouse_enabled;
+ if (read_user_config(user_cfg_path, &cfg_keyboard_sound, &cfg_mouse_sound,
+ &cfg_keyboard_volume, &cfg_mouse_volume,
+ &cfg_keyboard_enabled, &cfg_mouse_enabled)) {
+ printf("Read config - keyboard:%s mouse:%s kvol:%d mvol:%d kenabled:%d "
+ "menabled:%d\n",
+ cfg_keyboard_sound ? cfg_keyboard_sound : "NULL",
+ cfg_mouse_sound ? cfg_mouse_sound : "NULL", cfg_keyboard_volume,
+ cfg_mouse_volume, cfg_keyboard_enabled, cfg_mouse_enabled);
+ int keyboard_changed = 0, mouse_changed = 0, keyboard_volume_changed = 0,
+ mouse_volume_changed = 0, keyboard_enabled_changed = 0,
+ mouse_enabled_changed = 0;
+ if (cfg_keyboard_sound &&
+ strcmp(current_sound_name, cfg_keyboard_sound) != 0) {
+ keyboard_changed = 1;
+ strncpy(current_sound_name, cfg_keyboard_sound, 1023);
+ current_sound_name[1023] = '\0';
+ }
+ if (cfg_mouse_sound &&
+ strcmp(current_mouse_sound_name, cfg_mouse_sound) != 0) {
+ mouse_changed = 1;
+ strncpy(current_mouse_sound_name, cfg_mouse_sound, 1023);
+ current_mouse_sound_name[1023] = '\0';
+ }
+ free(cfg_keyboard_sound);
+ free(cfg_mouse_sound);
+ if (cfg_keyboard_volume != *current_keyboard_volume) {
+ keyboard_volume_changed = 1;
+ *current_keyboard_volume = cfg_keyboard_volume;
+ }
+ if (cfg_mouse_volume != *current_mouse_volume) {
+ mouse_volume_changed = 1;
+ *current_mouse_volume = cfg_mouse_volume;
+ }
+ if (cfg_keyboard_enabled != *current_keyboard_enabled) {
+ keyboard_enabled_changed = 1;
+ *current_keyboard_enabled = cfg_keyboard_enabled;
+ }
+ if (cfg_mouse_enabled != *current_mouse_enabled) {
+ mouse_enabled_changed = 1;
+ *current_mouse_enabled = cfg_mouse_enabled;
+ }
+ printf("Config changed - keyboard:%d mouse:%d kvol:%d mvol:%d kenabled:%d "
+ "menabled:%d\n",
+ keyboard_changed, mouse_changed, keyboard_volume_changed,
+ mouse_volume_changed, keyboard_enabled_changed,
+ mouse_enabled_changed);
+ if (keyboard_changed || mouse_changed || keyboard_volume_changed ||
+ mouse_volume_changed || keyboard_enabled_changed ||
+ mouse_enabled_changed) {
+ // Persist enabled state to runtime files so the audio process picks it up
+ // immediately
+ write_runtime_keyboard_enabled_file(*current_keyboard_enabled);
+ write_runtime_mouse_enabled_file(*current_mouse_enabled);
+ int keyboard_valid = validate_keyboard_sound_pack(current_sound_name);
+ int mouse_valid = validate_mouse_sound_pack(current_mouse_sound_name);
+ if (keyboard_valid && mouse_valid) {
+ if (build_paths_for_keyboard_sound(current_sound_name,
+ current_config_path, 1024,
+ current_sound_dir, 1024)) {
+ if (build_paths_for_mouse_sound(current_mouse_sound_name,
+ current_mouse_config_path, 1024,
+ current_mouse_sound_dir, 1024)) {
+ stop_children();
+ start_children(current_sound_dir, current_config_path,
+ *current_keyboard_volume, current_verbose,
+ current_mute, current_mouse_sound_dir,
+ current_mouse_config_path, current_mouse_volume_arg,
+ current_keyboard_mute, current_mouse_mute,
+ *current_keyboard_enabled, *current_mouse_enabled);
+ printf("Reloaded successfully: keyboard=%s, mouse=%s, kvol=%d, "
+ "mvol=%d\n",
+ current_sound_name, current_mouse_sound_name,
+ *current_keyboard_volume, *current_mouse_volume);
+ return 1;
+ } else {
+ printf("Failed to build mouse sound paths\n");
+ }
+ } else {
+ printf("Failed to build keyboard sound paths\n");
+ }
+ } else {
+ if (!keyboard_valid)
+ printf("Reload failed: invalid keyboard sound pack '%s'\n",
+ current_sound_name);
+ if (!mouse_valid)
+ printf("Reload failed: invalid mouse sound pack '%s'\n",
+ current_mouse_sound_name);
+ }
+ }
+ } else {
+ printf("Failed to read user config\n");
+ }
+ return 0;
+}
diff --git a/src/app/watch.c b/src/app/watch.c
new file mode 100644
index 0000000..b9e56d4
--- /dev/null
+++ b/src/app/watch.c
@@ -0,0 +1,67 @@
+#include "app/watch.h"
+#include "common/utils.h"
+#include "app/process.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct inotify_thread_args {
+ char path[1024];
+};
+
+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) {
+ printf("Failed to initialize inotify: %s\n", strerror(errno));
+ return NULL;
+ }
+ int wd = inotify_add_watch(
+ fd, a->path, IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB | IN_MODIFY);
+ if (wd < 0) {
+ printf("Failed to add inotify watch for %s: %s\n", a->path,
+ strerror(errno));
+ close(fd);
+ return NULL;
+ }
+ printf("Watching config file: %s\n", a->path);
+ char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
+ while (1) {
+ ssize_t len = read(fd, buf, sizeof(buf));
+ if (len <= 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 100000000L;
+ nanosleep(&ts, NULL);
+ continue;
+ }
+ printf("inotify read error: %s\n", strerror(errno));
+ break;
+ }
+ printf("Config file changed, requesting reload...\n");
+ handle_sighup(0);
+ }
+ inotify_rm_watch(fd, wd);
+ close(fd);
+ return NULL;
+}
+
+int start_config_watcher(const char *path) {
+ static pthread_t inotify_thread;
+ static struct inotify_thread_args in_args;
+ safe_strncpy(in_args.path, path, sizeof(in_args.path));
+ if (access(in_args.path, F_OK) == 0) {
+ pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args);
+ pthread_detach(inotify_thread);
+ return 1;
+ } else {
+ printf("Warning: Config file %s does not exist, file watching disabled\n",
+ in_args.path);
+ return 0;
+ }
+}
diff --git a/src/audio/config.c b/src/audio/config.c
new file mode 100644
index 0000000..3d9758d
--- /dev/null
+++ b/src/audio/config.c
@@ -0,0 +1,244 @@
+#include "audio/types.h"
+#include "common/utils.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+SoundPack g_sound_pack = {0};
+SoundPack g_mouse_sound_pack = {0};
+float g_volume = 1.0f;
+float g_mouse_volume = 1.0f;
+int g_verbose = 0;
+int g_mute = 0;
+int g_keyboard_mute = 0;
+int g_mouse_mute = 0;
+
+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] == '/') {
+ safe_strncpy(buffer, filename, buffer_size);
+ } else {
+ if (!safe_snprintf(buffer, buffer_size, "%s/%s", base_dir, filename)) {
+ buffer[0] = '\0';
+ }
+ }
+}
+
+
+int load_sound_config(const char *config_path) {
+ FILE *file = fopen(config_path, "r");
+ if (!file) {
+ safe_fprintf(stderr, "Error: Cannot open sound pack config: %s\n", config_path);
+ perror("fopen");
+ return -1;
+ }
+ char config_path_copy[1024];
+ safe_strncpy(config_path_copy, config_path, sizeof(config_path_copy));
+ char *config_dir = dirname(config_path_copy);
+ fseek(file, 0, SEEK_END);
+ long size = ftell(file);
+ if (size < 0) {
+ fclose(file);
+ safe_fprintf(stderr, "Error: ftell failed while reading %s\n", config_path);
+ return -1;
+ }
+ errno = 0;
+ rewind(file);
+ if (errno != 0) {
+ fclose(file);
+ safe_fprintf(stderr, "Error: rewind failed while reading %s\n", config_path);
+ return -1;
+ }
+ char *json_string = malloc((size_t)size + 1);
+ if (!json_string) {
+ fclose(file);
+ safe_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) {
+ safe_fprintf(stderr, "Error: Invalid JSON in sound pack config\n");
+ safe_fprintf(stderr, "Check that the config.json file is properly formatted.\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;
+ if (g_verbose) {
+ 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);
+ if (g_verbose)
+ 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];
+ safe_strncpy(temp_pattern, pattern, sizeof(temp_pattern));
+ char *brace_start = strstr(temp_pattern, "{");
+ char *brace_end = strstr(temp_pattern, "}");
+ if (brace_start && brace_end) {
+ *brace_start = '%';
+ *(brace_start + 1) = 'd';
+ safe_memmove(brace_start + 2, brace_end + 1,
+ strlen(brace_end + 1) + 1);
+ }
+ safe_snprintf_wrapper(temp_filename, sizeof(temp_filename), temp_pattern, i);
+ } else {
+ safe_snprintf_wrapper(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 {
+ if (g_verbose)
+ 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;
+ if (g_verbose)
+ printf("Found single generic sound file: %s\n",
+ g_sound_pack.generic_press_files[0]);
+ }
+ }
+ if (g_verbose)
+ 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];
+ safe_strncpy(temp_release_file, json_object_get_string(obj),
+ sizeof(temp_release_file));
+ get_full_path(g_sound_pack.release_file,
+ sizeof(g_sound_pack.release_file), config_dir,
+ temp_release_file);
+ if (g_verbose)
+ 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;
+ if (strcmp(key, "MouseLeft") == 0) {
+ key_code = 272;
+ } else if (strcmp(key, "MouseRight") == 0) {
+ key_code = 273;
+ } else if (strcmp(key, "MouseMiddle") == 0) {
+ key_code = 274;
+ } else {
+ char *dash = strstr(key, "-up");
+ if (dash) {
+ char key_copy[16];
+ safe_strncpy(key_copy, key, dash - key + 1);
+ key_copy[dash - key] = '\0';
+ key_code = atoi(key_copy);
+ is_release = 1;
+ } else {
+ key_code = atoi(key);
+ }
+ }
+ if (key_code >= 0 && key_code < 512) {
+ const char *filename_relative = json_object_get_string(val);
+ char full_filename[1024];
+ 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 =
+ xstrdup(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 =
+ xstrdup(full_filename);
+ }
+ }
+ }
+ }
+ } else {
+ if (json_object_object_get_ex(root, "sound", &obj) ||
+ json_object_object_get_ex(root, "audio_file", &obj)) {
+ char temp_sound_file[256];
+ safe_strncpy(temp_sound_file, json_object_get_string(obj),
+ sizeof(temp_sound_file));
+ get_full_path(g_sound_pack.sound_file, sizeof(g_sound_pack.sound_file),
+ config_dir, temp_sound_file);
+ if (g_verbose)
+ printf("Single mode sound file: %s\n", g_sound_pack.sound_file);
+ }
+ json_object *defines_obj = NULL;
+ if (json_object_object_get_ex(root, "defines", &defines_obj) ||
+ json_object_object_get_ex(root, "definitions", &defines_obj)) {
+ json_object_object_foreach(defines_obj, key, val) {
+ int key_code;
+ if (strcmp(key, "MouseLeft") == 0) {
+ key_code = 272;
+ } else if (strcmp(key, "MouseRight") == 0) {
+ key_code = 273;
+ } else if (strcmp(key, "MouseMiddle") == 0) {
+ key_code = 274;
+ } else {
+ key_code = atoi(key);
+ }
+ if (key_code >= 0 && key_code < 512) {
+ if (json_object_is_type(val, json_type_array)) {
+ if (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));
+ }
+ } else if (json_object_is_type(val, json_type_object)) {
+ json_object *timing_array;
+ if (json_object_object_get_ex(val, "timing", &timing_array) &&
+ json_object_is_type(timing_array, json_type_array) &&
+ json_object_array_length(timing_array) > 0) {
+ json_object *first_timing =
+ json_object_array_get_idx(timing_array, 0);
+ if (json_object_is_type(first_timing, json_type_array) &&
+ json_object_array_length(first_timing) >= 2) {
+ g_sound_pack.key_mappings[key_code].start_ms =
+ json_object_get_int(
+ json_object_array_get_idx(first_timing, 0));
+ g_sound_pack.key_mappings[key_code].duration_ms =
+ json_object_get_int(
+ json_object_array_get_idx(first_timing, 1));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ json_object_put(root);
+ return 0;
+}
diff --git a/src/audio/main.c b/src/audio/main.c
new file mode 100644
index 0000000..d9f46b8
--- /dev/null
+++ b/src/audio/main.c
@@ -0,0 +1,191 @@
+#include "audio/playback.h"
+#include "audio/types.h"
+#include "common/utils.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+int g_keyboard_enabled = 1;
+int g_mouse_enabled = 1;
+
+int load_sound_config(const char *config_path);
+
+// Generic function to read runtime state files
+static int read_runtime_state(const char *filename_suffix, int default_value) {
+ char state_file[1024];
+ const char *rd = getenv("XDG_RUNTIME_DIR");
+ if (!rd || strlen(rd) == 0) {
+ rd = "/tmp";
+ }
+ if (!safe_snprintf(state_file, sizeof(state_file), "%s/vbx-%s-%d", rd,
+ filename_suffix, (int)getuid())) {
+ return default_value;
+ }
+ FILE *f = fopen(state_file, "r");
+ if (!f) {
+ return default_value;
+ }
+ int state = default_value;
+ if (safe_fscanf(f, "%d", &state) == 1) {
+ fclose(f);
+ return state;
+ }
+ fclose(f);
+ return default_value;
+}
+
+static int read_mute_state() {
+ return read_runtime_state("mute", 0);
+}
+
+static int read_keyboard_mute_state() {
+ return read_runtime_state("kbd-mute", 0);
+}
+
+static int read_mouse_mute_state() {
+ return read_runtime_state("mouse-mute", 0);
+}
+
+static int read_keyboard_enabled_state() {
+ return read_runtime_state("kbd-enabled", 1);
+}
+
+static int read_mouse_enabled_state() {
+ return read_runtime_state("mouse-enabled", 1);
+}
+
+int main(int argc, char *argv[]) {
+ if (argc < 2 || argc > 11) {
+ safe_fprintf(stderr,
+ "Usage: %s [volume] [verbose] [mute] [mouse_config] "
+ "[mouse_volume] [keyboard_mute] [mouse_mute] [keyboard_enabled] "
+ "[mouse_enabled]\n",
+ argv[0]);
+ safe_fprintf(stderr, " volume: 0-100 (default: 50)\n");
+ safe_fprintf(stderr, " verbose: 1 to enable verbose output (default: 0)\n");
+ safe_fprintf(stderr, " mute: 1 to mute all sound (default: 0)\n");
+ safe_fprintf(stderr, " mouse_config: path to mouse config.json (optional)\n");
+ safe_fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n");
+ safe_fprintf(stderr, " keyboard_mute: 1 to mute keyboard sounds (optional)\n");
+ safe_fprintf(stderr, " mouse_mute: 1 to mute mouse sounds (optional)\n");
+ safe_fprintf(stderr,
+ " keyboard_enabled: 1 to enable keyboard sounds (optional)\n");
+ safe_fprintf(stderr, " mouse_enabled: 1 to enable mouse sounds (optional)\n");
+ return 1;
+ }
+ if (argc >= 3) {
+ int volume_percent = validate_volume(atoi(argv[2]));
+ g_volume = volume_percent / 100.0f;
+ if (g_verbose)
+ printf("Volume set to: %d%%\n", volume_percent);
+ } else {
+ if (g_verbose)
+ printf("Volume set to: 50%% (default)\n");
+ }
+ if (argc >= 4) {
+ g_verbose = atoi(argv[3]);
+ if (g_verbose) {
+ printf("Verbose mode enabled\n");
+ }
+ }
+ if (argc >= 5) {
+ g_mute = atoi(argv[4]);
+ if (g_mute) {
+ printf("Sound muted\n");
+ }
+ }
+ if (argc >= 6) {
+ if (load_sound_config(argv[5]) != 0) {
+ safe_fprintf(stderr, "Failed to load mouse sound configuration\n");
+ return 1;
+ }
+ g_mouse_sound_pack = g_sound_pack;
+ if (g_verbose) {
+ printf("Mouse sound pack loaded from: %s\n", argv[5]);
+ }
+ }
+ if (argc >= 7) {
+ int mouse_volume_percent = validate_volume(atoi(argv[6]));
+ g_mouse_volume = mouse_volume_percent / 100.0f;
+ if (g_verbose)
+ printf("Mouse volume set to: %d%%\n", mouse_volume_percent);
+ }
+ if (argc >= 8) {
+ g_keyboard_mute = atoi(argv[7]);
+ if (g_verbose)
+ printf("Keyboard mute: %s\n", g_keyboard_mute ? "enabled" : "disabled");
+ }
+ if (argc >= 9) {
+ g_mouse_mute = atoi(argv[8]);
+ if (g_verbose)
+ printf("Mouse mute: %s\n", g_mouse_mute ? "enabled" : "disabled");
+ }
+ if (argc >= 10) {
+ g_keyboard_enabled = atoi(argv[9]);
+ if (g_verbose)
+ printf("Keyboard enabled: %s\n", g_keyboard_enabled ? "yes" : "no");
+ }
+ if (argc >= 11) {
+ g_mouse_enabled = atoi(argv[10]);
+ if (g_verbose)
+ printf("Mouse enabled: %s\n", g_mouse_enabled ? "yes" : "no");
+ }
+ if (load_sound_config(argv[1]) != 0) {
+ safe_fprintf(stderr, "Failed to load keyboard sound configuration\n");
+ return 1;
+ }
+ if (init_audio() != 0) {
+ safe_fprintf(stderr, "Failed to initialize audio\n");
+ return 1;
+ }
+ fd_set readfds;
+ struct timeval timeout;
+ char line[1024];
+ while (1) {
+ g_mute = read_mute_state();
+ g_keyboard_mute = read_keyboard_mute_state();
+ g_mouse_mute = read_mouse_mute_state();
+
+ g_keyboard_enabled = read_keyboard_enabled_state();
+ g_mouse_enabled = read_mouse_enabled_state();
+
+ 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) {
+ if (errno == EINTR) {
+ continue;
+ }
+ 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)) {
+ if (g_verbose)
+ 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);
+ }
+ }
+ }
+ return 0;
+}
diff --git a/src/audio/playback.c b/src/audio/playback.c
new file mode 100644
index 0000000..e6b7578
--- /dev/null
+++ b/src/audio/playback.c
@@ -0,0 +1,306 @@
+#include "audio/playback.h"
+#include "audio/types.h"
+#include "common/utils.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+pthread_t sound_threads[MAX_CONCURRENT_SOUNDS];
+volatile int thread_active[MAX_CONCURRENT_SOUNDS] = {0};
+pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+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 sound pack config\n");
+ fprintf(stderr, "Check that your sound pack has a valid config.json file.\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);
+ if (g_verbose) {
+ 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;
+
+ int is_mouse_event = (key_code == 272 || key_code == 273 || key_code == 274);
+ SoundPack *sound_pack = is_mouse_event ? &g_mouse_sound_pack : &g_sound_pack;
+ float volume = is_mouse_event ? g_mouse_volume : g_volume;
+ int mute_state = is_mouse_event ? g_mouse_mute : g_keyboard_mute;
+
+ if (g_verbose) {
+ printf("Thread %d: Playing %s sound for key %d (%s)\n", thread_id,
+ is_mouse_event ? "mouse" : "keyboard", key_code,
+ is_pressed ? "press" : "release");
+ }
+
+ if (g_mute || mute_state) {
+ if (g_verbose) {
+ printf("Thread %d: %s sound is muted\n", thread_id,
+ is_mouse_event ? "Mouse" : "Keyboard");
+ }
+ goto exit_cleanup;
+ }
+ if (sound_pack->is_multi) {
+ if (is_pressed && sound_pack->multi_key_mappings[key_code].press)
+ file_to_play = sound_pack->multi_key_mappings[key_code].press;
+ else if (!is_pressed && sound_pack->multi_key_mappings[key_code].release)
+ file_to_play = sound_pack->multi_key_mappings[key_code].release;
+ else if (is_pressed && sound_pack->num_generic_press_files > 0) {
+ int idx = rand() % sound_pack->num_generic_press_files;
+ file_to_play = sound_pack->generic_press_files[idx];
+ } else if (!is_pressed && strlen(sound_pack->release_file) > 0)
+ file_to_play = 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, "Error: Could not open sound file: %s\n", file_to_play);
+ fprintf(stderr, "Details: %s\n", sf_strerror(NULL));
+ fprintf(stderr, "Check that the audio file exists and is readable.\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) {
+ 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] * 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 >= 512 ||
+ 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 = &sound_pack->key_mappings[key_code];
+ SF_INFO sf_info = sound_pack->sf_info;
+ SNDFILE *sf = sf_open(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] * 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_mute) {
+ if (g_verbose) {
+ printf("Sound muted - ignoring key %d (%s)\n", key_code,
+ is_pressed ? "press" : "release");
+ }
+ return;
+ }
+
+ // Check if this is a mouse event and if mouse is disabled
+ int is_mouse_event = (key_code == 272 || key_code == 273 || key_code == 274);
+ if (is_mouse_event) {
+ // For mouse events, we need to check if mouse is enabled
+ // This will be handled by the main loop passing enabled state
+ // For now, we'll add a simple check here
+ extern int g_mouse_enabled;
+ if (!g_mouse_enabled) {
+ if (g_verbose) {
+ printf("Mouse sounds disabled - ignoring mouse event %d\n", key_code);
+ }
+ return;
+ }
+ } else {
+ // For keyboard events, check if keyboard is enabled
+ extern int g_keyboard_enabled;
+ if (!g_keyboard_enabled) {
+ if (g_verbose) {
+ printf("Keyboard sounds disabled - ignoring key %d\n", key_code);
+ }
+ return;
+ }
+ }
+ 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->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 = xstrdup(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;
+}
diff --git a/src/cli.c b/src/cli.c
new file mode 100644
index 0000000..61c26de
--- /dev/null
+++ b/src/cli.c
@@ -0,0 +1,217 @@
+#include "app/cli.h"
+#include "common/utils.h"
+#include "config.h"
+#include
+#include
+#include
+#include
+
+// Helper function to parse device-specific arguments
+static int parse_device_argument(const char *optarg, const char *action,
+ int *keyboard_value, int *mouse_value,
+ int value) {
+ if (optarg == NULL || strcmp(optarg, "both") == 0) {
+ *keyboard_value = value;
+ *mouse_value = value;
+ } else if (strcmp(optarg, "keyboard") == 0) {
+ *keyboard_value = value;
+ } else if (strcmp(optarg, "mouse") == 0) {
+ *mouse_value = value;
+ } else {
+ safe_fprintf(stderr, "Error: Invalid device '%s' for %s\n", optarg, action);
+ safe_fprintf(stderr, "Valid options: keyboard, mouse, or both\n");
+ return 1;
+ }
+ return 0;
+}
+
+// Helper function to validate and set volume
+static void set_volume(int *volume_ptr, int value) {
+ *volume_ptr = validate_volume(value);
+}
+
+void print_usage(const char *program_name) {
+ printf("VBX - Mechanical Keyboard Sound Simulator\n");
+ printf("Version: %s\n\n", PROJECT_VERSION);
+
+ printf("DESCRIPTION:\n");
+ printf(" VBX brings realistic mechanical keyboard and mouse sounds to every "
+ "keystroke.\n");
+ printf(" Run interactively or as a background daemon with live config "
+ "reload.\n\n");
+
+ printf("USAGE:\n");
+ printf(" %s [OPTIONS]\n\n", program_name);
+
+ printf("SOUND PACKS:\n");
+ printf(" -S, --sound PACK Choose keyboard sound pack\n");
+ printf(" -M, --mouse PACK Choose mouse sound pack\n");
+ printf(" -l, --list Show available sound packs\n\n");
+
+ printf("VOLUME CONTROL:\n");
+ printf(" -V, --volume LEVEL Set volume for both devices [0-100]\n");
+ printf(" -K, --keyboard-volume Set keyboard volume only [0-100]\n");
+ printf(" -O, --mouse-volume Set mouse volume only [0-100]\n\n");
+
+ printf("AUDIO CONTROL:\n");
+ printf(" -m, --mute[=DEVICE] Mute audio (keyboard|mouse|both)\n");
+ printf(" -u, --unmute[=DEVICE] Unmute audio (keyboard|mouse|both)\n");
+ printf(" --enable[=DEVICE] Enable device sounds "
+ "(keyboard|mouse|both)\n");
+ printf(" --disable[=DEVICE] Disable device sounds "
+ "(keyboard|mouse|both)\n\n");
+
+ printf("DAEMON MODE:\n");
+ printf(" -d, --daemon Run in background with auto-reload\n");
+ printf(" -s, --stop Stop background daemon\n\n");
+
+ printf("OTHER OPTIONS:\n");
+ printf(" -v, --verbose Show detailed output\n");
+ printf(" -h, --help Show this help message\n\n");
+
+ printf("QUICK EXAMPLES:\n");
+ printf(
+ " %s --list # Browse sound packs\n",
+ program_name);
+ printf(" %s -S cherrymx-blue -V 75 # Cherry MX Blue at "
+ "75%%\n",
+ program_name);
+ printf(" %s --daemon # Start background "
+ "service\n",
+ program_name);
+ printf(" %s --mute keyboard # Mute keyboard only\n",
+ program_name);
+ printf(
+ " %s --disable mouse # Disable mouse sounds\n",
+ program_name);
+ printf(" %s --keyboard-volume 80 --mouse-volume 60 # Different volumes\n",
+ program_name);
+ printf("\n");
+ printf("CONFIGURATION:\n");
+ printf(
+ " Settings are saved to ~/.vbx.json and auto-reload in daemon mode.\n");
+ printf(" Use --list to see available sound packs and their sources.\n");
+}
+
+int parse_cli(int argc, char **argv, CliOptions *out) {
+ safe_memset(out, 0, sizeof(*out));
+ out->volume = -1;
+ out->keyboard_volume = -1;
+ out->mouse_volume = -1;
+ out->keyboard_mute = -1;
+ out->mouse_mute = -1;
+ out->keyboard_enabled = -1;
+ out->mouse_enabled = -1;
+ static struct option long_options[] = {
+ {"sound", required_argument, 0, 'S'},
+ {"mouse", required_argument, 0, 'M'},
+ {"volume", required_argument, 0, 'V'},
+ {"keyboard-volume", required_argument, 0, 'K'},
+ {"mouse-volume", required_argument, 0, 'O'},
+ {"mute", optional_argument, 0, 'm'},
+ {"unmute", optional_argument, 0, 'u'},
+ {"enable", optional_argument, 0, 1000},
+ {"disable", optional_argument, 0, 1001},
+ {"list", no_argument, 0, 'l'},
+ {"daemon", no_argument, 0, 'd'},
+ {"stop", no_argument, 0, 's'},
+ {"help", no_argument, 0, 'h'},
+ {"verbose", no_argument, 0, 'v'},
+ {0, 0, 0, 0}};
+ for (int i = 1; i < argc; i++) {
+ if (argv[i][0] == '-' && argv[i][1] == '-') {
+ const char *a = argv[i] + 2;
+ const char *e = strchr(a, '=');
+ size_t n = e ? (size_t)(e - a) : strlen(a);
+ if (!((n == 5 && strncmp(a, "sound", 5) == 0) ||
+ (n == 5 && strncmp(a, "mouse", 5) == 0) ||
+ (n == 6 && strncmp(a, "volume", 6) == 0) ||
+ (n == 4 && strncmp(a, "list", 4) == 0) ||
+ (n == 6 && strncmp(a, "daemon", 6) == 0) ||
+ (n == 4 && strncmp(a, "stop", 4) == 0) ||
+ (n == 4 && strncmp(a, "mute", 4) == 0) ||
+ (n == 6 && strncmp(a, "unmute", 6) == 0) ||
+ (n == 6 && strncmp(a, "enable", 6) == 0) ||
+ (n == 7 && strncmp(a, "disable", 7) == 0) ||
+ (n == 4 && strncmp(a, "help", 4) == 0) ||
+ (n == 7 && strncmp(a, "verbose", 7) == 0))) {
+ safe_fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]);
+ safe_fprintf(stderr, "Run '%s --help' to see available options.\n",
+ argv[0]);
+ return 1;
+ }
+ }
+ }
+ optind = 1;
+ int opt;
+ while ((opt = getopt_long(argc, argv, "S:M:V:K:O:lhdsm::u::v", long_options,
+ NULL)) != -1) {
+ switch (opt) {
+ case 'd':
+ out->daemon_flag = 1;
+ break;
+ case 's':
+ out->stop_flag = 1;
+ break;
+ case 'S':
+ out->sound = optarg;
+ break;
+ case 'M':
+ out->mouse_sound = optarg;
+ break;
+ case 'V':
+ set_volume(&out->volume, atoi(optarg));
+ break;
+ case 'K':
+ set_volume(&out->keyboard_volume, atoi(optarg));
+ break;
+ case 'O':
+ set_volume(&out->mouse_volume, atoi(optarg));
+ break;
+ case 'm':
+ if (parse_device_argument(optarg, "mute", &out->keyboard_mute,
+ &out->mouse_mute, 1) != 0) {
+ return 1;
+ }
+ break;
+ case 'u':
+ if (parse_device_argument(optarg, "unmute", &out->keyboard_mute,
+ &out->mouse_mute, 0) != 0) {
+ return 1;
+ }
+ break;
+ case 1000: // --enable
+ case 1001: // --disable
+ {
+ int enable_value = (opt == 1000) ? 1 : 0;
+ const char *action = (opt == 1000) ? "enable" : "disable";
+
+ if (optarg == NULL) {
+ // Handle space-separated argument: --enable mouse
+ if (optind < argc && argv[optind][0] != '-') {
+ optarg = argv[optind++];
+ } else {
+ optarg = "both"; // default to both if no argument
+ }
+ }
+ if (parse_device_argument(optarg, action, &out->keyboard_enabled,
+ &out->mouse_enabled, enable_value) != 0) {
+ return 1;
+ }
+ } break;
+ case 'l':
+ out->list_flag = 1;
+ break;
+ case 'h':
+ print_usage(argv[0]);
+ return 2; // signal printed help
+ case 'v':
+ out->verbose = 1;
+ break;
+ default:
+ print_usage(argv[0]);
+ return 1;
+ }
+ }
+ return 0;
+}
diff --git a/src/common/utils.c b/src/common/utils.c
new file mode 100644
index 0000000..a4d73d1
--- /dev/null
+++ b/src/common/utils.c
@@ -0,0 +1,219 @@
+#define _POSIX_C_SOURCE 200809L
+#include "common/utils.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+int read_runtime_mute_file() {
+ char mute_file[1024];
+ const char *rd = getenv("XDG_RUNTIME_DIR");
+ if (!rd || strlen(rd) == 0)
+ rd = "/tmp";
+ if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/vbx-mute-%d", rd,
+ (int)getuid()))
+ return 0;
+ FILE *f = fopen(mute_file, "r");
+ if (!f)
+ return 0;
+ int mute = 0;
+ if (safe_fscanf(f, "%d", &mute) == 1) {
+ fclose(f);
+ return mute;
+ }
+ fclose(f);
+ return 0;
+}
+
+// Generic helper to write runtime state files
+static void write_runtime_state_file(const char *filename_suffix, int value) {
+ char state_file[1024];
+ const char *rd = getenv("XDG_RUNTIME_DIR");
+ if (!rd || strlen(rd) == 0)
+ rd = "/tmp";
+ if (!safe_snprintf(state_file, sizeof(state_file), "%s/vbx-%s-%d", rd,
+ filename_suffix, (int)getuid()))
+ return;
+ FILE *f = fopen(state_file, "w");
+ if (!f)
+ return;
+ safe_fprintf(f, "%d\n", value);
+ fclose(f);
+}
+
+void write_runtime_mute_file(int mute) {
+ write_runtime_state_file("mute", mute ? 1 : 0);
+}
+
+void write_runtime_keyboard_mute_file(int mute) {
+ write_runtime_state_file("kbd-mute", mute ? 1 : 0);
+}
+
+void write_runtime_mouse_mute_file(int mute) {
+ write_runtime_state_file("mouse-mute", mute ? 1 : 0);
+}
+
+int safe_snprintf(char *dst, size_t dst_sz, const char *fmt, ...) {
+ if (!dst || dst_sz == 0)
+ return 0;
+ va_list ap;
+ va_start(ap, fmt);
+ int n = vsnprintf(dst, dst_sz, fmt, ap);
+ va_end(ap);
+ return (n >= 0 && (size_t)n < dst_sz);
+}
+
+int errorf(const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ return 1;
+}
+
+const char *get_runtime_dir(void) {
+ const char *rd = getenv("XDG_RUNTIME_DIR");
+ if (rd && strlen(rd) > 0)
+ return rd;
+ return "/tmp";
+}
+
+void build_pidfile_path(char *buffer, size_t buflen) {
+ const char *rd = get_runtime_dir();
+ safe_snprintf(buffer, buflen, "%s/vbx-%d.pid", rd, (int)getuid());
+}
+
+int read_pidfile(const char *path, pid_t *out_pid) {
+ FILE *f = fopen(path, "r");
+ if (!f)
+ return 0;
+ long val = -1;
+ if (safe_fscanf(f, "%ld", &val) == 1 && val > 0) {
+ *out_pid = (pid_t)val;
+ fclose(f);
+ return 1;
+ }
+ fclose(f);
+ return 0;
+}
+
+int write_pidfile(const char *path, pid_t pid) {
+ FILE *f = fopen(path, "w");
+ if (!f)
+ return 0;
+ safe_fprintf(f, "%ld\n", (long)pid);
+ fclose(f);
+ return 1;
+}
+
+int process_is_running(pid_t pid) {
+ if (pid <= 0)
+ return 0;
+ return kill(pid, 0) == 0;
+}
+
+int write_runtime_keyboard_enabled_file(int enabled) {
+ write_runtime_state_file("kbd-enabled", enabled);
+ return 1;
+}
+
+int write_runtime_mouse_enabled_file(int enabled) {
+ write_runtime_state_file("mouse-enabled", enabled);
+ return 1;
+}
+
+
+void int_to_str(char *buffer, size_t size, int value) {
+ safe_snprintf_wrapper(buffer, size, "%d", value);
+}
+
+// Safe string duplication
+char *xstrdup(const char *s) {
+ if (!s)
+ return NULL;
+ size_t n = strlen(s) + 1;
+ char *p = (char *)malloc(n);
+ if (!p)
+ return NULL;
+ safe_memcpy(p, s, n);
+ return p;
+}
+
+// Validate and clamp volume to 0-100 range
+int validate_volume(int volume) {
+ if (volume < 0) return 0;
+ if (volume > 100) return 100;
+ return volume;
+}
+
+// Get HOME directory with fallback to passwd
+const char *get_home_dir(void) {
+ const char *home = getenv("HOME");
+ if (!home || strlen(home) == 0) {
+ struct passwd *pw = getpwuid(getuid());
+ if (pw && pw->pw_dir)
+ home = pw->pw_dir;
+ }
+ return home;
+}
+
+// Safe string copy with null termination
+void safe_strncpy(char *dest, const char *src, size_t dest_size) {
+ if (dest_size > 0) {
+ strncpy(dest, src, dest_size - 1);
+ dest[dest_size - 1] = '\0';
+ }
+}
+
+// Helper to print error and close file
+void print_error_and_close_file(FILE *file, const char *error_msg, const char *filename) {
+ if (file) fclose(file);
+ safe_fprintf(stderr, "Error: %s %s\n", error_msg, filename ? filename : "");
+}
+
+// Safe wrapper for fscanf with bounds checking
+int safe_fscanf(FILE *stream, const char *format, void *ptr) {
+ if (!stream || !format || !ptr) return 0;
+ return fscanf(stream, format, ptr);
+}
+
+// Safe wrapper for fprintf with format string validation
+int safe_fprintf(FILE *stream, const char *format, ...) {
+ if (!stream || !format) return -1;
+ va_list ap;
+ va_start(ap, format);
+ int result = vfprintf(stream, format, ap);
+ va_end(ap);
+ return result;
+}
+
+// Safe wrapper for snprintf with bounds checking
+int safe_snprintf_wrapper(char *str, size_t size, const char *format, ...) {
+ if (!str || size == 0 || !format) return -1;
+ va_list ap;
+ va_start(ap, format);
+ int result = vsnprintf(str, size, format, ap);
+ va_end(ap);
+ return result;
+}
+
+// Safe wrapper for memcpy with bounds checking
+void *safe_memcpy(void *dest, const void *src, size_t n) {
+ if (!dest || !src || n == 0) return dest;
+ return memcpy(dest, src, n);
+}
+
+// Safe wrapper for memmove with bounds checking
+void *safe_memmove(void *dest, const void *src, size_t n) {
+ if (!dest || !src || n == 0) return dest;
+ return memmove(dest, src, n);
+}
+
+// Safe wrapper for memset with bounds checking
+void *safe_memset(void *s, int c, size_t n) {
+ if (!s || n == 0) return s;
+ return memset(s, c, n);
+}
diff --git a/src/config.c b/src/config.c
new file mode 100644
index 0000000..c1ab28f
--- /dev/null
+++ b/src/config.c
@@ -0,0 +1,141 @@
+#include "common/utils.h"
+#include "user_config.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+char *get_user_config_path(char *buffer, size_t buflen) {
+ const char *home = get_home_dir();
+ if (!home)
+ return NULL;
+ if (!safe_snprintf(buffer, buflen, "%s/%s", home, ".vbx.json"))
+ return NULL;
+ return buffer;
+}
+
+int write_user_config(const char *path, const char *keyboard_sound,
+ const char *mouse_sound, int keyboard_volume,
+ int mouse_volume, int keyboard_enabled,
+ int mouse_enabled) {
+ json_object *root = json_object_new_object();
+ if (!root)
+ return 0;
+
+ json_object *keyboard = json_object_new_object();
+ json_object_object_add(keyboard, "enabled",
+ json_object_new_boolean(keyboard_enabled));
+ json_object_object_add(keyboard, "keyboard_sound",
+ json_object_new_string(keyboard_sound));
+ json_object_object_add(keyboard, "volume",
+ json_object_new_int(keyboard_volume));
+ json_object_object_add(root, "keyboard", keyboard);
+
+ json_object *mouse = json_object_new_object();
+ json_object_object_add(mouse, "enabled",
+ json_object_new_boolean(mouse_enabled));
+ json_object_object_add(mouse, "mouse_sound",
+ json_object_new_string(mouse_sound));
+ json_object_object_add(mouse, "volume", json_object_new_int(mouse_volume));
+ json_object_object_add(root, "mouse", mouse);
+
+ 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;
+ }
+ safe_fprintf(f, "%s\n", json_str);
+ fclose(f);
+ json_object_put(root);
+ return 1;
+}
+
+int read_user_config(const char *path, char **out_keyboard_sound,
+ char **out_mouse_sound, int *out_keyboard_volume,
+ int *out_mouse_volume, int *out_keyboard_enabled,
+ int *out_mouse_enabled) {
+ FILE *f = fopen(path, "r");
+ if (!f)
+ return 0;
+ fseek(f, 0, SEEK_END);
+ long sz = ftell(f);
+ if (sz < 0) {
+ fclose(f);
+ return 0;
+ }
+ errno = 0;
+ rewind(f);
+ if (errno != 0) {
+ fclose(f);
+ return 0;
+ }
+ char *buf = malloc((size_t)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 *keyboard_obj, *mouse_obj;
+ if (json_object_object_get_ex(root, "keyboard", &keyboard_obj) &&
+ json_object_object_get_ex(root, "mouse", &mouse_obj)) {
+ json_object *o;
+ if (json_object_object_get_ex(keyboard_obj, "enabled", &o)) {
+ *out_keyboard_enabled = json_object_get_boolean(o);
+ } else {
+ *out_keyboard_enabled = 1; // default enabled
+ }
+ if (json_object_object_get_ex(mouse_obj, "enabled", &o)) {
+ *out_mouse_enabled = json_object_get_boolean(o);
+ } else {
+ *out_mouse_enabled = 1; // default enabled
+ }
+ if (json_object_object_get_ex(keyboard_obj, "keyboard_sound", &o)) {
+ const char *s = json_object_get_string(o);
+ if (s)
+ *out_keyboard_sound = xstrdup(s);
+ }
+ if (json_object_object_get_ex(mouse_obj, "mouse_sound", &o)) {
+ const char *s = json_object_get_string(o);
+ if (s)
+ *out_mouse_sound = xstrdup(s);
+ }
+ if (json_object_object_get_ex(keyboard_obj, "volume", &o)) {
+ *out_keyboard_volume = json_object_get_int(o);
+ }
+ if (json_object_object_get_ex(mouse_obj, "volume", &o)) {
+ *out_mouse_volume = json_object_get_int(o);
+ }
+ } else {
+ // Legacy format - default to enabled
+ *out_keyboard_enabled = 1;
+ *out_mouse_enabled = 1;
+ json_object *o;
+ if (json_object_object_get_ex(root, "sound", &o)) {
+ const char *s = json_object_get_string(o);
+ if (s) {
+ *out_keyboard_sound = xstrdup(s);
+ *out_mouse_sound = xstrdup("ping");
+ }
+ }
+ if (json_object_object_get_ex(root, "volume", &o)) {
+ int legacy_volume = json_object_get_int(o);
+ *out_keyboard_volume = legacy_volume;
+ *out_mouse_volume = legacy_volume;
+ }
+ }
+ json_object_put(root);
+ return 1;
+}
diff --git a/src/keyvibe_input.c b/src/input.c
similarity index 99%
rename from src/keyvibe_input.c
rename to src/input.c
index e70fdbf..3e505f7 100644
--- a/src/keyvibe_input.c
+++ b/src/input.c
@@ -1,5 +1,11 @@
-#include "utils.h"
+#include "common/utils.h"
#include
+#include
+#include
+#include
+#include
+#include
+
#include
#include
#include
@@ -7,11 +13,6 @@
#include
#include
#include
-#include
-#include
-#include
-#include
-#include
#include
#include "config.h"
diff --git a/src/keyvibe_audio.c b/src/keyvibe_audio.c
deleted file mode 100644
index 6683a60..0000000
--- a/src/keyvibe_audio.c
+++ /dev/null
@@ -1,623 +0,0 @@
-
-#define _GNU_SOURCE
-#include "utils.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#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;
- int thread_id;
- int is_pressed;
-} PlaybackData;
-
-SoundPack g_sound_pack = {0};
-float g_volume = 1.0f;
-int g_verbose = 0;
-int g_mute = 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 {
- if (!safe_snprintf(buffer, buffer_size, "%s/%s", base_dir, filename)) {
- buffer[0] = '\0';
- }
- }
-}
-
-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);
- if (size < 0) {
- fclose(file);
- fprintf(stderr, "Error: ftell failed while reading %s\n", config_path);
- return -1;
- }
- errno = 0;
- rewind(file);
- if (errno != 0) {
- fclose(file);
- fprintf(stderr, "Error: rewind failed while reading %s\n", config_path);
- return -1;
- }
- char *json_string = malloc((size_t)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;
- if (g_verbose) {
- 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);
- if (g_verbose)
- 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 {
- if (g_verbose)
- 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;
- if (g_verbose)
- printf("Found single generic sound file: %s\n",
- g_sound_pack.generic_press_files[0]);
- }
- }
- if (g_verbose)
- 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);
- if (g_verbose)
- 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);
- if (g_verbose)
- 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);
- if (g_verbose) {
- 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_mute) {
- if (g_verbose) {
- printf("Sound muted - ignoring key %d (%s)\n", key_code,
- is_pressed ? "press" : "release");
- }
- return;
- }
- 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->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() {
- if (g_verbose)
- 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);
-}
-
-static int read_mute_state() {
- char mute_file[MAX_LINE_LENGTH];
- const char *rd = getenv("XDG_RUNTIME_DIR");
- if (!rd || strlen(rd) == 0) {
- rd = "/tmp";
- }
- if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/keyvibe-mute-%d", rd,
- (int)getuid())) {
- return 0;
- }
-
- FILE *f = fopen(mute_file, "r");
- if (!f) {
- return 0; // Default to unmuted if file doesn't exist
- }
-
- int mute = 0;
- if (fscanf(f, "%d", &mute) == 1) {
- fclose(f);
- return mute;
- }
- fclose(f);
- return 0;
-}
-
-int main(int argc, char *argv[]) {
- if (argc < 2 || argc > 5) {
- fprintf(stderr, "Usage: %s [volume] [verbose] [mute]\n",
- argv[0]);
- fprintf(stderr, " volume: 0-100 (default: 50)\n");
- fprintf(stderr, " verbose: 1 to enable verbose output (default: 0)\n");
- fprintf(stderr, " mute: 1 to mute sound (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;
- if (g_verbose)
- printf("Volume set to: %d%%\n", volume_percent);
- } else {
- if (g_verbose)
- printf("Volume set to: 50%% (default)\n");
- }
- if (argc >= 4) {
- g_verbose = atoi(argv[3]);
- if (g_verbose) {
- printf("Verbose mode enabled\n");
- }
- }
- if (argc >= 5) {
- g_mute = atoi(argv[4]);
- if (g_mute) {
- printf("Sound muted\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;
- }
- // Remove signal handlers - we'll use file-based approach instead
- fd_set readfds;
- struct timeval timeout;
- char line[MAX_LINE_LENGTH];
- while (1) {
- // Check mute state from file periodically
- g_mute = read_mute_state();
-
- 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) {
- if (errno == EINTR) {
- // Signal interrupted select, continue
- continue;
- }
- 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)) {
- if (g_verbose)
- 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;
-}
diff --git a/src/keyvibe_main.c b/src/keyvibe_main.c
deleted file mode 100644
index b782cd9..0000000
--- a/src/keyvibe_main.c
+++ /dev/null
@@ -1,793 +0,0 @@
-#define _POSIX_C_SOURCE 200809L
-
-#include "config.h"
-#include "utils.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#define MAX_PATH_LENGTH 1024
-#define AUDIO_BASE_DIR KeyVibe_DATA_DIR "/audio"
-#define USER_AUDIO_SUBPATH "/.local/share/keyvibe/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};
-static int current_mute = 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(" -l, --list List available sound packs\n");
- printf(" -d, --daemon Run in background (write PID file)\n");
- printf(" -s, --stop Stop background daemon\n");
- printf(" -m, --mute Mute sound\n");
- printf(" -u, --unmute Unmute sound\n");
- printf(" -h, --help Show this help message\n");
- printf(" -v, --verbose Enable verbose output\n");
- printf(" In daemon mode, editing ~/.keyvibe.json will auto-reload.\n");
-}
-
-int list_sound_packs() {
- DIR *dir;
- struct dirent *entry;
- char path[MAX_PATH_LENGTH];
- struct stat st;
-
- // Determine user audio dir
- char user_audio_dir[MAX_PATH_LENGTH] = {0};
- 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) {
- snprintf(user_audio_dir, sizeof(user_audio_dir), "%s%s", home,
- USER_AUDIO_SUBPATH);
- }
-
- printf("Available sound packs:\n");
- printf("======================\n");
-
- // List user packs first (if available)
- if (user_audio_dir[0] && (dir = opendir(user_audio_dir)) != NULL) {
- while ((entry = readdir(dir)) != NULL) {
- if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
- continue;
- if ((size_t)snprintf(path, sizeof(path), "%s/%s", user_audio_dir,
- entry->d_name) >= sizeof(path)) {
- fprintf(stderr, "Warning: Path too long, skipping: %s/%s\n",
- user_audio_dir, entry->d_name);
- continue;
- }
- if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
- printf(" %s (user)\n", entry->d_name);
- }
- }
- closedir(dir);
- }
-
- // Then list system packs
- dir = opendir(AUDIO_BASE_DIR);
- if (dir != NULL) {
- while ((entry = readdir(dir)) != NULL) {
- if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
- continue;
- if ((size_t)snprintf(path, sizeof(path), "%s/%s", AUDIO_BASE_DIR,
- entry->d_name) >= sizeof(path)) {
- fprintf(stderr, "Warning: Path too long, skipping: %s/%s\n",
- AUDIO_BASE_DIR, entry->d_name);
- continue;
- }
- if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
- printf(" %s (system)\n", entry->d_name);
- }
- }
- closedir(dir);
- }
-
- return 0;
-}
-
-static void get_user_audio_dir(char *buffer, size_t buflen) {
- buffer[0] = '\0';
- 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)
- snprintf(buffer, buflen, "%s%s", home, USER_AUDIO_SUBPATH);
-}
-
-static int resolve_sound_base_dir(const char *sound_name, char *out_basedir,
- size_t out_sz) {
- char user_audio_dir[MAX_PATH_LENGTH];
- get_user_audio_dir(user_audio_dir, sizeof(user_audio_dir));
- if (user_audio_dir[0]) {
- char p[MAX_PATH_LENGTH];
- if ((size_t)snprintf(p, sizeof(p), "%s/%s/config.json", user_audio_dir,
- sound_name) < sizeof(p) &&
- access(p, R_OK) == 0) {
- strncpy(out_basedir, user_audio_dir, out_sz - 1);
- out_basedir[out_sz - 1] = '\0';
- return 1;
- }
- }
- // Fallback to system dir
- char p2[MAX_PATH_LENGTH];
- if ((size_t)snprintf(p2, sizeof(p2), "%s/%s/config.json", AUDIO_BASE_DIR,
- sound_name) >= sizeof(p2)) {
- return 0; // Path too long
- }
- if (access(p2, R_OK) == 0) {
- strncpy(out_basedir, AUDIO_BASE_DIR, out_sz - 1);
- out_basedir[out_sz - 1] = '\0';
- return 1;
- }
- return 0;
-}
-
-int validate_sound_pack(const char *sound_name) {
- char basedir[MAX_PATH_LENGTH];
- if (!resolve_sound_base_dir(sound_name, basedir, sizeof(basedir))) {
- fprintf(stderr,
- "Error: Sound pack '%s' not found in user or system dirs.\n",
- sound_name);
- fprintf(stderr, "Use --list to see available sound packs.\n");
- return 0;
- }
- char config_path[MAX_PATH_LENGTH];
- if ((size_t)snprintf(config_path, sizeof(config_path), "%s/%s/config.json",
- basedir, sound_name) >= sizeof(config_path)) {
- fprintf(stderr, "Error: Sound pack path too long: %s/%s\n", basedir,
- sound_name);
- 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);
-}
-
-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;
- if (!safe_snprintf(buffer, buflen, "%s/%s", home, USER_CONFIG_FILENAME))
- return NULL;
- return buffer;
-}
-
-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) {
- char basedir[MAX_PATH_LENGTH];
- if (!resolve_sound_base_dir(sound_name, basedir, sizeof(basedir))) {
- return 0;
- }
- size_t base_len = strlen(basedir);
- 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;
- }
- if (!safe_snprintf(out_config_path, out_config_sz, "%s/%s%s", basedir,
- sound_name, cfg_suffix))
- return 0;
- if (!safe_snprintf(out_sound_dir, out_sound_sz, "%s/%s", basedir, sound_name))
- return 0;
- return 1;
-}
-
-// Helper: ensure daemon is running, return its pid or print a consistent error
-static int require_running_pid(pid_t *out_pid) {
- 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 0;
- }
- *out_pid = running_pid;
- return 1;
-}
-
-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, int mute) {
- (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);
- char mute_str[8];
- snprintf(mute_str, sizeof(mute_str), "%d", mute);
- execl(sound_player_path, "keyvibe-audio", "config.json", volume_str,
- verbose_str, mute_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;
-}
-
-static void write_mute_state(int mute) {
- char mute_file[MAX_PATH_LENGTH];
- const char *rd = get_runtime_dir();
- snprintf(mute_file, sizeof(mute_file), "%s/keyvibe-mute-%d", rd,
- (int)getuid());
-
- FILE *f = fopen(mute_file, "w");
- if (f) {
- fprintf(f, "%d\n", mute);
- fclose(f);
- }
-}
-
-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);
- if (sz < 0) {
- fclose(f);
- return 0;
- }
- errno = 0;
- rewind(f);
- if (errno != 0) {
- fclose(f);
- return 0;
- }
- char *buf = malloc((size_t)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"; // may be replaced by strdup; track for free
- int sound_name_owned = 0;
- 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'},
- {"list", no_argument, 0, 'l'},
- {"daemon", no_argument, 0, 'd'},
- {"stop", no_argument, 0, 's'},
- {"mute", no_argument, 0, 'm'},
- {"unmute", no_argument, 0, 'u'},
- {"help", no_argument, 0, 'h'},
- {"verbose", no_argument, 0, 'v'},
- {0, 0, 0, 0}};
- int volume = 50;
- 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; // now owned, must free on exit paths
- sound_name_owned = 1;
- }
- volume = cfg_volume;
- }
- }
- }
- for (int i = 1; i < argc; i++) {
- if (argv[i][0] == '-' && argv[i][1] == '-') {
- const char *a = argv[i] + 2;
- const char *e = strchr(a, '=');
- size_t n = e ? (size_t)(e - a) : strlen(a);
- if (!((n == 5 && strncmp(a, "sound", 5) == 0) ||
- (n == 6 && strncmp(a, "volume", 6) == 0) ||
- (n == 4 && strncmp(a, "list", 4) == 0) ||
- (n == 6 && strncmp(a, "daemon", 6) == 0) ||
- (n == 4 && strncmp(a, "stop", 4) == 0) ||
- (n == 4 && strncmp(a, "mute", 4) == 0) ||
- (n == 6 && strncmp(a, "unmute", 6) == 0) ||
- (n == 4 && strncmp(a, "help", 4) == 0) ||
- (n == 7 && strncmp(a, "verbose", 7) == 0))) {
- fprintf(stderr, "Unknown option: %s (use full option name)\n", argv[i]);
- return 1;
- }
- }
- }
- int opt;
- while ((opt = getopt_long(argc, argv, "S:V:lhdsmuv", long_options, NULL)) !=
- -1) {
- switch (opt) {
- case 'd':
- flag_daemon = 1;
- break;
- case 's':
- flag_stop = 1;
- break;
- case 'm': {
- pid_t running_pid = 0;
- if (!require_running_pid(&running_pid)) {
- if (sound_name_owned) {
- free(sound_name);
- }
- return 1;
- }
- write_mute_state(1);
- printf("KeyVibe muted.\n");
- if (sound_name_owned) {
- free(sound_name);
- }
- return 0;
- }
- case 'u': {
- pid_t running_pid = 0;
- if (!require_running_pid(&running_pid)) {
- if (sound_name_owned) {
- free(sound_name);
- }
- return 1;
- }
- write_mute_state(0);
- printf("KeyVibe unmuted.\n");
- if (sound_name_owned) {
- free(sound_name);
- }
- return 0;
- }
- 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 'l':
- list_sounds = 1;
- break;
- /* removed legacy long mute/unmute cases; use -m/-u or --mute/--unmute */
- case 'h':
- print_usage(argv[0]);
- if (sound_name_owned) {
- free(sound_name);
- }
- return 0;
- case 'v':
- verbose = 1;
- break;
- default:
- print_usage(argv[0]);
- if (sound_name_owned) {
- free(sound_name);
- }
- return 1;
- }
- }
- if (list_sounds) {
- int rc = list_sound_packs();
- if (sound_name_owned) {
- free(sound_name);
- }
- return rc;
- }
- build_pidfile_path(pidfile_path, sizeof(pidfile_path));
- if (flag_stop) {
- pid_t running_pid = 0;
- if (!require_running_pid(&running_pid))
- return 1;
- if (kill(running_pid, SIGTERM) != 0)
- return (perror("kill"), 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);
- }
- return errorf("KeyVibe: process did not stop in time\n");
- }
- // Apply CLI options and update config file if needed
- int config_updated = 0;
- if (cli_sound != NULL) {
- if (sound_name_owned) {
- free(sound_name);
- }
- sound_name = cli_sound;
- sound_name_owned = 0; // cli_sound is not owned by us
- config_updated = 1;
- }
- if (cli_volume >= 0) {
- volume = cli_volume;
- config_updated = 1;
- }
-
- // Update config file if CLI options were used
- if (config_updated &&
- get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
- if (!write_user_config(user_cfg_path, sound_name, volume)) {
- fprintf(stderr, "Warning: Failed to update config file %s\n",
- user_cfg_path);
- } else if (verbose) {
- fprintf(stderr, "Updated config file %s\n", user_cfg_path);
- }
- }
- // Create default config file if it doesn't exist and no CLI options were used
- if (!config_updated &&
- 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)) {
- if (sound_name_owned) {
- free(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 ||
- access(sound_player_path, X_OK) != 0)
- return errorf("Error: Cannot find or execute required binaries in %s\n",
- KeyVibe_BIN_DIR);
- 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, current_mute)) {
- if (sound_name_owned) {
- free(sound_name);
- }
- 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, current_mute);
- }
- 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");
- }
- if (sound_name_owned) {
- free(sound_name);
- }
- return 0;
-}
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..67977f4
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,496 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "app/cli.h"
+#include "app/process.h"
+#include "app/reload.h"
+#include "app/watch.h"
+#include "common/mute.h"
+#include "common/utils.h"
+#include "config.h"
+#include "soundpacks.h"
+#include "user_config.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define MAX_PATH_LENGTH 1024
+#define AUDIO_BASE_DIR VBX_DATA_DIR "/soundpacks"
+#define KEYBOARD_AUDIO_DIR AUDIO_BASE_DIR "/keyboard"
+#define MOUSE_AUDIO_DIR AUDIO_BASE_DIR "/mouse"
+#define USER_KEYBOARD_AUDIO_SUBPATH "/.local/share/vbx/soundpacks/keyboard"
+#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/vbx/soundpacks/mouse"
+#define USER_CONFIG_FILENAME ".vbx.json"
+
+extern pid_t keyboard_pid;
+extern pid_t sound_pid;
+extern char pidfile_path[];
+extern int is_daemon;
+extern volatile sig_atomic_t reload_requested;
+static int current_keyboard_volume = 50;
+static int current_mouse_volume = 50;
+static char current_sound_name[MAX_PATH_LENGTH] = {0};
+static char current_mouse_sound_name[MAX_PATH_LENGTH] = {0};
+static int current_verbose = 0;
+static int current_keyboard_mute = 0;
+static int current_mouse_mute = 0;
+static int current_keyboard_enabled = 1;
+static int current_mouse_enabled = 1;
+static char current_config_path[MAX_PATH_LENGTH] = {0};
+static char current_sound_dir[MAX_PATH_LENGTH] = {0};
+static char current_mouse_config_path[MAX_PATH_LENGTH] = {0};
+static char current_mouse_sound_dir[MAX_PATH_LENGTH] = {0};
+static int current_mute = 0;
+
+int main(int argc, char *argv[]) {
+ char *sound_name = strdup("eg-oreo");
+ char *mouse_sound_name = strdup("ping");
+ int sound_name_owned = 1;
+ int mouse_sound_name_owned = 1;
+ int verbose = 0;
+ int list_sounds = 0;
+ int flag_daemon = 0;
+ int flag_stop = 0;
+ int volume = 50;
+ CliOptions cli_opts;
+ 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) {
+ if (verbose) {
+ printf("Loading configuration from: %s\n", user_cfg_path);
+ }
+ char *cfg_keyboard_sound = NULL;
+ char *cfg_mouse_sound = NULL;
+ int cfg_keyboard_volume = volume;
+ int cfg_mouse_volume = volume;
+ int cfg_keyboard_enabled = 1;
+ int cfg_mouse_enabled = 1;
+ if (read_user_config(user_cfg_path, &cfg_keyboard_sound, &cfg_mouse_sound,
+ &cfg_keyboard_volume, &cfg_mouse_volume,
+ &cfg_keyboard_enabled, &cfg_mouse_enabled)) {
+ if (cfg_keyboard_sound) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ sound_name = cfg_keyboard_sound;
+ sound_name_owned = 1;
+ }
+ if (cfg_mouse_sound) {
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ mouse_sound_name = cfg_mouse_sound;
+ mouse_sound_name_owned = 1;
+ }
+ volume = cfg_keyboard_volume;
+ current_keyboard_enabled = cfg_keyboard_enabled;
+ current_mouse_enabled = cfg_mouse_enabled;
+ }
+ }
+ }
+
+ int parse_result = parse_cli(argc, argv, &cli_opts);
+ if (parse_result == 1) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 1;
+ } else if (parse_result == 2) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 0;
+ }
+
+ verbose = cli_opts.verbose;
+ list_sounds = cli_opts.list_flag;
+ flag_daemon = cli_opts.daemon_flag;
+ flag_stop = cli_opts.stop_flag;
+ if (list_sounds) {
+ int rc = list_sound_packs();
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return rc;
+ }
+
+ // Show welcome message for first-time users
+ if (access(user_cfg_path, F_OK) != 0 && !flag_daemon) {
+ printf("\nWelcome to VBX!\n");
+ printf("This appears to be your first time running VBX.\n");
+ printf("A default configuration will be created at: %s\n", user_cfg_path);
+ printf("\n");
+ }
+ build_pidfile_path(pidfile_path, 1024);
+ if (flag_stop) {
+ pid_t running_pid = 0;
+ if (!require_running_pid(&running_pid)) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 1;
+ }
+ if (kill(running_pid, SIGTERM) != 0) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return (perror("kill"), 1);
+ }
+ for (int i = 0; i < 30; i++) {
+ if (!process_is_running(running_pid)) {
+ unlink(pidfile_path);
+ printf("VBX daemon stopped successfully.\n");
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 0;
+ }
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 100000000L;
+ nanosleep(&ts, NULL);
+ }
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return errorf(
+ "VBX: daemon did not stop in time. Try running 'vbx --stop' again.\n");
+ }
+ current_mute = read_runtime_mute_file();
+
+ int config_updated = 0;
+ if (cli_opts.sound != NULL) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ sound_name = cli_opts.sound;
+ sound_name_owned = 0;
+ config_updated = 1;
+ }
+ if (cli_opts.mouse_sound != NULL) {
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ mouse_sound_name = cli_opts.mouse_sound;
+ mouse_sound_name_owned = 0;
+ config_updated = 1;
+ }
+ if (cli_opts.volume >= 0) {
+ volume = cli_opts.volume;
+ config_updated = 1;
+ }
+ if (cli_opts.keyboard_volume >= 0) {
+ current_keyboard_volume = cli_opts.keyboard_volume;
+ config_updated = 1;
+ }
+ if (cli_opts.mouse_volume >= 0) {
+ current_mouse_volume = cli_opts.mouse_volume;
+ config_updated = 1;
+ }
+ if (cli_opts.keyboard_mute >= 0) {
+ current_keyboard_mute = cli_opts.keyboard_mute;
+ config_updated = 1;
+ if (current_keyboard_mute) {
+ printf("Keyboard sounds muted.\n");
+ } else {
+ printf("Keyboard sounds unmuted.\n");
+ }
+ }
+ if (cli_opts.mouse_mute >= 0) {
+ current_mouse_mute = cli_opts.mouse_mute;
+ config_updated = 1;
+ if (current_mouse_mute) {
+ printf("Mouse sounds muted.\n");
+ } else {
+ printf("Mouse sounds unmuted.\n");
+ }
+ }
+ if (cli_opts.keyboard_enabled >= 0) {
+ current_keyboard_enabled = cli_opts.keyboard_enabled;
+ config_updated = 1;
+ if (current_keyboard_enabled) {
+ printf("Keyboard sounds enabled.\n");
+ } else {
+ printf("Keyboard sounds disabled.\n");
+ }
+ }
+ if (cli_opts.mouse_enabled >= 0) {
+ current_mouse_enabled = cli_opts.mouse_enabled;
+ config_updated = 1;
+ if (current_mouse_enabled) {
+ printf("Mouse sounds enabled.\n");
+ } else {
+ printf("Mouse sounds disabled.\n");
+ }
+ }
+
+ write_runtime_mute_file(current_mute);
+ write_runtime_keyboard_mute_file(current_keyboard_mute);
+ write_runtime_mouse_mute_file(current_mouse_mute);
+ write_runtime_keyboard_enabled_file(current_keyboard_enabled);
+ write_runtime_mouse_enabled_file(current_mouse_enabled);
+
+ if (config_updated &&
+ get_user_config_path(user_cfg_path, sizeof(user_cfg_path))) {
+ if (!write_user_config(user_cfg_path, sound_name, mouse_sound_name,
+ current_keyboard_volume, current_mouse_volume,
+ current_keyboard_enabled, current_mouse_enabled)) {
+ safe_fprintf(stderr, "Warning: Failed to update config file %s\n",
+ user_cfg_path);
+ safe_fprintf(stderr, "Your settings will not be saved for next time.\n");
+ } else if (verbose) {
+ safe_fprintf(stderr, "Updated config file %s\n", user_cfg_path);
+ }
+ }
+
+ if (config_updated && !flag_daemon && !list_sounds && !flag_stop) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 0;
+ }
+
+ if (!config_updated &&
+ 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, mouse_sound_name,
+ current_keyboard_volume, current_mouse_volume,
+ current_keyboard_enabled, current_mouse_enabled)) {
+ safe_fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path);
+ } else if (verbose) {
+ printf("Created default configuration: %s\n", user_cfg_path);
+ }
+ }
+ }
+ if (!validate_keyboard_sound_pack(sound_name)) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 1;
+ }
+ if (!validate_mouse_sound_pack(mouse_sound_name)) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 1;
+ }
+ char get_key_presses_path[MAX_PATH_LENGTH];
+ char sound_player_path[MAX_PATH_LENGTH];
+ safe_snprintf_wrapper(get_key_presses_path, sizeof(get_key_presses_path),
+ "%s/vbx-input", VBX_BIN_DIR);
+ safe_snprintf_wrapper(sound_player_path, sizeof(sound_player_path),
+ "%s/vbx-audio", VBX_BIN_DIR);
+ if (access(get_key_presses_path, X_OK) != 0 ||
+ access(sound_player_path, X_OK) != 0) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return errorf("Error: Cannot find or execute required binaries in %s\n",
+ VBX_BIN_DIR);
+ }
+ 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)) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ safe_fprintf(stderr,
+ "VBX daemon is already running (PID: %ld). Use 'vbx --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];
+ char mouse_config_path[MAX_PATH_LENGTH];
+ char mouse_sound_dir[MAX_PATH_LENGTH];
+ if (!build_paths_for_keyboard_sound(sound_name, config_path,
+ sizeof(config_path), sound_dir,
+ sizeof(sound_dir))) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 1;
+ }
+ if (!build_paths_for_mouse_sound(mouse_sound_name, mouse_config_path,
+ sizeof(mouse_config_path), mouse_sound_dir,
+ sizeof(mouse_sound_dir))) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 1;
+ }
+ current_keyboard_volume = volume;
+ current_mouse_volume = volume;
+ current_verbose = verbose;
+ safe_strncpy(current_sound_name, sound_name, sizeof(current_sound_name));
+ safe_strncpy(current_mouse_sound_name, mouse_sound_name,
+ sizeof(current_mouse_sound_name));
+ safe_strncpy(current_config_path, config_path, sizeof(current_config_path));
+ safe_strncpy(current_sound_dir, sound_dir, sizeof(current_sound_dir));
+ safe_strncpy(current_mouse_config_path, mouse_config_path,
+ sizeof(current_mouse_config_path));
+ safe_strncpy(current_mouse_sound_dir, mouse_sound_dir,
+ sizeof(current_mouse_sound_dir));
+ if (verbose && !is_daemon) {
+ printf("Starting VBX daemon...\n");
+ printf("Keyboard sound pack: %s\n", sound_name);
+ printf("Mouse sound pack: %s\n", mouse_sound_name);
+ printf("Keyboard config file: %s\n", config_path);
+ printf("Mouse config file: %s\n", mouse_config_path);
+ printf("Keyboard working directory: %s\n", sound_dir);
+ printf("Mouse working directory: %s\n", mouse_sound_dir);
+ printf("Press Ctrl+C to exit.\n\n");
+ } else {
+ if (!is_daemon) {
+ printf("VBX daemon started successfully!\n");
+ printf(" Keyboard: %s (volume: %d%%)\n", sound_name,
+ current_keyboard_volume);
+ printf(" Mouse: %s (volume: %d%%)\n", mouse_sound_name,
+ current_mouse_volume);
+ printf(" Config: ~/.vbx.json (auto-reload enabled)\n");
+ printf("\nUse 'vbx --stop' to stop the daemon.\n");
+ printf("Press Ctrl+C to exit.\n");
+ }
+ }
+ if (!start_children(sound_dir, config_path, volume, verbose, current_mute,
+ mouse_sound_dir, mouse_config_path, current_mouse_volume,
+ current_keyboard_mute, current_mouse_mute,
+ current_keyboard_enabled, current_mouse_enabled)) {
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 1;
+ }
+ int status;
+ pid_t finished_pid;
+ signal(SIGHUP, handle_sighup);
+
+ char user_cfg_path2[MAX_PATH_LENGTH];
+ if (get_user_config_path(user_cfg_path2, sizeof(user_cfg_path2))) {
+ printf("Setting up file watcher for: %s\n", user_cfg_path2);
+ start_config_watcher(user_cfg_path2);
+ } else {
+ printf("Warning: Could not get user config path for file watching\n");
+ }
+ while (1) {
+ if (reload_requested) {
+ printf("Reload requested, processing config changes...\n");
+ reload_requested = 0;
+ char user_cfg_path3[MAX_PATH_LENGTH];
+ if (get_user_config_path(user_cfg_path3, sizeof(user_cfg_path3))) {
+ handle_reload(
+ user_cfg_path3, current_sound_name, current_mouse_sound_name,
+ ¤t_keyboard_volume, ¤t_mouse_volume,
+ current_config_path, current_sound_dir, current_mouse_config_path,
+ current_mouse_sound_dir, current_mute, current_verbose,
+ current_mouse_volume, current_keyboard_mute, current_mouse_mute,
+ ¤t_keyboard_enabled, ¤t_mouse_enabled);
+ } else {
+ printf("Failed to get user config path\n");
+ }
+ }
+ 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("VBX daemon exited.\n");
+ }
+ if (sound_name_owned) {
+ free(sound_name);
+ }
+ if (mouse_sound_name_owned) {
+ free(mouse_sound_name);
+ }
+ return 0;
+}
diff --git a/src/soundpacks.c b/src/soundpacks.c
new file mode 100644
index 0000000..fab5fde
--- /dev/null
+++ b/src/soundpacks.c
@@ -0,0 +1,279 @@
+#include "soundpacks.h"
+#include "common/utils.h"
+#include "config.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define AUDIO_BASE_DIR VBX_DATA_DIR "/soundpacks"
+#define KEYBOARD_AUDIO_DIR AUDIO_BASE_DIR "/keyboard"
+#define MOUSE_AUDIO_DIR AUDIO_BASE_DIR "/mouse"
+#define USER_KEYBOARD_AUDIO_SUBPATH "/.local/share/vbx/soundpacks/keyboard"
+#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/vbx/soundpacks/mouse"
+
+static void get_user_keyboard_audio_dir(char *buffer, size_t buflen) {
+ buffer[0] = '\0';
+ const char *home = get_home_dir();
+ if (home)
+ safe_snprintf_wrapper(buffer, buflen, "%s%s", home, USER_KEYBOARD_AUDIO_SUBPATH);
+}
+
+static void get_user_mouse_audio_dir(char *buffer, size_t buflen) {
+ buffer[0] = '\0';
+ const char *home = get_home_dir();
+ if (home)
+ safe_snprintf_wrapper(buffer, buflen, "%s%s", home, USER_MOUSE_AUDIO_SUBPATH);
+}
+
+int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir,
+ size_t out_sz) {
+ char user_audio_dir[1024];
+ get_user_keyboard_audio_dir(user_audio_dir, sizeof(user_audio_dir));
+ if (user_audio_dir[0]) {
+ char p[1024];
+ if ((size_t)snprintf(p, sizeof(p), "%s/%s/config.json", user_audio_dir,
+ sound_name) < sizeof(p) &&
+ access(p, R_OK) == 0) {
+ safe_strncpy(out_basedir, user_audio_dir, out_sz);
+ return 1;
+ }
+ }
+ char p2[1024];
+ if ((size_t)snprintf(p2, sizeof(p2), "%s/%s/config.json", KEYBOARD_AUDIO_DIR,
+ sound_name) >= sizeof(p2)) {
+ return 0;
+ }
+ if (access(p2, R_OK) == 0) {
+ safe_strncpy(out_basedir, KEYBOARD_AUDIO_DIR, out_sz);
+ return 1;
+ }
+ return 0;
+}
+
+int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir,
+ size_t out_sz) {
+ char user_audio_dir[1024];
+ get_user_mouse_audio_dir(user_audio_dir, sizeof(user_audio_dir));
+ if (user_audio_dir[0]) {
+ char p[1024];
+ if ((size_t)snprintf(p, sizeof(p), "%s/%s/config.json", user_audio_dir,
+ sound_name) < sizeof(p) &&
+ access(p, R_OK) == 0) {
+ safe_strncpy(out_basedir, user_audio_dir, out_sz);
+ return 1;
+ }
+ }
+ char p2[1024];
+ if ((size_t)snprintf(p2, sizeof(p2), "%s/%s/config.json", MOUSE_AUDIO_DIR,
+ sound_name) >= sizeof(p2)) {
+ return 0;
+ }
+ if (access(p2, R_OK) == 0) {
+ safe_strncpy(out_basedir, MOUSE_AUDIO_DIR, out_sz);
+ return 1;
+ }
+ return 0;
+}
+
+int build_paths_for_keyboard_sound(const char *sound_name,
+ char *out_config_path, size_t out_config_sz,
+ char *out_sound_dir, size_t out_sound_sz) {
+ char basedir[1024];
+ if (!resolve_keyboard_sound_base_dir(sound_name, basedir, sizeof(basedir))) {
+ return 0;
+ }
+ size_t base_len = strlen(basedir);
+ 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)
+ return 0;
+ if (base_len + 1 + name_len + 1 > out_sound_sz)
+ return 0;
+ if (!safe_snprintf(out_config_path, out_config_sz, "%s/%s%s", basedir,
+ sound_name, cfg_suffix))
+ return 0;
+ if (!safe_snprintf(out_sound_dir, out_sound_sz, "%s/%s", basedir, sound_name))
+ return 0;
+ return 1;
+}
+
+int build_paths_for_mouse_sound(const char *sound_name, char *out_config_path,
+ size_t out_config_sz, char *out_sound_dir,
+ size_t out_sound_sz) {
+ char basedir[1024];
+ if (!resolve_mouse_sound_base_dir(sound_name, basedir, sizeof(basedir))) {
+ return 0;
+ }
+ size_t base_len = strlen(basedir);
+ 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)
+ return 0;
+ if (base_len + 1 + name_len + 1 > out_sound_sz)
+ return 0;
+ if (!safe_snprintf(out_config_path, out_config_sz, "%s/%s%s", basedir,
+ sound_name, cfg_suffix))
+ return 0;
+ if (!safe_snprintf(out_sound_dir, out_sound_sz, "%s/%s", basedir, sound_name))
+ return 0;
+ return 1;
+}
+
+int validate_keyboard_sound_pack(const char *sound_name) {
+ char basedir[1024];
+ if (!resolve_keyboard_sound_base_dir(sound_name, basedir, sizeof(basedir))) {
+ fprintf(
+ stderr,
+ "Error: Keyboard sound pack '%s' not found in user or system dirs.\n",
+ sound_name);
+ fprintf(stderr, "Use --list to see available sound packs.\n");
+ return 0;
+ }
+ char config_path[1024];
+ if ((size_t)snprintf(config_path, sizeof(config_path), "%s/%s/config.json",
+ basedir, sound_name) >= sizeof(config_path)) {
+ fprintf(stderr, "Error: Sound pack path too long: %s/%s\n", basedir,
+ sound_name);
+ fprintf(stderr, "Try using a shorter sound pack name.\n");
+ return 0;
+ }
+ if (access(config_path, R_OK) != 0) {
+ fprintf(stderr, "Error: Sound pack config not found: %s\n", config_path);
+ fprintf(stderr, "Make sure the sound pack directory contains a valid config.json file.\n");
+ return 0;
+ }
+ return 1;
+}
+
+int validate_mouse_sound_pack(const char *sound_name) {
+ char basedir[1024];
+ if (!resolve_mouse_sound_base_dir(sound_name, basedir, sizeof(basedir))) {
+ fprintf(stderr,
+ "Error: Mouse sound pack '%s' not found in user or system dirs.\n",
+ sound_name);
+ fprintf(stderr, "Use --list to see available sound packs.\n");
+ return 0;
+ }
+ char config_path[1024];
+ if ((size_t)snprintf(config_path, sizeof(config_path), "%s/%s/config.json",
+ basedir, sound_name) >= sizeof(config_path)) {
+ fprintf(stderr, "Error: Sound pack path too long: %s/%s\n", basedir,
+ sound_name);
+ fprintf(stderr, "Try using a shorter sound pack name.\n");
+ return 0;
+ }
+ if (access(config_path, R_OK) != 0) {
+ fprintf(stderr, "Error: Sound pack config not found: %s\n", config_path);
+ fprintf(stderr, "Make sure the sound pack directory contains a valid config.json file.\n");
+ return 0;
+ }
+ return 1;
+}
+
+int list_sound_packs(void) {
+ DIR *dir;
+ struct dirent *entry;
+ char path[1024];
+ struct stat st;
+
+ char user_keyboard_dir[1024] = {0};
+ char user_mouse_dir[1024] = {0};
+ 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) {
+ snprintf(user_keyboard_dir, sizeof(user_keyboard_dir), "%s%s", home,
+ USER_KEYBOARD_AUDIO_SUBPATH);
+ snprintf(user_mouse_dir, sizeof(user_mouse_dir), "%s%s", home,
+ USER_MOUSE_AUDIO_SUBPATH);
+ }
+
+ printf("Available Sound Packs\n");
+ printf("=====================\n\n");
+ printf("\nKEYBOARD SOUND PACKS:\n");
+ printf("======================\n");
+ printf("%-30s %-15s\n", "Pack Name", "Source");
+ printf("%-30s %-15s\n", "---------", "------");
+ if (user_keyboard_dir[0] && (dir = opendir(user_keyboard_dir)) != NULL) {
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+ if ((size_t)snprintf(path, sizeof(path), "%s/%s", user_keyboard_dir,
+ entry->d_name) >= sizeof(path)) {
+ fprintf(stderr, "Warning: Path too long, skipping: %s/%s\n",
+ user_keyboard_dir, entry->d_name);
+ continue;
+ }
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ printf("%-30s %-15s\n", entry->d_name, "user");
+ }
+ }
+ closedir(dir);
+ }
+ dir = opendir(KEYBOARD_AUDIO_DIR);
+ if (dir != NULL) {
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+ if ((size_t)snprintf(path, sizeof(path), "%s/%s", KEYBOARD_AUDIO_DIR,
+ entry->d_name) >= sizeof(path)) {
+ fprintf(stderr, "Warning: Path too long, skipping: %s/%s\n",
+ KEYBOARD_AUDIO_DIR, entry->d_name);
+ continue;
+ }
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ printf("%-30s %-15s\n", entry->d_name, "system");
+ }
+ }
+ closedir(dir);
+ }
+ printf("\nMOUSE SOUND PACKS:\n");
+ printf("==================\n");
+ printf("%-30s %-15s\n", "Pack Name", "Source");
+ printf("%-30s %-15s\n", "---------", "------");
+ if (user_mouse_dir[0] && (dir = opendir(user_mouse_dir)) != NULL) {
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+ if ((size_t)snprintf(path, sizeof(path), "%s/%s", user_mouse_dir,
+ entry->d_name) >= sizeof(path)) {
+ fprintf(stderr, "Warning: Path too long, skipping: %s/%s\n",
+ user_mouse_dir, entry->d_name);
+ continue;
+ }
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ printf("%-30s %-15s\n", entry->d_name, "user");
+ }
+ }
+ closedir(dir);
+ }
+ dir = opendir(MOUSE_AUDIO_DIR);
+ if (dir != NULL) {
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+ if ((size_t)snprintf(path, sizeof(path), "%s/%s", MOUSE_AUDIO_DIR,
+ entry->d_name) >= sizeof(path)) {
+ fprintf(stderr, "Warning: Path too long, skipping: %s/%s\n",
+ MOUSE_AUDIO_DIR, entry->d_name);
+ continue;
+ }
+ if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ printf("%-30s %-15s\n", entry->d_name, "system");
+ }
+ }
+ closedir(dir);
+ }
+ printf("\nUsage: vbx -S for keyboard or vbx -M for mouse\n");
+ return 0;
+}
diff --git a/src/utils.c b/src/utils.c
deleted file mode 100644
index 39a25a5..0000000
--- a/src/utils.c
+++ /dev/null
@@ -1,69 +0,0 @@
-#define _POSIX_C_SOURCE 200809L
-#include "utils.h"
-#include "config.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-int safe_snprintf(char *dst, size_t dst_sz, const char *fmt, ...) {
- if (!dst || dst_sz == 0)
- return 0;
- va_list ap;
- va_start(ap, fmt);
- int n = vsnprintf(dst, dst_sz, fmt, ap);
- va_end(ap);
- return (n >= 0 && (size_t)n < dst_sz);
-}
-
-int errorf(const char *fmt, ...) {
- va_list ap;
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
- return 1;
-}
-
-const char *get_runtime_dir(void) {
- const char *rd = getenv("XDG_RUNTIME_DIR");
- if (rd && strlen(rd) > 0)
- return rd;
- return "/tmp";
-}
-
-void build_pidfile_path(char *buffer, size_t buflen) {
- const char *rd = get_runtime_dir();
- safe_snprintf(buffer, buflen, "%s/keyvibe-%d.pid", rd, (int)getuid());
-}
-
-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;
-}
-
-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;
-}
-
-int process_is_running(pid_t pid) {
- if (pid <= 0)
- return 0;
- return kill(pid, 0) == 0;
-}