├── demo.gif
├── .gitignore
├── default_xhisperrc
├── Makefile
├── LICENSE
├── README.md
├── xhisper.sh
├── test.c
└── xhispertool.c
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imaginalnika/xhisper/HEAD/demo.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.un
3 | *~
4 | *.o
5 | CLAUDE.md
6 | context.txt
7 | archive/
8 | xhispertool
9 | xhispertoold
10 | test
11 |
--------------------------------------------------------------------------------
/default_xhisperrc:
--------------------------------------------------------------------------------
1 | # xhisper configuration
2 | # Copy to ~/.config/xhisper/xhisperrc and customize
3 | # When in doubt, check by running 'xhisper --log'
4 |
5 | # Transcription Settings:
6 | long-recording-threshold : 1000
7 | transcription-prompt : ""
8 |
9 | # Paste Timing (seconds):
10 | non-ascii-initial-delay : 0.15 # Increase this if first character comes out wrong.
11 | non-ascii-default-delay : 0.025
12 |
13 | # Silence Detection:
14 | silence-threshold : -50
15 | silence-percentage : 95
16 |
17 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for xhisper
2 |
3 | CC = gcc
4 | CFLAGS = -O2 -Wall -Wextra
5 | PREFIX = /usr/local
6 | BINDIR = $(PREFIX)/bin
7 |
8 | all: xhispertool test
9 |
10 | xhispertool: xhispertool.c
11 | $(CC) $(CFLAGS) xhispertool.c -o xhispertool
12 | ln -sf xhispertool xhispertoold
13 |
14 | test: test.c
15 | $(CC) $(CFLAGS) test.c -o test
16 |
17 | install: xhispertool xhisper.sh
18 | install -d $(DESTDIR)$(BINDIR)
19 | install -m 755 xhispertool $(DESTDIR)$(BINDIR)/xhispertool
20 | ln -sf xhispertool $(DESTDIR)$(BINDIR)/xhispertoold
21 | install -m 755 xhisper.sh $(DESTDIR)$(BINDIR)/xhisper
22 |
23 | uninstall:
24 | rm -f $(DESTDIR)$(BINDIR)/xhisper
25 | rm -f $(DESTDIR)$(BINDIR)/xhispertool
26 | rm -f $(DESTDIR)$(BINDIR)/xhispertoold
27 |
28 | clean:
29 | rm -f xhispertool xhispertoold test
30 |
31 | .PHONY: all install uninstall clean
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 xhisper contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
xhisper /ˈzɪspər/
3 |

