From 2e3b870efe89b8aa91fd13a22a336d009d3aa657 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 13 Oct 2025 09:32:42 +0545 Subject: [PATCH 01/18] oops many changes i forgot to push lol --- Makefile | 3 +- include/config.h | 4 +- include/utils.h | 5 +- .../keyboard}/banana-split-lubed/LICENSE.txt | 0 .../banana-split-lubed/banana-l-1.wav | Bin .../banana-split-lubed/banana-l-2.wav | Bin .../banana-split-lubed/banana-l-3.wav | Bin .../banana-split-lubed/banana-l-4.wav | Bin .../banana-split-lubed/banana-l-5.wav | Bin .../banana-split-lubed/banana-l-6.wav | Bin .../banana-split-lubed/banana-l-7.wav | Bin .../keyboard}/banana-split-lubed/config.json | 0 .../keyboard}/cherrymx-black-abs/config.json | 0 .../keyboard}/cherrymx-black-abs/sound.ogg | Bin .../keyboard}/cherrymx-black-pbt/config.json | 0 .../keyboard}/cherrymx-black-pbt/sound.ogg | Bin .../keyboard}/cherrymx-blue-abs/config.json | 0 .../keyboard}/cherrymx-blue-abs/sound.ogg | Bin .../keyboard}/cherrymx-blue-pbt/config.json | 0 .../keyboard}/cherrymx-blue-pbt/sound.ogg | Bin .../keyboard}/cherrymx-brown-abs/config.json | 0 .../keyboard}/cherrymx-brown-abs/sound.ogg | Bin .../keyboard}/cherrymx-brown-pbt/config.json | 0 .../keyboard}/cherrymx-brown-pbt/sound.ogg | Bin .../keyboard}/cherrymx-red-abs/config.json | 0 .../keyboard}/cherrymx-red-abs/sound.ogg | Bin .../keyboard}/cherrymx-red-pbt/config.json | 0 .../keyboard}/cherrymx-red-pbt/sound.ogg | Bin .../keyboard}/cream-travel/config.json | 0 .../cream-travel/press/BACKSPACE.mp3 | Bin .../keyboard}/cream-travel/press/ENTER.mp3 | Bin .../cream-travel/press/GENERIC_R0.mp3 | Bin .../cream-travel/press/GENERIC_R1.mp3 | Bin .../cream-travel/press/GENERIC_R2.mp3 | Bin .../cream-travel/press/GENERIC_R3.mp3 | Bin .../cream-travel/press/GENERIC_R4.mp3 | Bin .../keyboard}/cream-travel/press/SPACE.mp3 | Bin .../cream-travel/release/BACKSPACE.mp3 | Bin .../keyboard}/cream-travel/release/ENTER.mp3 | Bin .../cream-travel/release/GENERIC.mp3 | Bin .../keyboard}/cream-travel/release/SPACE.mp3 | Bin .../keyboard}/eg-crystal-purple/config.json | 0 .../keyboard}/eg-crystal-purple/purple.ogg | Bin .../keyboard}/eg-oreo/config.json | 12 + .../keyboard}/eg-oreo/oreo.ogg | Bin .../keyboard}/holy-pandas/BACKSPACE.mp3 | Bin .../keyboard}/holy-pandas/ENTER.mp3 | Bin .../keyboard}/holy-pandas/GENERIC_R0.mp3 | Bin .../keyboard}/holy-pandas/GENERIC_R1.mp3 | Bin .../keyboard}/holy-pandas/GENERIC_R2.mp3 | Bin .../keyboard}/holy-pandas/GENERIC_R3.mp3 | Bin .../keyboard}/holy-pandas/GENERIC_R4.mp3 | Bin .../keyboard}/holy-pandas/LICENSE | 0 .../keyboard}/holy-pandas/README.md | 0 .../keyboard}/holy-pandas/SPACE.mp3 | Bin .../keyboard}/holy-pandas/config.json | 0 .../holy-pandas/release/BACKSPACE.mp3 | Bin .../keyboard}/holy-pandas/release/ENTER.mp3 | Bin .../keyboard}/holy-pandas/release/GENERIC.mp3 | Bin .../keyboard}/holy-pandas/release/SPACE.mp3 | Bin .../kalih-box-white/Untitled Clip(3).mp3 | Bin 0 -> 6895 bytes .../kalih-box-white/Untitled Clip(4).mp3 | Bin 0 -> 11284 bytes .../kalih-box-white/Untitled Clip(5).mp3 | Bin 0 -> 6895 bytes .../kalih-box-white/Untitled Clip(6).mp3 | Bin 0 -> 11284 bytes .../kalih-box-white/Untitled Clip(7).mp3 | Bin 0 -> 5641 bytes .../kalih-box-white/Untitled Clip.mp3 | Bin 0 -> 11284 bytes soundpacks/keyboard/kalih-box-white/alt.mp3 | Bin 0 -> 6895 bytes .../keyboard/kalih-box-white/config.json | 135 ++++ soundpacks/keyboard/kalih-box-white/enter.mp3 | Bin 0 -> 7522 bytes .../keyboard/kalih-box-white/kboxwh - 01.mp3 | Bin 0 -> 11284 bytes .../keyboard/kalih-box-white/kboxwh - 02.mp3 | Bin 0 -> 11284 bytes .../keyboard/kalih-box-white/kboxwh - 03.mp3 | Bin 0 -> 11284 bytes .../keyboard/kalih-box-white/kboxwh - 04.mp3 | Bin 0 -> 6268 bytes .../keyboard/kalih-box-white/kboxwh - 05.mp3 | Bin 0 -> 9403 bytes .../keyboard/kalih-box-white/kboxwh - 06.mp3 | Bin 0 -> 8149 bytes .../keyboard/kalih-box-white/kboxwh - 07.mp3 | Bin 0 -> 12537 bytes .../keyboard/kalih-box-white/kboxwh - 08.mp3 | Bin 0 -> 6895 bytes .../keyboard/kalih-box-white/kboxwh - 09.mp3 | Bin 0 -> 8149 bytes .../keyboard/kalih-box-white/kboxwh - 10.mp3 | Bin 0 -> 10657 bytes .../keyboard}/mxblack-travel/config.json | 0 .../mxblack-travel/press/BACKSPACE.mp3 | Bin .../keyboard}/mxblack-travel/press/ENTER.mp3 | Bin .../mxblack-travel/press/GENERIC_R0.mp3 | Bin .../mxblack-travel/press/GENERIC_R1.mp3 | Bin .../mxblack-travel/press/GENERIC_R2.mp3 | Bin .../mxblack-travel/press/GENERIC_R3.mp3 | Bin .../mxblack-travel/press/GENERIC_R4.mp3 | Bin .../keyboard}/mxblack-travel/press/SPACE.mp3 | Bin .../mxblack-travel/release/BACKSPACE.mp3 | Bin .../mxblack-travel/release/ENTER.mp3 | Bin .../mxblack-travel/release/GENERIC.mp3 | Bin .../mxblack-travel/release/SPACE.mp3 | Bin .../keyboard}/mxblue-travel/config.json | 0 .../mxblue-travel/press/GENERIC_R0.mp3 | Bin .../mxblue-travel/press/GENERIC_R1.mp3 | Bin .../mxblue-travel/press/GENERIC_R2.mp3 | Bin .../mxblue-travel/press/GENERIC_R3.mp3 | Bin .../mxblue-travel/press/GENERIC_R4.mp3 | Bin .../mxblue-travel/release/GENERIC.mp3 | Bin .../keyboard}/mxbrown-travel/config.json | 0 .../mxbrown-travel/press/BACKSPACE.mp3 | Bin .../keyboard}/mxbrown-travel/press/ENTER.mp3 | Bin .../mxbrown-travel/press/GENERIC_R0.mp3 | Bin .../mxbrown-travel/press/GENERIC_R1.mp3 | Bin .../mxbrown-travel/press/GENERIC_R2.mp3 | Bin .../mxbrown-travel/press/GENERIC_R3.mp3 | Bin .../mxbrown-travel/press/GENERIC_R4.mp3 | Bin .../keyboard}/mxbrown-travel/press/SPACE.mp3 | Bin .../mxbrown-travel/release/BACKSPACE.mp3 | Bin .../mxbrown-travel/release/ENTER.mp3 | Bin .../mxbrown-travel/release/GENERIC.mp3 | Bin .../mxbrown-travel/release/SPACE.mp3 | Bin {audio => soundpacks/keyboard}/nk-cream/[.wav | Bin {audio => soundpacks/keyboard}/nk-cream/].wav | Bin {audio => soundpacks/keyboard}/nk-cream/a.wav | Bin {audio => soundpacks/keyboard}/nk-cream/b.wav | Bin .../keyboard}/nk-cream/backspace.wav | Bin {audio => soundpacks/keyboard}/nk-cream/c.wav | Bin .../keyboard}/nk-cream/caps lock.wav | Bin .../keyboard}/nk-cream/config.json | 0 {audio => soundpacks/keyboard}/nk-cream/d.wav | Bin {audio => soundpacks/keyboard}/nk-cream/e.wav | Bin .../keyboard}/nk-cream/enter.wav | Bin {audio => soundpacks/keyboard}/nk-cream/f.wav | Bin {audio => soundpacks/keyboard}/nk-cream/g.wav | Bin {audio => soundpacks/keyboard}/nk-cream/h.wav | Bin {audio => soundpacks/keyboard}/nk-cream/i.wav | Bin {audio => soundpacks/keyboard}/nk-cream/j.wav | Bin {audio => soundpacks/keyboard}/nk-cream/k.wav | Bin {audio => soundpacks/keyboard}/nk-cream/l.wav | Bin {audio => soundpacks/keyboard}/nk-cream/m.wav | Bin {audio => soundpacks/keyboard}/nk-cream/n.wav | Bin {audio => soundpacks/keyboard}/nk-cream/o.wav | Bin {audio => soundpacks/keyboard}/nk-cream/p.wav | Bin {audio => soundpacks/keyboard}/nk-cream/q.wav | Bin {audio => soundpacks/keyboard}/nk-cream/r.wav | Bin {audio => soundpacks/keyboard}/nk-cream/s.wav | Bin .../keyboard}/nk-cream/shift.wav | Bin .../keyboard}/nk-cream/space.wav | Bin {audio => soundpacks/keyboard}/nk-cream/t.wav | Bin .../keyboard}/nk-cream/tab.wav | Bin {audio => soundpacks/keyboard}/nk-cream/u.wav | Bin {audio => soundpacks/keyboard}/nk-cream/v.wav | Bin {audio => soundpacks/keyboard}/nk-cream/w.wav | Bin {audio => soundpacks/keyboard}/nk-cream/x.wav | Bin {audio => soundpacks/keyboard}/nk-cream/y.wav | Bin {audio => soundpacks/keyboard}/nk-cream/z.wav | Bin .../topre-purple-hybrid-pbt/config.json | 0 .../topre-purple-hybrid-pbt/sound.ogg | Bin .../keyboard}/turquoise/config.json | 0 .../keyboard}/turquoise/press/BACKSPACE.mp3 | Bin .../keyboard}/turquoise/press/ENTER.mp3 | Bin .../keyboard}/turquoise/press/GENERIC_R0.mp3 | Bin .../keyboard}/turquoise/press/GENERIC_R1.mp3 | Bin .../keyboard}/turquoise/press/GENERIC_R2.mp3 | Bin .../keyboard}/turquoise/press/GENERIC_R3.mp3 | Bin .../keyboard}/turquoise/press/GENERIC_R4.mp3 | Bin .../keyboard}/turquoise/press/SPACE.mp3 | Bin .../keyboard}/turquoise/release/BACKSPACE.mp3 | Bin .../keyboard}/turquoise/release/ENTER.mp3 | Bin .../keyboard}/turquoise/release/GENERIC.mp3 | Bin .../keyboard}/turquoise/release/SPACE.mp3 | Bin soundpacks/mouse/chat/chat.mp3 | Bin 0 -> 18141 bytes soundpacks/mouse/chat/config.json | 28 + soundpacks/mouse/ping/config.json | 28 + soundpacks/mouse/ping/ping.mp3 | Bin 0 -> 35108 bytes soundpacks/mouse/vibrate/config.json | 28 + soundpacks/mouse/vibrate/vibrate.mp3 | Bin 0 -> 29584 bytes soundpacks/mouse/wooden/config.json | 28 + soundpacks/mouse/wooden/wooden.mp3 | Bin 0 -> 50688 bytes src/keyvibe_audio.c | 210 ++++-- src/keyvibe_input.c | 26 +- src/keyvibe_main.c | 662 +++++++++++++----- src/utils.c | 17 +- 174 files changed, 898 insertions(+), 288 deletions(-) rename {audio => soundpacks/keyboard}/banana-split-lubed/LICENSE.txt (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/banana-l-1.wav (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/banana-l-2.wav (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/banana-l-3.wav (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/banana-l-4.wav (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/banana-l-5.wav (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/banana-l-6.wav (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/banana-l-7.wav (100%) rename {audio => soundpacks/keyboard}/banana-split-lubed/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-black-abs/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-black-abs/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cherrymx-black-pbt/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-black-pbt/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cherrymx-blue-abs/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-blue-abs/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cherrymx-blue-pbt/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-blue-pbt/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cherrymx-brown-abs/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-brown-abs/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cherrymx-brown-pbt/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-brown-pbt/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cherrymx-red-abs/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-red-abs/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cherrymx-red-pbt/config.json (100%) rename {audio => soundpacks/keyboard}/cherrymx-red-pbt/sound.ogg (100%) rename {audio => soundpacks/keyboard}/cream-travel/config.json (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/GENERIC_R0.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/GENERIC_R1.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/GENERIC_R2.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/GENERIC_R3.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/GENERIC_R4.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/press/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/release/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/release/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/release/GENERIC.mp3 (100%) rename {audio => soundpacks/keyboard}/cream-travel/release/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/eg-crystal-purple/config.json (100%) rename {audio => soundpacks/keyboard}/eg-crystal-purple/purple.ogg (100%) rename {audio => soundpacks/keyboard}/eg-oreo/config.json (97%) rename {audio => soundpacks/keyboard}/eg-oreo/oreo.ogg (100%) rename {audio => soundpacks/keyboard}/holy-pandas/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/GENERIC_R0.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/GENERIC_R1.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/GENERIC_R2.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/GENERIC_R3.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/GENERIC_R4.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/LICENSE (100%) rename {audio => soundpacks/keyboard}/holy-pandas/README.md (100%) rename {audio => soundpacks/keyboard}/holy-pandas/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/config.json (100%) rename {audio => soundpacks/keyboard}/holy-pandas/release/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/release/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/release/GENERIC.mp3 (100%) rename {audio => soundpacks/keyboard}/holy-pandas/release/SPACE.mp3 (100%) create mode 100644 soundpacks/keyboard/kalih-box-white/Untitled Clip(3).mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/Untitled Clip(4).mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/Untitled Clip(5).mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/Untitled Clip(6).mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/Untitled Clip(7).mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/Untitled Clip.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/alt.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/config.json create mode 100644 soundpacks/keyboard/kalih-box-white/enter.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 01.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 02.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 03.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 04.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 05.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 06.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 07.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 08.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 09.mp3 create mode 100644 soundpacks/keyboard/kalih-box-white/kboxwh - 10.mp3 rename {audio => soundpacks/keyboard}/mxblack-travel/config.json (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/GENERIC_R0.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/GENERIC_R1.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/GENERIC_R2.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/GENERIC_R3.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/GENERIC_R4.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/press/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/release/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/release/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/release/GENERIC.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblack-travel/release/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblue-travel/config.json (100%) rename {audio => soundpacks/keyboard}/mxblue-travel/press/GENERIC_R0.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblue-travel/press/GENERIC_R1.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblue-travel/press/GENERIC_R2.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblue-travel/press/GENERIC_R3.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblue-travel/press/GENERIC_R4.mp3 (100%) rename {audio => soundpacks/keyboard}/mxblue-travel/release/GENERIC.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/config.json (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/GENERIC_R0.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/GENERIC_R1.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/GENERIC_R2.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/GENERIC_R3.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/GENERIC_R4.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/press/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/release/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/release/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/release/GENERIC.mp3 (100%) rename {audio => soundpacks/keyboard}/mxbrown-travel/release/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/nk-cream/[.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/].wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/a.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/b.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/backspace.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/c.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/caps lock.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/config.json (100%) rename {audio => soundpacks/keyboard}/nk-cream/d.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/e.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/enter.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/f.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/g.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/h.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/i.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/j.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/k.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/l.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/m.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/n.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/o.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/p.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/q.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/r.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/s.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/shift.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/space.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/t.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/tab.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/u.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/v.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/w.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/x.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/y.wav (100%) rename {audio => soundpacks/keyboard}/nk-cream/z.wav (100%) rename {audio => soundpacks/keyboard}/topre-purple-hybrid-pbt/config.json (100%) rename {audio => soundpacks/keyboard}/topre-purple-hybrid-pbt/sound.ogg (100%) rename {audio => soundpacks/keyboard}/turquoise/config.json (100%) rename {audio => soundpacks/keyboard}/turquoise/press/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/press/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/press/GENERIC_R0.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/press/GENERIC_R1.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/press/GENERIC_R2.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/press/GENERIC_R3.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/press/GENERIC_R4.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/press/SPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/release/BACKSPACE.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/release/ENTER.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/release/GENERIC.mp3 (100%) rename {audio => soundpacks/keyboard}/turquoise/release/SPACE.mp3 (100%) create mode 100644 soundpacks/mouse/chat/chat.mp3 create mode 100644 soundpacks/mouse/chat/config.json create mode 100644 soundpacks/mouse/ping/config.json create mode 100644 soundpacks/mouse/ping/ping.mp3 create mode 100644 soundpacks/mouse/vibrate/config.json create mode 100644 soundpacks/mouse/vibrate/vibrate.mp3 create mode 100644 soundpacks/mouse/wooden/config.json create mode 100644 soundpacks/mouse/wooden/wooden.mp3 diff --git a/Makefile b/Makefile index ba124e8..391f659 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ - CC = gcc CFLAGS = -Wall -Wextra -std=c99 -Iinclude PREFIX ?= /usr @@ -55,7 +54,7 @@ install: install -Dm755 $(SOUND_TARGET) $(DESTDIR)$(BINDIR)/$(SOUND_TARGET) install -Dm755 $(KEYBOARD_TARGET) $(DESTDIR)$(BINDIR)/$(KEYBOARD_TARGET) 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 diff --git a/include/config.h b/include/config.h index 8b2bbe4..9c2c9ff 100644 --- a/include/config.h +++ b/include/config.h @@ -1,7 +1,7 @@ #ifndef __CONFIG_H__ #define __CONFIG_H__ -#define PROJECT_VERSION "0.1.0" +#define PROJECT_VERSION "1.1.0" #define GETTEXT_PACKAGE "keyvibe" #define PKEXEC_PATH "/usr/bin/pkexec" @@ -18,3 +18,5 @@ #define KeyVibe_BIN_DIR PACKAGE_PREFIX "/bin" #endif + + diff --git a/include/utils.h b/include/utils.h index 03cd5bf..38a3b07 100644 --- a/include/utils.h +++ b/include/utils.h @@ -1,4 +1,3 @@ - #ifndef KEYVIBE_UTILS_H #define KEYVIBE_UTILS_H @@ -8,8 +7,7 @@ // 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. +// fprintf to stderr with printf-style formatting. Returns 1 for convenient `return errorf(...)` usage. int errorf(const char *fmt, ...); // Common runtime helpers @@ -20,3 +18,4 @@ 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 0000000000000000000000000000000000000000..9bbdc661a0e52c7d311dbf4c22559a3dfc20cf5a GIT binary patch literal 6895 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9e03f93cb7f7041f074f34e5256703cc1344aa5f GIT binary patch literal 11284 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2789cfed7e1b3ed4e2c16f63f4a5de3f7ebfd11f GIT binary patch literal 6895 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 J{V!JFKLB%?@0|bu literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f3ac560367c7368d6c4efcb53cd0351ad31feb8f GIT binary patch literal 11284 zcmeI1bxd8szVA2g6nEOq#wl(ET43XB+@0d?ZpGc*-QA_Q6?ckzai^tNfx^DDC+FVW ztDA2^5ZYiKfH{^CnF>)#=*?X>j?9o0XciA z;22E+fZjhjhC`q~{`1NIHvWfH0Q%QBI4teieRzFeqd_FJ-V{!Zax*Qi0$JKx-oqI@rkYAKp0&_M)$b4(D2syH}#9SmLz)06N}-@W?ehVgP#U=j)y)$ zo^420s(l~2HkH-CZ||Rc)>?G1QzO!bzEom?{`KS1UAIkbnEFPwjM1)7t-MgBX3~ms z-5H<&E3&%0VU<1=C~;il9`%Jdt6-N}S#(#a)Z~{-TpAgrJ}PXI#Q!ZcOO>R<(a^9f zawxonsiag74)!9Lih1ej5?#6OW8p>WRBfDfim6 zEFChlfn2VjhyNaX!NE?wW$R`39*QZqJfH6-n=h&FtdxmX)AVxtS8{o11;Ix@meE9*$mQ>m-hj}n6)WH&y zR4xG)tQFo#F%EUh_7Kn#+yDxbyq?CgenU*yX#j&(#|BR2yBY$4g6;0(P;hxLJ__$* zUGGqnG9fg|yk z^piBLTKMmB+3di)*%VSt6e_K6?*7!}r>CUo1X^LILFJa;ou#TaW^Bq0e5>KnRC%|& z=mx(N4kWECE(rvhIvu9ZbDVE|wUMvxcHa^a(R(bsnL%j@(tpw2i27_BgVQKuI~ON- zzfmtL;Nyz2l13vo{_7q3qu$KHoOuO)HX9~$5K?IQ=A~9&WKn+sEFlJFSTYxuPW|B| zhl3}QSF>VCr0KPz+*aejspZb4RAO0TU^OMEqnM<@!m*OX3pdb7ZGyo~vkZ~?CJ<9! zHcBSIKczHwX`C6`QWVpEIqg71S)jbeXV`+v+7;1%5crGmcO9np?ub9<8e|o{h+>9a zG4N9o3+oq|0kF@!br;THT3Fc~w!FkD_^Mym?XKRy|5AxHo#Z9nh$Oy)5p{tjh(ViY!ROG$7|3BR(BVP$6q4wpQpTk(wOO zRdu9oYK*IILwsw%`x!n>w9F?IKIYs^TabX`k8-Sj@a9GfUMOTcQB|I`29z->Ft~z~ z^_;>kAamYM);8kas>}Jfd=f-4F=iM~xvFH>+58Ortc=v3@`k&bDvIaz9nQ^m+mCPo z-X;GCT=d@pM>2GM-3AdS@DE7|>8=ehPS89QKmi+%89-`x=}1A{gU{9!~MqfkkLbq=DJd@Bj#8LYoX9<0(d;V7sK zxkK?->8-E0Ib&Y};z#Uy>RDhTV?zwy)SC%Z4JwF+7FJ$}wmG;EHCu9k+HWe}E>6m% z9sVS@(8;1Kd5>+iI^yH&WLRmN?{qqheCQN>1=Y#$suQd!9-3 zK_;I7cwt}dQ*{e$&iY(Z2C1EBs4of2d{R{g(5VH+TwMhK0F!bg$9TXlv8 z?z5^}9do0Ua2KJ=i>Wy@(dFPBWXFUr*HXIi(J>aCDJ6^uM^QvXj-KR1Dgw_FH3PNs}9EtL5L0P3_Vj^lU)t4_5pH>e zn21rp6W$K(mn%uO)ElH_y{<3M|4KIJkOb)f?(|jSUSL;N&p&nU2Os21>x6$Wsrj7A z(zbpYY+;WW91I4)!!+`09L9->8odOUS;*`sNVuF~L)qmp_}ub;nx!I!=S#OB|~Fh`MAUk4edvx!&T7~Odr%`8`3 zEkgLKI|9(r-^;6{5MV|XvNji0^Hgq?-3=Kz6nb?D%t!E=C3DIfDn{_ESbv>kDn|A4 ztt~mwu7p08EiU;cvpo82=uwnjdt{pWzb4$VTT*2$WahsI$ci+)=;wz%XkS2ug^X*B z0SE5~DtE{0l3lXWbI}#TA~kWuc}PxTN5sIgu@q#8?wfn03uvw{vlnl*!-S^2d^;TR2o=7>m_`zV zh<*yE4)PPRSH5R#R$VJ}(4ViTjVRU7UhW8N^{;GV;m2)zn{Oc1AGiBG`4wbdBQs^e zm2cn`myk3X@gL>r0C*FcQ;#y7-x4Z$zgy6x;rdfDRd0v)b5WJFl@|SQ)qj60Fqg9E zMYBG~BJ6sKIBUXWeA2OW)-YE4-RnGAm%5%~yYz8`ZZ3RK-q4;Lc+~xqL2|EhA}cj3 zJ#B4?=@>}YH{8`Xs!t!DssKQsg8jWp##$exDR$Hz`K^==BCATLM)lQIgTbN`Gg7WN z{DUgr^fvktl09ewZjv|STuV;jo4Pj8STzjIuwG22TuBR15r!|3BrmgiE@%6$if2BO zCJIwod||<@c1^ePar4o7vpGCBT5$7}RCK4b!JeFAr2ERTq9?tm#o@sFW1AfWMx5@h zl{f_z=JTH4%SNaIUqZBhwZR_-r_kI}(pk$wNtLo#?A7O6OSg#mCMym0>$yWh4U?&Oo~E9IhH6BEL~YXt{ivi)$3e0 zR*u3Ji`xGP@NCmHc60VjrDll)_-h zuqambmsKGFz@wWXl0+C{6^+QyL)c7mznUT`D-^e_qt!QniZ;nm;E}OpV6sRO!{au! zDd2_UOeme{O$s8h;1p$3ccX+JwBTn0oD05~Yxv^C7uS!kUqw3Vrr4Sz=2*v7vimqv z$*%@<#ICN(CR@fGGji`mGUGK&tLr-4lR_)TzDQ*JIKP#M@i=CAx&gR`i@a0{SQ}de z&jy@PVW`!o=~!dK|CJ^TkmdWOpcXVp+=O){(*l1A26-`di_Qrhhl<%O)YGEpsVLba zHcBo+PT#b#`u|am%8yD@T*fP$OCc?6RMrh$?v)L;;8~Hwjz)no+eE$n2So@Iyj!My zz=ga)eaif!Vn#x}OlCf(Mxr&Q@mJB9tPJ*%rlC>z+G1N=vx{GzH3O z9t!5))L{cOb4mHwZW_fH^YJI(M6;8tW|iaNmkb!bEj|xji+*Rmiwz_bz$aiPRm2X) z<9Hf$$xKK4Q50L*qm!>gC531s4o}C$PM~6bLN9Gnm+W1&Pjp&YlcW0sYdG^~SD{lH z?*a@C(Nd>fAD}f=jL+0Qq2g&m{S-#B7XU-+^_=O_px;L~bMFT0H^pGahOl}vGoUWL zx?fRcDPBlDfEH)AsG&3DoH?rqmajC#2@3OmBW_i7d-mB?Wv%<@=gc0kP{i^4UI`Yg zIyb;TzevVVbvvP}@l$pA?r`sL=wpOXpHaUi4O{(ciqd6x;uXogW$i_I3KL?|6-}kC z)hP$B^@`vAta8`Vvuf@Q1A;LC{zEMlyUhS=9GI<$z$ltGt4Eo0=DLL8EY-QSiUNXf z4$B8bKBgKWBFc*u3DY0-FU$bM;Mf3Qk~Io4>5bCK_=1Zv{5V0qL|Hf-91`I_CiccD zkCq43*Y}$B7xDfm#|DD{;uRNZ^RN#JIkvH(slXmsA!aey?{_3M+&=4UHJkHrijA;c z3ERhj(Dh@?_S%t#@`hEIemVMVpE9MGPgkrF8-9Z7H%|@33X=su63Wn?$UX+?nfXF% zkD>W5ryF7DLod9v%3_WHMMNVJz@F}&4*%<*Lt%aC14>qi>RW6-(tSQ-HiVkJcL+*a zu^bmWeT;D!o?mKHGT6V(>W^T>vvE#)(pX?X%BA5B7LybPO;g;|P8V%ArRM9gL>1~q zO-WLvY>DL=S}y+`y&*sPNF~u!)AkIHL%J^m~87^a9Cd$d6ohbyS#Rd4R}D9_=QIDK9J3z+Z=Bfpva)^3QQ4DANC~zO26+wDe7`bc zwxuvXo_IiAD+gNs{eW@lRg0UOx~#^4^y?wsu+(}bTnMI^>H4*YV+1Gxs9-rN}Rg1J0cNu%g0o*n~fEl-aWu(IFwyzy(UPrM|BtC1`7HC(~UCPDgLN zlVyr0&M58Wghv8MnjnFyLVy(H%-z#+p$xCoaGRP5z^(Ugt}>_;I9TcmeIEXlu_!3D zZKqkUX+)ec>Cj1@3ZaXsVoV!M4%_=}x)M9&$Q*AAklvw5c9Nh4M!(sn?G|tHVsNx> zVEG#>0ikIdfUC(?3uELX1hrvN-Cck6eU>wTIiUNW0 zS33G)YRdXbOk0ooGe9LND8+Wz?V{Gaj{tY(o1E2RxAug0|3N&#W*-2Mh(uV zLsn$P0~pugUQxx$2~Ii$jGBy&l&4_PIg7IrC*n;P6^DJ-EvT$ZxyaOBC#^v*arbqgI?n9Y)al$|; zz3DmByVuWQjO6K#dh1^8tg*w{L{3;y*!r32Asbo^x?M< z&R{iSZkw0XR9}S-eu)%CI*-yqqpBgY`~5~4L-f~)cP_4ss}M%s>?`pAV~na)sd7g3 zx$Ehi0SCT6%F+74rHG6@T8n7dk3jfsD6_HSi3wQ@sQcCnslA7Qo%ldTY`b^9N}RKu zN@VE zEI_=$$MMm#nKsOD{*aUb zeY1_ZdHg~YYgdT%bN=$jFdWPyR46e_`i=j0j z<8jLp?(em&p;HU>8+aAa(*y)l(@>z`lP%{IkrxO**gtQ(Ns8WSnq9y};Rq}M%+6+HTHx(`K6tl<1yupe`@VTs69w;2g?z1q};I>zpxMlQ16;4u~qR??-4c zTC!!i7ABczb|OBe->x}0kt`eiJ5^%C&6VwgrQRfI()&h_0gcrOqxIzyl%nqNS87uj z&8T#mSt}b_ESLcxkuojjw}ayz?@|@rWz5;;5Dzh4jS<)1lVG+K!Dq{f^t60)f~rFo zzdt3tikt-tn(VHl!r*WK{|ucfvz8t?Mqjo+ZyV{BGKf>wHYPCjW{1_9-M_%>Vcr(3 zd*1Y#X6TP{+5osq;UQ%Ua)n??$r2k-)~jS}LOCjjzKb`M&7D-p$D#ZogRITW#!=9D z?uI0V@Z3Y+=KXYBB*UB0pH?37tMM%m;)VW=oMS9xk6bHEXY1L+8OPJE%Dh&yvs)Lw zC#%k6*{2he@dGa1pTPWq4s20B2e{(^u@G2fT4{r1-Jv_X6P+WGc?$>T2J zlz0ipzu<&~C_wx5{dw^5Sj1$2>mXJekmE^CT9{gx|9X-N~gcEIFPb$~Ja z2rcr|+4b@o1eKS)Y#dE58mj%Yba_u2o4V1nB5o*ZhmD2C%1#DrI@0GT;7#^kE)v(3 zIRayaE5@b(0&rF4=|u->Yf2#pcx!ZX`lYQG$J9tElgoNPV=B-YDHap(X!cX^ER!-` z;n3y>P;w~-XMw8d=6LSxLVK_=j<%1Lcof>7PZ(<6MnIoDv){4q&Q(g1jGe2~@on_XP z%U@gO>#SCHADv0Mg&xW+m{0<<9W;h(TSpggw1`fS1$=A=OzkG@)|#KDV=t>LAqOpA zAj~(}yKH9M98by&;O`IJtGaG4TgE*g(N;pYy>*qAI2MsS07k(^pX$0%_299?@z+{> z1?Iwh5ZNE)g8bmr#35Eu;9p)NM`JD_a+t)}Z))nQ`}%VBTqdY^O36PZoV;<-^BD&k z5u`AY+Vs5B9sO1~MA-fKJlf3@zX@oOcX`+VY^_LoZPE8%8akvI@Wk;oqX&h zeWkFT$nvI4KKrOr6hjLxzKdb)ktytTQI(-Rt2#B9mcVv?y@s&E{xN9J`?UG2S1!Vs zg8~y_zEkr$KoW=#i~UCC)Yev!C0tUm}XKe^J2ijWk?&s$BBTYZPV_eAO zO!&2T*sZBbrkd@_#Vo`9+9pV3>tQ9gdrYp4;=D;NWA@;F)!yemJ`WBaoqptq0%z-b zn(707w`9XGJ+M&(?mMBs$l)cCzt6Dpie#D96XM{Su6M`%TQS@nIBX%f7Almy4%_Tt z@orloMXo%3?bjgvd{bG9+JWPDl6BPkm{V%!rS}Ap7I)1*%INIlf8uVS`-)2H)O^?J5saDUnp=Y1J(Ifdm`Pm(S!Uim)h)&6Tx4M7$i z92{H%V&{aVZb$)6#m`JUMGq!21WP@o^w9_@2BW46&3r~jEblS)n;FLqmR4C)`t+5i zAm*t>)21@4>nBt347%*+%bll3R7w_~o?aCB&QHAtZ}yQwZ{Z#{quU2BvWuM^zC>!S zW^`C@#!OQ-K3Nb)MvF^{!u^s6s|+4T1%SbO5MXjID~=)TcA^;r8Z58(PyE;^bieRj z;x)OS8i+7qf0P>mz&Q$xT3L{d*qN=!T7*brVK5QK5V;5=6c-#15w&6o%tv)NligLV z&=xbKucQFjtIka?+9`|=lH-fB9q`HkOU2mYz4GjPMbIQzB@h<`E<_EC;z-fxrdpnn?8Yp6Wy)Nvk`HsQl z=biiIupb}$J#>idRfV3C^W4G-4K4d)>V9)frvu6ji4zl?9@@LkuaOEr>M!<&{Q@MX zXc&(0$j$O+&G)KH=pUa%{Tw5R)6HubI4{C#OkL{k9J?@b7QG&C7$~Otk)%jEzB@9X zg~4JaylTN`{xTeK72doP)y6I-b~~eWTGFQ|x#niPXHM>}o~DC0n5oXkT{pFE(8v=@ zW^i>eYc$-;uW;MVWe9lqUd_UJE#rxu$ky7URPF0vNV*Y0kyq91Q6$tJ`eks3uk**Z zB5g*oR@lJVFJH+^%|z3<5t0wycj;1T;3x3icjJd*_<+1hb2~7yINf1XTt_`pg^k2u4k z)l9#17s=ou9T|#{a`?*5zh38};S;?6jI4o&X`JQG_}#)J+vgb1wr8t{`rUA{n+3?; zt8u>(W6`kTl0}WktVkDIdu!D1WllXeHc)682UP-ZSAw|E4k*Jt?neN!fI`gWXCXsj zVSQ;f2TUxFt474A0-T`Y+Ht;V!fk)}Kgu2Y!GARs<(PPD?|bZi?h^J9D;b5jh7G0f zxsbBCg@R|^b!5ZDV(CzuB!_jG@q^>K2KP)r-?YY}i{lu=w;9pb*q27a#JuX=yw$cA z2kJL9-@Z(dy_MI~?k5z?K`CUGuC*P|g*s(EiDkd6y-%wDrJU-&501u7%lX+?R*RiP zQcj`8g#LD!1JW`2%I>8?PREfZ&WE-Hc+?Rj_;1(FuNzyW>Z)d+H2GEeh2Zl>s^T{K zn50zFi!Gma8Ek(UUKU8rqM{i$g2FhbTK9FNCSb_}zFOe(S{k-_2k6V>7R{3C1b zAJXS1%EYf6HTg}CAd^r|AXcyZKb`q4>}k{pd_%f2}-ryIW( z$7+9HQxNm}$UjOFb~;mXDJzLTL!GNF9b@M6vtpr8e2kI8w(phkdG3mu!GIwEY&4*7 zdaeBn?cV%Plw#7eIs1yD#Z-z$Z|`Y-J(th=1L>ryhrwrcL8j6eME8g=6vCbrvdaf{ zQvN~yI3)^t;Ge^Zo$?#HA6S&E2dH$aH;uzu%*JgBuLM%``tG22Z|t5AeR5x>?g1vn zCj}S|_&(b-0L*JBM<8)nJ+`Jel9g6KYcuSwEAm;l`a&Jh&z5ABm;h#%H@A;VqLXVZ zxOTE8R&>6d??Hd9$oK1yavK1+1L+Yf2&26(#DmEsTR1Y^((zIET7Ne@sP?| zP3pOa(4ZHb*GYt7$*WdN`F2gHn^`oaJFV%v^AS6eEMWDUH?7;*I`VZ9LRYIBh)U<5 zAE>t;wKn)&+8j&Fm-sF5EZ8M=9#)*Hg)3z)H?>?EBDN^Mz5pA?bSGO-eD zB?M_2hc9X^bHt9vyKaHrpC(!#QkE)0fVoiLpM|0eRllc+Mu($O6R@(X-W$%Z` z@Z4q(hX7|YawJx?)}X8Z#fF4tfR^hjn0ALRUTvF>HV_{BM>;1pT{DPpS~?>iCICq^ z-CXH{d^305=;|bt_joa~p;d-!=8;OA#>@E)5)%NK@D+^aTQZbebtoYeYOgeE++n*H zAE)hJ_rCSC`Ov<>5X?f>9;5X51t)!#`F-vBT>Il%N&BF&y2;%_Re&=Yv1Gy@r!dbaom=@%>Sm&6Qem{h3pdfXp&g0>}_V$I-1qn*Zd9^p!zN~z~!i90S zr@sEsum82m{Y(4mKRdyQKaI>$!A+$R7%{O~W!#Zjq{|C5wm)en^?4%PI4rzP4?W}lTwE#6zl$W2$eWI`CJrK$1NNm5l{O_?Ph!5)HR!P zr+MfgD!me#+~UHXEIs?+36*O zKnlk(WkTuyVTjP}``#XP97m}ojQ%!y^U?S7`BsPNQ*@SGc!?D0=>ZG!fv?&$bxU^2 zPxKny>{C1CNvIVP6nYLl;sgNNRZdFVY+%){y z-Tu%_SEaR}GlZr)r%Y^qlo#9FAr;f1q$a_!Jo%yJ0P<8rpO(6$x`=)q*pb!z@v26$ z9RTeyJf*^da542r<4EqJ#IgG0tVdu@jh7bph(MUeEIQw5`Ztw(Q4cVA(qzeAcL>YAw_sDyveU2)f>?m_$gIN4sx#CqHEU}u z?eMD`0>5L5300_u!^4o#;3nHxC9^SnQQ+myaM93$?k-jvWS+YwFYQ>xthHv0wYDbT z6n|8UXbG)h?kd`{+O`_&QrAdUUG%h7Y^6KKfK!@|?Gw@VwvDfgCsKoohgebp7#kfq z6U0N>%;4tO)sJX?HcU_@+mgUUBp(UCv*k;sJ8;rkP}f;>ggeeJ$U`;E($8ZC=r8x} z!tK+fCThg@9(iFvt7YY%s(o+Hn}6C_QD*R$LPMap8M+=C7N*^#NqQF5nNQVfU8Nei z32HstF{yx9Z>lc6jvYN{*$e>I`m=_vl`>jHDFVILMoOg6zx6S99hpZc+ zkBv{0i4mBi=Raqe9K>nyq}CWJ!qPV4zB3o#uXTg94b(o9->`zG_b dNM>%$=Z>ebjD(2xe_y$OE_nZY{~ug|{{T2L-|PSY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4a4b64b11b219c337c377d10fcd3287655cac826 GIT binary patch literal 5641 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 R_m8{4%PKYh&;P~*{vBI%*IfVr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dda4af7f7dfef5f2cbd50470f1b99e92683f52de GIT binary patch literal 11284 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 aXQ6{Ref9al|3kU|%qPSDKjy!91^yd9a(}u2 literal 0 HcmV?d00001 diff --git a/soundpacks/keyboard/kalih-box-white/alt.mp3 b/soundpacks/keyboard/kalih-box-white/alt.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7ba5dc199b78594b3bbcc435906cf97d10da00a4 GIT binary patch literal 6895 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 NIc-({;{V?W{2P-iTKWJ0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7a54bdb9a7e2bcce65db2a2398d1a18686ef66c0 GIT binary patch literal 7522 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≶2FD|OgY&fpcRxJa#RcMd? zYs&q{I+8SVEFL#j#Cd@6hRhN^O#0~DbX05_WMDG4jPo<-8r39B(^|RkMWscq105XX zoZGGooKmwzCQKY6DQ(Z6$z={zaH$v?4;fc2mfH!r9o^8s=hb_NeP1BhZFU$ov~{(r z@&)0ds7lIkUcB)g3GXX>R#ibfW8IQ5-|8sWLpFO<^I=sdhJPBiQMkMlrcojx8ly>2 zRTsGyx5bIJ%nBaNMnacUH_XUG#hM!nn32VHv=|`eH@#=l3R5oIG0T$7Tdu)TrkRUatL^=|Cd6zmBU&KLKRq-K?GJV<<<-K$Iq*C3hj`x;M`a5=!_aB2TL4g(2Cch!$m z0e_|6=zLZl(Twp(#>X#|QWOZl2CBU9xM^*ijJBdvL1UtCf=T-Ua0*3#b53 z#d|xRk;SdmG*VQ@C6N5j-6^k+U19gcVmO}K)}u_O~S^rMc7cJ6c`9-ZYcDPO#o6;cO`#IZJg zNd~P2Co!!F>p%$98lG8qinAdm%WuwkEwv74w%ms=P$v5*7Rg$W66)U@tcA^lBQocS1%^Cd^gL-+`J7f7Vwi!`w`P+Bz!p2Uke&S@waY^wPT;&V~Q2a zYpqdk$hT|S{%W$>T$oD!{j)Y*&2|61&#ViBM9znKyA+Kg& zCDSIL4ROp}X4O$a99ve#*9*l%m5SC#aO zp1D>VeVK;TPt@cVZ=B-7OFT$%V#vd8fdb)es%NCm6SvonkL!g<3PSAO#*UEZEdzYC zjw(XGlc5vb?jp{tDx5L0pzqD}e$sxU*(nz5%p9vjdpp3L&HdQ1T%+Du{X;4l=D62J zMM5o%x2F6&R#g9p|F*ph)>FmZGmPnG+OFvToS=N8BYw&-} z9PA|3AQg$Q*sPjtencQH75}B&f83J&kIn*+oD^5UM0tF(DX1iF1)Nt|$1fN7VWV#4 zFq=YtJcG8!D&3>T4mTZ2U45B|nvx|eI1MBAkfL0V6tRfvyZT)}>S)DqH-_w9D1{)w zcM4P_a-bR&AAj1XvGnA$?~epm!AZNO4@!11*k$lf%z=mp&Z0(Es;fgJEy6?1!Sc#g zH>Q+0d*@#>;x~bXfbeEVCuCJHk+8&b`hlVt{rlFA=2(Gum1QkD{u~y7;&D^@)O6#v^>al!V<)9N z8Owp32rhhDIcuG?8Pp@i*1cxwfQ*>xK?sl!;D|;ZipKO?Wu)GEvjQmXKoj%VQ!cno0#XZJcJW zEH}f_Po9h`E&wpHnp<4O>r0r1I6uL(cX~e)EXGi8UjOf(B=Z2EvuTf(0=2~Zs38OU z;8;AU^Hp?>C3A7vxBKtWVZyZ)b^+leRHAtm0$!GS4hmH;6&xG6T2vMaZz5<6gzt5) zp6dbyKjMTR2PaTOykkCyQm{*6ID(5i<2uDJ5jC|GoQGj8OLQpvkd*lNcS+*u$IJir QjP-xy;{QJM|5yq98(II$qyPW_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..94e7daf43f47876a6c31ad1de854bb6811310e52 GIT binary patch literal 11284 zcmeI2bx>RHzvqLyOVJhy?(W4QK?(%77I!HWEv0C2FYXfDy;yN~cPWM9ZUqWi=u+1dM=$t2H{b57nl^LjqdlXC*QuR;X;2Y?+c9G^D9d3xXh0IY-noM&GC zh3zk*e^LC4-e0W$;_;W@XA=LC|ChSIbpK`QFF*cr`WM(UPoE{MB&jUR!!9J`4EJvV zc{_mcj^+SB$8(N@8^iE_d*{Dj{6BIAV1JE6AY`BNMl^yqE4Wk6jsR$Gf3ds`_JyD1 zEQ4{M0t7XHc&3OfNADzJ`nV#Dp1&E|K~|5vyLgC7OH0euCC7wF(__f-?c=U~)72y% z#(^)Y1ON^J_A}^dQ=cc@o}W?QJSj>70Kmc8VX%(z5m8aj=Oc-VJ+)Avp!9B{>yvEI zh$%_Ob2q`fcu3T-WiIXU@$tiFpKSLJkF+g2_c;zb^D;6V6#!~HJUq(P)zuXMKmq^= zd=}Oh0Dvcd8VZQ@;< zX#G1U(<1yk1?XeU>ii+TMsOAuCOm)PCucUUGKmi_P*f7%d}S^}>Wm#R!2TkA_nV-0 zO%xP%?Q!~{)dKeLFujIgCg=TN`8suZNZsScw0({pd~Q$-@oHkZk&LP@N*|_C4(KcB){#mmA__ zFd4tm86zx)hyd{Iuwb1a{zR2ixEbX6mR{tW;hnt0mwv>snB&9LH>gV)A?b8q_vE8Y zHufiVHD}-8NcsIEFdumuW_3BX*v-ui!T*db=%?J8FA(j4sbHp$W%?9m4W{z5=P@)lT}wLmEi)? za04s#CD62xANW|=Jbja(!?><|&fRyX4Y*(x*nJHmo-bmN#`2?iHhh9jn$Jib0OB2N z{16CWR;oG_ukuAwq9yOeUFL|RMW@UCh_*|bqh_+l=wODr+ev~SVd6<+u+nK{5IV3$ zL(Ux!4Igro**GMj(dkjN;pbN-DhC&vPt%-UbIHuZE>Wsk&ED?Yz}U0vh##yf@|(dl zhA`Nl+&BiM=++-qRaRvhZdxm1Q4q0G8E(4&Jvi^3zo8n9ZyED;@YN4Hs<_N#Ie`$W ztfWCaQjDTZ|BuL48amAi5Jyn_TeHnq;@@)Vuv_Tk`D~V2n`L5y-DXM#v%%f z8d?sn?wD+8*tgg^qrdHR-V$kM6rRA#T~6*W>R**a1u)-1qIdxuS=KeI(Fz_r)rVQy0_}NTGH4#(HcGH(7(yvnHdZi_(c9?qz_2g_T z*H%w=&2PIvSRAEZZUomI>+Sz?+}ycLu8)i(st{HP4JTLC#Qva@ebMUjApcI&^b7kC`;h!sjHOvup?*F6bxyJLDYh1uv^U!o z6&8=|KHzYjMQXjD(4yqN(|<(@3qM9jE?G`cC)BV?h%CebxnRs>Fv1(uo-PEkJIix0 z#7d?Gi>^9%9aUE~xc!MA*v(;3&^bTavSl2`aY?v%8Wze?GZ zv?khz)RnOo?YX-WyQe1VUzMGc+~=K>?7n>2%VdazN!G~pF~NS;Kvc=Wub{y=zLRhf z$uzawnbfzNB_5(q`rD3OM=X|`5zmzItE_dlJ?`&73Wr{)0IVv(%*EBihop4)roVCNMggOBTrg@Lj}=1(hzYpTq`HAUt3JSpnf(JjM~# zI)%Y>a;e$T>i2Ek{AphEa8qr@ZQpnQactW;a*M0OBnzpM#YPLpQiBP-+$_JZ8A>Ik`kYYtkd)`=VFM-^n!(#;-DqQ5u90 z6f+QFwmd8M(ihRnQ2m~X;x1-5^_@9wCWPw)_z>u{FJECT78oT~P@dg{e}G+7;?KL- zMFN#@Q&j`%mAOAv-YMq>=;CmY0@3k1nu>R=>&To1mFWcvcf{o>#QKZLVku+jf;FVw zI+~s0)q7UfBT?^*)=aAq!b;sF@#FJ{$GqW|6a2r)`#TesmvFd7EYq))e-XrPiC;Aq zV3Eq3rMJ+s_ctl~XX58Hq;r&NVW`(*xHYkGi;^<2Hr2~d)4+3@g^%z1ayYEYC#4Rq zj|RAkOLv%JUwRr_lC;-SMqZ5>94Vzn4u~Kbq8D_oy z(_Uj9YD!doLPwv5x?uQnKO8#qwOzFS=R*Utp?)!shG?ml3pu~$JWju z;42tfHL-lRLRjq696NkXS&VBI#2Ah{H402M!-$KMJ;OqOk>qKfxt?^bRLyEq-?jxE ztcv_Hd0f=23>LXhZ1rjARV)}HEeieOf&+M8b1PhjNn(6SnTX!`xWDMr*N@KzKnmB{ z$^!-z|)m<2e7 zSc_8;ZPTY3Zv4NQW)LvTFhndNO`ch;Lotcq+ybp<<>&y2%MfKR zYs#r4X7IW-ZJFwsuFw(ZXqQ0AueQ2WBN?Sk=-%c~Z%{d3Sbc~gmTPHZF5y-K?G2NJ zF6F2>npEMX|FGD%-x`Ht#s=1rBqnKMygwDR$3w5liseZ=Jc|s6%>}1Eqr1o3t=~Ly z_WkiyQF-GJfMX>Rq>mMvB$->CSB?WP9x}s$sJ=XWn3$B!U^E}gidO2|iViG*tiHRs zYpc4QoXR(u0?JRs;pyeaSrw#p0UN8FdWDa&wVo6pC*3w~u2h%OG@loUz9>7^!@jNYgsl-Mhfu91KBPf}|Kl%RX z(4xs!2G4yqMDE3K7&k{0qxRTPNK^j_MWDW9;Phjx7)b7-LLc_-O6W~-CUMl_!#sPV z;qqP;a#&O>yOgrSur1D~1!^Qawlgt)B)6bsybjR)qFVfoeeXbx5vXH^& zC>lv7da@LzQKfm7lYo$92mpX@tTJZS$0^Awg$QMYqd{v;(PA(df{EkySv zhlGT7zsBEVkXRB>Wktp+GS)EaybX(>fS2VGD9d&vvHbAD{m&93wB%ViMqePM#K?U# z_2qh^c9Nc76J=^jwS};?V^N)HI zs5**nn8I8f0U_I#B*O3XKq|$$+(8aZ+Ix|JBHS&*FOP>D@<{!1!9p;h1dNp zbsE_&h*y=>TcJP<+0F^R{`sv#_OweG5S;o1`>8S28mNnTm+uvt+%h7 zDIi*0&Ix?DwN3j~RJ{|Zn-UD2IPCk*AK}@fE{e?utcht_r#K3IE3MK$j4HaLSjf~WIb0`z1e?ywhzt~UmMTPv->;~O~N6-WT^WkT!wp}%WkFyMBivk5?@r%I$w1Pu$NuK5(30n}Hnv?IO3VA0b#D#zAs4JE%S8K5!R(WezNp@{8e1dPt>P zUDPO16SzqzZjvt1lmmu^Op;RyG$gl%gwSd+NO|>| zoBHXnTdU`7xKu+@@RjEu(T7*?W4s3;BN{p?eDpL-DzJUw4n>Z!?o*?k(8k8cCZT~vQke9#WK;^ zomE}4GWMO8X~x6v#uA+}bfK02CC1?yo#D+lT{n(iLyCXVrhm&N5BKcpX_rdrJyh^% z)w^$m-W3J|&2L?!KU&}4H!7Yrdr~lbf+|&Sl6i^r@!%+up%_gS=O~e|^gSyl;)|Hf zC(E+rfg6ilRN%Sy7RPGwRYexzI#(O5TOK<%pP$ znj3?as8fwR$feAX9CJZe?ecDnZdyOI>?bipe>!wMp_)O8Wk*@BgjQ`dt-1Z0>?_%% zWZdO9)(nmE&(+0(yd_o|h%6QJEr;Cfkv_W&$9wsycq?AHbA2CUb{u{3L2%kYD*B&J zP!Gu{QwcI-K|)Lb7!C=E?fghvEcP%uODr$m{A&2(vWbAp+St@M7hrHl%0agvY15|` zGjXomeXLjEaxD}jCh1*FwXTRhh(@4-SMD_ESv(rcA6X9=BxG}FJdDK*)CCj`cZ@L4 zQP+%_h`qXTNK;Dn)m^l}Rq%$|^TgqF2T@}iLy2XHT(iZ?9pwwnqt(2s48p(2) zCCyb7yh}uZR{lQ&akaQd$q0zw;x3G8i@0(2iGx3mjt<3?#l*QEx1V1G+uOgjLDuqp zsz%w);1Mw1JUWx-i8;(tF4Qv95FhLoILfRP_&%?iTMz?22TlnbX0BlZ#Yn)(D@wqC zp1OW1C=R3Ioo3C|(O{Y%U3g>A6qzwr&ouOKS@26Qsd#`Gd5|^ z!>oGxcWNxo2R^$>*dYuyGNGef_(=S4)}y=0Ou&I8lZp{QUjTiSUvbxKT`53wxN4Lp zpZ$I0Z$M}&wMPgB`1>+I2Z~sAs*@bw*AmdV+|kU)Ft@$C8-klHwcT$z{`Rb#o-YEs zAow5!glZGiWN0^#hD(k1frzb`!yfL_O&2?oWEYmjC;V zEP8WnJ|s}=j{3vSEBDX#HF27XVIhF#RP5mIFOH)1!kL^DiG**3Tf1Y*ZjUwFS$_T6 z{N$>fW_VtguK&&JQor$SZ_?5I*W7jQDLz=fOkeH0t27wUit;yhK0GIv4(ai)MhADD zYqFNexf6=iNU0rLo|RbY{vX!$gtjz+orp8`AIwH-epPG8Jj$<8z5KJ>oGU~83I)(s z%NM&Rn$o9wD1uxd-Q#HfyGP-`Ej+9ZrLULe^4O0?-&7N6=%{+x#LCw^eoL_$*r=3|;POw=vMC=bPUO|QE8ZC}X{!XlU z00c!ru&p(>l|x~1w0X!o%!Qw{<-p+|xmnP9X!0ktWoTQVv99NawLnMJsjq$Ad+Xgu z+(Zn5oL4(!ezziN5BJ7q)>DW?1!S(BwT#Dv!u z5vB3)6ozJT`Qf4Jf^IFtuJOvcve?_u=6~H~VCO6OzXMWg^}vu!K;*(#tZo@57J%^* zcmyuMH^7KJ#?bGkB zcOM_t?%N-YbRCv&+$ZbQmRo&&0}vFtHia_^YnkH3UFpRU7@+Y|Og7>nOADLx_%A%8 zoRJsYW2;7#uuv=u@Kdg$>Wb7O!CZ>|l_jyL6ljMw2N&}cvxm7DiKgCHJTs|r3G1|i zgy>Z^DpZ)7-QGW8%FFYsP<{JlulDj9wO(+PPAc4#pw^I;qigt_xOW?C?|HA)*qYw( zrtbTypLLq0^tH&MZk~#E%wi!eOlEY|;`SL{hAm#8s2NO>enyyWPQu}T+zCbyLYiK3vgssn+MO=!QJjw&*mlKN`u@~vO${7I>Qhuw3P z`wk$)7VMoaQ#f@(SroGmd57gP%ur$MOI??>_0UK42URYycXz%*$bv_ZQ1A$TO49ZW zo;1`f5|2XFm0D@W+OD**WuUWjAsLs{XXb*>yJSKDH}u7~tm~WG)P0X}NmFYH4E|no z;ks2^5}5tllK%q1@DKREKa|+xiyN}2PTo2fS?590J{<{gY&RP;!J=O>c}0)oVFX_Zb=SHiGj^nzqMzigzg}8V#gqrDgS=9 z<*|llk-f$K!9F+QkDs5_A?ufGH(SlSh4J%5T}z#NK7~iz5?=$xV&V!~m zi7y$JAmw_tQFm&RdzAC80vu=KJ`3|jwX!7>zx%ipl%Fo0!(dRk)y9L*qq%4~F`R7X zSXjQlMuvXIdX*ZuQ}QDzSPV0Zp5qVu)`+G(vv6EP^S*ZjZfCP(E60> zTsY;KW!aw;JQJGW~oAEJ5%q0!~x>yz2 zKNg7LQ;_2lAd?f8X+q86^@e_wQSGHyGV@bjSs+smL1{rC%Vvf^z ziHRAts5mB1``9pq+7rZC{jGUU1nlr5r? zOyt07se7e%YgXMaNx13OsArT(qgdN7zeapz zsvMGrc^}vep&lR&-wCXewV1v4-W=cnbu7+BUe`eDP|1Gq(qjVIN7N(}-d77Od_|}4 zN#DFr%R?;wWp~9Zt4h6aoeMHro(m0G8`{Avf`Df6@!1olM7Hwy83SasXK?V7^ss6e zc@3r}JlUohw|#k8k3=x6KZmCcp<%yFZH5W#o=Ev<(IC57dbr!e=6HjL$)-4|NaN~m z9FR53;5iYd7#S4kg$?RRv_>wr2w#8cvgxiac3+ku2zGf9@0yFqb6+|Tkz9iMdm{y4 zMCDy_+LF1}%_1x}E86WmJbI68j}lj$v)Mn0yjZ2M_yB{g!zLhkm({!jC`c)hI8o{V zKME}p(^pw`e2*6EL>q>9w~Kzw^3%lNJjq(N@3%TK4wOP+dMd|d33p#w_M=yH$98P$ zyHn{c63*4$*Szz7Mf3UuI$D&!{d;k8wBh9K&Br>oVJ57Uln?Q~A;njmq+Wq`Btz(I zN;D6Ab=^nb+i%1s@hWPDWNJiKf-2WxsvZF1M5u)5I~l=CqYh8&uiGE^Y(!3PEG!() z6E!toaBO=ty3>>r=;o36%{VCGzr#{qWvCL@_D;{D1zp`+RUJ$;Cu9y7}PT_Bvn%v zSJ7b5aoHNt#_dWt(e`oS<|QnK5~(h?#*oD+*X?}Lrg8!zqGrJ4<>OpZw}{x%i?DO~ zy%nY>qd%Q^zmmA8idThf25hF&LOhAn*8|4^1%jK0GOy!ADjE(+}O2TRh4tM zg0La`uIg&vUY;uClgash%zQ~ay`jRo@P0Rs-@RS6i@a?Td6{*-+FP5NqYFoGir}+^ z6waz@pI5BS=50(Q6gX12UA{_{g=fbGxBG-EPIv$;mad}QyVvbb5OG5)z$=A>WDqhH zUu)Ex_J)%_I~{1L3fnNT*)+4Y*1#Gz2HXaTnkX|}K>%dF7#>kouum3Vy?4bW1O!)1 zR;jw@?*W zZ;Y!VeHTROF<@PAOb$B2EFp)#!%6-|vQDm?udG#1F|;Vk&<7G7jb^` z?J0toURKHweB4CZ%6kB2Td|T z@z=%Z%UO;!5EPPEQLiqYk`So&N`HqHl?E_ddiarW0|_EXhGR_$KWUt$#*T`OjzT zGFh$|aK;b*3@m6W!^0FQ-zfRpz8}BV{j5!Uh`|Sx#f}Nrpya&?|2VPrz5%t?LiJ#_ zH5qM96ZZT!4_@@JvXPTlFj`PL+7=y;_NYEriFNoS+S2s56TuA-CIWgRf6q=2!$dTi zVl^NaPiXEt+e}Os;cGl&pqSvwJ=x`i#WhjTIM@UYZ97_v>sJeRc$E&t>%8mSU%dn4 zlLAPDoYmm|ZzEB`yuBk(^`9b%RREN9?PL;qATlay*HW<1XLaVn0HDV7%w_UVOYPL} zHVZ}WHQVE?x1Yzv%T3+*4d@V!Cwzzs!oyNlR4=eH{#qn89w z0DYE`6j0zYsiwKBX$?^#*UJK_A$c+lu7tuC+_Fb=O=-azY;{hne7YLS+3oTWsEmMY z{mAZ04`(hI7&az26sTLXwmlgF^aK2nA z+t#Q9%gw79x-y6XWtUz;em)Uk()T2hBQ4}AbYN*K$SpY_^}NmAhX*1CP=CQ!Vp2q>=QP%oCbcQi3x$(2g{Tr* z!1MjR$2#GQh{7$cP7gZuIRHf((|*Qo3r8tTw%^<3dAH{mf%u@Q!38lL>pksCB+-Jw z;gcr5?5kCkich}!$z06il4g1>UW6pmprgk6p_H_k(|9INjhn2}ZNJZSe~ORMSk1Fo dfCz-D3PJz!_lJ$tzm)sOs{g+m|9_l;{{gIR^1uK9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..770b929bd3b150ad87a4527ef86540e9022ce2d7 GIT binary patch literal 11284 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 Ny@>z&?f<|T_z$l#za#(v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2490ac31661027fc688babba301c1abdf05f36d4 GIT binary patch literal 6268 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 F{sTWljlBQ> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..daac6829f447333b48672cc3906f19ea85c55fff GIT binary patch literal 9403 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d05eebfd04d375b3cabfdf94d599e5b6898536da GIT binary patch literal 12537 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..16b4115d40d85cab2b0dbe73b80a5abacd634686 GIT binary patch literal 6895 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 VvEoUUk6U;Cl_ArA^Z%~`{{c>*&(8n= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..485ed835c30d5927aa551b98648db9ff3909b8a4 GIT binary patch literal 8149 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 z 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 Qxcy&}@;~VIKV+Q$2Lgu!RR910 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..945eac5ec6c1ac3dc42d6518326ee03627305473 GIT binary patch literal 35108 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 W$yuiUu77hO{A&~G{{{c?3H%9SM*r&o literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4bd2435eae2f265582adf3ba220f96b10ae071c5 GIT binary patch literal 29584 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 literal 0 HcmV?d00001 diff --git a/src/keyvibe_audio.c b/src/keyvibe_audio.c index 6683a60..e16dc96 100644 --- a/src/keyvibe_audio.c +++ b/src/keyvibe_audio.c @@ -1,8 +1,6 @@ - #define _GNU_SOURCE -#include "utils.h" -#include #include +#include "utils.h" #include #include #include @@ -14,6 +12,7 @@ #include #include #include +#include #define MAX_LINE_LENGTH 1024 #define MAX_CONCURRENT_SOUNDS 10 @@ -28,11 +27,11 @@ typedef struct { char generic_press_files[5][256]; int num_generic_press_files; char sound_file[256]; - SoundMapping key_mappings[256]; + SoundMapping key_mappings[512]; struct { char *press; char *release; - } multi_key_mappings[256]; + } multi_key_mappings[512]; int is_multi; SF_INFO sf_info; } SoundPack; @@ -43,13 +42,16 @@ typedef struct { } PlaybackData; 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; 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) { @@ -119,8 +121,7 @@ int load_sound_config(const char *config_path) { 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 (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]; @@ -175,24 +176,35 @@ int load_sound_config(const char *config_path) { 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 (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; + + // Handle mouse key names first + 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); + // Handle regular key codes with -up suffix + 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) { + + if (key_code >= 0 && key_code < 512) { 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, @@ -212,26 +224,62 @@ int load_sound_config(const char *config_path) { } } } else { - if (json_object_object_get_ex(root, "sound", &obj)) { + // Try "sound" first, then "audio_file" for mouse configs + if (json_object_object_get_ex(root, "sound", &obj) || + json_object_object_get_ex(root, "audio_file", &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 (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)); + // Try "defines" first, then "definitions" for mouse configs + 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; + + // Handle mouse key names + 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) { + // Handle different config formats + if (json_object_is_type(val, json_type_array)) { + // Standard format: [start_ms, duration_ms] + 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)) { + // Mouse format: {"timing": [[start1, duration1], [start2, duration2]]} + 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) { + // Use the first timing entry + 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)); + } + } + } } } } @@ -274,20 +322,27 @@ void *play_sound_thread(void *arg) { int thread_id = data->thread_id; int is_pressed = data->is_pressed; const char *file_to_play = NULL; + + // Determine if this is a mouse event (key codes 272, 273, 274) + 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; + if (g_verbose) { - printf("Thread %d: Playing sound for key %d (%s)\n", thread_id, key_code, + 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_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 (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, @@ -328,7 +383,7 @@ void *play_sound_thread(void *arg) { 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); + buffer[i] = (short)(buffer[i] * volume); } int pa_write_error; if (pa_simple_write(pa_handle, buffer, @@ -345,16 +400,16 @@ void *play_sound_thread(void *arg) { sf_close(sf); free(buffer); } else { - if (key_code >= 256 || - g_sound_pack.key_mappings[key_code].duration_ms == 0) { + 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 = &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); + 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; @@ -384,7 +439,7 @@ void *play_sound_thread(void *arg) { } 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); + buffer[i] = (short)(buffer[i] * volume); } int pa_write_error, pa_drain_error; pa_simple_write(pa_handle, buffer, @@ -489,10 +544,9 @@ int parse_keyboard_event(const char *json_line, int *key_code, } void cleanup() { - if (g_verbose) - printf("Cleaning up...\n"); + if (g_verbose) printf("Cleaning up...\n"); usleep(500000); - for (int i = 0; i < 256; i++) { + for (int i = 0; i < 512; 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; @@ -515,16 +569,15 @@ static int read_mute_state() { if (!rd || strlen(rd) == 0) { rd = "/tmp"; } - if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/keyvibe-mute-%d", rd, - (int)getuid())) { + 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); @@ -534,13 +587,15 @@ static int read_mute_state() { return 0; } + int main(int argc, char *argv[]) { - if (argc < 2 || argc > 5) { - fprintf(stderr, "Usage: %s [volume] [verbose] [mute]\n", - argv[0]); + if (argc < 2 || argc > 7) { + fprintf(stderr, "Usage: %s [volume] [verbose] [mute] [mouse_config] [mouse_volume]\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"); + fprintf(stderr, " mouse_config: path to mouse config.json (optional)\n"); + fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n"); return 1; } if (argc >= 3) { @@ -550,11 +605,9 @@ int main(int argc, char *argv[]) { if (volume_percent > 100) volume_percent = 100; g_volume = volume_percent / 100.0f; - if (g_verbose) - printf("Volume set to: %d%%\n", volume_percent); + if (g_verbose) printf("Volume set to: %d%%\n", volume_percent); } else { - if (g_verbose) - printf("Volume set to: 50%% (default)\n"); + if (g_verbose) printf("Volume set to: 50%% (default)\n"); } if (argc >= 4) { g_verbose = atoi(argv[3]); @@ -568,8 +621,32 @@ int main(int argc, char *argv[]) { printf("Sound muted\n"); } } + + // Parse mouse config and volume if provided + if (argc >= 6) { + if (load_sound_config(argv[5]) != 0) { + fprintf(stderr, "Failed to load mouse sound configuration\n"); + return 1; + } + // Copy the loaded config to mouse sound pack + 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 = atoi(argv[6]); + if (mouse_volume_percent < 0) + mouse_volume_percent = 0; + if (mouse_volume_percent > 100) + mouse_volume_percent = 100; + g_mouse_volume = mouse_volume_percent / 100.0f; + if (g_verbose) printf("Mouse volume set to: %d%%\n", mouse_volume_percent); + } + if (load_sound_config(argv[1]) != 0) { - fprintf(stderr, "Failed to load sound configuration\n"); + fprintf(stderr, "Failed to load keyboard sound configuration\n"); return 1; } if (init_audio() != 0) { @@ -583,7 +660,7 @@ int main(int argc, char *argv[]) { 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; @@ -605,8 +682,7 @@ int main(int argc, char *argv[]) { 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"); + if (g_verbose) printf("EOF reached on stdin\n"); } else { perror("fgets"); } diff --git a/src/keyvibe_input.c b/src/keyvibe_input.c index e70fdbf..90f04d0 100644 --- a/src/keyvibe_input.c +++ b/src/keyvibe_input.c @@ -1,18 +1,19 @@ -#include "utils.h" #include -#include -#include -#include -#include -#include -#include -#include #include #include #include +#include "utils.h" #include #include + +#include +#include +#include +#include #include +#include +#include +#include #include "config.h" @@ -126,13 +127,11 @@ static int run_mainloop(struct libinput *libinput) { fd.events = POLLIN; fd.revents = 0; if (handle_events(libinput) != 0) - return errorf("Expected device added events on startup but got none. Maybe " - "you don't have the right permissions?\n"); + return errorf("Expected device added events on startup but got none. Maybe you don't have the right permissions?\n"); while (1) { int pr = poll(&fd, 1, -1); if (pr < 0) { - if (errno == EINTR) - continue; + if (errno == EINTR) continue; return errorf("poll failed: %s\n", strerror(errno)); } handle_events(libinput); @@ -192,8 +191,7 @@ int main(int argc, char *argv[]) { } pthread_t input_handler; struct input_handler_data input_handler_data = {udev, libinput}; - if (pthread_create(&input_handler, NULL, handle_input, &input_handler_data) != - 0) { + if (pthread_create(&input_handler, NULL, handle_input, &input_handler_data) != 0) { errorf("Failed to create input handler thread.\n"); } else { pthread_detach(input_handler); diff --git a/src/keyvibe_main.c b/src/keyvibe_main.c index b782cd9..5b88cfd 100644 --- a/src/keyvibe_main.c +++ b/src/keyvibe_main.c @@ -10,8 +10,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -22,8 +22,11 @@ #include #define MAX_PATH_LENGTH 1024 -#define AUDIO_BASE_DIR KeyVibe_DATA_DIR "/audio" -#define USER_AUDIO_SUBPATH "/.local/share/keyvibe/audio" +#define AUDIO_BASE_DIR KeyVibe_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/keyvibe/soundpacks/keyboard" +#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/mouse" #define USER_CONFIG_FILENAME ".keyvibe.json" // Global variables for cleanup @@ -32,19 +35,29 @@ 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 volatile sig_atomic_t keyboard_reload_requested = 0; +static volatile sig_atomic_t mouse_reload_requested = 0; +static volatile sig_atomic_t volume_reload_requested = 0; +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 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; 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(" -S, --sound SOUND_NAME Select keyboard sound pack (default: eg-oreo)\n"); + printf(" -M, --mouse SOUND_NAME Select mouse sound pack (default: ping)\n"); + printf(" -V, --volume VOLUME Set volume [0-100] for both keyboard and mouse (default: 50)\n"); + printf(" -K, --keyboard-volume VOLUME Set keyboard volume [0-100] (default: 50)\n"); + printf(" -O, --mouse-volume VOLUME Set mouse 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"); @@ -61,31 +74,31 @@ int list_sound_packs() { char path[MAX_PATH_LENGTH]; struct stat st; - // Determine user audio dir - char user_audio_dir[MAX_PATH_LENGTH] = {0}; + // Determine user audio dirs + char user_keyboard_dir[MAX_PATH_LENGTH] = {0}; + char user_mouse_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 (pw && pw->pw_dir) home = pw->pw_dir; } if (home) { - snprintf(user_audio_dir, sizeof(user_audio_dir), "%s%s", home, - USER_AUDIO_SUBPATH); + 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"); + printf("======================\n\n"); - // List user packs first (if available) - if (user_audio_dir[0] && (dir = opendir(user_audio_dir)) != NULL) { + printf("Keyboard Sound Packs:\n"); + printf("---------------------\n"); + + // List user keyboard packs first (if available) + 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_audio_dir, - entry->d_name) >= sizeof(path)) { - fprintf(stderr, "Warning: Path too long, skipping: %s/%s\n", - user_audio_dir, entry->d_name); + 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)) { @@ -95,16 +108,47 @@ int list_sound_packs() { closedir(dir); } - // Then list system packs - dir = opendir(AUDIO_BASE_DIR); + // Then list system keyboard packs + dir = opendir(KEYBOARD_AUDIO_DIR); if (dir != NULL) { while ((entry = readdir(dir)) != NULL) { - if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + 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 ((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); + } + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + printf(" %s (system)\n", entry->d_name); + } + } + closedir(dir); + } + + printf("\nMouse Sound Packs:\n"); + printf("------------------\n"); + + // List user mouse packs first (if available) + 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(" %s (user)\n", entry->d_name); + } + } + closedir(dir); + } + + // Then list system mouse packs + 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)) { @@ -117,60 +161,101 @@ int list_sound_packs() { return 0; } -static void get_user_audio_dir(char *buffer, size_t buflen) { +static void get_user_keyboard_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 (pw && pw->pw_dir) home = pw->pw_dir; } - if (home) - snprintf(buffer, buflen, "%s%s", home, USER_AUDIO_SUBPATH); + if (home) snprintf(buffer, buflen, "%s%s", home, USER_KEYBOARD_AUDIO_SUBPATH); } -static int resolve_sound_base_dir(const char *sound_name, char *out_basedir, - size_t out_sz) { +static void get_user_mouse_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_MOUSE_AUDIO_SUBPATH); +} + +static int resolve_keyboard_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)); + get_user_keyboard_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'; + 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)) { + if ((size_t)snprintf(p2, sizeof(p2), "%s/%s/config.json", KEYBOARD_AUDIO_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'; + strncpy(out_basedir, KEYBOARD_AUDIO_DIR, out_sz - 1); out_basedir[out_sz-1] = '\0'; return 1; } return 0; } -int validate_sound_pack(const char *sound_name) { +static int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir, size_t out_sz) { + char user_audio_dir[MAX_PATH_LENGTH]; + get_user_mouse_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", MOUSE_AUDIO_DIR, sound_name) >= sizeof(p2)) { + return 0; // Path too long + } + if (access(p2, R_OK) == 0) { + strncpy(out_basedir, MOUSE_AUDIO_DIR, out_sz - 1); out_basedir[out_sz-1] = '\0'; + return 1; + } + return 0; +} + +int validate_keyboard_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); + 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[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); + 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; +} + +int validate_mouse_sound_pack(const char *sound_name) { + char basedir[MAX_PATH_LENGTH]; + 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[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) { @@ -213,11 +298,12 @@ static char *get_user_config_path(char *buffer, size_t buflen) { return buffer; } -static int build_paths_for_sound(const char *sound_name, char *out_config_path, + +static 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[MAX_PATH_LENGTH]; - if (!resolve_sound_base_dir(sound_name, basedir, sizeof(basedir))) { + if (!resolve_keyboard_sound_base_dir(sound_name, basedir, sizeof(basedir))) { return 0; } size_t base_len = strlen(basedir); @@ -232,20 +318,45 @@ static int build_paths_for_sound(const char *sound_name, char *out_config_path, 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)) + 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; } +static 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[MAX_PATH_LENGTH]; + 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) { + 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)) { + if (!read_pidfile(pidfile_path, &running_pid) || !process_is_running(running_pid)) { fprintf(stderr, "KeyVibe: not running.\n"); return 0; } @@ -292,8 +403,11 @@ static void stop_children() { } static int start_children(const char *sound_dir, const char *config_path, - int volume, int verbose, int mute) { + int volume, int verbose, int mute, + const char *mouse_sound_dir, const char *mouse_config_path, + int mouse_volume) { (void)config_path; + (void)mouse_sound_dir; int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); @@ -319,12 +433,14 @@ static int start_children(const char *sound_dir, const char *config_path, } char volume_str[32]; snprintf(volume_str, sizeof(volume_str), "%d", volume); + char mouse_volume_str[32]; + snprintf(mouse_volume_str, sizeof(mouse_volume_str), "%d", mouse_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); + verbose_str, mute_str, mouse_config_path, mouse_volume_str, (char *)NULL); perror("execl keyvibe-audio"); exit(1); } @@ -360,9 +476,8 @@ static void handle_sighup(int sig) { 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()); - + 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); @@ -370,6 +485,7 @@ static void write_mute_state(int mute) { } } + struct inotify_thread_args { char path[MAX_PATH_LENGTH]; }; @@ -377,24 +493,33 @@ struct inotify_thread_args { 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) + 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); + } + 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 = 200000000L; + ts.tv_nsec = 100000000L; // 100ms nanosleep(&ts, NULL); continue; } + printf("inotify read error: %s\n", strerror(errno)); + break; + } + printf("Config file changed, requesting reload...\n"); reload_requested = 1; } inotify_rm_watch(fd, wd); @@ -402,12 +527,23 @@ static void *inotify_thread_fn(void *arg) { return NULL; } -static int write_user_config(const char *path, const char *sound, int volume) { +static int write_user_config(const char *path, const char *keyboard_sound, const char *mouse_sound, int keyboard_volume, int mouse_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)); + + // Create keyboard section + json_object *keyboard = json_object_new_object(); + 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); + + // Create mouse section + json_object *mouse = json_object_new_object(); + 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"); @@ -421,8 +557,8 @@ static int write_user_config(const char *path, const char *sound, int volume) { return 1; } -static int read_user_config(const char *path, char **out_sound, - int *out_volume) { +static int read_user_config(const char *path, char **out_keyboard_sound, char **out_mouse_sound, + int *out_keyboard_volume, int *out_mouse_volume) { FILE *f = fopen(path, "r"); if (!f) return 0; @@ -450,74 +586,110 @@ static int read_user_config(const char *path, char **out_sound, free(buf); if (!root) return 0; + + // Try new format first (keyboard/mouse sections) + 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)) { + // New format + json_object *o; + if (json_object_object_get_ex(keyboard_obj, "keyboard_sound", &o)) { + const char *s = json_object_get_string(o); + if (s) + *out_keyboard_sound = strdup(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 = strdup(s); + } + + // Read separate volumes for keyboard and mouse + 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 - try old single sound/volume format 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 (s) { + *out_keyboard_sound = strdup(s); + *out_mouse_sound = strdup("ping"); // default mouse sound + } } if (json_object_object_get_ex(root, "volume", &o)) { - *out_volume = json_object_get_int(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; } int main(int argc, char *argv[]) { - char *sound_name = "eg-oreo"; // may be replaced by strdup; track for free - int sound_name_owned = 0; + char *sound_name = strdup("eg-oreo"); // may be replaced by strdup; track for free + char *mouse_sound_name = strdup("ping"); // may be replaced by strdup; track for free + 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; - 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}}; + 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'}, + {"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; + char *cli_mouse_sound = NULL; int cli_volume = -1; + int cli_keyboard_volume = -1; + int cli_mouse_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 + char *cfg_keyboard_sound = NULL; + char *cfg_mouse_sound = NULL; + int cfg_keyboard_volume = volume; + int cfg_mouse_volume = volume; + if (read_user_config(user_cfg_path, &cfg_keyboard_sound, &cfg_mouse_sound, &cfg_keyboard_volume, &cfg_mouse_volume)) { + if (cfg_keyboard_sound) { + if (sound_name_owned) { free(sound_name); } + sound_name = cfg_keyboard_sound; // now owned, must free on exit paths sound_name_owned = 1; } - volume = cfg_volume; + if (cfg_mouse_sound) { + if (mouse_sound_name_owned) { free(mouse_sound_name); } + mouse_sound_name = cfg_mouse_sound; // now owned, must free on exit paths + mouse_sound_name_owned = 1; + } + volume = cfg_keyboard_volume; // Use keyboard volume as main volume for CLI compatibility } } } 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; - } + 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==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)) != + while ((opt = getopt_long(argc, argv, "S:M:V:K:O:lhdsmuv", long_options, NULL)) != -1) { switch (opt) { case 'd': @@ -528,37 +700,26 @@ int main(int argc, char *argv[]) { break; case 'm': { pid_t running_pid = 0; - if (!require_running_pid(&running_pid)) { - if (sound_name_owned) { - free(sound_name); - } - return 1; - } + 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); - } + 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; - } + 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); - } + if (sound_name_owned) { free(sound_name); } return 0; } case 'S': cli_sound = optarg; break; + case 'M': + cli_mouse_sound = optarg; + break; case 'V': cli_volume = atoi(optarg); if (cli_volume < 0) @@ -566,41 +727,47 @@ int main(int argc, char *argv[]) { if (cli_volume > 100) cli_volume = 100; break; + case 'K': + cli_keyboard_volume = atoi(optarg); + if (cli_keyboard_volume < 0) + cli_keyboard_volume = 0; + if (cli_keyboard_volume > 100) + cli_keyboard_volume = 100; + break; + case 'O': + cli_mouse_volume = atoi(optarg); + if (cli_mouse_volume < 0) + cli_mouse_volume = 0; + if (cli_mouse_volume > 100) + cli_mouse_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); - } + 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); - } + if (sound_name_owned) { free(sound_name); } return 1; } } if (list_sounds) { int rc = list_sound_packs(); - if (sound_name_owned) { - free(sound_name); - } + 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); + 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); @@ -620,40 +787,57 @@ int main(int argc, char *argv[]) { if (sound_name_owned) { free(sound_name); } - sound_name = cli_sound; + 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; + if (cli_mouse_sound != NULL) { + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } + mouse_sound_name = cli_mouse_sound; + mouse_sound_name_owned = 0; // cli_mouse_sound is not owned by us config_updated = 1; } - + if (cli_volume >= 0) { + volume = cli_volume; + config_updated = 1; + } + if (cli_keyboard_volume >= 0) { + current_keyboard_volume = cli_keyboard_volume; + config_updated = 1; + } + if (cli_mouse_volume >= 0) { + current_mouse_volume = cli_mouse_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); + 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)) { + 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 (!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)) { + if (!write_user_config(user_cfg_path, sound_name, mouse_sound_name, current_keyboard_volume, current_mouse_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); - } + 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]; @@ -662,10 +846,8 @@ int main(int argc, char *argv[]) { "%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); + 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) { @@ -682,34 +864,53 @@ int main(int argc, char *argv[]) { } char config_path[MAX_PATH_LENGTH]; char sound_dir[MAX_PATH_LENGTH]; - if (!build_paths_for_sound(sound_name, config_path, sizeof(config_path), + 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; } - current_volume = volume; + 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; strncpy(current_sound_name, sound_name, sizeof(current_sound_name) - 1); current_sound_name[sizeof(current_sound_name) - 1] = '\0'; + strncpy(current_mouse_sound_name, mouse_sound_name, sizeof(current_mouse_sound_name) - 1); + current_mouse_sound_name[sizeof(current_mouse_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'; + strncpy(current_mouse_config_path, mouse_config_path, sizeof(current_mouse_config_path) - 1); + current_mouse_config_path[sizeof(current_mouse_config_path) - 1] = '\0'; + strncpy(current_mouse_sound_dir, mouse_sound_dir, sizeof(current_mouse_sound_dir) - 1); + current_mouse_sound_dir[sizeof(current_mouse_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("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("KeyVibe started with sound pack: %s\n", sound_name); + printf("KeyVibe started with keyboard sound pack: %s, mouse sound pack: %s\n", sound_name, mouse_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); - } + if (!start_children(sound_dir, config_path, volume, verbose, current_mute, + mouse_sound_dir, mouse_config_path, current_mouse_volume)) { + if (sound_name_owned) { free(sound_name); } return 1; } int status; @@ -717,43 +918,119 @@ int main(int argc, char *argv[]) { signal(SIGHUP, handle_sighup); pthread_t inotify_thread; struct inotify_thread_args in_args; - if (is_daemon) { + // Enable live reloading for both daemon and non-daemon modes 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'; + printf("Setting up file watcher for: %s\n", in_args.path); + + // Check if the config file exists + if (access(in_args.path, F_OK) == 0) { pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args); pthread_detach(inotify_thread); + } else { + printf("Warning: Config file %s does not exist, file watching disabled\n", in_args.path); } + } 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))) { - 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); + char *cfg_keyboard_sound = NULL; + char *cfg_mouse_sound = NULL; + int cfg_keyboard_volume = current_keyboard_volume; + int cfg_mouse_volume = current_mouse_volume; + + if (read_user_config(user_cfg_path3, &cfg_keyboard_sound, &cfg_mouse_sound, &cfg_keyboard_volume, &cfg_mouse_volume)) { + printf("Read config - keyboard:%s mouse:%s kvol:%d mvol:%d (current kvol:%d mvol:%d)\n", + cfg_keyboard_sound ? cfg_keyboard_sound : "NULL", + cfg_mouse_sound ? cfg_mouse_sound : "NULL", + cfg_keyboard_volume, cfg_mouse_volume, current_keyboard_volume, current_mouse_volume); + + // Check what changed + int keyboard_changed = 0; + int mouse_changed = 0; + int keyboard_volume_changed = 0; + int mouse_volume_changed = 0; + + if (cfg_keyboard_sound && strcmp(current_sound_name, cfg_keyboard_sound) != 0) { + keyboard_changed = 1; + strncpy(current_sound_name, cfg_keyboard_sound, sizeof(current_sound_name) - 1); + current_sound_name[sizeof(current_sound_name) - 1] = '\0'; } - current_volume = cfg_volume; - } - } - if (validate_sound_pack(current_sound_name)) { - if (!build_paths_for_sound(current_sound_name, current_config_path, + free(cfg_keyboard_sound); + + if (cfg_mouse_sound && strcmp(current_mouse_sound_name, cfg_mouse_sound) != 0) { + mouse_changed = 1; + strncpy(current_mouse_sound_name, cfg_mouse_sound, sizeof(current_mouse_sound_name) - 1); + current_mouse_sound_name[sizeof(current_mouse_sound_name) - 1] = '\0'; + } + 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; + } + + printf("Config changed - keyboard:%d mouse:%d kvol:%d mvol:%d\n", + keyboard_changed, mouse_changed, keyboard_volume_changed, mouse_volume_changed); + + // Handle the changes immediately + if (keyboard_changed || mouse_changed || keyboard_volume_changed || mouse_volume_changed) { + // Validate both sound packs + 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) { + // Update keyboard paths + if (build_paths_for_keyboard_sound(current_sound_name, current_config_path, sizeof(current_config_path), current_sound_dir, sizeof(current_sound_dir))) { - continue; - } + // Update mouse paths + if (build_paths_for_mouse_sound(current_mouse_sound_name, current_mouse_config_path, + sizeof(current_mouse_config_path), + current_mouse_sound_dir, + sizeof(current_mouse_sound_dir))) { + // Restart children with new config (including volume changes) stop_children(); - start_children(current_sound_dir, current_config_path, current_volume, - current_verbose, current_mute); + 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); + 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); + } 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"); + } + } else { + printf("Failed to get user config path\n"); } - continue; } finished_pid = waitpid(-1, &status, WNOHANG); if (finished_pid == 0) { @@ -786,8 +1063,7 @@ int main(int argc, char *argv[]) { if (!is_daemon) { printf("KeyVibe exited.\n"); } - if (sound_name_owned) { - free(sound_name); - } + if (sound_name_owned) { free(sound_name); } + if (mouse_sound_name_owned) { free(mouse_sound_name); } return 0; } diff --git a/src/utils.c b/src/utils.c index 39a25a5..387c4d2 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,17 +1,16 @@ #define _POSIX_C_SOURCE 200809L -#include "utils.h" -#include "config.h" -#include -#include #include #include -#include -#include #include +#include +#include +#include +#include +#include "utils.h" +#include "config.h" int safe_snprintf(char *dst, size_t dst_sz, const char *fmt, ...) { - if (!dst || dst_sz == 0) - return 0; + if (!dst || dst_sz == 0) return 0; va_list ap; va_start(ap, fmt); int n = vsnprintf(dst, dst_sz, fmt, ap); @@ -67,3 +66,5 @@ int process_is_running(pid_t pid) { return 0; return kill(pid, 0) == 0; } + + -- 2.43.0 From 028785ac27f348be1555bc3b76ebb8ba746a39f0 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 13 Oct 2025 21:35:35 +0545 Subject: [PATCH 02/18] oops many changes --- Makefile | 10 +- include/config_io.h | 16 + include/mute.h | 11 + include/soundpacks.h | 15 + include/utils.h | 6 + src/{keyvibe_audio.c => audio.c} | 152 ++++- src/config_io.c | 137 ++++ src/{keyvibe_input.c => input.c} | 17 +- src/keyvibe_main.c | 1069 ------------------------------ src/main.c | 905 +++++++++++++++++++++++++ src/soundpacks.c | 284 ++++++++ src/utils.c | 82 ++- 12 files changed, 1582 insertions(+), 1122 deletions(-) create mode 100644 include/config_io.h create mode 100644 include/mute.h create mode 100644 include/soundpacks.h rename src/{keyvibe_audio.c => audio.c} (87%) create mode 100644 src/config_io.c rename src/{keyvibe_input.c => input.c} (97%) delete mode 100644 src/keyvibe_main.c create mode 100644 src/main.c create mode 100644 src/soundpacks.c diff --git a/Makefile b/Makefile index 391f659..025f4d0 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,13 @@ LDFLAGS_KEYBOARD = $(shell pkg-config --libs libevdev libinput libudev) -lpthrea # Targets KeyVibe_TARGET = keyvibe -SOUND_TARGET = keyvibe-audio -KEYBOARD_TARGET = keyvibe-input +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 +KeyVibe_SOURCE = src/main.c src/utils.c src/config_io.c src/soundpacks.c +SOUND_SOURCE = src/audio.c src/utils.c +KEYBOARD_SOURCE = src/input.c src/utils.c # Install paths BINDIR = $(PREFIX)/bin diff --git a/include/config_io.h b/include/config_io.h new file mode 100644 index 0000000..58def05 --- /dev/null +++ b/include/config_io.h @@ -0,0 +1,16 @@ +#ifndef KEYVIBE_CONFIG_IO_H +#define KEYVIBE_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 ~/.keyvibe.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); + +// Read ~/.keyvibe.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); + +#endif // KEYVIBE_CONFIG_IO_H + + diff --git a/include/mute.h b/include/mute.h new file mode 100644 index 0000000..cfeecd6 --- /dev/null +++ b/include/mute.h @@ -0,0 +1,11 @@ +#ifndef KEYVIBE_MUTE_H +#define KEYVIBE_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 // KEYVIBE_MUTE_H + + diff --git a/include/soundpacks.h b/include/soundpacks.h new file mode 100644 index 0000000..871b91a --- /dev/null +++ b/include/soundpacks.h @@ -0,0 +1,15 @@ +#ifndef KEYVIBE_SOUNDPACKS_H +#define KEYVIBE_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 // KEYVIBE_SOUNDPACKS_H + + diff --git a/include/utils.h b/include/utils.h index 38a3b07..65e6471 100644 --- a/include/utils.h +++ b/include/utils.h @@ -17,5 +17,11 @@ 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); + #endif // KEYVIBE_UTILS_H diff --git a/src/keyvibe_audio.c b/src/audio.c similarity index 87% rename from src/keyvibe_audio.c rename to src/audio.c index e16dc96..2b3beaa 100644 --- a/src/keyvibe_audio.c +++ b/src/audio.c @@ -1,6 +1,7 @@ #define _GNU_SOURCE -#include #include "utils.h" +#include +#include #include #include #include @@ -12,7 +13,6 @@ #include #include #include -#include #define MAX_LINE_LENGTH 1024 #define MAX_CONCURRENT_SOUNDS 10 @@ -47,11 +47,12 @@ 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; 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) { @@ -121,7 +122,8 @@ int load_sound_config(const char *config_path) { 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 (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]; @@ -176,13 +178,14 @@ int load_sound_config(const char *config_path) { 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 (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; - + // Handle mouse key names first if (strcmp(key, "MouseLeft") == 0) { key_code = 272; @@ -203,7 +206,7 @@ int load_sound_config(const char *config_path) { key_code = atoi(key); } } - + if (key_code >= 0 && key_code < 512) { const char *filename_relative = json_object_get_string(val); char full_filename[MAX_LINE_LENGTH]; @@ -233,7 +236,8 @@ int load_sound_config(const char *config_path) { 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 (g_verbose) + printf("Single mode sound file: %s\n", g_sound_pack.sound_file); } // Try "defines" first, then "definitions" for mouse configs json_object *defines_obj = NULL; @@ -241,7 +245,7 @@ int load_sound_config(const char *config_path) { json_object_object_get_ex(root, "definitions", &defines_obj)) { json_object_object_foreach(defines_obj, key, val) { int key_code; - + // Handle mouse key names if (strcmp(key, "MouseLeft") == 0) { key_code = 272; @@ -252,7 +256,7 @@ int load_sound_config(const char *config_path) { } else { key_code = atoi(key); } - + if (key_code >= 0 && key_code < 512) { // Handle different config formats if (json_object_is_type(val, json_type_array)) { @@ -264,19 +268,23 @@ int load_sound_config(const char *config_path) { json_object_get_int(json_object_array_get_idx(val, 1)); } } else if (json_object_is_type(val, json_type_object)) { - // Mouse format: {"timing": [[start1, duration1], [start2, duration2]]} + // Mouse format: {"timing": [[start1, duration1], [start2, + // duration2]]} 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) { // Use the first timing entry - json_object *first_timing = json_object_array_get_idx(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)); + 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_get_int( + json_object_array_get_idx(first_timing, 1)); } } } @@ -322,17 +330,27 @@ void *play_sound_thread(void *arg) { int thread_id = data->thread_id; int is_pressed = data->is_pressed; const char *file_to_play = NULL; - + // Determine if this is a mouse event (key codes 272, 273, 274) 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"); } + + // Check if this type of sound is muted + 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; @@ -544,7 +562,8 @@ int parse_keyboard_event(const char *json_line, int *key_code, } void cleanup() { - if (g_verbose) printf("Cleaning up...\n"); + if (g_verbose) + printf("Cleaning up...\n"); usleep(500000); for (int i = 0; i < 512; i++) { if (g_sound_pack.multi_key_mappings[i].press) { @@ -569,15 +588,16 @@ static int read_mute_state() { if (!rd || strlen(rd) == 0) { rd = "/tmp"; } - if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/keyvibe-mute-%d", rd, (int)getuid())) { + 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); @@ -587,15 +607,65 @@ static int read_mute_state() { return 0; } +static int read_keyboard_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-kbd-mute-%d", rd, + (int)getuid())) { + return 0; + } + FILE *f = fopen(mute_file, "r"); + if (!f) { + return 0; + } + int mute = 0; + if (fscanf(f, "%d", &mute) == 1) { + fclose(f); + return mute; + } + fclose(f); + return 0; +} + +static int read_mouse_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-mouse-mute-%d", + rd, (int)getuid())) { + return 0; + } + FILE *f = fopen(mute_file, "r"); + if (!f) { + return 0; + } + 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 > 7) { - fprintf(stderr, "Usage: %s [volume] [verbose] [mute] [mouse_config] [mouse_volume]\n", argv[0]); + if (argc < 2 || argc > 9) { + fprintf(stderr, + "Usage: %s [volume] [verbose] [mute] [mouse_config] " + "[mouse_volume] [keyboard_mute] [mouse_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"); + fprintf(stderr, " mute: 1 to mute all sound (default: 0)\n"); fprintf(stderr, " mouse_config: path to mouse config.json (optional)\n"); fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n"); + fprintf(stderr, " keyboard_mute: 1 to mute keyboard sounds (optional)\n"); + fprintf(stderr, " mouse_mute: 1 to mute mouse sounds (optional)\n"); return 1; } if (argc >= 3) { @@ -605,9 +675,11 @@ int main(int argc, char *argv[]) { if (volume_percent > 100) volume_percent = 100; g_volume = volume_percent / 100.0f; - if (g_verbose) printf("Volume set to: %d%%\n", volume_percent); + if (g_verbose) + printf("Volume set to: %d%%\n", volume_percent); } else { - if (g_verbose) printf("Volume set to: 50%% (default)\n"); + if (g_verbose) + printf("Volume set to: 50%% (default)\n"); } if (argc >= 4) { g_verbose = atoi(argv[3]); @@ -621,7 +693,7 @@ int main(int argc, char *argv[]) { printf("Sound muted\n"); } } - + // Parse mouse config and volume if provided if (argc >= 6) { if (load_sound_config(argv[5]) != 0) { @@ -634,7 +706,7 @@ int main(int argc, char *argv[]) { printf("Mouse sound pack loaded from: %s\n", argv[5]); } } - + if (argc >= 7) { int mouse_volume_percent = atoi(argv[6]); if (mouse_volume_percent < 0) @@ -642,9 +714,22 @@ int main(int argc, char *argv[]) { if (mouse_volume_percent > 100) mouse_volume_percent = 100; g_mouse_volume = mouse_volume_percent / 100.0f; - if (g_verbose) printf("Mouse volume set to: %d%%\n", mouse_volume_percent); + 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 (load_sound_config(argv[1]) != 0) { fprintf(stderr, "Failed to load keyboard sound configuration\n"); return 1; @@ -658,9 +743,11 @@ int main(int argc, char *argv[]) { struct timeval timeout; char line[MAX_LINE_LENGTH]; while (1) { - // Check mute state from file periodically + // Check mute state from files periodically g_mute = read_mute_state(); - + g_keyboard_mute = read_keyboard_mute_state(); + g_mouse_mute = read_mouse_mute_state(); + FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); timeout.tv_sec = 1; @@ -682,7 +769,8 @@ int main(int argc, char *argv[]) { 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"); + if (g_verbose) + printf("EOF reached on stdin\n"); } else { perror("fgets"); } diff --git a/src/config_io.c b/src/config_io.c new file mode 100644 index 0000000..f01911a --- /dev/null +++ b/src/config_io.c @@ -0,0 +1,137 @@ +/* #include "config_io.h" */ +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include + +static char *xstrdup(const char *s) { + if (!s) + return NULL; + size_t n = strlen(s) + 1; + char *p = (char *)malloc(n); + if (!p) + return NULL; + memcpy(p, s, n); + return p; +} + +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, ".keyvibe.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) { + json_object *root = json_object_new_object(); + if (!root) + return 0; + + json_object *keyboard = json_object_new_object(); + 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, "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; + } + 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) { + 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, "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 { + 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 97% rename from src/keyvibe_input.c rename to src/input.c index 90f04d0..01825e1 100644 --- a/src/keyvibe_input.c +++ b/src/input.c @@ -1,19 +1,19 @@ +#include "utils.h" #include #include #include #include -#include "utils.h" #include #include #include #include -#include -#include -#include #include #include #include +#include +#include +#include #include "config.h" @@ -127,11 +127,13 @@ static int run_mainloop(struct libinput *libinput) { fd.events = POLLIN; fd.revents = 0; if (handle_events(libinput) != 0) - return errorf("Expected device added events on startup but got none. Maybe you don't have the right permissions?\n"); + return errorf("Expected device added events on startup but got none. Maybe " + "you don't have the right permissions?\n"); while (1) { int pr = poll(&fd, 1, -1); if (pr < 0) { - if (errno == EINTR) continue; + if (errno == EINTR) + continue; return errorf("poll failed: %s\n", strerror(errno)); } handle_events(libinput); @@ -191,7 +193,8 @@ int main(int argc, char *argv[]) { } pthread_t input_handler; struct input_handler_data input_handler_data = {udev, libinput}; - if (pthread_create(&input_handler, NULL, handle_input, &input_handler_data) != 0) { + if (pthread_create(&input_handler, NULL, handle_input, &input_handler_data) != + 0) { errorf("Failed to create input handler thread.\n"); } else { pthread_detach(input_handler); diff --git a/src/keyvibe_main.c b/src/keyvibe_main.c deleted file mode 100644 index 5b88cfd..0000000 --- a/src/keyvibe_main.c +++ /dev/null @@ -1,1069 +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 "/soundpacks" -#define KEYBOARD_AUDIO_DIR AUDIO_BASE_DIR "/keyboard" -#define MOUSE_AUDIO_DIR AUDIO_BASE_DIR "/mouse" -#define USER_KEYBOARD_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/keyboard" -#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/mouse" -#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 volatile sig_atomic_t keyboard_reload_requested = 0; -static volatile sig_atomic_t mouse_reload_requested = 0; -static volatile sig_atomic_t volume_reload_requested = 0; -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 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; - -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 keyboard sound pack (default: eg-oreo)\n"); - printf(" -M, --mouse SOUND_NAME Select mouse sound pack (default: ping)\n"); - printf(" -V, --volume VOLUME Set volume [0-100] for both keyboard and mouse (default: 50)\n"); - printf(" -K, --keyboard-volume VOLUME Set keyboard volume [0-100] (default: 50)\n"); - printf(" -O, --mouse-volume VOLUME Set mouse 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 dirs - char user_keyboard_dir[MAX_PATH_LENGTH] = {0}; - char user_mouse_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_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("Keyboard Sound Packs:\n"); - printf("---------------------\n"); - - // List user keyboard packs first (if available) - 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(" %s (user)\n", entry->d_name); - } - } - closedir(dir); - } - - // Then list system keyboard packs - 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(" %s (system)\n", entry->d_name); - } - } - closedir(dir); - } - - printf("\nMouse Sound Packs:\n"); - printf("------------------\n"); - - // List user mouse packs first (if available) - 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(" %s (user)\n", entry->d_name); - } - } - closedir(dir); - } - - // Then list system mouse packs - 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(" %s (system)\n", entry->d_name); - } - } - closedir(dir); - } - - return 0; -} - -static void get_user_keyboard_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_KEYBOARD_AUDIO_SUBPATH); -} - -static void get_user_mouse_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_MOUSE_AUDIO_SUBPATH); -} - -static int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir, size_t out_sz) { - char user_audio_dir[MAX_PATH_LENGTH]; - get_user_keyboard_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", KEYBOARD_AUDIO_DIR, sound_name) >= sizeof(p2)) { - return 0; // Path too long - } - if (access(p2, R_OK) == 0) { - strncpy(out_basedir, KEYBOARD_AUDIO_DIR, out_sz - 1); out_basedir[out_sz-1] = '\0'; - return 1; - } - return 0; -} - -static int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir, size_t out_sz) { - char user_audio_dir[MAX_PATH_LENGTH]; - get_user_mouse_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", MOUSE_AUDIO_DIR, sound_name) >= sizeof(p2)) { - return 0; // Path too long - } - if (access(p2, R_OK) == 0) { - strncpy(out_basedir, MOUSE_AUDIO_DIR, out_sz - 1); out_basedir[out_sz-1] = '\0'; - return 1; - } - return 0; -} - -int validate_keyboard_sound_pack(const char *sound_name) { - char basedir[MAX_PATH_LENGTH]; - 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[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; -} - -int validate_mouse_sound_pack(const char *sound_name) { - char basedir[MAX_PATH_LENGTH]; - 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[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_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[MAX_PATH_LENGTH]; - 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) { - 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; -} - -static 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[MAX_PATH_LENGTH]; - 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) { - 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, - const char *mouse_sound_dir, const char *mouse_config_path, - int mouse_volume) { - (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/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 mouse_volume_str[32]; - snprintf(mouse_volume_str, sizeof(mouse_volume_str), "%d", mouse_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, mouse_config_path, mouse_volume_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) { - 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; // 100ms - nanosleep(&ts, NULL); - continue; - } - printf("inotify read error: %s\n", strerror(errno)); - break; - } - printf("Config file changed, requesting reload...\n"); - reload_requested = 1; - } - inotify_rm_watch(fd, wd); - close(fd); - return NULL; -} - -static int write_user_config(const char *path, const char *keyboard_sound, const char *mouse_sound, int keyboard_volume, int mouse_volume) { - json_object *root = json_object_new_object(); - if (!root) - return 0; - - // Create keyboard section - json_object *keyboard = json_object_new_object(); - 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); - - // Create mouse section - json_object *mouse = json_object_new_object(); - 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; - } - fprintf(f, "%s\n", json_str); - fclose(f); - json_object_put(root); - return 1; -} - -static int read_user_config(const char *path, char **out_keyboard_sound, char **out_mouse_sound, - int *out_keyboard_volume, int *out_mouse_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; - - // Try new format first (keyboard/mouse sections) - 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)) { - // New format - json_object *o; - if (json_object_object_get_ex(keyboard_obj, "keyboard_sound", &o)) { - const char *s = json_object_get_string(o); - if (s) - *out_keyboard_sound = strdup(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 = strdup(s); - } - - // Read separate volumes for keyboard and mouse - 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 - try old single sound/volume format - 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 = strdup(s); - *out_mouse_sound = strdup("ping"); // default mouse sound - } - } - 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; -} - -int main(int argc, char *argv[]) { - char *sound_name = strdup("eg-oreo"); // may be replaced by strdup; track for free - char *mouse_sound_name = strdup("ping"); // may be replaced by strdup; track for free - 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; - 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'}, - {"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; - char *cli_mouse_sound = NULL; - int cli_volume = -1; - int cli_keyboard_volume = -1; - int cli_mouse_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_keyboard_sound = NULL; - char *cfg_mouse_sound = NULL; - int cfg_keyboard_volume = volume; - int cfg_mouse_volume = volume; - if (read_user_config(user_cfg_path, &cfg_keyboard_sound, &cfg_mouse_sound, &cfg_keyboard_volume, &cfg_mouse_volume)) { - if (cfg_keyboard_sound) { - if (sound_name_owned) { free(sound_name); } - sound_name = cfg_keyboard_sound; // now owned, must free on exit paths - sound_name_owned = 1; - } - if (cfg_mouse_sound) { - if (mouse_sound_name_owned) { free(mouse_sound_name); } - mouse_sound_name = cfg_mouse_sound; // now owned, must free on exit paths - mouse_sound_name_owned = 1; - } - volume = cfg_keyboard_volume; // Use keyboard volume as main volume for CLI compatibility - } - } - } - 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==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:M:V:K:O: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 'M': - cli_mouse_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 'K': - cli_keyboard_volume = atoi(optarg); - if (cli_keyboard_volume < 0) - cli_keyboard_volume = 0; - if (cli_keyboard_volume > 100) - cli_keyboard_volume = 100; - break; - case 'O': - cli_mouse_volume = atoi(optarg); - if (cli_mouse_volume < 0) - cli_mouse_volume = 0; - if (cli_mouse_volume > 100) - cli_mouse_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_mouse_sound != NULL) { - if (mouse_sound_name_owned) { - free(mouse_sound_name); - } - mouse_sound_name = cli_mouse_sound; - mouse_sound_name_owned = 0; // cli_mouse_sound is not owned by us - config_updated = 1; - } - if (cli_volume >= 0) { - volume = cli_volume; - config_updated = 1; - } - if (cli_keyboard_volume >= 0) { - current_keyboard_volume = cli_keyboard_volume; - config_updated = 1; - } - if (cli_mouse_volume >= 0) { - current_mouse_volume = cli_mouse_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, mouse_sound_name, current_keyboard_volume, current_mouse_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, mouse_sound_name, current_keyboard_volume, current_mouse_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_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]; - 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]; - 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; - strncpy(current_sound_name, sound_name, sizeof(current_sound_name) - 1); - current_sound_name[sizeof(current_sound_name) - 1] = '\0'; - strncpy(current_mouse_sound_name, mouse_sound_name, sizeof(current_mouse_sound_name) - 1); - current_mouse_sound_name[sizeof(current_mouse_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'; - strncpy(current_mouse_config_path, mouse_config_path, sizeof(current_mouse_config_path) - 1); - current_mouse_config_path[sizeof(current_mouse_config_path) - 1] = '\0'; - strncpy(current_mouse_sound_dir, mouse_sound_dir, sizeof(current_mouse_sound_dir) - 1); - current_mouse_sound_dir[sizeof(current_mouse_sound_dir) - 1] = '\0'; - if (verbose && !is_daemon) { - printf("KeyVibe starting...\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("KeyVibe started with keyboard sound pack: %s, mouse sound pack: %s\n", sound_name, mouse_sound_name); - 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)) { - 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; - // Enable live reloading for both daemon and non-daemon modes - 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'; - printf("Setting up file watcher for: %s\n", in_args.path); - - // Check if the config file exists - if (access(in_args.path, F_OK) == 0) { - pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args); - pthread_detach(inotify_thread); - } else { - printf("Warning: Config file %s does not exist, file watching disabled\n", in_args.path); - } - } 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))) { - char *cfg_keyboard_sound = NULL; - char *cfg_mouse_sound = NULL; - int cfg_keyboard_volume = current_keyboard_volume; - int cfg_mouse_volume = current_mouse_volume; - - if (read_user_config(user_cfg_path3, &cfg_keyboard_sound, &cfg_mouse_sound, &cfg_keyboard_volume, &cfg_mouse_volume)) { - printf("Read config - keyboard:%s mouse:%s kvol:%d mvol:%d (current kvol:%d mvol:%d)\n", - cfg_keyboard_sound ? cfg_keyboard_sound : "NULL", - cfg_mouse_sound ? cfg_mouse_sound : "NULL", - cfg_keyboard_volume, cfg_mouse_volume, current_keyboard_volume, current_mouse_volume); - - // Check what changed - int keyboard_changed = 0; - int mouse_changed = 0; - int keyboard_volume_changed = 0; - int mouse_volume_changed = 0; - - if (cfg_keyboard_sound && strcmp(current_sound_name, cfg_keyboard_sound) != 0) { - keyboard_changed = 1; - strncpy(current_sound_name, cfg_keyboard_sound, sizeof(current_sound_name) - 1); - current_sound_name[sizeof(current_sound_name) - 1] = '\0'; - } - free(cfg_keyboard_sound); - - if (cfg_mouse_sound && strcmp(current_mouse_sound_name, cfg_mouse_sound) != 0) { - mouse_changed = 1; - strncpy(current_mouse_sound_name, cfg_mouse_sound, sizeof(current_mouse_sound_name) - 1); - current_mouse_sound_name[sizeof(current_mouse_sound_name) - 1] = '\0'; - } - 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; - } - - printf("Config changed - keyboard:%d mouse:%d kvol:%d mvol:%d\n", - keyboard_changed, mouse_changed, keyboard_volume_changed, mouse_volume_changed); - - // Handle the changes immediately - if (keyboard_changed || mouse_changed || keyboard_volume_changed || mouse_volume_changed) { - // Validate both sound packs - 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) { - // Update keyboard paths - if (build_paths_for_keyboard_sound(current_sound_name, current_config_path, - sizeof(current_config_path), - current_sound_dir, - sizeof(current_sound_dir))) { - // Update mouse paths - if (build_paths_for_mouse_sound(current_mouse_sound_name, current_mouse_config_path, - sizeof(current_mouse_config_path), - current_mouse_sound_dir, - sizeof(current_mouse_sound_dir))) { - // Restart children with new config (including volume changes) - 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); - 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); - } 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"); - } - } 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("KeyVibe exited.\n"); - } - if (sound_name_owned) { free(sound_name); } - if (mouse_sound_name_owned) { free(mouse_sound_name); } - return 0; -} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..6b052d8 --- /dev/null +++ b/src/main.c @@ -0,0 +1,905 @@ +#define _POSIX_C_SOURCE 200809L + +#include "config.h" +#include "config_io.h" +#include "mute.h" +#include "soundpacks.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 "/soundpacks" +#define KEYBOARD_AUDIO_DIR AUDIO_BASE_DIR "/keyboard" +#define MOUSE_AUDIO_DIR AUDIO_BASE_DIR "/mouse" +#define USER_KEYBOARD_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/keyboard" +#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/mouse" +#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 volatile sig_atomic_t keyboard_reload_requested = 0; +static volatile sig_atomic_t mouse_reload_requested = 0; +static volatile sig_atomic_t volume_reload_requested = 0; +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 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; + +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 keyboard sound pack (default: " + "eg-oreo)\n"); + printf( + " -M, --mouse SOUND_NAME Select mouse sound pack (default: ping)\n"); + printf(" -V, --volume VOLUME Set volume [0-100] for both keyboard and " + "mouse (default: 50)\n"); + printf(" -K, --keyboard-volume VOLUME Set keyboard volume [0-100] " + "(default: 50)\n"); + printf(" -O, --mouse-volume VOLUME Set mouse 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[=DEVICE] Mute sounds (keyboard|mouse|both, " + "default: both)\n"); + printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both, " + "default: both)\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"); +} + +// moved to soundpacks.c +int list_sound_packs(void); + +// moved to soundpacks.c +void get_user_keyboard_audio_dir(char *buffer, size_t buflen); + +// moved to soundpacks.c +void get_user_mouse_audio_dir(char *buffer, size_t buflen); + +// moved to soundpacks.c +int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir, + size_t out_sz); + +// moved to soundpacks.c +int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir, + size_t out_sz); + +// moved to soundpacks.c +int validate_keyboard_sound_pack(const char *sound_name); + +// moved to soundpacks.c +int validate_mouse_sound_pack(const char *sound_name); + +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); +} + +// moved to config_io.c +char *get_user_config_path(char *buffer, size_t buflen); + +// moved to soundpacks.c +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); + +// moved to soundpacks.c +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); + +// 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, + const char *mouse_sound_dir, + const char *mouse_config_path, int mouse_volume, + int keyboard_mute, int mouse_mute) { + (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/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 mouse_volume_str[32]; + snprintf(mouse_volume_str, sizeof(mouse_volume_str), "%d", mouse_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); + char keyboard_mute_str[8]; + snprintf(keyboard_mute_str, sizeof(keyboard_mute_str), "%d", keyboard_mute); + char mouse_mute_str[8]; + snprintf(mouse_mute_str, sizeof(mouse_mute_str), "%d", mouse_mute); + execl(sound_player_path, "keyvibe-audio", "config.json", volume_str, + verbose_str, mute_str, mouse_config_path, mouse_volume_str, + keyboard_mute_str, mouse_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; +} + +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) { + 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; // 100ms + nanosleep(&ts, NULL); + continue; + } + printf("inotify read error: %s\n", strerror(errno)); + break; + } + printf("Config file changed, requesting reload...\n"); + reload_requested = 1; + } + inotify_rm_watch(fd, wd); + close(fd); + return NULL; +} + +// moved to config_io.c +int write_user_config(const char *path, const char *keyboard_sound, + const char *mouse_sound, int keyboard_volume, + int mouse_volume); + +// moved to config_io.c +int read_user_config(const char *path, char **out_keyboard_sound, + char **out_mouse_sound, int *out_keyboard_volume, + int *out_mouse_volume); + +int main(int argc, char *argv[]) { + char *sound_name = + strdup("eg-oreo"); // may be replaced by strdup; track for free + char *mouse_sound_name = + strdup("ping"); // may be replaced by strdup; track for free + 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; + 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'}, + {"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}}; + int volume = 50; + char *cli_sound = NULL; + char *cli_mouse_sound = NULL; + int cli_volume = -1; + int cli_keyboard_volume = -1; + int cli_mouse_volume = -1; + int cli_keyboard_mute = -1; + int cli_mouse_mute = -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_keyboard_sound = NULL; + char *cfg_mouse_sound = NULL; + int cfg_keyboard_volume = volume; + int cfg_mouse_volume = volume; + if (read_user_config(user_cfg_path, &cfg_keyboard_sound, &cfg_mouse_sound, + &cfg_keyboard_volume, &cfg_mouse_volume)) { + if (cfg_keyboard_sound) { + if (sound_name_owned) { + free(sound_name); + } + sound_name = cfg_keyboard_sound; // now owned, must free on exit paths + sound_name_owned = 1; + } + if (cfg_mouse_sound) { + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } + mouse_sound_name = + cfg_mouse_sound; // now owned, must free on exit paths + mouse_sound_name_owned = 1; + } + volume = cfg_keyboard_volume; // Use keyboard volume as main volume for + // CLI compatibility + } + } + } + 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 == 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:M:V:K:O:lhdsm::u::v", long_options, + NULL)) != -1) { + switch (opt) { + case 'd': + flag_daemon = 1; + break; + case 's': + flag_stop = 1; + break; + case 'S': + cli_sound = optarg; + break; + case 'M': + cli_mouse_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 'K': + cli_keyboard_volume = atoi(optarg); + if (cli_keyboard_volume < 0) + cli_keyboard_volume = 0; + if (cli_keyboard_volume > 100) + cli_keyboard_volume = 100; + break; + case 'O': + cli_mouse_volume = atoi(optarg); + if (cli_mouse_volume < 0) + cli_mouse_volume = 0; + if (cli_mouse_volume > 100) + cli_mouse_volume = 100; + break; + case 'm': + if (optarg == NULL) { + // No argument provided, mute both + cli_keyboard_mute = 1; + cli_mouse_mute = 1; + current_mute = 1; // global mute + } else if (strcmp(optarg, "keyboard") == 0) { + cli_keyboard_mute = 1; + write_runtime_keyboard_mute_file(1); + } else if (strcmp(optarg, "mouse") == 0) { + cli_mouse_mute = 1; + write_runtime_mouse_mute_file(1); + } else if (strcmp(optarg, "both") == 0) { + cli_keyboard_mute = 1; + cli_mouse_mute = 1; + current_mute = 1; // global mute + } else { + fprintf(stderr, + "Invalid mute option: %s. Use 'keyboard', 'mouse', or 'both'\n", + optarg); + return 1; + } + break; + case 'u': + if (optarg == NULL) { + // No argument provided, unmute both + cli_keyboard_mute = 0; + cli_mouse_mute = 0; + current_mute = 0; // clear global mute + } else if (strcmp(optarg, "keyboard") == 0) { + cli_keyboard_mute = 0; + write_runtime_keyboard_mute_file(0); + } else if (strcmp(optarg, "mouse") == 0) { + cli_mouse_mute = 0; + write_runtime_mouse_mute_file(0); + } else if (strcmp(optarg, "both") == 0) { + cli_keyboard_mute = 0; + cli_mouse_mute = 0; + current_mute = 0; // clear global mute + } else { + fprintf( + stderr, + "Invalid unmute option: %s. Use 'keyboard', 'mouse', or 'both'\n", + optarg); + return 1; + } + 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"); + } + // Initialize global mute from runtime file so child inherits correct state + current_mute = read_runtime_mute_file(); + + // 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_mouse_sound != NULL) { + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } + mouse_sound_name = cli_mouse_sound; + mouse_sound_name_owned = 0; // cli_mouse_sound is not owned by us + config_updated = 1; + } + if (cli_volume >= 0) { + volume = cli_volume; + config_updated = 1; + } + if (cli_keyboard_volume >= 0) { + current_keyboard_volume = cli_keyboard_volume; + config_updated = 1; + } + if (cli_mouse_volume >= 0) { + current_mouse_volume = cli_mouse_volume; + config_updated = 1; + } + if (cli_keyboard_mute >= 0) { + current_keyboard_mute = cli_keyboard_mute; + config_updated = 1; + } + if (cli_mouse_mute >= 0) { + current_mouse_mute = cli_mouse_mute; + config_updated = 1; + } + + // Persist runtime mute state so audio child can pick it up + write_runtime_mute_file(current_mute); + write_runtime_keyboard_mute_file(current_keyboard_mute); + write_runtime_mouse_mute_file(current_mouse_mute); + + // If only CLI control options were provided and not daemon/list/stop, exit + // quietly + 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; + } + + // 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, mouse_sound_name, + current_keyboard_volume, current_mouse_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, mouse_sound_name, + current_keyboard_volume, current_mouse_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_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]; + 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]; + 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; + strncpy(current_sound_name, sound_name, sizeof(current_sound_name) - 1); + current_sound_name[sizeof(current_sound_name) - 1] = '\0'; + strncpy(current_mouse_sound_name, mouse_sound_name, + sizeof(current_mouse_sound_name) - 1); + current_mouse_sound_name[sizeof(current_mouse_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'; + strncpy(current_mouse_config_path, mouse_config_path, + sizeof(current_mouse_config_path) - 1); + current_mouse_config_path[sizeof(current_mouse_config_path) - 1] = '\0'; + strncpy(current_mouse_sound_dir, mouse_sound_dir, + sizeof(current_mouse_sound_dir) - 1); + current_mouse_sound_dir[sizeof(current_mouse_sound_dir) - 1] = '\0'; + if (verbose && !is_daemon) { + printf("KeyVibe starting...\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("KeyVibe started with keyboard sound pack: %s, mouse sound pack: " + "%s\n", + sound_name, mouse_sound_name); + 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)) { + 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; + // Enable live reloading for both daemon and non-daemon modes + 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'; + printf("Setting up file watcher for: %s\n", in_args.path); + + // Check if the config file exists + if (access(in_args.path, F_OK) == 0) { + pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args); + pthread_detach(inotify_thread); + } else { + printf("Warning: Config file %s does not exist, file watching disabled\n", + in_args.path); + } + } 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))) { + char *cfg_keyboard_sound = NULL; + char *cfg_mouse_sound = NULL; + int cfg_keyboard_volume = current_keyboard_volume; + int cfg_mouse_volume = current_mouse_volume; + + if (read_user_config(user_cfg_path3, &cfg_keyboard_sound, + &cfg_mouse_sound, &cfg_keyboard_volume, + &cfg_mouse_volume)) { + printf("Read config - keyboard:%s mouse:%s kvol:%d mvol:%d (current " + "kvol:%d mvol:%d)\n", + cfg_keyboard_sound ? cfg_keyboard_sound : "NULL", + cfg_mouse_sound ? cfg_mouse_sound : "NULL", + cfg_keyboard_volume, cfg_mouse_volume, current_keyboard_volume, + current_mouse_volume); + + // Check what changed + int keyboard_changed = 0; + int mouse_changed = 0; + int keyboard_volume_changed = 0; + int mouse_volume_changed = 0; + + if (cfg_keyboard_sound && + strcmp(current_sound_name, cfg_keyboard_sound) != 0) { + keyboard_changed = 1; + strncpy(current_sound_name, cfg_keyboard_sound, + sizeof(current_sound_name) - 1); + current_sound_name[sizeof(current_sound_name) - 1] = '\0'; + } + free(cfg_keyboard_sound); + + if (cfg_mouse_sound && + strcmp(current_mouse_sound_name, cfg_mouse_sound) != 0) { + mouse_changed = 1; + strncpy(current_mouse_sound_name, cfg_mouse_sound, + sizeof(current_mouse_sound_name) - 1); + current_mouse_sound_name[sizeof(current_mouse_sound_name) - 1] = + '\0'; + } + 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; + } + + printf("Config changed - keyboard:%d mouse:%d kvol:%d mvol:%d\n", + keyboard_changed, mouse_changed, keyboard_volume_changed, + mouse_volume_changed); + + // Handle the changes immediately + if (keyboard_changed || mouse_changed || keyboard_volume_changed || + mouse_volume_changed) { + // Validate both sound packs + 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) { + // Update keyboard paths + if (build_paths_for_keyboard_sound( + current_sound_name, current_config_path, + sizeof(current_config_path), current_sound_dir, + sizeof(current_sound_dir))) { + // Update mouse paths + if (build_paths_for_mouse_sound( + current_mouse_sound_name, current_mouse_config_path, + sizeof(current_mouse_config_path), + current_mouse_sound_dir, + sizeof(current_mouse_sound_dir))) { + // Restart children with new config (including volume changes) + 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, current_keyboard_mute, + current_mouse_mute); + 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); + } 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"); + } + } 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("KeyVibe 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..f051297 --- /dev/null +++ b/src/soundpacks.c @@ -0,0 +1,284 @@ +/* #include "soundpacks.h" */ +#include "config.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define AUDIO_BASE_DIR KeyVibe_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/keyvibe/soundpacks/keyboard" +#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/mouse" + +static void get_user_keyboard_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_KEYBOARD_AUDIO_SUBPATH); +} + +static void get_user_mouse_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_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) { + strncpy(out_basedir, user_audio_dir, out_sz - 1); + out_basedir[out_sz - 1] = '\0'; + 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) { + strncpy(out_basedir, KEYBOARD_AUDIO_DIR, out_sz - 1); + out_basedir[out_sz - 1] = '\0'; + 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) { + strncpy(out_basedir, user_audio_dir, out_sz - 1); + out_basedir[out_sz - 1] = '\0'; + 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) { + strncpy(out_basedir, MOUSE_AUDIO_DIR, out_sz - 1); + out_basedir[out_sz - 1] = '\0'; + 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); + return 0; + } + if (access(config_path, R_OK) != 0) { + fprintf(stderr, "Error: Config file not found: %s\n", config_path); + 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); + return 0; + } + if (access(config_path, R_OK) != 0) { + fprintf(stderr, "Error: Config file not found: %s\n", config_path); + 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("Keyboard Sound Packs:\n"); + printf("---------------------\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(" %s (user)\n", entry->d_name); + } + } + 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(" %s (system)\n", entry->d_name); + } + } + closedir(dir); + } + printf("\nMouse Sound Packs:\n"); + printf("------------------\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(" %s (user)\n", entry->d_name); + } + } + 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(" %s (system)\n", entry->d_name); + } + } + closedir(dir); + } + return 0; +} diff --git a/src/utils.c b/src/utils.c index 387c4d2..9427388 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,16 +1,82 @@ #define _POSIX_C_SOURCE 200809L +#include "utils.h" +#include +#include #include #include -#include -#include -#include -#include #include -#include "utils.h" -#include "config.h" +#include +#include + +// Mute helpers moved here for re-use +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/keyvibe-mute-%d", rd, + (int)getuid())) + return 0; + FILE *f = fopen(mute_file, "r"); + if (!f) + return 0; + int mute = 0; + if (fscanf(f, "%d", &mute) == 1) { + fclose(f); + return mute; + } + fclose(f); + return 0; +} + +void write_runtime_mute_file(int mute) { + 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/keyvibe-mute-%d", rd, + (int)getuid())) + return; + FILE *f = fopen(mute_file, "w"); + if (!f) + return; + fprintf(f, "%d\n", mute ? 1 : 0); + fclose(f); +} + +void write_runtime_keyboard_mute_file(int mute) { + 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/keyvibe-kbd-mute-%d", rd, + (int)getuid())) + return; + FILE *f = fopen(mute_file, "w"); + if (!f) + return; + fprintf(f, "%d\n", mute ? 1 : 0); + fclose(f); +} + +void write_runtime_mouse_mute_file(int mute) { + 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/keyvibe-mouse-mute-%d", + rd, (int)getuid())) + return; + FILE *f = fopen(mute_file, "w"); + if (!f) + return; + fprintf(f, "%d\n", mute ? 1 : 0); + fclose(f); +} int safe_snprintf(char *dst, size_t dst_sz, const char *fmt, ...) { - if (!dst || dst_sz == 0) return 0; + if (!dst || dst_sz == 0) + return 0; va_list ap; va_start(ap, fmt); int n = vsnprintf(dst, dst_sz, fmt, ap); @@ -66,5 +132,3 @@ int process_is_running(pid_t pid) { return 0; return kill(pid, 0) == 0; } - - -- 2.43.0 From 4343cf56bf59e576b93c709c827470bd91ff059d Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 13 Oct 2025 23:03:22 +0545 Subject: [PATCH 03/18] oops many changes --- Makefile | 11 +- include/app/cli.h | 23 + include/app/process.h | 23 + include/app/reload.h | 15 + include/app/watch.h | 6 + include/audio/playback.h | 10 + include/audio/types.h | 43 ++ include/{ => common}/mute.h | 2 - include/{ => common}/utils.h | 4 +- include/config.h | 2 - include/soundpacks.h | 16 +- include/{config_io.h => user_config.h} | 10 +- src/app/process.c | 164 ++++++ src/app/reload.c | 95 +++ src/app/watch.c | 67 +++ src/audio.c | 787 ------------------------- src/audio/config.c | 256 ++++++++ src/audio/main.c | 204 +++++++ src/audio/playback.c | 289 +++++++++ src/cli.c | 158 +++++ src/{ => common}/utils.c | 3 +- src/{config_io.c => config.c} | 4 +- src/input.c | 2 +- src/main.c | 449 +------------- src/soundpacks.c | 4 +- 25 files changed, 1413 insertions(+), 1234 deletions(-) create mode 100644 include/app/cli.h create mode 100644 include/app/process.h create mode 100644 include/app/reload.h create mode 100644 include/app/watch.h create mode 100644 include/audio/playback.h create mode 100644 include/audio/types.h rename include/{ => common}/mute.h (99%) rename include/{ => common}/utils.h (94%) rename include/{config_io.h => user_config.h} (66%) create mode 100644 src/app/process.c create mode 100644 src/app/reload.c create mode 100644 src/app/watch.c delete mode 100644 src/audio.c create mode 100644 src/audio/config.c create mode 100644 src/audio/main.c create mode 100644 src/audio/playback.c create mode 100644 src/cli.c rename src/{ => common}/utils.c (98%) rename src/{config_io.c => config.c} (98%) diff --git a/Makefile b/Makefile index 025f4d0..28554f7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +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 @@ -13,10 +14,10 @@ KeyVibe_TARGET = keyvibe SOUND_TARGET = audio KEYBOARD_TARGET = input -# Sources -KeyVibe_SOURCE = src/main.c src/utils.c src/config_io.c src/soundpacks.c -SOUND_SOURCE = src/audio.c src/utils.c -KEYBOARD_SOURCE = src/input.c src/utils.c +# Sources (reorganized) +KeyVibe_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 diff --git a/include/app/cli.h b/include/app/cli.h new file mode 100644 index 0000000..00c8e70 --- /dev/null +++ b/include/app/cli.h @@ -0,0 +1,23 @@ +#ifndef KEYVIBE_CLI_H +#define KEYVIBE_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 +} CliOptions; + +void print_usage(const char *program_name); +int parse_cli(int argc, char **argv, CliOptions *out); + +#endif // KEYVIBE_CLI_H + + diff --git a/include/app/process.h b/include/app/process.h new file mode 100644 index 0000000..55d91f3 --- /dev/null +++ b/include/app/process.h @@ -0,0 +1,23 @@ +#ifndef KEYVIBE_PROCESS_H +#define KEYVIBE_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); +void handle_sighup(int sig); + +#endif // KEYVIBE_PROCESS_H diff --git a/include/app/reload.h b/include/app/reload.h new file mode 100644 index 0000000..681b366 --- /dev/null +++ b/include/app/reload.h @@ -0,0 +1,15 @@ +#ifndef KEYVIBE_RELOAD_H +#define KEYVIBE_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); + +#endif // KEYVIBE_RELOAD_H + + diff --git a/include/app/watch.h b/include/app/watch.h new file mode 100644 index 0000000..5ff54a5 --- /dev/null +++ b/include/app/watch.h @@ -0,0 +1,6 @@ +#ifndef KEYVIBE_WATCH_H +#define KEYVIBE_WATCH_H + +int start_config_watcher(const char *path); + +#endif // KEYVIBE_WATCH_H diff --git a/include/audio/playback.h b/include/audio/playback.h new file mode 100644 index 0000000..a0cba5c --- /dev/null +++ b/include/audio/playback.h @@ -0,0 +1,10 @@ +#ifndef KEYVIBE_AUDIO_PLAYBACK_H +#define KEYVIBE_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 // KEYVIBE_AUDIO_PLAYBACK_H + + diff --git a/include/audio/types.h b/include/audio/types.h new file mode 100644 index 0000000..8e60206 --- /dev/null +++ b/include/audio/types.h @@ -0,0 +1,43 @@ +#ifndef KEYVIBE_AUDIO_TYPES_H +#define KEYVIBE_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; + +#endif // KEYVIBE_AUDIO_TYPES_H diff --git a/include/mute.h b/include/common/mute.h similarity index 99% rename from include/mute.h rename to include/common/mute.h index cfeecd6..d51cfc8 100644 --- a/include/mute.h +++ b/include/common/mute.h @@ -7,5 +7,3 @@ void write_runtime_keyboard_mute_file(int mute); void write_runtime_mouse_mute_file(int mute); #endif // KEYVIBE_MUTE_H - - diff --git a/include/utils.h b/include/common/utils.h similarity index 94% rename from include/utils.h rename to include/common/utils.h index 65e6471..6d9bd9e 100644 --- a/include/utils.h +++ b/include/common/utils.h @@ -7,7 +7,8 @@ // 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. +// fprintf to stderr with printf-style formatting. Returns 1 for convenient +// `return errorf(...)` usage. int errorf(const char *fmt, ...); // Common runtime helpers @@ -24,4 +25,3 @@ void write_runtime_keyboard_mute_file(int mute); void write_runtime_mouse_mute_file(int mute); #endif // KEYVIBE_UTILS_H - diff --git a/include/config.h b/include/config.h index 9c2c9ff..1347bde 100644 --- a/include/config.h +++ b/include/config.h @@ -18,5 +18,3 @@ #define KeyVibe_BIN_DIR PACKAGE_PREFIX "/bin" #endif - - diff --git a/include/soundpacks.h b/include/soundpacks.h index 871b91a..b1115cf 100644 --- a/include/soundpacks.h +++ b/include/soundpacks.h @@ -2,14 +2,18 @@ #define KEYVIBE_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 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 // KEYVIBE_SOUNDPACKS_H - - diff --git a/include/config_io.h b/include/user_config.h similarity index 66% rename from include/config_io.h rename to include/user_config.h index 58def05..f89a799 100644 --- a/include/config_io.h +++ b/include/user_config.h @@ -6,11 +6,13 @@ char *get_user_config_path(char *buffer, size_t buflen); // Write ~/.keyvibe.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 write_user_config(const char *path, const char *keyboard_sound, + const char *mouse_sound, int keyboard_volume, + int mouse_volume); // Read ~/.keyvibe.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 read_user_config(const char *path, char **out_keyboard_sound, + char **out_mouse_sound, int *out_keyboard_volume, + int *out_mouse_volume); #endif // KEYVIBE_CONFIG_IO_H - - diff --git a/src/app/process.c b/src/app/process.c new file mode 100644 index 0000000..44c1ca8 --- /dev/null +++ b/src/app/process.c @@ -0,0 +1,164 @@ +#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 KeyVibe...\n"); + } + if (sound_pid > 0) { + kill(sound_pid, SIGTERM); + waitpid(sound_pid, NULL, 0); + } + if (keyboard_pid > 0) { + kill(keyboard_pid, SIGTERM); + waitpid(keyboard_pid, NULL, 0); + } + if (pidfile_path[0] != '\0') { + unlink(pidfile_path); + } + exit(0); +} + +int 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; +} + +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) { + (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/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 mouse_volume_str[32]; + snprintf(mouse_volume_str, sizeof(mouse_volume_str), "%d", mouse_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); + char keyboard_mute_str[8]; + snprintf(keyboard_mute_str, sizeof(keyboard_mute_str), "%d", keyboard_mute); + char mouse_mute_str[8]; + snprintf(mouse_mute_str, sizeof(mouse_mute_str), "%d", mouse_mute); + execl(sound_player_path, "keyvibe-audio", "config.json", volume_str, + verbose_str, mute_str, mouse_config_path, mouse_volume_str, + keyboard_mute_str, mouse_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; +} + +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..6a9b1a9 --- /dev/null +++ b/src/app/reload.c @@ -0,0 +1,95 @@ +#include "app/reload.h" +#include "app/process.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) { + char *cfg_keyboard_sound = NULL; + char *cfg_mouse_sound = NULL; + int cfg_keyboard_volume = *current_keyboard_volume; + int cfg_mouse_volume = *current_mouse_volume; + if (read_user_config(user_cfg_path, &cfg_keyboard_sound, &cfg_mouse_sound, + &cfg_keyboard_volume, &cfg_mouse_volume)) { + printf("Read config - keyboard:%s mouse:%s kvol:%d mvol:%d (current " + "kvol:%d mvol:%d)\n", + cfg_keyboard_sound ? cfg_keyboard_sound : "NULL", + cfg_mouse_sound ? cfg_mouse_sound : "NULL", cfg_keyboard_volume, + cfg_mouse_volume, *current_keyboard_volume, *current_mouse_volume); + int keyboard_changed = 0, mouse_changed = 0, keyboard_volume_changed = 0, + mouse_volume_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; + } + printf("Config changed - keyboard:%d mouse:%d kvol:%d mvol:%d\n", + keyboard_changed, mouse_changed, keyboard_volume_changed, + mouse_volume_changed); + if (keyboard_changed || mouse_changed || keyboard_volume_changed || + mouse_volume_changed) { + 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); + 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..9e5d0e9 --- /dev/null +++ b/src/app/watch.c @@ -0,0 +1,67 @@ +#include "app/watch.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; + strncpy(in_args.path, path, sizeof(in_args.path) - 1); + in_args.path[sizeof(in_args.path) - 1] = '\0'; + 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.c b/src/audio.c deleted file mode 100644 index 2b3beaa..0000000 --- a/src/audio.c +++ /dev/null @@ -1,787 +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[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; - -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; -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; - - // Handle mouse key names first - 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 { - // Handle regular key codes with -up suffix - 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 < 512) { - 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 { - // Try "sound" first, then "audio_file" for mouse configs - if (json_object_object_get_ex(root, "sound", &obj) || - json_object_object_get_ex(root, "audio_file", &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); - } - // Try "defines" first, then "definitions" for mouse configs - 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; - - // Handle mouse key names - 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) { - // Handle different config formats - if (json_object_is_type(val, json_type_array)) { - // Standard format: [start_ms, duration_ms] - 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)) { - // Mouse format: {"timing": [[start1, duration1], [start2, - // duration2]]} - 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) { - // Use the first timing entry - 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; -} - -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; - - // Determine if this is a mouse event (key codes 272, 273, 274) - 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"); - } - - // Check if this type of sound is muted - 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, "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] * 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; - } - 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 < 512; 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; -} - -static int read_keyboard_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-kbd-mute-%d", rd, - (int)getuid())) { - return 0; - } - FILE *f = fopen(mute_file, "r"); - if (!f) { - return 0; - } - int mute = 0; - if (fscanf(f, "%d", &mute) == 1) { - fclose(f); - return mute; - } - fclose(f); - return 0; -} - -static int read_mouse_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-mouse-mute-%d", - rd, (int)getuid())) { - return 0; - } - FILE *f = fopen(mute_file, "r"); - if (!f) { - return 0; - } - 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 > 9) { - fprintf(stderr, - "Usage: %s [volume] [verbose] [mute] [mouse_config] " - "[mouse_volume] [keyboard_mute] [mouse_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 all sound (default: 0)\n"); - fprintf(stderr, " mouse_config: path to mouse config.json (optional)\n"); - fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n"); - fprintf(stderr, " keyboard_mute: 1 to mute keyboard sounds (optional)\n"); - fprintf(stderr, " mouse_mute: 1 to mute mouse sounds (optional)\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"); - } - } - - // Parse mouse config and volume if provided - if (argc >= 6) { - if (load_sound_config(argv[5]) != 0) { - fprintf(stderr, "Failed to load mouse sound configuration\n"); - return 1; - } - // Copy the loaded config to mouse sound pack - 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 = atoi(argv[6]); - if (mouse_volume_percent < 0) - mouse_volume_percent = 0; - if (mouse_volume_percent > 100) - mouse_volume_percent = 100; - 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 (load_sound_config(argv[1]) != 0) { - fprintf(stderr, "Failed to load keyboard 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 files periodically - g_mute = read_mute_state(); - g_keyboard_mute = read_keyboard_mute_state(); - g_mouse_mute = read_mouse_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/audio/config.c b/src/audio/config.c new file mode 100644 index 0000000..4ff53b1 --- /dev/null +++ b/src/audio/config.c @@ -0,0 +1,256 @@ +#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] == '/') { + 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'; + } + } +} + +static char *xstrdup(const char *s) { + if (!s) return NULL; + size_t n = strlen(s) + 1; + char *p = (char *)malloc(n); + if (!p) return NULL; + memcpy(p, s, n); + return p; +} + +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[1024]; + 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; + 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]; + 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 < 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]; + 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); + } + 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..07b4192 --- /dev/null +++ b/src/audio/main.c @@ -0,0 +1,204 @@ +#include "audio/playback.h" +#include "audio/types.h" +#include "common/utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +int load_sound_config(const char *config_path); + +static int read_mute_state() { + 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/keyvibe-mute-%d", rd, + (int)getuid())) { + return 0; + } + FILE *f = fopen(mute_file, "r"); + if (!f) { + return 0; + } + int mute = 0; + if (fscanf(f, "%d", &mute) == 1) { + fclose(f); + return mute; + } + fclose(f); + return 0; +} + +static int read_keyboard_mute_state() { + 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/keyvibe-kbd-mute-%d", rd, + (int)getuid())) { + return 0; + } + FILE *f = fopen(mute_file, "r"); + if (!f) { + return 0; + } + int mute = 0; + if (fscanf(f, "%d", &mute) == 1) { + fclose(f); + return mute; + } + fclose(f); + return 0; +} + +static int read_mouse_mute_state() { + 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/keyvibe-mouse-mute-%d", + rd, (int)getuid())) { + return 0; + } + FILE *f = fopen(mute_file, "r"); + if (!f) { + return 0; + } + 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 > 9) { + fprintf(stderr, + "Usage: %s [volume] [verbose] [mute] [mouse_config] " + "[mouse_volume] [keyboard_mute] [mouse_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 all sound (default: 0)\n"); + fprintf(stderr, " mouse_config: path to mouse config.json (optional)\n"); + fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n"); + fprintf(stderr, " keyboard_mute: 1 to mute keyboard sounds (optional)\n"); + fprintf(stderr, " mouse_mute: 1 to mute mouse sounds (optional)\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 (argc >= 6) { + if (load_sound_config(argv[5]) != 0) { + 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 = atoi(argv[6]); + if (mouse_volume_percent < 0) + mouse_volume_percent = 0; + if (mouse_volume_percent > 100) + mouse_volume_percent = 100; + 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 (load_sound_config(argv[1]) != 0) { + fprintf(stderr, "Failed to load keyboard sound configuration\n"); + return 1; + } + if (init_audio() != 0) { + fprintf(stderr, "Failed to initialize audio\n"); + return 1; + } + fd_set readfds; + struct timeval timeout; + char line[1024]; + while (1) { + g_mute = read_mute_state(); + g_keyboard_mute = read_keyboard_mute_state(); + g_mouse_mute = read_mouse_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) { + 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..c8a4437 --- /dev/null +++ b/src/audio/playback.c @@ -0,0 +1,289 @@ +#include "audio/playback.h" +#include "audio/types.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 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; + + 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, "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] * 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; + } + 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); +} + +static char *xstrdup2(const char *s) { + if (!s) + return NULL; + size_t n = strlen(s) + 1; + char *p = (char *)malloc(n); + if (!p) + return NULL; + memcpy(p, s, n); + return p; +} + +int parse_keyboard_event(const char *json_line, int *key_code, + int *is_pressed) { + char *line_copy = xstrdup2(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..8d193e4 --- /dev/null +++ b/src/cli.c @@ -0,0 +1,158 @@ +#include "app/cli.h" +#include +#include +#include +#include + +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 keyboard sound pack (default: " + "eg-oreo)\n"); + printf( + " -M, --mouse SOUND_NAME Select mouse sound pack (default: ping)\n"); + printf(" -V, --volume VOLUME Set volume [0-100] for both keyboard and " + "mouse (default: 50)\n"); + printf(" -K, --keyboard-volume VOLUME Set keyboard volume [0-100] " + "(default: 50)\n"); + printf(" -O, --mouse-volume VOLUME Set mouse 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[=DEVICE] Mute sounds (keyboard|mouse|both, " + "default: both)\n"); + printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both, " + "default: both)\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 parse_cli(int argc, char **argv, CliOptions *out) { + memset(out, 0, sizeof(*out)); + out->volume = -1; + out->keyboard_volume = -1; + out->mouse_volume = -1; + out->keyboard_mute = -1; + out->mouse_mute = -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'}, + {"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 == 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; + } + } + } + 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': + out->volume = atoi(optarg); + if (out->volume < 0) + out->volume = 0; + if (out->volume > 100) + out->volume = 100; + break; + case 'K': + out->keyboard_volume = atoi(optarg); + if (out->keyboard_volume < 0) + out->keyboard_volume = 0; + if (out->keyboard_volume > 100) + out->keyboard_volume = 100; + break; + case 'O': + out->mouse_volume = atoi(optarg); + if (out->mouse_volume < 0) + out->mouse_volume = 0; + if (out->mouse_volume > 100) + out->mouse_volume = 100; + break; + case 'm': + if (optarg == NULL || strcmp(optarg, "both") == 0) { + out->keyboard_mute = 1; + out->mouse_mute = 1; + } else if (strcmp(optarg, "keyboard") == 0) { + out->keyboard_mute = 1; + } else if (strcmp(optarg, "mouse") == 0) { + out->mouse_mute = 1; + } else { + fprintf(stderr, + "Invalid mute option: %s. Use 'keyboard', 'mouse', or 'both'\n", + optarg); + return 1; + } + break; + case 'u': + if (optarg == NULL || strcmp(optarg, "both") == 0) { + out->keyboard_mute = 0; + out->mouse_mute = 0; + } else if (strcmp(optarg, "keyboard") == 0) { + out->keyboard_mute = 0; + } else if (strcmp(optarg, "mouse") == 0) { + out->mouse_mute = 0; + } else { + fprintf( + stderr, + "Invalid unmute option: %s. Use 'keyboard', 'mouse', or 'both'\n", + optarg); + 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/utils.c b/src/common/utils.c similarity index 98% rename from src/utils.c rename to src/common/utils.c index 9427388..c66f192 100644 --- a/src/utils.c +++ b/src/common/utils.c @@ -1,5 +1,5 @@ #define _POSIX_C_SOURCE 200809L -#include "utils.h" +#include "common/utils.h" #include #include #include @@ -8,7 +8,6 @@ #include #include -// Mute helpers moved here for re-use int read_runtime_mute_file() { char mute_file[1024]; const char *rd = getenv("XDG_RUNTIME_DIR"); diff --git a/src/config_io.c b/src/config.c similarity index 98% rename from src/config_io.c rename to src/config.c index f01911a..96a0c07 100644 --- a/src/config_io.c +++ b/src/config.c @@ -1,5 +1,5 @@ -/* #include "config_io.h" */ -#include "utils.h" +#include "common/utils.h" +#include "user_config.h" #include #include #include diff --git a/src/input.c b/src/input.c index 01825e1..3e505f7 100644 --- a/src/input.c +++ b/src/input.c @@ -1,4 +1,4 @@ -#include "utils.h" +#include "common/utils.h" #include #include #include diff --git a/src/main.c b/src/main.c index 6b052d8..54ee093 100644 --- a/src/main.c +++ b/src/main.c @@ -1,10 +1,14 @@ #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 "config_io.h" -#include "mute.h" +#include "user_config.h" #include "soundpacks.h" -#include "utils.h" #include #include #include @@ -32,15 +36,11 @@ #define USER_MOUSE_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/mouse" #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 volatile sig_atomic_t keyboard_reload_requested = 0; -static volatile sig_atomic_t mouse_reload_requested = 0; -static volatile sig_atomic_t volume_reload_requested = 0; +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}; @@ -54,273 +54,9 @@ 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; -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 keyboard sound pack (default: " - "eg-oreo)\n"); - printf( - " -M, --mouse SOUND_NAME Select mouse sound pack (default: ping)\n"); - printf(" -V, --volume VOLUME Set volume [0-100] for both keyboard and " - "mouse (default: 50)\n"); - printf(" -K, --keyboard-volume VOLUME Set keyboard volume [0-100] " - "(default: 50)\n"); - printf(" -O, --mouse-volume VOLUME Set mouse 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[=DEVICE] Mute sounds (keyboard|mouse|both, " - "default: both)\n"); - printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both, " - "default: both)\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"); -} - -// moved to soundpacks.c -int list_sound_packs(void); - -// moved to soundpacks.c -void get_user_keyboard_audio_dir(char *buffer, size_t buflen); - -// moved to soundpacks.c -void get_user_mouse_audio_dir(char *buffer, size_t buflen); - -// moved to soundpacks.c -int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir, - size_t out_sz); - -// moved to soundpacks.c -int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir, - size_t out_sz); - -// moved to soundpacks.c -int validate_keyboard_sound_pack(const char *sound_name); - -// moved to soundpacks.c -int validate_mouse_sound_pack(const char *sound_name); - -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); -} - -// moved to config_io.c -char *get_user_config_path(char *buffer, size_t buflen); - -// moved to soundpacks.c -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); - -// moved to soundpacks.c -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); - -// 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, - const char *mouse_sound_dir, - const char *mouse_config_path, int mouse_volume, - int keyboard_mute, int mouse_mute) { - (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/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 mouse_volume_str[32]; - snprintf(mouse_volume_str, sizeof(mouse_volume_str), "%d", mouse_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); - char keyboard_mute_str[8]; - snprintf(keyboard_mute_str, sizeof(keyboard_mute_str), "%d", keyboard_mute); - char mouse_mute_str[8]; - snprintf(mouse_mute_str, sizeof(mouse_mute_str), "%d", mouse_mute); - execl(sound_player_path, "keyvibe-audio", "config.json", volume_str, - verbose_str, mute_str, mouse_config_path, mouse_volume_str, - keyboard_mute_str, mouse_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; -} - -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) { - 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; // 100ms - nanosleep(&ts, NULL); - continue; - } - printf("inotify read error: %s\n", strerror(errno)); - break; - } - printf("Config file changed, requesting reload...\n"); - reload_requested = 1; - } - inotify_rm_watch(fd, wd); - close(fd); - return NULL; -} - -// moved to config_io.c -int write_user_config(const char *path, const char *keyboard_sound, - const char *mouse_sound, int keyboard_volume, - int mouse_volume); - -// moved to config_io.c -int read_user_config(const char *path, char **out_keyboard_sound, - char **out_mouse_sound, int *out_keyboard_volume, - int *out_mouse_volume); - int main(int argc, char *argv[]) { - char *sound_name = - strdup("eg-oreo"); // may be replaced by strdup; track for free - char *mouse_sound_name = - strdup("ping"); // may be replaced by strdup; track for free + 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; @@ -362,19 +98,17 @@ int main(int argc, char *argv[]) { if (sound_name_owned) { free(sound_name); } - sound_name = cfg_keyboard_sound; // now owned, must free on exit paths + 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; // now owned, must free on exit paths + mouse_sound_name = cfg_mouse_sound; mouse_sound_name_owned = 1; } - volume = cfg_keyboard_volume; // Use keyboard volume as main volume for - // CLI compatibility + volume = cfg_keyboard_volume; } } } @@ -437,7 +171,6 @@ int main(int argc, char *argv[]) { break; case 'm': if (optarg == NULL) { - // No argument provided, mute both cli_keyboard_mute = 1; cli_mouse_mute = 1; current_mute = 1; // global mute @@ -460,7 +193,6 @@ int main(int argc, char *argv[]) { break; case 'u': if (optarg == NULL) { - // No argument provided, unmute both cli_keyboard_mute = 0; cli_mouse_mute = 0; current_mute = 0; // clear global mute @@ -485,7 +217,6 @@ int main(int argc, char *argv[]) { 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) { @@ -510,7 +241,7 @@ int main(int argc, char *argv[]) { } return rc; } - build_pidfile_path(pidfile_path, sizeof(pidfile_path)); + build_pidfile_path(pidfile_path, 1024); if (flag_stop) { pid_t running_pid = 0; if (!require_running_pid(&running_pid)) @@ -530,17 +261,15 @@ int main(int argc, char *argv[]) { } return errorf("KeyVibe: process did not stop in time\n"); } - // Initialize global mute from runtime file so child inherits correct state current_mute = read_runtime_mute_file(); - // 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 + sound_name_owned = 0; config_updated = 1; } if (cli_mouse_sound != NULL) { @@ -548,7 +277,7 @@ int main(int argc, char *argv[]) { free(mouse_sound_name); } mouse_sound_name = cli_mouse_sound; - mouse_sound_name_owned = 0; // cli_mouse_sound is not owned by us + mouse_sound_name_owned = 0; config_updated = 1; } if (cli_volume >= 0) { @@ -572,13 +301,10 @@ int main(int argc, char *argv[]) { config_updated = 1; } - // Persist runtime mute state so audio child can pick it up write_runtime_mute_file(current_mute); write_runtime_keyboard_mute_file(current_keyboard_mute); write_runtime_mouse_mute_file(current_mouse_mute); - // If only CLI control options were provided and not daemon/list/stop, exit - // quietly if (config_updated && !flag_daemon && !list_sounds && !flag_stop) { if (sound_name_owned) { free(sound_name); @@ -589,7 +315,6 @@ int main(int argc, char *argv[]) { return 0; } - // 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, mouse_sound_name, @@ -600,7 +325,7 @@ int main(int argc, char *argv[]) { 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) { @@ -726,23 +451,11 @@ int main(int argc, char *argv[]) { int status; pid_t finished_pid; signal(SIGHUP, handle_sighup); - pthread_t inotify_thread; - struct inotify_thread_args in_args; - // Enable live reloading for both daemon and non-daemon modes + 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'; - printf("Setting up file watcher for: %s\n", in_args.path); - - // Check if the config file exists - if (access(in_args.path, F_OK) == 0) { - pthread_create(&inotify_thread, NULL, inotify_thread_fn, &in_args); - pthread_detach(inotify_thread); - } else { - printf("Warning: Config file %s does not exist, file watching disabled\n", - in_args.path); - } + 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"); } @@ -750,116 +463,14 @@ int main(int argc, char *argv[]) { 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))) { - char *cfg_keyboard_sound = NULL; - char *cfg_mouse_sound = NULL; - int cfg_keyboard_volume = current_keyboard_volume; - int cfg_mouse_volume = current_mouse_volume; - - if (read_user_config(user_cfg_path3, &cfg_keyboard_sound, - &cfg_mouse_sound, &cfg_keyboard_volume, - &cfg_mouse_volume)) { - printf("Read config - keyboard:%s mouse:%s kvol:%d mvol:%d (current " - "kvol:%d mvol:%d)\n", - cfg_keyboard_sound ? cfg_keyboard_sound : "NULL", - cfg_mouse_sound ? cfg_mouse_sound : "NULL", - cfg_keyboard_volume, cfg_mouse_volume, current_keyboard_volume, - current_mouse_volume); - - // Check what changed - int keyboard_changed = 0; - int mouse_changed = 0; - int keyboard_volume_changed = 0; - int mouse_volume_changed = 0; - - if (cfg_keyboard_sound && - strcmp(current_sound_name, cfg_keyboard_sound) != 0) { - keyboard_changed = 1; - strncpy(current_sound_name, cfg_keyboard_sound, - sizeof(current_sound_name) - 1); - current_sound_name[sizeof(current_sound_name) - 1] = '\0'; - } - free(cfg_keyboard_sound); - - if (cfg_mouse_sound && - strcmp(current_mouse_sound_name, cfg_mouse_sound) != 0) { - mouse_changed = 1; - strncpy(current_mouse_sound_name, cfg_mouse_sound, - sizeof(current_mouse_sound_name) - 1); - current_mouse_sound_name[sizeof(current_mouse_sound_name) - 1] = - '\0'; - } - 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; - } - - printf("Config changed - keyboard:%d mouse:%d kvol:%d mvol:%d\n", - keyboard_changed, mouse_changed, keyboard_volume_changed, - mouse_volume_changed); - - // Handle the changes immediately - if (keyboard_changed || mouse_changed || keyboard_volume_changed || - mouse_volume_changed) { - // Validate both sound packs - 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) { - // Update keyboard paths - if (build_paths_for_keyboard_sound( - current_sound_name, current_config_path, - sizeof(current_config_path), current_sound_dir, - sizeof(current_sound_dir))) { - // Update mouse paths - if (build_paths_for_mouse_sound( - current_mouse_sound_name, current_mouse_config_path, - sizeof(current_mouse_config_path), - current_mouse_sound_dir, - sizeof(current_mouse_sound_dir))) { - // Restart children with new config (including volume changes) - 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, current_keyboard_mute, - current_mouse_mute); - 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); - } 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"); - } + 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); } else { printf("Failed to get user config path\n"); } diff --git a/src/soundpacks.c b/src/soundpacks.c index f051297..d701c1c 100644 --- a/src/soundpacks.c +++ b/src/soundpacks.c @@ -1,6 +1,6 @@ -/* #include "soundpacks.h" */ +#include "soundpacks.h" +#include "common/utils.h" #include "config.h" -#include "utils.h" #include #include #include -- 2.43.0 From 52a7629834fcfc00c726514bb6aedebdd6b680c1 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 13 Oct 2025 23:05:37 +0545 Subject: [PATCH 04/18] docs: improve CLI usage instructions and error messages --- src/cli.c | 79 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/src/cli.c b/src/cli.c index 8d193e4..4578c6e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -1,33 +1,56 @@ + #include "app/cli.h" +#include "config.h" #include #include #include #include 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 keyboard sound pack (default: " - "eg-oreo)\n"); + printf("KeyVibe - Mechanical Keyboard Sound Simulator\n"); + printf("Version: %s\n\n", PROJECT_VERSION); + + printf("USAGE:\n"); + printf(" %s [OPTIONS]\n\n", program_name); + + printf("SOUND CONTROL:\n"); + printf(" -S, --sound PACK Keyboard sound pack (default: eg-oreo)\n"); + printf(" -M, --mouse PACK Mouse sound pack (default: ping)\n"); + printf(" -l, --list List available sound packs\n\n"); + + printf("VOLUME CONTROL:\n"); printf( - " -M, --mouse SOUND_NAME Select mouse sound pack (default: ping)\n"); - printf(" -V, --volume VOLUME Set volume [0-100] for both keyboard and " - "mouse (default: 50)\n"); - printf(" -K, --keyboard-volume VOLUME Set keyboard volume [0-100] " - "(default: 50)\n"); - printf(" -O, --mouse-volume VOLUME Set mouse 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[=DEVICE] Mute sounds (keyboard|mouse|both, " - "default: both)\n"); - printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both, " - "default: both)\n"); - printf(" -h, --help Show this help message\n"); + " -V, --volume LEVEL Set both keyboard & mouse volume [0-100]\n"); + printf(" -K, --keyboard-volume Set keyboard volume [0-100]\n"); + printf(" -O, --mouse-volume Set mouse volume [0-100]\n\n"); + + printf("MUTE CONTROL:\n"); + printf(" -m, --mute[=DEVICE] Mute sounds (keyboard|mouse|both)\n"); + printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both)\n\n"); + + printf("DAEMON CONTROL:\n"); + printf(" -d, --daemon Run in background\n"); + printf(" -s, --stop Stop background daemon\n\n"); + + printf("OTHER:\n"); printf(" -v, --verbose Enable verbose output\n"); - printf(" In daemon mode, editing ~/.keyvibe.json will auto-reload.\n"); + printf(" -h, --help Show this help\n\n"); + + printf("EXAMPLES:\n"); + printf(" %s --list # List sound packs\n", + program_name); + printf(" %s -S cherrymx-blue -V 75 # Cherry MX Blue at " + "75%%\n", + program_name); + printf(" %s --daemon # Run in background\n", + program_name); + printf(" %s --mute keyboard # Mute only keyboard\n", + program_name); + printf(" %s --keyboard-volume 80 --mouse-volume 60 # Different volumes\n", + program_name); + printf("\n"); + printf("CONFIG: Settings are saved to ~/.keyvibe.json and auto-reload in " + "daemon mode.\n"); } int parse_cli(int argc, char **argv, CliOptions *out) { @@ -66,7 +89,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { (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]); + fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]); + fprintf(stderr, "Use '%s --help' for available options.\n", argv[0]); return 1; } } @@ -118,9 +142,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { } else if (strcmp(optarg, "mouse") == 0) { out->mouse_mute = 1; } else { - fprintf(stderr, - "Invalid mute option: %s. Use 'keyboard', 'mouse', or 'both'\n", - optarg); + fprintf(stderr, "Error: Invalid mute device '%s'\n", optarg); + fprintf(stderr, "Use: keyboard, mouse, or both\n"); return 1; } break; @@ -133,10 +156,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { } else if (strcmp(optarg, "mouse") == 0) { out->mouse_mute = 0; } else { - fprintf( - stderr, - "Invalid unmute option: %s. Use 'keyboard', 'mouse', or 'both'\n", - optarg); + fprintf(stderr, "Error: Invalid unmute device '%s'\n", optarg); + fprintf(stderr, "Use: keyboard, mouse, or both\n"); return 1; } break; -- 2.43.0 From c9460d54a43a27eecb2a13aa657527c18fd08eb3 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 13 Oct 2025 23:08:08 +0545 Subject: [PATCH 05/18] style(ui): improve sound pack listing output formatting --- src/cli.c | 1 - src/soundpacks.c | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/cli.c b/src/cli.c index 4578c6e..9ea8bc4 100644 --- a/src/cli.c +++ b/src/cli.c @@ -1,4 +1,3 @@ - #include "app/cli.h" #include "config.h" #include diff --git a/src/soundpacks.c b/src/soundpacks.c index d701c1c..7d097ca 100644 --- a/src/soundpacks.c +++ b/src/soundpacks.c @@ -208,10 +208,10 @@ int list_sound_packs(void) { USER_MOUSE_AUDIO_SUBPATH); } - printf("Available sound packs:\n"); - printf("======================\n\n"); - printf("Keyboard Sound Packs:\n"); - printf("---------------------\n"); + printf("Available Sound Packs\n"); + printf("=====================\n\n"); + printf("KEYBOARD SOUNDS:\n"); + printf("----------------\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) @@ -240,13 +240,13 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf(" %s (system)\n", entry->d_name); + printf(" %s (preinstalled)\n", entry->d_name); } } closedir(dir); } - printf("\nMouse Sound Packs:\n"); - printf("------------------\n"); + printf("\nMOUSE SOUNDS:\n"); + printf("-------------\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) @@ -275,7 +275,7 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf(" %s (system)\n", entry->d_name); + printf(" %s (pre-installed)\n", entry->d_name); } } closedir(dir); -- 2.43.0 From 7c10a7a5e309d0a6031d23ea288d533e6318d05a Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 13 Oct 2025 23:10:02 +0545 Subject: [PATCH 06/18] style(soundpacks): improve sound pack listing format for readability --- src/soundpacks.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/soundpacks.c b/src/soundpacks.c index 7d097ca..a321983 100644 --- a/src/soundpacks.c +++ b/src/soundpacks.c @@ -212,6 +212,8 @@ int list_sound_packs(void) { printf("=====================\n\n"); printf("KEYBOARD SOUNDS:\n"); printf("----------------\n"); + printf("%-25s %-12s\n", "Name", "Source"); + printf("%-25s %-12s\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) @@ -223,7 +225,7 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf(" %s (user)\n", entry->d_name); + printf("%-25s %-12s\n", entry->d_name, "user"); } } closedir(dir); @@ -240,13 +242,15 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf(" %s (preinstalled)\n", entry->d_name); + printf("%-25s %-12s\n", entry->d_name, "pre-installed"); } } closedir(dir); } printf("\nMOUSE SOUNDS:\n"); printf("-------------\n"); + printf("%-25s %-12s\n", "Name", "Source"); + printf("%-25s %-12s\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) @@ -258,7 +262,7 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf(" %s (user)\n", entry->d_name); + printf("%-25s %-12s\n", entry->d_name, "user"); } } closedir(dir); @@ -275,7 +279,7 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf(" %s (pre-installed)\n", entry->d_name); + printf("%-25s %-12s\n", entry->d_name, "pre-installed"); } } closedir(dir); -- 2.43.0 From 347d3048f08d3550027b7f448210c8fccedb5088 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 13 Oct 2025 23:43:50 +0545 Subject: [PATCH 07/18] feat: add enable/disable keyboard and mouse sounds with CLI options and config file --- include/app/cli.h | 2 + include/app/reload.h | 2 +- include/audio/types.h | 2 + include/common/utils.h | 4 + include/user_config.h | 4 +- src/app/reload.c | 28 +++-- src/audio/main.c | 54 ++++++++++ src/audio/playback.c | 24 +++++ src/cli.c | 91 +++++++++++++---- src/common/utils.c | 38 +++++++ src/config.c | 21 +++- src/main.c | 227 ++++++++++++----------------------------- 12 files changed, 298 insertions(+), 199 deletions(-) diff --git a/include/app/cli.h b/include/app/cli.h index 00c8e70..aec605a 100644 --- a/include/app/cli.h +++ b/include/app/cli.h @@ -13,6 +13,8 @@ typedef struct { 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); diff --git a/include/app/reload.h b/include/app/reload.h index 681b366..da71817 100644 --- a/include/app/reload.h +++ b/include/app/reload.h @@ -8,7 +8,7 @@ int handle_reload(const char *user_cfg_path, 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_mouse_mute, int *current_keyboard_enabled, int *current_mouse_enabled); #endif // KEYVIBE_RELOAD_H diff --git a/include/audio/types.h b/include/audio/types.h index 8e60206..bb9a080 100644 --- a/include/audio/types.h +++ b/include/audio/types.h @@ -39,5 +39,7 @@ 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 // KEYVIBE_AUDIO_TYPES_H diff --git a/include/common/utils.h b/include/common/utils.h index 6d9bd9e..b18330a 100644 --- a/include/common/utils.h +++ b/include/common/utils.h @@ -24,4 +24,8 @@ 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); + #endif // KEYVIBE_UTILS_H diff --git a/include/user_config.h b/include/user_config.h index f89a799..5d2d719 100644 --- a/include/user_config.h +++ b/include/user_config.h @@ -8,11 +8,11 @@ char *get_user_config_path(char *buffer, size_t buflen); // Write ~/.keyvibe.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 mouse_volume, int keyboard_enabled, int mouse_enabled); // Read ~/.keyvibe.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_mouse_volume, int *out_keyboard_enabled, int *out_mouse_enabled); #endif // KEYVIBE_CONFIG_IO_H diff --git a/src/app/reload.c b/src/app/reload.c index 6a9b1a9..2be8cb6 100644 --- a/src/app/reload.c +++ b/src/app/reload.c @@ -12,20 +12,22 @@ int handle_reload(const char *user_cfg_path, char *current_sound_name, 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_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)) { - printf("Read config - keyboard:%s mouse:%s kvol:%d mvol:%d (current " - "kvol:%d mvol:%d)\n", + &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, *current_keyboard_volume, *current_mouse_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; + 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; @@ -48,11 +50,19 @@ int handle_reload(const char *user_cfg_path, char *current_sound_name, mouse_volume_changed = 1; *current_mouse_volume = cfg_mouse_volume; } - printf("Config changed - keyboard:%d mouse:%d kvol:%d mvol:%d\n", + 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); + mouse_volume_changed, keyboard_enabled_changed, mouse_enabled_changed); if (keyboard_changed || mouse_changed || keyboard_volume_changed || - mouse_volume_changed) { + mouse_volume_changed || keyboard_enabled_changed || mouse_enabled_changed) { 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) { diff --git a/src/audio/main.c b/src/audio/main.c index 07b4192..3f72373 100644 --- a/src/audio/main.c +++ b/src/audio/main.c @@ -10,6 +10,10 @@ #include #include +// Global variables for enabled state +int g_keyboard_enabled = 1; +int g_mouse_enabled = 1; + int load_sound_config(const char *config_path); static int read_mute_state() { @@ -81,6 +85,52 @@ static int read_mouse_mute_state() { return 0; } +static int read_keyboard_enabled_state() { + char enabled_file[1024]; + const char *rd = getenv("XDG_RUNTIME_DIR"); + if (!rd || strlen(rd) == 0) { + rd = "/tmp"; + } + if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-kbd-enabled-%d", rd, + (int)getuid())) { + return 1; // default enabled + } + FILE *f = fopen(enabled_file, "r"); + if (!f) { + return 1; // default enabled + } + int enabled = 1; + if (fscanf(f, "%d", &enabled) == 1) { + fclose(f); + return enabled; + } + fclose(f); + return 1; // default enabled +} + +static int read_mouse_enabled_state() { + char enabled_file[1024]; + const char *rd = getenv("XDG_RUNTIME_DIR"); + if (!rd || strlen(rd) == 0) { + rd = "/tmp"; + } + if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-mouse-enabled-%d", + rd, (int)getuid())) { + return 1; // default enabled + } + FILE *f = fopen(enabled_file, "r"); + if (!f) { + return 1; // default enabled + } + int enabled = 1; + if (fscanf(f, "%d", &enabled) == 1) { + fclose(f); + return enabled; + } + fclose(f); + return 1; // default enabled +} + int main(int argc, char *argv[]) { if (argc < 2 || argc > 9) { fprintf(stderr, @@ -166,6 +216,10 @@ int main(int argc, char *argv[]) { g_mute = read_mute_state(); g_keyboard_mute = read_keyboard_mute_state(); g_mouse_mute = read_mouse_mute_state(); + + // Check enabled state from runtime files + g_keyboard_enabled = read_keyboard_enabled_state(); + g_mouse_enabled = read_mouse_enabled_state(); FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); diff --git a/src/audio/playback.c b/src/audio/playback.c index c8a4437..326fe80 100644 --- a/src/audio/playback.c +++ b/src/audio/playback.c @@ -212,6 +212,30 @@ void play_sound_segment(int key_code, int is_pressed) { } 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); diff --git a/src/cli.c b/src/cli.c index 9ea8bc4..6eb6f6f 100644 --- a/src/cli.c +++ b/src/cli.c @@ -8,48 +8,45 @@ void print_usage(const char *program_name) { printf("KeyVibe - Mechanical Keyboard Sound Simulator\n"); printf("Version: %s\n\n", PROJECT_VERSION); - + printf("USAGE:\n"); printf(" %s [OPTIONS]\n\n", program_name); - + printf("SOUND CONTROL:\n"); printf(" -S, --sound PACK Keyboard sound pack (default: eg-oreo)\n"); printf(" -M, --mouse PACK Mouse sound pack (default: ping)\n"); printf(" -l, --list List available sound packs\n\n"); - + printf("VOLUME CONTROL:\n"); - printf( - " -V, --volume LEVEL Set both keyboard & mouse volume [0-100]\n"); + printf(" -V, --volume LEVEL Set both keyboard & mouse volume [0-100]\n"); printf(" -K, --keyboard-volume Set keyboard volume [0-100]\n"); printf(" -O, --mouse-volume Set mouse volume [0-100]\n\n"); - + printf("MUTE CONTROL:\n"); printf(" -m, --mute[=DEVICE] Mute sounds (keyboard|mouse|both)\n"); printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both)\n\n"); - + + printf("ENABLE/DISABLE:\n"); + printf(" --enable[=DEVICE] Enable sounds (keyboard|mouse|both)\n"); + printf(" --disable[=DEVICE] Disable sounds (keyboard|mouse|both)\n\n"); + printf("DAEMON CONTROL:\n"); printf(" -d, --daemon Run in background\n"); printf(" -s, --stop Stop background daemon\n\n"); - + printf("OTHER:\n"); printf(" -v, --verbose Enable verbose output\n"); printf(" -h, --help Show this help\n\n"); - + printf("EXAMPLES:\n"); - printf(" %s --list # List sound packs\n", - program_name); - printf(" %s -S cherrymx-blue -V 75 # Cherry MX Blue at " - "75%%\n", - program_name); - printf(" %s --daemon # Run in background\n", - program_name); - printf(" %s --mute keyboard # Mute only keyboard\n", - program_name); - printf(" %s --keyboard-volume 80 --mouse-volume 60 # Different volumes\n", - program_name); + printf(" %s --list # List sound packs\n", program_name); + printf(" %s -S cherrymx-blue -V 75 # Cherry MX Blue at 75%%\n", program_name); + printf(" %s --daemon # Run in background\n", program_name); + printf(" %s --mute keyboard # Mute only keyboard\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("CONFIG: Settings are saved to ~/.keyvibe.json and auto-reload in " - "daemon mode.\n"); + printf("CONFIG: Settings are saved to ~/.keyvibe.json and auto-reload in daemon mode.\n"); } int parse_cli(int argc, char **argv, CliOptions *out) { @@ -59,6 +56,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { 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'}, @@ -67,6 +66,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { {"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'}, @@ -86,6 +87,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { (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))) { fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]); @@ -160,6 +163,50 @@ int parse_cli(int argc, char **argv, CliOptions *out) { return 1; } break; + case 1000: // --enable + 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 (strcmp(optarg, "both") == 0) { + out->keyboard_enabled = 1; + out->mouse_enabled = 1; + } else if (strcmp(optarg, "keyboard") == 0) { + out->keyboard_enabled = 1; + } else if (strcmp(optarg, "mouse") == 0) { + out->mouse_enabled = 1; + } else { + fprintf(stderr, "Error: Invalid enable device '%s'\n", optarg); + fprintf(stderr, "Use: keyboard, mouse, or both\n"); + return 1; + } + break; + case 1001: // --disable + if (optarg == NULL) { + // Handle space-separated argument: --disable mouse + if (optind < argc && argv[optind][0] != '-') { + optarg = argv[optind++]; + } else { + optarg = "both"; // default to both if no argument + } + } + if (strcmp(optarg, "both") == 0) { + out->keyboard_enabled = 0; + out->mouse_enabled = 0; + } else if (strcmp(optarg, "keyboard") == 0) { + out->keyboard_enabled = 0; + } else if (strcmp(optarg, "mouse") == 0) { + out->mouse_enabled = 0; + } else { + fprintf(stderr, "Error: Invalid disable device '%s'\n", optarg); + fprintf(stderr, "Use: keyboard, mouse, or both\n"); + return 1; + } + break; case 'l': out->list_flag = 1; break; diff --git a/src/common/utils.c b/src/common/utils.c index c66f192..3b13fe1 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -131,3 +131,41 @@ int process_is_running(pid_t pid) { return 0; return kill(pid, 0) == 0; } + +int write_runtime_keyboard_enabled_file(int enabled) { + char enabled_file[1024]; + const char *rd = getenv("XDG_RUNTIME_DIR"); + if (!rd || strlen(rd) == 0) { + rd = "/tmp"; + } + if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-kbd-enabled-%d", rd, + (int)getuid())) { + return 0; + } + FILE *f = fopen(enabled_file, "w"); + if (!f) { + return 0; + } + fprintf(f, "%d\n", enabled); + fclose(f); + return 1; +} + +int write_runtime_mouse_enabled_file(int enabled) { + char enabled_file[1024]; + const char *rd = getenv("XDG_RUNTIME_DIR"); + if (!rd || strlen(rd) == 0) { + rd = "/tmp"; + } + if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-mouse-enabled-%d", rd, + (int)getuid())) { + return 0; + } + FILE *f = fopen(enabled_file, "w"); + if (!f) { + return 0; + } + fprintf(f, "%d\n", enabled); + fclose(f); + return 1; +} diff --git a/src/config.c b/src/config.c index 96a0c07..d9e6ee5 100644 --- a/src/config.c +++ b/src/config.c @@ -35,12 +35,14 @@ char *get_user_config_path(char *buffer, size_t buflen) { int write_user_config(const char *path, const char *keyboard_sound, const char *mouse_sound, int keyboard_volume, - int mouse_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", @@ -48,6 +50,8 @@ int write_user_config(const char *path, const char *keyboard_sound, 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)); @@ -68,7 +72,7 @@ int write_user_config(const char *path, const char *keyboard_sound, 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_mouse_volume, int *out_keyboard_enabled, int *out_mouse_enabled) { FILE *f = fopen(path, "r"); if (!f) return 0; @@ -101,6 +105,16 @@ int read_user_config(const char *path, char **out_keyboard_sound, 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) @@ -118,6 +132,9 @@ int read_user_config(const char *path, char **out_keyboard_sound, *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); diff --git a/src/main.c b/src/main.c index 54ee093..4ed5101 100644 --- a/src/main.c +++ b/src/main.c @@ -48,6 +48,8 @@ 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}; @@ -63,28 +65,8 @@ int main(int argc, char *argv[]) { int list_sounds = 0; int flag_daemon = 0; int flag_stop = 0; - 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'}, - {"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}}; int volume = 50; - char *cli_sound = NULL; - char *cli_mouse_sound = NULL; - int cli_volume = -1; - int cli_keyboard_volume = -1; - int cli_mouse_volume = -1; - int cli_keyboard_mute = -1; - int cli_mouse_mute = -1; + 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) { @@ -92,8 +74,10 @@ int main(int argc, char *argv[]) { 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_volume, &cfg_mouse_volume, &cfg_keyboard_enabled, &cfg_mouse_enabled)) { if (cfg_keyboard_sound) { if (sound_name_owned) { free(sound_name); @@ -109,131 +93,36 @@ int main(int argc, char *argv[]) { mouse_sound_name_owned = 1; } volume = cfg_keyboard_volume; + current_keyboard_enabled = cfg_keyboard_enabled; + current_mouse_enabled = cfg_mouse_enabled; } } } - 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 == 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:M:V:K:O:lhdsm::u::v", long_options, - NULL)) != -1) { - switch (opt) { - case 'd': - flag_daemon = 1; - break; - case 's': - flag_stop = 1; - break; - case 'S': - cli_sound = optarg; - break; - case 'M': - cli_mouse_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 'K': - cli_keyboard_volume = atoi(optarg); - if (cli_keyboard_volume < 0) - cli_keyboard_volume = 0; - if (cli_keyboard_volume > 100) - cli_keyboard_volume = 100; - break; - case 'O': - cli_mouse_volume = atoi(optarg); - if (cli_mouse_volume < 0) - cli_mouse_volume = 0; - if (cli_mouse_volume > 100) - cli_mouse_volume = 100; - break; - case 'm': - if (optarg == NULL) { - cli_keyboard_mute = 1; - cli_mouse_mute = 1; - current_mute = 1; // global mute - } else if (strcmp(optarg, "keyboard") == 0) { - cli_keyboard_mute = 1; - write_runtime_keyboard_mute_file(1); - } else if (strcmp(optarg, "mouse") == 0) { - cli_mouse_mute = 1; - write_runtime_mouse_mute_file(1); - } else if (strcmp(optarg, "both") == 0) { - cli_keyboard_mute = 1; - cli_mouse_mute = 1; - current_mute = 1; // global mute - } else { - fprintf(stderr, - "Invalid mute option: %s. Use 'keyboard', 'mouse', or 'both'\n", - optarg); - return 1; - } - break; - case 'u': - if (optarg == NULL) { - cli_keyboard_mute = 0; - cli_mouse_mute = 0; - current_mute = 0; // clear global mute - } else if (strcmp(optarg, "keyboard") == 0) { - cli_keyboard_mute = 0; - write_runtime_keyboard_mute_file(0); - } else if (strcmp(optarg, "mouse") == 0) { - cli_mouse_mute = 0; - write_runtime_mouse_mute_file(0); - } else if (strcmp(optarg, "both") == 0) { - cli_keyboard_mute = 0; - cli_mouse_mute = 0; - current_mute = 0; // clear global mute - } else { - fprintf( - stderr, - "Invalid unmute option: %s. Use 'keyboard', 'mouse', or 'both'\n", - optarg); - return 1; - } - break; - case 'l': - list_sounds = 1; - break; - case 'h': - print_usage(argv[0]); + + // Parse CLI arguments + int parse_result = parse_cli(argc, argv, &cli_opts); + if (parse_result == 1) { if (sound_name_owned) { free(sound_name); } - return 0; - case 'v': - verbose = 1; - break; - default: - print_usage(argv[0]); + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } + return 1; + } else if (parse_result == 2) { if (sound_name_owned) { free(sound_name); } - return 1; + 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) { @@ -264,46 +153,68 @@ int main(int argc, char *argv[]) { current_mute = read_runtime_mute_file(); int config_updated = 0; - if (cli_sound != NULL) { + if (cli_opts.sound != NULL) { if (sound_name_owned) { free(sound_name); } - sound_name = cli_sound; + sound_name = cli_opts.sound; sound_name_owned = 0; config_updated = 1; } - if (cli_mouse_sound != NULL) { + if (cli_opts.mouse_sound != NULL) { if (mouse_sound_name_owned) { free(mouse_sound_name); } - mouse_sound_name = cli_mouse_sound; + mouse_sound_name = cli_opts.mouse_sound; mouse_sound_name_owned = 0; config_updated = 1; } - if (cli_volume >= 0) { - volume = cli_volume; + if (cli_opts.volume >= 0) { + volume = cli_opts.volume; config_updated = 1; } - if (cli_keyboard_volume >= 0) { - current_keyboard_volume = cli_keyboard_volume; + if (cli_opts.keyboard_volume >= 0) { + current_keyboard_volume = cli_opts.keyboard_volume; config_updated = 1; } - if (cli_mouse_volume >= 0) { - current_mouse_volume = cli_mouse_volume; + if (cli_opts.mouse_volume >= 0) { + current_mouse_volume = cli_opts.mouse_volume; config_updated = 1; } - if (cli_keyboard_mute >= 0) { - current_keyboard_mute = cli_keyboard_mute; + if (cli_opts.keyboard_mute >= 0) { + current_keyboard_mute = cli_opts.keyboard_mute; config_updated = 1; } - if (cli_mouse_mute >= 0) { - current_mouse_mute = cli_mouse_mute; + if (cli_opts.mouse_mute >= 0) { + current_mouse_mute = cli_opts.mouse_mute; + config_updated = 1; + } + if (cli_opts.keyboard_enabled >= 0) { + current_keyboard_enabled = cli_opts.keyboard_enabled; + config_updated = 1; + } + if (cli_opts.mouse_enabled >= 0) { + current_mouse_enabled = cli_opts.mouse_enabled; config_updated = 1; } 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); + + // Update config file if needed + 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)) { + 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); + } + } if (config_updated && !flag_daemon && !list_sounds && !flag_stop) { if (sound_name_owned) { @@ -315,22 +226,11 @@ int main(int argc, char *argv[]) { return 0; } - 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)) { - 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); - } - } - 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_volume, current_mouse_volume, current_keyboard_enabled, current_mouse_enabled)) { fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path); } else if (verbose) { fprintf(stderr, "Created default config %s\n", user_cfg_path); @@ -470,7 +370,8 @@ int main(int argc, char *argv[]) { ¤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); + current_mouse_volume, current_keyboard_mute, current_mouse_mute, + ¤t_keyboard_enabled, ¤t_mouse_enabled); } else { printf("Failed to get user config path\n"); } -- 2.43.0 From 65dc7da8008e81192b0e01ea9316c11291406546 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 14 Oct 2025 23:32:58 +0545 Subject: [PATCH 08/18] oops many changes --- Makefile | 8 +-- include/app/process.h | 2 +- include/common/utils.h | 5 ++ src/app/process.c | 32 +++++---- src/app/reload.c | 26 +++++-- src/audio/config.c | 10 +-- src/audio/main.c | 145 ++++++++++--------------------------- src/audio/playback.c | 15 +--- src/cli.c | 159 ++++++++++++++++++----------------------- src/common/utils.c | 99 ++++++++++--------------- src/config.c | 16 ++--- src/main.c | 32 +++++---- 12 files changed, 219 insertions(+), 330 deletions(-) diff --git a/Makefile b/Makefile index 28554f7..cb41f06 100644 --- a/Makefile +++ b/Makefile @@ -52,8 +52,8 @@ test: all 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) + install -Dm755 $(SOUND_TARGET) $(DESTDIR)$(BINDIR)/keyvibe-audio + install -Dm755 $(KEYBOARD_TARGET) $(DESTDIR)$(BINDIR)/keyvibe-input install -d $(DESTDIR)$(SHAREDIR) cp -r soundpacks $(DESTDIR)$(SHAREDIR)/ @echo "Installing udev rule for non-root keyboard access..." @@ -72,8 +72,8 @@ install: 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) + rm -f $(DESTDIR)$(BINDIR)/keyvibe-audio + rm -f $(DESTDIR)$(BINDIR)/keyvibe-input rm -rf $(DESTDIR)$(SHAREDIR) @echo "Removing udev rule..." @sudo rm -f $(UDEV_RULE) || true diff --git a/include/app/process.h b/include/app/process.h index 55d91f3..4f35bc8 100644 --- a/include/app/process.h +++ b/include/app/process.h @@ -17,7 +17,7 @@ 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_mute, int mouse_mute, int keyboard_enabled, int mouse_enabled); void handle_sighup(int sig); #endif // KEYVIBE_PROCESS_H diff --git a/include/common/utils.h b/include/common/utils.h index b18330a..a85677b 100644 --- a/include/common/utils.h +++ b/include/common/utils.h @@ -28,4 +28,9 @@ void write_runtime_mouse_mute_file(int mute); 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); + #endif // KEYVIBE_UTILS_H diff --git a/src/app/process.c b/src/app/process.c index 44c1ca8..351be06 100644 --- a/src/app/process.c +++ b/src/app/process.c @@ -87,10 +87,12 @@ 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_mute, int mouse_mute, int keyboard_enabled, + int mouse_enabled) { (void)config_path; (void)mouse_sound_dir; int pipefd[2]; @@ -116,21 +118,23 @@ int start_children(const char *sound_dir, const char *config_path, int volume, perror("chdir"); exit(1); } - char volume_str[32]; - snprintf(volume_str, sizeof(volume_str), "%d", volume); - char mouse_volume_str[32]; - snprintf(mouse_volume_str, sizeof(mouse_volume_str), "%d", mouse_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); - char keyboard_mute_str[8]; - snprintf(keyboard_mute_str, sizeof(keyboard_mute_str), "%d", keyboard_mute); - char mouse_mute_str[8]; - snprintf(mouse_mute_str, sizeof(mouse_mute_str), "%d", mouse_mute); + 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, "keyvibe-audio", "config.json", volume_str, verbose_str, mute_str, mouse_config_path, mouse_volume_str, - keyboard_mute_str, mouse_mute_str, (char *)NULL); + keyboard_mute_str, mouse_mute_str, keyboard_enabled_str, + mouse_enabled_str, (char *)NULL); perror("execl keyvibe-audio"); exit(1); } diff --git a/src/app/reload.c b/src/app/reload.c index 2be8cb6..7435646 100644 --- a/src/app/reload.c +++ b/src/app/reload.c @@ -1,5 +1,6 @@ #include "app/reload.h" #include "app/process.h" +#include "common/utils.h" #include "soundpacks.h" #include "user_config.h" #include @@ -21,13 +22,16 @@ int handle_reload(const char *user_cfg_path, char *current_sound_name, 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_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; + 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; @@ -58,11 +62,18 @@ int handle_reload(const char *user_cfg_path, char *current_sound_name, 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", + 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); + 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) { + 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) { @@ -77,7 +88,8 @@ int handle_reload(const char *user_cfg_path, char *current_sound_name, *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_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, diff --git a/src/audio/config.c b/src/audio/config.c index 4ff53b1..2184704 100644 --- a/src/audio/config.c +++ b/src/audio/config.c @@ -1,12 +1,12 @@ #include "audio/types.h" #include "common/utils.h" +#include #include #include #include #include #include #include -#include SoundPack g_sound_pack = {0}; SoundPack g_mouse_sound_pack = {0}; @@ -33,14 +33,6 @@ static void get_full_path(char *buffer, size_t buffer_size, } } -static char *xstrdup(const char *s) { - if (!s) return NULL; - size_t n = strlen(s) + 1; - char *p = (char *)malloc(n); - if (!p) return NULL; - memcpy(p, s, n); - return p; -} int load_sound_config(const char *config_path) { FILE *file = fopen(config_path, "r"); diff --git a/src/audio/main.c b/src/audio/main.c index 3f72373..e84a7fb 100644 --- a/src/audio/main.c +++ b/src/audio/main.c @@ -10,132 +10,61 @@ #include #include -// Global variables for enabled state int g_keyboard_enabled = 1; int g_mouse_enabled = 1; int load_sound_config(const char *config_path); -static int read_mute_state() { - char mute_file[1024]; +// 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(mute_file, sizeof(mute_file), "%s/keyvibe-mute-%d", rd, - (int)getuid())) { - return 0; + if (!safe_snprintf(state_file, sizeof(state_file), "%s/keyvibe-%s-%d", rd, + filename_suffix, (int)getuid())) { + return default_value; } - FILE *f = fopen(mute_file, "r"); + FILE *f = fopen(state_file, "r"); if (!f) { - return 0; + return default_value; } - int mute = 0; - if (fscanf(f, "%d", &mute) == 1) { + int state = default_value; + if (fscanf(f, "%d", &state) == 1) { fclose(f); - return mute; + return state; } fclose(f); - return 0; + return default_value; +} + +static int read_mute_state() { + return read_runtime_state("mute", 0); } static int read_keyboard_mute_state() { - 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/keyvibe-kbd-mute-%d", rd, - (int)getuid())) { - return 0; - } - FILE *f = fopen(mute_file, "r"); - if (!f) { - return 0; - } - int mute = 0; - if (fscanf(f, "%d", &mute) == 1) { - fclose(f); - return mute; - } - fclose(f); - return 0; + return read_runtime_state("kbd-mute", 0); } static int read_mouse_mute_state() { - 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/keyvibe-mouse-mute-%d", - rd, (int)getuid())) { - return 0; - } - FILE *f = fopen(mute_file, "r"); - if (!f) { - return 0; - } - int mute = 0; - if (fscanf(f, "%d", &mute) == 1) { - fclose(f); - return mute; - } - fclose(f); - return 0; + return read_runtime_state("mouse-mute", 0); } static int read_keyboard_enabled_state() { - char enabled_file[1024]; - const char *rd = getenv("XDG_RUNTIME_DIR"); - if (!rd || strlen(rd) == 0) { - rd = "/tmp"; - } - if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-kbd-enabled-%d", rd, - (int)getuid())) { - return 1; // default enabled - } - FILE *f = fopen(enabled_file, "r"); - if (!f) { - return 1; // default enabled - } - int enabled = 1; - if (fscanf(f, "%d", &enabled) == 1) { - fclose(f); - return enabled; - } - fclose(f); - return 1; // default enabled + return read_runtime_state("kbd-enabled", 1); } static int read_mouse_enabled_state() { - char enabled_file[1024]; - const char *rd = getenv("XDG_RUNTIME_DIR"); - if (!rd || strlen(rd) == 0) { - rd = "/tmp"; - } - if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-mouse-enabled-%d", - rd, (int)getuid())) { - return 1; // default enabled - } - FILE *f = fopen(enabled_file, "r"); - if (!f) { - return 1; // default enabled - } - int enabled = 1; - if (fscanf(f, "%d", &enabled) == 1) { - fclose(f); - return enabled; - } - fclose(f); - return 1; // default enabled + return read_runtime_state("mouse-enabled", 1); } int main(int argc, char *argv[]) { - if (argc < 2 || argc > 9) { + if (argc < 2 || argc > 11) { fprintf(stderr, "Usage: %s [volume] [verbose] [mute] [mouse_config] " - "[mouse_volume] [keyboard_mute] [mouse_mute]\n", + "[mouse_volume] [keyboard_mute] [mouse_mute] [keyboard_enabled] " + "[mouse_enabled]\n", argv[0]); fprintf(stderr, " volume: 0-100 (default: 50)\n"); fprintf(stderr, " verbose: 1 to enable verbose output (default: 0)\n"); @@ -144,14 +73,13 @@ int main(int argc, char *argv[]) { fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n"); fprintf(stderr, " keyboard_mute: 1 to mute keyboard sounds (optional)\n"); fprintf(stderr, " mouse_mute: 1 to mute mouse sounds (optional)\n"); + fprintf(stderr, + " keyboard_enabled: 1 to enable keyboard sounds (optional)\n"); + fprintf(stderr, " mouse_enabled: 1 to enable mouse sounds (optional)\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; + 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); @@ -182,11 +110,7 @@ int main(int argc, char *argv[]) { } } if (argc >= 7) { - int mouse_volume_percent = atoi(argv[6]); - if (mouse_volume_percent < 0) - mouse_volume_percent = 0; - if (mouse_volume_percent > 100) - mouse_volume_percent = 100; + 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); @@ -201,6 +125,16 @@ int main(int argc, char *argv[]) { 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) { fprintf(stderr, "Failed to load keyboard sound configuration\n"); return 1; @@ -216,8 +150,7 @@ int main(int argc, char *argv[]) { g_mute = read_mute_state(); g_keyboard_mute = read_keyboard_mute_state(); g_mouse_mute = read_mouse_mute_state(); - - // Check enabled state from runtime files + g_keyboard_enabled = read_keyboard_enabled_state(); g_mouse_enabled = read_mouse_enabled_state(); diff --git a/src/audio/playback.c b/src/audio/playback.c index 326fe80..31a00b9 100644 --- a/src/audio/playback.c +++ b/src/audio/playback.c @@ -1,5 +1,6 @@ #include "audio/playback.h" #include "audio/types.h" +#include "common/utils.h" #include #include #include @@ -212,7 +213,7 @@ void play_sound_segment(int key_code, int is_pressed) { } 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) { @@ -268,20 +269,10 @@ void play_sound_segment(int key_code, int is_pressed) { pthread_mutex_unlock(&thread_mutex); } -static char *xstrdup2(const char *s) { - if (!s) - return NULL; - size_t n = strlen(s) + 1; - char *p = (char *)malloc(n); - if (!p) - return NULL; - memcpy(p, s, n); - return p; -} int parse_keyboard_event(const char *json_line, int *key_code, int *is_pressed) { - char *line_copy = xstrdup2(json_line); + char *line_copy = xstrdup(json_line); if (!line_copy) return -1; char *newline = strchr(line_copy, '\n'); diff --git a/src/cli.c b/src/cli.c index 6eb6f6f..19c351b 100644 --- a/src/cli.c +++ b/src/cli.c @@ -1,52 +1,86 @@ #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 { + fprintf(stderr, "Error: Invalid %s device '%s'\n", action, optarg); + fprintf(stderr, "Use: 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("KeyVibe - Mechanical Keyboard Sound Simulator\n"); printf("Version: %s\n\n", PROJECT_VERSION); - + printf("USAGE:\n"); printf(" %s [OPTIONS]\n\n", program_name); - + printf("SOUND CONTROL:\n"); printf(" -S, --sound PACK Keyboard sound pack (default: eg-oreo)\n"); printf(" -M, --mouse PACK Mouse sound pack (default: ping)\n"); printf(" -l, --list List available sound packs\n\n"); - + printf("VOLUME CONTROL:\n"); - printf(" -V, --volume LEVEL Set both keyboard & mouse volume [0-100]\n"); + printf( + " -V, --volume LEVEL Set both keyboard & mouse volume [0-100]\n"); printf(" -K, --keyboard-volume Set keyboard volume [0-100]\n"); printf(" -O, --mouse-volume Set mouse volume [0-100]\n\n"); - + printf("MUTE CONTROL:\n"); printf(" -m, --mute[=DEVICE] Mute sounds (keyboard|mouse|both)\n"); printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both)\n\n"); - + printf("ENABLE/DISABLE:\n"); printf(" --enable[=DEVICE] Enable sounds (keyboard|mouse|both)\n"); printf(" --disable[=DEVICE] Disable sounds (keyboard|mouse|both)\n\n"); - + printf("DAEMON CONTROL:\n"); printf(" -d, --daemon Run in background\n"); printf(" -s, --stop Stop background daemon\n\n"); - + printf("OTHER:\n"); printf(" -v, --verbose Enable verbose output\n"); printf(" -h, --help Show this help\n\n"); - + printf("EXAMPLES:\n"); - printf(" %s --list # List sound packs\n", program_name); - printf(" %s -S cherrymx-blue -V 75 # Cherry MX Blue at 75%%\n", program_name); - printf(" %s --daemon # Run in background\n", program_name); - printf(" %s --mute keyboard # Mute only keyboard\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(" %s --list # List sound packs\n", + program_name); + printf(" %s -S cherrymx-blue -V 75 # Cherry MX Blue at " + "75%%\n", + program_name); + printf(" %s --daemon # Run in background\n", + program_name); + printf(" %s --mute keyboard # Mute only keyboard\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("CONFIG: Settings are saved to ~/.keyvibe.json and auto-reload in daemon mode.\n"); + printf("CONFIG: Settings are saved to ~/.keyvibe.json and auto-reload in " + "daemon mode.\n"); } int parse_cli(int argc, char **argv, CliOptions *out) { @@ -115,96 +149,41 @@ int parse_cli(int argc, char **argv, CliOptions *out) { out->mouse_sound = optarg; break; case 'V': - out->volume = atoi(optarg); - if (out->volume < 0) - out->volume = 0; - if (out->volume > 100) - out->volume = 100; + set_volume(&out->volume, atoi(optarg)); break; case 'K': - out->keyboard_volume = atoi(optarg); - if (out->keyboard_volume < 0) - out->keyboard_volume = 0; - if (out->keyboard_volume > 100) - out->keyboard_volume = 100; + set_volume(&out->keyboard_volume, atoi(optarg)); break; case 'O': - out->mouse_volume = atoi(optarg); - if (out->mouse_volume < 0) - out->mouse_volume = 0; - if (out->mouse_volume > 100) - out->mouse_volume = 100; + set_volume(&out->mouse_volume, atoi(optarg)); break; case 'm': - if (optarg == NULL || strcmp(optarg, "both") == 0) { - out->keyboard_mute = 1; - out->mouse_mute = 1; - } else if (strcmp(optarg, "keyboard") == 0) { - out->keyboard_mute = 1; - } else if (strcmp(optarg, "mouse") == 0) { - out->mouse_mute = 1; - } else { - fprintf(stderr, "Error: Invalid mute device '%s'\n", optarg); - fprintf(stderr, "Use: keyboard, mouse, or both\n"); + if (parse_device_argument(optarg, "mute", &out->keyboard_mute, &out->mouse_mute, 1) != 0) { return 1; } break; case 'u': - if (optarg == NULL || strcmp(optarg, "both") == 0) { - out->keyboard_mute = 0; - out->mouse_mute = 0; - } else if (strcmp(optarg, "keyboard") == 0) { - out->keyboard_mute = 0; - } else if (strcmp(optarg, "mouse") == 0) { - out->mouse_mute = 0; - } else { - fprintf(stderr, "Error: Invalid unmute device '%s'\n", optarg); - fprintf(stderr, "Use: keyboard, mouse, or both\n"); + if (parse_device_argument(optarg, "unmute", &out->keyboard_mute, &out->mouse_mute, 0) != 0) { return 1; } break; case 1000: // --enable - 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 (strcmp(optarg, "both") == 0) { - out->keyboard_enabled = 1; - out->mouse_enabled = 1; - } else if (strcmp(optarg, "keyboard") == 0) { - out->keyboard_enabled = 1; - } else if (strcmp(optarg, "mouse") == 0) { - out->mouse_enabled = 1; - } else { - fprintf(stderr, "Error: Invalid enable device '%s'\n", optarg); - fprintf(stderr, "Use: keyboard, mouse, or both\n"); - return 1; - } - break; case 1001: // --disable - if (optarg == NULL) { - // Handle space-separated argument: --disable mouse - if (optind < argc && argv[optind][0] != '-') { - optarg = argv[optind++]; - } else { - optarg = "both"; // default to both if no argument + { + 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; } - } - if (strcmp(optarg, "both") == 0) { - out->keyboard_enabled = 0; - out->mouse_enabled = 0; - } else if (strcmp(optarg, "keyboard") == 0) { - out->keyboard_enabled = 0; - } else if (strcmp(optarg, "mouse") == 0) { - out->mouse_enabled = 0; - } else { - fprintf(stderr, "Error: Invalid disable device '%s'\n", optarg); - fprintf(stderr, "Use: keyboard, mouse, or both\n"); - return 1; } break; case 'l': diff --git a/src/common/utils.c b/src/common/utils.c index 3b13fe1..a31d7dc 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -28,49 +28,32 @@ int read_runtime_mute_file() { return 0; } -void write_runtime_mute_file(int mute) { - char mute_file[1024]; +// 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(mute_file, sizeof(mute_file), "%s/keyvibe-mute-%d", rd, - (int)getuid())) + if (!safe_snprintf(state_file, sizeof(state_file), "%s/keyvibe-%s-%d", rd, + filename_suffix, (int)getuid())) return; - FILE *f = fopen(mute_file, "w"); + FILE *f = fopen(state_file, "w"); if (!f) return; - fprintf(f, "%d\n", mute ? 1 : 0); + 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) { - 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/keyvibe-kbd-mute-%d", rd, - (int)getuid())) - return; - FILE *f = fopen(mute_file, "w"); - if (!f) - return; - fprintf(f, "%d\n", mute ? 1 : 0); - fclose(f); + write_runtime_state_file("kbd-mute", mute ? 1 : 0); } void write_runtime_mouse_mute_file(int mute) { - 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/keyvibe-mouse-mute-%d", - rd, (int)getuid())) - return; - FILE *f = fopen(mute_file, "w"); - if (!f) - return; - fprintf(f, "%d\n", mute ? 1 : 0); - fclose(f); + write_runtime_state_file("mouse-mute", mute ? 1 : 0); } int safe_snprintf(char *dst, size_t dst_sz, const char *fmt, ...) { @@ -133,39 +116,35 @@ int process_is_running(pid_t pid) { } int write_runtime_keyboard_enabled_file(int enabled) { - char enabled_file[1024]; - const char *rd = getenv("XDG_RUNTIME_DIR"); - if (!rd || strlen(rd) == 0) { - rd = "/tmp"; - } - if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-kbd-enabled-%d", rd, - (int)getuid())) { - return 0; - } - FILE *f = fopen(enabled_file, "w"); - if (!f) { - return 0; - } - fprintf(f, "%d\n", enabled); - fclose(f); + write_runtime_state_file("kbd-enabled", enabled); return 1; } int write_runtime_mouse_enabled_file(int enabled) { - char enabled_file[1024]; - const char *rd = getenv("XDG_RUNTIME_DIR"); - if (!rd || strlen(rd) == 0) { - rd = "/tmp"; - } - if (!safe_snprintf(enabled_file, sizeof(enabled_file), "%s/keyvibe-mouse-enabled-%d", rd, - (int)getuid())) { - return 0; - } - FILE *f = fopen(enabled_file, "w"); - if (!f) { - return 0; - } - fprintf(f, "%d\n", enabled); - fclose(f); + write_runtime_state_file("mouse-enabled", enabled); return 1; } + + +void int_to_str(char *buffer, size_t size, int value) { + snprintf(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; + 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; +} diff --git a/src/config.c b/src/config.c index d9e6ee5..26c5a9e 100644 --- a/src/config.c +++ b/src/config.c @@ -8,16 +8,6 @@ #include #include -static char *xstrdup(const char *s) { - if (!s) - return NULL; - size_t n = strlen(s) + 1; - char *p = (char *)malloc(n); - if (!p) - return NULL; - memcpy(p, s, n); - return p; -} char *get_user_config_path(char *buffer, size_t buflen) { const char *home = getenv("HOME"); @@ -35,7 +25,8 @@ char *get_user_config_path(char *buffer, size_t buflen) { 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) { + int mouse_volume, int keyboard_enabled, + int mouse_enabled) { json_object *root = json_object_new_object(); if (!root) return 0; @@ -72,7 +63,8 @@ int write_user_config(const char *path, const char *keyboard_sound, 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) { + int *out_mouse_volume, int *out_keyboard_enabled, + int *out_mouse_enabled) { FILE *f = fopen(path, "r"); if (!f) return 0; diff --git a/src/main.c b/src/main.c index 4ed5101..fb2b8ce 100644 --- a/src/main.c +++ b/src/main.c @@ -7,8 +7,8 @@ #include "common/mute.h" #include "common/utils.h" #include "config.h" -#include "user_config.h" #include "soundpacks.h" +#include "user_config.h" #include #include #include @@ -77,7 +77,8 @@ int main(int argc, char *argv[]) { 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)) { + &cfg_keyboard_volume, &cfg_mouse_volume, + &cfg_keyboard_enabled, &cfg_mouse_enabled)) { if (cfg_keyboard_sound) { if (sound_name_owned) { free(sound_name); @@ -98,27 +99,26 @@ int main(int argc, char *argv[]) { } } } - - // Parse CLI arguments + int parse_result = parse_cli(argc, argv, &cli_opts); if (parse_result == 1) { - if (sound_name_owned) { - free(sound_name); - } + 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 (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; @@ -204,11 +204,11 @@ int main(int argc, char *argv[]) { write_runtime_keyboard_enabled_file(current_keyboard_enabled); write_runtime_mouse_enabled_file(current_mouse_enabled); - // Update config file if needed 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)) { + current_keyboard_volume, current_mouse_volume, + current_keyboard_enabled, current_mouse_enabled)) { fprintf(stderr, "Warning: Failed to update config file %s\n", user_cfg_path); } else if (verbose) { @@ -230,7 +230,8 @@ int main(int argc, char *argv[]) { 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)) { + current_keyboard_volume, current_mouse_volume, + current_keyboard_enabled, current_mouse_enabled)) { fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path); } else if (verbose) { fprintf(stderr, "Created default config %s\n", user_cfg_path); @@ -342,7 +343,8 @@ int main(int argc, char *argv[]) { } 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_mute, current_mouse_mute, + current_keyboard_enabled, current_mouse_enabled)) { if (sound_name_owned) { free(sound_name); } -- 2.43.0 From ce28dc2fde8bb6913d6495865d2df599e181d1b2 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 15 Oct 2025 17:04:01 +0545 Subject: [PATCH 09/18] oops many changes, DRY Principle applied other i dont know --- include/common/utils.h | 12 ++++++ src/app/watch.c | 4 +- src/audio/config.c | 37 ++++++++---------- src/audio/main.c | 28 +++++++------- src/cli.c | 10 ++--- src/common/utils.c | 81 ++++++++++++++++++++++++++++++++++++--- src/config.c | 9 +---- src/main.c | 86 ++++++++++++++++++++++++++++++------------ src/soundpacks.c | 30 ++++----------- 9 files changed, 195 insertions(+), 102 deletions(-) diff --git a/include/common/utils.h b/include/common/utils.h index a85677b..e8db7e5 100644 --- a/include/common/utils.h +++ b/include/common/utils.h @@ -2,6 +2,7 @@ #define KEYVIBE_UTILS_H #include +#include #include // Returns 1 on success, 0 on truncation/error @@ -32,5 +33,16 @@ int write_runtime_mouse_enabled_file(int enabled); 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 // KEYVIBE_UTILS_H diff --git a/src/app/watch.c b/src/app/watch.c index 9e5d0e9..b9e56d4 100644 --- a/src/app/watch.c +++ b/src/app/watch.c @@ -1,4 +1,5 @@ #include "app/watch.h" +#include "common/utils.h" #include "app/process.h" #include #include @@ -53,8 +54,7 @@ static void *inotify_thread_fn(void *arg) { int start_config_watcher(const char *path) { static pthread_t inotify_thread; static struct inotify_thread_args in_args; - strncpy(in_args.path, path, sizeof(in_args.path) - 1); - in_args.path[sizeof(in_args.path) - 1] = '\0'; + 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); diff --git a/src/audio/config.c b/src/audio/config.c index 2184704..810b6cc 100644 --- a/src/audio/config.c +++ b/src/audio/config.c @@ -24,8 +24,7 @@ static void get_full_path(char *buffer, size_t buffer_size, return; } if (filename[0] == '/') { - strncpy(buffer, filename, buffer_size - 1); - buffer[buffer_size - 1] = '\0'; + safe_strncpy(buffer, filename, buffer_size); } else { if (!safe_snprintf(buffer, buffer_size, "%s/%s", base_dir, filename)) { buffer[0] = '\0'; @@ -37,32 +36,31 @@ static void get_full_path(char *buffer, size_t buffer_size, 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); + safe_fprintf(stderr, "Error: Cannot open config file: %s\n", config_path); perror("fopen"); return -1; } char config_path_copy[1024]; - strncpy(config_path_copy, config_path, sizeof(config_path_copy) - 1); - config_path_copy[sizeof(config_path_copy) - 1] = '\0'; + 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); - fprintf(stderr, "Error: ftell failed while reading %s\n", config_path); + safe_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); + 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); - fprintf(stderr, "Error: Memory allocation failed\n"); + safe_fprintf(stderr, "Error: Memory allocation failed\n"); return -1; } fread(json_string, 1, size, file); @@ -71,7 +69,7 @@ int load_sound_config(const char *config_path) { json_object *root = json_tokener_parse(json_string); free(json_string); if (!root) { - fprintf(stderr, "Error: Invalid JSON in config file\n"); + safe_fprintf(stderr, "Error: Invalid JSON in config file\n"); return -1; } const char *key_type = "single"; @@ -94,19 +92,18 @@ int load_sound_config(const char *config_path) { 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'; + 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'; - memmove(brace_start + 2, brace_end + 1, + safe_memmove(brace_start + 2, brace_end + 1, strlen(brace_end + 1) + 1); } - snprintf(temp_filename, sizeof(temp_filename), temp_pattern, i); + safe_snprintf_wrapper(temp_filename, sizeof(temp_filename), temp_pattern, i); } else { - snprintf(temp_filename, sizeof(temp_filename), pattern, i); + 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, @@ -137,9 +134,8 @@ int load_sound_config(const char *config_path) { } 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'; + 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); @@ -160,7 +156,7 @@ int load_sound_config(const char *config_path) { char *dash = strstr(key, "-up"); if (dash) { char key_copy[16]; - strncpy(key_copy, key, dash - key); + safe_strncpy(key_copy, key, dash - key + 1); key_copy[dash - key] = '\0'; key_code = atoi(key_copy); is_release = 1; @@ -191,9 +187,8 @@ int load_sound_config(const char *config_path) { if (json_object_object_get_ex(root, "sound", &obj) || json_object_object_get_ex(root, "audio_file", &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'; + 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) diff --git a/src/audio/main.c b/src/audio/main.c index e84a7fb..9a0850a 100644 --- a/src/audio/main.c +++ b/src/audio/main.c @@ -31,7 +31,7 @@ static int read_runtime_state(const char *filename_suffix, int default_value) { return default_value; } int state = default_value; - if (fscanf(f, "%d", &state) == 1) { + if (safe_fscanf(f, "%d", &state) == 1) { fclose(f); return state; } @@ -61,21 +61,21 @@ static int read_mouse_enabled_state() { int main(int argc, char *argv[]) { if (argc < 2 || argc > 11) { - fprintf(stderr, + safe_fprintf(stderr, "Usage: %s [volume] [verbose] [mute] [mouse_config] " "[mouse_volume] [keyboard_mute] [mouse_mute] [keyboard_enabled] " "[mouse_enabled]\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 all sound (default: 0)\n"); - fprintf(stderr, " mouse_config: path to mouse config.json (optional)\n"); - fprintf(stderr, " mouse_volume: 0-100 for mouse volume (optional)\n"); - fprintf(stderr, " keyboard_mute: 1 to mute keyboard sounds (optional)\n"); - fprintf(stderr, " mouse_mute: 1 to mute mouse sounds (optional)\n"); - fprintf(stderr, + 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"); - fprintf(stderr, " mouse_enabled: 1 to enable mouse sounds (optional)\n"); + safe_fprintf(stderr, " mouse_enabled: 1 to enable mouse sounds (optional)\n"); return 1; } if (argc >= 3) { @@ -101,7 +101,7 @@ int main(int argc, char *argv[]) { } if (argc >= 6) { if (load_sound_config(argv[5]) != 0) { - fprintf(stderr, "Failed to load mouse sound configuration\n"); + safe_fprintf(stderr, "Failed to load mouse sound configuration\n"); return 1; } g_mouse_sound_pack = g_sound_pack; @@ -136,11 +136,11 @@ int main(int argc, char *argv[]) { printf("Mouse enabled: %s\n", g_mouse_enabled ? "yes" : "no"); } if (load_sound_config(argv[1]) != 0) { - fprintf(stderr, "Failed to load keyboard sound configuration\n"); + safe_fprintf(stderr, "Failed to load keyboard sound configuration\n"); return 1; } if (init_audio() != 0) { - fprintf(stderr, "Failed to initialize audio\n"); + safe_fprintf(stderr, "Failed to initialize audio\n"); return 1; } fd_set readfds; diff --git a/src/cli.c b/src/cli.c index 19c351b..91de5a3 100644 --- a/src/cli.c +++ b/src/cli.c @@ -17,8 +17,8 @@ static int parse_device_argument(const char *optarg, const char *action, } else if (strcmp(optarg, "mouse") == 0) { *mouse_value = value; } else { - fprintf(stderr, "Error: Invalid %s device '%s'\n", action, optarg); - fprintf(stderr, "Use: keyboard, mouse, or both\n"); + safe_fprintf(stderr, "Error: Invalid %s device '%s'\n", action, optarg); + safe_fprintf(stderr, "Use: keyboard, mouse, or both\n"); return 1; } return 0; @@ -84,7 +84,7 @@ void print_usage(const char *program_name) { } int parse_cli(int argc, char **argv, CliOptions *out) { - memset(out, 0, sizeof(*out)); + safe_memset(out, 0, sizeof(*out)); out->volume = -1; out->keyboard_volume = -1; out->mouse_volume = -1; @@ -125,8 +125,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { (n == 7 && strncmp(a, "disable", 7) == 0) || (n == 4 && strncmp(a, "help", 4) == 0) || (n == 7 && strncmp(a, "verbose", 7) == 0))) { - fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]); - fprintf(stderr, "Use '%s --help' for available options.\n", argv[0]); + safe_fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]); + safe_fprintf(stderr, "Use '%s --help' for available options.\n", argv[0]); return 1; } } diff --git a/src/common/utils.c b/src/common/utils.c index a31d7dc..026a11d 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -20,7 +20,7 @@ int read_runtime_mute_file() { if (!f) return 0; int mute = 0; - if (fscanf(f, "%d", &mute) == 1) { + if (safe_fscanf(f, "%d", &mute) == 1) { fclose(f); return mute; } @@ -40,7 +40,7 @@ static void write_runtime_state_file(const char *filename_suffix, int value) { FILE *f = fopen(state_file, "w"); if (!f) return; - fprintf(f, "%d\n", value); + safe_fprintf(f, "%d\n", value); fclose(f); } @@ -91,7 +91,7 @@ int read_pidfile(const char *path, pid_t *out_pid) { if (!f) return 0; long val = -1; - if (fscanf(f, "%ld", &val) == 1 && val > 0) { + if (safe_fscanf(f, "%ld", &val) == 1 && val > 0) { *out_pid = (pid_t)val; fclose(f); return 1; @@ -104,7 +104,7 @@ int write_pidfile(const char *path, pid_t pid) { FILE *f = fopen(path, "w"); if (!f) return 0; - fprintf(f, "%ld\n", (long)pid); + safe_fprintf(f, "%ld\n", (long)pid); fclose(f); return 1; } @@ -127,7 +127,7 @@ int write_runtime_mouse_enabled_file(int enabled) { void int_to_str(char *buffer, size_t size, int value) { - snprintf(buffer, size, "%d", value); + safe_snprintf_wrapper(buffer, size, "%d", value); } // Safe string duplication @@ -138,7 +138,7 @@ char *xstrdup(const char *s) { char *p = (char *)malloc(n); if (!p) return NULL; - memcpy(p, s, n); + safe_memcpy(p, s, n); return p; } @@ -148,3 +148,72 @@ int validate_volume(int volume) { 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 index 26c5a9e..6e68c17 100644 --- a/src/config.c +++ b/src/config.c @@ -10,12 +10,7 @@ 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; - } + const char *home = get_home_dir(); if (!home) return NULL; if (!safe_snprintf(buffer, buflen, "%s/%s", home, ".keyvibe.json")) @@ -55,7 +50,7 @@ int write_user_config(const char *path, const char *keyboard_sound, json_object_put(root); return 0; } - fprintf(f, "%s\n", json_str); + safe_fprintf(f, "%s\n", json_str); fclose(f); json_object_put(root); return 1; diff --git a/src/main.c b/src/main.c index fb2b8ce..89adfed 100644 --- a/src/main.c +++ b/src/main.c @@ -128,19 +128,42 @@ int main(int argc, char *argv[]) { if (sound_name_owned) { free(sound_name); } + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } return rc; } build_pidfile_path(pidfile_path, 1024); if (flag_stop) { pid_t running_pid = 0; - if (!require_running_pid(&running_pid)) + 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 (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("KeyVibe stopped.\n"); + if (sound_name_owned) { + free(sound_name); + } + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } return 0; } struct timespec ts; @@ -148,6 +171,12 @@ int main(int argc, char *argv[]) { 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("KeyVibe: process did not stop in time\n"); } current_mute = read_runtime_mute_file(); @@ -209,10 +238,10 @@ int main(int argc, char *argv[]) { if (!write_user_config(user_cfg_path, sound_name, mouse_sound_name, current_keyboard_volume, current_mouse_volume, current_keyboard_enabled, current_mouse_enabled)) { - fprintf(stderr, "Warning: Failed to update config file %s\n", + safe_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); + safe_fprintf(stderr, "Updated config file %s\n", user_cfg_path); } } @@ -232,9 +261,9 @@ int main(int argc, char *argv[]) { if (!write_user_config(user_cfg_path, sound_name, mouse_sound_name, current_keyboard_volume, current_mouse_volume, current_keyboard_enabled, current_mouse_enabled)) { - fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path); + safe_fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path); } else if (verbose) { - fprintf(stderr, "Created default config %s\n", user_cfg_path); + safe_fprintf(stderr, "Created default config %s\n", user_cfg_path); } } } @@ -258,20 +287,33 @@ int main(int argc, char *argv[]) { } 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), + safe_snprintf_wrapper(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", + safe_snprintf_wrapper(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) + 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", 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, + if (sound_name_owned) { + free(sound_name); + } + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } + safe_fprintf(stderr, "KeyVibe already running (pid %ld). Use --stop to stop it.\n", (long)existing); return 1; @@ -309,21 +351,12 @@ int main(int argc, char *argv[]) { current_keyboard_volume = volume; current_mouse_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_mouse_sound_name, mouse_sound_name, - sizeof(current_mouse_sound_name) - 1); - current_mouse_sound_name[sizeof(current_mouse_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'; - strncpy(current_mouse_config_path, mouse_config_path, - sizeof(current_mouse_config_path) - 1); - current_mouse_config_path[sizeof(current_mouse_config_path) - 1] = '\0'; - strncpy(current_mouse_sound_dir, mouse_sound_dir, - sizeof(current_mouse_sound_dir) - 1); - current_mouse_sound_dir[sizeof(current_mouse_sound_dir) - 1] = '\0'; + 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("KeyVibe starting...\n"); printf("Keyboard sound pack: %s\n", sound_name); @@ -348,6 +381,9 @@ int main(int argc, char *argv[]) { if (sound_name_owned) { free(sound_name); } + if (mouse_sound_name_owned) { + free(mouse_sound_name); + } return 1; } int status; diff --git a/src/soundpacks.c b/src/soundpacks.c index a321983..52184c6 100644 --- a/src/soundpacks.c +++ b/src/soundpacks.c @@ -18,26 +18,16 @@ static void get_user_keyboard_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; - } + const char *home = get_home_dir(); if (home) - snprintf(buffer, buflen, "%s%s", home, USER_KEYBOARD_AUDIO_SUBPATH); + 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 = getenv("HOME"); - if (!home || strlen(home) == 0) { - struct passwd *pw = getpwuid(getuid()); - if (pw && pw->pw_dir) - home = pw->pw_dir; - } + const char *home = get_home_dir(); if (home) - snprintf(buffer, buflen, "%s%s", home, USER_MOUSE_AUDIO_SUBPATH); + 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, @@ -49,8 +39,7 @@ int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir, 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'; + safe_strncpy(out_basedir, user_audio_dir, out_sz); return 1; } } @@ -60,8 +49,7 @@ int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir, return 0; } if (access(p2, R_OK) == 0) { - strncpy(out_basedir, KEYBOARD_AUDIO_DIR, out_sz - 1); - out_basedir[out_sz - 1] = '\0'; + safe_strncpy(out_basedir, KEYBOARD_AUDIO_DIR, out_sz); return 1; } return 0; @@ -76,8 +64,7 @@ int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir, 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'; + safe_strncpy(out_basedir, user_audio_dir, out_sz); return 1; } } @@ -87,8 +74,7 @@ int resolve_mouse_sound_base_dir(const char *sound_name, char *out_basedir, return 0; } if (access(p2, R_OK) == 0) { - strncpy(out_basedir, MOUSE_AUDIO_DIR, out_sz - 1); - out_basedir[out_sz - 1] = '\0'; + safe_strncpy(out_basedir, MOUSE_AUDIO_DIR, out_sz); return 1; } return 0; -- 2.43.0 From 20b81a69ddca753925ef31f6602636158ca3a980 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 15 Oct 2025 17:12:43 +0545 Subject: [PATCH 10/18] refactor(build): use consistent naming for KeyVibe target in Makefile --- Makefile | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index cb41f06..3692a42 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ + CC = gcc # Updated include paths for modular headers CFLAGS = -Wall -Wextra -std=c99 -Iinclude -Iinclude/app -Iinclude/audio -Iinclude/common -Iinclude/sound -Iinclude/config @@ -10,12 +11,12 @@ LDFLAGS_SOUND = -ljson-c -lpulse -lpulse-simple -lsndfile -lpthread LDFLAGS_KEYBOARD = $(shell pkg-config --libs libevdev libinput libudev) -lpthread # Targets -KeyVibe_TARGET = keyvibe +KEYVIBE_TARGET = keyvibe SOUND_TARGET = audio KEYBOARD_TARGET = input # Sources (reorganized) -KeyVibe_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 +KEYVIBE_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 @@ -24,11 +25,11 @@ BINDIR = $(PREFIX)/bin SHAREDIR = $(PREFIX)/share/keyvibe UDEV_RULE = /etc/udev/rules.d/99-keyvibe-allow-keyboard.rules -all: $(KeyVibe_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET) +all: $(KEYVIBE_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET) # Build main launcher (needs json-c) -$(KeyVibe_TARGET): $(KeyVibe_SOURCE) +$(KEYVIBE_TARGET): $(KEYVIBE_SOURCE) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ -ljson-c -lpthread $(SOUND_TARGET): $(SOUND_SOURCE) @@ -38,20 +39,20 @@ $(KEYBOARD_TARGET): $(KEYBOARD_SOURCE) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS_KEYBOARD) clean: - rm -f $(KeyVibe_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET) + rm -f $(KEYVIBE_TARGET) $(SOUND_TARGET) $(KEYBOARD_TARGET) test: all @echo "Testing sound packs:" - ./$(KeyVibe_TARGET) --list + ./$(KEYVIBE_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 " sudo ./$(KEYVIBE_TARGET) # Default sound" + @echo " sudo ./$(KEYVIBE_TARGET) -s cherrymx-blue-abs # Specific sound" + @echo " sudo ./$(KEYVIBE_TARGET) --help # Show help" install: @echo "Installing KeyVibe to $(DESTDIR)$(BINDIR) and $(DESTDIR)$(SHAREDIR)..." - install -Dm755 $(KeyVibe_TARGET) $(DESTDIR)$(BINDIR)/$(KeyVibe_TARGET) + install -Dm755 $(KEYVIBE_TARGET) $(DESTDIR)$(BINDIR)/$(KEYVIBE_TARGET) install -Dm755 $(SOUND_TARGET) $(DESTDIR)$(BINDIR)/keyvibe-audio install -Dm755 $(KEYBOARD_TARGET) $(DESTDIR)$(BINDIR)/keyvibe-input install -d $(DESTDIR)$(SHAREDIR) @@ -71,7 +72,7 @@ install: uninstall: @echo "Uninstalling KeyVibe from $(DESTDIR)$(BINDIR) and $(DESTDIR)$(SHAREDIR)..." - rm -f $(DESTDIR)$(BINDIR)/$(KeyVibe_TARGET) + rm -f $(DESTDIR)$(BINDIR)/$(KEYVIBE_TARGET) rm -f $(DESTDIR)$(BINDIR)/keyvibe-audio rm -f $(DESTDIR)$(BINDIR)/keyvibe-input rm -rf $(DESTDIR)$(SHAREDIR) -- 2.43.0 From 1e9f5c784b968c7287a165a9085a3cb6a8fa5b1c Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:17:52 +0545 Subject: [PATCH 11/18] rebranding --- Makefile | 41 ++++++++++++++++++++-------------------- include/app/cli.h | 6 +++--- include/app/process.h | 6 +++--- include/app/reload.h | 6 +++--- include/app/watch.h | 6 +++--- include/audio/playback.h | 6 +++--- include/audio/types.h | 6 +++--- include/common/mute.h | 6 +++--- include/common/utils.h | 6 +++--- include/config.h | 8 ++++---- include/soundpacks.h | 6 +++--- include/user_config.h | 10 +++++----- src/app/process.c | 18 +++++++++--------- src/audio/main.c | 2 +- src/cli.c | 4 ++-- src/common/utils.c | 6 +++--- src/config.c | 2 +- src/main.c | 28 +++++++++++++-------------- src/soundpacks.c | 6 +++--- 19 files changed, 89 insertions(+), 90 deletions(-) diff --git a/Makefile b/Makefile index 3692a42..c91aec2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,3 @@ - CC = gcc # Updated include paths for modular headers CFLAGS = -Wall -Wextra -std=c99 -Iinclude -Iinclude/app -Iinclude/audio -Iinclude/common -Iinclude/sound -Iinclude/config @@ -11,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 +VBX_TARGET = vbx SOUND_TARGET = audio KEYBOARD_TARGET = input # Sources (reorganized) -KEYVIBE_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 +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) @@ -39,22 +38,22 @@ $(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)/keyvibe-audio - install -Dm755 $(KEYBOARD_TARGET) $(DESTDIR)$(BINDIR)/keyvibe-input + @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 soundpacks $(DESTDIR)$(SHAREDIR)/ @echo "Installing udev rule for non-root keyboard access..." @@ -71,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)/keyvibe-audio - rm -f $(DESTDIR)$(BINDIR)/keyvibe-input + @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/include/app/cli.h b/include/app/cli.h index aec605a..69a1e91 100644 --- a/include/app/cli.h +++ b/include/app/cli.h @@ -1,5 +1,5 @@ -#ifndef KEYVIBE_CLI_H -#define KEYVIBE_CLI_H +#ifndef VBX_CLI_H +#define VBX_CLI_H typedef struct { char *sound; @@ -20,6 +20,6 @@ typedef struct { void print_usage(const char *program_name); int parse_cli(int argc, char **argv, CliOptions *out); -#endif // KEYVIBE_CLI_H +#endif // VBX_CLI_H diff --git a/include/app/process.h b/include/app/process.h index 4f35bc8..3696b2a 100644 --- a/include/app/process.h +++ b/include/app/process.h @@ -1,5 +1,5 @@ -#ifndef KEYVIBE_PROCESS_H -#define KEYVIBE_PROCESS_H +#ifndef VBX_PROCESS_H +#define VBX_PROCESS_H #include #include @@ -20,4 +20,4 @@ int start_children(const char *sound_dir, const char *config_path, int volume, int keyboard_mute, int mouse_mute, int keyboard_enabled, int mouse_enabled); void handle_sighup(int sig); -#endif // KEYVIBE_PROCESS_H +#endif // VBX_PROCESS_H diff --git a/include/app/reload.h b/include/app/reload.h index da71817..a213bcd 100644 --- a/include/app/reload.h +++ b/include/app/reload.h @@ -1,5 +1,5 @@ -#ifndef KEYVIBE_RELOAD_H -#define KEYVIBE_RELOAD_H +#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, @@ -10,6 +10,6 @@ int handle_reload(const char *user_cfg_path, int current_mouse_volume_arg, int current_keyboard_mute, int current_mouse_mute, int *current_keyboard_enabled, int *current_mouse_enabled); -#endif // KEYVIBE_RELOAD_H +#endif // VBX_RELOAD_H diff --git a/include/app/watch.h b/include/app/watch.h index 5ff54a5..df483c3 100644 --- a/include/app/watch.h +++ b/include/app/watch.h @@ -1,6 +1,6 @@ -#ifndef KEYVIBE_WATCH_H -#define KEYVIBE_WATCH_H +#ifndef VBX_WATCH_H +#define VBX_WATCH_H int start_config_watcher(const char *path); -#endif // KEYVIBE_WATCH_H +#endif // VBX_WATCH_H diff --git a/include/audio/playback.h b/include/audio/playback.h index a0cba5c..325a7a2 100644 --- a/include/audio/playback.h +++ b/include/audio/playback.h @@ -1,10 +1,10 @@ -#ifndef KEYVIBE_AUDIO_PLAYBACK_H -#define KEYVIBE_AUDIO_PLAYBACK_H +#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 // KEYVIBE_AUDIO_PLAYBACK_H +#endif // VBX_AUDIO_PLAYBACK_H diff --git a/include/audio/types.h b/include/audio/types.h index bb9a080..11f4861 100644 --- a/include/audio/types.h +++ b/include/audio/types.h @@ -1,5 +1,5 @@ -#ifndef KEYVIBE_AUDIO_TYPES_H -#define KEYVIBE_AUDIO_TYPES_H +#ifndef VBX_AUDIO_TYPES_H +#define VBX_AUDIO_TYPES_H #include @@ -42,4 +42,4 @@ extern int g_mouse_mute; extern int g_keyboard_enabled; extern int g_mouse_enabled; -#endif // KEYVIBE_AUDIO_TYPES_H +#endif // VBX_AUDIO_TYPES_H diff --git a/include/common/mute.h b/include/common/mute.h index d51cfc8..f1fae62 100644 --- a/include/common/mute.h +++ b/include/common/mute.h @@ -1,9 +1,9 @@ -#ifndef KEYVIBE_MUTE_H -#define KEYVIBE_MUTE_H +#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 // KEYVIBE_MUTE_H +#endif // VBX_MUTE_H diff --git a/include/common/utils.h b/include/common/utils.h index e8db7e5..eb808d7 100644 --- a/include/common/utils.h +++ b/include/common/utils.h @@ -1,5 +1,5 @@ -#ifndef KEYVIBE_UTILS_H -#define KEYVIBE_UTILS_H +#ifndef VBX_UTILS_H +#define VBX_UTILS_H #include #include @@ -45,4 +45,4 @@ 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 // KEYVIBE_UTILS_H +#endif // VBX_UTILS_H diff --git a/include/config.h b/include/config.h index 1347bde..68adb25 100644 --- a/include/config.h +++ b/include/config.h @@ -2,7 +2,7 @@ #define __CONFIG_H__ #define PROJECT_VERSION "1.1.0" -#define GETTEXT_PACKAGE "keyvibe" +#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 index b1115cf..3083b7d 100644 --- a/include/soundpacks.h +++ b/include/soundpacks.h @@ -1,5 +1,5 @@ -#ifndef KEYVIBE_SOUNDPACKS_H -#define KEYVIBE_SOUNDPACKS_H +#ifndef VBX_SOUNDPACKS_H +#define VBX_SOUNDPACKS_H #include int resolve_keyboard_sound_base_dir(const char *sound_name, char *out_basedir, @@ -16,4 +16,4 @@ int validate_keyboard_sound_pack(const char *sound_name); int validate_mouse_sound_pack(const char *sound_name); int list_sound_packs(void); -#endif // KEYVIBE_SOUNDPACKS_H +#endif // VBX_SOUNDPACKS_H diff --git a/include/user_config.h b/include/user_config.h index 5d2d719..f063805 100644 --- a/include/user_config.h +++ b/include/user_config.h @@ -1,18 +1,18 @@ -#ifndef KEYVIBE_CONFIG_IO_H -#define KEYVIBE_CONFIG_IO_H +#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 ~/.keyvibe.json in new format +// 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 ~/.keyvibe.json supporting new and legacy formats +// 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 // KEYVIBE_CONFIG_IO_H +#endif // VBX_CONFIG_IO_H diff --git a/src/app/process.c b/src/app/process.c index 351be06..ba46a8b 100644 --- a/src/app/process.c +++ b/src/app/process.c @@ -21,7 +21,7 @@ volatile sig_atomic_t reload_requested = 0; void cleanup_processes(int sig) { (void)sig; if (!is_daemon) { - printf("\nShutting down KeyVibe...\n"); + printf("\nShutting down VBX...\n"); } if (sound_pid > 0) { kill(sound_pid, SIGTERM); @@ -42,7 +42,7 @@ int require_running_pid(pid_t *out_pid) { pid_t running_pid = 0; if (!read_pidfile(pidfile_path, &running_pid) || !process_is_running(running_pid)) { - fprintf(stderr, "KeyVibe: not running.\n"); + fprintf(stderr, "VBX: not running.\n"); return 0; } *out_pid = running_pid; @@ -108,8 +108,8 @@ int start_children(const char *sound_dir, const char *config_path, int volume, return 0; } char sound_player_path[MAX_PATH_LENGTH]; - snprintf(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio", - KeyVibe_BIN_DIR); + 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); @@ -131,11 +131,11 @@ int start_children(const char *sound_dir, const char *config_path, int volume, 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, "keyvibe-audio", "config.json", volume_str, + 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 keyvibe-audio"); + perror("execl vbx-audio"); exit(1); } keyboard_pid = fork(); @@ -148,13 +148,13 @@ int start_children(const char *sound_dir, const char *config_path, int volume, } char get_key_presses_path[MAX_PATH_LENGTH]; snprintf(get_key_presses_path, sizeof(get_key_presses_path), - "%s/keyvibe-input", KeyVibe_BIN_DIR); + "%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, "keyvibe-input", (char *)NULL); - perror("execl keyvibe-input"); + execl(get_key_presses_path, "vbx-input", (char *)NULL); + perror("execl vbx-input"); exit(1); } close(pipefd[0]); diff --git a/src/audio/main.c b/src/audio/main.c index 9a0850a..d9f46b8 100644 --- a/src/audio/main.c +++ b/src/audio/main.c @@ -22,7 +22,7 @@ static int read_runtime_state(const char *filename_suffix, int default_value) { if (!rd || strlen(rd) == 0) { rd = "/tmp"; } - if (!safe_snprintf(state_file, sizeof(state_file), "%s/keyvibe-%s-%d", rd, + if (!safe_snprintf(state_file, sizeof(state_file), "%s/vbx-%s-%d", rd, filename_suffix, (int)getuid())) { return default_value; } diff --git a/src/cli.c b/src/cli.c index 91de5a3..c9a2470 100644 --- a/src/cli.c +++ b/src/cli.c @@ -30,7 +30,7 @@ static void set_volume(int *volume_ptr, int value) { } void print_usage(const char *program_name) { - printf("KeyVibe - Mechanical Keyboard Sound Simulator\n"); + printf("VBX - Mechanical Keyboard Sound Simulator\n"); printf("Version: %s\n\n", PROJECT_VERSION); printf("USAGE:\n"); @@ -79,7 +79,7 @@ void print_usage(const char *program_name) { printf(" %s --keyboard-volume 80 --mouse-volume 60 # Different volumes\n", program_name); printf("\n"); - printf("CONFIG: Settings are saved to ~/.keyvibe.json and auto-reload in " + printf("CONFIG: Settings are saved to ~/.vbx.json and auto-reload in " "daemon mode.\n"); } diff --git a/src/common/utils.c b/src/common/utils.c index 026a11d..a4d73d1 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -13,7 +13,7 @@ int read_runtime_mute_file() { 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, + if (!safe_snprintf(mute_file, sizeof(mute_file), "%s/vbx-mute-%d", rd, (int)getuid())) return 0; FILE *f = fopen(mute_file, "r"); @@ -34,7 +34,7 @@ static void write_runtime_state_file(const char *filename_suffix, int value) { const char *rd = getenv("XDG_RUNTIME_DIR"); if (!rd || strlen(rd) == 0) rd = "/tmp"; - if (!safe_snprintf(state_file, sizeof(state_file), "%s/keyvibe-%s-%d", rd, + if (!safe_snprintf(state_file, sizeof(state_file), "%s/vbx-%s-%d", rd, filename_suffix, (int)getuid())) return; FILE *f = fopen(state_file, "w"); @@ -83,7 +83,7 @@ const char *get_runtime_dir(void) { 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()); + safe_snprintf(buffer, buflen, "%s/vbx-%d.pid", rd, (int)getuid()); } int read_pidfile(const char *path, pid_t *out_pid) { diff --git a/src/config.c b/src/config.c index 6e68c17..c1ab28f 100644 --- a/src/config.c +++ b/src/config.c @@ -13,7 +13,7 @@ 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, ".keyvibe.json")) + if (!safe_snprintf(buffer, buflen, "%s/%s", home, ".vbx.json")) return NULL; return buffer; } diff --git a/src/main.c b/src/main.c index 89adfed..090cdc6 100644 --- a/src/main.c +++ b/src/main.c @@ -29,12 +29,12 @@ #include #define MAX_PATH_LENGTH 1024 -#define AUDIO_BASE_DIR KeyVibe_DATA_DIR "/soundpacks" +#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/keyvibe/soundpacks/keyboard" -#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/mouse" -#define USER_CONFIG_FILENAME ".keyvibe.json" +#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; @@ -157,7 +157,7 @@ int main(int argc, char *argv[]) { for (int i = 0; i < 30; i++) { if (!process_is_running(running_pid)) { unlink(pidfile_path); - printf("KeyVibe stopped.\n"); + printf("VBX stopped.\n"); if (sound_name_owned) { free(sound_name); } @@ -177,7 +177,7 @@ int main(int argc, char *argv[]) { if (mouse_sound_name_owned) { free(mouse_sound_name); } - return errorf("KeyVibe: process did not stop in time\n"); + return errorf("VBX: process did not stop in time\n"); } current_mute = read_runtime_mute_file(); @@ -288,9 +288,9 @@ int main(int argc, char *argv[]) { 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/keyvibe-input", KeyVibe_BIN_DIR); - safe_snprintf_wrapper(sound_player_path, sizeof(sound_player_path), "%s/keyvibe-audio", - KeyVibe_BIN_DIR); + "%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) { @@ -300,7 +300,7 @@ int main(int argc, char *argv[]) { free(mouse_sound_name); } return errorf("Error: Cannot find or execute required binaries in %s\n", - KeyVibe_BIN_DIR); + VBX_BIN_DIR); } signal(SIGINT, cleanup_processes); signal(SIGTERM, cleanup_processes); @@ -314,7 +314,7 @@ int main(int argc, char *argv[]) { free(mouse_sound_name); } safe_fprintf(stderr, - "KeyVibe already running (pid %ld). Use --stop to stop it.\n", + "VBX already running (pid %ld). Use --stop to stop it.\n", (long)existing); return 1; } @@ -358,7 +358,7 @@ int main(int argc, char *argv[]) { 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("KeyVibe starting...\n"); + printf("VBX starting...\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); @@ -368,7 +368,7 @@ int main(int argc, char *argv[]) { printf("Press Ctrl+C to exit.\n\n"); } else { if (!is_daemon) { - printf("KeyVibe started with keyboard sound pack: %s, mouse sound pack: " + printf("VBX started with keyboard sound pack: %s, mouse sound pack: " "%s\n", sound_name, mouse_sound_name); printf("Press Ctrl+C to exit.\n"); @@ -443,7 +443,7 @@ int main(int argc, char *argv[]) { } } if (!is_daemon) { - printf("KeyVibe exited.\n"); + printf("VBX exited.\n"); } if (sound_name_owned) { free(sound_name); diff --git a/src/soundpacks.c b/src/soundpacks.c index 52184c6..ceb3817 100644 --- a/src/soundpacks.c +++ b/src/soundpacks.c @@ -10,11 +10,11 @@ #include #include -#define AUDIO_BASE_DIR KeyVibe_DATA_DIR "/soundpacks" +#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/keyvibe/soundpacks/keyboard" -#define USER_MOUSE_AUDIO_SUBPATH "/.local/share/keyvibe/soundpacks/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'; -- 2.43.0 From a967ddb6ef84666e436bf34b4ed2053e7d7852c5 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:22:10 +0545 Subject: [PATCH 12/18] refactor: rename keyvibe to vbx in CI workflow for consistency --- .github/workflows/ci.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) 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 -- 2.43.0 From 52ae286c43dffa9c661d3f1a6a470da56fccc93f Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:32:04 +0545 Subject: [PATCH 13/18] refactor(cli): improve CLI UX with clearer messages and usage instructions --- src/app/process.c | 4 +-- src/audio/config.c | 5 ++-- src/audio/playback.c | 8 ++++-- src/cli.c | 68 ++++++++++++++++++++++---------------------- src/main.c | 50 +++++++++++++++++++++++++++----- src/soundpacks.c | 33 ++++++++++++--------- 6 files changed, 106 insertions(+), 62 deletions(-) diff --git a/src/app/process.c b/src/app/process.c index ba46a8b..125cbed 100644 --- a/src/app/process.c +++ b/src/app/process.c @@ -21,7 +21,7 @@ volatile sig_atomic_t reload_requested = 0; void cleanup_processes(int sig) { (void)sig; if (!is_daemon) { - printf("\nShutting down VBX...\n"); + printf("\nShutting down VBX daemon...\n"); } if (sound_pid > 0) { kill(sound_pid, SIGTERM); @@ -42,7 +42,7 @@ int require_running_pid(pid_t *out_pid) { pid_t running_pid = 0; if (!read_pidfile(pidfile_path, &running_pid) || !process_is_running(running_pid)) { - fprintf(stderr, "VBX: not running.\n"); + fprintf(stderr, "VBX daemon is not running.\n"); return 0; } *out_pid = running_pid; diff --git a/src/audio/config.c b/src/audio/config.c index 810b6cc..3d9758d 100644 --- a/src/audio/config.c +++ b/src/audio/config.c @@ -36,7 +36,7 @@ static void get_full_path(char *buffer, size_t buffer_size, int load_sound_config(const char *config_path) { FILE *file = fopen(config_path, "r"); if (!file) { - safe_fprintf(stderr, "Error: Cannot open config file: %s\n", config_path); + safe_fprintf(stderr, "Error: Cannot open sound pack config: %s\n", config_path); perror("fopen"); return -1; } @@ -69,7 +69,8 @@ int load_sound_config(const char *config_path) { json_object *root = json_tokener_parse(json_string); free(json_string); if (!root) { - safe_fprintf(stderr, "Error: Invalid JSON in config file\n"); + 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"; diff --git a/src/audio/playback.c b/src/audio/playback.c index 31a00b9..e6b7578 100644 --- a/src/audio/playback.c +++ b/src/audio/playback.c @@ -19,7 +19,8 @@ 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"); + 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) { @@ -91,8 +92,9 @@ void *play_sound_thread(void *arg) { 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)); + 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, diff --git a/src/cli.c b/src/cli.c index c9a2470..c5bd349 100644 --- a/src/cli.c +++ b/src/cli.c @@ -17,8 +17,8 @@ static int parse_device_argument(const char *optarg, const char *action, } else if (strcmp(optarg, "mouse") == 0) { *mouse_value = value; } else { - safe_fprintf(stderr, "Error: Invalid %s device '%s'\n", action, optarg); - safe_fprintf(stderr, "Use: keyboard, mouse, or both\n"); + 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; @@ -33,54 +33,54 @@ 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 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 CONTROL:\n"); - printf(" -S, --sound PACK Keyboard sound pack (default: eg-oreo)\n"); - printf(" -M, --mouse PACK Mouse sound pack (default: ping)\n"); - printf(" -l, --list List available sound packs\n\n"); + 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 both keyboard & mouse volume [0-100]\n"); - printf(" -K, --keyboard-volume Set keyboard volume [0-100]\n"); - printf(" -O, --mouse-volume Set mouse volume [0-100]\n\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("MUTE CONTROL:\n"); - printf(" -m, --mute[=DEVICE] Mute sounds (keyboard|mouse|both)\n"); - printf(" -u, --unmute[=DEVICE] Unmute sounds (keyboard|mouse|both)\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("ENABLE/DISABLE:\n"); - printf(" --enable[=DEVICE] Enable sounds (keyboard|mouse|both)\n"); - printf(" --disable[=DEVICE] Disable sounds (keyboard|mouse|both)\n\n"); - - printf("DAEMON CONTROL:\n"); - printf(" -d, --daemon Run in background\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:\n"); - printf(" -v, --verbose Enable verbose output\n"); - printf(" -h, --help Show this help\n\n"); + printf("OTHER OPTIONS:\n"); + printf(" -v, --verbose Show detailed output\n"); + printf(" -h, --help Show this help message\n\n"); - printf("EXAMPLES:\n"); - printf(" %s --list # List sound packs\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", + printf(" %s -S cherrymx-blue -V 75 # Cherry MX Blue at 75%%\n", program_name); - printf(" %s --daemon # Run in background\n", + printf(" %s --daemon # Start background service\n", program_name); - printf(" %s --mute keyboard # Mute only keyboard\n", + printf(" %s --mute keyboard # Mute keyboard only\n", + program_name); + printf(" %s --disable mouse # Disable mouse sounds\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("CONFIG: Settings are saved to ~/.vbx.json and auto-reload in " - "daemon mode.\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) { @@ -126,7 +126,7 @@ int parse_cli(int argc, char **argv, CliOptions *out) { (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, "Use '%s --help' for available options.\n", argv[0]); + safe_fprintf(stderr, "Run '%s --help' to see available options.\n", argv[0]); return 1; } } diff --git a/src/main.c b/src/main.c index 090cdc6..949264c 100644 --- a/src/main.c +++ b/src/main.c @@ -70,6 +70,9 @@ int main(int argc, char *argv[]) { 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; @@ -133,6 +136,14 @@ int main(int argc, char *argv[]) { } 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; @@ -157,7 +168,7 @@ int main(int argc, char *argv[]) { for (int i = 0; i < 30; i++) { if (!process_is_running(running_pid)) { unlink(pidfile_path); - printf("VBX stopped.\n"); + printf("VBX daemon stopped successfully.\n"); if (sound_name_owned) { free(sound_name); } @@ -177,7 +188,7 @@ int main(int argc, char *argv[]) { if (mouse_sound_name_owned) { free(mouse_sound_name); } - return errorf("VBX: process did not stop in time\n"); + return errorf("VBX: daemon did not stop in time. Try running 'vbx --stop' again.\n"); } current_mute = read_runtime_mute_file(); @@ -213,18 +224,38 @@ int main(int argc, char *argv[]) { 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); @@ -240,6 +271,7 @@ int main(int argc, char *argv[]) { 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); } @@ -263,7 +295,7 @@ int main(int argc, char *argv[]) { current_keyboard_enabled, current_mouse_enabled)) { safe_fprintf(stderr, "Warning: Failed to write %s\n", user_cfg_path); } else if (verbose) { - safe_fprintf(stderr, "Created default config %s\n", user_cfg_path); + printf("Created default configuration: %s\n", user_cfg_path); } } } @@ -314,7 +346,7 @@ int main(int argc, char *argv[]) { free(mouse_sound_name); } safe_fprintf(stderr, - "VBX already running (pid %ld). Use --stop to stop it.\n", + "VBX daemon is already running (PID: %ld). Use 'vbx --stop' to stop it.\n", (long)existing); return 1; } @@ -358,7 +390,7 @@ int main(int argc, char *argv[]) { 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("VBX starting...\n"); + 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); @@ -368,7 +400,11 @@ int main(int argc, char *argv[]) { printf("Press Ctrl+C to exit.\n\n"); } else { if (!is_daemon) { - printf("VBX started with keyboard sound pack: %s, mouse sound pack: " + 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"); "%s\n", sound_name, mouse_sound_name); printf("Press Ctrl+C to exit.\n"); @@ -443,7 +479,7 @@ int main(int argc, char *argv[]) { } } if (!is_daemon) { - printf("VBX exited.\n"); + printf("VBX daemon exited.\n"); } if (sound_name_owned) { free(sound_name); diff --git a/src/soundpacks.c b/src/soundpacks.c index ceb3817..fab5fde 100644 --- a/src/soundpacks.c +++ b/src/soundpacks.c @@ -141,10 +141,12 @@ int validate_keyboard_sound_pack(const char *sound_name) { 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: Config file not found: %s\n", config_path); + 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; @@ -164,10 +166,12 @@ int validate_mouse_sound_pack(const char *sound_name) { 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: Config file not found: %s\n", config_path); + 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; @@ -196,10 +200,10 @@ int list_sound_packs(void) { printf("Available Sound Packs\n"); printf("=====================\n\n"); - printf("KEYBOARD SOUNDS:\n"); - printf("----------------\n"); - printf("%-25s %-12s\n", "Name", "Source"); - printf("%-25s %-12s\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) @@ -211,7 +215,7 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf("%-25s %-12s\n", entry->d_name, "user"); + printf("%-30s %-15s\n", entry->d_name, "user"); } } closedir(dir); @@ -228,15 +232,15 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf("%-25s %-12s\n", entry->d_name, "pre-installed"); + printf("%-30s %-15s\n", entry->d_name, "system"); } } closedir(dir); } - printf("\nMOUSE SOUNDS:\n"); - printf("-------------\n"); - printf("%-25s %-12s\n", "Name", "Source"); - printf("%-25s %-12s\n", "----", "------"); + 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) @@ -248,7 +252,7 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf("%-25s %-12s\n", entry->d_name, "user"); + printf("%-30s %-15s\n", entry->d_name, "user"); } } closedir(dir); @@ -265,10 +269,11 @@ int list_sound_packs(void) { continue; } if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - printf("%-25s %-12s\n", entry->d_name, "pre-installed"); + printf("%-30s %-15s\n", entry->d_name, "system"); } } closedir(dir); } + printf("\nUsage: vbx -S for keyboard or vbx -M for mouse\n"); return 0; } -- 2.43.0 From d388b0aec0e643436649ea95e006753703b70788 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:35:46 +0545 Subject: [PATCH 14/18] style(main): apply minor code formatting and readability improvements --- src/main.c | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/main.c b/src/main.c index 949264c..67977f4 100644 --- a/src/main.c +++ b/src/main.c @@ -188,7 +188,8 @@ int main(int argc, char *argv[]) { if (mouse_sound_name_owned) { free(mouse_sound_name); } - return errorf("VBX: daemon did not stop in time. Try running 'vbx --stop' again.\n"); + return errorf( + "VBX: daemon did not stop in time. Try running 'vbx --stop' again.\n"); } current_mute = read_runtime_mute_file(); @@ -270,7 +271,7 @@ int main(int argc, char *argv[]) { 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); + 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); @@ -320,9 +321,9 @@ int main(int argc, char *argv[]) { 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); + "%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) { @@ -346,8 +347,9 @@ int main(int argc, char *argv[]) { free(mouse_sound_name); } safe_fprintf(stderr, - "VBX daemon is already running (PID: %ld). Use 'vbx --stop' to stop it.\n", - (long)existing); + "VBX daemon is already running (PID: %ld). Use 'vbx --stop' " + "to stop it.\n", + (long)existing); return 1; } daemonize_self(); @@ -384,11 +386,14 @@ int main(int argc, char *argv[]) { 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_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)); + 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); @@ -401,12 +406,12 @@ int main(int argc, char *argv[]) { } 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(" 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"); - "%s\n", - sound_name, mouse_sound_name); printf("Press Ctrl+C to exit.\n"); } } -- 2.43.0 From 652d14c9235de0827cb176e0c00f14d492c31bcd Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:41:46 +0545 Subject: [PATCH 15/18] refactor(cli): rename KeyVibe to VBX and add mouse support --- README.md | 20 +++++++-------- src/cli.c | 75 ++++++++++++++++++++++++++++++++----------------------- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 37a6636..31ede66 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,20 @@ > A CLI tool that brings realistic mechanical keyboard sounds to every keystroke. -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. +vbx 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.

-[![Contributors](https://img.shields.io/github/contributors/S4NKALP/KeyVibe?style=for-the-badge&color=6366f1)](https://github.com/S4NKALP/KeyVibe/graphs/contributors) -[![Stars](https://img.shields.io/github/stars/S4NKALP/KeyVibe?style=for-the-badge&color=10b981)](https://github.com/S4NKALP/KeyVibe/stargazers) -[![Forks](https://img.shields.io/github/forks/S4NKALP/KeyVibe?style=for-the-badge&color=06b6d4)](https://github.com/S4NKALP/KeyVibe/network/members) -[![License](https://img.shields.io/github/license/S4NKALP/KeyVibe?style=for-the-badge&color=f59e0b)](LICENSE) -[![Issues](https://img.shields.io/github/issues/S4NKALP/KeyVibe?style=for-the-badge&color=ef4444)](https://github.com/S4NKALP/KeyVibe/issues) -[![Last Commit](https://img.shields.io/github/last-commit/S4NKALP/KeyVibe?style=for-the-badge&color=8b5cf6)](https://github.com/S4NKALP/KeyVibe/pulse) +[![Contributors](https://img.shields.io/github/contributors/S4NKALP/vbx?style=for-the-badge&color=6366f1)](https://github.com/S4NKALP/vbx/graphs/contributors) +[![Stars](https://img.shields.io/github/stars/S4NKALP/vbx?style=for-the-badge&color=10b981)](https://github.com/S4NKALP/vbx/stargazers) +[![Forks](https://img.shields.io/github/forks/S4NKALP/vbx?style=for-the-badge&color=06b6d4)](https://github.com/S4NKALP/vbx/network/members) +[![License](https://img.shields.io/github/license/S4NKALP/vbx?style=for-the-badge&color=f59e0b)](LICENSE) +[![Issues](https://img.shields.io/github/issues/S4NKALP/vbx?style=for-the-badge&color=ef4444)](https://github.com/S4NKALP/vbx/issues) +[![Last Commit](https://img.shields.io/github/last-commit/S4NKALP/vbx?style=for-the-badge&color=8b5cf6)](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)
@@ -52,8 +52,8 @@ paru -S keyvibe ### 🛠️ 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 ``` diff --git a/src/cli.c b/src/cli.c index c5bd349..61c26de 100644 --- a/src/cli.c +++ b/src/cli.c @@ -7,8 +7,9 @@ #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) { +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; @@ -34,8 +35,10 @@ void print_usage(const char *program_name) { printf("Version: %s\n\n", PROJECT_VERSION); printf("DESCRIPTION:\n"); - printf(" VBX brings realistic mechanical keyboard sounds to every keystroke.\n"); - printf(" Run interactively or as a background daemon with live config reload.\n\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); @@ -53,8 +56,10 @@ void print_usage(const char *program_name) { 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(" --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"); @@ -65,21 +70,26 @@ void print_usage(const char *program_name) { printf(" -h, --help Show this help message\n\n"); printf("QUICK EXAMPLES:\n"); - printf(" %s --list # Browse sound packs\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 -S cherrymx-blue -V 75 # Cherry MX Blue at 75%%\n", - program_name); - printf(" %s --daemon # Start background service\n", + 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 --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( + " 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"); } @@ -126,7 +136,8 @@ int parse_cli(int argc, char **argv, CliOptions *out) { (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]); + safe_fprintf(stderr, "Run '%s --help' to see available options.\n", + argv[0]); return 1; } } @@ -158,34 +169,36 @@ int parse_cli(int argc, char **argv, CliOptions *out) { set_volume(&out->mouse_volume, atoi(optarg)); break; case 'm': - if (parse_device_argument(optarg, "mute", &out->keyboard_mute, &out->mouse_mute, 1) != 0) { + 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) { + 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; + { + 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 } } - break; + 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; -- 2.43.0 From 0638bba56f6282e7c97bf42551d309dece113235 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:52:07 +0545 Subject: [PATCH 16/18] docs: update README to reflect mouse support and config changes --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 31ede66..d0ad07e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@
- Project Logo -> A CLI tool that brings realistic mechanical keyboard sounds to every keystroke. +# vbx -vbx 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.
-
[![Contributors](https://img.shields.io/github/contributors/S4NKALP/vbx?style=for-the-badge&color=6366f1)](https://github.com/S4NKALP/vbx/graphs/contributors) @@ -24,11 +24,12 @@ vbx listens to your keyboard events and plays high‑quality switch sounds with ## ✨ 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,7 +47,7 @@ vbx listens to your keyboard events and plays high‑quality switch sounds with ### Arch Linux ```bash -paru -S keyvibe +paru -S vbx-git ``` ### 🛠️ Build From Source @@ -60,7 +61,7 @@ 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: -- 2.43.0 From 498ba0b5bb6123167f617dc028d7a660f3f43af9 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:53:10 +0545 Subject: [PATCH 17/18] docs: update contributing guidelines in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0ad07e..85d9ad7 100644 --- a/README.md +++ b/README.md @@ -93,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 -- 2.43.0 From a18899003aa948748e59f1c1c43e37224ddc0185 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 18 Oct 2025 14:54:15 +0545 Subject: [PATCH 18/18] docs: add mechavibes-dx to credits in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 85d9ad7..9172a65 100644 --- a/README.md +++ b/README.md @@ -102,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 -- 2.43.0