kotsu-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotsu_cli-0.1.0.dist-info/METADATA +252 -0
- kotsu_cli-0.1.0.dist-info/RECORD +33 -0
- kotsu_cli-0.1.0.dist-info/WHEEL +4 -0
- kotsu_cli-0.1.0.dist-info/entry_points.txt +3 -0
- kotsu_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- money_cli/__init__.py +3 -0
- money_cli/__main__.py +197 -0
- money_cli/armazenamento/__init__.py +4 -0
- money_cli/armazenamento/backup.py +70 -0
- money_cli/armazenamento/banco.py +525 -0
- money_cli/armazenamento/caminhos.py +72 -0
- money_cli/armazenamento/migracoes/0001_inicial.sql +35 -0
- money_cli/armazenamento/migracoes/0002_recorrencia.sql +21 -0
- money_cli/armazenamento/migracoes/0003_mesclagem.sql +54 -0
- money_cli/armazenamento/migracoes/__init__.py +32 -0
- money_cli/armazenamento/preferencias.py +31 -0
- money_cli/armazenamento/trava.py +123 -0
- money_cli/armazenamento/troca.py +285 -0
- money_cli/dominio/__init__.py +5 -0
- money_cli/dominio/dinheiro.py +10 -0
- money_cli/dominio/filtro.py +63 -0
- money_cli/dominio/linha.py +179 -0
- money_cli/dominio/mes.py +57 -0
- money_cli/dominio/mescla.py +440 -0
- money_cli/dominio/parcelas.py +52 -0
- money_cli/dominio/recorrencia.py +20 -0
- money_cli/dominio/tipos.py +95 -0
- money_cli/dominio/totais.py +17 -0
- money_cli/tela/__init__.py +1 -0
- money_cli/tela/app.py +756 -0
- money_cli/tela/cores.py +31 -0
- money_cli/tela/perguntas.py +144 -0
- money_cli/tela/temas.py +74 -0
money_cli/tela/cores.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Paleta fixa do app. Mudanças aqui são poucas e de propósito (boas práticas)."""
|
|
2
|
+
|
|
3
|
+
# Cores de valor e de texto apagado; mudam com o tema (`aplicar`, D37).
|
|
4
|
+
RECEITA = "#4ec27a"
|
|
5
|
+
DESPESA = "#e5534b"
|
|
6
|
+
NEUTRO = "#8b949e"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def aplicar(receita: str, despesa: str, neutro: str) -> None:
|
|
10
|
+
"""Troca as cores de valor para as do tema atual."""
|
|
11
|
+
global RECEITA, DESPESA, NEUTRO
|
|
12
|
+
RECEITA, DESPESA, NEUTRO = receita, despesa, neutro
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Cores oferecidas ao criar uma conta (D3). Legíveis em fundo escuro
|
|
16
|
+
# no Windows Terminal e em terminais Linux com truecolor.
|
|
17
|
+
CONTAS = (
|
|
18
|
+
"#8a2be2", # roxo
|
|
19
|
+
"#ff7a00", # laranja
|
|
20
|
+
"#1f8fff", # azul
|
|
21
|
+
"#e8c547", # amarelo
|
|
22
|
+
"#2ec4b6", # ciano
|
|
23
|
+
"#ff5fa2", # rosa
|
|
24
|
+
"#9bd34a", # verde-limão
|
|
25
|
+
"#c0c0c0", # prata
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def do_saldo(centavos: int) -> str:
|
|
30
|
+
"""Negativo aparece só em vermelho, sem o sinal."""
|
|
31
|
+
return DESPESA if centavos < 0 else RECEITA
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Perguntas modais da tela. Cada uma devolve a resposta, ou None no Esc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.screen import ModalScreen
|
|
10
|
+
from textual.widgets import Input, Static
|
|
11
|
+
|
|
12
|
+
from money_cli.dominio.tipos import Alcance
|
|
13
|
+
from money_cli.tela import cores
|
|
14
|
+
|
|
15
|
+
_CSS = """
|
|
16
|
+
Pergunta { align: center middle; }
|
|
17
|
+
Pergunta > Vertical {
|
|
18
|
+
width: 64; height: auto; padding: 1 2;
|
|
19
|
+
border: round $accent; background: $surface;
|
|
20
|
+
}
|
|
21
|
+
Pergunta .dica { color: $text-muted; margin-top: 1; }
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Pergunta(ModalScreen):
|
|
26
|
+
DEFAULT_CSS = _CSS
|
|
27
|
+
BINDINGS = [Binding("escape", "cancelar", "Cancelar")]
|
|
28
|
+
|
|
29
|
+
def __init__(self, titulo: str | Text, corpo: str | Text = "", dica: str = "") -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.titulo, self.corpo, self.dica = titulo, corpo, dica
|
|
32
|
+
|
|
33
|
+
def compose(self) -> ComposeResult:
|
|
34
|
+
with Vertical():
|
|
35
|
+
yield Static(self.titulo, classes="titulo")
|
|
36
|
+
if self.corpo:
|
|
37
|
+
yield Static(self.corpo, classes="corpo")
|
|
38
|
+
yield from self.extras()
|
|
39
|
+
if self.dica:
|
|
40
|
+
yield Static(self.dica, classes="dica")
|
|
41
|
+
|
|
42
|
+
def extras(self) -> ComposeResult:
|
|
43
|
+
yield from ()
|
|
44
|
+
|
|
45
|
+
def action_cancelar(self) -> None:
|
|
46
|
+
self.dismiss(None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SimOuNao(Pergunta):
|
|
50
|
+
"""Enter ou s = sim; Esc ou n = não (devolve None)."""
|
|
51
|
+
|
|
52
|
+
BINDINGS = [
|
|
53
|
+
Binding("enter,s", "sim", "Sim"),
|
|
54
|
+
Binding("n", "cancelar", "Não"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
def __init__(self, titulo: str | Text, corpo: str | Text = "") -> None:
|
|
58
|
+
super().__init__(titulo, corpo, "Enter = sim Esc = não")
|
|
59
|
+
|
|
60
|
+
def action_sim(self) -> None:
|
|
61
|
+
self.dismiss(True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class EscolherCor(Pergunta):
|
|
65
|
+
"""Criar conta (D3, D11): 1 a 8 escolhem a cor; Enter usa a sugerida."""
|
|
66
|
+
|
|
67
|
+
BINDINGS = [
|
|
68
|
+
Binding("enter", "cor(-1)", "Sugerida"),
|
|
69
|
+
*(Binding(str(i + 1), f"cor({i})", show=False) for i in range(len(cores.CONTAS))),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
def __init__(self, nome: str, sugerida: str) -> None:
|
|
73
|
+
amostras = Text()
|
|
74
|
+
for i, cor in enumerate(cores.CONTAS):
|
|
75
|
+
marca = "▸" if cor == sugerida else " "
|
|
76
|
+
amostras.append(f"{marca}{i + 1} ", style="bold" if cor == sugerida else "")
|
|
77
|
+
amostras.append("██ ", style=cor)
|
|
78
|
+
titulo = Text.assemble("Criar a conta ", (nome, f"bold {sugerida}"), "?")
|
|
79
|
+
super().__init__(titulo, amostras, "1–8 = cor Enter = cor marcada Esc = não criar")
|
|
80
|
+
self.sugerida = sugerida
|
|
81
|
+
|
|
82
|
+
def action_cor(self, indice: int) -> None:
|
|
83
|
+
self.dismiss(self.sugerida if indice < 0 else cores.CONTAS[indice])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SoEstaOuTodas(Pergunta):
|
|
87
|
+
"""Pergunta da parcela (D12). Enter = todas (padrão)."""
|
|
88
|
+
|
|
89
|
+
BINDINGS = [
|
|
90
|
+
Binding("enter,t", "todas", "Todas"),
|
|
91
|
+
Binding("s", "so_esta", "Só esta"),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
def __init__(self, acao: str, rotulo: str) -> None:
|
|
95
|
+
super().__init__(
|
|
96
|
+
f"{acao} a parcela {rotulo}: só esta ou todas?",
|
|
97
|
+
dica="Enter ou t = todas s = só esta Esc = cancelar",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def action_todas(self) -> None:
|
|
101
|
+
self.dismiss(Alcance.TODAS)
|
|
102
|
+
|
|
103
|
+
def action_so_esta(self) -> None:
|
|
104
|
+
self.dismiss(Alcance.SO_ESTA)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SoEsteOuProximos(Pergunta):
|
|
108
|
+
"""Pergunta do lançamento recorrente (D36). Enter = só este mês (o mais seguro)."""
|
|
109
|
+
|
|
110
|
+
BINDINGS = [
|
|
111
|
+
Binding("enter,s", "so_este", "Só este mês"),
|
|
112
|
+
Binding("p", "proximos", "Este e os próximos"),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
def __init__(self, acao: str) -> None:
|
|
116
|
+
super().__init__(
|
|
117
|
+
f"{acao} o lançamento que se repete todo mês: só este mês ou os próximos?",
|
|
118
|
+
dica="Enter ou s = só este mês p = este e os próximos Esc = cancelar",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def action_so_este(self) -> None:
|
|
122
|
+
self.dismiss(Alcance.SO_ESTA)
|
|
123
|
+
|
|
124
|
+
def action_proximos(self) -> None:
|
|
125
|
+
self.dismiss(Alcance.PROXIMAS)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PedirTexto(Pergunta):
|
|
129
|
+
"""Campo de texto já preenchido. Enter confirma; Esc cancela."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, titulo: str, valor: str) -> None:
|
|
132
|
+
super().__init__(titulo, dica="Enter = salvar Esc = cancelar")
|
|
133
|
+
self.valor = valor
|
|
134
|
+
|
|
135
|
+
def extras(self) -> ComposeResult:
|
|
136
|
+
yield Input(self.valor, id="texto")
|
|
137
|
+
|
|
138
|
+
def on_mount(self) -> None:
|
|
139
|
+
self.query_one("#texto", Input).focus()
|
|
140
|
+
|
|
141
|
+
def on_input_submitted(self, evento: Input.Submitted) -> None:
|
|
142
|
+
evento.stop()
|
|
143
|
+
texto = evento.value.strip()
|
|
144
|
+
self.dismiss(texto or None)
|
money_cli/tela/temas.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Temas da tela (D37): Algodão-Doce claro e noite. Só cores e enfeites, sem regra de negócio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from textual.theme import Theme
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Tema:
|
|
12
|
+
nome: str # identificador gravado em tema.toml
|
|
13
|
+
rotulo: str
|
|
14
|
+
escuro: bool
|
|
15
|
+
fundo: str
|
|
16
|
+
superficie: str
|
|
17
|
+
painel: str
|
|
18
|
+
texto: str
|
|
19
|
+
destaque: str # rosa: título, cabeçalho, cursor
|
|
20
|
+
secundaria: str # azul: rótulos do topo
|
|
21
|
+
aviso: str # dourado: rostinho
|
|
22
|
+
receita: str
|
|
23
|
+
despesa: str
|
|
24
|
+
neutro: str
|
|
25
|
+
# Enfeites.
|
|
26
|
+
marcador: str = "♡"
|
|
27
|
+
zerada: str = "zzz… zerada"
|
|
28
|
+
barra_cheia: str = "♥"
|
|
29
|
+
barra_vazia: str = "·"
|
|
30
|
+
feliz: str = "(◕‿◕)♡"
|
|
31
|
+
triste: str = "(;ω;)"
|
|
32
|
+
|
|
33
|
+
def para_textual(self) -> Theme:
|
|
34
|
+
return Theme(
|
|
35
|
+
name=self.nome,
|
|
36
|
+
primary=self.destaque,
|
|
37
|
+
secondary=self.secundaria,
|
|
38
|
+
accent=self.aviso,
|
|
39
|
+
background=self.fundo,
|
|
40
|
+
surface=self.superficie,
|
|
41
|
+
panel=self.painel,
|
|
42
|
+
foreground=self.texto,
|
|
43
|
+
success=self.receita,
|
|
44
|
+
error=self.despesa,
|
|
45
|
+
warning=self.aviso,
|
|
46
|
+
dark=self.escuro,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
ALGODAO_NOITE = Tema(
|
|
51
|
+
nome="algodao-noite", rotulo="Algodão-Doce noite", escuro=True,
|
|
52
|
+
fundo="#1d1424", superficie="#291b33", painel="#41274f", texto="#ffe9f4",
|
|
53
|
+
destaque="#ff7fb8", secundaria="#8fb3ff", aviso="#ffd27a",
|
|
54
|
+
receita="#86efc0", despesa="#ff7a9e", neutro="#9a7fa8",
|
|
55
|
+
) # fmt: skip
|
|
56
|
+
|
|
57
|
+
ALGODAO_CLARO = Tema(
|
|
58
|
+
nome="algodao-claro", rotulo="Algodão-Doce claro", escuro=False,
|
|
59
|
+
fundo="#fff5fa", superficie="#ffeaf4", painel="#ffd6e8", texto="#5a3a5e",
|
|
60
|
+
destaque="#e0509a", secundaria="#4f74c7", aviso="#b87400",
|
|
61
|
+
receita="#1f9d6b", despesa="#d6336c", neutro="#8f6f8c",
|
|
62
|
+
) # fmt: skip
|
|
63
|
+
|
|
64
|
+
TEMAS = (ALGODAO_NOITE, ALGODAO_CLARO)
|
|
65
|
+
PADRAO = ALGODAO_NOITE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def tema_por_nome(nome: str | None) -> Tema:
|
|
69
|
+
"""Tema de nome conhecido; qualquer outra coisa volta ao padrão."""
|
|
70
|
+
return next((t for t in TEMAS if t.nome == nome), PADRAO)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def proximo(atual: Tema) -> Tema:
|
|
74
|
+
return TEMAS[(TEMAS.index(atual) + 1) % len(TEMAS)]
|