ganban

ganban.model

Reactive model tree.

ganban.model.node

Reactive tree nodes with change notification and bubbling.

Node Objects

class Node()

Reactive dict-like tree node.

Stores data in an internal dict, accessed via attribute syntax. Setting a value to None deletes the key. Dict values are auto-wrapped as child Nodes. Changes fire watchers and bubble up through the parent chain.

watch

def watch(key: str, callback: Callback) -> Callable[[], None]

Watch a key for changes. Returns an unwatch callable.

keys

def keys()

Return children keys.

items

def items()

Return children items.

values

def values()

Return children values.

path

@property
def path() -> str

Dotted path from root to this node.

update

def update(other: Node) -> None

Update this node in-place to match other, preserving watchers.

rename_key

def rename_key(old_key: str, new_key: str) -> None

Rename a key in _children, preserving insertion order.

ListNode Objects

class ListNode()

Ordered, id-keyed collection with change notification.

Items are accessed by string id. Setting to None deletes. Dicts are auto-wrapped as Nodes. Changes fire watchers and bubble up through the parent chain.

watch

def watch(key: str, callback: Callback) -> Callable[[], None]

Watch an item id for changes. Returns an unwatch callable.

path

@property
def path() -> str

Dotted path from root to this node.

keys

def keys()

Return ordered keys.

items

def items()

Return ordered (key, value) pairs.

update

def update(other: ListNode) -> None

Update this list in-place to match other, preserving watchers.

rename_first_key

def rename_first_key(new_title: str) -> None

Rename the first key by rebuilding the list.

ganban.model.loader

Load a ganban board from git into a Node tree.

file_creation_date

def file_creation_date(repo_path: str,
                       file_path: str,
                       branch: str = BRANCH_NAME) -> datetime | None

Get the author date of the commit that first added a file on a branch.

Returns None if the file has no history on the branch.

load_board

def load_board(repo_path: str, branch: str = BRANCH_NAME) -> Node

Load a complete board from a git branch as a Node tree.

ganban.model.card

Card mutation operations for ganban boards.

create_card

def create_card(board: Node,
                title: str,
                body: str = "",
                column: Node | None = None,
                position: int | None = None) -> tuple[str, Node]

Create a new card and add it to the board.

Returns (card_id, card_node).

find_card_column

def find_card_column(board: Node, card_id: str) -> Node | None

Find the column containing a card.

move_card

def move_card(board: Node,
              card_id: str,
              target_column: Node,
              position: int | None = None) -> None

Move a card to target_column at position.

Handles same-column reorder atomically (single list assignment) to avoid watchers removing the card widget between operations.

archive_card

def archive_card(board: Node, card_id: str) -> None

Archive a card by removing it from its column’s links.

ganban.model.writer

Save a ganban board (Node tree) to git without touching the working tree.

MergeRequired Objects

@dataclass
class MergeRequired()

Returned by check_for_merge when the branch has diverged.

save_board

def save_board(board: Node,
               message: str = "Update board",
               branch: str = BRANCH_NAME,
               parents: list[str] | None = None) -> str

Save a board to git and return the new commit hash.

check_for_merge

def check_for_merge(board: Node,
                    branch: str = BRANCH_NAME) -> MergeRequired | None

Check if saving would require a merge.

check_remote_for_merge

def check_remote_for_merge(board: Node,
                           remote: str = "origin",
                           branch: str = BRANCH_NAME) -> MergeRequired | None

Check if a remote has changes that need merging.

try_auto_merge

def try_auto_merge(board: Node,
                   merge_info: MergeRequired,
                   message: str = "Merge changes",
                   branch: str = BRANCH_NAME) -> str | None

Attempt an automatic merge if there are no conflicts.

Returns the new merge commit hash if successful, None if there are conflicts.

ganban.model.column

Column mutation operations for ganban boards.

slugify

def slugify(text: str) -> str

Convert text to a URL-friendly slug.

build_column_path

def build_column_path(order: str, name: str, hidden: bool = False) -> str

Build column directory path from components.

