├── 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 | xhisper demo 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 | --------------------------------------------------------------------------------