ProjectOpenTARS4's picture
Mirror of openTARS 5.2 Ranger (revision 2026110101)
2695555 verified
Raw History Blame Contribute Delete
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()