create_column

def create_column(board: Node,
                  name: str,
                  order: str | None = None,
                  hidden: bool = False) -> Node

Create a new column and add it to the board.

Returns the created column Node.

move_column

def move_column(board: Node, column: Node, new_index: int) -> None

Move column to new_index in the board’s columns ListNode.

Rebuilds the columns ListNode with updated order values and dir_paths.

archive_column

def archive_column(board: Node, column_order: str) -> None

Archive a column by removing it from the board.

rename_column

def rename_column(board: Node, column: Node, new_name: str) -> None

Rename a column: update its sections title and dir_path.

ganban.ids

Card ID comparison and generation.

compare_ids

def compare_ids(left: str, right: str) -> int

Compare two IDs, padding with leading zeros.

Returns -1 if left < right, 0 if equal, 1 if left > right.

max_id

def max_id(ids: list[str]) -> str | None

Find the highest ID from a list, or None if empty.

next_id

def next_id(current_max: str | None) -> str

Generate the next ID after current_max.

ganban.git

Git operations for ganban, with sync and async variants.

read_ganban_config

def read_ganban_config(repo_path: str | Path) -> dict

Read ganban.* keys from local git config, returned as python-keyed dict.

write_ganban_config_key

def write_ganban_config_key(repo_path: str | Path, key: str, value) -> None

Write one ganban.* key to local git config.

key is the python name (e.g. sync_interval), converted to git name (sync-interval).

is_git_repo

def is_git_repo(path: str | Path) -> bool

Check if path is inside a git repository.

init_repo

def init_repo(path: str | Path) -> Repo

Initialize a new git repository at path.

get_remotes_sync

def get_remotes_sync(repo_path: str | Path) -> list[str]

Get list of remote names for a repository.

fetch_sync

def fetch_sync(repo_path: str | Path, remote_name: str) -> None

Fetch from a specific remote.

push_sync

def push_sync(repo_path: str | Path,
              remote_name: str,
              branch: str = "ganban") -> None

Push a branch to a remote.

get_upstream

def get_upstream(repo_path: str | Path,
                 branch: str = "ganban") -> tuple[str, str] | None

Get the upstream remote and branch for a local branch.

Returns (remote_name, remote_branch) or None if no tracking branch is set.

remote_has_branch

def remote_has_branch(repo_path: str | Path,
                      remote_name: str,
                      branch: str = "ganban") -> bool

Check if refs/remotes/{remote}/{branch} exists.

has_branch

async def has_branch(repo_path: str | Path, branch: str = "ganban") -> bool

Check if a branch exists in the repository.

get_remotes

async def get_remotes(repo_path: str | Path) -> list[str]

Get list of remote names for a repository.

fetch

async def fetch(repo_path: str | Path, remote_name: str) -> None

Fetch from a specific remote.

push

async def push(repo_path: str | Path,
               remote_name: str,
               branch: str = "ganban") -> None

Push a branch to a remote.

create_orphan_branch

async def create_orphan_branch(repo_path: str | Path,
                               branch: str = "ganban") -> str

Create an orphan branch with an empty commit.

Does not touch the working tree. Returns the commit hash.

ganban.cli.init

Handler for ‘ganban init’.

init_board

def init_board(args) -> int

Initialize a ganban board in the repository.

ganban.cli

CLI argument parser and dispatch for ganban.

build_parser

def build_parser() -> argparse.ArgumentParser

Build the full CLI argument parser.

ganban.cli._common

Shared helpers for CLI command handlers.

load_board_or_die

def load_board_or_die(repo: str, json_mode: bool) -> Node

Load board from repo path. Exit 1 with message if not found.

find_column

def find_column(board: Node, col_id: str, json_mode: bool) -> Node

Lookup column by order ID. Exit 1 listing available columns if not found.

find_card

def find_card(board: Node, card_id: str, json_mode: bool) -> Node

Lookup card by ID. Exit 1 if not found.

save

def save(board: Node, message: str) -> str

Save board and return commit hash.

output_json

def output_json(data: dict | list) -> None

Write JSON to stdout.

error

def error(message: str, json_mode: bool) -> None

Print error to stderr and exit 1.

sections_to_markdown

def sections_to_markdown(sections: ListNode, meta) -> str

Convert sections ListNode + meta to markdown string.

meta_to_dict

def meta_to_dict(meta) -> dict

Convert meta Node to plain dict.

markdown_to_sections

def markdown_to_sections(text: str) -> tuple[ListNode, dict]

Parse markdown text into (ListNode, meta_dict).

ganban.cli.board

Handlers for ‘ganban board’ commands.

board_summary

def board_summary(args) -> int

Show board summary: title, columns, card counts.

board_get

def board_get(args) -> int

Dump board index.md content.

board_set

def board_set(args) -> int

Write board index.md from stdin.

ganban.cli.card

Handlers for ‘ganban card’ commands.

card_list

def card_list(args) -> int

List cards grouped by column.

card_get

def card_get(args) -> int

Dump card markdown content.

card_set

def card_set(args) -> int

Write card markdown from stdin.

card_add

def card_add(args) -> int

Create a new card.

card_move

def card_move(args) -> int

Move a card to a column.

card_archive

def card_archive(args) -> int

Archive a card.

ganban.cli.sync

Handlers for ‘ganban sync’ command.

sync

def sync(args) -> int

One-shot sync handler. Dispatches to daemon if -d.

sync_daemon

def sync_daemon(args, repo_path: str) -> int

Loop _do_sync on interval. SIGINT/SIGTERM stops cleanly.

ganban.cli.column

Handlers for ‘ganban column’ commands.

column_list

def column_list(args) -> int

List all columns.

column_get

def column_get(args) -> int

Dump column index.md content.

column_set

def column_set(args) -> int

Write column index.md from stdin.

column_add

def column_add(args) -> int

Create a new column.

column_move

def column_move(args) -> int

Move a column to a new position.

column_rename

def column_rename(args) -> int

Rename a column.

column_archive

def column_archive(args) -> int

Archive a column.

ganban.__main__

Entry point for ganban CLI.

ganban.ui.markdown

Markdown-it plugins for ganban.

mailto_display_plugin

def mailto_display_plugin(md: MarkdownIt, meta: Node,
                          committers: list[str] | None) -> None

Core rule replacing mailto link text with emoji + name.

card_ref_plugin

def card_ref_plugin(md: MarkdownIt, board: Node) -> None

Core rule replacing NNN card references with links.

ganban_parser_factory

def ganban_parser_factory(board: Node | None)

Return a parser_factory closure for Textual’s Markdown widget.

ganban.ui.blocked

Blocked toggle widget for card detail screen.

BlockedWidget Objects

class BlockedWidget(NodeWatcherMixin, Container)

Inline blocked toggle that reads and writes meta.blocked.

Shows 🚧 when blocked, 🏭 when not. Click to toggle.

ganban.ui.search

Autocomplete search input with dropdown suggestions.

SearchInput Objects

class SearchInput(Container)

Text input with a filterable dropdown of suggestions.

Options are (label, value) tuples. The label is shown in the dropdown, the value is returned on selection. Free-text is always allowed.

Submitted Objects

class Submitted(Message)

Posted when the user submits a selection or free text.

Cancelled Objects

class Cancelled(Message)

Posted when the user cancels (double-escape).

set_options

def set_options(options: list[tuple[str, str]]) -> None

Replace the option list.

on_input_changed

def on_input_changed(event: Input.Changed) -> None

Filter dropdown on every keystroke.

on_option_list_option_selected

def on_option_list_option_selected(event: OptionList.OptionSelected) -> None

Handle mouse click on a dropdown item.

on_descendant_blur

def on_descendant_blur(event: DescendantBlur) -> None

Close dropdown when focus leaves a child widget.

ganban.ui.confirm

Compact inline confirmation widget.

ConfirmButton Objects

class ConfirmButton(Static)

A button that shows a confirm/cancel menu on click.

