diff --git a/config.ini b/config.ini index bb87898..3829b2f 100644 --- a/config.ini +++ b/config.ini @@ -27,4 +27,4 @@ vad_min_speech_ms=250 # hard cap for one speech segment vad_max_speech_ms=15000 -autotype_enabled=0 +autotype_enabled=1 diff --git a/src/config.c b/src/config.c index 12aa0f5..7ad2bcc 100644 --- a/src/config.c +++ b/src/config.c @@ -36,7 +36,7 @@ void config_set_defaults(voice_config_t *cfg) { cfg->vad_preroll_ms = 500; cfg->vad_min_speech_ms = 250; cfg->vad_max_speech_ms = 15000; - cfg->autotype_enabled = 0; + cfg->autotype_enabled = 1; } int config_load_file(const char *path, voice_config_t *cfg) { diff --git a/src/gui_main.c b/src/gui_main.c index aa37290..1cabac8 100644 --- a/src/gui_main.c +++ b/src/gui_main.c @@ -175,6 +175,12 @@ static void process_segment(gui_state_t *s, float *samples, size_t count) { return; } + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->type_check))) { + if (typer_capture_focus() != 0) { + fprintf(stderr, "gui: failed to capture focus target for autotype\n"); + } + } + s->transcribing = 1; update_record_button(s); set_status(s, "Transcribing... (please wait)"); @@ -194,7 +200,12 @@ static void process_segment(gui_state_t *s, float *samples, size_t count) { append_transcript(s, text); if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->type_check))) { - typer_type_text(text); + if (typer_type_text(text) != 0) { + fprintf(stderr, "gui: autotype injection failed\n"); + set_status(s, "Autotype failed (see terminal logs)"); + free(text); + return; + } } set_status(s, audio_msg); diff --git a/src/typer.c b/src/typer.c index ca25b85..451aa8d 100644 --- a/src/typer.c +++ b/src/typer.c @@ -11,6 +11,16 @@ static Display *g_dpy = NULL; static int g_delay_us = 8000; +static int g_key_hold_us = 4000; +static Window g_target_window = None; + +static void sleep_us(int us) { + if (us <= 0) return; + struct timespec ts; + ts.tv_sec = us / 1000000; + ts.tv_nsec = (long)(us % 1000000) * 1000L; + nanosleep(&ts, NULL); +} static int type_keysym(KeySym ks, int need_shift) { KeyCode kc = XKeysymToKeycode(g_dpy, ks); @@ -19,21 +29,21 @@ static int type_keysym(KeySym ks, int need_shift) { KeyCode shift_kc = XKeysymToKeycode(g_dpy, XK_Shift_L); if (need_shift && shift_kc) { - XTestFakeKeyEvent(g_dpy, shift_kc, True, CurrentTime); + if (!XTestFakeKeyEvent(g_dpy, shift_kc, True, CurrentTime)) return -1; } - XTestFakeKeyEvent(g_dpy, kc, True, CurrentTime); - XTestFakeKeyEvent(g_dpy, kc, False, CurrentTime); + if (!XTestFakeKeyEvent(g_dpy, kc, True, CurrentTime)) return -1; + XSync(g_dpy, False); + sleep_us(g_key_hold_us); + + if (!XTestFakeKeyEvent(g_dpy, kc, False, CurrentTime)) return -1; if (need_shift && shift_kc) { - XTestFakeKeyEvent(g_dpy, shift_kc, False, CurrentTime); + if (!XTestFakeKeyEvent(g_dpy, shift_kc, False, CurrentTime)) return -1; } - XFlush(g_dpy); - struct timespec ts; - ts.tv_sec = g_delay_us / 1000000; - ts.tv_nsec = (long)(g_delay_us % 1000000) * 1000L; - nanosleep(&ts, NULL); + XSync(g_dpy, False); + sleep_us(g_delay_us); return 0; } @@ -62,32 +72,118 @@ static int type_char(char c) { case '-': return type_keysym(XK_minus, 0); case '_': return type_keysym(XK_minus, 1); case '/': return type_keysym(XK_slash, 0); + case '\\': return type_keysym(XK_backslash, 0); + case '|': return type_keysym(XK_backslash, 1); + case '=': return type_keysym(XK_equal, 0); + case '+': return type_keysym(XK_equal, 1); + case '[': return type_keysym(XK_bracketleft, 0); + case '{': return type_keysym(XK_bracketleft, 1); + case ']': return type_keysym(XK_bracketright, 0); + case '}': return type_keysym(XK_bracketright, 1); + case '`': return type_keysym(XK_grave, 0); + case '~': return type_keysym(XK_grave, 1); + case '@': return type_keysym(XK_2, 1); + case '#': return type_keysym(XK_3, 1); + case '$': return type_keysym(XK_4, 1); + case '%': return type_keysym(XK_5, 1); + case '^': return type_keysym(XK_6, 1); + case '&': return type_keysym(XK_7, 1); + case '*': return type_keysym(XK_8, 1); case '(': return type_keysym(XK_9, 1); case ')': return type_keysym(XK_0, 1); + case '<': return type_keysym(XK_comma, 1); + case '>': return type_keysym(XK_period, 1); default: return 0; } } int typer_init(int type_delay_us) { + int ev_base = 0; + int err_base = 0; + int major = 0; + int minor = 0; + g_dpy = XOpenDisplay(NULL); - if (!g_dpy) return -1; + if (!g_dpy) { + fprintf(stderr, "typer: XOpenDisplay failed\n"); + return -1; + } + if (!XTestQueryExtension(g_dpy, &ev_base, &err_base, &major, &minor)) { + fprintf(stderr, "typer: XTEST extension unavailable\n"); + XCloseDisplay(g_dpy); + g_dpy = NULL; + return -1; + } + g_delay_us = type_delay_us > 0 ? type_delay_us : 8000; + g_key_hold_us = g_delay_us / 2; + if (g_key_hold_us < 4000) g_key_hold_us = 4000; + if (g_key_hold_us > 20000) g_key_hold_us = 20000; + g_target_window = None; + return 0; +} + +int typer_capture_focus(void) { + if (!g_dpy) return -1; + + Window focused = None; + int revert = 0; + XGetInputFocus(g_dpy, &focused, &revert); + + if (focused == None || focused == PointerRoot) { + return -1; + } + + g_target_window = focused; return 0; } int typer_type_text(const char *text) { if (!g_dpy || !text) return -1; - for (size_t i = 0; i < strlen(text); ++i) { + Window previous_focus = None; + int previous_revert = 0; + XGetInputFocus(g_dpy, &previous_focus, &previous_revert); + + Window target = g_target_window; + if (target == None || target == PointerRoot) { + target = previous_focus; + } + if (target == None || target == PointerRoot) { + fprintf(stderr, "typer: no valid target window for autotype\n"); + return -1; + } + + if (previous_focus != target) { + XSetInputFocus(g_dpy, target, RevertToParent, CurrentTime); + XSync(g_dpy, False); + sleep_us(20000); + } + + int failures = 0; + const size_t n = strlen(text); + for (size_t i = 0; i < n; ++i) { if (type_char(text[i]) != 0) { - fprintf(stderr, "typer: failed at char '%c'\n", text[i]); + fprintf(stderr, "typer: failed at char 0x%02x ('%c')\n", + (unsigned char)text[i], isprint((unsigned char)text[i]) ? text[i] : '?'); + failures++; } } - return 0; + if (previous_focus != None && previous_focus != PointerRoot && previous_focus != target) { + XSetInputFocus(g_dpy, previous_focus, RevertToParent, CurrentTime); + XSync(g_dpy, False); + } + + if (failures > 0) { + fprintf(stderr, "typer: %d/%zu characters failed to inject\n", failures, n); + } + + return failures == 0 ? 0 : -1; } void typer_cleanup(void) { + g_target_window = None; if (g_dpy) { XCloseDisplay(g_dpy); g_dpy = NULL; diff --git a/src/typer.h b/src/typer.h index a6eaf43..8d53b51 100644 --- a/src/typer.h +++ b/src/typer.h @@ -2,6 +2,7 @@ #define VOICE_TYPER_H int typer_init(int type_delay_us); +int typer_capture_focus(void); int typer_type_text(const char *text); void typer_cleanup(void); diff --git a/voice_linux_gui b/voice_linux_gui index 80edb42..4d37dec 100755 Binary files a/voice_linux_gui and b/voice_linux_gui differ