Download app/tars_plataforma/linux.py from OpenTARS/openTARS: direct link, hf CLI and curl.
- Browser
- Download file 7.19 kB
-
https://huggingface.co/OpenTARS/openTARS/resolve/main/app/tars_plataforma/linux.py
- Command line
-
hf download hf://OpenTARS/openTARS/app/tars_plataforma/linux.py
-
curl -L -o linux.py https://huggingface.co/OpenTARS/openTARS/resolve/main/app/tars_plataforma/linux.py
7.19 kB
| """Backend do Linux: o que o app já fazia (xdg-open, gio, notify-send, wl-clipboard/xclip/xsel, ~/.config/autostart).""" | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| from pathlib import Path | |
| from . import NaoSuportado | |
| TIMEOUT = 10 | |
| def _wayland(): | |
| return bool(os.environ.get("WAYLAND_DISPLAY")) or os.environ.get("XDG_SESSION_TYPE", "").lower() == "wayland" | |
| # Nome do pacote no Debian -> nos outros gerenciadores (só os que o app cita nas dicas; os iguais não precisam estar aqui) | |
| PACOTES = { | |
| "python3-gi": {"dnf": "python3-gobject", "pacman": "python-gobject", "zypper": "python3-gobject"}, | |
| "gir1.2-atspi-2.0": {"dnf": "at-spi2-core", "pacman": "at-spi2-core", "zypper": "typelib-1_0-Atspi-2_0"}, | |
| "python3-tk": {"dnf": "python3-tkinter", "pacman": "tk", "zypper": "python3-tk"}, | |
| "poppler-utils": {"pacman": "poppler", "zypper": "poppler-tools"}, | |
| "libnotify-bin": {"dnf": "libnotify", "pacman": "libnotify", "zypper": "libnotify-tools"}, | |
| } | |
| INSTALAR = {"apt": "sudo apt install", "dnf": "sudo dnf install", "pacman": "sudo pacman -S", "zypper": "sudo zypper install"} | |
| def gerenciador(): | |
| """apt, dnf, pacman ou zypper: o gerenciador de pacotes desta distribuição (None se nenhum conhecido).""" | |
| return next((nome for programa, nome in (("apt-get", "apt"), ("dnf", "dnf"), ("pacman", "pacman"), ("zypper", "zypper")) | |
| if shutil.which(programa)), None) | |
| def comando_instalar(*pacotes): | |
| qual = gerenciador() or "apt" | |
| nomes = dict.fromkeys(p if qual == "apt" else PACOTES.get(p, {}).get(qual, p) for p in pacotes) | |
| return f"{INSTALAR[qual]} {' '.join(nomes)}" | |
| def abrir(alvo): | |
| subprocess.Popen(["xdg-open", str(alvo)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) | |
| def lixeira(caminho): | |
| if not shutil.which("gio"): | |
| raise NaoSuportado("gio not found") | |
| resultado = subprocess.run(["gio", "trash", str(caminho)], capture_output=True, text=True, timeout=30) | |
| if resultado.returncode: | |
| raise OSError((resultado.stderr or "").strip()[:200] or f"gio trash falhou ({resultado.returncode})") | |
| def notificar(titulo, texto): | |
| if not shutil.which("notify-send"): | |
| raise NaoSuportado("notify-send não está instalado") | |
| subprocess.Popen(["notify-send", "-a", "openTARS", "-i", "opentars", str(titulo), str(texto)], | |
| stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) | |
| def _comandos(leitura): | |
| # No Wayland, o wl-clipboard fala com o compositor; xclip/xsel só alcançam a área de transferência do XWayland. | |
| comandos = ([["xclip", "-selection", "clipboard", "-o"], ["xsel", "--clipboard", "--output"]] if leitura | |
| else [["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]]) | |
| if _wayland(): | |
| comandos.insert(0, ["wl-paste", "--no-newline"] if leitura else ["wl-copy"]) | |
| disponiveis = [c for c in comandos if shutil.which(c[0])] | |
| if not disponiveis: | |
| raise NaoSuportado("nem wl-clipboard, nem xclip, nem xsel estão instalados") | |
| return disponiveis | |
| def ler_area_de_transferencia(): | |
| erro = None | |
| for comando in _comandos(leitura=True): | |
| try: | |
| return subprocess.run(comando, capture_output=True, text=True, timeout=TIMEOUT, check=True).stdout | |
| except (OSError, subprocess.SubprocessError) as e: | |
| erro = e | |
| raise OSError(f"não consegui ler a área de transferência: {erro}") | |
| def escrever_area_de_transferencia(texto): | |
| erro = None | |
| for comando in _comandos(leitura=False): | |
| try: | |
| subprocess.run(comando, input=str(texto).encode("utf-8"), capture_output=True, timeout=TIMEOUT, check=True) | |
| return | |
| except (OSError, subprocess.SubprocessError) as e: | |
| erro = e | |
| raise OSError(f"não consegui escrever na área de transferência: {erro}") | |
| def arquivos_selecionados(): | |
| """No Linux não há cópia simulada (Ctrl+C num terminal mata o programa): valem os arquivos que a pessoa copiou no | |
| gerenciador de arquivos (text/uri-list na área de transferência). Só os que existem; [] se não houver.""" | |
| from urllib.parse import unquote, urlparse | |
| comandos = [["xclip", "-selection", "clipboard", "-t", "text/uri-list", "-o"]] | |
| if _wayland(): | |
| comandos.insert(0, ["wl-paste", "--no-newline", "--type", "text/uri-list"]) | |
| for comando in [c for c in comandos if shutil.which(c[0])]: | |
| try: | |
| r = subprocess.run(comando, capture_output=True, text=True, timeout=TIMEOUT) | |
| except (OSError, subprocess.SubprocessError): | |
| continue | |
| if r.returncode == 0: | |
| caminhos = [unquote(urlparse(linha.strip()).path) for linha in r.stdout.splitlines() | |
| if linha.strip().startswith("file://")] | |
| return [c for c in caminhos if os.path.exists(c)] | |
| return [] | |
| def ler_selecao(): | |
| """A seleção PRIMARY: o texto marcado agora, sem tecla simulada (campo de senha não entra nela).""" | |
| comandos = [["xclip", "-selection", "primary", "-o"], ["xsel", "--primary", "--output"]] | |
| if _wayland(): | |
| comandos.insert(0, ["wl-paste", "--primary", "--no-newline"]) | |
| disponiveis = [c for c in comandos if shutil.which(c[0])] | |
| if not disponiveis: | |
| raise NaoSuportado("nem wl-clipboard, nem xclip, nem xsel estão instalados") | |
| for comando in disponiveis: | |
| try: | |
| r = subprocess.run(comando, capture_output=True, text=True, timeout=TIMEOUT) | |
| except (OSError, subprocess.SubprocessError): | |
| continue | |
| if r.returncode == 0: | |
| return r.stdout | |
| return "" | |
| _TERMINAIS = re.compile(r"term|konsole|kitty|alacritty|tilix|kgx|console|wezterm|foot|ptyxis|guake|terminator|xterm", re.I) | |
| def _classe_da_janela_ativa(): | |
| """WM_CLASS da janela com o foco (só X11; no Wayland o compositor não conta).""" | |
| try: | |
| from Xlib import X, display | |
| tela = display.Display() | |
| try: | |
| raiz = tela.screen().root | |
| ativa = raiz.get_full_property(tela.intern_atom("_NET_ACTIVE_WINDOW"), X.AnyPropertyType) | |
| if not ativa or not ativa.value: | |
| return "" | |
| return " ".join(tela.create_resource_object("window", ativa.value[0]).get_wm_class() or ()) | |
| finally: | |
| tela.close() | |
| except Exception: # noqa: BLE001 (sem X, sem python-xlib, janela que sumiu) | |
| return "" | |
| def teclas_de_colar(): | |
| return ("ctrl", "shift", "v") if _TERMINAIS.search(_classe_da_janela_ativa()) else ("ctrl", "v") | |
| def arquivo_autostart(nome): | |
| base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") | |
| return Path(base) / "autostart" / f"{nome}.desktop" | |
| def autostart(nome, titulo, comando, ligado): | |
| atalho = arquivo_autostart(nome) | |
| if ligado: | |
| atalho.parent.mkdir(parents=True, exist_ok=True) | |
| atalho.write_text(f"[Desktop Entry]\nType=Application\nName={titulo}\nExec={' '.join(comando)}\nNoDisplay=true\n" | |
| "X-GNOME-Autostart-enabled=true\n", encoding="utf-8") | |
| else: | |
| atalho.unlink(missing_ok=True) | |
| return atalho.exists() | |