Compare commits
6
Commits
v1.0.0
..
e9d43f6e79
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d43f6e79 | ||
|
|
f7ab9e9923 | ||
|
|
d14df4e32c | ||
|
|
024f7a7387 | ||
|
|
6e45d64ee0 | ||
|
|
16affedd2b |
No files matched your search
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,69 @@
|
||||
name: CI
|
||||
|
||||
# Na main não roda por push: o release.yml é quem o chama, e uma vez só por
|
||||
# commit. Sem gatilho de pull_request: o push da branch já valida o mesmo commit,
|
||||
# e os dois juntos rodariam tudo duas vezes (o repositório não recebe PR de fork).
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [main]
|
||||
paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.wolf/**', 'scripts/release-notes.sh']
|
||||
schedule:
|
||||
- cron: '0 9 * * 1' # toda segunda: reavalia com a base de vulnerabilidades nova
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verificar:
|
||||
name: Código e scripts
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Sintaxe e shellcheck
|
||||
run: |
|
||||
python3 -m py_compile copiado.py
|
||||
command -v shellcheck >/dev/null || { apt-get update -qq && apt-get install -y -qq shellcheck; }
|
||||
shellcheck instalar.sh desinstalar.sh empacotamento/gerar-deb.sh scripts/*.sh
|
||||
|
||||
# Trivy roda pela imagem oficial: o trivy-action clona o repositório do Trivy
|
||||
# do mesmo servidor do workflow, que aqui é o Gitea. O workspace do job não é
|
||||
# visível ao daemon, então os arquivos entram no container com `docker cp`.
|
||||
seguranca:
|
||||
name: Trivy (dependências, segredos e configuração)
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
|
||||
fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 .)
|
||||
docker cp . "$cid:/src"
|
||||
docker start -a "$cid"
|
||||
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||
docker rm "$cid" >/dev/null
|
||||
exit "$status"
|
||||
|
||||
pacote:
|
||||
name: Pacote .deb
|
||||
needs: [verificar, seguranca]
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Gera o .deb
|
||||
run: |
|
||||
empacotamento/gerar-deb.sh "0.0.0~ci"
|
||||
(cd dist && sha256sum copiado_all.deb > SHA256SUMS)
|
||||
# Confere o conteúdo sem instalar: instalar puxaria python3-gi e o GTK só
|
||||
# para provar que os arquivos estão no lugar.
|
||||
- name: Confere o conteúdo do pacote
|
||||
run: |
|
||||
dpkg-deb -I dist/copiado_all.deb
|
||||
mkdir conteudo && dpkg-deb -x dist/copiado_all.deb conteudo
|
||||
test -x conteudo/usr/bin/copiado
|
||||
test -f conteudo/etc/xdg/autostart/copiado.desktop
|
||||
python3 -m py_compile conteudo/usr/bin/copiado
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Release
|
||||
|
||||
# Na main valida o commit (chamando o ci.yml). Numa tag v*, gera o .deb com a
|
||||
# versão da tag e publica a release no Gitea com o pacote, o SHA256SUMS e as
|
||||
# notas da seção do CHANGELOG.md. Se o commit da tag já passou na main, a suíte
|
||||
# não roda de novo.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.wolf/**']
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
plano:
|
||||
name: Plano (este commit já foi validado?)
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
reuse: ${{ steps.plano.outputs.reuse }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: plano
|
||||
run: scripts/plano-ci.sh "${{ gitea.server_url }}" "${{ gitea.repository }}" "${{ gitea.sha }}" "${{ gitea.token }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
ci:
|
||||
needs: plano
|
||||
if: ${{ needs.plano.outputs.reuse != 'true' }}
|
||||
uses: ./.gitea/workflows/ci.yml
|
||||
|
||||
release:
|
||||
name: Publica a release
|
||||
needs: [plano, ci]
|
||||
# O ci é pulado, e não bem-sucedido, quando o commit já foi validado.
|
||||
if: ${{ !cancelled() && needs.plano.result == 'success' && (needs.ci.result == 'success' || needs.plano.outputs.reuse == 'true') && startsWith(gitea.ref, 'refs/tags/v') }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Pacote
|
||||
run: |
|
||||
empacotamento/gerar-deb.sh "${GITEA_REF_NAME#v}"
|
||||
(cd dist && sha256sum copiado_all.deb > SHA256SUMS)
|
||||
env:
|
||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||
- name: Release
|
||||
env:
|
||||
TAG: ${{ gitea.ref_name }}
|
||||
TOKEN: ${{ gitea.token }}
|
||||
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||
run: |
|
||||
scripts/release-notes.sh "$TAG" > notes.md
|
||||
jq -n --arg tag "$TAG" --arg name "Copiado ${TAG#v}" --rawfile body notes.md \
|
||||
'{tag_name:$tag, name:$name, body:$body, prerelease:false}' > release.json
|
||||
id=$(curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||
-H 'Content-Type: application/json' -d @release.json | jq -r .id)
|
||||
for f in dist/copiado_all.deb dist/SHA256SUMS; do
|
||||
curl -fsS -X POST "$API/releases/$id/assets?name=$(basename "$f")" \
|
||||
-H "Authorization: token $TOKEN" -F "attachment=@$f" >/dev/null
|
||||
done
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Pacote
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
pacote:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verificar código
|
||||
run: |
|
||||
python3 -m py_compile copiado.py
|
||||
shellcheck instalar.sh desinstalar.sh empacotamento/gerar-deb.sh
|
||||
|
||||
- name: Definir versão
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
||||
echo "VERSAO=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "VERSAO=0.0.0~dev${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Gerar .deb
|
||||
run: empacotamento/gerar-deb.sh "$VERSAO"
|
||||
|
||||
- name: Testar instalação do pacote
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y ./dist/copiado_all.deb
|
||||
test -x /usr/bin/copiado
|
||||
test -f /etc/xdg/autostart/copiado.desktop
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: copiado-${{ env.VERSAO }}
|
||||
path: dist/copiado_all.deb
|
||||
|
||||
- name: Publicar release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create "$GITHUB_REF_NAME" dist/copiado_all.deb \
|
||||
--title "Copiado $VERSAO" --generate-notes
|
||||
@@ -0,0 +1,76 @@
|
||||
# Registro de mudanças
|
||||
|
||||
Tudo o que muda de relevante no Copiado está aqui. O formato segue o
|
||||
[Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e o projeto segue o
|
||||
[Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
||||
|
||||
## [Não lançado]
|
||||
|
||||
## [1.3.2] - 2026-11-01
|
||||
|
||||
### Alterado
|
||||
|
||||
- O repositório é **público** (projeto de código aberto). O `instalar.sh` volta a funcionar
|
||||
sem token: `GITEA_TOKEN` passa a ser opcional.
|
||||
|
||||
## [1.3.1] - 2026-11-01
|
||||
|
||||
### Alterado
|
||||
|
||||
- O código e as releases passam a viver no Gitea (`git.lucasrguerra.dev.br`). O
|
||||
`instalar.sh` consulta a API do Gitea para achar a release mais recente.
|
||||
- A esteira de CI/CD foi reescrita para os runners do Gitea (`.gitea/workflows`):
|
||||
branches validam (sintaxe, shellcheck, Trivy, pacote), a `main` e as tags chamam a
|
||||
mesma suíte uma vez por commit, e a tag publica a release no Gitea com o `.deb`, o
|
||||
`SHA256SUMS` e estas notas. O Trivy roda pela imagem oficial.
|
||||
|
||||
### Corrigido
|
||||
|
||||
- `gerar-deb.sh` fixa o `umask 022`: com `umask 000` (o padrão dos runners de CI) o
|
||||
diretório `DEBIAN` saía com permissão 777 e o `dpkg-deb` recusava o pacote.
|
||||
|
||||
## [1.3.0] - 2026-10-03
|
||||
|
||||
### Alterado
|
||||
|
||||
- Senhas de gerenciadores de senha voltam a entrar no histórico.
|
||||
|
||||
## [1.2.0] - 2026-10-03
|
||||
|
||||
### Adicionado
|
||||
|
||||
- Comandos `copiado consertar` (recria e recarrega o atalho Win+V e inicia o app se
|
||||
estiver parado) e `copiado ajuda`.
|
||||
|
||||
### Alterado
|
||||
|
||||
- `instalar.sh` e `desinstalar.sh` passam a usar os comandos do próprio app.
|
||||
|
||||
## [1.1.0] - 2026-10-03
|
||||
|
||||
### Adicionado
|
||||
|
||||
- Comando `copiado esquecer`, que apaga todo o histórico.
|
||||
- Trivy (repositório e pacote) na esteira, ações fixadas por hash e permissões mínimas.
|
||||
- A release publica o `SHA256SUMS` e o instalador confere o checksum antes de instalar.
|
||||
|
||||
### Segurança
|
||||
|
||||
- Só os itens fixados vão para o disco, com permissão 600/700.
|
||||
- Conteúdo marcado por gerenciadores de senha é ignorado.
|
||||
- Limites de tamanho e validação do histórico salvo.
|
||||
|
||||
## [1.0.0] - 2026-10-03
|
||||
|
||||
### Adicionado
|
||||
|
||||
- Histórico da área de transferência no Win+V, no visual do Windows 11, com texto e
|
||||
imagens, fixação de itens e colagem por clique.
|
||||
|
||||
[Não lançado]: https://git.lucasrguerra.dev.br/lucasrguerra/copiado/compare/v1.3.2...HEAD
|
||||
[1.3.2]: https://git.lucasrguerra.dev.br/lucasrguerra/copiado/compare/v1.3.1...v1.3.2
|
||||
[1.3.1]: https://git.lucasrguerra.dev.br/lucasrguerra/copiado/compare/v1.3.0...v1.3.1
|
||||
[1.3.0]: https://git.lucasrguerra.dev.br/lucasrguerra/copiado/compare/v1.2.0...v1.3.0
|
||||
[1.2.0]: https://git.lucasrguerra.dev.br/lucasrguerra/copiado/compare/v1.1.0...v1.2.0
|
||||
[1.1.0]: https://git.lucasrguerra.dev.br/lucasrguerra/copiado/compare/v1.0.0...v1.1.0
|
||||
[1.0.0]: https://git.lucasrguerra.dev.br/lucasrguerra/copiado/releases/tag/v1.0.0
|
||||
@@ -7,10 +7,10 @@ Clique em um item e ele é colado na hora.
|
||||
## Instalar
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/lucasrguerra/copiado/main/instalar.sh | bash
|
||||
curl -fsSL https://git.lucasrguerra.dev.br/lucasrguerra/copiado/raw/branch/main/instalar.sh | bash
|
||||
```
|
||||
|
||||
Baixa o pacote `.deb` mais recente das [Releases](https://github.com/lucasrguerra/copiado/releases),
|
||||
Baixa o pacote `.deb` mais recente das [Releases](https://git.lucasrguerra.dev.br/lucasrguerra/copiado/releases),
|
||||
instala, cria o atalho Win+V e já deixa rodando. Pede sua senha (sudo).
|
||||
Para **atualizar**, rode o mesmo comando de novo.
|
||||
|
||||
@@ -25,24 +25,52 @@ Para **atualizar**, rode o mesmo comando de novo.
|
||||
| Apagar tudo (menos os fixados) | **Limpar tudo** |
|
||||
| Fechar sem colar | **Esc** ou clicar fora |
|
||||
|
||||
Guarda os 25 últimos itens (texto e imagens) e continua lá depois de reiniciar o PC.
|
||||
Guarda os 25 últimos itens (texto e imagens). Como no Windows, ao reiniciar o PC
|
||||
só os itens **fixados** continuam; o resto existe apenas na memória.
|
||||
|
||||
## Privacidade e segurança
|
||||
|
||||
A área de transferência pode ter senhas, tokens e dados pessoais, então o Copiado:
|
||||
|
||||
- **Só grava no disco os itens fixados**, em `~/.local/share/copiado/`, com
|
||||
permissão só para você (pasta `700`, arquivos `600`).
|
||||
- **Não guarda textos acima de 1 MB nem imagens acima de 20 MB.**
|
||||
- **Valida o arquivo de histórico** ao abrir: entradas estranhas ou imagens fora da
|
||||
pasta do Copiado são descartadas.
|
||||
- **Não usa rede** e não executa nada do que foi copiado.
|
||||
|
||||
O instalador confere o checksum SHA-256 do pacote antes de instalar, e a esteira
|
||||
de CI/CD roda o [Trivy](https://trivy.dev) (vulnerabilidades, segredos e configurações)
|
||||
em todo push de branch, em toda release e toda segunda-feira. Um HIGH ou CRITICAL
|
||||
barra a esteira.
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
- `copiado.py`: o aplicativo inteiro (GTK 3 + xdotool).
|
||||
- `empacotamento/gerar-deb.sh <versão>`: gera `dist/copiado_all.deb`.
|
||||
- `.gitea/workflows/`: `ci.yml` (sintaxe, shellcheck, Trivy, pacote) roda nas branches
|
||||
e toda segunda; `release.yml` valida a `main` e, numa tag `vX.Y.Z`, publica a release
|
||||
com o `.deb`, o `SHA256SUMS` e as notas do [CHANGELOG](CHANGELOG.md). Uma tag num
|
||||
commit que já passou na `main` não repete a suíte.
|
||||
- Para lançar: mova o que está em "Não lançado" do CHANGELOG para a versão nova, faça
|
||||
o commit e envie a tag (`git tag vX.Y.Z && git push gitea vX.Y.Z`).
|
||||
|
||||
## Desinstalar
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/lucasrguerra/copiado/main/desinstalar.sh | bash
|
||||
curl -fsSL https://git.lucasrguerra.dev.br/lucasrguerra/copiado/raw/branch/main/desinstalar.sh | bash
|
||||
```
|
||||
|
||||
## Se algo der errado
|
||||
## Comandos
|
||||
|
||||
- **Win+V não abre nada:** faça o Cinnamon recarregar os atalhos:
|
||||
```bash
|
||||
gsettings set org.cinnamon.desktop.keybindings custom-list "[]"; gsettings set org.cinnamon.desktop.keybindings custom-list "['custom0']"
|
||||
```
|
||||
Se ainda assim não abrir, veja se outro atalho usa Win+V em
|
||||
*Configurações do Sistema → Teclado → Atalhos*.
|
||||
- **Abre, mas o histórico está vazio:** o app pode não estar rodando. Rode
|
||||
`copiado &`
|
||||
| Comando | Para quê |
|
||||
|---|---|
|
||||
| `copiado consertar` | **Win+V não abre ou o histórico está vazio?** Recria o atalho, faz o Cinnamon recarregá-lo e inicia o app se estiver parado. |
|
||||
| `copiado esquecer` | Apaga todo o histórico, inclusive os fixados. |
|
||||
| `copiado ajuda` | Lista os comandos. |
|
||||
|
||||
Se mesmo depois do `copiado consertar` o Win+V não abrir, veja se outro atalho usa
|
||||
Win+V em *Configurações do Sistema → Teclado → Atalhos*.
|
||||
|
||||
## Publicar uma versão nova
|
||||
|
||||
|
||||
+166
-12
@@ -2,8 +2,11 @@
|
||||
"""Copiado: histórico da área de transferência no estilo Windows 11 (Win+V).
|
||||
|
||||
Uso:
|
||||
copiado.py inicia em segundo plano (vigia o Ctrl+C)
|
||||
copiado.py show abre a janelinha do histórico
|
||||
copiado inicia em segundo plano (vigia o Ctrl+C)
|
||||
copiado show abre a janelinha do histórico
|
||||
copiado esquecer apaga todo o histórico, inclusive os fixados
|
||||
copiado consertar recria o atalho Win+V e inicia o app se estiver parado
|
||||
copiado ajuda mostra esta ajuda
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
@@ -22,6 +25,8 @@ MAX_ITENS = 25
|
||||
LARGURA, ALTURA = 380, 480
|
||||
PASTA = os.path.join(GLib.get_user_data_dir(), "copiado")
|
||||
ARQ_HIST = os.path.join(PASTA, "historico.json")
|
||||
MAX_TEXTO = 1024 * 1024 # textos maiores que 1 MB não são guardados
|
||||
MAX_IMAGEM = 20 * 1024 * 1024 # imagens maiores que 20 MB (PNG) também não
|
||||
|
||||
CSS = b"""
|
||||
#fundo {
|
||||
@@ -63,26 +68,60 @@ scrollbar slider { background-color: rgba(255,255,255,0.25); border-radius: 4px;
|
||||
"""
|
||||
|
||||
|
||||
def item_valido(item):
|
||||
"""Aceita só itens bem formados; imagens só dentro da nossa pasta."""
|
||||
if not isinstance(item, dict) or not isinstance(item.get("dado"), str):
|
||||
return False
|
||||
if item.get("tipo") == "texto":
|
||||
return len(item["dado"]) <= MAX_TEXTO
|
||||
if item.get("tipo") == "imagem":
|
||||
caminho = os.path.realpath(item["dado"])
|
||||
return (os.path.dirname(caminho) == os.path.realpath(PASTA)
|
||||
and caminho.endswith(".png") and os.path.isfile(caminho))
|
||||
return False
|
||||
|
||||
|
||||
class Historico:
|
||||
def __init__(self):
|
||||
os.makedirs(PASTA, exist_ok=True)
|
||||
# só o próprio usuário pode ler o histórico (pasta 700, arquivos 600)
|
||||
os.umask(0o077)
|
||||
os.makedirs(PASTA, mode=0o700, exist_ok=True)
|
||||
os.chmod(PASTA, 0o700)
|
||||
for nome in os.listdir(PASTA):
|
||||
caminho = os.path.join(PASTA, nome)
|
||||
if os.path.isfile(caminho) and not os.path.islink(caminho):
|
||||
os.chmod(caminho, 0o600)
|
||||
self.itens = [] # {"tipo": "texto"|"imagem", "dado": str, "fixo": bool}
|
||||
try:
|
||||
with open(ARQ_HIST) as f:
|
||||
self.itens = json.load(f)
|
||||
carregados = json.load(f)
|
||||
if isinstance(carregados, list):
|
||||
# como no Windows: ao ligar o PC, só os fixados continuam
|
||||
self.itens = [{"tipo": i["tipo"], "dado": i["dado"], "fixo": True}
|
||||
for i in carregados if item_valido(i) and i.get("fixo") is True]
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
self.salvar()
|
||||
|
||||
def salvar(self):
|
||||
with open(ARQ_HIST + ".tmp", "w") as f:
|
||||
json.dump(self.itens, f, ensure_ascii=False)
|
||||
os.replace(ARQ_HIST + ".tmp", ARQ_HIST)
|
||||
# só os fixados vão para o disco; o resto fica apenas na memória
|
||||
fixos = [i for i in self.itens if i["fixo"]]
|
||||
tmp = ARQ_HIST + ".tmp"
|
||||
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(fixos, f, ensure_ascii=False)
|
||||
os.replace(tmp, ARQ_HIST)
|
||||
usadas = {i["dado"] for i in self.itens if i["tipo"] == "imagem"}
|
||||
for nome in os.listdir(PASTA):
|
||||
caminho = os.path.join(PASTA, nome)
|
||||
if nome.endswith(".png") and caminho not in usadas:
|
||||
os.remove(caminho)
|
||||
|
||||
def esquecer_tudo(self):
|
||||
"""Apaga o histórico inteiro, inclusive os fixados."""
|
||||
self.itens = []
|
||||
self.salvar()
|
||||
|
||||
def adicionar(self, tipo, dado):
|
||||
antigo = next((i for i in self.itens if i["tipo"] == tipo and i["dado"] == dado), None)
|
||||
fixo = antigo["fixo"] if antigo else False
|
||||
@@ -125,8 +164,13 @@ class App(Gtk.Application):
|
||||
self.criar_janela()
|
||||
|
||||
def do_command_line(self, linha):
|
||||
if "show" in linha.get_arguments()[1:]:
|
||||
args = linha.get_arguments()[1:]
|
||||
if "show" in args:
|
||||
self.mostrar()
|
||||
elif "esquecer" in args:
|
||||
self.hist.esquecer_tudo()
|
||||
if self.janela.get_visible():
|
||||
self.preencher()
|
||||
return 0
|
||||
|
||||
# ---------- captura ----------
|
||||
@@ -136,17 +180,21 @@ class App(Gtk.Application):
|
||||
if img is None:
|
||||
return
|
||||
dados = img.save_to_bufferv("png", [], [])[1]
|
||||
nome = hashlib.sha1(dados).hexdigest()[:16] + ".png"
|
||||
if len(dados) > MAX_IMAGEM:
|
||||
return
|
||||
nome = hashlib.sha256(dados).hexdigest()[:32] + ".png"
|
||||
caminho = os.path.join(PASTA, nome)
|
||||
if caminho == self.ignorar:
|
||||
return
|
||||
if not os.path.exists(caminho):
|
||||
with open(caminho, "wb") as f:
|
||||
fd = os.open(caminho, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
f.write(dados)
|
||||
self.hist.adicionar("imagem", caminho)
|
||||
else:
|
||||
texto = clip.wait_for_text()
|
||||
if not texto or not texto.strip() or texto == self.ignorar:
|
||||
if (not texto or not texto.strip() or len(texto) > MAX_TEXTO
|
||||
or texto == self.ignorar):
|
||||
return
|
||||
self.hist.adicionar("texto", texto)
|
||||
if self.janela.get_visible():
|
||||
@@ -349,5 +397,111 @@ class App(Gtk.Application):
|
||||
return False
|
||||
|
||||
|
||||
# ---------- comandos de manutenção (não abrem janela) ----------
|
||||
ATALHOS = "org.cinnamon.desktop.keybindings"
|
||||
CAMINHO_ATALHO = "/org/cinnamon/desktop/keybindings/custom-keybindings/{}/"
|
||||
|
||||
|
||||
def gsettings(*args):
|
||||
return subprocess.run(["gsettings", *args], capture_output=True,
|
||||
text=True, check=True).stdout.strip()
|
||||
|
||||
|
||||
def ler_lista(valor):
|
||||
# gsettings devolve algo como "['custom0']" ou "@as []"
|
||||
return json.loads(valor.replace("@as ", "").replace("'", '"'))
|
||||
|
||||
|
||||
def esquema(ident):
|
||||
return f"{ATALHOS}.custom-keybinding:" + CAMINHO_ATALHO.format(ident)
|
||||
|
||||
|
||||
def configurar_atalho():
|
||||
"""Cria (ou corrige) o atalho Win+V e faz o Cinnamon recarregá-lo."""
|
||||
lista = ler_lista(gsettings("get", ATALHOS, "custom-list"))
|
||||
ident = next((i for i in lista
|
||||
if gsettings("get", esquema(i), "binding") == "['<Super>v']"), None)
|
||||
if ident is None:
|
||||
ident = next(f"custom{n}" for n in range(1000) if f"custom{n}" not in lista)
|
||||
lista.append(ident)
|
||||
gsettings("set", esquema(ident), "name", "Copiado")
|
||||
gsettings("set", esquema(ident), "command", "copiado show")
|
||||
gsettings("set", esquema(ident), "binding", "['<Super>v']")
|
||||
# o Cinnamon só relê os atalhos quando a lista muda
|
||||
gsettings("set", ATALHOS, "custom-list", "[]")
|
||||
time.sleep(0.5)
|
||||
gsettings("set", ATALHOS, "custom-list", json.dumps(lista).replace('"', "'"))
|
||||
return ident
|
||||
|
||||
|
||||
def remover_atalho():
|
||||
lista = ler_lista(gsettings("get", ATALHOS, "custom-list"))
|
||||
nossos = [i for i in lista if "copiado" in gsettings("get", esquema(i), "command")]
|
||||
for ident in nossos:
|
||||
gsettings("reset-recursively", esquema(ident))
|
||||
restantes = [i for i in lista if i not in nossos]
|
||||
gsettings("set", ATALHOS, "custom-list", json.dumps(restantes).replace('"', "'"))
|
||||
|
||||
|
||||
def esta_rodando():
|
||||
bus = Gio.bus_get_sync(Gio.BusType.SESSION)
|
||||
resp = bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", "NameHasOwner",
|
||||
GLib.Variant("(s)", ("io.github.copiado",)),
|
||||
GLib.VariantType("(b)"), Gio.DBusCallFlags.NONE, -1, None)
|
||||
return resp.unpack()[0]
|
||||
|
||||
|
||||
def iniciar_em_segundo_plano():
|
||||
subprocess.Popen([sys.executable, os.path.realpath(__file__)],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
for _ in range(30):
|
||||
time.sleep(0.2)
|
||||
if esta_rodando():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def consertar():
|
||||
try:
|
||||
ident = configurar_atalho()
|
||||
print(f"✓ Atalho Win+V configurado e recarregado ({ident}).")
|
||||
except (OSError, subprocess.CalledProcessError) as erro:
|
||||
print(f"✗ Não consegui configurar o atalho: {erro}")
|
||||
return 1
|
||||
if esta_rodando():
|
||||
print("✓ O Copiado já está rodando.")
|
||||
elif iniciar_em_segundo_plano():
|
||||
print("✓ O Copiado estava parado e foi iniciado.")
|
||||
else:
|
||||
print("✗ Não consegui iniciar o Copiado. Rode 'copiado' para ver o erro.")
|
||||
return 1
|
||||
print("Pronto! Aperte Win+V.")
|
||||
print("Se ainda não abrir, veja se outro atalho usa Win+V em")
|
||||
print("Configurações do Sistema → Teclado → Atalhos.")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv):
|
||||
comando = argv[1] if len(argv) > 1 else ""
|
||||
if comando in ("ajuda", "-h", "--help"):
|
||||
print(__doc__.strip())
|
||||
return 0
|
||||
if comando == "consertar":
|
||||
return consertar()
|
||||
if comando == "instalar-atalho":
|
||||
configurar_atalho()
|
||||
return 0
|
||||
if comando == "remover-atalho":
|
||||
remover_atalho()
|
||||
return 0
|
||||
if comando not in ("", "show", "esquecer"):
|
||||
print(f"Comando desconhecido: {comando}\n")
|
||||
print(__doc__.strip())
|
||||
return 2
|
||||
return App().run(argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(App().run(sys.argv))
|
||||
sys.exit(main(sys.argv))
|
||||
+1
-10
@@ -1,15 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Remove o Copiado e o atalho Win+V
|
||||
BASE=org.cinnamon.desktop.keybindings
|
||||
LISTA=$(gsettings get $BASE custom-list)
|
||||
for ID in $(python3 -c "import ast,sys; print(' '.join(ast.literal_eval(sys.argv[1].replace('@as ',''))))" "$LISTA"); do
|
||||
SCHEMA="$BASE.custom-keybinding:/org/cinnamon/desktop/keybindings/custom-keybindings/$ID/"
|
||||
if gsettings get "$SCHEMA" command | grep -q copiado; then
|
||||
LISTA=$(python3 -c "import ast,sys; l=ast.literal_eval(sys.argv[1].replace('@as ','')); print([x for x in l if x!='$ID'])" "$LISTA")
|
||||
gsettings set $BASE custom-list "$LISTA"
|
||||
gsettings reset-recursively "$SCHEMA"
|
||||
fi
|
||||
done
|
||||
command -v copiado >/dev/null && copiado remover-atalho
|
||||
pkill -f "/usr/bin/copiado" 2>/dev/null
|
||||
sudo apt-get remove -y copiado
|
||||
rm -rf "$HOME/.local/share/copiado"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gera o pacote copiado_all.deb. Uso: empacotamento/gerar-deb.sh 1.2.3
|
||||
set -e
|
||||
# O dpkg-deb recusa um DEBIAN com permissão fora de 0755-0775; fixa o umask para
|
||||
# não depender do ambiente (nos runners de CI ele é 000).
|
||||
umask 022
|
||||
VERSAO="${1:?informe a versão, ex.: 1.0.0}"
|
||||
cd "$(dirname "$0")/.."
|
||||
RAIZ=$(mktemp -d)
|
||||
@@ -28,7 +31,7 @@ Priority: optional
|
||||
Architecture: all
|
||||
Depends: python3, python3-gi, gir1.2-gtk-3.0, xdotool
|
||||
Maintainer: Lucas Guerra <l.rayanguerra@gmail.com>
|
||||
Homepage: https://github.com/lucasrguerra/copiado
|
||||
Homepage: https://git.lucasrguerra.dev.br/lucasrguerra/copiado
|
||||
Description: Histórico da área de transferência no estilo Windows 11
|
||||
Aperte Win+V para ver os últimos itens copiados e colar com um clique.
|
||||
CTRL
|
||||
|
||||
+30
-29
@@ -1,13 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Instala (ou atualiza) o Copiado a partir do pacote publicado no GitHub
|
||||
# Instala (ou atualiza) o Copiado a partir do pacote publicado no Gitea.
|
||||
# O repositório é público: não precisa de token. GITEA_TOKEN é opcional (útil num
|
||||
# fork privado ou para fugir do limite de requisições anônimas).
|
||||
set -e
|
||||
URL="https://github.com/lucasrguerra/copiado/releases/latest/download/copiado_all.deb"
|
||||
BASE=org.cinnamon.desktop.keybindings
|
||||
SERVIDOR="https://git.lucasrguerra.dev.br"
|
||||
API="$SERVIDOR/api/v1/repos/lucasrguerra/copiado"
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
echo "==> Baixando a versão mais recente..."
|
||||
curl -fsSL -o "$TMP/copiado_all.deb" "$URL"
|
||||
# O token (se houver) vai por um arquivo de configuração do curl, não pela linha de
|
||||
# comando, para não aparecer na lista de processos.
|
||||
: > "$TMP/curlrc"
|
||||
[ -z "${GITEA_TOKEN:-}" ] || printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$TMP/curlrc"
|
||||
|
||||
baixar() { curl -fsSL --proto '=https' --tlsv1.2 -K "$TMP/curlrc" -o "$2" "$1"; }
|
||||
|
||||
echo "==> Procurando a versão mais recente..."
|
||||
baixar "$API/releases/latest" "$TMP/release.json"
|
||||
url_do_arquivo() {
|
||||
python3 -c 'import json,sys; print(next(a["browser_download_url"] for a in json.load(open(sys.argv[1]))["assets"] if a["name"] == sys.argv[2]))' "$TMP/release.json" "$1"
|
||||
}
|
||||
|
||||
echo "==> Baixando..."
|
||||
baixar "$(url_do_arquivo copiado_all.deb)" "$TMP/copiado_all.deb"
|
||||
baixar "$(url_do_arquivo SHA256SUMS)" "$TMP/SHA256SUMS"
|
||||
|
||||
echo "==> Conferindo a integridade do pacote..."
|
||||
(cd "$TMP" && sha256sum --check --strict --quiet SHA256SUMS) || {
|
||||
echo "ERRO: o pacote baixado não confere com o checksum publicado. Instalação cancelada."
|
||||
exit 1
|
||||
}
|
||||
chmod 644 "$TMP/copiado_all.deb"
|
||||
|
||||
echo "==> Instalando o pacote..."
|
||||
@@ -27,31 +49,10 @@ if [ -f "$HOME/.local/share/clipboard-winv/historico.json" ]; then
|
||||
rm -rf "$HOME/.local/share/clipboard-winv"
|
||||
fi
|
||||
|
||||
echo "==> Criando atalho Win+V..."
|
||||
LISTA=$(gsettings get $BASE custom-list)
|
||||
ID=""
|
||||
for i in $(python3 -c "import ast,sys; print(' '.join(ast.literal_eval(sys.argv[1].replace('@as ',''))))" "$LISTA"); do
|
||||
B=$(gsettings get "$BASE.custom-keybinding:/org/cinnamon/desktop/keybindings/custom-keybindings/$i/" binding)
|
||||
[ "$B" = "['<Super>v']" ] && ID=$i && break
|
||||
done
|
||||
if [ -z "$ID" ]; then
|
||||
for i in $(seq 0 99); do
|
||||
echo "$LISTA" | grep -q "'custom$i'" || { ID="custom$i"; break; }
|
||||
done
|
||||
LISTA=$(python3 -c "import ast,sys; l=ast.literal_eval(sys.argv[1].replace('@as ','')); l.append('$ID'); print(l)" "$LISTA")
|
||||
fi
|
||||
SCHEMA="$BASE.custom-keybinding:/org/cinnamon/desktop/keybindings/custom-keybindings/$ID/"
|
||||
gsettings set "$SCHEMA" name "Copiado"
|
||||
gsettings set "$SCHEMA" command "copiado show"
|
||||
gsettings set "$SCHEMA" binding "['<Super>v']"
|
||||
# só agora registra a lista (e força o Cinnamon a recarregar os atalhos)
|
||||
gsettings set $BASE custom-list "[]"
|
||||
echo "==> Criando atalho Win+V e iniciando..."
|
||||
pkill -f "/usr/bin/copiado" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
gsettings set $BASE custom-list "$LISTA"
|
||||
|
||||
echo "==> Iniciando..."
|
||||
pkill -x copiado 2>/dev/null || pkill -f "/usr/bin/copiado" 2>/dev/null || true
|
||||
setsid copiado >/dev/null 2>&1 &
|
||||
copiado consertar >/dev/null
|
||||
|
||||
echo
|
||||
echo "Pronto! Versão instalada: $(dpkg-query -W -f='${Version}' copiado). Aperte Win+V."
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uso: scripts/plano-ci.sh <url-do-servidor> <dono/repo> <sha> <token>
|
||||
# Imprime reuse=true quando este commit já passou por uma execução bem-sucedida
|
||||
# (o caso de uma tag num commit da main que já foi validado): não há o que
|
||||
# verificar de novo, o conteúdo é idêntico.
|
||||
set -euo pipefail
|
||||
server=$1; repo=$2; sha=$3; token=$4
|
||||
ok=$(curl -fsS -H "Authorization: token $token" \
|
||||
"$server/api/v1/repos/$repo/actions/runs?head_sha=$sha&status=success&limit=5" \
|
||||
| jq '[.workflow_runs[]? | select(.event == "push")] | length')
|
||||
echo "commit ${sha:0:8}: $ok execução(ões) bem-sucedida(s)" >&2
|
||||
reuse=false; [ "$ok" -gt 0 ] && reuse=true
|
||||
echo "reuse=$reuse"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Imprime as notas de uma release: a imagem publicada (quando houver) e a seção
|
||||
# do CHANGELOG.md daquela versão (ou o log de commits desde a tag anterior, se
|
||||
# não houver seção).
|
||||
#
|
||||
# scripts/release-notes.sh v1.2.3 [git.exemplo.com/dono/projeto]
|
||||
set -euo pipefail
|
||||
|
||||
tag=${1:?uso: release-notes.sh <tag> [imagem]}
|
||||
image=${2:-}
|
||||
version=${tag#v}
|
||||
|
||||
if [ -n "$image" ]; then
|
||||
echo "## Imagem"
|
||||
echo
|
||||
echo '```'
|
||||
echo "$image:$version"
|
||||
echo '```'
|
||||
echo
|
||||
fi
|
||||
|
||||
section=""
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
section=$(awk -v head="## [$version]" '
|
||||
index($0, head) == 1 { found = 1; next }
|
||||
found && /^## \[/ { exit }
|
||||
found && /^\[[^]]+\]: / { next }
|
||||
found { print }
|
||||
' CHANGELOG.md)
|
||||
fi
|
||||
|
||||
if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then
|
||||
printf '%s\n' "$section"
|
||||
else
|
||||
previous=$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)
|
||||
echo "## Mudanças${previous:+ desde $previous}"
|
||||
echo
|
||||
git log --no-merges --format='- %s (%h)' "${previous:+$previous..}$tag"
|
||||
fi
|
||||
Reference in new issue
Block a user