Skip to content

jmwallet.utxo_tui

jmwallet.utxo_tui

Shared helpers for the curses-based UTXO TUIs.

Both the freeze manager (jm-wallet freeze) and the interactive UTXO selector (--select-utxos) render the whole wallet grouped by mixdepth. This module holds the layout and navigation primitives they share:

  • building a display list with None separators between mixdepth groups,
  • cursor navigation that skips separators (and other non-selectable rows),
  • the address column formatting (collapsing consecutive duplicates),
  • scroll-offset adjustment to keep the cursor visible.

Attributes

ADDRESS_COL_WIDTH = 42 module-attribute

Classes

Functions:

adjust_scroll(cursor_pos: int, scroll_offset: int, list_height: int) -> int

Return a scroll offset that keeps cursor_pos visible.

Source code in jmwallet/src/jmwallet/utxo_tui.py
92
93
94
95
96
97
98
def adjust_scroll(cursor_pos: int, scroll_offset: int, list_height: int) -> int:
    """Return a scroll offset that keeps ``cursor_pos`` visible."""
    if cursor_pos < scroll_offset:
        return cursor_pos
    if cursor_pos >= scroll_offset + list_height:
        return cursor_pos - list_height + 1
    return scroll_offset

build_display_items(utxos: list[UTXOInfo]) -> list[UTXOInfo | None]

Insert None separators between mixdepth groups.

utxos must already be sorted so that all UTXOs of a mixdepth are contiguous. A None entry is inserted between consecutive mixdepth groups; it renders as a separator line and is skipped during navigation.

Source code in jmwallet/src/jmwallet/utxo_tui.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def build_display_items(utxos: list[UTXOInfo]) -> list[UTXOInfo | None]:
    """Insert ``None`` separators between mixdepth groups.

    ``utxos`` must already be sorted so that all UTXOs of a mixdepth are
    contiguous. A ``None`` entry is inserted between consecutive mixdepth
    groups; it renders as a separator line and is skipped during navigation.
    """
    display_items: list[UTXOInfo | None] = []
    current_md = -1
    for utxo in utxos:
        if utxo.mixdepth != current_md:
            current_md = utxo.mixdepth
            if display_items:
                display_items.append(None)
        display_items.append(utxo)
    return display_items

format_address_column(address: str, prev_address: str) -> str

Format an address for the TUI address column.

Consecutive UTXOs sharing the same address render the address only once; subsequent rows show blanks so the column stays visually grouped. Long addresses (e.g. fidelity bond P2WSH) are truncated in the middle.

Source code in jmwallet/src/jmwallet/utxo_tui.py
78
79
80
81
82
83
84
85
86
87
88
89
def format_address_column(address: str, prev_address: str) -> str:
    """Format an address for the TUI address column.

    Consecutive UTXOs sharing the same address render the address only once;
    subsequent rows show blanks so the column stays visually grouped. Long
    addresses (e.g. fidelity bond P2WSH) are truncated in the middle.
    """
    if address == prev_address:
        return " " * ADDRESS_COL_WIDTH
    if len(address) > ADDRESS_COL_WIDTH:
        return address[:20] + "..." + address[-19:]
    return address

seek_selectable(display_items: list[UTXOInfo | None], start: int, direction: int, is_selectable: Callable[[UTXOInfo], bool]) -> int

Return the nearest index from start whose item satisfies is_selectable.

None separators are always skipped. Searches in direction first and falls back to the opposite direction; returns start when nothing matches at all.

Source code in jmwallet/src/jmwallet/utxo_tui.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def seek_selectable(
    display_items: list[UTXOInfo | None],
    start: int,
    direction: int,
    is_selectable: Callable[[UTXOInfo], bool],
) -> int:
    """Return the nearest index from ``start`` whose item satisfies ``is_selectable``.

    ``None`` separators are always skipped. Searches in ``direction`` first
    and falls back to the opposite direction; returns ``start`` when nothing
    matches at all.
    """
    pos = start

    # Search in the requested direction
    while 0 <= pos < len(display_items):
        item = display_items[pos]
        if item is not None and is_selectable(item):
            return pos
        pos += direction

    # If not found, search in the opposite direction from start
    opposite = -direction
    pos = start + opposite
    while 0 <= pos < len(display_items):
        item = display_items[pos]
        if item is not None and is_selectable(item):
            return pos
        pos += opposite

    return start