diff --git a/pyproject.toml b/pyproject.toml index fa96012..567dc40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,8 +6,6 @@ authors = [{ name = "gitmatt", email = "git@publicmatt.com" }] readme = "README.md" requires-python = ">=3.12" dependencies = [ - "click>=8.1.8", - "click-default-group>=1.2.4", "jinja2>=3.1.5", "lxml>=5.3.1", "markdown>=3.7", @@ -18,7 +16,7 @@ dependencies = [ ] [project.scripts] -plan = "plan:cli.cli" +plan = "plan:__main__.cli" [build-system] requires = ["hatchling"] diff --git a/src/plan/__init__.py b/src/plan/__init__.py index e36254c..e69de29 100644 --- a/src/plan/__init__.py +++ b/src/plan/__init__.py @@ -1,80 +0,0 @@ -import os -import subprocess -import sys -from datetime import datetime -from pathlib import Path - -from jinja2 import Environment, PackageLoader, select_autoescape - -from .config import Config - -config = Config() - - -def get_template(template_name: str = "plan.html", plan_path: Path | None = None): - """ """ - templates = Environment( - loader=PackageLoader("plan"), autoescape=select_autoescape() - ) - template = templates.get_template(template_name) - today = datetime.today().strftime("%Y-%m-%d") - return template.render(today=today, plan_path=plan_path) - - -def plan_path() -> Path: - """ - returns: Path to current date's .plan.md - """ - today = datetime.today().strftime("%Y-%m-%d") - path = config.app_path / f"{today}.md" - return path - - -def print_plan(plan_path: Path): - """ - send .plan.md to printer. - - uses pandoc and lp shell tools. - """ - # preamble =""" - # --- - # documentclass: extarticle - # fontsize: 20pt - # --- - # """ - cat_process = subprocess.Popen(["cat", str(plan_path)], stdout=subprocess.PIPE) - pandoc_process = subprocess.Popen( - ["pandoc", "-t", "pdf", "-V", "geometry:margin=.5in", "-"], - stdin=cat_process.stdout, - stdout=subprocess.PIPE, - ) - - subprocess.run( - ["lp", "-o", "sides=two-sided-long-edge", "-o", "number-up=2"], - stdin=pandoc_process.stdout, - ) - - -def create_and_open_template(plan: Path, name: str = "plan.html") -> int: - plan.parent.mkdir(parents=True, exist_ok=True) - plan.touch() - - template = get_template(name, plan_path=plan) - with plan.open("w") as f: - f.write(template) - - editor = os.getenv("EDITOR", "nvim") - os.chdir(plan.parent) - p = subprocess.Popen([editor, str(plan)]) - sys.exit(p.wait()) - - -def open_in_editor(plan: Path): - plan.parent.mkdir(parents=True, exist_ok=True) - if not plan.exists(): - plan.touch() - with plan.open("w") as f: - f.write(default_template(plan_path=plan)) - editor = os.getenv("EDITOR", "nvim") - os.chdir(Path().home() / ".plan") - subprocess.run([editor, str(plan)]) diff --git a/src/plan/__main__.py b/src/plan/__main__.py new file mode 100644 index 0000000..b6ce1bd --- /dev/null +++ b/src/plan/__main__.py @@ -0,0 +1,10 @@ +from pydantic_settings import ( + CliApp, +) + +from .cli import PlanCli + + +def cli(): + app = CliApp() + app.run(PlanCli) diff --git a/src/plan/cli.py b/src/plan/cli.py index 8520f5d..587a35e 100644 --- a/src/plan/cli.py +++ b/src/plan/cli.py @@ -1,54 +1,20 @@ -import sys -from pathlib import Path - -import click -from click_default_group import DefaultGroup - -from .main import ( - create_plan, - edit_plan, - plan_path, - print_plan, +from pydantic import Field +from pydantic_settings import ( + BaseSettings, + CliApp, + CliSubCommand, + get_subcommand, ) - -@click.group(cls=DefaultGroup, default="main", default_if_no_args=True) -def cli(): - pass +from .commands import main -@cli.command("print") -@click.option( - "--plan_path", - "-p", - "plan_path", - type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path), - default=plan_path(), -) -def send_to_printer(plan_path: Path): - print_plan(plan_path) +class PlanCli(BaseSettings): + main_: CliSubCommand[main.Main] = Field(alias="main") + print_: CliSubCommand[main.Print] = Field(alias="print") - -@cli.command("main") -@click.argument("template_name", required=True, default="plan.html") -@click.option( - "--print/--no-print", - "is_print", - is_flag=True, - default=False, -) -@click.option("--quiet", "quiet", type=bool, is_flag=True, default=True) -def open_today(template_name, is_print: bool = False, quiet: bool = True): - """ - open today - - plan {:template|plan.html} {--quiet} {--print} - - :template can be one of: {plan|work}.html - """ - path = plan_path() - content = create_plan(plan_path=path, name=template_name) - if is_print: - print(content) - else: - sys.exit(edit_plan(path)) + def cli_cmd(self) -> None: + if get_subcommand(self, is_required=False) is None: + CliApp.run(main.Main) + else: + CliApp.run_subcommand(self) diff --git a/src/plan/commands/__init__.py b/src/plan/commands/__init__.py new file mode 100644 index 0000000..6ed2956 --- /dev/null +++ b/src/plan/commands/__init__.py @@ -0,0 +1,13 @@ +from pathlib import Path + +from pydantic import Field +from pydantic_settings import ( + BaseSettings, +) + + +class Base(BaseSettings): + model_config = {"env_file": ".env", "env_prefix": "PLAN_", "extra": "ignore"} + base_path: Path = Field( + default=Path().home() / ".plan", description="base path for .plan files" + ) diff --git a/src/plan/commands/main.py b/src/plan/commands/main.py new file mode 100644 index 0000000..4767056 --- /dev/null +++ b/src/plan/commands/main.py @@ -0,0 +1,111 @@ +import os +import subprocess +import sys +from datetime import datetime +from pathlib import Path +from typing import ClassVar + +from pydantic import Field, computed_field +from pydantic_settings import ( + CliImplicitFlag, + CliPositionalArg, +) + +from plan.commands import Base +from plan.repository import PlanRepository + + +class Main(Base): + """ + open today's plan + + :template can be one of: {plan|work} + """ + + template_name: CliPositionalArg[str] = Field(default="plan") + + @computed_field() + @property + def template(self) -> str: + return f"{self.template_name}.md" + + print: CliImplicitFlag[bool] = Field(default=False) + + def create(self, force=False) -> str: + plan_path = PlanRepository.path_for_date(base_path=self.base_path) + plan_path.parent.mkdir(parents=True, exist_ok=True) + + if not plan_path.exists() or force: + content = PlanRepository.get_template(self.template) + with plan_path.open("w") as f: + f.write(content) + else: + content = self.get(plan_path) + return content + + def get(self, plan_path: Path) -> str: + with plan_path.open("r") as f: + content = f.read() + return content + + def edit(self) -> int: + plan_path = PlanRepository.path_for_date(base_path=self.base_path) + editor = os.getenv("EDITOR", "nvim") + os.chdir(plan_path.parent) + p = subprocess.Popen([editor, str(plan_path)]) + return p.wait() + + def cli_cmd(self) -> None: + content = self.create() + if self.print: + print(content) + else: + sys.exit(self.edit()) + + +class Print(Base): + PREAMBLE: ClassVar[str] = """ +--- +documentclass: extarticle +fontsize: 20pt +--- + """ + + def print_plan(self, plan_path: Path): + """ + send .plan.md to printer. + + uses pandoc and lp shell tools. + """ + preamble = subprocess.Popen( + ["echo", "-n", self.PREAMBLE], stdout=subprocess.PIPE + ) + cat = subprocess.Popen( + ["cat", "-", str(plan_path)], + stdin=preamble.stdout, + stdout=subprocess.PIPE, + ) + pandoc = subprocess.Popen( + ["pandoc", "-t", "pdf", "-V", "geometry:margin=.5in", "-"], + stdin=cat.stdout, + stdout=subprocess.PIPE, + ) + send = subprocess.Popen( + ["lp", "-o", "sides=two-sided-long-edge", "-o", "number-up=2"], + stdin=pandoc.stdout, + ) + sys.exit(send.wait()) + + def plan_path(self, date: datetime | None = None) -> Path: + """ + returns: Path to current date's .plan.md + """ + if date is None: + date = datetime.today() + today = date.strftime("%Y-%m-%d") + path = self.base_path / f"{today}.md" + return path + + def cli_cmd(self): + path = self.plan_path() + self.print_plan(path) diff --git a/src/plan/config.py b/src/plan/config.py deleted file mode 100644 index 590ca57..0000000 --- a/src/plan/config.py +++ /dev/null @@ -1,14 +0,0 @@ -from pathlib import Path - -from pydantic import Field -from pydantic_settings import BaseSettings - - -def default_app_path(): - path = Path().home() / ".plan" - return path - - -class Config(BaseSettings): - model_config = {"env_file": ".env", "env_prefix": "PLAN_", "extra": "ignore"} - app_path: Path = Field(default_factory=default_app_path) diff --git a/src/plan/main.py b/src/plan/main.py deleted file mode 100644 index 8a785d4..0000000 --- a/src/plan/main.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -import subprocess -import sys -from datetime import datetime -from pathlib import Path -from .config import Config - -from jinja2 import Environment, PackageLoader, select_autoescape - -config = Config() - - -def get_template(template_name: str = "plan.html", plan_path: Path | None = None): - """ """ - templates = Environment( - loader=PackageLoader("plan"), autoescape=select_autoescape() - ) - template = templates.get_template(template_name) - today = datetime.today().strftime("%Y-%m-%d") - return template.render(today=today, plan_path=plan_path) - - -def plan_path(date: datetime | None = None) -> Path: - """ - returns: Path to current date's .plan.md - """ - if date is None: - date = datetime.today() - today = date.strftime("%Y-%m-%d") - path = config.app_path / f"{today}.md" - return path - - -def print_plan(plan_path: Path): - """ - send .plan.md to printer. - - uses pandoc and lp shell tools. - """ - preamble = """ ---- -documentclass: extarticle -fontsize: 20pt ---- - -""" - preamble = subprocess.Popen(["echo", "-n", preamble], stdout=subprocess.PIPE) - cat = subprocess.Popen( - ["cat", "-", str(plan_path)], - stdin=preamble.stdout, - stdout=subprocess.PIPE, - ) - pandoc = subprocess.Popen( - ["pandoc", "-t", "pdf", "-V", "geometry:margin=.5in", "-"], - stdin=cat.stdout, - stdout=subprocess.PIPE, - ) - send = subprocess.Popen( - ["lp", "-o", "sides=two-sided-long-edge", "-o", "number-up=2"], - stdin=pandoc.stdout, - ) - sys.exit(send.wait()) - - -def create_plan(plan_path: Path, name: str = "plan.html", force=False) -> str: - plan_path.parent.mkdir(parents=True, exist_ok=True) - - if not plan_path.exists() or force: - content = get_template(name, plan_path=plan_path) - with plan_path.open("w") as f: - f.write(content) - else: - content = get_plan(plan_path) - return content - - -def get_plan(plan_path: Path) -> str: - with plan_path.open("r") as f: - content = f.read() - return content - - -def edit_plan(plan_path: Path) -> int: - editor = os.getenv("EDITOR", "nvim") - os.chdir(plan_path.parent) - p = subprocess.Popen([editor, str(plan_path)]) - return p.wait() diff --git a/src/plan/repository.py b/src/plan/repository.py new file mode 100644 index 0000000..8b9710e --- /dev/null +++ b/src/plan/repository.py @@ -0,0 +1,27 @@ +from datetime import datetime +from pathlib import Path + +from jinja2 import Environment, PackageLoader, select_autoescape + + +class PlanRepository: + @classmethod + def path_for_date(cls, base_path: Path, date: datetime | None = None) -> Path: + """ + returns: Path to current date's .plan.md + """ + if date is None: + date = datetime.today() + today = date.strftime("%Y-%m-%d") + path = base_path / f"{today}.md" + return path + + @classmethod + def get_template(cls, template_name: str = "plan.html"): + """ """ + templates = Environment( + loader=PackageLoader("plan"), autoescape=select_autoescape() + ) + template = templates.get_template(template_name) + today = datetime.today().strftime("%Y-%m-%d") + return template.render(today=today) diff --git a/src/plan/templates/plan.html b/src/plan/templates/plan.md similarity index 100% rename from src/plan/templates/plan.html rename to src/plan/templates/plan.md diff --git a/src/plan/templates/work.html b/src/plan/templates/work.md similarity index 100% rename from src/plan/templates/work.html rename to src/plan/templates/work.md diff --git a/uv.lock b/uv.lock index 63aed5d..237311e 100644 --- a/uv.lock +++ b/uv.lock @@ -24,30 +24,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "click-default-group" -version = "1.2.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123 }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -215,8 +191,6 @@ name = "plan" version = "0.1.1" source = { editable = "." } dependencies = [ - { name = "click" }, - { name = "click-default-group" }, { name = "jinja2" }, { name = "lxml" }, { name = "markdown" }, @@ -234,8 +208,6 @@ dev = [ [package.metadata] requires-dist = [ - { name = "click", specifier = ">=8.1.8" }, - { name = "click-default-group", specifier = ">=1.2.4" }, { name = "jinja2", specifier = ">=3.1.5" }, { name = "lxml", specifier = ">=5.3.1" }, { name = "markdown", specifier = ">=3.7" },