Zettel

The zettel subsystem handles Zettelkasten note management — reading, formatting, and syncing notes with external systems like Jira.

Core

Domain entities, use cases, and repository interfaces for Zettelkasten notes.

PrintZettelUseCase

class buvis.pybase.zettel.application.use_cases.print_zettel_use_case.PrintZettelUseCase(formatter: ZettelFormatter)

Bases: object

Use case for printing formatted Zettel data.

This class encapsulates the logic for formatting and printing Zettel data using a provided ZettelFormatter.

execute(zettel_data: ZettelData) str

Execute the use case by formatting the given Zettel data.

Format the provided Zettel data using the configured formatter and return the result.

Parameters:

zettel_data – The Zettel data to format and print.

Returns:

The formatted Zettel data.

ReadZettelUseCase

class buvis.pybase.zettel.application.use_cases.read_zettel_use_case.ReadZettelUseCase(repository: ZettelReader)

Bases: object

Read zettel from repository by location and downcast it to zettel.

This class is responsible for taking location of a zettel within a ZettelReader, and downcasting it using ZettelFactory service to specific zettel according to zettel type.

Parameters:

repository – An instance of a class that implements the ZettelReader interface, used to data access in persistence layer.

execute(repository_location: str) Zettel

Execute the use case of reading from repository by location and downcasting to zettel.

This method takes repository location, attempts to retrieve the corresponding zettel from the repository, downcasts it into a zettel, and returns it. It handles potential exceptions during the retrieval process.

Parameters:

repository_location – Unique identifier of location within repository containing zettel data.

Raises:

ZettelRepositoryZettelNotFoundError – If the zettel is not found in the repository.

Returns:

A Zettel object created from the retrieved zettel.

QueryZettelsUseCase

class buvis.pybase.zettel.application.use_cases.query_zettels_use_case.QueryZettelsUseCase(repository: ZettelReader, evaluator: ExpressionEvaluator)

Bases: object

execute(spec: QuerySpec) list[dict[str, Any]]

UpdateZettelUseCase

class buvis.pybase.zettel.application.use_cases.update_zettel_use_case.UpdateZettelUseCase(writer: ZettelWriter)

Bases: object

execute(zettel: Zettel, changes: dict[str, Any], target: str = 'metadata') None

DeleteZettelUseCase

class buvis.pybase.zettel.application.use_cases.delete_zettel_use_case.DeleteZettelUseCase(writer: ZettelWriter)

Bases: object

execute(zettel: Zettel) None

Templates

Templates define how bim create builds new zettels. Each template is a Python module in src/lib/buvis/pybase/zettel/domain/templates/. Drop a module there and discover_templates() finds it automatically — no registration needed.

Built-in templates: note (minimal, no questions) and project (asks for dev type, creates project dir).

Writing a template

A template is a class with three attributes/methods and a name class variable:

# src/lib/buvis/pybase/zettel/domain/templates/meeting.py
from __future__ import annotations

from pathlib import Path
from typing import Any

from buvis.pybase.zettel.domain.templates import Hook, Question
from buvis.pybase.zettel.domain.value_objects.zettel_data import ZettelData


class MeetingTemplate:
    name = "meeting"

    def questions(self) -> list[Question]:
        return [
            Question(
                key="attendees",
                prompt="Attendees (comma-separated)",
            ),
            Question(
                key="location",
                prompt="Location",
                choices=["office", "remote", "hybrid"],
                default="remote",
                required=True,
            ),
        ]

    def build_data(self, answers: dict[str, Any]) -> ZettelData:
        data = ZettelData()
        data.metadata["type"] = "meeting"
        data.metadata["title"] = answers.get("title", "")
        data.metadata["location"] = answers.get("location", "remote")
        data.metadata["attendees"] = answers.get("attendees", "")
        tags_raw = answers.get("tags", "")
        if tags_raw:
            data.metadata["tags"] = [
                t.strip() for t in tags_raw.split(",") if t.strip()
            ]
        title = data.metadata["title"]
        data.sections = [
            (f"# {title}", ""),
            ("## Agenda", ""),
            ("## Notes", ""),
            ("## Action items", ""),
        ]
        return data

    def hooks(self) -> list[Hook]:
        return []

That’s it. Save the file, and bim create -t meeting --title "Standup" works.

YAML templates

You can also define templates as YAML files without writing Python. Place .yaml files in your config directory under templates/:

  • $BUVIS_CONFIG_DIR/templates/ (highest priority)

  • ~/.config/buvis/templates/

  • ~/.buvis/templates/

A YAML template with the same name as a Python template overrides it. Higher-priority config dirs override lower-priority ones.

Basic example

# ~/.config/buvis/templates/meeting.yaml
name: meeting

questions:
  - key: attendees
    prompt: "Who attended?"
    required: true
  - key: location
    prompt: "Location"
    default: "online"
    choices: ["online", "room-a", "room-b"]

metadata:
  type: meeting
  attendees: "{attendees}"
  location: "{location}"

sections:
  - heading: "# {title}"
    body: ""
  - heading: "## Agenda"
    body: ""
  - heading: "## Notes"
    body: ""
  - heading: "## Action items"
    body: ""

Usage:

bim create -t meeting --title "Sprint review" -a attendees="Alice, Bob" -a location="room-a"

Field resolution

Three resolution modes for metadata and section values:

String substitution{key} is replaced with the answer value. Missing keys become empty strings:

metadata:
  attendees: "{attendees}"
  note: "Created by {author}"     # empty string if author not provided

Python eval — a dict with an eval key runs arbitrary Python. All answers are available as local variables:

metadata:
  tag_count:
    eval: "len(tags) if tags else 0"
  slug:
    eval: "title.lower().replace(' ', '-')"
  due:
    eval: "datetime.datetime.now() + datetime.timedelta(days=7)"

Passthrough — non-string, non-dict values (int, bool, list) are kept as-is:

metadata:
  completed: false
  priority: 3

Extending Python templates

YAML templates can extend existing Python (or YAML) templates with extends. This lets you reuse base logic while customizing fields:

# ~/.config/buvis/templates/standup.yaml
name: standup
extends: note

questions:
  - key: sprint
    prompt: "Sprint"

metadata:
  type: standup
  sprint: "{sprint}"

sections:
  - heading: "# {title}"
    body: ""
  - heading: "## Done"
    body: ""
  - heading: "## Doing"
    body: ""
  - heading: "## Blocked"
    body: ""

Inheritance rules:

  • Questions: base questions appear first, YAML questions appended after

  • Metadata: base metadata applied first, YAML keys overwrite or add

  • Sections: if YAML defines sections, they replace base sections entirely; otherwise base sections are kept

  • Hooks: inherited from base (YAML can’t define hooks)

  • Single-level extends only (no chaining standupnote → something else)

Example extending project to inherit its directory-creation hook:

name: research-project
extends: project

questions:
  - key: hypothesis
    prompt: "Research hypothesis"

metadata:
  type: project
  dev-type: spike
  hypothesis: "{hypothesis}"

Template protocol

Member

Purpose

name

String used in -t/--type. Must be unique across templates.

questions()

Returns list[Question]. Each question becomes an input field in the TUI or a -a key=value flag in scripted mode. Return [] for no extra questions.

build_data(answers)

Receives a dict with title, tags, and any question keys. Returns a ZettelData with metadata and sections populated. Consistency defaults (date, id, publish, processed) are filled automatically by the Zettel entity — don’t set them here.

hooks()

Returns list[Hook]. Each hook runs after the file is written. Receives (ZettelData, zettelkasten_path). Return [] for no hooks.

Question fields

Field

Type

Description

key

str

Answer key passed to build_data. Also the -a flag name.

prompt

str

Display text in TUI / help text.

default

Any

Pre-filled value. Used as fallback in scripted mode when -a omits this key.

choices

list[str] | None

If set, renders a select widget in TUI and validates input in scripted mode.

required

bool

If True and no answer or default exists, scripted mode aborts.

Hook signature

def my_hook(data: ZettelData, zettelkasten_path: Path) -> None:
    # data.metadata has the final zettel metadata (id, title, etc.)
    # zettelkasten_path is the resolved path to the zettelkasten dir
    ...

Wrap it in a Hook dataclass:

Hook(name="my_hook", fn=my_hook)

CLI usage

# TUI (interactive wizard)
bim create

# TUI with template pre-selected
bim create -t project

# Scripted (non-interactive, requires both -t and --title)
bim create -t note --title "My note" --tags "foo,bar"

# With template-specific answers
bim create -t project --title "Auth refactor" -a dev_type=spike

CreateZettelUseCase

class buvis.pybase.zettel.application.use_cases.create_zettel_use_case.CreateZettelUseCase(zettelkasten_path: Path, writer: ZettelWriter, hook_runner: Callable[[list[Hook], ZettelData, Path], None] | None = None)

Bases: object

execute(template: ZettelTemplate, answers: dict[str, Any]) Path

Integrations

Jira Assemblers

class buvis.pybase.zettel.integrations.jira.assemblers.project_zettel_jira_issue.ProjectZettelJiraIssueDTOAssembler(defaults: dict[str, Any] | None = None)

Bases: object

to_dto(source: ProjectZettel) JiraIssueDTO