Remove settings window
This commit is contained in:
parent
b6c0fc0793
commit
f826617ed4
4 changed files with 4 additions and 583 deletions
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
- Install deps: `uv sync`.
|
||||
- Run daemon: `uv run python3 src/leld.py --config ~/.config/lel/config.json`.
|
||||
- Open settings: `uv run python3 src/leld.py --settings --config ~/.config/lel/config.json`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
|
|
|
|||
|
|
@ -27,12 +27,6 @@ Run:
|
|||
uv run python3 src/leld.py --config ~/.config/lel/config.json
|
||||
```
|
||||
|
||||
Open settings:
|
||||
|
||||
```bash
|
||||
uv run python3 src/leld.py --settings --config ~/.config/lel/config.json
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
Create `~/.config/lel/config.json`:
|
||||
|
|
@ -63,7 +57,7 @@ Env overrides:
|
|||
- `LEL_AI_CLEANUP_BASE_URL`, `LEL_AI_CLEANUP_API_KEY`
|
||||
|
||||
Recording input can be a device index (preferred) or a substring of the device
|
||||
name. Use the settings window to list available input devices.
|
||||
name.
|
||||
|
||||
## systemd user service
|
||||
|
||||
|
|
|
|||
27
src/leld.py
27
src/leld.py
|
|
@ -17,7 +17,6 @@ from context import I3Provider
|
|||
from inject import inject
|
||||
from x11_hotkey import listen
|
||||
from tray import run_tray
|
||||
from settings_window import open_settings_window
|
||||
|
||||
|
||||
class State:
|
||||
|
|
@ -230,7 +229,6 @@ def main():
|
|||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--config", default="", help="path to config.json")
|
||||
parser.add_argument("--no-tray", action="store_true", help="disable tray icon")
|
||||
parser.add_argument("--settings", action="store_true", help="open settings window and exit")
|
||||
parser.add_argument("--dry-run", action="store_true", help="log hotkey only")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
@ -238,21 +236,12 @@ def main():
|
|||
cfg = load(args.config)
|
||||
config_path = Path(args.config) if args.config else Path.home() / ".config" / "lel" / "config.json"
|
||||
|
||||
if args.settings:
|
||||
open_settings_window(cfg, config_path)
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # type: ignore[import-not-found]
|
||||
Gtk.main()
|
||||
return
|
||||
|
||||
_lock_single_instance()
|
||||
|
||||
logging.info("ready (hotkey: %s)", cfg.daemon.get("hotkey", ""))
|
||||
logging.info("config (%s):\n%s", args.config or str(Path.home() / ".config" / "lel" / "config.json"), json.dumps(redacted_dict(cfg), indent=2))
|
||||
|
||||
daemon = Daemon(cfg)
|
||||
suppress_hotkeys = threading.Event()
|
||||
|
||||
def on_quit():
|
||||
os._exit(0)
|
||||
|
|
@ -271,28 +260,18 @@ def main():
|
|||
if args.no_tray:
|
||||
listen(
|
||||
cfg.daemon.get("hotkey", ""),
|
||||
lambda: logging.info("hotkey pressed (dry-run)")
|
||||
if args.dry_run
|
||||
else (None if suppress_hotkeys.is_set() else daemon.toggle()),
|
||||
lambda: logging.info("hotkey pressed (dry-run)") if args.dry_run else daemon.toggle(),
|
||||
)
|
||||
return
|
||||
|
||||
threading.Thread(
|
||||
target=lambda: listen(
|
||||
cfg.daemon.get("hotkey", ""),
|
||||
lambda: logging.info("hotkey pressed (dry-run)")
|
||||
if args.dry_run
|
||||
else (None if suppress_hotkeys.is_set() else daemon.toggle()),
|
||||
lambda: logging.info("hotkey pressed (dry-run)") if args.dry_run else daemon.toggle(),
|
||||
),
|
||||
daemon=True,
|
||||
).start()
|
||||
def open_settings():
|
||||
suppress_hotkeys.set()
|
||||
win = open_settings_window(load(args.config), config_path)
|
||||
win.window.connect("destroy", lambda *_: suppress_hotkeys.clear())
|
||||
return win
|
||||
|
||||
run_tray(daemon.get_state, on_quit, open_settings)
|
||||
run_tray(daemon.get_state, on_quit, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,551 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
|
||||
from gi.repository import Gdk, Gtk # type: ignore[import-not-found]
|
||||
|
||||
from config import Config, validate
|
||||
from recorder import default_input_device, list_input_devices, resolve_input_device
|
||||
from aiprocess import list_models
|
||||
|
||||
|
||||
class SettingsWindow:
|
||||
def __init__(self, cfg: Config, config_path: Path):
|
||||
self.cfg = cfg
|
||||
self.config_path = config_path
|
||||
self._model_cache: dict[str, list[str]] = {}
|
||||
self.window = Gtk.Window(title="lel settings")
|
||||
self.window.set_default_size(920, 700)
|
||||
self.window.set_position(Gtk.WindowPosition.CENTER)
|
||||
self.window.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
|
||||
self.error_label = Gtk.Label()
|
||||
self.error_label.set_xalign(0.0)
|
||||
self.error_label.get_style_context().add_class("error")
|
||||
|
||||
self.notebook = Gtk.Notebook()
|
||||
self.widgets: dict[str, Gtk.Widget] = {}
|
||||
|
||||
self._build_tabs()
|
||||
|
||||
btn_save = Gtk.Button(label="Save")
|
||||
btn_save.connect("clicked", self._on_save)
|
||||
btn_cancel = Gtk.Button(label="Cancel")
|
||||
btn_cancel.connect("clicked", lambda *_: self.window.destroy())
|
||||
|
||||
btn_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
btn_row.pack_end(btn_save, False, False, 0)
|
||||
btn_row.pack_end(btn_cancel, False, False, 0)
|
||||
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
vbox.set_border_width(12)
|
||||
vbox.pack_start(self.error_label, False, False, 0)
|
||||
vbox.pack_start(self.notebook, True, True, 0)
|
||||
vbox.pack_start(btn_row, False, False, 0)
|
||||
|
||||
self.window.add(vbox)
|
||||
self.window.show_all()
|
||||
|
||||
def _on_quick_run(self, *_args):
|
||||
buf = self.quick_text.get_buffer()
|
||||
start, end = buf.get_bounds()
|
||||
text = buf.get_text(start, end, True).strip()
|
||||
if not text:
|
||||
self.widgets["quick_status"].set_text("No input text")
|
||||
return
|
||||
language = "en"
|
||||
output = text
|
||||
steps = self._collect_quick_steps()
|
||||
if not steps:
|
||||
self.widgets["quick_status"].set_text("No AI steps")
|
||||
return
|
||||
from aiprocess import AIConfig, build_processor
|
||||
|
||||
for idx, step in enumerate(steps, 1):
|
||||
prompt_text = step.get("prompt_text") or ""
|
||||
if prompt_text:
|
||||
from aiprocess import GenericAPIProcessor
|
||||
|
||||
processor = GenericAPIProcessor(
|
||||
AIConfig(
|
||||
model=step["model"],
|
||||
temperature=step["temperature"],
|
||||
system_prompt_file="",
|
||||
base_url=step["base_url"],
|
||||
api_key=step["api_key"],
|
||||
timeout_sec=25,
|
||||
language_hint=language,
|
||||
)
|
||||
)
|
||||
processor.system = prompt_text
|
||||
else:
|
||||
processor = build_processor(
|
||||
AIConfig(
|
||||
model=step["model"],
|
||||
temperature=step["temperature"],
|
||||
system_prompt_file="",
|
||||
base_url=step["base_url"],
|
||||
api_key=step["api_key"],
|
||||
timeout_sec=25,
|
||||
language_hint=language,
|
||||
)
|
||||
)
|
||||
output = processor.process(output)
|
||||
self.widgets["quick_status"].set_text("Done")
|
||||
|
||||
def _collect_quick_steps(self) -> list[dict]:
|
||||
steps: list[dict] = []
|
||||
for row in self.quick_steps.get_children():
|
||||
e = row._lel_step_entries
|
||||
model = e["model_entry"].get_text().strip()
|
||||
combo = e["model_combo"]
|
||||
if combo.get_visible():
|
||||
combo_text = combo.get_active_text()
|
||||
if combo_text:
|
||||
model = combo_text
|
||||
prompt_buf = e["prompt_text"].get_buffer()
|
||||
start, end = prompt_buf.get_bounds()
|
||||
prompt_text = prompt_buf.get_text(start, end, True).strip()
|
||||
steps.append(
|
||||
{
|
||||
"model": model or self.cfg.ai_cleanup.get("model", ""),
|
||||
"temperature": float(e["temperature"].get_value()),
|
||||
"prompt_text": prompt_text,
|
||||
"base_url": e["base_url"].get_text().strip() or self.cfg.ai_cleanup.get("base_url", ""),
|
||||
"api_key": e["api_key"].get_text().strip() or self.cfg.ai_cleanup.get("api_key", ""),
|
||||
"timeout": 25,
|
||||
}
|
||||
)
|
||||
return steps
|
||||
|
||||
def _build_tabs(self):
|
||||
self._add_tab("Settings", self._build_settings_tab())
|
||||
self._add_tab("Quick Run", self._build_quick_run_tab())
|
||||
|
||||
def _add_tab(self, title: str, widget: Gtk.Widget):
|
||||
label = Gtk.Label(label=title)
|
||||
self.notebook.append_page(widget, label)
|
||||
|
||||
def _grid(self) -> Gtk.Grid:
|
||||
grid = Gtk.Grid()
|
||||
grid.set_row_spacing(8)
|
||||
grid.set_column_spacing(12)
|
||||
grid.set_margin_top(8)
|
||||
grid.set_margin_bottom(8)
|
||||
grid.set_margin_start(8)
|
||||
grid.set_margin_end(8)
|
||||
return grid
|
||||
|
||||
def _entry(self, value: str) -> Gtk.Entry:
|
||||
entry = Gtk.Entry()
|
||||
entry.set_text(value or "")
|
||||
return entry
|
||||
|
||||
def _spin(self, value: int, min_val: int, max_val: int) -> Gtk.SpinButton:
|
||||
adj = Gtk.Adjustment(value=value, lower=min_val, upper=max_val, step_increment=1, page_increment=10)
|
||||
spin = Gtk.SpinButton(adjustment=adj, climb_rate=1, digits=0)
|
||||
return spin
|
||||
|
||||
def _float_spin(self, value: float, min_val: float, max_val: float, step: float) -> Gtk.SpinButton:
|
||||
adj = Gtk.Adjustment(value=value, lower=min_val, upper=max_val, step_increment=step, page_increment=0.1)
|
||||
spin = Gtk.SpinButton(adjustment=adj, climb_rate=0.1, digits=2)
|
||||
return spin
|
||||
|
||||
def _combo(self, options: list[str], value: str) -> Gtk.ComboBoxText:
|
||||
combo = Gtk.ComboBoxText()
|
||||
for opt in options:
|
||||
combo.append_text(opt)
|
||||
combo.set_active(options.index(value) if value in options else 0)
|
||||
return combo
|
||||
|
||||
def _row(self, grid: Gtk.Grid, row: int, label: str, widget: Gtk.Widget):
|
||||
lbl = Gtk.Label(label=label)
|
||||
lbl.set_xalign(0.0)
|
||||
grid.attach(lbl, 0, row, 1, 1)
|
||||
grid.attach(widget, 1, row, 1, 1)
|
||||
|
||||
def _build_hotkeys_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
grid = self._grid()
|
||||
current_hotkey = self.cfg.daemon.get("hotkey", "")
|
||||
self.widgets["hotkey_value"] = Gtk.Label(label=current_hotkey or "(not set)")
|
||||
self.widgets["hotkey_value"].set_xalign(0.0)
|
||||
set_btn = Gtk.Button(label="Set Hotkey")
|
||||
set_btn.connect("clicked", lambda *_: self._capture_hotkey())
|
||||
hotkey_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
hotkey_row.pack_start(self.widgets["hotkey_value"], True, True, 0)
|
||||
hotkey_row.pack_start(set_btn, False, False, 0)
|
||||
self._row(grid, 0, "Hotkey", hotkey_row)
|
||||
box.pack_start(grid, False, False, 0)
|
||||
return box
|
||||
|
||||
def _build_recording_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
grid = self._grid()
|
||||
self.widgets["input_device"] = Gtk.ComboBoxText()
|
||||
self._populate_mic_sources()
|
||||
refresh_btn = Gtk.Button(label="Refresh")
|
||||
refresh_btn.connect("clicked", lambda *_: self._populate_mic_sources())
|
||||
mic_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
mic_row.pack_start(self.widgets["input_device"], True, True, 0)
|
||||
mic_row.pack_start(refresh_btn, False, False, 0)
|
||||
self._row(grid, 0, "Input Device", mic_row)
|
||||
# Record timeout is fixed (300s); no UI control.
|
||||
box.pack_start(grid, False, False, 0)
|
||||
return box
|
||||
|
||||
def _selected_mic_source(self) -> str:
|
||||
combo = self.widgets["input_device"]
|
||||
text = (combo.get_active_text() or "").strip()
|
||||
if ":" in text:
|
||||
prefix = text.split(":", 1)[0]
|
||||
if prefix.isdigit():
|
||||
return prefix
|
||||
return self.cfg.recording.get("input", "")
|
||||
|
||||
def _populate_mic_sources(self):
|
||||
combo: Gtk.ComboBoxText = self.widgets["input_device"]
|
||||
combo.remove_all()
|
||||
devices = list_input_devices()
|
||||
selected_spec = self.cfg.recording.get("input") or ""
|
||||
selected_device = resolve_input_device(selected_spec)
|
||||
default_device = default_input_device()
|
||||
selected_index = 0
|
||||
for idx, device in enumerate(devices):
|
||||
label = f"{device['index']}: {device['name']}"
|
||||
combo.append_text(label)
|
||||
if selected_device is not None and device["index"] == selected_device:
|
||||
selected_index = idx
|
||||
elif selected_device is None and default_device is not None and device["index"] == default_device:
|
||||
selected_index = idx
|
||||
if devices:
|
||||
combo.set_active(selected_index)
|
||||
else:
|
||||
combo.append_text("default (system)")
|
||||
combo.set_active(0)
|
||||
|
||||
def _build_stt_tab(self) -> Gtk.Widget:
|
||||
grid = self._grid()
|
||||
models = ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3"]
|
||||
model_value = self.cfg.transcribing.get("model", "base")
|
||||
self.widgets["whisper_model"] = self._combo(models, model_value if model_value in models else "base")
|
||||
device_value = (self.cfg.transcribing.get("device", "cpu") or "cpu").lower()
|
||||
self.widgets["whisper_device"] = self._combo(["cpu", "cuda"], device_value if device_value in {"cpu", "cuda"} else "cpu")
|
||||
self._row(grid, 0, "Model", self.widgets["whisper_model"])
|
||||
self._row(grid, 1, "Device", self.widgets["whisper_device"])
|
||||
return grid
|
||||
|
||||
def _build_injection_tab(self) -> Gtk.Widget:
|
||||
grid = self._grid()
|
||||
backend_value = self.cfg.injection.get("backend", "clipboard")
|
||||
self.widgets["injection_backend"] = self._combo(["clipboard", "injection"], backend_value)
|
||||
self._row(grid, 0, "Injection Backend", self.widgets["injection_backend"])
|
||||
return grid
|
||||
|
||||
def _build_ai_tab(self) -> Gtk.Widget:
|
||||
grid = self._grid()
|
||||
self.widgets["ai_enabled"] = Gtk.CheckButton()
|
||||
self.widgets["ai_enabled"].set_active(self.cfg.ai_cleanup.get("enabled", False))
|
||||
self.widgets["ai_model"] = self._entry(self.cfg.ai_cleanup.get("model", ""))
|
||||
self.widgets["ai_temperature"] = self._float_spin(float(self.cfg.ai_cleanup.get("temperature", 0.0)), 0.0, 2.0, 0.05)
|
||||
self.widgets["ai_base_url"] = self._entry(self.cfg.ai_cleanup.get("base_url", ""))
|
||||
self.widgets["ai_api_key"] = self._entry(self.cfg.ai_cleanup.get("api_key", ""))
|
||||
self.widgets["ai_api_key"].set_visibility(False)
|
||||
self._row(grid, 0, "AI Enabled", self.widgets["ai_enabled"])
|
||||
self._row(grid, 1, "AI Cleanup Model", self.widgets["ai_model"])
|
||||
self._row(grid, 2, "AI Cleanup Temperature", self.widgets["ai_temperature"])
|
||||
self._row(grid, 3, "AI Cleanup Base URL", self.widgets["ai_base_url"])
|
||||
self._row(grid, 4, "AI Cleanup API Key", self.widgets["ai_api_key"])
|
||||
# AI timeout is fixed (25s); no UI control.
|
||||
return grid
|
||||
|
||||
def _build_quick_run_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
header = Gtk.Label(label="Bypass recording and run from text")
|
||||
header.set_xalign(0.0)
|
||||
box.pack_start(header, False, False, 0)
|
||||
|
||||
self.quick_text = Gtk.TextView()
|
||||
self.quick_text.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
scroll.add(self.quick_text)
|
||||
scroll.set_size_request(600, 140)
|
||||
box.pack_start(scroll, True, True, 0)
|
||||
|
||||
opts = self._grid()
|
||||
self.widgets["quick_language"] = self._entry("en")
|
||||
self.widgets["quick_language"].set_editable(False)
|
||||
self._row(opts, 0, "Language Hint", self.widgets["quick_language"])
|
||||
box.pack_start(opts, False, False, 0)
|
||||
|
||||
steps_label = Gtk.Label(label="AI Steps (run in order)")
|
||||
steps_label.set_xalign(0.0)
|
||||
box.pack_start(steps_label, False, False, 0)
|
||||
|
||||
self.quick_steps = Gtk.ListBox()
|
||||
self.quick_steps.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
self.quick_steps.set_can_focus(False)
|
||||
self._add_quick_step_row(
|
||||
{
|
||||
"model": self.cfg.ai_cleanup.get("model", ""),
|
||||
"temperature": self.cfg.ai_cleanup.get("temperature", 0.0),
|
||||
"base_url": self.cfg.ai_cleanup.get("base_url", ""),
|
||||
"api_key": self.cfg.ai_cleanup.get("api_key", ""),
|
||||
"timeout": 25,
|
||||
}
|
||||
)
|
||||
box.pack_start(self.quick_steps, False, False, 0)
|
||||
|
||||
step_actions = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
add_btn = Gtk.Button(label="Add Step")
|
||||
add_btn.connect("clicked", lambda *_: self._add_quick_step_row({}))
|
||||
step_actions.pack_start(add_btn, False, False, 0)
|
||||
box.pack_start(step_actions, False, False, 0)
|
||||
|
||||
action = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
run_btn = Gtk.Button(label="Run")
|
||||
run_btn.connect("clicked", self._on_quick_run)
|
||||
action.pack_start(run_btn, False, False, 0)
|
||||
self.widgets["quick_status"] = Gtk.Label(label="")
|
||||
self.widgets["quick_status"].set_xalign(0.0)
|
||||
action.pack_start(self.widgets["quick_status"], True, True, 0)
|
||||
box.pack_start(action, False, False, 0)
|
||||
return box
|
||||
|
||||
def _add_quick_step_row(self, step: dict):
|
||||
row = Gtk.ListBoxRow()
|
||||
row.set_activatable(False)
|
||||
row.set_selectable(False)
|
||||
row.set_can_focus(False)
|
||||
content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
content.set_can_focus(False)
|
||||
grid = self._grid()
|
||||
model_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
model_combo = Gtk.ComboBoxText()
|
||||
model_entry = self._entry(step.get("model", self.cfg.ai_cleanup.get("model", "")))
|
||||
model_box.pack_start(model_combo, True, True, 0)
|
||||
model_box.pack_start(model_entry, True, True, 0)
|
||||
temperature = self._float_spin(step.get("temperature", self.cfg.ai_cleanup.get("temperature", 0.0)), 0.0, 2.0, 0.05)
|
||||
prompt_text = Gtk.TextView()
|
||||
prompt_text.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||
prompt_buf = prompt_text.get_buffer()
|
||||
prompt_buf.set_text(step.get("prompt_text", ""))
|
||||
prompt_scroll = Gtk.ScrolledWindow()
|
||||
prompt_scroll.set_size_request(400, 120)
|
||||
prompt_scroll.add(prompt_text)
|
||||
base_url = self._entry(step.get("base_url", self.cfg.ai_cleanup.get("base_url", "")))
|
||||
api_key = self._entry(step.get("api_key", self.cfg.ai_cleanup.get("api_key", "")))
|
||||
api_key.set_visibility(False)
|
||||
self._row(grid, 0, "AI Model", model_box)
|
||||
self._row(grid, 1, "AI Temperature", temperature)
|
||||
self._row(grid, 2, "AI Prompt", prompt_scroll)
|
||||
self._row(grid, 3, "AI Base URL", base_url)
|
||||
self._row(grid, 4, "AI API Key", api_key)
|
||||
# AI timeout is fixed (25s); no UI control.
|
||||
base_url.connect("changed", lambda *_: self._refresh_models_for_row(row))
|
||||
|
||||
controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
btn_up = Gtk.Button(label="Up")
|
||||
btn_down = Gtk.Button(label="Down")
|
||||
btn_remove = Gtk.Button(label="Remove")
|
||||
btn_up.connect("clicked", lambda *_: self._move_step(row, -1))
|
||||
btn_down.connect("clicked", lambda *_: self._move_step(row, 1))
|
||||
btn_remove.connect("clicked", lambda *_: self.quick_steps.remove(row))
|
||||
controls.pack_start(btn_up, False, False, 0)
|
||||
controls.pack_start(btn_down, False, False, 0)
|
||||
controls.pack_start(btn_remove, False, False, 0)
|
||||
|
||||
content.pack_start(grid, False, False, 0)
|
||||
content.pack_start(controls, False, False, 0)
|
||||
row.add(content)
|
||||
row._lel_step_entries = {
|
||||
"model_combo": model_combo,
|
||||
"model_entry": model_entry,
|
||||
"temperature": temperature,
|
||||
"prompt_text": prompt_text,
|
||||
"base_url": base_url,
|
||||
"api_key": api_key,
|
||||
"timeout": 25,
|
||||
}
|
||||
self._refresh_models_for_row(row)
|
||||
self.quick_steps.add(row)
|
||||
self.quick_steps.show_all()
|
||||
|
||||
def _move_step(self, row: Gtk.Widget, direction: int):
|
||||
children = self.quick_steps.get_children()
|
||||
idx = children.index(row)
|
||||
new_idx = idx + direction
|
||||
if new_idx < 0 or new_idx >= len(children):
|
||||
return
|
||||
self.quick_steps.remove(row)
|
||||
self.quick_steps.insert(row, new_idx)
|
||||
self.quick_steps.show_all()
|
||||
|
||||
|
||||
def _refresh_models_for_row(self, row: Gtk.Widget):
|
||||
e = row._lel_step_entries
|
||||
base_url = e["base_url"].get_text().strip()
|
||||
api_key = e["api_key"].get_text().strip()
|
||||
models = self._get_models(base_url, api_key, 25)
|
||||
combo = e["model_combo"]
|
||||
entry = e["model_entry"]
|
||||
combo.remove_all()
|
||||
if models:
|
||||
for m in models:
|
||||
combo.append_text(m)
|
||||
combo.set_active(0)
|
||||
combo.show()
|
||||
entry.hide()
|
||||
else:
|
||||
combo.hide()
|
||||
entry.show()
|
||||
|
||||
def _get_models(self, base_url: str, api_key: str, timeout: int) -> list[str]:
|
||||
key = f"{base_url}|{api_key}|{timeout}"
|
||||
if key in self._model_cache:
|
||||
return self._model_cache[key]
|
||||
models = list_models(base_url, api_key, timeout)
|
||||
self._model_cache[key] = models
|
||||
return models
|
||||
|
||||
def _on_save(self, *_args):
|
||||
try:
|
||||
cfg = self._collect_config()
|
||||
validate(cfg)
|
||||
self._write_config(cfg)
|
||||
self.window.destroy()
|
||||
except Exception as exc:
|
||||
self._set_error(str(exc))
|
||||
|
||||
def _set_error(self, text: str):
|
||||
self.error_label.set_text(text)
|
||||
|
||||
def _collect_config(self) -> Config:
|
||||
cfg = Config()
|
||||
hotkey_text = self.widgets["hotkey_value"].get_text().strip()
|
||||
cfg.daemon["hotkey"] = "" if hotkey_text == "(not set)" else hotkey_text
|
||||
cfg.recording["input"] = self._selected_mic_source()
|
||||
model = self.widgets["whisper_model"].get_active_text()
|
||||
cfg.transcribing["model"] = (model or "base").strip()
|
||||
device = self.widgets["whisper_device"].get_active_text()
|
||||
cfg.transcribing["device"] = (device or "cpu").strip()
|
||||
backend = self.widgets["injection_backend"].get_active_text()
|
||||
cfg.injection["backend"] = (backend or "").strip() or "clipboard"
|
||||
cfg.ai_cleanup["enabled"] = self.widgets["ai_enabled"].get_active()
|
||||
cfg.ai_cleanup["model"] = self.widgets["ai_model"].get_text().strip()
|
||||
cfg.ai_cleanup["temperature"] = float(self.widgets["ai_temperature"].get_value())
|
||||
cfg.ai_cleanup["base_url"] = self.widgets["ai_base_url"].get_text().strip()
|
||||
cfg.ai_cleanup["api_key"] = self.widgets["ai_api_key"].get_text().strip()
|
||||
return cfg
|
||||
|
||||
def _section_label(self, text: str) -> Gtk.Label:
|
||||
label = Gtk.Label(label=text)
|
||||
label.set_xalign(0.0)
|
||||
ctx = label.get_style_context()
|
||||
ctx.add_class("section")
|
||||
return label
|
||||
|
||||
def _capture_hotkey(self):
|
||||
dialog = Gtk.Dialog(title="Set Hotkey", transient_for=self.window, flags=0)
|
||||
dialog.set_modal(True)
|
||||
dialog.set_default_size(360, 120)
|
||||
dialog.set_decorated(True)
|
||||
dialog.set_resizable(False)
|
||||
|
||||
box = dialog.get_content_area()
|
||||
info = Gtk.Label(label="Press desired hotkey…\nEsc cancels, Backspace/Delete clears")
|
||||
info.set_xalign(0.0)
|
||||
status = Gtk.Label(label="")
|
||||
status.set_xalign(0.0)
|
||||
box.pack_start(info, False, False, 8)
|
||||
box.pack_start(status, False, False, 8)
|
||||
|
||||
def on_key(_widget, event):
|
||||
keyval = event.keyval
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
dialog.destroy()
|
||||
return True
|
||||
if keyval in (Gdk.KEY_BackSpace, Gdk.KEY_Delete):
|
||||
self.widgets["hotkey_value"].set_text("(not set)")
|
||||
dialog.destroy()
|
||||
return True
|
||||
|
||||
hotkey = self._format_hotkey(event)
|
||||
if not hotkey:
|
||||
status.set_text("Please include a modifier key.")
|
||||
return True
|
||||
self.widgets["hotkey_value"].set_text(hotkey)
|
||||
dialog.destroy()
|
||||
return True
|
||||
|
||||
dialog.connect("key-press-event", on_key)
|
||||
dialog.show_all()
|
||||
|
||||
def _format_hotkey(self, event) -> str:
|
||||
state = event.state
|
||||
mods = []
|
||||
if state & Gdk.ModifierType.SUPER_MASK:
|
||||
mods.append("Cmd")
|
||||
if state & Gdk.ModifierType.CONTROL_MASK:
|
||||
mods.append("Ctrl")
|
||||
if state & Gdk.ModifierType.MOD1_MASK:
|
||||
mods.append("Alt")
|
||||
if state & Gdk.ModifierType.SHIFT_MASK:
|
||||
mods.append("Shift")
|
||||
if not mods:
|
||||
return ""
|
||||
|
||||
key_name = Gdk.keyval_name(event.keyval) or ""
|
||||
if not key_name:
|
||||
return ""
|
||||
if len(key_name) == 1:
|
||||
key_name = key_name.upper()
|
||||
key_map = {
|
||||
"Return": "Enter",
|
||||
"space": "Space",
|
||||
"Tab": "Tab",
|
||||
"Escape": "Esc",
|
||||
}
|
||||
key_name = key_map.get(key_name, key_name)
|
||||
return "+".join(mods + [key_name])
|
||||
|
||||
def _build_settings_tab(self) -> Gtk.Widget:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
box.set_border_width(8)
|
||||
|
||||
box.pack_start(self._section_label("Daemon"), False, False, 0)
|
||||
box.pack_start(self._build_hotkeys_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("Recording"), False, False, 0)
|
||||
box.pack_start(self._build_recording_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("Transcribing"), False, False, 0)
|
||||
box.pack_start(self._build_stt_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("Injection"), False, False, 0)
|
||||
box.pack_start(self._build_injection_tab(), False, False, 0)
|
||||
|
||||
box.pack_start(self._section_label("AI Cleanup"), False, False, 0)
|
||||
box.pack_start(self._build_ai_tab(), False, False, 0)
|
||||
|
||||
scroll = Gtk.ScrolledWindow()
|
||||
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
scroll.add(box)
|
||||
return scroll
|
||||
|
||||
def _write_config(self, cfg: Config):
|
||||
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = asdict(cfg)
|
||||
self.config_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def open_settings_window(cfg: Config, config_path: Path):
|
||||
return SettingsWindow(cfg, config_path)
|
||||
Loading…
Add table
Add a link
Reference in a new issue