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.
@@ -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)
@@ -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)]