scriptling.console
The scriptling.console library provides a TUI (terminal UI) console for building interactive chat-style applications, with support for layout panels, streaming output, and slash commands. There is a single shared TUI instance: no constructor needed; everything is accessed through module-level functions.
Note: This library is for TUI use only. For plain stdin/stdout I/O, use the built-in
print()andinput()functions.
Available Functions
| Function | Description |
|---|---|
panel([name]) |
Get a Panel by name (default: "main") |
main_panel() |
Get the main chat panel |
create_panel([name], **kwargs) |
Create a new panel (independent of layout) |
add_left(panel) |
Add a panel to the left of main |
add_right(panel) |
Add a panel to the right of main |
clear_layout() |
Remove the layout tree but keep all panels |
has_panels() |
Return True if a multi-panel layout is active |
styled(color, text) |
Apply a color to text and return the styled string |
set_status(left, right) |
Set both status bar texts |
set_status_left(text) |
Set left status bar text only |
set_status_right(text) |
Set right status bar text only |
set_labels(user, assistant, system) |
Set default role labels; empty string leaves a label unchanged |
register_command(name, description, fn) |
Register a slash command with the TUI |
remove_command(name) |
Remove a registered slash command |
spinner_start([text]) |
Show a spinner (TUI overlay, not per-panel) |
spinner_stop() |
Hide the spinner |
set_progress(label, pct) |
Set the progress bar (0.0-1.0, or <0 to clear) |
on_submit(fn) |
Register a handler called when the user submits input |
on_escape(fn) |
Register a callback for the Esc key |
run() |
Start the console event loop (blocks until exit) |
Color constants console.PRIMARY, console.SECONDARY, console.ERROR, console.DIM, console.USER, and console.TEXT are also available for use with styled(): see Styling below.
Panel objects returned by panel() and create_panel() have their own methods, documented under Panel Methods below.
Functions
panel(name="main")
Gets an existing Panel instance by name. Returns None if the panel doesn’t exist.
Parameters:
name(str, optional): Panel name. Default:"main".
Returns: Panel or None.
import scriptling.console as console
logs = console.panel("logs")
if logs:
logs.write("Application started\n")main_panel()
Gets the main chat panel.
Returns: Panel: the main panel instance.
main = console.main_panel()
main.add_message("Hello", "world")create_panel(name="", **kwargs)
Creates a new panel. Panels exist independently of the layout: they can be added and removed without losing their content. Use add_left/add_right (or add_row/add_column on a parent panel) to place a created panel into the layout.
Parameters:
name(str, optional): Panel identifier. Default:"".width(int, optional): Positive = columns, negative = percentage (e.g.-25= 25%).height(int, optional): Positive = rows, negative = percentage (e.g.-50= 50%).min_width(int, optional): Minimum width to render; the panel hides if narrower.scrollable(bool, optional):True= content scrolls,False= fixed viewport. Default:False.title(str, optional): Title shown in the border.no_border(bool, optional):True= hide border. Default:False.skip_focus(bool, optional):True= exclude from the Tab focus cycle. Default:False.
Returns: Panel: a panel instance.
logs = console.create_panel("logs", width=-25, min_width=15, scrollable=True, title="Logs")
right = console.create_panel(width=-30, min_width=16)
cpu_panel = console.create_panel("cpu", height=-50, title="CPU")
mem_panel = console.create_panel("mem", height=-50, title="Memory")
right.add_row(cpu_panel)
right.add_row(mem_panel)
console.add_left(logs)
console.add_right(right)add_left(panel)
Adds a panel to the left of the main panel.
Parameters:
panel(Panel): Panel to add.
Returns: None
add_right(panel)
Adds a panel to the right of the main panel.
Parameters:
panel(Panel): Panel to add.
Returns: None
clear_layout()
Removes the layout tree but keeps all panels and their content. Panels can be re-added with add_left/add_right to toggle layouts, preserving scroll state.
Returns: None
def cmd_layout(args):
if console.has_panels():
console.clear_layout()
else:
console.add_left(logs)
console.add_right(right)
console.register_command("layout", "Toggle panel layout", cmd_layout)has_panels()
Returns True if a multi-panel layout is currently active.
Returns: bool: True if a multi-panel layout is active.
if console.has_panels():
console.clear_layout() # toggle back to single panelstyled(color, text)
Applies a foreground color to text and returns the styled string. color accepts a constant (console.PRIMARY, console.SECONDARY, console.ERROR, console.DIM, console.USER, console.TEXT), a theme color name string ("primary", "secondary", "error", "dim", "user", "text"), or a hex color ("#ff6600" or "ff6600").
Parameters:
color(str): Color constant, theme name string, or hex color.text(str): Text to style.
Returns: str: the styled text.
console.styled(console.PRIMARY, "ScriptlingCoder")
console.styled(console.DIM, "type /exit to quit")
console.styled(console.ERROR, "WARNING: executes AI-generated code")
console.styled("primary", "ScriptlingCoder")
console.styled("#ff6600", "orange text")
main = console.main_panel()
main.add_message(
console.styled(console.PRIMARY, "MyApp") + ": " +
console.styled(console.DIM, "type /exit to quit")
)set_status(left, right)
Sets both status bar texts at once.
Parameters:
left(str): Left-aligned status text.right(str): Right-aligned status text.
Returns: None
console.set_status("myapp", "v1.0")set_status_left(text)
Sets the left status bar text only.
Parameters:
text(str): Left-aligned status text.
Returns: None
set_status_right(text)
Sets the right status bar text only.
Parameters:
text(str): Right-aligned status text.
Returns: None
set_labels(user, assistant, system)
Sets the default role labels used by add_message/stream_start. An empty string leaves the corresponding label unchanged.
Parameters:
user(str): Label for user messages.assistant(str): Label for assistant messages.system(str): Label for system messages.
Returns: None
console.set_labels("You", "Assistant", "") # empty string leaves system label unchangedregister_command(name, description, fn)
Registers a slash command with the TUI. The command is invoked as /name args.... Command handlers run on the same serialized handler goroutine as on_submit/on_escape: see Handler concurrency.
Parameters:
name(str): Command name (without the leading/).description(str): Short description shown in command help.fn(callable): Function called with the command’s argument string.
Returns: None
def cmd_clear(args):
console.main_panel().clear()
console.register_command("clear", "Clear output", cmd_clear)remove_command(name)
Removes a previously registered slash command.
Parameters:
name(str): Command name to remove.
Returns: None
console.remove_command("clear")spinner_start(text="Working")
Shows a spinner overlay with the given label. The spinner is a TUI-level overlay: it’s not tied to any specific panel.
Parameters:
text(str, optional): Spinner label. Default:"Working".
Returns: None
console.spinner_start("Thinking")
result = do_work()
console.spinner_stop()spinner_stop()
Hides the spinner overlay.
Returns: None
set_progress(label, pct)
Sets the progress bar overlay. The progress bar is a TUI-level overlay: it’s not tied to any specific panel.
Parameters:
label(str): Progress label.pct(float): Progress fraction from0.0to1.0. Pass a negative value to clear the progress bar.
Returns: None
console.set_progress("Downloading", 0.5) # 50%
console.set_progress("", -1) # clearon_submit(fn)
Registers the handler called when the user submits input from the prompt.
Parameters:
fn(callable): Function called with the submitted text as its single argument.
Returns: None
def on_submit(text):
main = console.main_panel()
main.stream_start()
main.stream_chunk("You said: " + text)
main.stream_end()
console.on_submit(on_submit)on_escape(fn)
Registers a callback invoked when the user presses Esc. Submitting new input or pressing Esc cancels any in-flight on_submit/on_escape/command handler, allowing cooperative handlers to stop early.
Parameters:
fn(callable): Function called with no arguments.
Returns: None
console.on_escape(lambda: console.spinner_stop())run()
Starts the console event loop. Blocks the calling thread until the user exits (e.g. via /exit or Ctrl+C).
Returns: None
import scriptling.console as console
main = console.main_panel()
console.set_status("MyApp", "v1.0")
main.add_message(console.styled(console.PRIMARY, "MyApp") + " ready!")
console.on_submit(lambda text: main.add_message("Echo: " + text))
console.run() # blocks until /exit or Ctrl+CHandler concurrency
The interpreter lock (GIL) already makes your handlers memory-safe: you never need locks around shared state. On top of that, on_submit, on_escape, and register_command handlers run one at a time, to completion before the next starts. This is deliberate for a TUI: a chat conversation is only coherent if each submit finishes before the next begins (otherwise two handlers would interleave at blocking points and scramble the order). Serializing also keeps input responsive: the input reader enqueues events and keeps reading while a handler runs.
Because handlers are sequential, a long-running handler delays the next one until it returns. If a handler may run for a while, do its work in a loop that checks for cancellation: submitting new input or pressing Esc cancels the in-flight handler (and fires on_escape), so cooperative handlers can stop early and let the next one run.
Panel Methods
console.create_panel(name) and console.panel(name) return a Panel instance with the following methods. Panels support both raw text content and message-based content.
| Method | Description |
|---|---|
write(text) |
Append text to the panel |
set_content(text) |
Replace all panel content |
clear() |
Remove all panel content |
set_title(title) |
Set the panel border title |
set_color(color) |
Set border/accent color (name or hex) |
set_scrollable(bool) |
Set whether content scrolls |
add_message(*args, label=, role=) |
Add a message to the panel |
stream_start(label=, role=) |
Begin a streaming message |
stream_chunk(text) |
Append a chunk to the current stream |
stream_end() |
Finalize the current stream |
add_row(panel) |
Add a child panel as a vertical row |
add_column(panel) |
Add a child panel as a horizontal column |
scroll_to_top() |
Scroll to top of panel content |
scroll_to_bottom() |
Scroll to bottom of panel content |
size() |
Return [width, height] of the panel |
styled(color, text) |
Apply a theme color to text |
write_at(row, col, text) |
Write text at a specific position |
clear_line(row) |
Clear a specific line |
Panel.write(text)
Appends text to the panel’s raw content buffer.
Parameters:
text(str): Text to append.
Returns: None
logs = console.panel("logs")
logs.write("entry\n")Panel.set_content(text)
Replaces all of the panel’s raw content.
Parameters:
text(str): New panel content.
Returns: None
Panel.clear()
Removes all content (messages and raw text) from the panel.
Returns: None
main = console.main_panel()
main.clear()Panel.set_title(title)
Sets the panel’s border title.
Parameters:
title(str): New title text.
Returns: None
Panel.set_color(color)
Sets the panel’s border/accent color.
Parameters:
color(str): Theme color name or hex color.
Returns: None
Panel.set_scrollable(scrollable)
Sets whether the panel’s content scrolls or uses a fixed viewport.
Parameters:
scrollable(bool):Trueto enable scrolling.
Returns: None
Panel.add_message(*args, label="", role="")
Adds a message to the panel.
Parameters:
*args: Message content parts, joined together.label(str, optional): Custom label shown next to the message. Default:"".role(str, optional): Message role, used for default styling. Default:"".
Returns: None
main = console.main_panel()
main.add_message("Hello", "world") # adds a message to the main area
main.add_message("result here", label="Tool") # message with a custom labelPanel.stream_start(label="", role="")
Begins a streaming message. Use with stream_chunk and stream_end to display text as it arrives (e.g. LLM token output).
Parameters:
label(str, optional): Custom label for the streamed message. Default:"".role(str, optional): Message role, used for default styling. Default:"".
Returns: None
main = console.main_panel()
main.stream_start() # default label
main.stream_start(label="Assistant") # custom label
for chunk in response_chunks:
main.stream_chunk(chunk)
main.stream_end()Panel.stream_chunk(text)
Appends a chunk of text to the current stream started by stream_start.
Parameters:
text(str): Chunk of text to append.
Returns: None
Panel.stream_end()
Finalizes the current stream started by stream_start.
Returns: None
Panel.add_row(child)
Adds a child panel as a vertical row (appends top to bottom). A panel cannot mix rows and columns: adding a row to a panel that already has columns is a no-op.
Parameters:
child(Panel): Child panel to add.
Returns: None
Panel.add_column(child)
Adds a child panel as a horizontal column (appends left to right). A panel cannot mix rows and columns: adding a column to a panel that already has rows is a no-op.
Parameters:
child(Panel): Child panel to add.
Returns: None
Panel.scroll_to_top()
Scrolls to the top of the panel’s content.
Returns: None
Panel.scroll_to_bottom()
Scrolls to the bottom of the panel’s content.
Returns: None
Panel.size()
Returns the panel’s current rendered size.
Returns: list: [width, height] of the panel.
Panel.styled(color, text)
Applies a theme color to text, identical to the module-level console.styled.
Parameters:
color(str): Color constant, theme name string, or hex color.text(str): Text to style.
Returns: str: the styled text.
Panel.write_at(row, col, text)
Writes text at a specific row/column position within the panel.
Parameters:
row(int): Row position.col(int): Column position.text(str): Text to write.
Returns: None
Panel.clear_line(row)
Clears a specific line within the panel.
Parameters:
row(int): Row to clear.
Returns: None
Layout
The console supports a multi-panel layout with a main chat area in the center, plus optional left and right side panels. Use the two-step builder pattern: create panels first, then add them to the layout.
┌──────────┬─────────────────────┬──────────────┐
│ Left │ Main Chat │ Right Top │
│ Panel │ ├──────────────┤
│ │ │ Right Bottom │
└──────────┴─────────────────────┴──────────────┘- Tab cycles focus through all visible, non-skip-focus panels (left children, main, right children).
- Focused panel border uses the panel’s color; unfocused uses the theme’s dim color.
- Call
clear_layout()to reset to single-panel mode at any time. - Panels and their content survive
clear_layout(): re-add them withadd_left/add_right.
Background tasks can access panels by name without needing a Console instance:
def pump_logs():
logs = console.panel("logs")
if logs:
logs.write("Background message\n")Full dashboard example
import scriptling.console as console
import scriptling.runtime as runtime
import time
logs = console.create_panel("logs", width=-25, min_width=15, scrollable=True, title="Logs")
right = console.create_panel(width=-30, min_width=16)
cpu_panel = console.create_panel("cpu", height=-50, title="CPU")
mem_panel = console.create_panel("mem", height=-50, title="Memory")
right.add_row(cpu_panel)
right.add_row(mem_panel)
console.add_left(logs)
console.add_right(right)
shared = runtime.sync.Shared("state", {"count": 0})
def pump_logs():
while True:
state = shared.get()
count = state["count"]
timestamp = time.strftime("%H:%M:%S")
line = console.styled(console.DIM, timestamp) + " Log entry #" + str(count)
logs.write(line + "\n")
state["count"] = count + 1
shared.set(state)
time.sleep(1)
runtime.background("log_pump", "pump_logs")
main = console.main_panel()
console.on_submit(lambda text: main.add_message("Echo: " + text))
console.run()See Also
- scriptling.ai.agent.interact - Interactive agent loop using this library