Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/claude-code-plugin/hooks/lib/tiny_actor_grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Responsive Tiny Actor Grid layout engine with column fallback (#1268).

Accepts pre-rendered card lines and assembles them into a responsive grid
that adapts to the available terminal width.
"""
from __future__ import annotations

from itertools import zip_longest


def layout_grid(
cards: list[list[str]],
*,
available_width: int = 80,
card_width: int = 14,
gap: int = 2,
) -> list[str]:
"""Arrange pre-rendered cards into a responsive grid.

Parameters
----------
cards:
Each card is a list of strings (one per visual line).
available_width:
Maximum terminal columns for the output.
card_width:
Display width of a single card.
gap:
Number of space characters between adjacent columns.

Returns
-------
list[str]
Assembled lines ready for terminal output.
"""
if not cards:
return []

max_columns = max(1, (available_width + gap) // (card_width + gap))

rows: list[list[list[str]]] = []
for i in range(0, len(cards), max_columns):
rows.append(cards[i : i + max_columns])

separator = " " * gap
row_width = max_columns * card_width + (max_columns - 1) * gap

output: list[str] = []
for row in rows:
tallest = max(len(card) for card in row)

padded_cards: list[list[str]] = []
for card in row:
padded = list(card) + [" " * card_width] * (tallest - len(card))
padded_cards.append(padded)

for line_parts in zip_longest(*padded_cards, fillvalue=" " * card_width):
parts = [part.ljust(card_width) for part in line_parts]
# Pad row to full width when fewer cards than max_columns
while len(parts) < max_columns:
parts.append(" " * card_width)
line = separator.join(parts)
output.append(line)

return output
109 changes: 109 additions & 0 deletions packages/claude-code-plugin/tests/test_tiny_actor_grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Tests for responsive Tiny Actor Grid layout engine (#1268)."""
import os
import sys

import pytest

# Ensure hooks/lib is on path
_tests_dir = os.path.dirname(os.path.abspath(__file__))
_lib_dir = os.path.join(os.path.dirname(_tests_dir), "hooks", "lib")
if _lib_dir not in sys.path:
sys.path.insert(0, _lib_dir)

from tiny_actor_grid import layout_grid

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_CARD_WIDTH = 14


def _make_card(lines: int = 3, width: int = _CARD_WIDTH, label: str = "X") -> list[str]:
"""Create a fake pre-rendered card with *lines* rows, each *width* chars."""
rows: list[str] = []
for i in range(lines):
text = f"{label}{i}"
rows.append(text.ljust(width))
return rows


# ---------------------------------------------------------------------------
# Column calculation
# ---------------------------------------------------------------------------


class TestColumnCalculation:
"""layout_grid picks the right number of columns for the available width."""

def test_three_cards_fit_one_row_at_80_cols(self):
"""3 cards (14 wide, gap 2) need 14+2+14+2+14 = 46 chars => fits in 80."""
cards = [_make_card(label=c) for c in "ABC"]
lines = layout_grid(cards, available_width=80, card_width=_CARD_WIDTH, gap=2)
# All 3 cards in one row => only 3 visual lines (card height)
assert len(lines) == 3

def test_six_cards_wrap_to_two_rows_at_80_cols(self):
"""80 cols fits max 5 cards (14+2)*5-2=78. 6 cards => 2 rows."""
cards = [_make_card(label=c) for c in "ABCDEF"]
lines = layout_grid(cards, available_width=80, card_width=_CARD_WIDTH, gap=2)
# 5 cards first row + 1 card second row => 3 + 3 = 6 visual lines
assert len(lines) == 6

def test_single_card_at_40_cols(self):
"""1 card at 40 cols => single column, 3 lines."""
cards = [_make_card()]
lines = layout_grid(cards, available_width=40, card_width=_CARD_WIDTH, gap=2)
assert len(lines) == 3

def test_narrow_width_forces_column_reduction(self):
"""Width of 20 with card_width 14 and gap 2 => max 1 column."""
cards = [_make_card(label=c) for c in "AB"]
lines = layout_grid(cards, available_width=20, card_width=_CARD_WIDTH, gap=2)
# 2 cards in 1 column => 2 rows => 6 visual lines
assert len(lines) == 6


# ---------------------------------------------------------------------------
# Output consistency
# ---------------------------------------------------------------------------


class TestOutputConsistency:
"""All output lines have consistent display width within each row group."""

def test_all_lines_consistent_width(self):
"""Every line in the output should have the same display width."""
cards = [_make_card(label=c) for c in "ABCD"]
lines = layout_grid(cards, available_width=80, card_width=_CARD_WIDTH, gap=2)
widths = {len(line) for line in lines}
assert len(widths) == 1, f"Expected uniform width, got {widths}"


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
"""Edge cases: empty input, uneven card heights."""

def test_empty_input_returns_empty_list(self):
assert layout_grid([], available_width=80, card_width=_CARD_WIDTH) == []

def test_cards_with_different_line_counts_are_padded(self):
"""Cards with 2 and 4 lines in same row => shorter card padded to 4."""
short_card = _make_card(lines=2, label="S")
tall_card = _make_card(lines=4, label="T")
lines = layout_grid(
[short_card, tall_card],
available_width=80,
card_width=_CARD_WIDTH,
gap=2,
)
# Both in same row, padded to tallest (4 lines)
assert len(lines) == 4
# Every line should have content (no None, no missing)
for line in lines:
assert isinstance(line, str)
assert len(line) > 0
Loading