import subprocess
import time
from typing import Iterable
from rich.live import Live
from rich.prompt import Prompt
from super_pocket.settings import centered_spinner, display_logo
EXIT_KEYWORDS = {"exit", "quit", "q", "x"}
EXIT_CHOICES = ["exit", "quit", "q", "Q", "X", "x", "EXIT", "QUIT"]
def _run_with_spinner(message: str, command: list[str], style: str = "bold blue") -> None:
"""Render a centered spinner while running a command."""
with Live(centered_spinner(message, style), refresh_per_second=20, transient=True):
subprocess.run(command)
def _pause(message: str = "Press Enter to continue") -> None:
"""Pause to keep output visible before re-rendering logo."""
Prompt.ask(message, default="")
def _run_with_spinner_and_pause(
message: str,
command: list[str],
style: str = "bold blue",
) -> None:
"""Run a command with spinner, then pause."""
_run_with_spinner(message, command, style)
_pause()
def _handle_common_navigation(choice: str, allow_back: bool = False) -> str | None:
"""
Handle common navigation keywords like 'exit', 'quit', 'q', 'x' and optionally 'back'.
Returns:
"exit" | "back" | None
"""
normalized = choice.lower()
if normalized in EXIT_KEYWORDS:
return "exit"
if allow_back and normalized == "back":
return "back"
return None
def _prompt_with_choices(
prompt: str,
base_choices: Iterable[str],
default: str = "help",
allow_back: bool = True,
) -> str:
"""
Wrapper around Prompt.ask to avoid repeating choices wiring.
- Adds 'back' if allow_back is True.
- Always appends EXIT_CHOICES.
- Hides choices from the UI.
"""
choices = list(base_choices)
if allow_back and "back" not in choices:
choices.append("back")
# Ensure all exit aliases are present only once
for exit_choice in EXIT_CHOICES:
if exit_choice not in choices:
choices.append(exit_choice)
return Prompt.ask(
prompt,
default=default,
choices=choices,
show_choices=False,
)
def _show_help(section: str, message: str) -> None:
"""Standardized help renderer for a given section."""
display_logo()
_run_with_spinner_and_pause(
message=message,
command=["pocket", section, "--help"],
style="bold orange_red1",
)
[docs]
def pocket_cmd() -> None:
display_logo()
with Live(
centered_spinner("Loading..."),
refresh_per_second=10,
transient=True,
):
time.sleep(2)
base_choices = ["help", "h", "iconify", "icon", "project", "proj", "documents", "docs", "doc", "pdf", "web", "readme", "read", "xml"]
while True:
display_logo()
user_input = Prompt.ask(
"[bold blue]help -[/] "
"[bold orange_red1]iconify - project - documents - pdf - web - readme - xml[/] "
"[bold blue]- exit/Q[/] >>>",
default="help",
choices=base_choices + EXIT_CHOICES,
show_choices=False,
)
nav = _handle_common_navigation(user_input, allow_back=False)
if nav == "exit":
return
match user_input:
case "help" | "h":
display_logo()
_run_with_spinner_and_pause(
"Loading help...",
["pocket", "--help"],
style="bold orange_red1",
)
case "iconify" | "icon":
input_file = Prompt.ask("Input image")
output_file = Prompt.ask("Output PNG")
size = Prompt.ask("Size", default="1024")
exponent = Prompt.ask("Exponent", default="5.0")
command = ["pocket", "iconify", "-i", input_file, "-o", output_file]
if size.strip():
command += ["--size", size]
if exponent.strip():
command += ["--exponent", exponent]
_run_with_spinner_and_pause("Generating squircle icon...", command)
case "project" | "proj":
if project_cmd() == "exit":
return
case "documents" | "docs" | "doc":
if documents_cmd() == "exit":
return
case "pdf":
if pdf_cmd() == "exit":
return
case "web":
if web_cmd() == "exit":
return
case "readme" | "read":
if readme_cmd() == "exit":
return
case "xml":
if xml_cmd() == "exit":
return
[docs]
def project_cmd() -> str | None:
base_choices = ["help", "h", "to-file", "tf", "req-to-date", "req", "rtd", "init", "i"]
while True:
display_logo()
user_input = _prompt_with_choices(
"[bold blue]help - [/][bold orange_red1]"
"to-file - req-to-date - init[/] "
"[bold blue]- exit/Q[/] >>>",
base_choices=base_choices,
default="help",
allow_back=True,
)
nav = _handle_common_navigation(user_input, allow_back=True)
if nav == "exit":
return "exit"
if nav == "back":
return "back"
match user_input:
case "help" | "h":
_show_help("project", "Loading project help...")
case "to-file" | "tf":
path = Prompt.ask("Project path", default=".")
output = Prompt.ask("Output file (optional)", default="")
exclude = Prompt.ask("Exclude (comma-separated, optional)", default="")
command = ["pocket", "project", "to-file", "--path", path]
if output.strip():
command += ["--output", output]
if exclude.strip():
command += ["--exclude", exclude]
_run_with_spinner_and_pause("Exporting project...", command)
case "req-to-date" | "req" | "rtd":
packages = Prompt.ask(
"Packages (comma list or requirements file)",
default="",
)
if not packages.strip():
continue
command = ["pocket", "project", "req-to-date", packages]
_run_with_spinner_and_pause("Checking requirements...", command)
case "init":
_run_with_spinner_and_pause(
"Loading templates...",
["pocket", "project", "init", "list"],
)
template = Prompt.ask(
"Template name (or 'back' to cancel)",
default="",
)
if not template.strip() or template.lower() == "back":
continue
_run_with_spinner_and_pause(
"Loading template details...",
["pocket", "project", "init", "show", template],
)
confirm = Prompt.ask(
"Create project with this template?",
choices=["y", "n"],
default="y",
)
if confirm.strip().lower() == "n":
continue
path = Prompt.ask("Output path", default=".")
quick = Prompt.ask(
"Quick mode (skip prompts)?",
choices=["y", "n"],
default="n",
)
command = [
"pocket",
"project",
"init",
"new",
template,
"-p",
path,
]
if quick == "y":
command.append("--quick")
_run_with_spinner_and_pause("Creating project...", command)
[docs]
def documents_cmd() -> str | None:
base_choices = ["help", "h", "list", "ls", "view", "v", "copy", "cp", "init"]
while True:
display_logo()
user_input = _prompt_with_choices(
"[bold blue]help -[/] [bold orange_red1]list - view - copy - init [/]"
"[bold blue]- exit/Q[/] >>>",
base_choices=base_choices,
default="help",
allow_back=True,
)
nav = _handle_common_navigation(user_input, allow_back=True)
if nav == "exit":
return "exit"
if nav == "back":
return "back"
match user_input:
case "help" | "h":
_show_help("documents", "Loading documents help...")
case "list" | "ls":
type_choice = Prompt.ask(
"Type",
choices=["all", "templates", "cheatsheets"],
default="all",
)
command = ["pocket", "documents", "list", "--type", type_choice]
_run_with_spinner_and_pause("Listing documents...", command)
case "view" | "v":
name = Prompt.ask("Template or cheatsheet name")
type_choice = Prompt.ask(
"Type (optional)",
choices=["auto", "template", "cheatsheet"],
default="auto",
)
command = ["pocket", "documents", "view", name]
if type_choice != "auto":
command += ["--type", type_choice]
_run_with_spinner_and_pause("Loading item...", command)
case "copy" | "cp":
name = Prompt.ask("Template or cheatsheet name")
output = Prompt.ask("Output path (optional)", default="")
type_choice = Prompt.ask(
"Type (optional)",
choices=["auto", "template", "cheatsheet"],
default="auto",
)
force = Prompt.ask(
"Overwrite if exists?",
choices=["y", "n"],
default="y",
)
command = ["pocket", "documents", "copy", name]
if output.strip():
command += ["--output", output]
if type_choice != "auto":
command += ["--type", type_choice]
if force == "y":
command.append("--force")
_run_with_spinner_and_pause("Copying item...", command)
case "init":
output = Prompt.ask(
"Target directory (default .AGENTS)",
default=".AGENTS",
)
command = ["pocket", "documents", "init", "--output", output]
_run_with_spinner_and_pause("Initializing documents...", command)
[docs]
def pdf_cmd() -> str | None:
base_choices = ["help", "h", "convert", "conv", "c"]
while True:
display_logo()
user_input = _prompt_with_choices(
"[bold blue]help -[/] [bold orange_red1]convert [/] "
"[bold blue]- exit/Q[/] >>>",
base_choices=base_choices,
default="help",
allow_back=True,
)
nav = _handle_common_navigation(user_input, allow_back=True)
if nav == "exit":
return "exit"
if nav == "back":
return "back"
match user_input:
case "help" | "h":
_show_help("pdf", "Loading PDF help...")
case "convert" | "conv" | "c":
input_file = Prompt.ask("Input file")
output_file = Prompt.ask("Output file (optional)", default="")
command = ["pocket", "pdf", "convert", input_file]
if output_file.strip():
command += ["--output", output_file]
_run_with_spinner_and_pause("Converting to PDF...", command)
[docs]
def web_cmd() -> str | None:
base_choices = ["help", "h", "favicon", "fav", "job-search", "js", "j"]
while True:
display_logo()
user_input = _prompt_with_choices(
"[bold blue]help -[/] [bold orange_red1]favicon - job-search[/]"
"[bold blue] - exit/Q[/] >>>",
base_choices=base_choices,
default="help",
allow_back=True,
)
nav = _handle_common_navigation(user_input, allow_back=True)
if nav == "exit":
return "exit"
if nav == "back":
return "back"
match user_input:
case "help" | "h":
_show_help("web", "Loading web help...")
case "favicon" | "fav":
input_file = Prompt.ask("Input image")
output_file = Prompt.ask("Output ico (optional)", default="")
sizes = Prompt.ask("Sizes (optional: 64x64,32x32)", default="")
command = ["pocket", "web", "favicon", input_file]
if output_file.strip():
command += ["--output", output_file]
if sizes.strip():
command += ["--sizes", sizes]
_run_with_spinner_and_pause("Generating favicon...", command)
case "job-search" | "js" | "j":
query = Prompt.ask("Search query", default="Python developer")
page = Prompt.ask("Start page", default="1")
num_pages = Prompt.ask("Number of pages", default="10")
country = Prompt.ask("Country code", default="fr")
language = Prompt.ask("Language", default="fr")
date_posted = Prompt.ask(
"Date posted",
choices=["all", "today", "3days", "week", "month"],
default="month",
)
employment_types = Prompt.ask(
"Employment types",
default="FULLTIME",
)
job_requirements = Prompt.ask(
"Job requirements",
default="no_experience",
)
work_from_home = Prompt.ask(
"Work from home?",
choices=["y", "n"],
default="n",
)
output = Prompt.ask("Output file", default="jobs.json")
command = [
"pocket",
"web",
"job-search",
query,
"--page",
page,
"--num_pages",
num_pages,
"--country",
country,
"--language",
language,
"--date_posted",
date_posted,
"--employment_types",
employment_types,
"--job_requirements",
job_requirements,
]
if work_from_home == "y":
command.append("--work_from_home")
command += ["--output", output]
_run_with_spinner_and_pause("Searching jobs...", command)
[docs]
def readme_cmd() -> str | None:
base_choices = ["help", "h", "analyze", "ana", "generate", "gen", "g"]
while True:
display_logo()
user_input = _prompt_with_choices(
"[bold blue]help -[/] [bold orange_red1]analyze - generate [/] "
"[bold blue]- exit/Q[/] >>>",
base_choices=base_choices,
default="help",
allow_back=True,
)
nav = _handle_common_navigation(user_input, allow_back=True)
if nav == "exit":
return "exit"
if nav == "back":
return "back"
match user_input:
case "help" | "h":
_show_help("readme", "Loading README help...")
case "analyze" | "ana":
path = Prompt.ask("Project path", default=".")
command = ["pocket", "readme", "analyze", path]
_run_with_spinner_and_pause("Analyzing project...", command)
case "generate" | "gen" | "g":
path = Prompt.ask("Project path", default=".")
output = Prompt.ask("Output file", default="README.md")
command = [
"pocket",
"readme",
"generate",
"--path",
path,
"--output",
output,
]
_run_with_spinner_and_pause("Generating README...", command)
[docs]
def xml_cmd() -> str | None:
base_choices = ["help", "h", "convert", "conv", "c"]
while True:
display_logo()
user_input = _prompt_with_choices(
"[bold blue]help -[/] [bold orange_red1]convert [/] "
"[bold blue]- exit/Q[/] >>>",
base_choices=base_choices,
default="help",
allow_back=True,
)
nav = _handle_common_navigation(user_input, allow_back=True)
if nav == "exit":
return "exit"
if nav == "back":
return "back"
match user_input:
case "help" | "h":
_show_help("xml", "Loading XML help...")
case "convert" | "conv" | "c":
source_type = Prompt.ask(
"Source type",
choices=["text", "file"],
default="text",
)
if source_type == "text":
text = Prompt.ask("Text to convert (custom tag syntax)")
output = Prompt.ask("Output file (optional)", default="")
command = ["pocket", "xml", text]
if output.strip():
command += ["--output", output]
else:
input_file = Prompt.ask("Input file path")
output = Prompt.ask("Output file (optional)", default="")
command = ["pocket", "xml", "--file", input_file]
if output.strip():
command += ["--output", output]
_run_with_spinner_and_pause("Converting to XML...", command)
if __name__ == "__main__":
pocket_cmd()