Shows a single icon (default: ❌). When clicked, opens a context menu with ❌ (cancel) and ✅ (confirm). Emits Confirmed message on confirm.

Confirmed Objects

class Confirmed(Message)

Emitted when the action is confirmed.

ganban.ui.assignee

Assignee widget with user picker.

resolve_assignee

def resolve_assignee(assigned: str, board: Node) -> tuple[str, str, str]

Parse an assigned string and resolve against board users.

Returns (emoji, display_name, email). Board users override the name and emoji from the parsed committer string.

build_assignee_options

def build_assignee_options(board: Node) -> list[tuple[str, str]]

Build options for the assignee SearchInput from board users and git committers.

Returns (label, value) tuples where label includes emoji and value is the committer string.

AssigneeWidget Objects

class AssigneeWidget(NodeWatcherMixin, Container)

Inline assignee display with user picker.

Reads and writes meta.assigned on the given card meta Node, and watches the node so external changes are reflected immediately.

ganban.ui.due

Due date widget with inline editing.

DueDateWidget Objects

class DueDateWidget(NodeWatcherMixin, Container)

Inline due date display with calendar picker.

Reads and writes meta.due on the given Node directly, and watches the node so external changes (e.g. the meta editor) are reflected immediately.

ganban.ui.menu

Context menu system for ganban UI.

class MenuItem(Static)

A focusable menu item.

Clicked Objects

class Clicked(Message)

Posted when this item is clicked.

class MenuSeparator(Static)

A horizontal separator line.

class MenuRow(Horizontal)

A horizontal row of menu items within a vertical menu.

get_focusable_items

def get_focusable_items() -> list[MenuItem]

Return enabled items in this row.

def navigate(direction: int) -> MenuItem | None

Move focus by direction (+1/-1). Return new item, or None at edge.

class MenuList(VerticalScroll)

Container for menu items.

Ready Objects

class Ready(Message)

Posted when menu has been laid out and has a size.

on_resize

def on_resize(event) -> None

Notify when we have actual dimensions.

get_navigable_items

def get_navigable_items() -> list[tuple[Static, list[MenuItem]]]

Return (top_level_child, focusable_descendants) for vertical nav.

A plain MenuItem has [itself]. A container has its focusable MenuItems. Non-focusable items (separators) are skipped.

ContextMenu Objects

class ContextMenu(ModalScreen[MenuItem | None])

Context menu with keyboard navigation.

ItemSelected Objects

class ItemSelected(Message)

Posted when an item is selected.

on_menu_list_ready

def on_menu_list_ready(event: MenuList.Ready) -> None

Adjust menu position when we know actual dimensions.

on_descendant_focus

def on_descendant_focus(event) -> None

React to any focus change within the menu.

action_focus_prev

def action_focus_prev() -> None

Focus the previous enabled item in current menu (wraps around).

action_focus_next

def action_focus_next() -> None

Focus the next enabled item in current menu (wraps around).

action_select_item

def action_select_item() -> None

Select leaf item or enter submenu.

action_navigate_right

def action_navigate_right() -> None

Move right in row, or enter submenu / select.

action_navigate_left

def action_navigate_left() -> None

Move left in row, or close submenu.

action_close

def action_close() -> None

Close the entire menu.

on_menu_item_clicked

def on_menu_item_clicked(event: MenuItem.Clicked) -> None

Handle item click.

on_click

def on_click(event: Click) -> None

Dismiss menu when clicking outside.

on_calendar_menu_item_selected

def on_calendar_menu_item_selected(event) -> None

Handle calendar menu item selection.

ganban.ui.watcher

Mixin that manages Node watches with suppression and auto-cleanup.

NodeWatcherMixin Objects

class NodeWatcherMixin()

Mixin for widgets that watch Node keys.

Subclasses should:

node_watch

def node_watch(node: Node | ListNode, key: str, callback: Callback) -> None

Register a watch that is auto-guarded by suppression and auto-cleaned on unmount.

suppressing

@contextmanager
def suppressing()

Context manager that suppresses watch callbacks for model writes.

