lazyplan 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.
- lazyplan/__init__.py +0 -0
- lazyplan/app.py +102 -0
- lazyplan/cli.py +53 -0
- lazyplan/github.py +126 -0
- lazyplan/lazyplan.tcss +332 -0
- lazyplan/models.py +44 -0
- lazyplan/screens/__init__.py +0 -0
- lazyplan/screens/base.py +20 -0
- lazyplan/screens/confirm_delete.py +53 -0
- lazyplan/screens/detail.py +123 -0
- lazyplan/screens/editor.py +160 -0
- lazyplan/screens/github_screen.py +132 -0
- lazyplan/screens/main.py +203 -0
- lazyplan/storage.py +59 -0
- lazyplan-0.1.0.dist-info/METADATA +8 -0
- lazyplan-0.1.0.dist-info/RECORD +18 -0
- lazyplan-0.1.0.dist-info/WHEEL +4 -0
- lazyplan-0.1.0.dist-info/entry_points.txt +2 -0
lazyplan/__init__.py
ADDED
|
File without changes
|
lazyplan/app.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
from textual.theme import Theme
|
|
3
|
+
from textual.app import App
|
|
4
|
+
|
|
5
|
+
from lazyplan.models import Project
|
|
6
|
+
from lazyplan.storage import load_projects, save_projects, upsert_project, delete_project
|
|
7
|
+
from lazyplan.screens.main import MainScreen
|
|
8
|
+
from lazyplan.screens.detail import DetailScreen
|
|
9
|
+
from lazyplan.screens.editor import EditorScreen
|
|
10
|
+
from lazyplan.screens.confirm_delete import ConfirmDeleteScreen
|
|
11
|
+
from lazyplan.screens.github_screen import GithubScreen
|
|
12
|
+
|
|
13
|
+
arctic_theme = Theme(
|
|
14
|
+
name="onyx-violet",
|
|
15
|
+
primary="#A78BFA",
|
|
16
|
+
secondary="#7C3AED",
|
|
17
|
+
accent="#C4B5FD",
|
|
18
|
+
background="#0C0C10",
|
|
19
|
+
surface="#16151D",
|
|
20
|
+
panel="#100F17",
|
|
21
|
+
foreground="#EDEDF0",
|
|
22
|
+
boost="#0A0912",
|
|
23
|
+
success="#4ADE80",
|
|
24
|
+
warning="#FCD34D",
|
|
25
|
+
error="#F87171",
|
|
26
|
+
dark=True,
|
|
27
|
+
variables={
|
|
28
|
+
"block-cursor-text-style": "none",
|
|
29
|
+
"footer-key-foreground": "#A78BFA",
|
|
30
|
+
"input-selection-background": "#A78BFA 35%",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LazyPlanApp(App):
|
|
36
|
+
"""TUI para capturar y gestionar ideas de proyectos."""
|
|
37
|
+
|
|
38
|
+
CSS_PATH = "lazyplan.tcss"
|
|
39
|
+
TITLE = "LazyPlan"
|
|
40
|
+
DARK = True
|
|
41
|
+
|
|
42
|
+
# SCREENS = {
|
|
43
|
+
# "main": MainScreen,
|
|
44
|
+
# "detail": DetailScreen,
|
|
45
|
+
# "editor": EditorScreen,
|
|
46
|
+
# "confirm_delete": ConfirmDeleteScreen,
|
|
47
|
+
# "github": GithubScreen,
|
|
48
|
+
# }
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self._projects = load_projects()
|
|
53
|
+
|
|
54
|
+
def on_mount(self) -> None:
|
|
55
|
+
self.register_theme(arctic_theme)
|
|
56
|
+
self.theme = "onyx-violet"
|
|
57
|
+
self.push_screen(MainScreen(self._projects))
|
|
58
|
+
|
|
59
|
+
# ── Screen helpers ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def push_screen(self, screen, *args, **kwargs):
|
|
62
|
+
"""Override para pasar argumentos a las screens por nombre."""
|
|
63
|
+
if isinstance(screen, str):
|
|
64
|
+
name = screen
|
|
65
|
+
if name == "detail" and args:
|
|
66
|
+
screen = DetailScreen(args[0])
|
|
67
|
+
elif name == "editor":
|
|
68
|
+
screen = EditorScreen(args[0] if args else None)
|
|
69
|
+
elif name == "confirm_delete" and args:
|
|
70
|
+
screen = ConfirmDeleteScreen(args[0])
|
|
71
|
+
elif name == "github" and args:
|
|
72
|
+
screen = GithubScreen(args[0])
|
|
73
|
+
else:
|
|
74
|
+
screen = self.SCREENS[name]()
|
|
75
|
+
return super().push_screen(screen, **kwargs)
|
|
76
|
+
|
|
77
|
+
# ── Data operations (llamadas desde cualquier screen) ────────────────────
|
|
78
|
+
|
|
79
|
+
def save_project(self, project: Project) -> None:
|
|
80
|
+
self._projects = upsert_project(self._projects, project)
|
|
81
|
+
save_projects(self._projects)
|
|
82
|
+
self._refresh_main()
|
|
83
|
+
|
|
84
|
+
def delete_project(self, project_id: str) -> None:
|
|
85
|
+
self._projects = delete_project(self._projects, project_id)
|
|
86
|
+
save_projects(self._projects)
|
|
87
|
+
self._refresh_main()
|
|
88
|
+
|
|
89
|
+
def _refresh_main(self) -> None:
|
|
90
|
+
"""Actualiza la lista en MainScreen si está en el stack."""
|
|
91
|
+
for screen in self.screen_stack:
|
|
92
|
+
if isinstance(screen, MainScreen):
|
|
93
|
+
screen.refresh_projects(self._projects)
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run():
|
|
98
|
+
LazyPlanApp().run()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
run()
|
lazyplan/cli.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from lazyplan.storage import load_projects, save_projects, upsert_project
|
|
3
|
+
from lazyplan.models import Project, Status
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(
|
|
6
|
+
name="lazyplan",
|
|
7
|
+
help="TUI para capturar y gestionar ideas de proyectos.",
|
|
8
|
+
no_args_is_help=False,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.callback(invoke_without_command=True)
|
|
13
|
+
def default(ctx: typer.Context):
|
|
14
|
+
"""Abre la TUI si no se pasa ningún subcomando."""
|
|
15
|
+
if ctx.invoked_subcommand is None:
|
|
16
|
+
from lazyplan.app import LazyPlanApp
|
|
17
|
+
LazyPlanApp().run()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def new(
|
|
22
|
+
title: str = typer.Option(..., "-t", "--title", help="Título del proyecto"),
|
|
23
|
+
stack: str = typer.Option("", "-s", "--stack", help="Stack separado por comas"),
|
|
24
|
+
desc: str = typer.Option("", "-d", "--desc", help="Descripción"),
|
|
25
|
+
status: str = typer.Option("cruda", "--status", help="Estado inicial"),
|
|
26
|
+
):
|
|
27
|
+
"""Guarda un proyecto rápido sin abrir la TUI."""
|
|
28
|
+
stack_list = [s.strip() for s in stack.split(",") if s.strip()]
|
|
29
|
+
try:
|
|
30
|
+
st = Status(status)
|
|
31
|
+
except ValueError:
|
|
32
|
+
st = Status.CRUDA
|
|
33
|
+
|
|
34
|
+
project = Project(title=title, description=desc, stack=stack_list, status=st)
|
|
35
|
+
projects = load_projects()
|
|
36
|
+
projects = upsert_project(projects, project)
|
|
37
|
+
save_projects(projects)
|
|
38
|
+
typer.echo(f"✅ Proyecto '{title}' guardado [ID: {project.id}]")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def ls():
|
|
43
|
+
"""Lista los proyectos en la terminal (sin TUI)."""
|
|
44
|
+
projects = load_projects()
|
|
45
|
+
if not projects:
|
|
46
|
+
typer.echo("Sin proyectos todavía. Usá: lazyplan new -t 'Mi idea'")
|
|
47
|
+
return
|
|
48
|
+
for p in projects:
|
|
49
|
+
typer.echo(f" {p.id} {p.status_label} {p.title} [{p.stack_str}]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
app()
|
lazyplan/github.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integración con GitHub CLI (gh) para LazyPlan.
|
|
3
|
+
Permite crear repos y hacer push inicial desde la TUI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class GitHubResult:
|
|
13
|
+
success: bool
|
|
14
|
+
url: str = ""
|
|
15
|
+
error: str = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def gh_available() -> bool:
|
|
19
|
+
"""Devuelve True si gh está instalado y autenticado."""
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["gh", "auth", "status"],
|
|
23
|
+
capture_output=True, text=True
|
|
24
|
+
)
|
|
25
|
+
return result.returncode == 0
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def gh_username() -> str:
|
|
31
|
+
"""Devuelve el username de GitHub autenticado."""
|
|
32
|
+
try:
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
["gh", "api", "user", "--jq", ".login"],
|
|
35
|
+
capture_output=True, text=True
|
|
36
|
+
)
|
|
37
|
+
return result.stdout.strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_github_repo(
|
|
43
|
+
title: str,
|
|
44
|
+
description: str = "",
|
|
45
|
+
private: bool = True,
|
|
46
|
+
folder_path: str = "",
|
|
47
|
+
) -> GitHubResult:
|
|
48
|
+
"""
|
|
49
|
+
Crea un repo en GitHub y hace push inicial si hay carpeta.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
title: Nombre del proyecto (se convierte a slug)
|
|
53
|
+
description: Descripción del proyecto
|
|
54
|
+
private: True = privado, False = público
|
|
55
|
+
folder_path: Ruta local del proyecto para git init + push
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
GitHubResult con success, url y error
|
|
59
|
+
"""
|
|
60
|
+
slug = _to_slug(title)
|
|
61
|
+
visibility = "--private" if private else "--public"
|
|
62
|
+
|
|
63
|
+
# 1. Crear el repo en GitHub
|
|
64
|
+
cmd = ["gh", "repo", "create", slug, visibility, "--confirm"]
|
|
65
|
+
if description:
|
|
66
|
+
cmd += ["--description", description[:255]]
|
|
67
|
+
|
|
68
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
69
|
+
if result.returncode != 0:
|
|
70
|
+
return GitHubResult(
|
|
71
|
+
success=False,
|
|
72
|
+
error=result.stderr.strip() or "Error al crear el repositorio"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# URL del repo
|
|
76
|
+
username = gh_username()
|
|
77
|
+
repo_url = f"https://github.com/{username}/{slug}"
|
|
78
|
+
|
|
79
|
+
# 2. Si hay carpeta local, inicializar git y hacer push
|
|
80
|
+
if folder_path:
|
|
81
|
+
folder = Path(folder_path)
|
|
82
|
+
if folder.exists():
|
|
83
|
+
push_result = _git_init_and_push(folder, repo_url, slug)
|
|
84
|
+
if not push_result.success:
|
|
85
|
+
# El repo se creó pero el push falló: no es fatal
|
|
86
|
+
return GitHubResult(
|
|
87
|
+
success=True,
|
|
88
|
+
url=repo_url,
|
|
89
|
+
error=f"Repo creado pero push falló: {push_result.error}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return GitHubResult(success=True, url=repo_url)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _git_init_and_push(folder: Path, repo_url: str, slug: str) -> GitHubResult:
|
|
96
|
+
"""git init + add + commit + push en la carpeta dada."""
|
|
97
|
+
commands = [
|
|
98
|
+
["git", "init"],
|
|
99
|
+
["git", "add", "."],
|
|
100
|
+
["git", "commit", "-m", "Initial commit – LazyPlan 💤"],
|
|
101
|
+
["git", "branch", "-M", "main"],
|
|
102
|
+
["git", "remote", "add", "origin", f"{repo_url}.git"],
|
|
103
|
+
["git", "push", "-u", "origin", "main"],
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for cmd in commands:
|
|
107
|
+
result = subprocess.run(
|
|
108
|
+
cmd, cwd=str(folder), capture_output=True, text=True
|
|
109
|
+
)
|
|
110
|
+
if result.returncode != 0:
|
|
111
|
+
return GitHubResult(
|
|
112
|
+
success=False,
|
|
113
|
+
error=f"Falló '{' '.join(cmd)}': {result.stderr.strip()}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return GitHubResult(success=True, url=repo_url)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _to_slug(title: str) -> str:
|
|
120
|
+
"""Convierte un título a slug válido para GitHub."""
|
|
121
|
+
import re
|
|
122
|
+
slug = title.lower().strip()
|
|
123
|
+
slug = re.sub(r"[^\w\s-]", "", slug)
|
|
124
|
+
slug = re.sub(r"[\s_]+", "-", slug)
|
|
125
|
+
slug = re.sub(r"-+", "-", slug).strip("-")
|
|
126
|
+
return slug or "mi-proyecto"
|
lazyplan/lazyplan.tcss
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/* ── Global ─────────────────────────────────────────────── */
|
|
6
|
+
Screen {
|
|
7
|
+
background: $background;
|
|
8
|
+
overflow-y: auto;
|
|
9
|
+
}
|
|
10
|
+
/* ── Main screen ────────────────────────────────────────── */
|
|
11
|
+
#main-layout {
|
|
12
|
+
height: 1fr;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#app-title {
|
|
16
|
+
color: $primary;
|
|
17
|
+
text-style: bold;
|
|
18
|
+
height: 1;
|
|
19
|
+
margin-bottom: 1;
|
|
20
|
+
margin-top: 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
#search-bar {
|
|
25
|
+
height: 3;
|
|
26
|
+
padding: 0 1;
|
|
27
|
+
margin-bottom: 1;
|
|
28
|
+
}
|
|
29
|
+
#search {
|
|
30
|
+
width: 1fr;
|
|
31
|
+
border: round $secondary;
|
|
32
|
+
background: $background;
|
|
33
|
+
&:focus {
|
|
34
|
+
background-tint: $background;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#filter-bar {
|
|
39
|
+
height: 1;
|
|
40
|
+
padding: 0 2;
|
|
41
|
+
align: left middle;
|
|
42
|
+
}
|
|
43
|
+
#filter-label {
|
|
44
|
+
margin-right: 1;
|
|
45
|
+
color: $foreground;
|
|
46
|
+
}
|
|
47
|
+
.filter-btn {
|
|
48
|
+
padding: 0 2;
|
|
49
|
+
margin-right: 1;
|
|
50
|
+
color: $foreground;
|
|
51
|
+
}
|
|
52
|
+
.filter-btn.active {
|
|
53
|
+
color: $primary;
|
|
54
|
+
text-style: bold underline;
|
|
55
|
+
background: $boost;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#project-table {
|
|
59
|
+
height: 1fr;
|
|
60
|
+
border: round $secondary;
|
|
61
|
+
margin-bottom: 1;
|
|
62
|
+
margin-left: 1;
|
|
63
|
+
margin-right: 1;
|
|
64
|
+
background: $background;
|
|
65
|
+
&:focus {
|
|
66
|
+
background-tint: $background;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
#status-bar {
|
|
70
|
+
padding: 0 2;
|
|
71
|
+
color: $foreground;
|
|
72
|
+
margin-bottom: 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/* ── Detail screen ──────────────────────────────────────── */
|
|
77
|
+
#detail-scroll {
|
|
78
|
+
height: 1fr;
|
|
79
|
+
overflow-y: auto;
|
|
80
|
+
background: $background;
|
|
81
|
+
}
|
|
82
|
+
#detail-content {
|
|
83
|
+
padding: 1 2;
|
|
84
|
+
height: auto;
|
|
85
|
+
}
|
|
86
|
+
/* Bloque 1 — Header */
|
|
87
|
+
#detail-header {
|
|
88
|
+
height: 3;
|
|
89
|
+
background: $boost;
|
|
90
|
+
border: round $secondary;
|
|
91
|
+
margin-bottom: 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#detail-title {
|
|
95
|
+
color: #FFF;
|
|
96
|
+
text-style: bold;
|
|
97
|
+
width: 1fr;
|
|
98
|
+
content-align: left middle;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#status-badge {
|
|
102
|
+
color: $primary;
|
|
103
|
+
text-style: bold;
|
|
104
|
+
content-align: right middle;
|
|
105
|
+
width: 1fr;
|
|
106
|
+
padding: 0 1;
|
|
107
|
+
}
|
|
108
|
+
#badges {
|
|
109
|
+
height: 2;
|
|
110
|
+
margin-bottom: 0;
|
|
111
|
+
background: $boost;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#id-badge {
|
|
115
|
+
color: $foreground;
|
|
116
|
+
}
|
|
117
|
+
/* Bloque 2 — Descripción */
|
|
118
|
+
#desc-block {
|
|
119
|
+
border: round $secondary;
|
|
120
|
+
background: $boost;
|
|
121
|
+
margin-bottom: 1;
|
|
122
|
+
padding: 0 2 0 2;
|
|
123
|
+
height: auto;
|
|
124
|
+
max-height: 10;
|
|
125
|
+
}
|
|
126
|
+
/* Bloque 3 — Info + Metadatos */
|
|
127
|
+
#info-block {
|
|
128
|
+
height: auto;
|
|
129
|
+
margin-bottom: 1;
|
|
130
|
+
}
|
|
131
|
+
/* Bloque 4 — Links + Fechas */
|
|
132
|
+
#links-block {
|
|
133
|
+
height: auto;
|
|
134
|
+
margin-bottom: 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Columnas compartidas */
|
|
138
|
+
.detail-col {
|
|
139
|
+
width: 1fr;
|
|
140
|
+
border: round $secondary;
|
|
141
|
+
background: $boost;
|
|
142
|
+
padding: 0 2;
|
|
143
|
+
height: auto;
|
|
144
|
+
max-height: 100%;
|
|
145
|
+
}
|
|
146
|
+
/* Títulos de bloque */
|
|
147
|
+
.block-title {
|
|
148
|
+
color: $foreground;
|
|
149
|
+
text-style: bold;
|
|
150
|
+
border-bottom: tall $secondary;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Campos */
|
|
154
|
+
.field-label {
|
|
155
|
+
color: $secondary;
|
|
156
|
+
height: 1;
|
|
157
|
+
margin-top: 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.field-value {
|
|
161
|
+
color: $foreground;
|
|
162
|
+
margin-bottom: 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#detail-desc {
|
|
166
|
+
color: $foreground;
|
|
167
|
+
margin-top: 1;
|
|
168
|
+
margin-bottom: 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#detail-links {
|
|
172
|
+
color: $foreground;
|
|
173
|
+
margin-bottom: 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#detail-hint {
|
|
177
|
+
margin-top: 1;
|
|
178
|
+
color: $secondary;
|
|
179
|
+
text-align: center;
|
|
180
|
+
height: 1;
|
|
181
|
+
}
|
|
182
|
+
/* ── Editor screen ──────────────────────────────────────── */
|
|
183
|
+
#editor-scroll {
|
|
184
|
+
height: 1fr;
|
|
185
|
+
overflow-y: auto;
|
|
186
|
+
background: $background;
|
|
187
|
+
}
|
|
188
|
+
#editor-form {
|
|
189
|
+
padding: 1 2;
|
|
190
|
+
height: auto;
|
|
191
|
+
}
|
|
192
|
+
#editor-title {
|
|
193
|
+
background: $boost;
|
|
194
|
+
color: $foreground;
|
|
195
|
+
text-style: bold;
|
|
196
|
+
padding: 0 2;
|
|
197
|
+
height: 1;
|
|
198
|
+
margin-bottom: 1;
|
|
199
|
+
}
|
|
200
|
+
.form-label {
|
|
201
|
+
text-style: bold;
|
|
202
|
+
color: $foreground;
|
|
203
|
+
margin-top: 1;
|
|
204
|
+
height: 1;
|
|
205
|
+
}
|
|
206
|
+
.form-hint {
|
|
207
|
+
color: $foreground;
|
|
208
|
+
text-style: italic;
|
|
209
|
+
padding: 0 2;
|
|
210
|
+
margin-bottom: 1;
|
|
211
|
+
}
|
|
212
|
+
#input-title {
|
|
213
|
+
margin-bottom: 0;
|
|
214
|
+
}
|
|
215
|
+
#input-desc {
|
|
216
|
+
height: 8;
|
|
217
|
+
border: tall $secondary;
|
|
218
|
+
margin-bottom: 0;
|
|
219
|
+
}
|
|
220
|
+
#input-stack {
|
|
221
|
+
margin-bottom: 0;
|
|
222
|
+
}
|
|
223
|
+
#input-status {
|
|
224
|
+
margin-bottom: 0;
|
|
225
|
+
}
|
|
226
|
+
#input-links {
|
|
227
|
+
margin-bottom: 0;
|
|
228
|
+
}
|
|
229
|
+
#form-error {
|
|
230
|
+
height: 1;
|
|
231
|
+
margin-top: 1;
|
|
232
|
+
}
|
|
233
|
+
#form-buttons {
|
|
234
|
+
margin-top: 2;
|
|
235
|
+
height: 3;
|
|
236
|
+
align: left middle;
|
|
237
|
+
}
|
|
238
|
+
#form-buttons Button {
|
|
239
|
+
margin-right: 2;
|
|
240
|
+
}
|
|
241
|
+
/* ── Confirm delete modal ───────────────────────────────── */
|
|
242
|
+
ConfirmDeleteScreen {
|
|
243
|
+
align: center middle;
|
|
244
|
+
}
|
|
245
|
+
#confirm-dialog {
|
|
246
|
+
width: 50;
|
|
247
|
+
height: auto;
|
|
248
|
+
padding: 2 3;
|
|
249
|
+
border: thick $error;
|
|
250
|
+
background: $surface;
|
|
251
|
+
}
|
|
252
|
+
#confirm-title {
|
|
253
|
+
text-style: bold;
|
|
254
|
+
color: $error;
|
|
255
|
+
margin-bottom: 1;
|
|
256
|
+
height: 1;
|
|
257
|
+
}
|
|
258
|
+
#confirm-body {
|
|
259
|
+
margin-bottom: 2;
|
|
260
|
+
}
|
|
261
|
+
#confirm-buttons {
|
|
262
|
+
height: 3;
|
|
263
|
+
align: left middle;
|
|
264
|
+
}
|
|
265
|
+
#confirm-buttons Button {
|
|
266
|
+
margin-right: 2;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ── GitHub modal ───────────────────────────────────────── */
|
|
270
|
+
|
|
271
|
+
GithubScreen {
|
|
272
|
+
align: center middle;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#gh-dialog {
|
|
276
|
+
width: 60;
|
|
277
|
+
height: auto;
|
|
278
|
+
padding: 2 3;
|
|
279
|
+
border: thick #A78BFA;
|
|
280
|
+
background: #16151D;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#gh-title {
|
|
284
|
+
text-style: bold;
|
|
285
|
+
color: #A78BFA;
|
|
286
|
+
margin-bottom: 1;
|
|
287
|
+
height: 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#gh-project-label {
|
|
291
|
+
color: #EDEDF0;
|
|
292
|
+
margin-bottom: 0;
|
|
293
|
+
height: 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#gh-slug-label {
|
|
297
|
+
color: #5C5A6E;
|
|
298
|
+
margin-bottom: 1;
|
|
299
|
+
height: 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#gh-private-row {
|
|
303
|
+
height: 3;
|
|
304
|
+
align: left middle;
|
|
305
|
+
margin-bottom: 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#gh-private-label {
|
|
309
|
+
color: #EDEDF0;
|
|
310
|
+
margin-right: 2;
|
|
311
|
+
width: auto;
|
|
312
|
+
content-align: left middle;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#gh-status {
|
|
316
|
+
height: 1;
|
|
317
|
+
margin-bottom: 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#gh-error-body {
|
|
321
|
+
color: #EDEDF0;
|
|
322
|
+
margin-bottom: 2;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#gh-buttons {
|
|
326
|
+
height: 3;
|
|
327
|
+
align: left middle;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#gh-buttons Button {
|
|
331
|
+
margin-right: 2;
|
|
332
|
+
}
|
lazyplan/models.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Status(str, Enum):
|
|
8
|
+
CRUDA = "cruda"
|
|
9
|
+
ACTIVA = "activa"
|
|
10
|
+
PAUSADA = "pausada"
|
|
11
|
+
DESCARTADA = "descartada"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
STATUS_LABELS = {
|
|
15
|
+
Status.CRUDA: "🟡 Cruda",
|
|
16
|
+
Status.ACTIVA: "🟢 Activa",
|
|
17
|
+
Status.PAUSADA: "⚪ Pausada",
|
|
18
|
+
Status.DESCARTADA: "🔴 Descartada",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Project:
|
|
24
|
+
title: str
|
|
25
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
26
|
+
description: str = ""
|
|
27
|
+
stack: list[str] = field(default_factory=list)
|
|
28
|
+
status: Status = Status.CRUDA
|
|
29
|
+
links: list[str] = field(default_factory=list)
|
|
30
|
+
github_url: str = ""
|
|
31
|
+
folder_path: str = ""
|
|
32
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
33
|
+
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
34
|
+
|
|
35
|
+
def touch(self):
|
|
36
|
+
self.updated_at = datetime.now().isoformat()
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def stack_str(self) -> str:
|
|
40
|
+
return ", ".join(self.stack) if self.stack else "—"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def status_label(self) -> str:
|
|
44
|
+
return STATUS_LABELS.get(self.status, self.status)
|
|
File without changes
|
lazyplan/screens/base.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import TYPE_CHECKING, cast
|
|
3
|
+
from textual.screen import ModalScreen, Screen
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from lazyplan.app import LazyPlanApp
|
|
7
|
+
|
|
8
|
+
class BaseScreen(Screen):
|
|
9
|
+
|
|
10
|
+
# Esto es una propiedad para acceder al app como LazyPlanApp,
|
|
11
|
+
# y así tener acceso a los métodos y propiedades específicos de la app.
|
|
12
|
+
@property
|
|
13
|
+
def lazyplan(self) -> "LazyPlanApp":
|
|
14
|
+
return cast("LazyPlanApp", self.app)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LazyPlanModalScreen(ModalScreen):
|
|
18
|
+
@property
|
|
19
|
+
def app(self) -> "LazyPlanApp":
|
|
20
|
+
return cast("LazyPlanApp", super().app)
|