tmuxp progress - tmuxp.cli._progress

Progress indicators for tmuxp CLI.

This module provides a threaded spinner for long-running operations, using only standard library and ANSI escape sequences.

tmuxp.cli._progress._visible_len(s)[source]

Return visible length of s, ignoring ANSI escapes.

Return type:

int

Examples

>>> _visible_len("hello")
5
>>> _visible_len("\033[32mgreen\033[0m")
5
>>> _visible_len("")
0
tmuxp.cli._progress._truncate_visible(text, max_visible, suffix='...')[source]

Truncate text to max_visible visible characters, preserving ANSI sequences.

If the visible length of text is already within max_visible, it is returned unchanged. Otherwise the text is cut so that exactly max_visible visible characters remain, a \x1b[0m reset is appended (to prevent color bleed), followed by suffix.

Return type:

str

Parameters:
  • text (str) – Input string, possibly containing ANSI escape sequences.

  • max_visible (int) – Maximum number of visible (non-ANSI) characters to keep.

  • suffix (str) – Appended after the reset when truncation occurs. Default "...".

Returns:

Truncated string with ANSI sequences intact.

Return type:

str

Examples

Plain text truncation:

>>> _truncate_visible("hello world", 5)
'hello\x1b[0m...'

ANSI sequences are preserved whole:

>>> _truncate_visible("\033[32mgreen\033[0m", 3)
'\x1b[32mgre\x1b[0m...'

No truncation needed:

>>> _truncate_visible("short", 10)
'short'

Empty string:

>>> _truncate_visible("", 5)
''
tmuxp.cli._progress.render_bar(done, total, width=10)[source]

Render a plain-text ASCII progress bar without color.

Return type:

str

Parameters:
  • done (int) – Completed units.

  • total (int) – Total units. When <= 0, returns "".

  • width (int) – Inner fill character count; default BAR_WIDTH.

Returns:

A bar like "█████░░░░░". Returns "" when total <= 0 or width <= 0.

Return type:

str

Examples

>>> render_bar(0, 10)
'░░░░░░░░░░'
>>> render_bar(5, 10)
'█████░░░░░'
>>> render_bar(10, 10)
'██████████'
>>> render_bar(0, 0)
''
>>> render_bar(3, 10, width=5)
'█░░░░'
class tmuxp.cli._progress._SafeFormatMap[source]

Bases: dict

dict subclass that returns {key} for missing keys in format_map.

tmuxp.cli._progress.resolve_progress_format(fmt)[source]

Return the format string for fmt, resolving preset names.

If fmt is a key in PROGRESS_PRESETS the corresponding format string is returned; otherwise fmt is returned as-is.

Return type:

str

Examples

>>> resolve_progress_format("minimal") == PROGRESS_PRESETS["minimal"]
True
>>> resolve_progress_format("{session} w{window_progress}")
'{session} w{window_progress}'
>>> resolve_progress_format("unknown-preset")
'unknown-preset'
class tmuxp.cli._progress._WindowStatus(name, done=False, pane_num=None, pane_total=None, pane_done=0)[source]

Bases: object

State for a single window in the build tree.

name: str
done: bool = False
pane_num: int | None = None
pane_total: int | None = None
pane_done: int = 0
class tmuxp.cli._progress.BuildTree(workspace_path='')[source]

Bases: object

Tracks session/window/pane build state; renders a structural progress tree.

Template Token Lifecycle

Each token is first available at the event listed in its column. means the value does not change at that phase.

Token

Pre-session_created

After session_created

After window_started

After pane_creating

After window_done

{session}

""

session name

{window}

""

""

window name

last window name

{window_index}

0

0

N (1-based started count)

{window_total}

0

total

{window_progress}

""

""

"N/M" when > 0

{windows_done}

0

0

0

0

increments

{windows_remaining}

0

total

total

total

decrements

{window_progress_rel}

""

"0/M"

"0/M"

"N/M"

{pane_index}

0

0

0

pane_num

0

{pane_total}

0

0

window’s pane total

window’s pane total

{pane_progress}

""

""

""

"N/M"

""

{pane_done}

0

0

0

pane_num

pane_total

{pane_remaining}

0

0

pane_total

decrements

0

{pane_progress_rel}

""

""

"0/M"

"N/M"

"M/M"

{progress}

""

""

"N/M win"

"N/M win · P/Q pane"

{session_pane_total}

0

total

{session_panes_done}

0

0

0

0

accumulated

{session_panes_remaining}

0

total

total

total

decrements

{session_pane_progress}

""

"0/T"

"N/T"

{overall_percent}

0

0

0

0

updates

{summary}

""

""

""

""

"[N win, M panes]"

{bar} (spinner)

[░░…]

[░░…]

starts filling

fractional

jumps

{pane_bar} (spinner)

""

[░░…]

updates

{window_bar} (spinner)

""

[░░…]

updates

{status_icon} (spinner)

""

""

""

""

""

During before_script: {bar}, {pane_bar}, {window_bar} show a marching animation; {status_icon} = .

Examples

Empty tree renders nothing:

>>> from tmuxp.cli._colors import ColorMode, Colors
>>> colors = Colors(ColorMode.NEVER)
>>> tree = BuildTree()
>>> tree.render(colors, 80)
[]

After session_created event the header appears:

>>> tree.on_event({"event": "session_created", "name": "my-session"})
>>> tree.render(colors, 80)
['Session']

After window_started and pane_creating:

>>> tree.on_event({"event": "window_started", "name": "editor", "pane_total": 2})
>>> tree.on_event({"event": "pane_creating", "pane_num": 1, "pane_total": 2})
>>> lines = tree.render(colors, 80)
>>> lines[1]
'- editor, pane (1 of 2)'

After window_done the window gets a checkmark:

>>> tree.on_event({"event": "window_done"})
>>> lines = tree.render(colors, 80)
>>> lines[1]
'- ✓ editor'

Inline status format:

>>> tree2 = BuildTree()
>>> tree2.format_inline("Building projects...")
'Building projects...'
>>> tree2.on_event({"event": "session_created", "name": "cihai", "window_total": 3})
>>> tree2.format_inline("Building projects...")
'Building projects... cihai'
>>> tree2.on_event({"event": "window_started", "name": "gp-libs", "pane_total": 2})
>>> tree2.on_event({"event": "pane_creating", "pane_num": 1, "pane_total": 2})
>>> tree2.format_inline("Building projects...")
'Building projects... cihai [1 of 3 windows, 1 of 2 panes] gp-libs'
on_event(event)[source]

Update tree state from a build event dict.

Return type:

None

Examples

>>> tree = BuildTree()
>>> tree.on_event({
...     "event": "session_created", "name": "dev", "window_total": 2,
... })
>>> tree.session_name
'dev'
>>> tree.window_total
2
>>> tree.on_event({
...     "event": "window_started", "name": "editor", "pane_total": 3,
... })
>>> len(tree.windows)
1
>>> tree.windows[0].name
'editor'
render(colors, width)[source]

Render the current tree state to a list of display strings.

Return type:

list[str]

Parameters:
  • colors (Colors) – Colors instance for ANSI styling.

  • width (int) – Terminal width; window lines are truncated to width - 1.

Returns:

Lines to display; empty list if no session has been created yet.

Return type:

list[str]

_context()[source]

Return the current build-state token dict for template rendering.

Return type:

dict[str, Any]

Examples

Zero-state before any events:

>>> tree = BuildTree(workspace_path="~/.tmuxp/myapp.yaml")
>>> ev = {
...     "event": "session_created",
...     "name": "myapp",
...     "window_total": 5,
...     "session_pane_total": 10,
... }
>>> tree.on_event(ev)
>>> ctx = tree._context()
>>> ctx["workspace_path"]
'~/.tmuxp/myapp.yaml'
>>> ctx["session"]
'myapp'
>>> ctx["window_total"]
5
>>> ctx["window_index"]
0
>>> ctx["progress"]
''
>>> ctx["windows_done"]
0
>>> ctx["windows_remaining"]
5
>>> ctx["window_progress_rel"]
'0/5'
>>> ctx["session_pane_total"]
10
>>> ctx["session_panes_remaining"]
10
>>> ctx["session_pane_progress"]
'0/10'
>>> ctx["summary"]
''

After windows complete, summary shows counts:

>>> tree.on_event({"event": "window_started", "name": "w1", "pane_total": 3})
>>> tree.on_event({"event": "window_done"})
>>> tree.on_event({"event": "window_started", "name": "w2", "pane_total": 5})
>>> tree.on_event({"event": "window_done"})
>>> tree._context()["summary"]
'[2 win, 8 panes]'
format_template(fmt, extra=None)[source]

Render fmt with the current build state.

Returns "" before session_created fires so callers can fall back to a pre-build message. Unknown {tokens} are left as-is (not dropped silently).

The optional extra dict is merged on top of _context() so callers (e.g. Spinner) can inject ANSI-colored tokens like {bar} without adding color concerns to BuildTree.

Return type:

str

Examples

>>> tree = BuildTree()
>>> tree.format_template("{session} [{progress}] {window}")
''
>>> ev = {"event": "session_created", "name": "cihai", "window_total": 3}
>>> tree.on_event(ev)
>>> tree.format_template("{session} [{progress}] {window}")
'cihai [] '
>>> ev = {"event": "window_started", "name": "editor", "pane_total": 4}
>>> tree.on_event(ev)
>>> tree.format_template("{session} [{progress}] {window}")
'cihai [1/3 win] editor'
>>> tree.on_event({"event": "pane_creating", "pane_num": 2, "pane_total": 4})
>>> tree.format_template("{session} [{progress}] {window}")
'cihai [1/3 win · 2/4 pane] editor'
>>> tree.format_template("minimal: {session} [{window_progress}]")
'minimal: cihai [1/3]'
>>> tree.format_template("{session} {unknown_token}")
'cihai {unknown_token}'
>>> tree.format_template("{session}", extra={"custom": "value"})
'cihai'
format_inline(base)[source]

Return base message with current build state appended inline.

Return type:

str

Parameters:

base (str) – The original spinner message to start from.

Returns:

base alone if no session has been created yet; otherwise "base session_name [W of N windows, P of M panes] window_name", omitting the bracket section when there is no current window, and omitting individual parts when their totals are not known.

Return type:

str

class tmuxp.cli._progress.Spinner(message='Loading...', color_mode=ColorMode.AUTO, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, interval=0.1, output_lines=3, progress_format=None, workspace_path='')[source]

Bases: object

A threaded spinner for CLI progress.

Examples

>>> import io
>>> stream = io.StringIO()
>>> with Spinner("Build...", color_mode=ColorMode.NEVER, stream=stream) as spinner:
...     spinner.add_output_line("Session created: test")
...     spinner.update_message("Creating window: editor")
_should_enable()[source]

Check if spinner should be enabled (TTY check).

Return type:

bool

_restore_cursor()[source]

Unconditionally restore cursor — called by atexit on abnormal exit.

Return type:

None

_spin()[source]

Spin in background thread.

Return type:

None

add_output_line(line)[source]

Append a line to the live output panel (thread-safe via GIL).

When the spinner is disabled (non-TTY), writes directly to the stream so output is not silently swallowed.

Return type:

None

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("test", color_mode=ColorMode.NEVER, stream=stream)
>>> spinner.add_output_line("hello world")
>>> stream.getvalue()
'hello world\n'
update_message(message)[source]

Update the message displayed next to the spinner.

Return type:

None

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("initial", color_mode=ColorMode.NEVER, stream=stream)
>>> spinner.message
'initial'
>>> spinner.update_message("updated")
>>> spinner.message
'updated'
_build_extra()[source]

Return spinner-owned template tokens (colored bar, status_icon).

These are separated from BuildTree._context() to keep ANSI/color concerns out of BuildTree, which is also used in tests without colors.

Return type:

dict[str, Any]

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("x", color_mode=ColorMode.NEVER, stream=stream)
>>> spinner._build_tree.on_event(
...     {
...         "event": "session_created",
...         "name": "s",
...         "window_total": 4,
...         "session_pane_total": 8,
...     }
... )
>>> extra = spinner._build_extra()
>>> extra["bar"]
'░░░░░░░░░░'
>>> extra["status_icon"]
''
on_build_event(event)[source]

Forward build event to BuildTree and update spinner message inline.

Return type:

None

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("Loading", color_mode=ColorMode.NEVER, stream=stream)
>>> spinner.on_build_event({
...     "event": "session_created", "name": "myapp",
...     "window_total": 2, "session_pane_total": 3,
... })
>>> spinner._build_tree.session_name
'myapp'
start()[source]

Start the spinner thread.

Return type:

None

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("test", color_mode=ColorMode.NEVER, stream=stream)
>>> spinner.start()
>>> spinner.stop()
stop()[source]

Stop the spinner thread.

Return type:

None

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("test", color_mode=ColorMode.NEVER, stream=stream)
>>> spinner.start()
>>> spinner.stop()
>>> spinner._thread is None
True
format_success()[source]

Render the success template with current build state.

Uses SUCCESS_TEMPLATE with colored {session} (highlight()), {workspace_path} (info()), and {summary} (muted()) from BuildTree._context().

Return type:

str

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("x", color_mode=ColorMode.NEVER, stream=stream,
...     workspace_path="~/.tmuxp/myapp.yaml")
>>> spinner._build_tree.on_event({
...     "event": "session_created", "name": "myapp",
...     "window_total": 2, "session_pane_total": 4,
... })
>>> spinner._build_tree.on_event(
...     {"event": "window_started", "name": "w1", "pane_total": 2})
>>> spinner._build_tree.on_event({"event": "window_done"})
>>> spinner._build_tree.on_event(
...     {"event": "window_started", "name": "w2", "pane_total": 2})
>>> spinner._build_tree.on_event({"event": "window_done"})
>>> spinner.format_success()
'Loaded workspace: myapp (~/.tmuxp/myapp.yaml) [2 win, 4 panes]'
success(text=None)[source]

Stop the spinner and print a success line.

Return type:

None

Parameters:

text (str | None) – The success message to display after the checkmark. When None, uses format_success() if a progress format is configured, otherwise falls back to _base_message.

Examples

>>> import io
>>> stream = io.StringIO()
>>> spinner = Spinner("x", color_mode=ColorMode.NEVER, stream=stream)
>>> spinner.success("done")
>>> "✓ done" in stream.getvalue()
True

With no args and no progress format, falls back to base message:

>>> stream2 = io.StringIO()
>>> spinner2 = Spinner("Loading...", color_mode=ColorMode.NEVER, stream=stream2)
>>> spinner2.success()
>>> "✓ Loading..." in stream2.getvalue()
True