ganban.ui.color

Color picker for columns.

ColorSwatch Objects

class ColorSwatch(MenuItem)

A colored menu item that uses outline for focus instead of background.

build_color_menu

def build_color_menu() -> list[MenuRow]

Build a 4x4 color picker grid with clear in place of black.

ColorButton Objects

class ColorButton(Static)

A button that opens a color picker menu.

ColorSelected Objects

class ColorSelected(Message)

Posted when a color is selected.

ganban.ui

Textual UI for ganban.

ganban.ui.users

Users editor for board meta.

EmailTag Objects

class EmailTag(Container)

A single email address tag — click to edit with committer search.

AddEmailButton Objects

class AddEmailButton(Container)

Searchable input to add a new email address from git committers.

UserRow Objects

class UserRow(Vertical)

A single user card with title bar and email list.

AddUserRow Objects

class AddUserRow(Static)

EditableText with ‘+’ to add a new user.

UsersEditor Objects

class UsersEditor(NodeWatcherMixin, Container)

Editor for board.meta.users – a dict of display name -> user info.

ganban.ui.detail

Detail modals for viewing and editing markdown content.

TabButton Objects

class TabButton(Static)

A clickable tab icon button.

DetailModal Objects

class DetailModal(ModalScreen[None])

Base modal screen for detail editing.

on_click

def on_click(event: Click) -> None

Dismiss modal when clicking outside the detail container.

action_close

def action_close() -> None

Close the modal.

action_quit

async def action_quit() -> None

Quit the app via the main save-and-exit path.

CardDetailModal Objects

class CardDetailModal(DetailModal)

Modal screen showing full card details.

CompactButton Objects

class CompactButton(Static)

Toggle button for compact/card view mode.

ColumnDetailModal Objects

class ColumnDetailModal(DetailModal)

Modal screen showing full column details.

BoardDetailModal Objects

class BoardDetailModal(DetailModal)

Modal screen showing full board details.

ganban.ui.edit.messages

Messages for editable widgets.

Save Objects

class Save(Message)

Editor finished - save this value.

Cancel Objects

class Cancel(Message)

Editor finished - discard changes.

ganban.ui.edit.document

Markdown document editor widget.

DocHeader Objects

class DocHeader(NodeWatcherMixin, Container)

Editable document title with rule underneath.

TitleChanged Objects

class TitleChanged(Message)

Emitted when the title changes.

AddSection Objects

class AddSection(Static)

Widget to add a new subsection.

SectionCreated Objects

class SectionCreated(Message)

Posted when a new section is created.

MarkdownDocEditor Objects

class MarkdownDocEditor(NodeWatcherMixin, Container)

Two-panel editor for markdown sections content.

Changed Objects

class Changed(Message)

Emitted when the document content changes.

on_section_editor_heading_changed

def on_section_editor_heading_changed(
        event: SectionEditor.HeadingChanged) -> None

Update sections when a section heading changes.

on_section_editor_body_changed

def on_section_editor_body_changed(event: SectionEditor.BodyChanged) -> None

Update sections when a body changes.

on_section_editor_delete_requested

def on_section_editor_delete_requested(
        event: SectionEditor.DeleteRequested) -> None

Remove a subsection.

on_add_section_section_created

def on_add_section_section_created(event: AddSection.SectionCreated) -> None

Add a new subsection.

ganban.ui.edit

Editable widget components.

ganban.ui.edit.viewers

Viewer widgets that display content and support update(value).

TextViewer Objects

class TextViewer(Static)

Simple text viewer.

update

def update(value: str) -> None

Update the displayed text.

MarkdownViewer Objects

class MarkdownViewer(VerticalScroll)

Markdown viewer container.

update

def update(value: str) -> None

Update the displayed markdown.

refresh_content

def refresh_content() -> None

Re-render current value (e.g. after external data changes).

ganban.ui.edit.section

Section editor widget.

SectionEditor Objects

class SectionEditor(Container)

Editor for a section with heading and markdown body.

HeadingChanged Objects

