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.
- If None, returns “001”
- If numeric (e.g., “99”), returns str(int + 1) (e.g., “100”)
- If non-numeric (e.g., “fish”), returns “1” + “0” * len (e.g., “10000”)
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.
MenuItem Objects
class MenuItem(Static)
A focusable menu item.
Clicked Objects
class Clicked(Message)
Posted when this item is clicked.
MenuSeparator Objects
class MenuSeparator(Static)
A horizontal separator line.
MenuRow Objects
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.
navigate
def navigate(direction: int) -> MenuItem | None
Move focus by direction (+1/-1). Return new item, or None at edge.
MenuList Objects
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:
- Call
_init_watcher()in__init__ - Use
self.node_watch(node, key, callback)instead ofnode.watch(...) - Use
with self.suppressing():around model writes - Skip writing
on_unmount– the mixin handles cleanup
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.
NavButton Objects
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:
- DraggableMixin: on dragged widgets, owns the “flying” phase
- DropTarget: on containers, owns the “landing” phase
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:
- Call _init_draggable() in init
- Implement draggable_make_ghost() to return the ghost widget
- Implement draggable_clicked() for click-without-drag behavior
- Optionally override DRAG_THRESHOLD and HORIZONTAL_ONLY
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
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.
build_footer_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:
- sections is a list of (title, body) tuples
- First section is the h1 (title may be “” if no h1)
- Subsequent sections are h2s
- meta is the front-matter dict (or {})
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.