On this page
Lays every Claude Code session on the machine out as one framed table of styled spans, with both scrollbars drawn into its borders and no terminal in sight.
#claudewheel.sessions_table
#claudewheel.sessions_table
Lay every Claude Code session on this machine out as one framed table.
This module is the looking, and nothing else: rows, dimensions, a clock, a home directory and two view flags in, a :class:Frame of styled :class:Span\ s out. It opens no file, reads no environment and knows no terminal, so the whole layout -- the column widths, both truncation directions, both scrollbars, the expanded row's detail lines -- is exercised as arithmetic over strings.
What the frame is made of -------------------------
The bottom terminal row belongs to the hint line, which is the screen's business, not this module's; the frame occupies everything above it:
=============== ========================================================== Top border ╭, the column joints, ╮ Header the column labels Header rule ├, the column joints, ┤ Data window one line per row, four for the expanded one Bottom border ╰, the column joints, ╯, and the horizontal handle =============== ==========================================================
Two scrollbars, each drawn INTO a border rather than beside it: the vertical one over the right border of the data-window lines, the horizontal one over the interior of the bottom border. Neither costs a column or a line, which is why the table can afford eight columns on an 80-column terminal.
The column strip and the two things that are not in it ------------------------------------------------------
Every cell line -- the header, the joint lines, each row -- is one strip as wide as the columns ask for (:attr:Frame.strip_width), shifted left by hscroll and clipped to the frame's interior. The expanded row's detail lines are deliberately NOT part of that strip: they are full-width prose about one session, and scrolling them sideways with the columns would hide the beginning of a path rather than reveal the end of a column.
Styles, not colours -------------------
A span carries a style NAME (frame, header, row, row_focus, state:<state>, state_focus:<state>, detail, empty) and :mod:claudewheel.sessions_overview is the only place one becomes an escape sequence. The focused row's every span -- including the padding out to the frame's edge -- carries a focus style, so the highlight is a full-width band rather than a coloured word.
#state_style
def state_style(state: str, *, focused: bool) -> strThe style name of a State cell in state.
#SessionRow
One session, as the table needs it: already joined, already resolved.
Everything here is a value the gatherer decided -- the state it derived, the profile it attributed a lifecycle-only session to, the memory it measured -- so the layout never has to ask anything about the world. record and lifecycle are carried for the screen's own keys (pruning needs the record, marking needs the session id), not for the layout.
#Span
A run of characters drawn in one style.
#Frame
A laid-out table, and the dimensions the key loop needs back.
hscroll is the CLAMPED horizontal offset, so a screen that asked for more than the strip can offer reads the real one back instead of accumulating a number that stopped meaning anything. window is the data window in lines (what a page key moves by) and total_lines the summed height of every visible row.
#_Spec
One column: its label, how wide it gets, and how it handles overflow.
fixed is a width the content cannot change. Otherwise the column is natural: as wide as its longest cell (never narrower than its own label), floored at minimum and capped at maximum -- and a maximum of None is no cap at all, for a column whose cells must never be cut.
#sort_rows
def sort_rows(rows: Sequence[SessionRow]) -> list[SessionRow]Group rows by state, newest first inside each group.
A row with no start time sorts after every dated row of its group rather than among them, and rows that cannot be told apart by age sort by name, so the order is total and a redraw never reshuffles equals.
#visible_rows
def visible_rows(rows: Sequence[SessionRow], show_all: bool) -> list[SessionRow]The rows shown at this filter setting, in the order given.
The default view hides what is finished -- an exited session and one the user marked done -- because those accumulate without end and are not what the screen is opened to look at.
#tildify
def tildify(path: str | None, home: str) -> strpath with home written as ~, or :data:MISSING when absent.
home is a parameter, never read from the environment: this module has no business knowing whose machine it is drawing.
#_truncate
def _truncate(text: str, width: int, *, from_left: bool) -> strtext cut to width, with an ellipsis where the cut was made.
#cells
def cells(row: SessionRow, *, now_ms: int, home: str) -> tuple[str, ...]The eight untruncated cell texts of row, in column order.
#_widths
def _widths(table: Sequence[Sequence[str]]) -> tuple[int, ...]The drawn width of each column, given every row's cell texts.
#_pad
def _pad(text: str, width: int, spec: _Spec) -> strtext fitted to width: truncated if long, aligned if short.
#_strip_cells
def _strip_cells(texts: Sequence[str], widths: Sequence[int]) -> list[str]Each cell as it is drawn, one space of padding on either side.
#_joint_line
def _joint_line(widths: Sequence[int], joint: str) -> strA horizontal rule with a joint wherever a column separator falls.
#_clip
def _clip(pieces: Sequence[Span], start: int, width: int, *, pad: str, pad_style: str) -> LineThe width characters of pieces beginning at start, padded if short.
The one place the horizontal viewport is applied, over a sequence of spans rather than a string, so a row's state cell keeps its own style through the shift and the clip.
#_cut
def _cut(line: Line, width: int) -> Lineline with everything past width dropped, for a terminal too narrow.
#_handle
def _handle(visible: int, total: int, offset: int) -> tuple[int, int]Where a scrollbar handle starts and how long it is, in track units.
The handle is the visible fraction of the content, never shorter than one unit (a handle that rounded to nothing would leave a scrollbar with no handle at all), and never past the end of the track.
#_detail_lines
def _detail_lines(row: SessionRow, home: str) -> tuple[str, ...]The three lines under an expanded row.
#_row_line
def _row_line(texts: Sequence[str], widths: Sequence[int], *, focused: bool) -> tuple[Span, ...]One row as three strip pieces: before the State cell, it, and after.
The column separators carry the row's own style rather than the frame's: inside a focused row they are part of the highlighted band, and splitting them out would draw a gap through it.
#labels
def labels() -> tuple[str, ...]The header labels, in column order.
#_clipped_text
def _clipped_text(text: str, start: int, width: int, pad: str) -> strtext shifted left by start and fitted to width with pad.
#layout
def layout(rows: Sequence[SessionRow], *, focus: int, expanded: int | None, height: int, width: int, hscroll: int, now_ms: int, home: str, show_all: bool) -> FrameLay rows out into a frame of height by width characters.
focus and expanded index the VISIBLE rows -- the ones :func:visible_rows keeps at this show_all setting -- which is the same list the screen's keys act on. The vertical window is placed by :func:claudewheel.vertical_viewport.compute_viewport, so a row at an edge is drawn clipped rather than dropped; the horizontal offset is clamped here and reported back on the frame.
Every line comes out exactly width characters long, and a terminal too short even for the frame's own chrome yields the prefix that fits rather than raising.