- Diálogo de preferências: seletor "insere espaços / insere tabulação" + campo de quantidade (1–16, validado e clampado) - tab-size do textarea e da camada de realce passa a seguir o valor via --tab-size, mantendo as duas camadas alinhadas - Persistido em localStorage; README atualizado
Notas — editor de notas web estilo Mousepad
Aplicação web completa para gerenciamento de notas inspirada no visual sóbrio e minimalista do Mousepad (editor de texto do XFCE): barra de menus superior, barra de ferramentas, área de texto limpa e barra de status na parte inferior.
Backend único em Go (binário estático de ~11 MB com o frontend já embutido), armazenamento em disco no estilo Git (blobs endereçados por SHA-256 + histórico de revisões com diff e restauração) e autenticação por token (Bearer para API / cookie HttpOnly para o navegador).
┌──────────────────────────────────────────────────────────────────────┐
│ ☰ Notas Arquivo Editar Exibir Ajuda │ ← barra de menus
├──────────────────────────────────────────────────────────────────────┤
│ +Nova 💾Salvar ⬇Baixar │ B I S H1 H2 • 1 ❝ 🔗 </> { } │ Formato ▾ │ ← barra de ferramentas
│ [ Editar | Preview ] 🕘Versões │
├──────────────┬──────────────────────────────────────┬────────────────
│ Buscar… │ Título da nota categoria │ HISTÓRICO DE │
│ CATEGORIA ▾ ├──────────────────────────────────────┤ │ REVISÕES │
│ TAGS │ # Placas │ │ hash · autor │
│ #linux 2 │ **nvme** em `/dev/nvme0` │ │ mensagem · data│
│ NOTAS │ - controladora #linux │ ├────────────────┤
│ nota1 .md │ … │ │ diff unificado │
│ nota2 .txt │ │ │ +restaurar │
├──────────────┴──────────────────────────────────────┴────────────────┤
│ nota.md .md Ln 4, Col 12 128 caracteres · 20 palavras ●● │ ← barra de status
└──────────────────────────────────────────────────────────────────────┘
Funcionalidades
- 3 formatos por nota: Texto puro (
.txt), Markdown (.md) e HTML (.html), alternáveis no editor; a nota é baixada com a extensão correspondente. - Barra de ferramentas de formatação: negrito, itálico, tachado, títulos H1/H2, listas, citação, link, código inline e bloco de código — inserindo sintaxe Markdown ou HTML conforme o formato ativo (desabilitada em texto puro).
- Modo Edição ↔ Preview: Markdown é renderizado no servidor (goldmark, GFM, HTML cru escapado);
HTML é pré-visualizado em
<iframe sandbox>; texto puro é exibido como está. - Categorias como árvore de pastas: o painel lateral traz um explorador de arquivos —
categorias aninhadas (subpastas), expansão/colapso, contagem de notas por pasta e menu de
contexto (Nova subpasta / Renomear / Excluir, sem ciclos). Uma nota pode pertencer a
várias categorias ao mesmo tempo (seletor multi-marca no topo do editor). Filtro por
pasta é recursivo por padrão (
?recursive=0desliga); há também "Todas as notas" e "Sem categoria". Dados antigos com o campo únicocategorysão migrados automaticamente na abertura (vira categoria raiz de mesmo nome). - Tags
#tagextraídas automaticamente do conteúdo (cabeçalhos Markdown como# Títulonão contam — só#tagsem espaço), com filtro por nuvem de tags e busca em título/categorias/conteúdo. - Abas de arquivos: abra várias notas ao mesmo tempo (clique na lista,
Ctrl+Nou botão ✚); cada aba tem estado sujo próprio (●), botão ✕ com confirmação, fechar com clique do meio, trocar comAlt+←/→ouAlt+1..9, fechar comAlt+W. As abas abertas (inclusive rascunhos não salvos) são restauradas entre sessões vialocalStorage. - Preferências do editor (menu Editar → Preferências… ou
Ctrl+,, em diálogo próprio):- Números de linha (gutter alinhado mesmo com quebra de linha; alternável pelo menu Exibir).
- Marcador de coluna customizável — padrão 80 (linha vertical no editor, exibida com quebra de linha desligada, como no Mousepad/gedit).
- Quebrar linha ligado/desligável (alternável também pelo menu Exibir).
- Tecla Tab: insere N espaços (padrão 4, configurável 1–16) ou o caractere de
tabulação
\t; o valor também define a largura exibida do\t(tab-size) no editor e no realce, mantendo as camadas alinhadas. - Fonte (DejaVu Sans Mono, Ubuntu Mono, Cascadia Code, Liberation Mono, Courier New ou monoespaçada do sistema) e tamanho (10–32 px).
- Esquema de cores / tema: Claro (Greybird), Escuro (Adwaita), Solarizado claro e Solarizado escuro — aplicam-se a toda a interface, incluindo preview e diffs.
- Colorização de sintaxe: Markdown (títulos, negrito/itálico, código inline e blocos
```, listas, citações, links, tags#tag, URLs) e HTML (tags, atributos, strings, comentários, entidades) renderizados por overlay sobre o textarea; desligável. - Tudo persistido no navegador (
localStorage), com pré-visualização ao vivo no diálogo, botões Cancelar (reverte) e Restaurar padrões.
- Versionamento estilo Git: cada salvamento de conteúdo cria uma revisão imutável (autor, data, mensagem, hash SHA-256 e parent). O painel de histórico mostra o diff unificado de cada revisão e permite restaurar qualquer versão anterior (a restauração vira uma nova revisão — nada se perde). Conteúdo idêntico é deduplicado (content-addressed).
- Autenticação por token: sem token, a interface exibe um diálogo pedindo o token; ele é
guardado em cookie HttpOnly/SameSite=Lax (30 dias). A API também aceita
Authorization: Bearer <token>para scripts. - Responsivo para celular (≤760px): barra lateral vira gaveta com backdrop (☰ fecha ao
tocar uma nota), painel de versões ocupa a tela toda, toolbar e menus rolam horizontalmente
sem rótulos, alvos de toque ampliados, diálogos em largura total, fonte do editor com mínimo
de 16px (evita zoom automático do iOS), altura em
dvhesafe-area-insetpara notch. - Estilo Mousepad: tema claro Greybird-like, menus nativos (Arquivo/Editar/Exibir/Ajuda),
atalhos
Ctrl+S,Ctrl+N,Ctrl+E/R(editar/preview),Ctrl+H(histórico),F9(barra lateral),Ctrl+B/I/K(formatação),Ctrl+,(preferências),Alt+W/Alt+←/→/Alt+1..9(abas), barra de status com arquivo, formato, linha/coluna, contagem de palavras e estado de salvamento.
Estrutura do projeto
.
├── cmd/notas/ # main: flags, env, graceful shutdown
├── internal/
│ ├── store/ # persistência em disco + revisões (estilo Git)
│ ├── diffx/ # diff unificado (gotextdiff/Myers)
│ └── api/ # rotas HTTP, auth, render de Markdown
├── web/ # frontend estático (go:embed no binário)
│ ├── index.html css/app.css js/app.js
├── Dockerfile # multi-stage: golang:alpine → scratch
├── docker-compose.yml
└── .env.example
Como rodar com Docker (recomendado)
cp .env.example .env
# edite .env e defina o token (gere um forte):
# openssl rand -hex 24
docker compose up -d
# abra http://localhost:8080 e informe o token na tela de login
Se a porta 8080 estiver ocupada: NOTAS_PORT=9090 docker compose up -d.
Com Podman: tudo acima funciona igualmente com podman-compose.
Os dados ficam no volume nomeado notas-data — docker compose down não os apaga
(não use down -v se quiser preservá-los). Faça backup copiando o volume.
Como rodar sem Docker
export NOTES_TOKEN=$(openssl rand -hex 24)
go run ./cmd/notas # escuta :8080, dados em ./data
# ou o binário:
go build -trimpath -ldflags="-s -w" -o notas ./cmd/notas
./notas -addr :8080 -data ./data
Configuração (variáveis de ambiente / flags)
| Variável | Flag | Padrão | Descrição |
|---|---|---|---|
NOTES_TOKEN |
— | (sem) | Obrigatório. Token de acesso. |
NOTES_DATA_DIR |
-data |
./data |
Diretório de dados (notas + blobs). |
NOTES_ADDR |
-addr |
:8080 |
Endereço de escuta. |
API (para scripts)
Todos os endpoints sob /api/* exigem o token, exceto login/logout/me e o frontend.
T=$NOTES_TOKEN; B=http://localhost:8080
curl -H "Authorization: Bearer $T" $B/api/notes # lista (?category=<id|none>&recursive=0&tag=&q=)
curl -H "Authorization: Bearer $T" $B/api/notes/<id> # nota + conteúdo
curl -s -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","categories":["<id>"],"content":"# Oi #linux"}' $B/api/notes
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"title":"Olá","format":"md","categories":["<id>"],"content":"editado","author":"lg","message":"revisão 2"}' \
$B/api/notes/<id>
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions # histórico
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions/<hash>/diff # diff unificado
curl -X POST -H "Authorization: Bearer $T" $B/api/notes/<id>/revisions/<hash>/restore \
-H 'Content-Type: application/json' -d '{"author":"lg"}' # restaurar
curl -H "Authorization: Bearer $T" $B/api/notes/<id>/raw # download .txt/.md/.html
curl -H "Authorization: Bearer $T" $B/api/tags $B/api/categories # agregações
# árvore de categorias (lista plana; parent="" é raiz)
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"name":"devops","parent":""}' $B/api/categories
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
-d '{"name":"infra","parent":"<novo-pai>"}' $B/api/categories/<id> # renomear/mover
curl -X DELETE -H "Authorization: Bearer $T" $B/api/categories/<id> # remove subtree
Como funciona o versionamento
- O conteúdo é guardado em
data/blobs/<aa>/<hash-sha256>(content-addressed, como objetos do Git): conteúdo repetido ocupa um único blob. - Cada nota tem
data/notes/<id>/meta.json(estado atual) ehistory.json(lista ordenada de revisões:hash,parent,author,when,message,bytes). - A árvore de categorias fica em
data/categories.json(lista plana comparent; nomes únicos entre irmãos; mover para dentro de si mesma é recusado). - Salvar sem alterar o conteúdo atualiza apenas os metadados (título/categorias/formato) — sem revisão nova. Restaurar cria uma revisão nova apontando para o blob antigo.
- Apagar uma nota faz GC dos blobs que ficaram sem referência.
Segurança
- Token verificado em tempo constante; nunca é registrado nos logs.
- Sessão do navegador em cookie HttpOnly + SameSite=Lax (mitiga CSRF); o token em
localStoragefoi preterido de propósito por ser exposto a XSS. - CSP restritiva (
default-src 'self'),X-Frame-Options: DENY,nosniff; preview de HTML roda em iframesandboxsem mesma origem; Markdown renderiza com HTML cru escapado. - Limite de 2 MiB por nota; corpo HTTP limitado a 4 MiB.
- Rode atrás de TLS (reverse proxy) em produção — o cookie marca
Secureautomaticamente quando a requisição chega por HTTPS/X-Forwarded-Proto. O modelo é token único compartilhado (sem usuários múltiplos); a imagem final éscratchnon-root (uid 65534) — sem shell paraexecde depuração, por design.
Testes
go test ./... # store (revisões/GC/tags), diffx (diff unificado), api (fluxo HTTP completo)
go vet ./...
node --check web/js/app.js