class HeadingChanged(Message)

Emitted when the section heading changes.

BodyChanged Objects

class BodyChanged(Message)

Emitted when the section body changes.

DeleteRequested Objects

class DeleteRequested(Message)

Emitted when the section delete is confirmed.

ganban.ui.edit.meta

Tree-shaped metadata editor for Node objects.

BoolToggle Objects

class BoolToggle(Static)

A simple true/false toggle that cycles on click.

ListItemRow Objects

class ListItemRow(Vertical)

A single item row in a list editor.

Scalar values render inline. Compound values (dict, list) render with a header row and the nested editor below.

on_key_value_row_value_changed

def on_key_value_row_value_changed(event) -> None

Propagate changes from nested DictEditor.

AddListItemRow Objects

class AddListItemRow(Static)

Clickable ‘+’ that opens a type picker to add a new list item.

ListEditor Objects

class ListEditor(Vertical)

Renders a list’s items as editable rows.

Changed Objects

class Changed(Message)

Emitted when the list contents change.

KeyValueRow Objects

class KeyValueRow(Vertical)

A single key:value row in the metadata editor.

Scalar values render inline as key : value. Compound values (dict, list) render with a header row and the nested editor indented below.

AddKeyRow Objects

class AddKeyRow(Container)

Row to add a new key to the metadata.

DictEditor Objects

class DictEditor(NodeWatcherMixin, Vertical)

Renders a Node’s children as KeyValueRows + AddKeyRow.

MetaEditor Objects

class MetaEditor(Container)

Thin wrapper for the tree metadata editor with scroll support.

ganban.ui.edit.editors

Editor widgets that emit Save/Cancel messages.

BaseEditor Objects

class BaseEditor(TextArea)

Base editor that emits Save/Cancel.

start_editing

def start_editing(value: str) -> None

Start editing. Called by EditableText.

TextEditor Objects

class TextEditor(BaseEditor)

Single-line editor. Enter saves.

NumberEditor Objects

class NumberEditor(TextEditor)

Single-line numeric editor. Validates input is a number on save.

MarkdownEditor Objects

class MarkdownEditor(BaseEditor)

Multi-line editor. Enter inserts newline.

ganban.ui.edit.editable

Editable text container widget.

EditableText Objects

class EditableText(Container)

Orchestrates view/edit switching for any viewer + editor pair.

Changed Objects

class Changed(Message)

Emitted when the value changes.

control

@property
def control() -> EditableText

The EditableText that changed.

focus

def focus(scroll_visible: bool = True) -> None

Focus the widget - enters edit mode if not already editing.

ganban.ui.constants

Shared UI icon constants.

ganban.ui.cal

Calendar widget for date selection.

date_diff

def date_diff(target: date, reference: date) -> str

Return compact string showing difference between dates.

Examples: “1d”, “-3d”, “2m”, “-1m”, “5y”, “-2y” Uses days for <60 days, months for <24 months, years otherwise.

class NavButton(Static)

Navigation button for calendar.

CalendarDay Objects

class CalendarDay(Static)

A single day cell.

Clicked Objects

class Clicked(Message)

Posted when this day is clicked.

Calendar Objects

class Calendar(Container)

Date picker widget.

DateSelected Objects

class DateSelected(Message)

Emitted when a date is selected (None to clear).

CalendarMenuItem Objects

class CalendarMenuItem(Container)

Menu item containing a calendar picker.

Selected Objects

class Selected(Message)

Posted when a date is selected, signals menu to close.

DateButton Objects

class DateButton(Static)

A button that opens a calendar menu for date selection.

DateSelected Objects

class DateSelected(Message)

Emitted when a date is selected (None to clear).

ganban.ui.sync_widget

Sync status indicator widget for the board header.

SyncWidget Objects

class SyncWidget(NodeWatcherMixin, Container)

Sync status indicator. Shows current sync state as an emoji.

Click to open a context menu with local/remote toggles and interval presets.

ganban.ui.board

Board screen showing kanban columns and cards.

BoardScreen Objects

class BoardScreen(NodeWatcherMixin, DropTarget, Screen)

Main board screen showing all columns.

on_editable_text_changed

def on_editable_text_changed(event: EditableText.Changed) -> None

Update board title when header is edited.

on_click

def on_click(event) -> None

Handle clicks on board header area.

action_context_menu

def action_context_menu() -> None

Show context menu for the focused widget.

action_save

def action_save() -> None

Save the board to git.

on_card_widget_move_requested

def on_card_widget_move_requested(event: CardWidget.MoveRequested) -> None

Handle card move request.

on_card_widget_archive_requested

def on_card_widget_archive_requested(
        event: CardWidget.ArchiveRequested) -> None

Handle card archive request.

on_add_card_card_created

def on_add_card_card_created(event: AddCard.CardCreated) -> None

Handle new card creation — commit immediately for timestamp.

on_add_column_column_created

def on_add_column_column_created(event: AddColumn.ColumnCreated) -> None

Handle new column creation.

on_column_widget_move_requested

def on_column_widget_move_requested(event: ColumnWidget.MoveRequested) -> None

Handle column move request.

on_column_widget_archive_requested

def on_column_widget_archive_requested(
        event: ColumnWidget.ArchiveRequested) -> None

Handle column archive request.

ganban.ui.done

Done toggle widget for card detail screen.

DoneWidget Objects

class DoneWidget(NodeWatcherMixin, Container)

Inline done toggle that reads and writes meta.done.

Shows ✅ when done, ⬜ when not. Click to toggle. Watches the node so external changes (e.g. the meta editor or context menu) are reflected immediately.

ganban.ui.card

Card widgets for ganban UI.

CardWidget Objects

class CardWidget(NodeWatcherMixin, DraggableMixin, Static)

A single card in a column.

MoveRequested Objects

class MoveRequested(Message)

Posted when card should be moved to another column.

ArchiveRequested Objects

class ArchiveRequested(Message)

Posted when card should be archived.

AddCard Objects

class AddCard(Static)

Widget to add a new card to a column.

CardCreated Objects

class CardCreated(Message)

Posted when a new card is created.

ganban.ui.static

Static widget variants.

PlainStatic Objects

class PlainStatic(Static)

Static that doesn’t allow text selection.

ganban.ui.drag

Drag-and-drop infrastructure for ganban UI.

Two mixins:

DropTarget Objects

class DropTarget()

Mixin for widgets that can accept drops.

Returns False to ignore (bubbles to parent), True to consume.

drag_over

def drag_over(draggable: DraggableMixin, x: int, y: int) -> bool

Called while a draggable hovers over this target. Return True to accept.

drag_away

def drag_away(draggable: DraggableMixin) -> None

Called when a draggable leaves this target.

try_drop

def try_drop(draggable: DraggableMixin, x: int, y: int) -> bool

Called on mouse-up to attempt the drop. Return True if accepted.

DraggableMixin Objects

class DraggableMixin()

Mixin for widgets that can be dragged.

Subclasses should:

draggable_make_ghost

def draggable_make_ghost() -> Widget

Create and return the ghost widget for dragging. Override in subclass.

draggable_clicked

def draggable_clicked() -> None

Called when mouse released without dragging. Override for click behavior.

DragGhost Objects

class DragGhost(Static)

Floating overlay showing the card being dragged.

CardPlaceholder Objects

class CardPlaceholder(Static)

Placeholder showing where a dragged card will drop.

ColumnPlaceholder Objects

class ColumnPlaceholder(Static)

Placeholder showing where a dragged column will drop.

ganban.ui.app

Main Textual application for ganban.

ConfirmInitScreen Objects

class ConfirmInitScreen(ModalScreen[bool])

Modal screen asking to initialize a git repo.

GanbanApp Objects

class GanbanApp(App)

Git-based kanban board TUI.

action_quit

def action_quit() -> None

Cancel sync, save and quit.

ganban.ui.emoji

Emoji picker widget.

emoji_for_email

def emoji_for_email(email: str) -> str

