Обновил Linux-автозапуск для tray-версии.
Что изменено - Linux-настройки управляют автозапуском через XDG autostart. - Приложение читает фактическое состояние .desktop-файла при старте и при повторном открытии настроек. - Запись автозапуска сделана атомарной. - Если запись отсутствует, устарела или указывает не на текущий бинарник, при сохранении она пересоздается. - Для Linux добавлено логирование ключевых действий и проблем автозапуска.
This commit is contained in:
parent
77723d875f
commit
06f5345ec7
7 changed files with 360 additions and 26 deletions
|
|
@ -121,6 +121,14 @@ chmod +x TgWsProxy_linux_amd64
|
|||
|
||||
При первом запуске откроется окно с инструкцией. Приложение работает в системном трее (требуется AppIndicator).
|
||||
|
||||
В Linux-настройках доступен обычный tray-автозапуск:
|
||||
|
||||
- **Автозапуск tray при входе в сессию** - создаёт `~/.config/autostart/tg-ws-proxy.desktop` и подходит для GUI-режима
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> XDG-автозапуск tray поддерживается только для упакованной Linux-сборки (`.deb` / `TgWsProxy_linux_amd64`).
|
||||
|
||||
## Установка из исходников
|
||||
|
||||
### Консольный proxy
|
||||
|
|
@ -272,11 +280,12 @@ Tray-приложение хранит данные в:
|
|||
"cfproxy": true,
|
||||
"cfproxy_priority": true,
|
||||
"cfproxy_user_domain": "",
|
||||
"appearance": "auto"
|
||||
"appearance": "auto",
|
||||
"linux_gui_autostart": false
|
||||
}
|
||||
```
|
||||
|
||||
Ключ **`check_updates`** — при `true` при запросе к GitHub сравнивается версия с последним релизом (только уведомление и ссылка на страницу загрузки). На Windows в конфиге может быть **`autostart`** (автозапуск при входе в систему).
|
||||
Ключ **`check_updates`** - при `true` при запросе к GitHub сравнивается версия с последним релизом (только уведомление и ссылка на страницу загрузки). На Windows в конфиге может быть **`autostart`** (автозапуск при входе в систему). На Linux используется **`linux_gui_autostart`**, а фактическое состояние дополнительно синхронизируется с XDG autostart.
|
||||
|
||||
## Автоматическая сборка
|
||||
|
||||
|
|
|
|||
55
linux.py
55
linux.py
|
|
@ -14,6 +14,11 @@ from PIL import Image, ImageTk
|
|||
|
||||
from proxy import get_link_host
|
||||
|
||||
from utils.linux_integration import (
|
||||
linux_autostart_capabilities,
|
||||
read_linux_autostart_state,
|
||||
sync_linux_autostart,
|
||||
)
|
||||
from utils.tray_common import (
|
||||
APP_NAME, DEFAULT_CONFIG, FIRST_RUN_MARKER, LOG_FILE,
|
||||
acquire_lock, bootstrap, check_ipv6_warning, ctk_run_dialog,
|
||||
|
|
@ -22,6 +27,7 @@ from utils.tray_common import (
|
|||
save_config, start_proxy, stop_proxy, tg_proxy_url,
|
||||
)
|
||||
from ui.ctk_tray_ui import (
|
||||
AutostartSectionConfig,
|
||||
install_tray_config_buttons, install_tray_config_form,
|
||||
populate_first_run_window, tray_settings_scroll_and_footer,
|
||||
validate_config_form,
|
||||
|
|
@ -53,7 +59,7 @@ def _msgbox(kind: str, text: str, title: str, **kw):
|
|||
return result
|
||||
|
||||
|
||||
def _show_error(text: str, title: str = "TG WS Proxy — Ошибка") -> None:
|
||||
def _show_error(text: str, title: str = "TG WS Proxy - Ошибка") -> None:
|
||||
_msgbox("showerror", text, title)
|
||||
|
||||
|
||||
|
|
@ -142,19 +148,36 @@ def _edit_config_dialog() -> None:
|
|||
_show_error("customtkinter не установлен.")
|
||||
return
|
||||
|
||||
cfg = dict(_config)
|
||||
capabilities = linux_autostart_capabilities()
|
||||
cfg = read_linux_autostart_state(_config)
|
||||
|
||||
def _build(done: threading.Event) -> None:
|
||||
theme = ctk_theme_for_platform()
|
||||
w, h = CONFIG_DIALOG_SIZE
|
||||
root = create_ctk_toplevel(
|
||||
ctk, title="TG WS Proxy — Настройки", width=w, height=h, theme=theme,
|
||||
ctk, title="TG WS Proxy - Настройки", width=w, height=h, theme=theme,
|
||||
after_create=_apply_window_icon,
|
||||
)
|
||||
fpx, fpy = CONFIG_DIALOG_FRAME_PAD
|
||||
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
|
||||
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
||||
widgets = install_tray_config_form(ctk, scroll, theme, cfg, DEFAULT_CONFIG, show_autostart=False)
|
||||
widgets = install_tray_config_form(
|
||||
ctk,
|
||||
scroll,
|
||||
theme,
|
||||
cfg,
|
||||
DEFAULT_CONFIG,
|
||||
autostart_section=AutostartSectionConfig(
|
||||
title="Автозапуск",
|
||||
linux_gui_autostart=capabilities.gui_autostart_supported,
|
||||
linux_gui_reason=(
|
||||
"Создаёт XDG desktop entry в ~/.config/autostart."
|
||||
if capabilities.gui_autostart_supported
|
||||
else capabilities.gui_autostart_reason
|
||||
),
|
||||
),
|
||||
linux_gui_autostart_value=cfg.get("linux_gui_autostart", False),
|
||||
)
|
||||
|
||||
def _finish() -> None:
|
||||
root.destroy()
|
||||
|
|
@ -162,15 +185,35 @@ def _edit_config_dialog() -> None:
|
|||
|
||||
def on_save() -> None:
|
||||
from tkinter import messagebox
|
||||
merged = validate_config_form(widgets, DEFAULT_CONFIG, include_autostart=False)
|
||||
merged = validate_config_form(
|
||||
widgets,
|
||||
DEFAULT_CONFIG,
|
||||
include_autostart=False,
|
||||
include_linux_gui_autostart=capabilities.gui_autostart_supported,
|
||||
)
|
||||
if isinstance(merged, str):
|
||||
messagebox.showerror("TG WS Proxy — Ошибка", merged, parent=root)
|
||||
messagebox.showerror("TG WS Proxy - Ошибка", merged, parent=root)
|
||||
return
|
||||
save_config(merged)
|
||||
_config.update(merged)
|
||||
sync_errors = sync_linux_autostart(
|
||||
gui_enabled=bool(merged.get("linux_gui_autostart", False)),
|
||||
)
|
||||
log.info("Config saved: %s", merged)
|
||||
_tray_icon.menu = _build_menu()
|
||||
|
||||
if sync_errors:
|
||||
parts = []
|
||||
for scope, text in sync_errors:
|
||||
label = "XDG autostart" if scope == "gui" else scope
|
||||
parts.append(f"{label}: {text}")
|
||||
messagebox.showwarning(
|
||||
"TG WS Proxy - предупреждение",
|
||||
"Настройки сохранены, но автозапуск обновился не полностью:\n\n"
|
||||
+ "\n".join(parts),
|
||||
parent=root,
|
||||
)
|
||||
|
||||
do_restart = messagebox.askyesno(
|
||||
"Перезапустить?",
|
||||
"Настройки сохранены.\n\nПерезапустить прокси сейчас?",
|
||||
|
|
|
|||
11
packaging/linux/tg-ws-proxy.desktop
Normal file
11
packaging/linux/tg-ws-proxy.desktop
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=TG WS Proxy
|
||||
GenericName=Telegram Proxy
|
||||
Comment=Telegram Desktop WebSocket Bridge Proxy
|
||||
Exec=tg-ws-proxy
|
||||
Icon=tg-ws-proxy
|
||||
Terminal=false
|
||||
Categories=Network;
|
||||
StartupNotify=true
|
||||
Keywords=telegram;proxy;websocket;
|
||||
|
|
@ -299,6 +299,16 @@ class TrayConfigFormWidgets:
|
|||
cfproxy_priority_var: Optional[Any] = None
|
||||
cfproxy_user_domain_var: Optional[Any] = None
|
||||
appearance_var: Optional[Any] = None
|
||||
linux_gui_autostart_var: Optional[Any] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AutostartSectionConfig:
|
||||
title: str
|
||||
hint: Optional[str] = None
|
||||
windows_autostart: bool = False
|
||||
linux_gui_autostart: bool = False
|
||||
linux_gui_reason: Optional[str] = None
|
||||
|
||||
|
||||
def install_tray_config_form(
|
||||
|
|
@ -308,8 +318,9 @@ def install_tray_config_form(
|
|||
cfg: dict,
|
||||
default_config: dict,
|
||||
*,
|
||||
show_autostart: bool = False,
|
||||
autostart_section: Optional[AutostartSectionConfig] = None,
|
||||
autostart_value: bool = False,
|
||||
linux_gui_autostart_value: bool = False,
|
||||
) -> TrayConfigFormWidgets:
|
||||
header = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
header.pack(fill="x", pady=(0, 2))
|
||||
|
|
@ -571,18 +582,57 @@ def install_tray_config_form(
|
|||
).pack(anchor="w")
|
||||
|
||||
autostart_var = None
|
||||
if show_autostart:
|
||||
sys_inner = _config_section(ctk, frame, theme, "Запуск Windows", bottom_spacer=4)
|
||||
autostart_var = ctk.BooleanVar(value=autostart_value)
|
||||
as_cb = _checkbox(ctk, sys_inner, theme, "Автозапуск при включении компьютера", autostart_var)
|
||||
as_cb.pack(anchor="w", pady=(0, 4))
|
||||
as_hint = _label(
|
||||
ctk, sys_inner, theme,
|
||||
"Если переместить программу в другую папку, запись автозапуска может сброситься.",
|
||||
size=11, justify="left", wraplength=_INNER_W,
|
||||
)
|
||||
as_hint.pack(anchor="w")
|
||||
attach_tooltip_to_widgets([as_cb, as_hint], _TIP_AUTOSTART)
|
||||
linux_gui_autostart_var = None
|
||||
if autostart_section is not None:
|
||||
sys_inner = _config_section(ctk, frame, theme, autostart_section.title, bottom_spacer=4)
|
||||
added_rows = False
|
||||
|
||||
if autostart_section.hint:
|
||||
_label(
|
||||
ctk, sys_inner, theme, autostart_section.hint,
|
||||
size=11, justify="left", wraplength=_INNER_W,
|
||||
).pack(anchor="w", pady=(0, 6))
|
||||
|
||||
if autostart_section.windows_autostart:
|
||||
autostart_var = ctk.BooleanVar(value=autostart_value)
|
||||
as_cb = _checkbox(ctk, sys_inner, theme, "Автозапуск при включении компьютера", autostart_var)
|
||||
as_cb.pack(anchor="w", pady=(0, 4))
|
||||
as_hint = _label(
|
||||
ctk, sys_inner, theme,
|
||||
"Если переместить программу в другую папку, запись автозапуска может сброситься.",
|
||||
size=11, justify="left", wraplength=_INNER_W,
|
||||
)
|
||||
as_hint.pack(anchor="w", pady=(0, 6))
|
||||
attach_tooltip_to_widgets([as_cb, as_hint], _TIP_AUTOSTART)
|
||||
added_rows = True
|
||||
|
||||
if autostart_section.linux_gui_autostart:
|
||||
linux_gui_autostart_var = ctk.BooleanVar(value=linux_gui_autostart_value)
|
||||
gui_cb = _checkbox(ctk, sys_inner, theme, "Запускать tray при входе в графическую сессию", linux_gui_autostart_var)
|
||||
gui_cb.pack(anchor="w", pady=(0, 4))
|
||||
gui_hint = autostart_section.linux_gui_reason or "Создаёт XDG desktop entry в ~/.config/autostart."
|
||||
gui_lbl = _label(
|
||||
ctk, sys_inner, theme, gui_hint,
|
||||
size=11, justify="left", wraplength=_INNER_W,
|
||||
)
|
||||
gui_lbl.pack(anchor="w", pady=(0, 6))
|
||||
attach_tooltip_to_widgets([gui_cb, gui_lbl], gui_hint)
|
||||
added_rows = True
|
||||
|
||||
if not added_rows:
|
||||
unavailable = []
|
||||
if autostart_section.linux_gui_reason:
|
||||
unavailable.append(f"Tray autostart: {autostart_section.linux_gui_reason}")
|
||||
if unavailable:
|
||||
_label(
|
||||
ctk,
|
||||
sys_inner,
|
||||
theme,
|
||||
"\n".join(unavailable),
|
||||
size=11,
|
||||
justify="left",
|
||||
wraplength=_INNER_W,
|
||||
).pack(anchor="w")
|
||||
|
||||
return TrayConfigFormWidgets(
|
||||
host_var=host_var, port_var=port_var, secret_var=secret_var,
|
||||
|
|
@ -593,6 +643,7 @@ def install_tray_config_form(
|
|||
cfproxy_priority_var=cfproxy_priority_var,
|
||||
cfproxy_user_domain_var=cfproxy_user_domain_var,
|
||||
appearance_var=appearance_var,
|
||||
linux_gui_autostart_var=linux_gui_autostart_var,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -618,6 +669,7 @@ def validate_config_form(
|
|||
default_config: dict,
|
||||
*,
|
||||
include_autostart: bool,
|
||||
include_linux_gui_autostart: bool = False,
|
||||
) -> Union[dict, str]:
|
||||
import socket as _sock
|
||||
|
||||
|
|
@ -665,6 +717,12 @@ def validate_config_form(
|
|||
if widgets.autostart_var is not None
|
||||
else False
|
||||
)
|
||||
if include_linux_gui_autostart:
|
||||
new_cfg["linux_gui_autostart"] = (
|
||||
widgets.linux_gui_autostart_var.get()
|
||||
if widgets.linux_gui_autostart_var is not None
|
||||
else False
|
||||
)
|
||||
|
||||
merge_adv_from_form(widgets, new_cfg, default_config)
|
||||
if widgets.check_updates_var is not None:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
"""
|
||||
Общие значения по умолчанию для tray-приложений (Windows / Linux / macOS).
|
||||
Единственное отличие по платформе — ключ autostart только на Windows.
|
||||
"""
|
||||
"""Общие значения по умолчанию для tray-приложений."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
|
@ -29,5 +26,7 @@ def default_tray_config() -> Dict[str, Any]:
|
|||
|
||||
if sys.platform == "win32":
|
||||
cfg["autostart"] = False
|
||||
elif sys.platform.startswith("linux"):
|
||||
cfg["linux_gui_autostart"] = False
|
||||
|
||||
return cfg
|
||||
|
|
|
|||
210
utils/linux_integration.py
Normal file
210
utils/linux_integration.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from configparser import ConfigParser, Error as ConfigError
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from utils.tray_common import APP_NAME
|
||||
|
||||
log = logging.getLogger("tg-ws-tray")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LinuxAutostartCapabilities:
|
||||
gui_autostart_supported: bool
|
||||
gui_autostart_reason: Optional[str]
|
||||
|
||||
|
||||
def _xdg_config_home() -> Path:
|
||||
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
||||
|
||||
|
||||
def _xdg_autostart_dir() -> Path:
|
||||
return _xdg_config_home() / "autostart"
|
||||
|
||||
|
||||
def _xdg_autostart_file() -> Path:
|
||||
return _xdg_autostart_dir() / "tg-ws-proxy.desktop"
|
||||
|
||||
|
||||
def _shell_quote(value: str) -> str:
|
||||
if not value:
|
||||
return '""'
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def _is_frozen_build() -> bool:
|
||||
return bool(getattr(sys, "frozen", False))
|
||||
|
||||
|
||||
def _desktop_exec() -> Optional[str]:
|
||||
if not _is_frozen_build():
|
||||
return None
|
||||
return _shell_quote(sys.executable)
|
||||
|
||||
|
||||
def _desktop_entry_text(exec_cmd: str) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"[Desktop Entry]",
|
||||
"Type=Application",
|
||||
f"Name={APP_NAME}",
|
||||
"GenericName=Telegram Proxy",
|
||||
"Comment=Telegram Desktop WebSocket Bridge Proxy",
|
||||
f"Exec={exec_cmd}",
|
||||
"Icon=tg-ws-proxy",
|
||||
"Terminal=false",
|
||||
"Categories=Network;",
|
||||
"StartupNotify=true",
|
||||
"Hidden=false",
|
||||
"X-GNOME-Autostart-enabled=true",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _read_desktop_entry(path: Path) -> Optional[ConfigParser]:
|
||||
if not path.exists():
|
||||
return None
|
||||
|
||||
parser = ConfigParser(interpolation=None)
|
||||
parser.optionxform = str
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
parser.read_file(handle)
|
||||
except (OSError, ConfigError) as exc:
|
||||
log.warning("Failed to read Linux autostart entry %s: %s", path, exc)
|
||||
return None
|
||||
|
||||
if not parser.has_section("Desktop Entry"):
|
||||
log.warning("Linux autostart entry has no [Desktop Entry] section: %s", path)
|
||||
return None
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _atomic_write_text(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_path: Optional[Path] = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
delete=False,
|
||||
) as handle:
|
||||
handle.write(content)
|
||||
temp_path = Path(handle.name)
|
||||
os.chmod(temp_path, 0o644)
|
||||
os.replace(temp_path, path)
|
||||
finally:
|
||||
if temp_path is not None and temp_path.exists():
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def linux_autostart_capabilities() -> LinuxAutostartCapabilities:
|
||||
gui_exec = _desktop_exec()
|
||||
gui_reason = None if gui_exec else "XDG autostart доступен только в упакованной Linux-сборке."
|
||||
if gui_exec:
|
||||
log.debug("Linux GUI autostart supported, exec=%s", gui_exec)
|
||||
else:
|
||||
log.info("Linux GUI autostart unavailable: %s", gui_reason)
|
||||
return LinuxAutostartCapabilities(
|
||||
gui_autostart_supported=gui_exec is not None,
|
||||
gui_autostart_reason=gui_reason,
|
||||
)
|
||||
|
||||
|
||||
def is_linux_gui_autostart_enabled() -> bool:
|
||||
path = _xdg_autostart_file()
|
||||
exec_cmd = _desktop_exec()
|
||||
if exec_cmd is None:
|
||||
return False
|
||||
|
||||
parser = _read_desktop_entry(path)
|
||||
if parser is None:
|
||||
return False
|
||||
|
||||
section = parser["Desktop Entry"]
|
||||
file_exec = section.get("Exec", "").strip()
|
||||
file_type = section.get("Type", "").strip()
|
||||
hidden = section.get("Hidden", "false").strip().lower()
|
||||
enabled = section.get("X-GNOME-Autostart-enabled", "true").strip().lower()
|
||||
|
||||
if file_type != "Application":
|
||||
log.warning("Linux autostart entry has unexpected Type=%s in %s", file_type, path)
|
||||
return False
|
||||
if hidden in {"1", "true", "yes", "on"}:
|
||||
log.info("Linux autostart entry is marked Hidden=true: %s", path)
|
||||
return False
|
||||
if enabled in {"0", "false", "no", "off"}:
|
||||
log.info("Linux autostart entry is disabled by X-GNOME-Autostart-enabled=false: %s", path)
|
||||
return False
|
||||
if file_exec != exec_cmd:
|
||||
log.warning(
|
||||
"Linux autostart exec mismatch in %s: current=%s expected=%s",
|
||||
path,
|
||||
file_exec,
|
||||
exec_cmd,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def set_linux_gui_autostart_enabled(enabled: bool) -> None:
|
||||
path = _xdg_autostart_file()
|
||||
exec_cmd = _desktop_exec()
|
||||
if exec_cmd is None:
|
||||
raise RuntimeError("XDG autostart поддерживается только для упакованной Linux-сборки.")
|
||||
|
||||
if enabled:
|
||||
content = _desktop_entry_text(exec_cmd)
|
||||
current = None
|
||||
if path.exists():
|
||||
try:
|
||||
current = path.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
log.warning("Failed to read existing Linux autostart entry %s: %s", path, exc)
|
||||
if current == content:
|
||||
log.info("Linux autostart already up to date: %s", path)
|
||||
return
|
||||
_atomic_write_text(path, content)
|
||||
log.info("Linux autostart enabled: %s", path)
|
||||
return
|
||||
|
||||
if path.exists():
|
||||
path.unlink(missing_ok=True)
|
||||
log.info("Linux autostart disabled: %s", path)
|
||||
else:
|
||||
log.info("Linux autostart already disabled: %s", path)
|
||||
|
||||
|
||||
def sync_linux_autostart(
|
||||
*,
|
||||
gui_enabled: bool,
|
||||
) -> List[Tuple[str, str]]:
|
||||
errors: List[Tuple[str, str]] = []
|
||||
log.info("Syncing Linux autostart: gui_enabled=%s", gui_enabled)
|
||||
|
||||
try:
|
||||
set_linux_gui_autostart_enabled(gui_enabled)
|
||||
except Exception as exc:
|
||||
log.error("Failed to sync Linux autostart: %s", exc)
|
||||
errors.append(("gui", str(exc)))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def read_linux_autostart_state(cfg: Dict[str, object]) -> Dict[str, object]:
|
||||
merged = dict(cfg)
|
||||
merged["linux_gui_autostart"] = is_linux_gui_autostart_enabled()
|
||||
log.info("Detected Linux autostart state: linux_gui_autostart=%s", merged["linux_gui_autostart"])
|
||||
return merged
|
||||
|
|
@ -44,6 +44,7 @@ from utils.tray_common import (
|
|||
save_config, start_proxy, stop_proxy, tg_proxy_url,
|
||||
)
|
||||
from ui.ctk_tray_ui import (
|
||||
AutostartSectionConfig,
|
||||
install_tray_config_buttons, install_tray_config_form,
|
||||
populate_first_run_window, tray_settings_scroll_and_footer,
|
||||
validate_config_form,
|
||||
|
|
@ -257,7 +258,10 @@ def _edit_config_dialog() -> None:
|
|||
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
||||
widgets = install_tray_config_form(
|
||||
ctk, scroll, theme, cfg, DEFAULT_CONFIG,
|
||||
show_autostart=_supports_autostart(),
|
||||
autostart_section=AutostartSectionConfig(
|
||||
title="Автозапуск",
|
||||
windows_autostart=_supports_autostart(),
|
||||
),
|
||||
autostart_value=cfg.get("autostart", False),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue