kortalk 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kortalk/__init__.py +7 -0
- kortalk/__main__.py +4 -0
- kortalk/app.py +436 -0
- kortalk/config.py +265 -0
- kortalk/hotkeys.py +288 -0
- kortalk/i18n.py +136 -0
- kortalk/providers.py +243 -0
- kortalk/settings_dialog.py +487 -0
- kortalk/theme.py +139 -0
- kortalk/windows.py +336 -0
- kortalk-0.4.0.dist-info/METADATA +158 -0
- kortalk-0.4.0.dist-info/RECORD +16 -0
- kortalk-0.4.0.dist-info/WHEEL +5 -0
- kortalk-0.4.0.dist-info/entry_points.txt +2 -0
- kortalk-0.4.0.dist-info/licenses/LICENSE +21 -0
- kortalk-0.4.0.dist-info/top_level.txt +1 -0
kortalk/__init__.py
ADDED
kortalk/__main__.py
ADDED
kortalk/app.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Точка входа kortalk: CLI, single-instance, трей, глобальные хоткеи.
|
|
2
|
+
|
|
3
|
+
Архитектура: `kortalk` без аргументов поднимает резидентное приложение
|
|
4
|
+
(трей + QLocalServer + глобальные хоткеи). Все действия — из трея и по
|
|
5
|
+
хоткеям, назначаемым в настройках. CLI-флаги (--popup и т.п.) оставлены
|
|
6
|
+
для скриптов: они доставляются работающему экземпляру через локальный
|
|
7
|
+
сокет и завершаются мгновенно.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import getpass
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import logging.handlers
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from PySide6.QtCore import QByteArray
|
|
23
|
+
from PySide6.QtGui import QAction, QClipboard, QGuiApplication
|
|
24
|
+
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
|
25
|
+
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
|
|
26
|
+
|
|
27
|
+
from . import __version__, i18n, theme
|
|
28
|
+
from .config import Config
|
|
29
|
+
from .hotkeys import GlobalHotkeys
|
|
30
|
+
from .i18n import tr
|
|
31
|
+
from .providers import shutdown_workers
|
|
32
|
+
from .settings_dialog import SettingsDialog
|
|
33
|
+
from .windows import MainWindow, PopupWindow
|
|
34
|
+
|
|
35
|
+
SOCKET_NAME = f"kortalk-{getpass.getuser()}"
|
|
36
|
+
LOG_DIR = Path(os.environ.get("XDG_STATE_HOME",
|
|
37
|
+
str(Path.home() / ".local" / "state"))) / "kortalk"
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_logging(debug: bool) -> None:
|
|
43
|
+
"""Файл ~/.local/state/kortalk/kortalk.log всегда; stderr — при --debug."""
|
|
44
|
+
root = logging.getLogger("kortalk")
|
|
45
|
+
root.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
46
|
+
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
47
|
+
try:
|
|
48
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
50
|
+
LOG_DIR / "kortalk.log", maxBytes=1_000_000, backupCount=2,
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
53
|
+
file_handler.setFormatter(fmt)
|
|
54
|
+
root.addHandler(file_handler)
|
|
55
|
+
except OSError:
|
|
56
|
+
pass # нет прав/места — работаем без файла
|
|
57
|
+
if debug:
|
|
58
|
+
stream_handler = logging.StreamHandler()
|
|
59
|
+
stream_handler.setFormatter(fmt)
|
|
60
|
+
root.addHandler(stream_handler)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# CLI
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def build_arg_parser() -> argparse.ArgumentParser:
|
|
68
|
+
parser = argparse.ArgumentParser(
|
|
69
|
+
prog="kortalk",
|
|
70
|
+
description="AI popup for selected text. With no arguments starts the tray "
|
|
71
|
+
"daemon; actions via the tray and global hotkeys "
|
|
72
|
+
"(configured in the app).",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument("prompt", nargs="?", default=None,
|
|
75
|
+
help="Prompt for a one-off popup (implies --popup).")
|
|
76
|
+
parser.add_argument("--popup", action="store_true",
|
|
77
|
+
help="Popup near the cursor with a response to the selected text.")
|
|
78
|
+
parser.add_argument("--window", "--split", action="store_true", dest="window",
|
|
79
|
+
help="Two-column window: text on the left, response on the right.")
|
|
80
|
+
parser.add_argument("--settings", action="store_true", help="Open settings.")
|
|
81
|
+
parser.add_argument("--provider", default=None,
|
|
82
|
+
help="Provider ID for this request (see Settings).")
|
|
83
|
+
parser.add_argument("--quit", action="store_true", dest="quit_",
|
|
84
|
+
help="Quit the running instance.")
|
|
85
|
+
parser.add_argument("--check", action="store_true", help="Environment diagnostics.")
|
|
86
|
+
parser.add_argument("--debug", action="store_true", help="Verbose log and full tracebacks.")
|
|
87
|
+
parser.add_argument("--version", action="version", version=f"kortalk {__version__}")
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def args_to_command(args) -> dict:
|
|
92
|
+
if args.quit_:
|
|
93
|
+
action = "quit"
|
|
94
|
+
elif args.settings:
|
|
95
|
+
action = "settings"
|
|
96
|
+
elif args.window:
|
|
97
|
+
action = "window"
|
|
98
|
+
elif args.popup or args.prompt:
|
|
99
|
+
action = "popup"
|
|
100
|
+
else:
|
|
101
|
+
action = "daemon"
|
|
102
|
+
return {"action": action, "prompt": args.prompt, "provider": args.provider}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# --check
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def run_selftest(config: Config) -> int:
|
|
110
|
+
ok = True
|
|
111
|
+
|
|
112
|
+
def report(label: str, passed: bool, remedy: str = "") -> None:
|
|
113
|
+
nonlocal ok
|
|
114
|
+
print(f"{'✅' if passed else '❌'} {label}")
|
|
115
|
+
if not passed:
|
|
116
|
+
ok = False
|
|
117
|
+
if remedy:
|
|
118
|
+
print(f" → {remedy}")
|
|
119
|
+
|
|
120
|
+
report(f"PySide6 (Qt {__import__('PySide6').QtCore.qVersion()})", True)
|
|
121
|
+
|
|
122
|
+
clipboard = QGuiApplication.clipboard()
|
|
123
|
+
report("PRIMARY selection (QClipboard)",
|
|
124
|
+
clipboard.supportsSelection(),
|
|
125
|
+
"the environment does not support PRIMARY selection")
|
|
126
|
+
|
|
127
|
+
report("System tray", QSystemTrayIcon.isSystemTrayAvailable(),
|
|
128
|
+
"tray unavailable — use the CLI flags (--popup, --window)")
|
|
129
|
+
|
|
130
|
+
platform = QGuiApplication.platformName()
|
|
131
|
+
print(f"ℹ️ Platform: {platform} → hotkey backend: "
|
|
132
|
+
+ ("XGrabKey (X11)" if platform == "xcb" else "XDG portal GlobalShortcuts"))
|
|
133
|
+
|
|
134
|
+
for p in config.providers():
|
|
135
|
+
if p.type == "claude-cli":
|
|
136
|
+
report(f"provider “{p.name}”: claude CLI in PATH",
|
|
137
|
+
shutil.which("claude") is not None,
|
|
138
|
+
"install Claude Code CLI: https://docs.claude.com")
|
|
139
|
+
elif p.type == "anthropic":
|
|
140
|
+
report(f"provider “{p.name}”: API key set", bool(p.api_key),
|
|
141
|
+
"add the key: Settings → Providers")
|
|
142
|
+
elif p.type == "openai":
|
|
143
|
+
key_ok = bool(p.api_key) or not p.needs_api_key()
|
|
144
|
+
report(f"provider “{p.name}”: base URL and key",
|
|
145
|
+
bool(p.base_url) and key_ok,
|
|
146
|
+
"fill in the fields: Settings → Providers")
|
|
147
|
+
|
|
148
|
+
active = config.active_provider()
|
|
149
|
+
print(f"\nActive provider: {active.name} ({active.type}"
|
|
150
|
+
+ (f", {active.model}" if active.model else "") + ")")
|
|
151
|
+
print(f"Hotkeys: popup={config.hotkey('popup') or '—'}, "
|
|
152
|
+
f"window={config.hotkey('window') or '—'}")
|
|
153
|
+
print(f"Settings file: {config.file_path()}")
|
|
154
|
+
print("All good." if ok else "Issues found, see the recommendations above.")
|
|
155
|
+
return 0 if ok else 1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Single instance
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def try_send_to_running(command: dict) -> bool:
|
|
163
|
+
"""True, если работающий экземпляр найден и команда доставлена."""
|
|
164
|
+
socket = QLocalSocket()
|
|
165
|
+
socket.connectToServer(SOCKET_NAME)
|
|
166
|
+
if not socket.waitForConnected(300):
|
|
167
|
+
return False
|
|
168
|
+
socket.write(QByteArray(json.dumps(command).encode("utf-8")))
|
|
169
|
+
socket.flush()
|
|
170
|
+
socket.waitForBytesWritten(1000)
|
|
171
|
+
socket.disconnectFromServer()
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class KortalkApp:
|
|
176
|
+
"""Резидентное приложение: трей + сервер команд + хоткеи + окна."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, app: QApplication, config: Config):
|
|
179
|
+
self.app = app
|
|
180
|
+
self.config = config
|
|
181
|
+
self.popup: PopupWindow | None = None
|
|
182
|
+
self.main_window: MainWindow | None = None
|
|
183
|
+
self.settings_dialog: SettingsDialog | None = None
|
|
184
|
+
|
|
185
|
+
theme.apply_theme(app, str(config.get("theme")))
|
|
186
|
+
theme.apply_font(app, str(config.get("font_family")), int(config.get("font_size")))
|
|
187
|
+
app.setQuitOnLastWindowClosed(False)
|
|
188
|
+
|
|
189
|
+
self.server = QLocalServer()
|
|
190
|
+
QLocalServer.removeServer(SOCKET_NAME) # подчистить сокет после падения
|
|
191
|
+
self.server.listen(SOCKET_NAME)
|
|
192
|
+
self.server.newConnection.connect(self._on_connection)
|
|
193
|
+
|
|
194
|
+
# ВАЖНО: меню и трей храним в атрибутах — setContextMenu не берёт
|
|
195
|
+
# владение, и локальный QMenu был бы собран GC (краш по ПКМ).
|
|
196
|
+
self.tray = QSystemTrayIcon(theme.make_tray_icon())
|
|
197
|
+
self.menu = QMenu()
|
|
198
|
+
self.prompt_menu = QMenu(tr("Popup with prompt"))
|
|
199
|
+
self._rebuild_menu()
|
|
200
|
+
self.tray.setContextMenu(self.menu)
|
|
201
|
+
self.tray.activated.connect(self._tray_activated)
|
|
202
|
+
if QSystemTrayIcon.isSystemTrayAvailable():
|
|
203
|
+
self.tray.show()
|
|
204
|
+
|
|
205
|
+
self.hotkeys = GlobalHotkeys()
|
|
206
|
+
self.hotkeys.activated.connect(self._hotkey_activated)
|
|
207
|
+
self._apply_hotkeys()
|
|
208
|
+
|
|
209
|
+
# -- трей ----------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def _rebuild_menu(self) -> None:
|
|
212
|
+
self.menu.clear()
|
|
213
|
+
self.menu.addAction(tr("Popup with selection"), lambda: self.handle({"action": "popup"}))
|
|
214
|
+
|
|
215
|
+
self.prompt_menu.clear()
|
|
216
|
+
active_name = self.config.active_prompt().name
|
|
217
|
+
for p in self.config.prompts():
|
|
218
|
+
title = f"● {p.name}" if p.name == active_name else p.name
|
|
219
|
+
action = QAction(title, self.prompt_menu)
|
|
220
|
+
action.triggered.connect(
|
|
221
|
+
lambda _checked=False, name=p.name: self.handle(
|
|
222
|
+
{"action": "popup", "prompt_name": name})
|
|
223
|
+
)
|
|
224
|
+
self.prompt_menu.addAction(action)
|
|
225
|
+
self.prompt_menu.setTitle(tr("Popup with prompt"))
|
|
226
|
+
self.menu.addMenu(self.prompt_menu)
|
|
227
|
+
|
|
228
|
+
self.menu.addAction(tr("Open window"), lambda: self.handle({"action": "window"}))
|
|
229
|
+
self.menu.addSeparator()
|
|
230
|
+
self.menu.addAction(tr("Settings"), self.open_settings)
|
|
231
|
+
self.menu.addSeparator()
|
|
232
|
+
self.menu.addAction(tr("Quit"), self.quit)
|
|
233
|
+
self._update_tooltip()
|
|
234
|
+
|
|
235
|
+
def _update_tooltip(self) -> None:
|
|
236
|
+
provider = self.config.active_provider()
|
|
237
|
+
lines = [f"kortalk — {provider.name}"]
|
|
238
|
+
if self.config.hotkey("popup"):
|
|
239
|
+
lines.append(f"Popup: {self.config.hotkey('popup')}")
|
|
240
|
+
if self.hotkeys_note():
|
|
241
|
+
lines.append(self.hotkeys_note())
|
|
242
|
+
self.tray.setToolTip("\n".join(lines))
|
|
243
|
+
|
|
244
|
+
def hotkeys_note(self) -> str:
|
|
245
|
+
if getattr(self, "hotkeys", None) and self.hotkeys.backend == "none" and self.hotkeys.error:
|
|
246
|
+
return tr("Hotkeys unavailable: {error}").format(error=self.hotkeys.error)
|
|
247
|
+
return ""
|
|
248
|
+
|
|
249
|
+
def _apply_hotkeys(self) -> None:
|
|
250
|
+
self.hotkeys.apply({
|
|
251
|
+
"popup": self.config.hotkey("popup"),
|
|
252
|
+
"window": self.config.hotkey("window"),
|
|
253
|
+
})
|
|
254
|
+
self._update_tooltip()
|
|
255
|
+
|
|
256
|
+
def _hotkey_activated(self, action: str) -> None:
|
|
257
|
+
self.handle({"action": action})
|
|
258
|
+
|
|
259
|
+
def _tray_activated(self, reason) -> None:
|
|
260
|
+
if reason == QSystemTrayIcon.ActivationReason.Trigger: # ЛКМ
|
|
261
|
+
self.handle({"action": "popup"})
|
|
262
|
+
|
|
263
|
+
# -- команды -----------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
def handle(self, command: dict) -> None:
|
|
266
|
+
action = command.get("action", "daemon")
|
|
267
|
+
log.debug("command: %s", command)
|
|
268
|
+
if action == "quit":
|
|
269
|
+
self.quit()
|
|
270
|
+
elif action == "settings":
|
|
271
|
+
self.open_settings()
|
|
272
|
+
elif action == "window":
|
|
273
|
+
self.open_window(command)
|
|
274
|
+
elif action == "popup":
|
|
275
|
+
self.open_popup(command)
|
|
276
|
+
# прочее ("daemon", "noop") — просто живём дальше
|
|
277
|
+
|
|
278
|
+
def _selection_text(self) -> str:
|
|
279
|
+
clipboard = QGuiApplication.clipboard()
|
|
280
|
+
text = ""
|
|
281
|
+
if clipboard.supportsSelection():
|
|
282
|
+
text = clipboard.text(QClipboard.Mode.Selection)
|
|
283
|
+
return text.strip()
|
|
284
|
+
|
|
285
|
+
def _resolve(self, command: dict):
|
|
286
|
+
provider = None
|
|
287
|
+
if command.get("provider"):
|
|
288
|
+
provider = self.config.provider(command["provider"])
|
|
289
|
+
if provider is None:
|
|
290
|
+
provider = self.config.active_provider()
|
|
291
|
+
|
|
292
|
+
if command.get("prompt"):
|
|
293
|
+
prompt = command["prompt"]
|
|
294
|
+
elif command.get("prompt_name"):
|
|
295
|
+
named = self.config.prompt_by_name(command["prompt_name"])
|
|
296
|
+
prompt = named.text if named else self.config.active_prompt().text
|
|
297
|
+
else:
|
|
298
|
+
prompt = self.config.active_prompt().text
|
|
299
|
+
|
|
300
|
+
selection = self._selection_text()
|
|
301
|
+
full = f"{prompt}\n\n{selection}" if selection else prompt
|
|
302
|
+
return provider, full, selection
|
|
303
|
+
|
|
304
|
+
def open_popup(self, command: dict) -> None:
|
|
305
|
+
provider, full_prompt, _selection = self._resolve(command)
|
|
306
|
+
if self.popup is not None:
|
|
307
|
+
self.popup.close()
|
|
308
|
+
label = provider.name + (f" · {provider.model}" if provider.model else "")
|
|
309
|
+
popup = PopupWindow(self.config, label)
|
|
310
|
+
popup.open_in_window.connect(self._popup_to_window)
|
|
311
|
+
# отложенный destroyed старого popup не должен обнулять ссылку на новый
|
|
312
|
+
popup.destroyed.connect(lambda *_a, p=popup: self._popup_destroyed(p))
|
|
313
|
+
self.popup = popup
|
|
314
|
+
popup.show_near_cursor()
|
|
315
|
+
popup.ask(provider, full_prompt)
|
|
316
|
+
|
|
317
|
+
def _popup_destroyed(self, popup: PopupWindow) -> None:
|
|
318
|
+
if self.popup is popup:
|
|
319
|
+
self.popup = None
|
|
320
|
+
|
|
321
|
+
def open_window(self, command: dict) -> None:
|
|
322
|
+
_provider, full_prompt, selection = self._resolve(command)
|
|
323
|
+
window = self._ensure_main_window()
|
|
324
|
+
if selection or command.get("prompt"):
|
|
325
|
+
window.set_input(full_prompt)
|
|
326
|
+
window.show()
|
|
327
|
+
window.raise_()
|
|
328
|
+
window.activateWindow()
|
|
329
|
+
|
|
330
|
+
def _popup_to_window(self, answer: str) -> None:
|
|
331
|
+
window = self._ensure_main_window()
|
|
332
|
+
window.set_output(answer)
|
|
333
|
+
window.show()
|
|
334
|
+
window.raise_()
|
|
335
|
+
window.activateWindow()
|
|
336
|
+
|
|
337
|
+
def _ensure_main_window(self) -> MainWindow:
|
|
338
|
+
if self.main_window is None:
|
|
339
|
+
self.main_window = MainWindow(self.config)
|
|
340
|
+
self.main_window.settings_requested.connect(self.open_settings)
|
|
341
|
+
return self.main_window
|
|
342
|
+
|
|
343
|
+
def open_settings(self) -> None:
|
|
344
|
+
if self.settings_dialog is not None:
|
|
345
|
+
self.settings_dialog.raise_()
|
|
346
|
+
self.settings_dialog.activateWindow()
|
|
347
|
+
return
|
|
348
|
+
self.settings_dialog = SettingsDialog(self.config)
|
|
349
|
+
self.settings_dialog.saved.connect(self._settings_saved)
|
|
350
|
+
self.settings_dialog.finished.connect(
|
|
351
|
+
lambda _r: setattr(self, "settings_dialog", None)
|
|
352
|
+
)
|
|
353
|
+
self.settings_dialog.show()
|
|
354
|
+
|
|
355
|
+
def _settings_saved(self) -> None:
|
|
356
|
+
i18n.set_language(str(self.config.get("language")))
|
|
357
|
+
theme.apply_theme(self.app, str(self.config.get("theme")))
|
|
358
|
+
theme.apply_font(self.app, str(self.config.get("font_family")),
|
|
359
|
+
int(self.config.get("font_size")))
|
|
360
|
+
self.tray.setIcon(theme.make_tray_icon())
|
|
361
|
+
self._rebuild_menu()
|
|
362
|
+
self._apply_hotkeys()
|
|
363
|
+
if self.main_window is not None:
|
|
364
|
+
self.main_window.reload_providers()
|
|
365
|
+
|
|
366
|
+
def quit(self) -> None:
|
|
367
|
+
self.hotkeys.stop()
|
|
368
|
+
shutdown_workers()
|
|
369
|
+
self.server.close()
|
|
370
|
+
QLocalServer.removeServer(SOCKET_NAME)
|
|
371
|
+
self.app.quit()
|
|
372
|
+
|
|
373
|
+
# -- IPC -----------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
def _on_connection(self) -> None:
|
|
376
|
+
socket = self.server.nextPendingConnection()
|
|
377
|
+
if socket is None:
|
|
378
|
+
return
|
|
379
|
+
socket.readyRead.connect(lambda: self._read_command(socket))
|
|
380
|
+
|
|
381
|
+
def _read_command(self, socket) -> None:
|
|
382
|
+
data = bytes(socket.readAll()).decode("utf-8", errors="replace")
|
|
383
|
+
socket.disconnectFromServer()
|
|
384
|
+
try:
|
|
385
|
+
command = json.loads(data)
|
|
386
|
+
except json.JSONDecodeError:
|
|
387
|
+
command = {"action": "noop"}
|
|
388
|
+
self.handle(command)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
# main
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
def main(argv: list[str] | None = None) -> int:
|
|
396
|
+
parser = build_arg_parser()
|
|
397
|
+
args = parser.parse_args(argv)
|
|
398
|
+
command = args_to_command(args)
|
|
399
|
+
setup_logging(args.debug)
|
|
400
|
+
|
|
401
|
+
if not args.check:
|
|
402
|
+
if try_send_to_running(command):
|
|
403
|
+
if command["action"] == "daemon":
|
|
404
|
+
print("kortalk is already running.", file=sys.stderr)
|
|
405
|
+
return 0
|
|
406
|
+
if args.quit_:
|
|
407
|
+
print("kortalk is not running.", file=sys.stderr)
|
|
408
|
+
return 1
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
app = QApplication(sys.argv[:1])
|
|
412
|
+
app.setApplicationName("kortalk")
|
|
413
|
+
config = Config()
|
|
414
|
+
i18n.set_language(str(config.get("language")))
|
|
415
|
+
|
|
416
|
+
if args.check:
|
|
417
|
+
return run_selftest(config)
|
|
418
|
+
|
|
419
|
+
kortalk = KortalkApp(app, config)
|
|
420
|
+
log.info("kortalk %s started: platform=%s, hotkeys=%s",
|
|
421
|
+
__version__, QGuiApplication.platformName(), kortalk.hotkeys.backend)
|
|
422
|
+
if command["action"] != "daemon":
|
|
423
|
+
kortalk.handle(command)
|
|
424
|
+
code = app.exec()
|
|
425
|
+
log.info("kortalk exited (code %d)", code)
|
|
426
|
+
return code
|
|
427
|
+
except Exception as exc: # noqa: BLE001
|
|
428
|
+
log.exception("unhandled error")
|
|
429
|
+
if args.debug:
|
|
430
|
+
raise
|
|
431
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
432
|
+
return 1
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if __name__ == "__main__":
|
|
436
|
+
sys.exit(main())
|