lazyplan 0.1.0__tar.gz

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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: lazyplan
3
+ Version: 0.1.0
4
+ Summary: TUI para capturar y gestionar ideas de proyectos
5
+ Author-email: Marcelo Mastroiani <mastroianimarcelo04@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: textual>=0.47.0
8
+ Requires-Dist: typer>=0.9.0
@@ -0,0 +1,197 @@
1
+ # 💤 LazyPlan
2
+
3
+ TUI (interfaz de terminal) para capturar y gestionar ideas de proyectos, sin salir de la consola. Anotá una idea en 5 segundos, hacé seguimiento de su estado, y creá el repo en GitHub cuando estés listo para arrancar — todo desde el teclado.
4
+
5
+ <!--![status](https://img.shields.io/badge/status-en%20desarrollo-yellow)
6
+ ![python](https://img.shields.io/badge/python-3.11%2B-blue)
7
+ ![textual](https://img.shields.io/badge/built%20with-Textual-8839ef)-->
8
+
9
+ ---
10
+
11
+ ## ✨ Características
12
+
13
+ - **Captura rápida de ideas** desde la TUI o directo desde la terminal (`lazyplan new`).
14
+ - **Estados de proyecto**: 🟡 Cruda · 🟢 Activa · ⚪ Pausada · 🔴 Descartada.
15
+ - **Búsqueda y filtros** en tiempo real por título, descripción o stack.
16
+ - **Vista de detalle** con toda la info del proyecto.
17
+ - **Editor integrado** para crear y modificar proyectos sin salir de la app.
18
+ - **Integración con GitHub CLI (`gh`)**: creá un repositorio (público o privado) directamente desde la TUI, sin abrir el navegador.
19
+ - **Persistencia local** en JSON, sin bases de datos ni servicios externos.
20
+ - Tema oscuro propio (`onyx-violet`) 🎨
21
+
22
+ ---
23
+
24
+ ## 📦 Requisitos
25
+
26
+ - Python **3.11** o superior
27
+ - [GitHub CLI (`gh`)](https://cli.github.com/) instalado y autenticado — **solo si** querés usar la creación de repos desde la app (`ctrl+g`). El resto de LazyPlan funciona sin `gh`.
28
+
29
+ ---
30
+
31
+ ## 🚀 Instalación
32
+
33
+ ### Con pipx (recomendado)
34
+
35
+ ```bash
36
+ pipx install lazyplan
37
+ ```
38
+
39
+ ### Con pip
40
+
41
+ ```bash
42
+ pip install lazyplan
43
+ ```
44
+
45
+ ### Desde el código fuente
46
+
47
+ ```bash
48
+ git clone https://github.com/tu-usuario/lazyplan.git
49
+ cd lazyplan
50
+ pip install -e .
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 🕹️ Uso
56
+
57
+ ### Abrir la TUI
58
+
59
+ ```bash
60
+ lazyplan
61
+ ```
62
+
63
+ ### Crear un proyecto sin abrir la TUI
64
+
65
+ ```bash
66
+ lazyplan new -t "Mi idea genial" -s "python,textual" -d "Una app de terminal" --status cruda
67
+ ```
68
+
69
+ | Opción | Descripción |
70
+ |---|---|
71
+ | `-t`, `--title` | Título del proyecto **(obligatorio)** |
72
+ | `-s`, `--stack` | Tecnologías, separadas por coma |
73
+ | `-d`, `--desc` | Descripción breve |
74
+ | `--status` | Estado inicial: `cruda`, `activa`, `pausada`, `descartada` (default: `cruda`) |
75
+
76
+ ### Listar proyectos desde la terminal
77
+
78
+ ```bash
79
+ lazyplan ls
80
+ ```
81
+
82
+ ---
83
+
84
+ ## ⌨️ Atajos de teclado (dentro de la TUI)
85
+
86
+ **Pantalla principal**
87
+
88
+ | Tecla | Acción |
89
+ |---|---|
90
+ | `n` | Nuevo proyecto |
91
+ | `enter` | Ver detalle |
92
+ | `e` | Editar proyecto |
93
+ | `d` | Eliminar proyecto |
94
+ | `j` / `k` | Mover cursor abajo / arriba |
95
+ | `ctrl+f` | Buscar |
96
+ | `esc` | Limpiar búsqueda |
97
+ | `q` | Salir |
98
+
99
+ **Detalle del proyecto**
100
+
101
+ | Tecla | Acción |
102
+ |---|---|
103
+ | `e` | Editar |
104
+ | `d` | Eliminar |
105
+ | `ctrl+g` | Crear repositorio en GitHub |
106
+ | `esc` | Volver |
107
+
108
+ **Editor**
109
+
110
+ | Tecla | Acción |
111
+ |---|---|
112
+ | `ctrl+s` | Guardar |
113
+ | `esc` | Cancelar |
114
+
115
+ ---
116
+
117
+ ## 💾 Dónde se guardan los datos
118
+
119
+ LazyPlan guarda todo en un archivo JSON local, sin necesidad de conexión a internet ni servicios externos:
120
+
121
+ ```
122
+ ~/.local/share/lazyplan/projects.json
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 🏗️ Estructura del proyecto
128
+
129
+ ```
130
+ lazyplan/
131
+ ├── lazyplan/
132
+ │ ├── app.py # App principal de Textual
133
+ │ ├── cli.py # Entry point y comandos (typer)
134
+ │ ├── models.py # Modelo Project / Status
135
+ │ ├── storage.py # Persistencia en JSON
136
+ │ ├── github.py # Integración con GitHub CLI
137
+ │ ├── lazyplan.tcss # Estilos (Textual CSS)
138
+ │ └── screens/
139
+ │ ├── main.py # Listado principal
140
+ │ ├── detail.py # Vista de detalle
141
+ │ ├── editor.py # Crear/editar proyecto
142
+ │ ├── confirm_delete.py # Confirmación de borrado
143
+ │ └── github_screen.py # Modal de creación de repo
144
+ └── pyproject.toml
145
+ ```
146
+
147
+ ---
148
+
149
+ ## 🛠️ Desarrollo
150
+
151
+ Cloná el repo e instalá las dependencias de desarrollo (incluye herramientas de build y empaquetado):
152
+
153
+ ```bash
154
+ git clone https://github.com/tu-usuario/lazyplan.git
155
+ cd lazyplan
156
+ pip install -e ".[dev]"
157
+ ```
158
+
159
+ Correr la app en modo desarrollo:
160
+
161
+ ```bash
162
+ python -m lazyplan.cli
163
+ ```
164
+
165
+ ### Generar el paquete
166
+
167
+ ```bash
168
+ python -m build
169
+ ```
170
+
171
+ ### Publicar en PyPI
172
+
173
+ ```bash
174
+ twine upload dist/*
175
+ ```
176
+
177
+ ---
178
+
179
+ ## 🤝 Contribuir
180
+
181
+ Las contribuciones son bienvenidas. Si encontrás un bug o tenés una idea:
182
+
183
+ 1. Abrí un issue describiendo el problema o la propuesta.
184
+ 2. Forkeá el repo y creá una rama (`git checkout -b feature/mi-mejora`).
185
+ 3. Hacé tus cambios y abrí un Pull Request.
186
+
187
+ ---
188
+
189
+ ## 📄 Licencia
190
+
191
+ Este proyecto no tiene licencia definida todavía. Si pensás distribuirlo o aceptar contribuciones externas, considerá agregar una (por ejemplo, [MIT](https://choosealicense.com/licenses/mit/)).
192
+
193
+ ---
194
+
195
+ ## ✍️ Autor
196
+
197
+ **Marcelo Mastroiani** — mastroianimarcelo04@gmail.com
File without changes
@@ -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()
@@ -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()
@@ -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"