Pick a deterministic default emoji for an email address.

Uses the last nibble of the md5 digest, modulo 13.

parse_committer

def parse_committer(committer: str) -> tuple[str, str, str]

Parse a committer string into (emoji, name, email).

Accepts “Name " format. If parsing fails, the full string is used as both name and email.

build_emoji_menu

def build_emoji_menu(email: str | None = None) -> list[MenuRow]

Build a 6x5 emoji picker grid with default/clear as first cell.

EmojiButton Objects

class EmojiButton(Static)

A button that opens an emoji picker menu.

EmojiSelected Objects

class EmojiSelected(Message)

Posted when an emoji is selected.

find_user_by_email

def find_user_by_email(email: str,
                       meta: Node | None) -> tuple[str, Node] | None

Find the (user_name, user_node) for an email in meta.users.

resolve_email_display

def resolve_email_display(
        email: str,
        meta: Node | None = None,
        committers: list[str] | None = None) -> tuple[str, str] | None

Resolve an email to (emoji, display_name).

Checks meta.users first (custom emoji + user name), then git committers (hash emoji + committer name). Returns None if the email isn’t found in either source.

resolve_email_emoji

def resolve_email_emoji(email: str, meta: Node) -> str

Look up the emoji for an email from meta.users, falling back to hash.

EmailEmoji Objects

class EmailEmoji(NodeWatcherMixin, Static)

Display-only emoji resolved from an email address.

Watches meta.users so it updates when custom emojis change.

ganban.ui.column

Column widgets for ganban UI.

ColumnWidget Objects

class ColumnWidget(NodeWatcherMixin, DraggableMixin, DropTarget, Vertical)

A single column on the board.

MoveRequested Objects

class MoveRequested(Message)

Posted when column should be moved.

ArchiveRequested Objects

class ArchiveRequested(Message)

Posted when column should be archived.

draggable_make_ghost

def draggable_make_ghost()

Column IS the ghost — use self with CSS overlay positioning.

on_editable_text_changed

def on_editable_text_changed(event: EditableText.Changed) -> None

Update column name when header is edited.

on_click

def on_click(event) -> None

Show context menu on right-click.

on_key

def on_key(event) -> None

Arrow key navigation and shift+arrow card movement.

on_mouse_move

def on_mouse_move(event) -> None

Handle DraggableMixin threshold first, then hover-focus tracking.

AddColumn Objects

class AddColumn(Vertical)

Widget to add a new column.

ColumnCreated Objects

class ColumnCreated(Message)

Posted when a new column is created.

ganban.ui.card_indicators

Pure functions for building card indicator text.

def build_footer_text(sections: ListNode,
                      meta: Node,
                      board_meta: Node | None = None) -> Text

Build footer indicators from card sections and meta.

Shows assignee emoji if meta.assigned is set. Shows body icon (dim) if first section has body content. Shows calendar icon + Xd if meta.due is set, red if overdue.

ganban.sync

Background sync engine for the TUI.

Runs: pull → load+merge → save → push, gated by board.git.sync toggles.

run_sync_cycle

async def run_sync_cycle(board)

Run one sync cycle: pull → save → merge → load → push.

Reads board.git.sync.{local, remote} to decide which steps to run. Sets sync.status at each step (fires watchers → UI updates). All git I/O runs via asyncio.to_thread to stay non-blocking.

ganban.parser

Parse markdown documents with front-matter.

parse_sections

def parse_sections(text: str) -> tuple[list[tuple[str, str]], dict]

Parse markdown into an ordered list of (title, body) sections plus meta.

Returns (sections, meta) where:

serialize_sections

def serialize_sections(sections: list[tuple[str, str]],
                       meta: dict | None = None) -> str

Serialize sections and meta back to markdown text.

First section becomes # heading, rest become ## headings. Meta becomes YAML front-matter if non-empty.

first_title

def first_title(sections) -> str

Get the title (first key) of a sections ListNode, or empty string.

first_body

def first_body(sections) -> str

Get the body (first value) of a sections ListNode, or empty string.