ganban
ganban.sync
Background sync engine for the TUI.
Runs: pull → load+merge → save → push, gated by board.git.sync toggles.
apply_reload
def apply_reload(board, new_board) -> None
Apply a freshly loaded board in place, preserving live sync/config nodes.
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.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.
find_nearest
def find_nearest(region: Region) -> "Widget | None"
Find the nearest child widget to the given region.
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.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(AddValueMixin, 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.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.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_close
async def action_close() -> None
Close the board (quit the app).
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_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; reconciliation unmounts the widget.
ganban.ui.labels_editor
Board-level labels editor for the board detail modal.
SavedLabelRow Objects
class SavedLabelRow(_LabelRow)
A saved label (from board.meta.labels) shown as a tag with count.
Delete removes the override only - label stays on cards with computed color.
DeleteRequested Objects
class DeleteRequested(Message)
Delete override only - label stays on cards.
UsedLabelRow Objects
class UsedLabelRow(_LabelRow)
A used label (on cards, no override) shown as a tag with count and save.
Delete removes from all cards. Save creates an override.
DeleteRequested Objects
class DeleteRequested(Message)
Delete from all cards.
SaveRequested Objects
class SaveRequested(Message)
Create override from computed color.
AddLabelRow Objects
class AddLabelRow(AddValueMixin, Static)
EditableText with ‘+’ to add a new label.
LabelsEditor Objects
class LabelsEditor(NodeWatcherMixin, Container)
Editor for board-level labels — shown as a tab in BoardDetailModal.
Two sections:
- Saved Labels: from board.meta.labels (color overrides)
- Used Labels: on cards but no override (computed colors)
on_saved_label_row_delete_requested
def on_saved_label_row_delete_requested(
event: SavedLabelRow.DeleteRequested) -> None
Delete override only - label stays on cards.
on_used_label_row_delete_requested
def on_used_label_row_delete_requested(
event: UsedLabelRow.DeleteRequested) -> None
Delete from all cards.
on_used_label_row_save_requested
def on_used_label_row_save_requested(
event: UsedLabelRow.SaveRequested) -> None
Promote used label to saved with current computed color.
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.
find_nearest
def find_nearest(region)
Find the nearest focusable child by vertical midpoint.
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(AddValueMixin, Vertical)
Widget to add a new column.
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.cal
Calendar widget for date selection.
parse_due
def parse_due(due_str) -> date | None
Parse an ISO due-date string, returning None when absent or invalid.
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.card_indicators
Pure functions for building card indicator text.
build_label_text
def build_label_text(meta: Node, board: Node) -> Text
Build colored block characters for a card’s labels.
build_header_line
def build_header_line(meta: Node, board: Node, width: int) -> Text
Build a header line as colored horizontal rules.
No labels: a single dim rule. With labels: the width is split evenly into one colored rule segment per label.
build_footer_text
def build_footer_text(sections: ListNode,
meta: Node,
board_meta: Node | None = None,
blocked: bool = False) -> 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. Shows blocked icon if blocked is True.
ganban.ui.constants
Shared UI icon constants.
ganban.ui.tag
Reusable inline tag widget with edit and delete.
Tag Objects
class Tag(Static)
Inline tag: [label ×] in view mode, [SearchInput ×] in edit mode.
Parameters
value: Underlying data value (label name, card id, committer string). display: Rich renderable shown in view mode. Defaults to value.
Changed Objects
class Changed(Message)
User submitted a new value for this tag.
Deleted Objects
class Deleted(Message)
User clicked the × button.
update_display
def update_display(display: str | Text) -> None
Change the rendered label text.
start_editing
def start_editing(options: list[tuple[str, str]]) -> None
Enter edit mode with the given search options.
If called before the widget is composed, defers until on_mount.
TagListWidget Objects
class TagListWidget(NodeWatcherMixin, Container)
Base for inline tag-list editors bound to a list on a meta Node.
Renders an add icon plus one Tag per value. Subclasses set meta_key, icon and the element ids, implement options_for(), and may override display_for() and validate().
display_for
def display_for(value: str)
Renderable shown for a value in view mode.
options_for
def options_for(current: list[str]) -> list[tuple[str, str]]
Dropdown options offered while editing, given the other values.
validate
def validate(value: str) -> bool
Whether a submitted value may be stored.
ganban.ui.deps
Dependency editor widget for card detail bar.
ICON_DEPS
🔗
build_card_options
def build_card_options(board: Node,
exclude: str | set[str] = "") -> list[tuple[str, str]]
Build (label, value) options for card references.
Returns all non-archived cards, excluding the given id(s).
The label is "ID Title" and the value is the card ID.
build_dep_options
def build_dep_options(board: Node, card_id: str,
current_deps: list[str]) -> list[tuple[str, str]]
Build options for the dep search dropdown: non-archived cards minus the current card and existing deps.
DepsWidget Objects
class DepsWidget(TagListWidget)
Inline deps editor for card detail bar.
Displays dep IDs next to a link icon. Click the icon to add a dep, click a tag to edit it, click × to delete. Uses Tag widgets with SearchInput for card selection.
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.
target_column is the column’s order id, which is stable and unique where display titles need not be.
ArchiveRequested Objects
class ArchiveRequested(Message)
Posted when card should be archived.
AddCard Objects
class AddCard(AddValueMixin, Static)
Widget to add a new card to a column.
CardCreated Objects
class CardCreated(Message)
Posted when a new card is created.
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.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.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.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(AddValueMixin, 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.
on_resize
def on_resize(event) -> None
Apply pending click position once we know our layout.
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(CompletionMixin, BaseEditor)
Multi-line editor with inline autocomplete. Enter inserts newline.
ganban.ui.edit.section
Section editor widget.
match_editor_type
def match_editor_type(
heading: str | None,
editor_types: list[EditorType] | None) -> EditorType | None
Find the first EditorType whose pattern matches the heading.
EditorSelectButton Objects
class EditorSelectButton(Static)
Button that shows a context menu of available editor types.
Selected Objects
class Selected(Message)
Emitted when an editor type is selected.
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.
EditorTypeSelected Objects
class EditorTypeSelected(Message)
Emitted when the editor type is changed.
focus_body
def focus_body() -> None
Focus the body editor.
ItemRow Objects
class ItemRow(Horizontal)
Base for item rows in bullet list editors.
BulletListEditor Objects
class BulletListEditor(SectionEditor)
Base for editors that present a bullet list as individual rows.
Subclasses set class attributes for CSS styling and override three hooks:
_make_row, _format_new_item, _format_edited_item.
focus_body
def focus_body() -> None
Focus the add-item input.
ganban.ui.edit.completion
Inline autocomplete for editors.
Typing a trigger character (e.g. @ or #) after whitespace or at the
start of a line opens a floating dropdown that filters as you type.
Enter/Tab selects the top match, Space or Escape cancels.
Ctrl+Space shows all sources merged.
CompletionSource Objects
@dataclass
class CompletionSource()
A trigger character and a callable that returns completion options.
The callable returns list[tuple[str, str]] where each tuple is
(display_label, insert_value).
If replace_trigger is True, the trigger character is removed on
selection and only the value is inserted.
CompletionDropdown Objects
class CompletionDropdown(OptionList)
Floating dropdown for completion results.
Mounted once on the screen and shown/hidden as needed. Uses
overlay: screen CSS so it floats above all other widgets.
filter
def filter(query: str, options: list[tuple[str, str]]) -> None
Repopulate the list with options matching query.
select_highlighted
def select_highlighted() -> tuple[str, str] | None
Return (label, value) of the highlighted option, or None.
show_at
def show_at(x: int, y: int) -> None
Position the dropdown and make it visible.
hide
def hide() -> None
Hide the dropdown.
CompletionMixin Objects
class CompletionMixin()
Mixin for BaseEditor subclasses that adds trigger-based completion.
Must be listed before the editor base in the MRO so that _on_key
intercepts events before the editor handles them.
Call _init_completion(sources) from __init__ and
_deactivate_completion() on blur.
on_text_area_changed
def on_text_area_changed(event) -> None
React to text changes with reliable cursor position.
ganban.ui.edit.tasks
Task list editor widget.
TaskCheckbox Objects
class TaskCheckbox(Static)
Clickable checkbox indicator for a task.
Toggled Objects
class Toggled(Message)
Emitted when the checkbox is clicked.
TaskRow Objects
class TaskRow(ItemRow)
A single task in the task list.
TasksEditor Objects
class TasksEditor(BulletListEditor)
Editor for task list sections using bullet-list extraction.
ganban.ui.edit.blocks
Reusable block extraction from markdown body text.
ExtractedList Objects
@dataclass
class ExtractedList()
A bullet list extracted from markdown body text.
extract_bullet_list
def extract_bullet_list(body: str) -> ExtractedList
Extract the first bullet list from markdown body using markdown-it.
Uses Token.map line ranges to slice the source text. Skips code fences (handled by markdown-it’s tokenizer). If no bullet list found, returns ExtractedList(before=body, items=[], after=””).
reconstruct_body
def reconstruct_body(extracted: ExtractedList) -> str
Reconstruct body from before + items + after.
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.edit.document
Markdown document editor widget.
EditorType Objects
@dataclass
class EditorType()
Describes an available editor for markdown sections.
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(AddValueMixin, 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_section_editor_editor_type_selected
def on_section_editor_editor_type_selected(
event: SectionEditor.EditorTypeSelected) -> None
Swap a subsection editor to a different type.
on_add_section_section_created
def on_add_section_section_created(event: AddSection.SectionCreated) -> None
Add a new subsection.
ganban.ui.edit.comments
Comments editor widget.
CommentRow Objects
class CommentRow(ItemRow)
A single comment in the comments list.
CommentsEditor Objects
class CommentsEditor(BulletListEditor)
Editor for comment sections using bullet-list extraction.
ganban.ui.edit.add
Shared behavior for ‘+’ add-widgets.
AddValueMixin Objects
class AddValueMixin()
A ‘+’ EditableText that emits entered values and resets.
Mix into a widget class and implement value_entered(). Set editable_classes to style the inner EditableText. Focus bindings only take effect on widgets declared can_focus=True; set refocus_after_submit to take focus back after a submission (leave off when submission moves focus elsewhere, e.g. into a new section).
value_entered
def value_entered(value: str) -> None
Handle a newly entered value.
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
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.menu
Context menu system for ganban UI.
truncate
def truncate(text: str, width: int = 20) -> str
Shorten text to at most width characters, ending in an ellipsis.
Card, column and board titles are unbounded; anything embedding them in a menu label should cap them so the menu keeps a sane width.
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.
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.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(NodeWatcherMixin, 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.palette
Color palette and label color hashing.
color_for_label
def color_for_label(label: str) -> str
Deterministic hex color from label name.
Uses the sum of all md5 bytes mod palette size, which spreads common label names across the palette with minimal collisions.
get_label_color
def get_label_color(name: str, board: Node) -> str
Get label color: explicit override from meta or computed hash.
Checks board.meta.labels[name].color for an override, otherwise returns a deterministic color based on the label name hash.
ganban.ui.labels
Label editor widget for card detail bar.
ICON_LABEL
🔖
build_label_options
def build_label_options(board: Node,
current_labels: list[str]) -> list[tuple[str, str]]
Build (label, value) options for the label search dropdown.
Shows all known labels from board.labels, excluding those already on the card.
LabelsWidget Objects
class LabelsWidget(TagListWidget)
Inline label editor for card detail bar.
Displays label tags next to a bookmark icon. Click the icon to add a label, click a tag to edit it, click × to delete. Uses Tag widgets with SearchInput for label selection with free-text fallback for new labels.
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.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.static
Static widget variants.
PlainStatic Objects
class PlainStatic(Static)
Static that doesn’t allow text selection.
CloseButton Objects
class CloseButton(Static)
Close button that triggers the screen’s close action.
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.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
Textual UI for ganban.
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.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 assignee options; the value is the committer string.
build_mention_options
def build_mention_options(board: Node) -> list[tuple[str, str]]
Build @mention options; the value is [Name](mailto:email).
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.
Uses a single optional Tag widget for the assigned user.
ganban.__main__
Entry point for ganban CLI.
ganban.constants
Global constants for ganban.
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 of a sections ListNode: its first non-empty key.
An empty first key holds preamble text that sits above the H1.
first_body
def first_body(sections) -> str
Get the body (first value) of a sections ListNode, or empty string.
ganban.ids
Card ID comparison and generation.
normalize_id
def normalize_id(s: str) -> str
Strip leading zeros from an ID, preserving at least one digit.
“001” → “1”, “0” → “0”, “010” → “10”
pad_id
def pad_id(s: str, width: int) -> str
Zero-pad an ID to the given width.
“1” with width=3 → “001”, “10” with width=3 → “010”
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 “1”
- If numeric (e.g., “9”), returns str(int + 1) (e.g., “10”)
- 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_git_config
def read_git_config(repo_path: str | Path) -> dict[str, dict[str, Any]]
Read git config into {section: {key: value}} dict.
Skips subsectioned entries (e.g. remote “origin”). Converts key hyphens to underscores. Applies type coercion for the ganban section. Merges ganban defaults for missing keys.
write_git_config_key
def write_git_config_key(repo_path: str | Path, section: str, key: str,
value) -> None
Write one key to git config. key is python-style (underscores).
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.
resolve_upstream
def resolve_upstream(repo_path: str | Path, remotes: list[str]) -> str | None
Pick the upstream remote: configured tracking, else origin, else first.
merge_order
def merge_order(remotes: list[str], upstream: str | None) -> list[str]
Order remotes for merging: non-upstream first, upstream last.
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_sync
def has_branch_sync(repo_path: str | Path, branch: str = "ganban") -> bool
Check if a branch exists in the repository (sync).
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.model.node
Reactive tree nodes with change notification and bubbling.
_Watchable Objects
class _Watchable()
Shared watcher registry and path computation for Node and ListNode.
watch
def watch(key: str, callback: Callback) -> Callable[[], None]
Watch a key for changes. Returns an unwatch callable.
path
@property
def path() -> str
Dotted path from root to this node.
Node Objects
class Node(_Watchable)
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.
keys
def keys()
Return children keys.
items
def items()
Return children items.
values
def values()
Return children values.
update
def update(other: Node) -> None
Update this node in-place to match other, preserving watchers.
clone
def clone() -> "Node"
Deep-copy this subtree without watchers or the parent link.
Used to snapshot the board on the event-loop thread so background saves never iterate structures the UI is still mutating.
rename_key
def rename_key(old_key: str, new_key: str) -> None
Rename a key in _children, preserving insertion order.
ListNode Objects
class ListNode(_Watchable)
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. Order is the insertion order of _by_id; replacing a value keeps its position.
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.
clone
def clone() -> "ListNode"
Deep-copy this list without watchers or the parent link.
add
def add(key: str, value: Any) -> str
Add a new item, deduplicating the key if it already exists.
Returns the actual key used.
rename_first_key
def rename_first_key(new_title: str) -> None
Rename the first key by rebuilding the list.
ganban.model.board
Board-level helpers.
create_default_board
def create_default_board(repo_path: str | Path) -> Node
Build the default board (Backlog/Doing/Done) for a new repository.
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.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.
rename_label
def rename_label(board: Node, old_name: str, new_name: str) -> None
Rename a label across all cards and board meta.
delete_label
def delete_label(board: Node, name: str) -> None
Delete a label from all cards and board meta.
archive_card
def archive_card(board: Node, card_id: str) -> None
Archive a card by removing it from its column’s links.
ganban.model.loader
Load a ganban board from git into a Node tree.
build_sections_list
def build_sections_list(text: str,
fallback_title: str = "Untitled"
) -> tuple[ListNode, dict]
Parse markdown text into a ListNode of sections plus meta dict.
fallback_title is used only when no section has a title at all. An untitled first section alongside titled ones is preamble text above the H1 and must keep its empty key to round-trip cleanly.
normalise_label
def normalise_label(raw: str) -> str
Normalise a label name to lowercase, stripped.
load_board
def load_board(repo_path: str,
branch: str = BRANCH_NAME,
committers: bool = True) -> Node
Load a complete board from a git branch as a Node tree.
committers=False skips the git-history scan that feeds assignee suggestions — one-shot CLI commands never use it.
ganban.model.writer
Save a ganban board (Node tree) to git without touching the working tree.
meta_to_dict
def meta_to_dict(meta) -> dict
Convert a meta value (Node or dict) back to a plain dict, recursively.
sections_to_text
def sections_to_text(sections: ListNode, meta) -> str
Serialize a sections ListNode + meta back to markdown text.
MergeRequired Objects
@dataclass
class MergeRequired()
Returned by check_for_merge when the branch has diverged.
IdCollision Objects
@dataclass
class IdCollision()
An .all/ card file added independently on both sides of a merge.
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.
save_and_merge
def save_and_merge(board: Node,
message: str = "Update board",
branch: str = BRANCH_NAME,
max_attempts: int = 5) -> tuple[str, bool]
Save the board, merging any concurrent movement of the branch tip.
Returns (commit, merged). merged is True when a concurrent commit was merged in — long-lived callers should reload the board from git, since the in-memory tree does not contain the merged-in changes.
Raises RuntimeError if the branch diverged and auto-merge kept failing; the local changes are preserved in the returned commit’s history.
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, resolving conflicts with most-recent-commit-wins.
Returns the new merge commit hash. Conflicts are resolved by replacing conflicted files with the version from whichever commit is newer. Returns None if merge-tree fails entirely, or if the branch moved concurrently (merge_info is stale — re-check and retry).
ganban.model
Reactive model tree.
ganban.cli.web
Handlers for ‘ganban web’ command.
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.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.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.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.
output_result
def output_result(data: dict, text: str, json_mode: bool) -> None
Output mutation result as JSON or plain text.
error
def error(message: str, json_mode: bool) -> None
Print error to stderr and exit 1.
build_column_summaries
def build_column_summaries(board: Node) -> list[dict]
Build column summary dicts from board.
format_column_line
def format_column_line(c: dict, indent: str = "") -> str
Format a column summary dict as a text line.
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.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.