openTARS / app /tars_plataforma /__init__.py
ProjectOpenTARS4's picture
Mirror of openTARS 5.2 Ranger (revision 2026110101)
2695555 verified
Raw History Blame Contribute Delete
11.3 kB
"""openTARS 5.2: a única porta de entrada para o que muda de sistema (Linux, Windows, macOS).
Este arquivo traz a interface e o que não depende do sistema (as pastas); o resto vai para o backend de cada sistema
(`linux.py`, `windows.py`, `macos.py`), escolhido na hora da chamada. No Linux é o código que o app já usava; no Windows e no
macOS só existe o que dá para fazer sem testar na máquina, e o que falta levanta `NaoSuportado` dizendo a fase do
`PLANO_5.2.md` em que entra, para nada falhar em silêncio. Só biblioteca padrão, sem Qt.
O que continua preso ao Linux fora daqui, de propósito, até a fase do sistema: a descoberta de programas pelos `.desktop`
(`nucleo/aplicacoes.py`), as janelas do X (`nucleo/janelas.py`), a acessibilidade AT-SPI (`tars_acessibilidade.py`), o
ydotool do Wayland (`tars_wayland.py`), o atalho global do GNOME (`tars_atalho.py`) e o soquete de instância única em
`XDG_RUNTIME_DIR` (`tars_instancia.py`).
Interface (devolvem o resultado ou levantam `NaoSuportado`/`OSError`):
sistema() "linux" | "windows" | "macos"
pastas() {"casa", "config", "dados", "cache"} (Path)
abrir(alvo) abre arquivo, pasta ou endereço no programa padrão
lixeira(caminho) manda para a lixeira (nunca apaga de vez)
notificar(titulo, texto) notificação do sistema (não espera)
ler_area_de_transferencia() texto copiado
escrever_area_de_transferencia(texto)
arquivo_autostart(nome) onde fica o início automático com a sessão
autostart(nome, titulo, comando, ligado) liga ou desliga o início automático; devolve se ficou ligado
tecla_comando() "ctrl", ou "command" no macOS (colar, copiar, salvar)
ler_selecao() o texto marcado agora em qualquer programa ("" se nada)
teclas_de_colar() as teclas de colar na janela ativa (Ctrl+Shift+V num terminal do Linux)
nome_do_sistema() "Windows 11 23H2", "macOS 15.2 Sequoia" (o Linux lê o /etc/os-release no prompt)
janelas(), janela_ativa(), focar_janela(id), fechar_janela(id), mover_janela(id, x, y, largura, altura, maximizar),
area_util() fora do Linux (no Linux o núcleo fala com o X direto: nucleo/janelas.py)
programas() [{nome, outros_nomes, comando, id}] fora do Linux (no Linux: os .desktop)
recursos() {função: existe neste sistema?} (para o diagnóstico)
arquivos_selecionados() os arquivos do gerenciador de arquivos no momento do atalho (ações em arquivos)
permissoes(), pedir_permissao(qual) só no macOS: {"acessibilidade", "tela"} concedidas?; pedir abre os Ajustes
lancador(nome) o comando instalado (opentars, opentars-gui), no PATH ou em ~/.local/bin
comando_instalar(*pacotes) "sudo dnf install ..." com os nomes desta distribuição (só no Linux)
dica_instalar(texto) troca "sudo apt install x" num texto pelo comando desta distribuição
descrever() sistema, versão, arquitetura, núcleos, RAM e Python (para os relatórios)
"""
import os
import platform
import re
import shutil
import sys
from pathlib import Path
NOME = "opentars"
class NaoSuportado(OSError):
"""Esta função ainda não existe neste sistema; a mensagem diz em que fase da 5.2 ela entra."""
def sistema():
if sys.platform.startswith("win"):
return "windows"
return "macos" if sys.platform == "darwin" else "linux"
def esconder_consoles_dos_filhos(popen=None, tem_console=None):
"""Windows sem console (pythonw: a janela e o serviço dos Probes): cada programa de console que o app roda (nvidia-smi,
ollama, powershell) abria uma janela preta por cima, e a janela do app parecia travada. Aqui o padrão vira
CREATE_NO_WINDOW; quem abre um console para a pessoa passa CREATE_NEW_CONSOLE, e aí o Windows ignora o outro."""
import subprocess
popen = popen or subprocess.Popen
if tem_console is None:
import ctypes
tem_console = bool(ctypes.windll.kernel32.GetConsoleWindow())
if tem_console or getattr(popen, "_sem_console", False):
return False
original = popen.__init__
def __init__(self, *a, creationflags=0, **k):
if not creationflags & (0x10 | 0x08): # CREATE_NEW_CONSOLE, DETACHED_PROCESS
creationflags |= 0x08000000 # CREATE_NO_WINDOW
original(self, *a, creationflags=creationflags, **k)
popen.__init__ = __init__
popen._sem_console = True
return True
def _backend():
so = sistema()
if so == "windows":
from . import windows as backend
elif so == "macos":
from . import macos as backend
else:
from . import linux as backend
return backend
def pastas():
casa = Path.home()
so = sistema()
if so == "windows":
roaming = Path(os.environ.get("APPDATA") or casa / "AppData" / "Roaming")
local = Path(os.environ.get("LOCALAPPDATA") or casa / "AppData" / "Local")
return {"casa": casa, "config": roaming / NOME, "dados": local / NOME, "cache": local / NOME / "cache"}
if so == "macos":
base = casa / "Library"
return {"casa": casa, "config": base / "Application Support" / NOME, "dados": base / "Application Support" / NOME,
"cache": base / "Caches" / NOME}
xdg = lambda var, padrao: Path(os.environ.get(var) or casa / padrao) # noqa: E731
return {"casa": casa, "config": xdg("XDG_CONFIG_HOME", ".config") / NOME, "dados": xdg("XDG_DATA_HOME", ".local/share") / NOME,
"cache": xdg("XDG_CACHE_HOME", ".cache") / NOME}
def abrir(alvo):
return _backend().abrir(alvo)
def arquivos_selecionados():
"""Os arquivos selecionados no gerenciador de arquivos quando o atalho foi apertado (Windows e macOS: da cópia simulada
do ler_selecao; Linux: os que a pessoa copiou com Ctrl+C). [] se não eram arquivos."""
return _backend().arquivos_selecionados()
def permissoes():
return _backend().permissoes() if sistema() == "macos" else {}
def pedir_permissao(qual):
return _backend().pedir_permissao(qual)
def lancador(nome):
"""O comando instalado (opentars, opentars-gui): no PATH ou em ~/.local/bin, onde os instaladores por usuário o põem
(o Arch e o macOS aberto pelo Finder não têm ~/.local/bin no PATH). None se não houver."""
local = Path.home() / ".local" / "bin" / nome
return shutil.which(nome) or (str(local) if local.is_file() and os.access(local, os.X_OK) else None)
def comando_instalar(*pacotes):
"""Os pacotes vêm com o nome do Debian; volta o comando com os nomes desta distribuição. Fora do Linux, None."""
return _backend().comando_instalar(*pacotes) if sistema() == "linux" else None
_RE_APT = re.compile(r"sudo apt(?:-get)? install((?: [a-z0-9][\w.+:-]*)+)")
def dica_instalar(texto):
"""Troca "sudo apt install x y" num aviso pelo comando desta distribuição (dnf, pacman, zypper), com os nomes de lá."""
if sistema() != "linux" or (_backend().gerenciador() or "apt") == "apt":
return texto
return _RE_APT.sub(lambda m: comando_instalar(*m.group(1).split()), texto)
def lixeira(caminho):
return _backend().lixeira(caminho)
def notificar(titulo, texto):
return _backend().notificar(titulo, texto)
def ler_area_de_transferencia():
return _backend().ler_area_de_transferencia()
def escrever_area_de_transferencia(texto):
return _backend().escrever_area_de_transferencia(texto)
def ler_selecao():
return _backend().ler_selecao()
def teclas_de_colar():
return _backend().teclas_de_colar()
def arquivo_autostart(nome):
return _backend().arquivo_autostart(nome)
def autostart(nome, titulo, comando, ligado=True):
return _backend().autostart(nome, titulo, comando, ligado)
def nome_do_sistema():
"""O nome que vai no bloco THIS COMPUTER fora do Linux, no formato dos dados da 5.2."""
so = sistema()
if so == "macos":
versao = platform.mac_ver()[0]
nomes = {"26": "Tahoe", "15": "Sequoia", "14": "Sonoma", "13": "Ventura", "12": "Monterey", "11": "Big Sur"}
return " ".join(x for x in ("macOS", versao, nomes.get(versao.split(".")[0], "")) if x)
if so == "windows":
return " ".join(x for x in ("Windows", platform.release(), _versao_windows()) if x)
return "Linux"
def _versao_windows():
"""23H2, 24H2...: só o registro sabe (o platform dá o número do build)."""
try:
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion") as chave:
return winreg.QueryValueEx(chave, "DisplayVersion")[0]
except (ImportError, OSError):
return ""
def janelas():
return _backend().janelas()
def janela_ativa():
return _backend().janela_ativa()
def focar_janela(id_janela):
return _backend().focar_janela(id_janela)
def fechar_janela(id_janela):
return _backend().fechar_janela(id_janela)
def mover_janela(id_janela, x=None, y=None, largura=None, altura=None, maximizar=None):
return _backend().mover_janela(id_janela, x, y, largura, altura, maximizar)
def area_util():
return _backend().area_util()
def programas():
return _backend().programas()
def tecla_comando():
return "command" if sistema() == "macos" else "ctrl"
FUNCOES = ("abrir", "lixeira", "notificar", "ler_area_de_transferencia", "escrever_area_de_transferencia", "autostart",
"ler_selecao", "teclas_de_colar")
FUNCOES_FORA_DO_LINUX = ("janelas", "programas") # no Linux o núcleo usa o X e os .desktop
def recursos():
backend = _backend()
funcoes = FUNCOES + (FUNCOES_FORA_DO_LINUX if sistema() != "linux" else ())
return {funcao: funcao in vars(backend) for funcao in funcoes}
def descrever():
so = sistema()
versao = (platform.mac_ver()[0] if so == "macos" else " ".join(x for x in platform.win32_ver()[:2] if x) if so == "windows"
else platform.release())
try:
import psutil
ram = round(psutil.virtual_memory().total / 1024 ** 3, 1)
except (ImportError, OSError, AttributeError): # AttributeError: psutil de mentira nos testes
ram = None
return {"sistema": so, "versao": versao, "plataforma": platform.platform(), "arquitetura": platform.machine(),
"processador": platform.processor() or None, "nucleos": os.cpu_count(), "ram_gb": ram,
"python": platform.python_version()}
def janelas_prontas():
"""Dá para listar as janelas agora sem esperar? No macOS o Quartz carrega em segundo plano na abertura (leva segundos);
até lá, o contexto do pedido segue sem a lista de janelas. Nos outros sistemas, sempre."""
return sistema() != "macos" or _backend().frameworks_prontos()
if sistema() == "windows":
esconder_consoles_dos_filhos()
elif sistema() == "macos":
_backend() # começa a carregar o Quartz em segundo plano (macos.py)