File size: 11,274 Bytes
2695555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
"""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)