
A native macOS table view for displaying structured data in rows and columns. Table renders a collection of Python objects using declarative column definitions, with built-in support for column headers, sorting, selection, and custom cell views.
Backed by AppKit's NSTableView for native performance with large datasets.
Constructor¶
nib.Table(
rows,
columns,
selection=None,
on_select=None,
**modifiers,
)
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
rows |
Sequence |
() |
The data backing the table. Any sequence of Python objects (dicts, dataclasses, named tuples, etc.). |
columns |
list[TableColumn] |
None |
List of TableColumn definitions specifying how each column extracts, displays, and sorts data. |
selection |
Any \| list[Any] |
None |
Currently selected row object(s), or None. |
on_select |
Callable[[list[Any]], None] |
None |
Callback when selection changes. Receives a list of the selected row objects. |
**modifiers |
Common view modifiers: width, height, padding, background, etc. |
TableColumn¶
Defines how a single column behaves and renders.
nib.TableColumn(
title,
*,
key=None,
cell=None,
width=None,
alignment="leading",
sortable=True,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
title |
str |
(required) | Column header text. |
key |
Callable[[Any], Any] |
None |
Lambda that extracts a value from a row object. Used for default text rendering and sorting. |
cell |
Callable[[Any], View] |
None |
Custom cell renderer. Receives a row object, returns a View. If omitted, Text(str(key(row))) is used. |
width |
ColumnWidth |
None |
Column width specification. See ColumnWidth below. |
alignment |
str |
"leading" |
Text alignment within the column. Options: "leading", "center", "trailing". |
sortable |
bool |
True |
Whether the column header is clickable for sorting. Requires key to be set. |
ColumnWidth¶
Controls column sizing. Use the static constructors:
nib.ColumnWidth.fixed(100) # Exactly 100pt wide
nib.ColumnWidth.range(min=80, ideal=150, max=300) # Flexible with constraints
| Method | Description |
|---|---|
ColumnWidth.fixed(px) |
Fixed width in points. The column will not resize. |
ColumnWidth.range(min, ideal, max) |
Flexible width. The column resizes between min and max, preferring ideal. |
Mutable Properties¶
| Property | Type | Description |
|---|---|---|
rows |
Sequence |
Get or set the row data. Triggers cell rebuild and UI update. |
columns |
list[TableColumn] |
Get or set the column definitions. Triggers cell rebuild and UI update. |
selection |
Any \| list[Any] |
Get or set the selected row object(s). Triggers a UI update. |
How It Works¶
Each cell in the table is a nib View. For every row and column combination, the Table computes a cell view:
- If the column has a
celllambda, it callscell(row)to get a custom View. - Otherwise, if the column has a
keylambda, it rendersText(str(key(row))). - If neither is provided, it falls back to
Text(str(row)).
Sorting is handled automatically when you click a column header. The Table uses the column's key lambda to sort rows in Python, then rebuilds the cell views.
Examples¶
File browser table¶
import nib
def main(app: nib.App):
app.title = "Files"
app.icon = nib.SFSymbol("folder")
app.width = 450
app.height = 250
files = [
{"name": "Document.txt", "size": "4 KB", "modified": "Today"},
{"name": "Image.png", "size": "1.2 MB", "modified": "Yesterday"},
{"name": "Archive.zip", "size": "45 MB", "modified": "Last week"},
]
app.build(
nib.Table(
rows=files,
columns=[
nib.TableColumn("Name", key=lambda f: f["name"],
width=nib.ColumnWidth.range(min=50, ideal=150, max=300)),
nib.TableColumn("Size", key=lambda f: f["size"],
alignment="trailing",
width=nib.ColumnWidth.fixed(80)),
nib.TableColumn("Modified", key=lambda f: f["modified"],
width=nib.ColumnWidth.fixed(100)),
],
on_select=lambda rows: print(f"Selected: {[r['name'] for r in rows]}"),
)
)
nib.run(main)
Custom cell views¶
Use the cell parameter to render any nib View in a cell:
import nib
def main(app: nib.App):
app.title = "Status"
app.icon = nib.SFSymbol("list.bullet")
app.width = 400
app.height = 250
tasks = [
{"name": "Design", "status": "done", "priority": 1},
{"name": "Develop", "status": "in_progress", "priority": 2},
{"name": "Test", "status": "todo", "priority": 3},
]
STATUS_COLORS = {
"done": "#34C759",
"in_progress": "#FF9500",
"todo": "#8E8E93",
}
app.build(
nib.Table(
rows=tasks,
columns=[
nib.TableColumn("Task", key=lambda t: t["name"],
width=nib.ColumnWidth.range(min=80, ideal=150, max=250)),
nib.TableColumn(
"Status",
key=lambda t: t["status"],
cell=lambda t: nib.HStack(
controls=[
nib.Circle(fill=STATUS_COLORS.get(t["status"], "#888"),
width=8, height=8),
nib.Text(t["status"].replace("_", " ").title(),
font=nib.Font.CAPTION),
],
spacing=4,
),
width=nib.ColumnWidth.fixed(120),
),
nib.TableColumn("Priority", key=lambda t: t["priority"],
alignment="center",
width=nib.ColumnWidth.fixed(70)),
],
)
)
nib.run(main)
Dynamic row updates¶
import nib
def main(app: nib.App):
app.title = "Tasks"
app.icon = nib.SFSymbol("checklist")
app.width = 350
app.height = 250
table = nib.Table(
rows=[
{"name": "Design", "status": "Done"},
{"name": "Develop", "status": "In Progress"},
],
columns=[
nib.TableColumn("Task", key=lambda t: t["name"],
width=nib.ColumnWidth.range(min=80, ideal=150, max=250)),
nib.TableColumn("Status", key=lambda t: t["status"],
width=nib.ColumnWidth.fixed(100)),
],
)
count = [3]
def add_task():
table.rows = table.rows + [
{"name": f"Task {count[0]}", "status": "New"}
]
count[0] += 1
app.build(
nib.VStack(controls=[
table,
nib.Button("Add Task", action=add_task),
], spacing=8, padding=16)
)
nib.run(main)
Sorting with selection¶
Click a column header to sort. Click again to reverse. The on_select callback receives the actual row objects, not IDs:
import nib
def main(app: nib.App):
app.title = "Scores"
app.icon = nib.SFSymbol("chart.bar")
app.width = 380
app.height = 250
students = [
{"name": "Alice", "score": 95, "grade": "A"},
{"name": "Bob", "score": 87, "grade": "B+"},
{"name": "Carol", "score": 92, "grade": "A-"},
{"name": "Dave", "score": 78, "grade": "C+"},
]
status = nib.Text("Click a row to select", font=nib.Font.CAPTION,
foreground_color="#888")
def on_select(rows):
if rows:
names = ", ".join(r["name"] for r in rows)
status.content = f"Selected: {names}"
else:
status.content = "No selection"
app.build(
nib.VStack(controls=[
nib.Table(
rows=students,
columns=[
nib.TableColumn("Student", key=lambda s: s["name"],
width=nib.ColumnWidth.range(min=80, ideal=120, max=200)),
nib.TableColumn("Score", key=lambda s: s["score"],
alignment="center",
width=nib.ColumnWidth.fixed(60)),
nib.TableColumn("Grade", key=lambda s: s["grade"],
alignment="center",
width=nib.ColumnWidth.fixed(60)),
],
on_select=on_select,
),
status,
], spacing=8, padding=16)
)
nib.run(main)