Add settings history and quick AI chain

This commit is contained in:
Thales Maciel 2026-02-09 13:45:07 -03:00
parent 328dcec458
commit a0c3b02ab1
13 changed files with 1627 additions and 23 deletions

View file

@ -11,6 +11,7 @@ def _parse_bool(val: str) -> bool:
@dataclass
class Config:
hotkey: str = "Cmd+m"
edit_hotkey: str = "Cmd+n"
ffmpeg_input: str = "pulse:default"
ffmpeg_path: str = ""
@ -19,8 +20,10 @@ class Config:
whisper_device: str = "cpu"
record_timeout_sec: int = 120
edit_record_timeout_sec: int = 120
injection_backend: str = "clipboard"
edit_injection_backend: str = "clipboard"
ai_enabled: bool = False
ai_model: str = "llama3.2:3b"
@ -29,10 +32,22 @@ class Config:
ai_base_url: str = "http://localhost:11434/v1/chat/completions"
ai_api_key: str = ""
ai_timeout_sec: int = 20
edit_ai_enabled: bool = True
edit_ai_temperature: float = 0.0
edit_ai_system_prompt_file: str = ""
edit_window: dict = field(default_factory=lambda: {"width": 800, "height": 400})
context_capture: dict = field(default_factory=lambda: {"provider": "i3ipc", "on_focus_change": "abort"})
context_rules: list[dict] = field(default_factory=list)
languages: dict = field(
default_factory=lambda: {
"en": {"code": "en", "hotkey": "Cmd+m", "label": "English"},
"ptBR": {"code": "pt-BR", "hotkey": "Cmd+b", "label": "Português (Brasil)"},
}
)
edit_language_detection: dict = field(default_factory=lambda: {"enabled": True, "provider": "langdetect", "fallback_code": "en"})
def default_path() -> Path:
return Path.home() / ".config" / "lel" / "config.json"
@ -66,10 +81,16 @@ def load(path: str | None) -> Config:
cfg.ffmpeg_path = os.environ["LEL_FFMPEG_PATH"]
if os.getenv("LEL_RECORD_TIMEOUT_SEC"):
cfg.record_timeout_sec = int(os.environ["LEL_RECORD_TIMEOUT_SEC"])
if os.getenv("LEL_EDIT_RECORD_TIMEOUT_SEC"):
cfg.edit_record_timeout_sec = int(os.environ["LEL_EDIT_RECORD_TIMEOUT_SEC"])
if os.getenv("LEL_HOTKEY"):
cfg.hotkey = os.environ["LEL_HOTKEY"]
if os.getenv("LEL_EDIT_HOTKEY"):
cfg.edit_hotkey = os.environ["LEL_EDIT_HOTKEY"]
if os.getenv("LEL_INJECTION_BACKEND"):
cfg.injection_backend = os.environ["LEL_INJECTION_BACKEND"]
if os.getenv("LEL_EDIT_INJECTION_BACKEND"):
cfg.edit_injection_backend = os.environ["LEL_EDIT_INJECTION_BACKEND"]
if os.getenv("LEL_AI_ENABLED"):
cfg.ai_enabled = _parse_bool(os.environ["LEL_AI_ENABLED"])
@ -85,22 +106,24 @@ def load(path: str | None) -> Config:
cfg.ai_api_key = os.environ["LEL_AI_API_KEY"]
if os.getenv("LEL_AI_TIMEOUT_SEC"):
cfg.ai_timeout_sec = int(os.environ["LEL_AI_TIMEOUT_SEC"])
if os.getenv("LEL_EDIT_AI_ENABLED"):
cfg.edit_ai_enabled = _parse_bool(os.environ["LEL_EDIT_AI_ENABLED"])
if os.getenv("LEL_EDIT_AI_TEMPERATURE"):
cfg.edit_ai_temperature = float(os.environ["LEL_EDIT_AI_TEMPERATURE"])
if os.getenv("LEL_EDIT_AI_SYSTEM_PROMPT_FILE"):
cfg.edit_ai_system_prompt_file = os.environ["LEL_EDIT_AI_SYSTEM_PROMPT_FILE"]
if os.getenv("LEL_LANGUAGES_JSON"):
cfg.languages = json.loads(os.environ["LEL_LANGUAGES_JSON"])
if os.getenv("LEL_EDIT_LANG_FALLBACK"):
cfg.edit_language_detection["fallback_code"] = os.environ["LEL_EDIT_LANG_FALLBACK"]
if os.getenv("LEL_CONTEXT_PROVIDER"):
cfg.context_capture["provider"] = os.environ["LEL_CONTEXT_PROVIDER"]
if os.getenv("LEL_CONTEXT_ON_FOCUS_CHANGE"):
cfg.context_capture["on_focus_change"] = os.environ["LEL_CONTEXT_ON_FOCUS_CHANGE"]
if not cfg.hotkey:
raise ValueError("hotkey cannot be empty")
if cfg.record_timeout_sec <= 0:
raise ValueError("record_timeout_sec must be > 0")
if cfg.context_capture.get("provider") not in {"i3ipc"}:
raise ValueError("context_capture.provider must be i3ipc")
if cfg.context_capture.get("on_focus_change") not in {"abort"}:
raise ValueError("context_capture.on_focus_change must be abort")
if not isinstance(cfg.context_rules, list):
cfg.context_rules = []
validate(cfg)
return cfg
@ -108,3 +131,39 @@ def redacted_dict(cfg: Config) -> dict:
d = cfg.__dict__.copy()
d["ai_api_key"] = ""
return d
def validate(cfg: Config) -> None:
if not cfg.hotkey:
raise ValueError("hotkey cannot be empty")
if not cfg.edit_hotkey:
raise ValueError("edit_hotkey cannot be empty")
if cfg.record_timeout_sec <= 0:
raise ValueError("record_timeout_sec must be > 0")
if cfg.edit_record_timeout_sec <= 0:
raise ValueError("edit_record_timeout_sec must be > 0")
if cfg.context_capture.get("provider") not in {"i3ipc"}:
raise ValueError("context_capture.provider must be i3ipc")
if cfg.context_capture.get("on_focus_change") not in {"abort"}:
raise ValueError("context_capture.on_focus_change must be abort")
if not isinstance(cfg.context_rules, list):
cfg.context_rules = []
if not isinstance(cfg.edit_window, dict):
cfg.edit_window = {"width": 800, "height": 400}
if not isinstance(cfg.languages, dict) or not cfg.languages:
raise ValueError("languages must be a non-empty map")
seen_hotkeys = set()
for name, info in cfg.languages.items():
if not isinstance(info, dict):
raise ValueError(f"languages[{name}] must be an object")
code = info.get("code")
hotkey = info.get("hotkey")
if not code or not hotkey:
raise ValueError(f"languages[{name}] must include code and hotkey")
if hotkey in seen_hotkeys:
raise ValueError(f"duplicate hotkey in languages: {hotkey}")
seen_hotkeys.add(hotkey)
if not isinstance(cfg.edit_language_detection, dict):
cfg.edit_language_detection = {"enabled": True, "provider": "langdetect", "fallback_code": "en"}
if cfg.edit_language_detection.get("provider") not in {"langdetect"}:
raise ValueError("edit_language_detection.provider must be langdetect")