4 |
5 |
6 |
7 | Dictation at cursor for Linux.
8 |
9 | ## Installation
10 |
11 | ### Dependencies
12 |
13 |
14 | Arch Linux / Manjaro
15 | sudo pacman -S pipewire jq curl ffmpeg gcc
16 |
17 |
18 |
19 | Debian / Ubuntu / Linux Mint
20 | sudo apt update
21 | sudo apt install pipewire jq curl ffmpeg gcc
22 |
23 |
24 |
25 | Fedora / RHEL / AlmaLinux / Rocky
26 | sudo dnf install -y pipewire pipewire-utils jq curl ffmpeg gcc
27 |
28 |
29 |
30 | OpenSUSE (Leap / Tumbleweed)
31 | sudo zypper refresh
32 | sudo zypper install pipewire jq curl ffmpeg gcc
33 |
34 |
35 |
36 | Void Linux
37 | sudo xbps-install -S
38 | sudo xbps-install pipewire jq curl ffmpeg gcc
39 |
40 |
41 | **Note:** `wl-clipboard` (Wayland) or `xclip` (X11) required for non-ASCII but usually pre-installed.
42 |
43 | ### Setup
44 |
45 | 1. **Add user to input group** to access `/dev/uinput`:
46 | ```sh
47 | sudo usermod -aG input $USER
48 | ```
49 | Then **log out and log back in** (restart is safer) for the group change to take effect.
50 |
51 | Check by running:
52 |
53 | ```sh
54 | groups
55 | ```
56 |
57 | You should see `input` in the output.
58 |
59 | 2. **Get a Groq API key** from [console.groq.com](https://console.groq.com) (free tier available) and add to `~/.env`:
60 | ```sh
61 | GROQ_API_KEY=
62 | ```
63 |
64 | 3. Clone the repository and install:
65 | ```sh
66 | git clone --depth 1 https://github.com/imaginalnika/xhisper.git
67 | cd xhisper && make
68 | sudo make install
69 | ```
70 |
71 | 4. Bind `xhisper` binary to your favorite key:
72 |
73 |
74 | keyd
75 |
76 | ```ini
77 | [main]
78 | capslock = layer(dictate)
79 |
80 | [dictate:C]
81 | d = macro(xhisper)
82 | ```
83 |
84 |
85 |
86 | sxhkd
87 |
88 | ```
89 | super + d
90 | xhisper
91 | ```
92 |
93 |
94 |
95 | i3 / sway
96 |
97 | ```
98 | bindsym $mod+d exec xhisper
99 | ```
100 |
101 |
102 |
103 | Hyprland
104 |
105 | ```
106 | bind = $mainMod, D, exec, xhisper
107 | ```
108 |
109 |
110 | ---
111 |
112 | ## Usage
113 |
114 | Simply run `xhisper` twice (via your favorite keybinding):
115 | - **First run**: Starts recording
116 | - **Second run**: Stops and transcribes
117 |
118 | The transcription will be typed at your cursor position.
119 |
120 | **View logs:**
121 | ```sh
122 | xhisper --log
123 | ```
124 |
125 | **Non-QWERTY layouts:**
126 |
127 | For non-QWERTY layouts (e.g. Dvorak, International), set up an input switch key to QWERTY (e.g. rightalt). Then instead of binding to `xhisper`, bind to:
128 | ```sh
129 | xhisper --
130 | ```
131 |
132 | **Available input switch keys:** `--leftalt`, `--rightalt`, `--leftctrl`, `--rightctrl`, `--leftshift`, `--rightshift`, `--super`
133 |
134 | Key chords (like ctrl-space) not available yet.
135 |
136 | ---
137 |
138 | ## Configuration
139 |
140 | Configuration is read from `~/.config/xhisper/xhisperrc`:
141 |
142 | ```sh
143 | mkdir -p ~/.config/xhisper
144 | cp default_xhisperrc ~/.config/xhisper/xhisperrc
145 | ```
146 |
147 | ## Troubleshooting
148 |
149 | **Terminal Applications**: Clipboard paste uses Ctrl+V, which doesn't work in terminal emulators (they require Ctrl+Shift+V). Temporary workaround is to remap Ctrl+V to paste in your terminal emulator's settings. Note that *this limitation only affects international/Unicode characters*. ASCII characters (a-z, A-Z, 0-9, punctuation) are typed directly and doesn't care whether terminal or not.
150 |
151 | **Non-ASCII Transcription**: Increase non-ascii-*-delay to give the transcription longer timing buffer.
152 |
153 | ---
154 |
155 |
156 | Low complexity dictation for Linux
157 |
158 |
--------------------------------------------------------------------------------
/xhisper.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # xhisper v1.0
4 | # Dictate anywhere in Linux. Transcription at your cursor.
5 | # - Transcription via Groq Whisper
6 |
7 | # Configuration (see default_xhisperrc or ~/.config/xhisper/xhisperrc):
8 | # - long-recording-threshold : threshold for using large vs turbo model (seconds)
9 | # - transcription-prompt : context words for better Whisper accuracy
10 | # - silence-threshold : max volume in dB to consider silent (e.g., -50)
11 | # - silence-percentage : percentage of recording that must be silent (e.g., 95)
12 | # - non-ascii-initial-delay : sleep after first non-ASCII paste (seconds)
13 | # - non-ascii-default-delay : sleep after subsequent non-ASCII pastes (seconds)
14 |
15 | # Requirements:
16 | # - pipewire, pipewire-utils (audio)
17 | # - wl-clipboard (Wayland) or xclip (X11) for clipboard
18 | # - jq, curl, ffmpeg (processing)
19 | # - make to build, sudo make install to install
20 |
21 | [ -f "$HOME/.env" ] && source "$HOME/.env"
22 |
23 | # Parse command-line arguments
24 | LOCAL_MODE=0
25 | WRAP_KEY=""
26 | for arg in "$@"; do
27 | case "$arg" in
28 | --local)
29 | LOCAL_MODE=1
30 | ;;
31 | --log)
32 | if [ -f "/tmp/xhisper.log" ]; then
33 | cat /tmp/xhisper.log
34 | else
35 | echo "No log file found at /tmp/xhisper.log" >&2
36 | fi
37 | exit 0
38 | ;;
39 | --leftalt|--rightalt|--leftctrl|--rightctrl|--leftshift|--rightshift|--super)
40 | if [ -n "$WRAP_KEY" ]; then
41 | echo "Error: Multiple wrap keys not yet supported" >&2
42 | exit 1
43 | fi
44 | WRAP_KEY="${arg#--}"
45 | ;;
46 | *)
47 | echo "Error: Unknown option '$arg'" >&2
48 | echo "Usage: xhisper [--local] [--log] [--leftalt|--rightalt|--leftctrl|--rightctrl|--leftshift|--rightshift|--super]" >&2
49 | exit 1
50 | ;;
51 | esac
52 | done
53 |
54 | # Set binary paths based on local mode
55 | if [ "$LOCAL_MODE" -eq 1 ]; then
56 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
57 | XHISPERTOOL="$SCRIPT_DIR/xhispertool"
58 | XHISPERTOOLD="$SCRIPT_DIR/xhispertoold"
59 | else
60 | XHISPERTOOL="xhispertool"
61 | XHISPERTOOLD="xhispertoold"
62 | fi
63 |
64 | RECORDING="/tmp/xhisper.wav"
65 | LOGFILE="/tmp/xhisper.log"
66 | PROCESS_PATTERN="pw-record.*$RECORDING"
67 |
68 | # Default configuration
69 | long_recording_threshold=1000
70 | transcription_prompt=""
71 | silence_threshold=-50
72 | silence_percentage=95
73 | non_ascii_initial_delay=0.1
74 | non_ascii_default_delay=0.025
75 |
76 | CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/xhisper/xhisperrc"
77 |
78 | if [ -f "$CONFIG_FILE" ]; then
79 | while IFS=: read -r key value || [ -n "$key" ]; do
80 | # Skip comments and empty lines
81 | [[ "$key" =~ ^[[:space:]]*# ]] && continue
82 | [[ -z "$key" ]] && continue
83 |
84 | # Trim whitespace and quotes
85 | key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
86 | value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/^"//;s/"$//')
87 |
88 | case "$key" in
89 | long-recording-threshold) long_recording_threshold="$value" ;;
90 | transcription-prompt) transcription_prompt="$value" ;;
91 | silence-threshold) silence_threshold="$value" ;;
92 | silence-percentage) silence_percentage="$value" ;;
93 | non-ascii-initial-delay) non_ascii_initial_delay="$value" ;;
94 | non-ascii-default-delay) non_ascii_default_delay="$value" ;;
95 | esac
96 | done < "$CONFIG_FILE"
97 | fi
98 |
99 | # Auto-start daemon if not running
100 | if ! pgrep -x xhispertoold > /dev/null; then
101 | "$XHISPERTOOLD" 2>> /tmp/xhispertoold.log &
102 | sleep 1 # Give daemon time to start
103 |
104 | # Verify daemon started successfully
105 | if ! pgrep -x xhispertoold > /dev/null; then
106 | echo "Error: Failed to start xhispertoold daemon" >&2
107 | echo "Check /tmp/xhispertoold.log for details" >&2
108 | exit 1
109 | fi
110 | fi
111 |
112 | # Check if xhispertool is available
113 | if ! command -v "$XHISPERTOOL" &> /dev/null; then
114 | echo "Error: xhispertool not found" >&2
115 | echo "Please either:" >&2
116 | echo " - Run 'sudo make install' to install system-wide" >&2
117 | echo " - Run 'xhisper --local' from the build directory" >&2
118 | exit 1
119 | fi
120 |
121 | # Detect clipboard tool
122 | if command -v wl-copy &> /dev/null; then
123 | CLIP_COPY="wl-copy"
124 | CLIP_PASTE="wl-paste"
125 | elif command -v xclip &> /dev/null; then
126 | CLIP_COPY() { xclip -selection clipboard; }
127 | CLIP_PASTE() { xclip -o -selection clipboard; }
128 | else
129 | echo "Error: No clipboard tool found. Install wl-clipboard or xclip." >&2
130 | exit 1
131 | fi
132 |
133 | press_wrap_key() {
134 | if [ -n "$WRAP_KEY" ]; then
135 | "$XHISPERTOOL" "$WRAP_KEY"
136 | fi
137 | }
138 |
139 | paste() {
140 | local text="$1"
141 | press_wrap_key
142 | # Type character by character
143 | # Use xhispertool type for ASCII (32-126), clipboard+paste for Unicode
144 | for ((i=0; i<${#text}; i++)); do
145 | local char="${text:$i:1}"
146 | local ascii=$(printf '%d' "'$char")
147 |
148 | if [[ $ascii -ge 32 && $ascii -le 126 ]]; then
149 | # ASCII printable character - use direct key typing (faster)
150 | "$XHISPERTOOL" type "$char"
151 | else
152 | # Unicode or special character - use clipboard
153 | echo -n "$char" | $CLIP_COPY
154 | "$XHISPERTOOL" paste
155 | # On first character (more error-prone), sleep longer
156 | [ "$i" -eq 0 ] && sleep "$non_ascii_initial_delay" || sleep "$non_ascii_default_delay"
157 | fi
158 | done
159 | press_wrap_key
160 | }
161 |
162 | delete_n_chars() {
163 | local n="$1"
164 | for ((i=0; i/dev/null || echo "0"
172 | }
173 |
174 | is_silent() {
175 | local recording="$1"
176 |
177 | # Use ffmpeg volumedetect to get mean and max volume
178 | local vol_stats=$(ffmpeg -i "$recording" -af "volumedetect" -f null /dev/null 2>&1 | grep -E "mean_volume|max_volume")
179 | local max_vol=$(echo "$vol_stats" | grep "max_volume" | awk '{print $5}')
180 |
181 | # If max volume is below threshold, consider it silent
182 | # Note: ffmpeg reports in dB, negative values (e.g., -50 dB is quiet)
183 | if [ -n "$max_vol" ]; then
184 | local is_quiet=$(echo "$max_vol < $silence_threshold" | bc -l)
185 | [ "$is_quiet" -eq 1 ] && return 0
186 | fi
187 |
188 | return 1
189 | }
190 |
191 | logging_end_and_write_to_logfile() {
192 | local title="$1"
193 | local result="$2"
194 | local logging_start="$3"
195 |
196 | local logging_end=$(date +%s%N)
197 | local time=$(echo "scale=3; ($logging_end - $logging_start) / 1000000000" | bc)
198 |
199 | echo "=== $title ===" >> "$LOGFILE"
200 | echo "Result: [$result]" >> "$LOGFILE"
201 | echo "Time: ${time}s" >> "$LOGFILE"
202 | }
203 |
204 | transcribe() {
205 | local recording="$1"
206 | local logging_start=$(date +%s%N)
207 |
208 | # Use large model for longer recordings, turbo for short ones
209 | local is_long_recording=$(echo "$(get_duration "$recording") > $long_recording_threshold" | bc -l)
210 | local model=$([[ $is_long_recording -eq 1 ]] && echo "whisper-large-v3" || echo "whisper-large-v3-turbo")
211 |
212 | local transcription=$(curl -s -X POST "https://api.groq.com/openai/v1/audio/transcriptions" \
213 | -H "Authorization: Bearer $GROQ_API_KEY" \
214 | -H "Content-Type: multipart/form-data" \
215 | -F "file=@$recording" \
216 | -F "model=$model" \
217 | -F "prompt=$transcription_prompt" \
218 | | jq -r '.text' | sed 's/^ //') # Transcription always returns a leading space, so remove it via sed
219 |
220 | logging_end_and_write_to_logfile "Transcription" "$transcription" "$logging_start"
221 |
222 | echo "$transcription"
223 | }
224 |
225 | # Main
226 |
227 | # Find recording process, if so then kill
228 | if pgrep -f "$PROCESS_PATTERN" > /dev/null; then
229 | pkill -f "$PROCESS_PATTERN"; sleep 0.2 # Buffer for flush
230 | delete_n_chars 14 # "(recording...)"
231 |
232 | # Check if recording is silent
233 | if is_silent "$RECORDING"; then
234 | paste "(no sound detected)"
235 | sleep 0.6
236 | delete_n_chars 19 # "(no sound detected)"
237 | rm -f "$RECORDING"
238 | exit 0
239 | fi
240 |
241 | paste "(transcribing...)"
242 | TRANSCRIPTION=$(transcribe "$RECORDING")
243 | delete_n_chars 17 # "(transcribing...)"
244 |
245 | paste "$TRANSCRIPTION"
246 |
247 | rm -f "$RECORDING"
248 | else
249 | # No recording running, so start
250 | sleep 0.2
251 | paste "(recording...)"
252 | pw-record --channels=1 --rate=16000 "$RECORDING"
253 | fi
254 |
--------------------------------------------------------------------------------
/test.c:
--------------------------------------------------------------------------------
1 | /*
2 | * test.c - Test program for xhisper
3 | */
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #define KEY_LEFTCTRL 29
15 | #define KEY_RIGHTCTRL 97
16 | #define KEY_LEFTALT 56
17 | #define KEY_RIGHTALT 100
18 | #define KEY_LEFTSHIFT 42
19 | #define KEY_RIGHTSHIFT 54
20 | #define KEY_LEFTMETA 125
21 | #define KEY_V 47
22 | #define FLAG_UPPERCASE 0x80000000
23 |
24 | // ASCII to Linux keycode mapping
25 | static const int32_t ascii2keycode_map[128] = {
26 | -1,-1,-1,-1,-1,-1,-1,-1,
27 | -1,KEY_TAB,KEY_ENTER,-1,-1,-1,-1,-1,
28 | -1,-1,-1,-1,-1,-1,-1,-1,
29 | -1,-1,-1,-1,-1,-1,-1,-1,
30 | KEY_SPACE,KEY_1|FLAG_UPPERCASE,KEY_APOSTROPHE|FLAG_UPPERCASE,KEY_3|FLAG_UPPERCASE,KEY_4|FLAG_UPPERCASE,KEY_5|FLAG_UPPERCASE,KEY_7|FLAG_UPPERCASE,KEY_APOSTROPHE,
31 | KEY_9|FLAG_UPPERCASE,KEY_0|FLAG_UPPERCASE,KEY_8|FLAG_UPPERCASE,KEY_EQUAL|FLAG_UPPERCASE,KEY_COMMA,KEY_MINUS,KEY_DOT,KEY_SLASH,
32 | KEY_0,KEY_1,KEY_2,KEY_3,KEY_4,KEY_5,KEY_6,KEY_7,
33 | KEY_8,KEY_9,KEY_SEMICOLON|FLAG_UPPERCASE,KEY_SEMICOLON,KEY_COMMA|FLAG_UPPERCASE,KEY_EQUAL,KEY_DOT|FLAG_UPPERCASE,KEY_SLASH|FLAG_UPPERCASE,
34 | KEY_2|FLAG_UPPERCASE,KEY_A|FLAG_UPPERCASE,KEY_B|FLAG_UPPERCASE,KEY_C|FLAG_UPPERCASE,KEY_D|FLAG_UPPERCASE,KEY_E|FLAG_UPPERCASE,KEY_F|FLAG_UPPERCASE,KEY_G|FLAG_UPPERCASE,
35 | KEY_H|FLAG_UPPERCASE,KEY_I|FLAG_UPPERCASE,KEY_J|FLAG_UPPERCASE,KEY_K|FLAG_UPPERCASE,KEY_L|FLAG_UPPERCASE,KEY_M|FLAG_UPPERCASE,KEY_N|FLAG_UPPERCASE,KEY_O|FLAG_UPPERCASE,
36 | KEY_P|FLAG_UPPERCASE,KEY_Q|FLAG_UPPERCASE,KEY_R|FLAG_UPPERCASE,KEY_S|FLAG_UPPERCASE,KEY_T|FLAG_UPPERCASE,KEY_U|FLAG_UPPERCASE,KEY_V|FLAG_UPPERCASE,KEY_W|FLAG_UPPERCASE,
37 | KEY_X|FLAG_UPPERCASE,KEY_Y|FLAG_UPPERCASE,KEY_Z|FLAG_UPPERCASE,KEY_LEFTBRACE,KEY_BACKSLASH,KEY_RIGHTBRACE,KEY_6|FLAG_UPPERCASE,KEY_MINUS|FLAG_UPPERCASE,
38 | KEY_GRAVE,KEY_A,KEY_B,KEY_C,KEY_D,KEY_E,KEY_F,KEY_G,
39 | KEY_H,KEY_I,KEY_J,KEY_K,KEY_L,KEY_M,KEY_N,KEY_O,
40 | KEY_P,KEY_Q,KEY_R,KEY_S,KEY_T,KEY_U,KEY_V,KEY_W,
41 | KEY_X,KEY_Y,KEY_Z,KEY_LEFTBRACE|FLAG_UPPERCASE,KEY_BACKSLASH|FLAG_UPPERCASE,KEY_RIGHTBRACE|FLAG_UPPERCASE,KEY_GRAVE|FLAG_UPPERCASE,-1
42 | };
43 |
44 | static int fd_uinput = -1;
45 |
46 | void cleanup() {
47 | if (fd_uinput >= 0) {
48 | ioctl(fd_uinput, UI_DEV_DESTROY);
49 | close(fd_uinput);
50 | }
51 | }
52 |
53 | void emit(int type, int code, int val) {
54 | struct input_event ie = {
55 | .type = type,
56 | .code = code,
57 | .value = val
58 | };
59 | write(fd_uinput, &ie, sizeof(ie));
60 | }
61 |
62 | void do_paste() {
63 | emit(EV_KEY, KEY_LEFTCTRL, 1);
64 | emit(EV_SYN, SYN_REPORT, 0);
65 | usleep(8000);
66 | emit(EV_KEY, KEY_V, 1);
67 | emit(EV_SYN, SYN_REPORT, 0);
68 | usleep(8000);
69 | emit(EV_KEY, KEY_V, 0);
70 | emit(EV_SYN, SYN_REPORT, 0);
71 | usleep(2000);
72 | emit(EV_KEY, KEY_LEFTCTRL, 0);
73 | emit(EV_SYN, SYN_REPORT, 0);
74 | }
75 |
76 | void type_char(unsigned char c) {
77 | if (c >= 128) return;
78 |
79 | int32_t kdef = ascii2keycode_map[c];
80 | if (kdef == -1) return;
81 |
82 | uint16_t keycode = kdef & 0xffff;
83 |
84 | if (kdef & FLAG_UPPERCASE) {
85 | emit(EV_KEY, KEY_LEFTSHIFT, 1);
86 | emit(EV_SYN, SYN_REPORT, 0);
87 | usleep(2000);
88 | }
89 |
90 | emit(EV_KEY, keycode, 1);
91 | emit(EV_SYN, SYN_REPORT, 0);
92 | usleep(8000);
93 |
94 | emit(EV_KEY, keycode, 0);
95 | emit(EV_SYN, SYN_REPORT, 0);
96 | usleep(2000);
97 |
98 | if (kdef & FLAG_UPPERCASE) {
99 | emit(EV_KEY, KEY_LEFTSHIFT, 0);
100 | emit(EV_SYN, SYN_REPORT, 0);
101 | }
102 | }
103 |
104 | void do_backspace() {
105 | emit(EV_KEY, KEY_BACKSPACE, 1);
106 | emit(EV_SYN, SYN_REPORT, 0);
107 | usleep(8000);
108 | emit(EV_KEY, KEY_BACKSPACE, 0);
109 | emit(EV_SYN, SYN_REPORT, 0);
110 | }
111 |
112 | void press_key(int keycode) {
113 | emit(EV_KEY, keycode, 1);
114 | emit(EV_SYN, SYN_REPORT, 0);
115 | usleep(8000);
116 | emit(EV_KEY, keycode, 0);
117 | emit(EV_SYN, SYN_REPORT, 0);
118 | }
119 |
120 | int setup_uinput() {
121 | fd_uinput = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
122 | if (fd_uinput < 0) {
123 | perror("failed to open /dev/uinput");
124 | fprintf(stderr, "Make sure you're in the 'input' group\n");
125 | return -1;
126 | }
127 |
128 | ioctl(fd_uinput, UI_SET_EVBIT, EV_KEY);
129 |
130 | // Register letters
131 | for (int i = KEY_Q; i <= KEY_P; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
132 | for (int i = KEY_A; i <= KEY_L; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
133 | for (int i = KEY_Z; i <= KEY_M; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
134 |
135 | // Register numbers
136 | for (int i = KEY_1; i <= KEY_0; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
137 |
138 | // Register special keys
139 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_SPACE);
140 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_MINUS);
141 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_EQUAL);
142 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTBRACE);
143 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTBRACE);
144 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_SEMICOLON);
145 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_APOSTROPHE);
146 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_GRAVE);
147 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_BACKSLASH);
148 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_COMMA);
149 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_DOT);
150 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_SLASH);
151 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_TAB);
152 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_ENTER);
153 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_BACKSPACE);
154 |
155 | // Register modifiers
156 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTCTRL);
157 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTCTRL);
158 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTALT);
159 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTALT);
160 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTSHIFT);
161 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTSHIFT);
162 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTMETA);
163 |
164 | struct uinput_setup setup = {0};
165 | setup.id.bustype = BUS_USB;
166 | setup.id.vendor = 0x1234;
167 | setup.id.product = 0x5678;
168 | snprintf(setup.name, UINPUT_MAX_NAME_SIZE, "xhisper-test");
169 |
170 | if (ioctl(fd_uinput, UI_DEV_SETUP, &setup) < 0) {
171 | perror("failed to setup uinput device");
172 | return -1;
173 | }
174 | if (ioctl(fd_uinput, UI_DEV_CREATE) < 0) {
175 | perror("failed to create uinput device");
176 | return -1;
177 | }
178 |
179 | usleep(100000);
180 | return 0;
181 | }
182 |
183 | void run_clipboard_command(const char *text) {
184 | char cmd[512];
185 | snprintf(cmd, sizeof(cmd), "command -v wl-copy >/dev/null 2>&1 && echo -n '%s' | wl-copy", text);
186 | if (system(cmd) != 0) {
187 | snprintf(cmd, sizeof(cmd), "command -v xclip >/dev/null 2>&1 && echo -n '%s' | xclip -selection clipboard", text);
188 | system(cmd);
189 | }
190 | }
191 |
192 | void test_typer() {
193 | printf("\n--- Testing ASCII typing ---\n");
194 |
195 | const char *lower = "abcdefghijklmnopqrstuvwxyz";
196 | printf("Typing: %s\n", lower);
197 | for (int i = 0; lower[i]; i++) {
198 | type_char(lower[i]);
199 | }
200 |
201 | usleep(200000);
202 | type_char(' ');
203 |
204 | const char *upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
205 | printf("Typing: %s\n", upper);
206 | for (int i = 0; upper[i]; i++) {
207 | type_char(upper[i]);
208 | }
209 |
210 | usleep(200000);
211 | type_char(' ');
212 |
213 | const char *numbers = "0123456789";
214 | printf("Typing: %s\n", numbers);
215 | for (int i = 0; numbers[i]; i++) {
216 | type_char(numbers[i]);
217 | }
218 |
219 | usleep(200000);
220 | type_char(' ');
221 |
222 | const char *symbols = "!@#$%^&*()_+-=[]{}\\|;:'\"<>,.?/`~";
223 | printf("Typing: %s\n", symbols);
224 | for (int i = 0; symbols[i]; i++) {
225 | type_char(symbols[i]);
226 | }
227 |
228 | usleep(500000);
229 |
230 | printf("Typing 'test' then deleting with backspace\n");
231 | type_char('t');
232 | type_char('e');
233 | type_char('s');
234 | type_char('t');
235 | usleep(300000);
236 | do_backspace();
237 | do_backspace();
238 | do_backspace();
239 | do_backspace();
240 |
241 | usleep(500000);
242 |
243 | const char *sentence = "Hello, World! Testing 123.";
244 | printf("Typing: %s\n", sentence);
245 | for (int i = 0; sentence[i]; i++) {
246 | type_char(sentence[i]);
247 | }
248 | }
249 |
250 | void test_paster() {
251 | printf("\n--- Testing Unicode/clipboard pasting ---\n");
252 |
253 | printf("Pasting: éàèùçäöüß你好世界مرحباМосква\n");
254 |
255 | const char *chars[] = {"é", "à", "è", "ù", "ç", "ä", "ö", "ü", "ß",
256 | "你", "好", "世", "界", "م", "ر", "ح", "ب", "ا",
257 | "М", "о", "с", "к", "в", "а"};
258 |
259 | for (int i = 0; i < 24; i++) {
260 | run_clipboard_command(chars[i]);
261 | usleep(50000);
262 | do_paste();
263 | usleep(100000);
264 | }
265 | }
266 |
267 | int main(int argc, char *argv[]) {
268 | int wrap_keycode = 0;
269 | const char *wrap_name = NULL;
270 |
271 | // Parse command-line arguments for input switching key
272 | if (argc > 1) {
273 | if (strcmp(argv[1], "--leftalt") == 0) {
274 | wrap_keycode = KEY_LEFTALT;
275 | wrap_name = "leftalt";
276 | } else if (strcmp(argv[1], "--rightalt") == 0) {
277 | wrap_keycode = KEY_RIGHTALT;
278 | wrap_name = "rightalt";
279 | } else if (strcmp(argv[1], "--leftctrl") == 0) {
280 | wrap_keycode = KEY_LEFTCTRL;
281 | wrap_name = "leftctrl";
282 | } else if (strcmp(argv[1], "--rightctrl") == 0) {
283 | wrap_keycode = KEY_RIGHTCTRL;
284 | wrap_name = "rightctrl";
285 | } else if (strcmp(argv[1], "--leftshift") == 0) {
286 | wrap_keycode = KEY_LEFTSHIFT;
287 | wrap_name = "leftshift";
288 | } else if (strcmp(argv[1], "--rightshift") == 0) {
289 | wrap_keycode = KEY_RIGHTSHIFT;
290 | wrap_name = "rightshift";
291 | } else if (strcmp(argv[1], "--super") == 0) {
292 | wrap_keycode = KEY_LEFTMETA;
293 | wrap_name = "super";
294 | } else {
295 | fprintf(stderr, "Usage: %s [--leftalt|--rightalt|--leftctrl|--rightctrl|--leftshift|--rightshift|--super]\n", argv[0]);
296 | return 1;
297 | }
298 | }
299 |
300 | printf("=== xhisper test ===\n");
301 | if (wrap_name) {
302 | printf("Wrapping with key: %s\n", wrap_name);
303 | }
304 | printf("\nMake sure you're in the 'input' group:\n");
305 | printf(" sudo usermod -aG input $USER\n");
306 | printf(" (then log out and back in)\n");
307 | printf("\nFocus a text editor and press Enter to start in 3 seconds...\n");
308 | getchar();
309 | sleep(3);
310 |
311 | atexit(cleanup);
312 |
313 | if (setup_uinput() < 0) {
314 | return 1;
315 | }
316 |
317 | // Press wrapping key before tests
318 | if (wrap_keycode) {
319 | printf("Pressing %s...\n", wrap_name);
320 | press_key(wrap_keycode);
321 | usleep(100000);
322 | }
323 |
324 | test_typer();
325 | sleep(1);
326 | test_paster();
327 |
328 | // Press wrapping key after tests
329 | if (wrap_keycode) {
330 | usleep(100000);
331 | printf("Pressing %s...\n", wrap_name);
332 | press_key(wrap_keycode);
333 | }
334 |
335 | printf("\n\n=== Test complete ===\n");
336 | printf("Expected output:\n");
337 | printf("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 !@#$%%^&*()_+-=[]{}\\|;:'\"<>,.?/`~ Hello, World! Testing 123.éàèùçäöüß你好世界مرحباМосква\n");
338 |
339 | return 0;
340 | }
341 |
--------------------------------------------------------------------------------
/xhispertool.c:
--------------------------------------------------------------------------------
1 | /*
2 | * xhisper - Whisper for Linux
3 | * Combined daemon and client for text input via uinput
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #define KEY_LEFTCTRL 29
18 | #define KEY_RIGHTCTRL 97
19 | #define KEY_LEFTALT 56
20 | #define KEY_RIGHTALT 100
21 | #define KEY_LEFTSHIFT 42
22 | #define KEY_RIGHTSHIFT 54
23 | #define KEY_LEFTMETA 125
24 | #define KEY_V 47
25 | #define FLAG_UPPERCASE 0x80000000
26 |
27 | // Function prototypes
28 | void cleanup(void);
29 | void emit(int type, int code, int val);
30 | void do_paste(void);
31 | void type_char(unsigned char c);
32 | void do_backspace(void);
33 | void do_key(int keycode);
34 | int setup_uinput(void);
35 | int setup_socket(void);
36 | int run_daemon(void);
37 | void show_usage(void);
38 | int run_client(int argc, char *argv[]);
39 |
40 | // ASCII to Linux keycode mapping for US QWERTY layout
41 | // Independently derived from:
42 | // - ASCII character set specification (characters 0-127)
43 | // - Linux input-event-codes.h KEY_* constants
44 | // - US QWERTY keyboard physical layout
45 | // Each entry maps an ASCII code to either:
46 | // -1 (unmapped/unsupported)
47 | // KEY_* constant for unshifted characters
48 | // KEY_* | FLAG_UPPERCASE for shifted characters
49 | static const int32_t ascii2keycode_map[128] = {
50 | // Control characters (0x00-0x1f): mostly unmapped except tab and enter
51 | -1,-1,-1,-1,-1,-1,-1,-1, // 0x00-0x07
52 | -1,KEY_TAB,KEY_ENTER,-1,-1,-1,-1,-1, // 0x08-0x0f (tab=0x09, enter=0x0a)
53 | -1,-1,-1,-1,-1,-1,-1,-1, // 0x10-0x17
54 | -1,-1,-1,-1,-1,-1,-1,-1, // 0x18-0x1f
55 |
56 | // Printable characters (0x20-0x7e)
57 | // Space and symbols (0x20-0x2f)
58 | KEY_SPACE, // 0x20 ' '
59 | KEY_1|FLAG_UPPERCASE, // 0x21 '!' (shift+1)
60 | KEY_APOSTROPHE|FLAG_UPPERCASE, // 0x22 '"' (shift+')
61 | KEY_3|FLAG_UPPERCASE, // 0x23 '#' (shift+3)
62 | KEY_4|FLAG_UPPERCASE, // 0x24 '$' (shift+4)
63 | KEY_5|FLAG_UPPERCASE, // 0x25 '%' (shift+5)
64 | KEY_7|FLAG_UPPERCASE, // 0x26 '&' (shift+7)
65 | KEY_APOSTROPHE, // 0x27 '''
66 | KEY_9|FLAG_UPPERCASE, // 0x28 '(' (shift+9)
67 | KEY_0|FLAG_UPPERCASE, // 0x29 ')' (shift+0)
68 | KEY_8|FLAG_UPPERCASE, // 0x2a '*' (shift+8)
69 | KEY_EQUAL|FLAG_UPPERCASE, // 0x2b '+' (shift+=)
70 | KEY_COMMA, // 0x2c ','
71 | KEY_MINUS, // 0x2d '-'
72 | KEY_DOT, // 0x2e '.'
73 | KEY_SLASH, // 0x2f '/'
74 |
75 | // Digits (0x30-0x39)
76 | KEY_0,KEY_1,KEY_2,KEY_3,KEY_4,KEY_5,KEY_6,KEY_7,KEY_8,KEY_9,
77 |
78 | // More symbols (0x3a-0x40)
79 | KEY_SEMICOLON|FLAG_UPPERCASE, // 0x3a ':' (shift+;)
80 | KEY_SEMICOLON, // 0x3b ';'
81 | KEY_COMMA|FLAG_UPPERCASE, // 0x3c '<' (shift+,)
82 | KEY_EQUAL, // 0x3d '='
83 | KEY_DOT|FLAG_UPPERCASE, // 0x3e '>' (shift+.)
84 | KEY_SLASH|FLAG_UPPERCASE, // 0x3f '?' (shift+/)
85 | KEY_2|FLAG_UPPERCASE, // 0x40 '@' (shift+2)
86 |
87 | // Uppercase letters (0x41-0x5a): A-Z
88 | KEY_A|FLAG_UPPERCASE,KEY_B|FLAG_UPPERCASE,KEY_C|FLAG_UPPERCASE,KEY_D|FLAG_UPPERCASE,
89 | KEY_E|FLAG_UPPERCASE,KEY_F|FLAG_UPPERCASE,KEY_G|FLAG_UPPERCASE,KEY_H|FLAG_UPPERCASE,
90 | KEY_I|FLAG_UPPERCASE,KEY_J|FLAG_UPPERCASE,KEY_K|FLAG_UPPERCASE,KEY_L|FLAG_UPPERCASE,
91 | KEY_M|FLAG_UPPERCASE,KEY_N|FLAG_UPPERCASE,KEY_O|FLAG_UPPERCASE,KEY_P|FLAG_UPPERCASE,
92 | KEY_Q|FLAG_UPPERCASE,KEY_R|FLAG_UPPERCASE,KEY_S|FLAG_UPPERCASE,KEY_T|FLAG_UPPERCASE,
93 | KEY_U|FLAG_UPPERCASE,KEY_V|FLAG_UPPERCASE,KEY_W|FLAG_UPPERCASE,KEY_X|FLAG_UPPERCASE,
94 | KEY_Y|FLAG_UPPERCASE,KEY_Z|FLAG_UPPERCASE,
95 |
96 | // Brackets and symbols (0x5b-0x60)
97 | KEY_LEFTBRACE, // 0x5b '['
98 | KEY_BACKSLASH, // 0x5c '\'
99 | KEY_RIGHTBRACE, // 0x5d ']'
100 | KEY_6|FLAG_UPPERCASE, // 0x5e '^' (shift+6)
101 | KEY_MINUS|FLAG_UPPERCASE, // 0x5f '_' (shift+-)
102 | KEY_GRAVE, // 0x60 '`'
103 |
104 | // Lowercase letters (0x61-0x7a): a-z
105 | KEY_A,KEY_B,KEY_C,KEY_D,KEY_E,KEY_F,KEY_G,KEY_H,
106 | KEY_I,KEY_J,KEY_K,KEY_L,KEY_M,KEY_N,KEY_O,KEY_P,
107 | KEY_Q,KEY_R,KEY_S,KEY_T,KEY_U,KEY_V,KEY_W,KEY_X,
108 | KEY_Y,KEY_Z,
109 |
110 | // Final symbols (0x7b-0x7e)
111 | KEY_LEFTBRACE|FLAG_UPPERCASE, // 0x7b '{' (shift+[)
112 | KEY_BACKSLASH|FLAG_UPPERCASE, // 0x7c '|' (shift+\)
113 | KEY_RIGHTBRACE|FLAG_UPPERCASE, // 0x7d '}' (shift+])
114 | KEY_GRAVE|FLAG_UPPERCASE, // 0x7e '~' (shift+`)
115 |
116 | -1 // 0x7f DEL (unmapped)
117 | };
118 |
119 | static int fd_uinput = -1;
120 | static int fd_socket = -1;
121 |
122 | void cleanup() {
123 | if (fd_uinput >= 0) {
124 | ioctl(fd_uinput, UI_DEV_DESTROY);
125 | close(fd_uinput);
126 | }
127 | if (fd_socket >= 0) {
128 | close(fd_socket);
129 | }
130 | }
131 |
132 | void emit(int type, int code, int val) {
133 | struct input_event ie = {
134 | .type = type,
135 | .code = code,
136 | .value = val
137 | };
138 | write(fd_uinput, &ie, sizeof(ie));
139 | }
140 |
141 | void do_paste() {
142 | emit(EV_KEY, KEY_LEFTCTRL, 1);
143 | emit(EV_SYN, SYN_REPORT, 0);
144 | usleep(8000);
145 | emit(EV_KEY, KEY_V, 1);
146 | emit(EV_SYN, SYN_REPORT, 0);
147 | usleep(8000);
148 | emit(EV_KEY, KEY_V, 0);
149 | emit(EV_SYN, SYN_REPORT, 0);
150 | usleep(2000);
151 | emit(EV_KEY, KEY_LEFTCTRL, 0);
152 | emit(EV_SYN, SYN_REPORT, 0);
153 | }
154 |
155 | void type_char(unsigned char c) {
156 | if (c >= 128) return;
157 |
158 | int32_t kdef = ascii2keycode_map[c];
159 | if (kdef == -1) return;
160 |
161 | uint16_t keycode = kdef & 0xffff;
162 |
163 | if (kdef & FLAG_UPPERCASE) {
164 | emit(EV_KEY, KEY_LEFTSHIFT, 1);
165 | emit(EV_SYN, SYN_REPORT, 0);
166 | usleep(2000);
167 | }
168 |
169 | emit(EV_KEY, keycode, 1);
170 | emit(EV_SYN, SYN_REPORT, 0);
171 | usleep(8000);
172 |
173 | emit(EV_KEY, keycode, 0);
174 | emit(EV_SYN, SYN_REPORT, 0);
175 | usleep(2000);
176 |
177 | if (kdef & FLAG_UPPERCASE) {
178 | emit(EV_KEY, KEY_LEFTSHIFT, 0);
179 | emit(EV_SYN, SYN_REPORT, 0);
180 | }
181 | }
182 |
183 | void do_backspace() {
184 | emit(EV_KEY, KEY_BACKSPACE, 1);
185 | emit(EV_SYN, SYN_REPORT, 0);
186 | usleep(8000);
187 | emit(EV_KEY, KEY_BACKSPACE, 0);
188 | emit(EV_SYN, SYN_REPORT, 0);
189 | }
190 |
191 | void do_key(int keycode) {
192 | emit(EV_KEY, keycode, 1);
193 | emit(EV_SYN, SYN_REPORT, 0);
194 | usleep(8000);
195 | emit(EV_KEY, keycode, 0);
196 | emit(EV_SYN, SYN_REPORT, 0);
197 | }
198 |
199 | int setup_uinput() {
200 | fd_uinput = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
201 | if (fd_uinput < 0) {
202 | perror("failed to open /dev/uinput");
203 | return -1;
204 | }
205 |
206 | ioctl(fd_uinput, UI_SET_EVBIT, EV_KEY);
207 |
208 | // Register letters
209 | for (int i = KEY_Q; i <= KEY_P; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
210 | for (int i = KEY_A; i <= KEY_L; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
211 | for (int i = KEY_Z; i <= KEY_M; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
212 |
213 | // Register numbers
214 | for (int i = KEY_1; i <= KEY_0; i++) ioctl(fd_uinput, UI_SET_KEYBIT, i);
215 |
216 | // Register special keys
217 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_SPACE);
218 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_MINUS);
219 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_EQUAL);
220 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTBRACE);
221 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTBRACE);
222 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_SEMICOLON);
223 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_APOSTROPHE);
224 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_GRAVE);
225 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_BACKSLASH);
226 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_COMMA);
227 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_DOT);
228 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_SLASH);
229 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_TAB);
230 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_ENTER);
231 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_BACKSPACE);
232 |
233 | // Register modifiers
234 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTCTRL);
235 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTCTRL);
236 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTALT);
237 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTALT);
238 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTSHIFT);
239 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_RIGHTSHIFT);
240 | ioctl(fd_uinput, UI_SET_KEYBIT, KEY_LEFTMETA);
241 |
242 | struct uinput_setup setup = {0};
243 | setup.id.bustype = BUS_USB;
244 | setup.id.vendor = 0x1234;
245 | setup.id.product = 0x5678;
246 | snprintf(setup.name, UINPUT_MAX_NAME_SIZE, "xhisper");
247 |
248 | if (ioctl(fd_uinput, UI_DEV_SETUP, &setup) < 0) {
249 | perror("failed to setup uinput device");
250 | return -1;
251 | }
252 | if (ioctl(fd_uinput, UI_DEV_CREATE) < 0) {
253 | perror("failed to create uinput device");
254 | return -1;
255 | }
256 |
257 | usleep(100000);
258 | return 0;
259 | }
260 |
261 | int setup_socket() {
262 | fd_socket = socket(AF_UNIX, SOCK_DGRAM, 0);
263 | if (fd_socket < 0) {
264 | perror("failed to create socket");
265 | return -1;
266 | }
267 |
268 | // Use abstract namespace socket (Linux-specific)
269 | // First byte is null, no filesystem entry, kernel manages lifecycle
270 | struct sockaddr_un addr = {.sun_family = AF_UNIX};
271 | addr.sun_path[0] = '\0';
272 | strncpy(addr.sun_path + 1, "xhisper_socket", sizeof(addr.sun_path) - 2);
273 |
274 | if (bind(fd_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
275 | if (errno == EADDRINUSE) {
276 | fprintf(stderr, "xhispertoold is already running\n");
277 | } else {
278 | perror("failed to bind socket");
279 | }
280 | return -1;
281 | }
282 |
283 | return 0;
284 | }
285 |
286 | // Daemon mode
287 | int run_daemon() {
288 | atexit(cleanup);
289 |
290 | if (setup_uinput() < 0) {
291 | return 1;
292 | }
293 |
294 | if (setup_socket() < 0) {
295 | return 1;
296 | }
297 |
298 | printf("xhispertoold: listening on @xhisper_socket\n");
299 |
300 | char buf[2];
301 | while (1) {
302 | ssize_t n = recv(fd_socket, buf, sizeof(buf), 0);
303 | if (n >= 1) {
304 | char cmd = buf[0];
305 | if (cmd == 'p') {
306 | do_paste();
307 | } else if (cmd == 't' && n == 2) {
308 | type_char((unsigned char)buf[1]);
309 | } else if (cmd == 'b') {
310 | do_backspace();
311 | } else if (cmd == 'r') {
312 | do_key(KEY_RIGHTALT);
313 | } else if (cmd == 'L') {
314 | do_key(KEY_LEFTALT);
315 | } else if (cmd == 'C') {
316 | do_key(KEY_LEFTCTRL);
317 | } else if (cmd == 'R') {
318 | do_key(KEY_RIGHTCTRL);
319 | } else if (cmd == 'S') {
320 | do_key(KEY_LEFTSHIFT);
321 | } else if (cmd == 'T') {
322 | do_key(KEY_RIGHTSHIFT);
323 | } else if (cmd == 'M') {
324 | do_key(KEY_LEFTMETA);
325 | }
326 | }
327 | }
328 |
329 | return 0;
330 | }
331 |
332 | // Client mode
333 | void show_usage() {
334 | fprintf(stderr,
335 | "Usage:\n"
336 | " xhispertool paste - Paste from clipboard (Ctrl+V)\n"
337 | " xhispertool type - Type a single ASCII character\n"
338 | " xhispertool backspace - Press backspace\n"
339 | "\n"
340 | "Input switching keys:\n"
341 | " xhispertool leftalt - Press left alt\n"
342 | " xhispertool rightalt - Press right alt\n"
343 | " xhispertool leftctrl - Press left ctrl\n"
344 | " xhispertool rightctrl - Press right ctrl\n"
345 | " xhispertool leftshift - Press left shift\n"
346 | " xhispertool rightshift - Press right shift\n"
347 | " xhispertool super - Press super (Windows key)\n"
348 | "\n"
349 | "Daemon:\n"
350 | " xhispertoold - Run daemon (or xhispertool --daemon)\n"
351 | );
352 | }
353 |
354 | int run_client(int argc, char *argv[]) {
355 | if (argc < 2) {
356 | show_usage();
357 | return 1;
358 | }
359 |
360 | int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
361 | if (fd < 0) {
362 | perror("failed to create socket");
363 | return 1;
364 | }
365 |
366 | // Use abstract namespace socket (same as daemon)
367 | struct sockaddr_un addr = {.sun_family = AF_UNIX};
368 | addr.sun_path[0] = '\0';
369 | strncpy(addr.sun_path + 1, "xhisper_socket", sizeof(addr.sun_path) - 2);
370 |
371 | if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
372 | int err = errno;
373 | fprintf(stderr, "failed to connect to xhispertoold: %s\n", strerror(err));
374 |
375 | switch (err) {
376 | case ENOENT:
377 | case ECONNREFUSED:
378 | fprintf(stderr, "Please check if xhispertoold is running.\n");
379 | fprintf(stderr, "Start it with: xhispertoold &\n");
380 | break;
381 | case EACCES:
382 | case EPERM:
383 | fprintf(stderr, "Permission denied. Check socket permissions.\n");
384 | break;
385 | }
386 | close(fd);
387 | return 2;
388 | }
389 |
390 | char buf[2];
391 | ssize_t len = 0;
392 |
393 | if (strcmp(argv[1], "paste") == 0) {
394 | buf[0] = 'p';
395 | len = 1;
396 | } else if (strcmp(argv[1], "backspace") == 0) {
397 | buf[0] = 'b';
398 | len = 1;
399 | } else if (strcmp(argv[1], "rightalt") == 0) {
400 | buf[0] = 'r';
401 | len = 1;
402 | } else if (strcmp(argv[1], "leftalt") == 0) {
403 | buf[0] = 'L';
404 | len = 1;
405 | } else if (strcmp(argv[1], "leftctrl") == 0) {
406 | buf[0] = 'C';
407 | len = 1;
408 | } else if (strcmp(argv[1], "rightctrl") == 0) {
409 | buf[0] = 'R';
410 | len = 1;
411 | } else if (strcmp(argv[1], "leftshift") == 0) {
412 | buf[0] = 'S';
413 | len = 1;
414 | } else if (strcmp(argv[1], "rightshift") == 0) {
415 | buf[0] = 'T';
416 | len = 1;
417 | } else if (strcmp(argv[1], "super") == 0) {
418 | buf[0] = 'M';
419 | len = 1;
420 | } else if (strcmp(argv[1], "type") == 0) {
421 | if (argc != 3 || strlen(argv[2]) != 1) {
422 | fprintf(stderr, "Error: 'type' requires exactly one character argument\n");
423 | show_usage();
424 | close(fd);
425 | return 1;
426 | }
427 | buf[0] = 't';
428 | buf[1] = argv[2][0];
429 | len = 2;
430 | } else {
431 | fprintf(stderr, "Error: Unknown command '%s'\n", argv[1]);
432 | show_usage();
433 | close(fd);
434 | return 1;
435 | }
436 |
437 | if (write(fd, buf, len) != len) {
438 | perror("failed to send command");
439 | close(fd);
440 | return 1;
441 | }
442 |
443 | close(fd);
444 | return 0;
445 | }
446 |
447 | int main(int argc, char *argv[]) {
448 | // Detect mode: daemon or client
449 | char *prog = basename(argv[0]);
450 |
451 | if (strcmp(prog, "xhispertoold") == 0 ||
452 | (argc > 1 && strcmp(argv[1], "--daemon") == 0)) {
453 | return run_daemon();
454 | } else {
455 | return run_client(argc, argv);
456 | }
457 | }
458 |
--------------------------------------------------------------------------------