gtksettings: align Input tab labels to uniform 175px width; autostart: add 12s delay

All rows in the Input tab (labeled, key_field, url_field, sound_field) now share
a single LW=175 label width, eliminating the jagged left edge on input fields.
Added X-GNOME-Autostart-Delay=12 to the .desktop entry so gnome-shell, AT-SPI,
and the input stack are fully initialised before Blitztext connects — fixes the
session crash on first login after installation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-08 20:00:33 +02:00
parent a472ce62ba
commit b0b7a8c95e
2 changed files with 42 additions and 31 deletions

View File

@ -50,6 +50,7 @@ def set_enabled(enabled: bool) -> None:
f"Exec={_exec_command()}\n" f"Exec={_exec_command()}\n"
"Icon=blitztext\n" "Icon=blitztext\n"
"Terminal=false\n" "Terminal=false\n"
"X-GNOME-Autostart-enabled=true\n", "X-GNOME-Autostart-enabled=true\n"
"X-GNOME-Autostart-Delay=12\n",
encoding="utf-8", encoding="utf-8",
) )

View File

@ -252,9 +252,9 @@ def _fill_combo(combo: ModelPicker, options, current: str) -> None:
def _url_field(parent: Gtk.Box, label: str, placeholder: str, on_reload, def _url_field(parent: Gtk.Box, label: str, placeholder: str, on_reload,
dot: Gtk.Label | None = None) -> Gtk.Entry: dot: Gtk.Label | None = None, width: int = 130) -> Gtk.Entry:
row = Gtk.Box(spacing=10); row.set_margin_top(3); row.set_margin_bottom(3) row = Gtk.Box(spacing=10); row.set_margin_top(3); row.set_margin_bottom(3)
lbl = Gtk.Label(label=label, xalign=0.0); lbl.set_size_request(130, -1) lbl = Gtk.Label(label=label, xalign=0.0); lbl.set_size_request(width, -1)
row.pack_start(lbl, False, False, 0) row.pack_start(lbl, False, False, 0)
if dot is not None: if dot is not None:
row.pack_start(dot, False, False, 0) # connection dot beside the field (left), like Engines row.pack_start(dot, False, False, 0) # connection dot beside the field (left), like Engines
@ -845,54 +845,61 @@ class SettingsDialog:
"modifiers mode: hold Ctrl and the Windows key to talk, then press " "modifiers mode: hold Ctrl and the Windows key to talk, then press "
"Ctrl to stop and paste. Below you can tune the noise filter, set up " "Ctrl to stop and paste. Below you can tune the noise filter, set up "
"hands-free wakeword, and pick sounds that confirm start and stop.") "hands-free wakeword, and pick sounds that confirm start and stop.")
LW = 175 # uniform label width for all rows in this tab
self.in_mode = _labeled(page, "Input mode", _combo(["modifiers", "hotkeys"], self.cfg.input_mode), self.in_mode = _labeled(page, "Input mode", _combo(["modifiers", "hotkeys"], self.cfg.input_mode),
tooltip="modifiers: hold/press the keys below. hotkeys: each preset has its own shortcut combo.") width=LW,
tooltip="modifiers: hold/press the keys below. hotkeys: each preset has its own shortcut combo.")
self.in_ptt = Gtk.Switch(); self.in_ptt.set_active(self.cfg.push_to_talk); self.in_ptt.set_halign(Gtk.Align.START) self.in_ptt = Gtk.Switch(); self.in_ptt.set_active(self.cfg.push_to_talk); self.in_ptt.set_halign(Gtk.Align.START)
_labeled(page, "Push-to-talk", self.in_ptt) _labeled(page, "Push-to-talk", self.in_ptt, width=LW)
self.in_start = self._key_field(page, "Start", self.cfg.key_start) self.in_start = self._key_field(page, "Start", self.cfg.key_start, width=LW)
self.in_stop = self._key_field(page, "Stop + paste", self.cfg.key_stop) self.in_stop = self._key_field(page, "Stop + paste", self.cfg.key_stop, width=LW)
self.in_send = self._key_field(page, "Stop + paste + Enter", self.cfg.key_send) self.in_send = self._key_field(page, "Stop + paste + Enter", self.cfg.key_send, width=LW)
self.in_cancel = self._key_field(page, "Cancel", self.cfg.key_cancel) self.in_cancel = self._key_field(page, "Cancel", self.cfg.key_cancel, width=LW)
page.pack_start(Gtk.Separator(), False, False, 8) page.pack_start(Gtk.Separator(), False, False, 8)
page.pack_start(Gtk.Label(label="Quality gate", xalign=0.0), False, False, 2) page.pack_start(Gtk.Label(label="Quality gate", xalign=0.0), False, False, 2)
self.q_min = _labeled(page, "Min seconds", _entry(self.cfg.min_speech_seconds), tooltip="Minimum audio length. Shorter clips are ignored.") self.q_min = _labeled(page, "Min seconds", _entry(self.cfg.min_speech_seconds), width=LW,
self.q_rms = _labeled(page, "Silence RMS", _entry(self.cfg.silence_rms), tooltip="Microphone volume threshold to ignore background noise.") tooltip="Minimum audio length. Shorter clips are ignored.")
self.q_rms = _labeled(page, "Silence RMS", _entry(self.cfg.silence_rms), width=LW,
tooltip="Microphone volume threshold to ignore background noise.")
self.q_halluc = Gtk.Switch(); self.q_halluc.set_active(self.cfg.reject_hallucinations); self.q_halluc.set_halign(Gtk.Align.START) self.q_halluc = Gtk.Switch(); self.q_halluc.set_active(self.cfg.reject_hallucinations); self.q_halluc.set_halign(Gtk.Align.START)
_labeled(page, "Reject hallucinations", self.q_halluc, tooltip="Automatically drop STT ghost outputs like 'Thank you.' or 'Bye.'") _labeled(page, "Reject hallucinations", self.q_halluc, width=LW,
tooltip="Automatically drop STT ghost outputs like Thank you. or Bye.")
self.q_strip = Gtk.Switch(); self.q_strip.set_active(self.cfg.strip_trailing_punctuation); self.q_strip.set_halign(Gtk.Align.START) self.q_strip = Gtk.Switch(); self.q_strip.set_active(self.cfg.strip_trailing_punctuation); self.q_strip.set_halign(Gtk.Align.START)
_labeled(page, "Strip trailing punctuation", self.q_strip, tooltip="Remove ending periods from pasted text, useful for code inserts.") _labeled(page, "Strip trailing punctuation", self.q_strip, width=LW,
tooltip="Remove ending periods from pasted text, useful for code inserts.")
page.pack_start(Gtk.Separator(), False, False, 8) page.pack_start(Gtk.Separator(), False, False, 8)
page.pack_start(Gtk.Label(label="Hands-free (Wakeword)", xalign=0.0), False, False, 2) page.pack_start(Gtk.Label(label="Hands-free (Wakeword)", xalign=0.0), False, False, 2)
self.ww_enabled = Gtk.Switch(); self.ww_enabled.set_active(self.cfg.wakeword_enabled); self.ww_enabled.set_halign(Gtk.Align.START) self.ww_enabled = Gtk.Switch(); self.ww_enabled.set_active(self.cfg.wakeword_enabled); self.ww_enabled.set_halign(Gtk.Align.START)
_labeled(page, "Enable wakeword", self.ww_enabled) _labeled(page, "Enable wakeword", self.ww_enabled, width=LW)
self.ww_dot = Gtk.Label(); self.ww_dot.set_markup(_dot(GREY)) self.ww_dot = Gtk.Label(); self.ww_dot.set_markup(_dot(GREY))
self.ww_dot.set_tooltip_text("Wakeword (Wyoming / openWakeWord) server: " self.ww_dot.set_tooltip_text("Wakeword (Wyoming / openWakeWord) server: "
"green = reachable, red = unreachable") "green = reachable, red = unreachable")
self.ww_uri = _url_field(page, "Wakeword engine", "tcp://127.0.0.1:10400", self.ww_uri = _url_field(page, "Wakeword engine", "tcp://127.0.0.1:10400",
self._ww_load, dot=self.ww_dot) self._ww_load, dot=self.ww_dot, width=LW)
self.ww_uri.set_text(self.cfg.wakeword_uri) self.ww_uri.set_text(self.cfg.wakeword_uri)
self.ww_uri.connect("focus-out-event", self._on_ww_uri_leave) self.ww_uri.connect("focus-out-event", self._on_ww_uri_leave)
self._probe_dot(self.ww_dot, self.cfg.wakeword_uri, 10400) self._probe_dot(self.ww_dot, self.cfg.wakeword_uri, 10400)
self.ww_model = _labeled(page, "Model name", _model_combo("Search models…")) self.ww_model = _labeled(page, "Model name", _model_combo("Search models…"), width=LW)
_fill_combo(self.ww_model, [], self.cfg.wakeword_model) _fill_combo(self.ww_model, [], self.cfg.wakeword_model)
self.ww_mic_level = Gtk.LevelBar(); self.ww_mic_level.set_min_value(0); self.ww_mic_level.set_max_value(1) self.ww_mic_level = Gtk.LevelBar(); self.ww_mic_level.set_min_value(0); self.ww_mic_level.set_max_value(1)
_labeled(page, "Input level", self.ww_mic_level) _labeled(page, "Input level", self.ww_mic_level, width=LW)
self.ww_test_btn = Gtk.Button(label="Test Wakeword"); self.ww_test_btn.set_halign(Gtk.Align.START) self.ww_test_btn = Gtk.Button(label="Test Wakeword"); self.ww_test_btn.set_halign(Gtk.Align.START)
self.ww_test_btn.connect("clicked", self._ww_test) self.ww_test_btn.connect("clicked", self._ww_test)
self.ww_test_lbl = Gtk.Label(label=""); self.ww_test_lbl.set_xalign(0.0) self.ww_test_lbl = Gtk.Label(label=""); self.ww_test_lbl.set_xalign(0.0)
box = Gtk.Box(spacing=10); box.pack_start(self.ww_test_btn, False, False, 0); box.pack_start(self.ww_test_lbl, False, False, 0) box = Gtk.Box(spacing=10); box.pack_start(self.ww_test_btn, False, False, 0); box.pack_start(self.ww_test_lbl, False, False, 0)
_labeled(page, "", box) _labeled(page, "", box, width=LW)
self.ww_silence = _labeled(page, "Silence to stop (s)", _entry(self.cfg.wakeword_silence_seconds), self.ww_silence = _labeled(page, "Silence to stop (s)", _entry(self.cfg.wakeword_silence_seconds), width=LW,
tooltip="After the wakeword starts recording, end it this many seconds " tooltip="After the wakeword starts recording, end it this many seconds "
"after you stop speaking. Hands-free auto-stop — the wakeword " "after you stop speaking. Hands-free auto-stop — the wakeword "
"can't be released like a key. Default 2.0.") "cant be released like a key. Default 2.0.")
self.cancel_keywords = _labeled( self.cancel_keywords = _labeled(
page, "Cancel words (comma)", page, "Cancel words (comma)",
_entry(", ".join(self.cfg.cancel_keywords), placeholder="abbrechen, cancel"), _entry(", ".join(self.cfg.cancel_keywords), placeholder="abbrechen, cancel"),
width=LW,
tooltip="Say one of these at the start or end of a clip to DISCARD it — " tooltip="Say one of these at the start or end of a clip to DISCARD it — "
"nothing is transcribed onward, routed, rewritten, or typed. " "nothing is transcribed onward, routed, rewritten, or typed. "
"Rescues an accidentally triggered dictation. Empty = off.") "Rescues an accidentally triggered dictation. Empty = off.")
@ -900,9 +907,10 @@ class SettingsDialog:
self.send_keywords = _labeled( self.send_keywords = _labeled(
page, "Send words (comma)", page, "Send words (comma)",
_entry(", ".join(self.cfg.send_keywords), placeholder="computer send, computer abschicken"), _entry(", ".join(self.cfg.send_keywords), placeholder="computer send, computer abschicken"),
width=LW,
tooltip="Say one of these at the start or end of a clip to SEND it: the word is " tooltip="Say one of these at the start or end of a clip to SEND it: the word is "
"stripped and the rest is typed AND submitted with Enter (spoken " "stripped and the rest is typed AND submitted with Enter (spoken "
"stop+paste+Enter). Because it presses Enter, use a distinctive " "stop+paste+Enter). Because it presses Enter, use a distinctive "
"multi-word phrase (e.g. your wakeword + send). Empty = off.") "multi-word phrase (e.g. your wakeword + send). Empty = off.")
self.ww_snd_detected = self._sound_field( self.ww_snd_detected = self._sound_field(
@ -910,35 +918,37 @@ class SettingsDialog:
"HANDS-FREE ONLY. Plays the instant the wake word is recognised and recording starts " "HANDS-FREE ONLY. Plays the instant the wake word is recognised and recording starts "
"— your speak now cue. (Keyboard/hotkey dictation ignores this and uses Play before.)", "— your speak now cue. (Keyboard/hotkey dictation ignores this and uses Play before.)",
empty_note="Leave empty for no sound. These wakeword cues are independent of the " empty_note="Leave empty for no sound. These wakeword cues are independent of the "
"Play audio cues switch below.", "Play audio cues switch below.",
clear_tip="Clear — no sound") clear_tip="Clear — no sound", width=LW)
self.ww_snd_done = self._sound_field( self.ww_snd_done = self._sound_field(
page, "Sound: captured", self.cfg.wakeword_sound_done, page, "Sound: captured", self.cfg.wakeword_sound_done,
"HANDS-FREE ONLY. Plays when your spoken command is captured and recording stops " "HANDS-FREE ONLY. Plays when your spoken command is captured and recording stops "
"(on silence or stop). (Keyboard/hotkey dictation ignores this and uses Play after.)", "(on silence or stop). (Keyboard/hotkey dictation ignores this and uses Play after.)",
empty_note="Leave empty for no sound.", empty_note="Leave empty for no sound.",
clear_tip="Clear — no sound") clear_tip="Clear — no sound", width=LW)
page.pack_start(Gtk.Separator(), False, False, 8) page.pack_start(Gtk.Separator(), False, False, 8)
page.pack_start(Gtk.Label(label="Audio cues (manual dictation)", xalign=0.0), False, False, 2) page.pack_start(Gtk.Label(label="Audio cues (manual dictation)", xalign=0.0), False, False, 2)
self.snd_enabled = Gtk.Switch(); self.snd_enabled.set_active(self.cfg.sounds_enabled); self.snd_enabled.set_halign(Gtk.Align.START) self.snd_enabled = Gtk.Switch(); self.snd_enabled.set_active(self.cfg.sounds_enabled); self.snd_enabled.set_halign(Gtk.Align.START)
_labeled(page, "Play audio cues", self.snd_enabled, _labeled(page, "Play audio cues", self.snd_enabled, width=LW,
tooltip="On/off for the MANUAL start/stop chimes below (keyboard/hotkey dictation). " tooltip="On/off for the MANUAL start/stop chimes below (keyboard/hotkey dictation). "
"The hands-free wakeword sounds above are separate and always play when set.") "The hands-free wakeword sounds above are separate and always play when set.")
self.snd_before = self._sound_field( self.snd_before = self._sound_field(
page, "Play before", self.cfg.sound_before, page, "Play before", self.cfg.sound_before,
"MANUAL (keyboard/hotkey) dictation only. Plays when recording starts. " "MANUAL (keyboard/hotkey) dictation only. Plays when recording starts. "
"(Hands-free sessions use Sound: detected instead.)") "(Hands-free sessions use Sound: detected instead.)", width=LW)
self.snd_after = self._sound_field( self.snd_after = self._sound_field(
page, "Play after", self.cfg.sound_after, page, "Play after", self.cfg.sound_after,
"MANUAL (keyboard/hotkey) dictation only. Plays when recording stops " "MANUAL (keyboard/hotkey) dictation only. Plays when recording stops "
"(paste, paste+Enter, or auto-stop on silence). (Hands-free uses Sound: captured instead.)") "(paste, paste+Enter, or auto-stop on silence). (Hands-free uses Sound: captured instead.)",
width=LW)
def _sound_field(self, page: Gtk.Box, label: str, value: str, tooltip: str = "", def _sound_field(self, page: Gtk.Box, label: str, value: str, tooltip: str = "",
empty_note: str = "Leave empty to use the built-in system sound.", empty_note: str = "Leave empty to use the built-in system sound.",
clear_tip: str = "Clear — use the built-in system sound") -> Gtk.FileChooserButton: clear_tip: str = "Clear — use the built-in system sound",
width: int = 150) -> Gtk.FileChooserButton:
row = Gtk.Box(spacing=10); row.set_margin_top(3); row.set_margin_bottom(3) row = Gtk.Box(spacing=10); row.set_margin_top(3); row.set_margin_bottom(3)
lbl = Gtk.Label(label=label, xalign=0.0); lbl.set_size_request(150, -1) lbl = Gtk.Label(label=label, xalign=0.0); lbl.set_size_request(width, -1)
if tooltip: if tooltip:
lbl.set_tooltip_text(tooltip) lbl.set_tooltip_text(tooltip)
row.pack_start(lbl, False, False, 0) row.pack_start(lbl, False, False, 0)