sidekick package

Submodules

sidekick.base_module module

Provides the foundational BaseModule class for all Sidekick visual modules.

This module defines the common blueprint and core functionalities shared by all the visual elements you create with Sidekick (like Grid, Console, Canvas, etc.). Think of it as the engine under the hood that handles essential tasks necessary for any Python object representing a component in the Sidekick UI.

Key responsibilities managed by BaseModule:

  • Unique Identification: Assigning a unique ID (target_id) to each module instance, allowing Sidekick to distinguish between different elements (e.g., multiple Grids).

  • Connection Activation: Automatically ensuring the connection to the Sidekick panel is active (activate_connection()) before sending any commands. This happens when you first create a module instance.

  • Command Sending: Providing internal helper methods (_send_command, _send_update) for constructing and sending standardized instruction messages (like “create this grid”, “update that cell”, “remove this console”) over the WebSocket connection according to the Sidekick protocol.

  • Removal: Offering a standard remove() method to destroy the visual element in the Sidekick UI and clean up associated resources in the Python library.

  • Error Handling: Providing a way (on_error()) for users to register a callback function to handle potential error messages sent back from the Sidekick UI related to a specific module instance.

  • Message Routing: Registering each instance with the connection manager so that incoming events (like clicks) or errors from the UI can be routed back to the correct Python object’s internal handler (_internal_message_handler).

Note

You will typically not use BaseModule directly in your scripts. Instead, you’ll instantiate its subclasses like sidekick.Grid, sidekick.Console, etc. This base class transparently handles the common low-level details for you.

class sidekick.base_module.BaseModule(module_type: str, instance_id: str | None = None, spawn: bool = True, payload: Dict[str, Any] | None = None)[source]

Bases: object

Base class for all Sidekick module interface classes (e.g., Grid, Console).

This abstract class manages the fundamental setup, unique identification, and communication logic required for any Python object that represents and controls a visual component within the Sidekick UI panel.

It ensures that when a module instance is created, the connection to Sidekick is established, a unique ID is assigned, and the instance is registered to receive relevant messages (events, errors) from the UI. It provides standardized methods for sending commands (spawn, update, remove) and handling cleanup.

Note

This class is designed for internal use by the library developers when creating new Sidekick module types. Users of the library should interact with the concrete subclasses (sidekick.Grid, sidekick.Console, etc.).

module_type

A string identifying the type of Sidekick module this class represents (e.g., “grid”, “console”). This must match the type expected by the Sidekick UI component and defined in the communication protocol.

Type:

str

target_id

The unique identifier assigned to this specific module instance. This ID is crucial for routing commands from Python to the correct UI element and for routing events/errors back from that UI element to this Python object.

Type:

str

__init__(module_type: str, instance_id: str | None = None, spawn: bool = True, payload: Dict[str, Any] | None = None)[source]

Initializes the base module, setting up ID, connection, and registration.

This constructor is called automatically by the __init__ method of subclasses (like Grid, Console). It performs the essential setup steps:

  1. Activates Connection: Calls connection.activate_connection(). This is a blocking call the first time any module is created in a script. It ensures the WebSocket connection is established and the Sidekick UI is ready before proceeding. Raises SidekickConnectionError on failure.

  2. Assigns ID: Determines the unique target_id for this instance, either using the provided instance_id or generating one automatically if spawn is True and instance_id is None.

  3. Registers Handler: Registers this instance’s _internal_message_handler method with the connection module, allowing it to receive messages specifically targeted at this instance’s target_id.

  4. Spawns UI Element (Optional): If spawn is True, sends the initial ‘spawn’ command to the Sidekick UI via the WebSocket, instructing it to create the corresponding visual element (using the provided payload for initial configuration).

Parameters:
  • module_type (str) – The internal type name of the module (e.g., “grid”, “console”, “viz”). This must match the type expected by the Sidekick UI and defined in the communication protocol.

  • instance_id (Optional[str]) – A specific ID for this module instance. If spawn is True (default), this is optional; if None, a unique ID (e.g., “grid-1”) is generated automatically. Providing an ID when spawning allows deterministic referencing but requires user management of uniqueness. If spawn is False (attaching to an existing UI element), this ID is required and must exactly match the ID of the pre-existing element in the Sidekick UI panel.

  • spawn (bool) – If True (the default), a “spawn” command is sent to Sidekick immediately after connection readiness to create the corresponding UI element using the payload. If False, the library assumes the UI element with the given instance_id already exists, and this Python object will simply “attach” to it to send subsequent commands (update, remove) or receive events/errors. The payload is ignored when spawn is False.

  • payload (Optional[Dict[str, Any]]) – A dictionary containing the initial configuration data needed by the Sidekick UI to correctly create (spawn) the visual element (e.g., grid dimensions, console settings, canvas size). This is only used if spawn is True. Keys within this dictionary should generally conform to the camelCase convention required by the Sidekick communication protocol. Defaults to None.

Raises:
  • ValueError – If spawn is False but no instance_id was provided, or if the determined target_id ends up being empty.

  • SidekickConnectionError (or subclass) – If connection.activate_connection() fails (e.g., cannot connect, timeout waiting for UI).

on_error(callback: Callable[[str], None] | None)[source]

Registers a function to handle error messages from the Sidekick UI for this module.

Occasionally, the Sidekick UI panel might encounter an issue while trying to process a command related to this specific module instance (e.g., you sent invalid coordinates to grid.set_color, or tried to draw on a non-existent canvas buffer). In such cases, the UI might send an ‘error’ message back to your script.

This method allows you to define a Python function (callback) that will be executed automatically when such an error message arrives for this module.

Parameters:

callback (Optional[Callable[[str], None]]) – The function to call when an error message arrives for this module instance. The function should accept one argument: a string containing the error message sent from the Sidekick UI. Pass None to remove any previously registered error handler for this instance.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> def my_grid_error_reporter(error_msg):
...     print(f"WARN: The grid '{my_grid.target_id}' reported an error: {error_msg}")
...
>>> my_grid = sidekick.Grid(5, 5)
>>> my_grid.on_error(my_grid_error_reporter)
>>>
>>> try:
...     my_grid.set_color(10, 10, 'red') # This might trigger an error if grid is 5x5
... except IndexError:
...     print("Caught local index error.") # Local check catches this first
... # If an error occurred *in the UI* processing a valid-looking command,
... # the callback would be triggered.
>>>
>>> # To stop handling errors this way:
>>> my_grid.on_error(None)
remove()[source]

Removes this module instance from the Sidekick UI and cleans up resources.

This method performs the necessary actions to gracefully remove the visual element associated with this Python object from the Sidekick panel and tidy up related resources within the library.

Specifically, it:

  1. Unregisters Handlers: Stops listening for messages (events/errors) specifically targeted at this module instance.

  2. Resets Callbacks: Clears any user-defined callback functions (like on_click or on_error) associated with this instance, both in the base class and any specific ones defined in subclasses via _reset_specific_callbacks().

  3. Sends Remove Command: Sends a ‘remove’ command to the Sidekick UI, instructing it to destroy the visual element corresponding to this instance’s target_id.

Important

After calling remove(), you should generally consider this module object inactive and avoid calling further methods on it, as it no longer corresponds to an element in the UI and cannot send commands effectively.

Raises:

SidekickConnectionError (or subclass) – Can potentially be raised by the underlying _send_command if sending the ‘remove’ command fails, though the local cleanup (steps 1 & 2) will still be attempted.

Example

>>> my_grid = sidekick.Grid(5, 5)
>>> my_console = sidekick.Console()
>>> # ... use the grid and console ...
>>>
>>> # Remove them when done
>>> my_grid.remove()
>>> my_console.remove()
__del__()[source]

Attempt to unregister the message handler upon garbage collection. (Fallback).

This special method is called by Python’s garbage collector when the BaseModule instance is about to be destroyed if its reference count reaches zero. It attempts to unregister the instance’s message handler from the connection module as a fallback safety measure in case the remove() method was not called explicitly.

Warning

Relying on __del__ for crucial cleanup like this is strongly discouraged in Python. The timing of garbage collection and __del__ execution is unpredictable and not guaranteed, especially during interpreter shutdown. You should always explicitly call the `remove()` method on your Sidekick module instances when you are finished with them to ensure proper cleanup in both the Python library and the Sidekick UI panel. This __del__ method is only a best-effort fallback.

sidekick.canvas module

Provides the Canvas class for creating a 2D drawing surface in Sidekick.

Use the sidekick.Canvas class to create a blank rectangular area within the Sidekick panel where your Python script can draw simple graphics. This allows you to visually represent geometric concepts, create algorithm visualizations, build simple game graphics, or even produce basic animations controlled by your code.

Key Features:

  • Drawing Primitives: Draw basic shapes like lines (draw_line), rectangles (draw_rect), circles (draw_circle), polygons (draw_polygon), ellipses (draw_ellipse), and text (draw_text).

  • Styling: Control the appearance with options for fill color (fill_color), line color (line_color), line width (line_width), and text size/color.

  • Coordinate System: The origin (0, 0) is at the top-left corner. The x-axis increases to the right, and the y-axis increases downwards. All units (coordinates, dimensions, radii) are in pixels.

  • Double Buffering: Create smooth, flicker-free animations using the canvas.buffer() context manager. This draws a complete frame off-screen before displaying it all at once.

  • Interactivity: Make your canvas respond to user clicks using the on_click() method to register a callback function.

Basic Usage:
>>> import sidekick
>>> # Create a 300 pixel wide, 200 pixel tall canvas
>>> canvas = sidekick.Canvas(300, 200)
>>>
>>> # Draw a red diagonal line across the canvas
>>> canvas.draw_line(10, 10, 290, 190, line_color='red')
>>>
>>> # Draw a blue filled rectangle with a thick border
>>> canvas.draw_rect(50, 50, 100, 80, fill_color='blue', line_width=3)
>>>
>>> # Draw some text
>>> canvas.draw_text(150, 120, "Hello Canvas!", text_color='white', text_size=16)
Animation Example (using double buffering):
>>> import sidekick, time, math
>>> canvas = sidekick.Canvas(200, 150)
>>> angle = 0
>>> for _ in range(60): # Animate briefly
...     # --- Draw onto a hidden buffer ---
...     with canvas.buffer() as buf: # 'buf' lets you draw on the hidden buffer
...         x = 100 + 80 * math.cos(angle)
...         y = 75 + 50 * math.sin(angle)
...         buf.draw_circle(int(x), int(y), 10, fill_color='orange')
...         buf.draw_text(10, 10, f"Angle: {angle:.1f}")
...     # --- Display the hidden buffer ---
...     # When the 'with' block ends, the hidden buffer's content is shown.
...     angle += 0.1
...     time.sleep(0.05)
...
>>> print("Animation finished.")
>>> # Use sidekick.run_forever() if you also need click handling.
class sidekick.canvas.Canvas(width: int, height: int, instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents a 2D drawing canvas module instance in the Sidekick UI.

Provides a surface within the Sidekick panel where your Python script can programmatically draw shapes, lines, and text. It’s useful for visualizing algorithms, creating simple graphics or simulations, basic game displays, or educational demonstrations.

Drawing commands (like draw_line(), draw_rect()) are sent immediately to the Sidekick UI to update the visual representation. By default, these draw directly onto the visible canvas area.

For creating animations or complex scenes smoothly without flickering, use the buffer() method within a with statement. This enables “double buffering”, where drawing happens on a hidden surface first, and the result is displayed all at once (see module docstring or buffer() method documentation for examples).

You can also make the canvas interactive by responding to user clicks via the on_click() method.

target_id

The unique identifier for this canvas instance, used for communication with the Sidekick UI. Generated automatically if not provided during initialization.

Type:

str

width

The width of the canvas drawing area in pixels, set during initialization. This value is read-only after creation.

Type:

int

height

The height of the canvas drawing area in pixels, set during initialization. This value is read-only after creation.

Type:

int

ONSCREEN_BUFFER_ID = 0
__init__(width: int, height: int, instance_id: str | None = None, spawn: bool = True)[source]

Initializes a new Canvas object and optionally creates its UI element in Sidekick.

Sets up the dimensions and prepares the canvas for drawing commands. The connection to Sidekick is established automatically during this initialization if it hasn’t been already (this might block).

Parameters:
  • width (int) – The desired width of the canvas drawing area in pixels. Must be a positive integer.

  • height (int) – The desired height of the canvas drawing area in pixels. Must be a positive integer.

  • instance_id (Optional[str]) –

    A specific ID to assign to this canvas instance. - If spawn=True (default): If provided, this ID will be used. Useful

    for deterministic identification. If None, a unique ID (e.g., “canvas-1”) will be generated automatically.

    • If spawn=False: This ID is required and must match the ID of an existing canvas element already present in the Sidekick UI that this Python object should connect to and control.

  • spawn (bool) – If True (the default), a command is sent to Sidekick (after connection) to create a new canvas UI element with the specified width and height. If False, the library assumes a canvas element with the given instance_id already exists in the UI, and this Python object will simply attach to it for sending drawing commands or receiving events. When spawn=False, the width and height arguments are still validated locally but are not sent in the (empty) spawn command.

Raises:
  • ValueError – If width or height are not positive integers, or if spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to the Sidekick UI cannot be established or fails during initialization.

Examples

>>> # Create a new 300x200 canvas in Sidekick
>>> main_canvas = sidekick.Canvas(300, 200)
>>>
>>> # Create another canvas with a specific ID
>>> mini_map = sidekick.Canvas(100, 100, instance_id="ui-mini-map")
>>>
>>> # Assume a canvas with ID "debug-overlay" already exists in Sidekick.
>>> # Attach a Python object to control it (local dimensions needed for validation).
>>> overlay_control = sidekick.Canvas(100, 50, instance_id="debug-overlay", spawn=False)
property width: int

The width of the canvas in pixels (read-only).

Type:

int

property height: int

The height of the canvas in pixels (read-only).

Type:

int

on_click(callback: Callable[[int, int], None] | None)[source]

Registers a function to be called when the user clicks on the canvas.

When the user clicks anywhere on this canvas’s visible area in the Sidekick UI panel, the function you provide (callback) will be executed within your running Python script.

Note

Click events are only triggered by interactions with the main, visible (onscreen) canvas (buffer ID 0). Clicks are not detected on hidden offscreen buffers used with canvas.buffer().

Parameters:

callback (Optional[Callable[[int, int], None]]) – The function to execute when a click occurs. This function must accept two integer arguments: x (the horizontal pixel coordinate of the click relative to the canvas’s left edge, starting at 0) and y (the vertical pixel coordinate relative to the canvas’s top edge, starting at 0). To remove a previously registered callback, pass None.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> def draw_dot_on_click(x, y):
...     print(f"Canvas clicked at ({x}, {y}). Drawing a small circle there.")
...     # Draw a small green circle at the click location
...     canvas.draw_circle(x, y, 5, fill_color='green')
...
>>> canvas = sidekick.Canvas(250, 250)
>>> canvas.on_click(draw_dot_on_click)
>>> print("Canvas created. Click on the canvas in the Sidekick panel!")
>>> # Keep the script running to listen for click events
>>> sidekick.run_forever()
buffer() ContextManager[_CanvasBufferProxy][source]

Provides a context manager (with statement) for efficient double buffering.

Using this method enables double buffering, the standard technique for creating smooth, flicker-free animations or complex drawing sequences. Instead of drawing directly to the visible screen (which can cause tearing or flickering as elements are drawn one by one), you draw everything for the next frame onto a hidden (offscreen) buffer. When you’re finished drawing the frame, the entire content of the hidden buffer is copied to the visible screen in one go.

How it works in practice:

  1. Enter `with` block: with canvas.buffer() as buf: * An offscreen buffer is acquired (or created/reused). * This buffer is automatically cleared. * The variable buf becomes a proxy object that mirrors the canvas’s

    drawing methods (e.g., buf.draw_line()).

  2. Inside `with` block: * All drawing commands called on buf (e.g., buf.draw_circle(…))

    are sent to the hidden offscreen buffer. The visible screen remains unchanged during this time.

  3. Exit `with` block: * The visible canvas is automatically cleared. * The entire content of the hidden buffer is drawn (“blitted”) onto

    the visible canvas in a single, fast operation.

    • The hidden buffer is released back into an internal pool so it can be reused efficiently next time you enter a buffer() context.

Returns:

An object designed to be used in a with statement. The object yielded by the with statement (buf in the example) provides the drawing methods that target the hidden buffer.

Return type:

ContextManager[_CanvasBufferProxy]

Example (Simple Animation):
>>> import sidekick, time, math
>>> canvas = sidekick.Canvas(150, 100)
>>> x_pos = 10
>>> for frame in range(50):
...     with canvas.buffer() as frame_buffer: # Get the hidden buffer proxy
...         # Draw background (optional, buffer starts clear)
...         # frame_buffer.draw_rect(0, 0, canvas.width, canvas.height, fill_color='lightblue')
...         # Draw moving element on the hidden buffer
...         frame_buffer.draw_circle(x_pos, 50, 10, fill_color='red')
...         frame_buffer.draw_text(5, 15, f"Frame: {frame}")
...     # --- Screen automatically updates here when 'with' block ends ---
...     x_pos += 2 # Move for next frame
...     time.sleep(0.03) # Control animation speed
>>> print("Animation finished.")
clear(buffer_id: int | None = None)[source]

Clears the specified canvas buffer (visible screen or an offscreen buffer).

Erases all previously drawn shapes, lines, and text from the target buffer, resetting it to a blank state (typically transparent or a default background color determined by the UI theme).

Parameters:

buffer_id (Optional[int]) –

The ID of the buffer to clear. - If None (default) or 0, clears the main visible (onscreen) canvas. - If a positive integer corresponding to an offscreen buffer (usually

obtained implicitly via canvas.buffer()), clears that specific hidden buffer.

Raises:

SidekickConnectionError (or subclass) – If sending the command fails.

Examples

>>> canvas = sidekick.Canvas(100, 50)
>>> canvas.draw_rect(10, 10, 30, 30, fill_color='red')
>>> # Clear the main visible canvas
>>> canvas.clear()
>>>
>>> # Example within double buffering: Clear the offscreen buffer
>>> with canvas.buffer() as buf:
...     # buf implicitly refers to an offscreen buffer
...     # To clear *that* specific buffer (e.g., at start of frame drawing):
...     buf.clear() # This calls canvas.clear(buffer_id=buf._buffer_id) internally
...     buf.draw_circle(50, 25, 10) # Draw new content
... # On exit, screen is cleared, then buffer is drawn.
draw_line(x1: int, y1: int, x2: int, y2: int, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a straight line segment between two points on the specified buffer.

Connects the start point (x1, y1) to the end point (x2, y2).

Parameters:
  • x1 (int) – The x-coordinate (pixels from left) of the line’s start point.

  • y1 (int) – The y-coordinate (pixels from top) of the line’s start point.

  • x2 (int) – The x-coordinate of the line’s end point.

  • y2 (int) – The y-coordinate of the line’s end point.

  • line_color (Optional[str]) – The color of the line. Accepts standard CSS color formats (e.g., ‘black’, ‘#FF0000’, ‘rgb(0, 255, 0)’, ‘hsl(120, 100%, 50%)’). If None, the Sidekick UI’s default line color (usually determined by the theme) is used.

  • line_width (Optional[int]) – The thickness of the line in pixels. Must be a positive integer (e.g., 1, 2, 3…). If None, the UI’s default line width (typically 1 pixel) is used.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None, which targets the main visible (onscreen) canvas (ID 0). When used inside with canvas.buffer() as buf:, drawing methods on buf automatically target the correct offscreen buffer ID.

Raises:

Example

>>> canvas = sidekick.Canvas(150, 150)
>>> # Draw line with default color/width from (10, 20) to (100, 120)
>>> canvas.draw_line(10, 20, 100, 120)
>>> # Draw a thicker, blue line
>>> canvas.draw_line(20, 30, 110, 130, line_color='blue', line_width=3)
draw_rect(x: int, y: int, width: int, height: int, fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a rectangle on the specified buffer.

The rectangle is defined by its top-left corner coordinates (x, y) and its width and height.

Parameters:
  • x (int) – The x-coordinate of the top-left corner.

  • y (int) – The y-coordinate of the top-left corner.

  • width (int) – The width of the rectangle in pixels. Should be non-negative. (A width of 0 might not be visible).

  • height (int) – The height of the rectangle in pixels. Should be non-negative. (A height of 0 might not be visible).

  • fill_color (Optional[str]) – The color to fill the inside of the rectangle. Accepts CSS color formats. If None (default), the rectangle will not be filled (its interior will be transparent).

  • line_color (Optional[str]) – The color of the rectangle’s outline (border). If None (default), the UI’s default outline color is used. If you don’t want an outline, you might need to set line_width to 0 or line_color to the same as fill_color or a transparent color, depending on the desired effect and UI behavior.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be positive if an outline is desired. If None (default), the UI’s default outline width (typically 1 pixel) is used. A line_width of 0 usually means no outline is drawn.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If line_width is provided but is not a non-negative integer, or if width or height are negative.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(200, 150)
>>> # Draw an empty rectangle with a 2px red border
>>> canvas.draw_rect(10, 10, 50, 80, line_color='red', line_width=2)
>>> # Draw a filled green rectangle with the default 1px border
>>> canvas.draw_rect(70, 30, 40, 40, fill_color='green')
>>> # Draw a filled yellow rectangle with effectively no border
>>> canvas.draw_rect(120, 50, 30, 30, fill_color='yellow', line_width=0)
draw_circle(cx: int, cy: int, radius: int, fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a circle on the specified buffer.

The circle is defined by its center coordinates (cx, cy) and its radius.

Parameters:
  • cx (int) – The x-coordinate of the center of the circle.

  • cy (int) – The y-coordinate of the center of the circle.

  • radius (int) – The radius of the circle in pixels. Must be positive.

  • fill_color (Optional[str]) – The color to fill the inside of the circle. Accepts CSS color formats. If None (default), the circle is not filled.

  • line_color (Optional[str]) – The color of the circle’s outline. If None (default), the UI’s default outline color is used.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be positive if an outline is desired. If None (default), the UI’s default outline width (typically 1 pixel) is used. Use 0 for no outline.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If radius is not positive, or if line_width is provided but is not a non-negative integer.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(250, 150)
>>> # Draw a filled red circle
>>> canvas.draw_circle(50, 75, 40, fill_color='red')
>>> # Draw an empty circle with a thick blue outline
>>> canvas.draw_circle(150, 75, 30, line_color='blue', line_width=4)
draw_polyline(points: List[Tuple[int, int]], line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a series of connected line segments (an open path) on the specified buffer.

Connects the points in the order they appear in the points list using straight lines. It’s an “open” path because, unlike draw_polygon, it does not automatically draw a line connecting the last point back to the first.

Parameters:
  • points (List[Tuple[int, int]]) – A list containing at least two vertex tuples, where each tuple (x, y) represents the integer coordinates of a corner point of the polyline.

  • line_color (Optional[str]) – The color for all line segments in the polyline. Uses UI default if None.

  • line_width (Optional[int]) – The thickness for all line segments in pixels. Must be positive. Uses UI default (typically 1) if None.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If points contains fewer than two points, or if line_width is provided but is not positive.

  • TypeError – If points is not a list or contains non-tuple/non-numeric elements.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(200, 100)
>>> # Draw a 'W' shape
>>> w_shape_points = [(20, 80), (40, 20), (60, 80), (80, 20), (100, 80)]
>>> canvas.draw_polyline(w_shape_points, line_color='purple', line_width=3)
draw_polygon(points: List[Tuple[int, int]], fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a closed polygon shape on the specified buffer.

Connects the vertices (points) in the order provided and automatically draws an additional line segment connecting the last point back to the first point to close the shape. The interior can optionally be filled.

Parameters:
  • points (List[Tuple[int, int]]) – A list containing at least three vertex tuples, where each tuple (x, y) represents the integer coordinates of a corner of the polygon.

  • fill_color (Optional[str]) – The color to fill the interior of the polygon. Accepts CSS color formats. If None (default), the polygon is not filled.

  • line_color (Optional[str]) – The color of the polygon’s outline. Uses UI default if None. Use 0 for no outline.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be non-negative. Uses UI default (typically 1) if None.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If points contains fewer than three points, or if line_width is provided but is negative.

  • TypeError – If points is not a list or contains invalid data.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(200, 200)
>>> # Draw a filled blue triangle
>>> triangle = [(50, 150), (100, 50), (150, 150)]
>>> canvas.draw_polygon(triangle, fill_color='blue')
>>> # Draw an empty hexagon outline
>>> hexagon = [(20, 10), (60, 10), (80, 50), (60, 90), (20, 90), (0, 50)]
>>> canvas.draw_polygon(hexagon, line_color='darkgreen', line_width=2)
draw_ellipse(cx: int, cy: int, radius_x: int, radius_y: int, fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws an ellipse (or oval) shape on the specified buffer.

The ellipse is defined by its center coordinates (cx, cy) and its horizontal radius (radius_x) and vertical radius (radius_y). If radius_x equals radius_y, this draws a circle.

Parameters:
  • cx (int) – The x-coordinate of the center of the ellipse.

  • cy (int) – The y-coordinate of the center of the ellipse.

  • radius_x (int) – The horizontal radius (half the total width) of the ellipse. Must be positive.

  • radius_y (int) – The vertical radius (half the total height) of the ellipse. Must be positive.

  • fill_color (Optional[str]) – The color to fill the inside of the ellipse. Accepts CSS color formats. If None (default), the ellipse is not filled.

  • line_color (Optional[str]) – The color of the ellipse’s outline. Uses UI default if None.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be non-negative. Uses UI default (typically 1) if None. Use 0 for no outline.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If radius_x or radius_y are not positive, or if line_width is provided but is negative.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(250, 150)
>>> # Draw a wide, short, filled red ellipse
>>> canvas.draw_ellipse(125, 50, 80, 30, fill_color='red')
>>> # Draw a tall, thin, empty ellipse outline
>>> canvas.draw_ellipse(125, 100, 20, 40, line_color='black', line_width=1)
draw_text(x: int, y: int, text: str, text_color: str | None = None, text_size: int | None = None, buffer_id: int | None = None)[source]

Draws a string of text on the specified buffer.

The (x, y) coordinates typically define the position of the text. The exact alignment (e.g., whether (x, y) is the top-left corner, bottom-left baseline, or center) might depend slightly on the underlying UI implementation, but bottom-left baseline is common for canvas APIs.

Parameters:
  • x (int) – The x-coordinate for the starting position of the text.

  • y (int) – The y-coordinate for the starting position of the text.

  • text (str) – The text string you want to display. Any Python object provided will be converted to its string representation using str().

  • text_color (Optional[str]) – The color of the text. Accepts CSS color formats. Uses the UI’s default text color (usually black or white depending on theme) if None.

  • text_size (Optional[int]) – The font size in pixels. Must be positive. Uses the UI’s default font size if None.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:

Example

>>> canvas = sidekick.Canvas(200, 100)
>>> score = 150
>>> canvas.draw_text(10, 20, f"Score: {score}") # Draw score with default style
>>> canvas.draw_text(100, 60, "GAME OVER", text_color='red', text_size=24) # Larger, red text
remove()[source]

Removes the canvas from the Sidekick UI and cleans up associated resources.

This performs the necessary cleanup actions:

  1. Destroys Offscreen Buffers: Sends commands to the Sidekick UI to destroy any hidden offscreen buffers that were created for this canvas instance via canvas.buffer(), releasing their resources in the UI.

  2. Calls Base `remove()`: Invokes the BaseModule.remove() method, which: a. Unregisters the internal message handler for this canvas. b. Resets registered callbacks (on_click, on_error) to None. c. Sends the final ‘remove’ command to the Sidekick UI to delete the

    main canvas element itself.

After calling remove(), you should no longer interact with this Canvas object.

Raises:

SidekickConnectionError (or subclass) – Can occur if sending the ‘destroyBuffer’ or the final ‘remove’ command fails. Cleanup of local Python resources (callbacks, handlers, buffer pool state) will still be attempted.

__del__()[source]

Internal: Fallback attempt to clean up resources upon garbage collection. (Internal).

Warning

Relying on __del__ for cleanup is not reliable in Python. You should always explicitly call the `canvas.remove()` method when you are finished with a canvas instance to ensure proper cleanup of both Python resources and UI elements (including offscreen buffers) in Sidekick. This __del__ method primarily attempts to call the base class’s __del__ for handler unregistration as a last resort. It does not attempt to destroy offscreen buffers.

sidekick.connection module

Manages the WebSocket connection between your Python script and the Sidekick UI.

This module acts as the central communication hub for the Sidekick library. It handles the technical details of establishing and maintaining a real-time connection with the Sidekick panel running in Visual Studio Code.

Key Responsibilities:

  • Connecting: Automatically attempts to connect to the Sidekick server (usually running within the VS Code extension) the first time your script tries to interact with a Sidekick module (e.g., when you create sidekick.Grid()).

  • Blocking Connection: It pauses (blocks) your script during the initial connection phase until it confirms that both the server is reached and the Sidekick UI panel is loaded and ready to receive commands. This ensures your commands don’t get lost.

  • Sending Commands: Provides the mechanism (send_message, used internally by modules like Grid, Console) to send instructions (like “set color”, “print text”) to the Sidekick UI.

  • Receiving Events: Runs a background thread (_listen_for_messages) to listen for messages coming from the Sidekick UI (like button clicks or text input) and routes them to the correct handler function in your script (e.g., the function you provided to grid.on_click).

  • Error Handling: Raises specific SidekickConnectionError exceptions if it cannot connect, if the UI doesn’t respond, or if the connection is lost later.

  • Lifecycle Management: Handles clean shutdown procedures, ensuring resources are released when your script finishes or when sidekick.shutdown() is called.

Note

You typically interact with this module indirectly through functions like sidekick.run_forever() or sidekick.shutdown(), or simply by using the visual module classes (Grid, Console, etc.). However, understanding its role helps explain the library’s behavior, especially regarding connection and event handling. The library does not automatically attempt to reconnect if the connection is lost after being established.

exception sidekick.connection.SidekickConnectionError[source]

Bases: Exception

Base error for all Sidekick connection-related problems.

Catch this exception type if you want to handle any issue related to establishing or maintaining the connection to the Sidekick panel.

Example

>>> try:
...     console = sidekick.Console() # Connection happens here
...     console.print("Connected!")
... except sidekick.SidekickConnectionError as e:
...     print(f"Could not connect to Sidekick: {e}")
exception sidekick.connection.SidekickConnectionRefusedError(url: str, original_exception: Exception)[source]

Bases: SidekickConnectionError

Raised when the library fails to connect to the Sidekick server initially.

This usually means the Sidekick WebSocket server wasn’t running or couldn’t be reached at the configured URL (ws://localhost:5163 by default).

Common Causes:

  1. The Sidekick panel isn’t open and active in VS Code.

  2. The Sidekick VS Code extension isn’t running correctly or has encountered an error.

  3. The WebSocket server couldn’t start (e.g., the port is already in use by another application). Check VS Code’s “Sidekick Server” output channel for details.

  4. A firewall is blocking the connection between your script and VS Code.

  5. The URL was changed via sidekick.set_url() to an incorrect address.

url

The WebSocket URL that the connection attempt was made to.

Type:

str

original_exception

The lower-level error that caused the failure (e.g., ConnectionRefusedError from the OS, TimeoutError from the websocket library).

Type:

Exception

exception sidekick.connection.SidekickTimeoutError(timeout: float)[source]

Bases: SidekickConnectionError

Raised when connection to the server succeeds, but the Sidekick UI panel doesn’t respond.

After successfully connecting to the WebSocket server (run by the VS Code extension), the library waits a short time (a few seconds) for the Sidekick UI panel itself (the web content inside the panel) to finish loading and send back a signal confirming it’s ready to receive commands. If this signal doesn’t arrive within the timeout period, this error is raised.

Common Causes:

  1. The Sidekick panel is open in VS Code, but it hasn’t finished loading its HTML/JavaScript content yet (e.g., due to slow system performance or network issues if loading remote resources, though usually local).

  2. There’s an error within the Sidekick UI panel’s JavaScript code preventing it from initializing correctly. Check the Webview Developer Tools in VS Code (Command Palette -> “Developer: Open Webview Developer Tools”) for errors.

timeout

The number of seconds the library waited for the UI response.

Type:

float

exception sidekick.connection.SidekickDisconnectedError(reason: str = 'Connection lost')[source]

Bases: SidekickConnectionError

Raised when the connection is lost after it was successfully established.

This indicates that communication was working previously, but the connection broke unexpectedly. This can happen if you try to send a command or if the background listener thread detects the disconnection.

Common Causes:

  1. The Sidekick panel was closed in VS Code while your script was still running.

  2. The Sidekick VS Code extension crashed, was disabled, or VS Code was closed.

  3. A network interruption occurred between the Python script and VS Code (less common for local connections but possible).

  4. An internal error occurred while trying to send or receive a message over the established connection.

Important: The library will not automatically try to reconnect if this error occurs. Any further attempts to use Sidekick modules (like grid.set_color()) will also fail until the script is potentially restarted and a new connection is established.

reason

A short description of why the disconnection occurred or was detected.

Type:

str

class sidekick.connection.ConnectionStatus(*values)[source]

Bases: Enum

Represents the different states of the WebSocket connection internally.

DISCONNECTED = 1
CONNECTING = 2
CONNECTED_WAITING_SIDEKICK = 3
CONNECTED_READY = 4
sidekick.connection.set_url(url: str)[source]

Sets the WebSocket URL where the Sidekick server is expected to be listening.

You must call this function before creating any Sidekick modules (like sidekick.Grid()) or calling any other Sidekick function that might trigger a connection attempt (like sidekick.clear_all()). The library uses the URL set here when it makes its first connection attempt.

Calling this after a connection attempt has already started (even if it failed) will log a warning and have no effect, unless you explicitly call sidekick.shutdown() first to completely reset the connection state.

Parameters:

url (str) – The full WebSocket URL, which must start with “ws://” or “wss://”. The default value is “ws://localhost:5163”.

Raises:

ValueError – If the provided URL does not start with “ws://” or “wss://”.

Example

>>> import sidekick
>>> # If the Sidekick server is running on a different machine or port
>>> try:
...     sidekick.set_url("ws://192.168.1.100:5163")
... except ValueError as e:
...     print(e)
>>>
>>> # Now it's safe to create Sidekick modules
>>> console = sidekick.Console()
sidekick.connection.set_config(clear_on_connect: bool = True, clear_on_disconnect: bool = False)[source]

Configures automatic clearing behavior for the Sidekick UI panel.

Like set_url, you must call this function before the first connection attempt is made (i.e., before creating any Sidekick modules). Calling it later will have no effect unless shutdown() is called first.

Parameters:
  • clear_on_connect (bool) – If True (the default), the library will automatically send a command to clear all existing elements from the Sidekick UI panel as soon as the connection becomes fully ready (i.e., when the UI panel signals it’s online and ready). This provides a clean slate for your script. Set this to False if you want your script to potentially add to or interact with UI elements left over from a previous script run (less common).

  • clear_on_disconnect (bool) – If True (default is False), the library will attempt (on a best-effort basis) to send a command to clear the Sidekick UI when your script disconnects cleanly. This happens when shutdown() is called explicitly or when the script exits normally (due to the atexit handler). This cleanup might not happen if the connection is lost abruptly or due to an error.

sidekick.connection.activate_connection()[source]

Ensures the connection to Sidekick is established and fully ready. (Internal use).

This function is the gateway for all communication. It’s called implicitly by send_message (which is used by all module methods like grid.set_color) and at the start of run_forever. You generally don’t need to call it directly.

It performs the crucial steps of:

  1. Checking the current connection status.

  2. If disconnected, initiating the connection attempt (_ensure_connection).

  3. Blocking execution if the WebSocket is connected but the UI panel hasn’t signaled readiness yet (waiting on _ready_event).

  4. Returning only when the status is CONNECTED_READY.

Raises:
sidekick.connection.send_message(message_dict: Dict[str, Any])[source]

Sends a command message (as a dictionary) to the Sidekick UI. (Internal use).

This is the core function used by all Sidekick modules (Grid, Console, etc.) to send their specific commands (like ‘setColor’, ‘append’, ‘add’) to the UI panel. You typically don’t call this directly.

It ensures the connection is ready via activate_connection() before attempting to serialize the message to JSON and send it over the WebSocket.

Parameters:

message_dict (Dict[str, Any]) – A Python dictionary representing the message. It must conform to the Sidekick communication protocol structure, including module, type, target/src, and a payload whose keys should generally be camelCase.

Raises:
sidekick.connection.clear_all()[source]

Sends a command to remove all visual elements from the Sidekick panel.

This effectively resets the Sidekick UI, removing any Grids, Consoles, Viz panels, Canvases, or Controls that were created by this running Python script instance.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or if sending the command fails.

sidekick.connection.close_connection(log_info=True, is_exception=False, reason='')[source]

Closes the WebSocket connection and cleans up resources. (Internal use).

This is the core cleanup function. It stops the listener thread, closes the WebSocket socket, sends final ‘offline’/’clearAll’ messages (best-effort), and resets internal state variables.

It’s called automatically by shutdown() and the atexit handler for clean exits, and also triggered internally by the listener thread or send_message if an unrecoverable error (is_exception=True) is detected.

Users should typically call `sidekick.shutdown()` instead of this directly.

Parameters:
  • log_info (bool) – If True, logs status messages during the closure process.

  • is_exception (bool) – If True, indicates this closure was triggered by an error condition (e.g., listener crash, send failure). This may influence whether a final SidekickDisconnectedError is raised after cleanup, depending on whether a clean shutdown() was also requested concurrently.

  • reason (str) – Optional description of why the connection is closing, used for logging and potentially included in error messages.

sidekick.connection.run_forever()[source]

Keeps your Python script running indefinitely to handle Sidekick UI events.

If your script needs to react to interactions in the Sidekick panel (like button clicks, grid cell clicks, console input, etc.), it needs to stay alive to listen for those events. Calling sidekick.run_forever() at the end of your script achieves this.

It essentially pauses the main thread of your script in a loop, while the background listener thread (managed internally) continues to receive messages from Sidekick and trigger your registered callback functions (e.g., the functions passed to grid.on_click() or console.on_input_text()).

How to Stop run_forever():

  1. Press Ctrl+C in the terminal where your script is running.

  2. Call sidekick.shutdown() from within one of your callback functions (e.g., have a “Quit” button call sidekick.shutdown in its on_click handler).

  3. If the connection to Sidekick breaks unexpectedly, run_forever() will also stop (typically after a SidekickDisconnectedError is raised).

Raises:

SidekickConnectionError (or subclass) – If the initial connection to Sidekick cannot be established when run_forever starts. The script won’t enter the waiting loop if it can’t connect first.

Example

>>> import sidekick
>>> console = sidekick.Console(show_input=True)
>>> def handle_input(text):
...     if text.lower() == 'quit':
...         console.print("Exiting...")
...         sidekick.shutdown() # Stop run_forever from callback
...     else:
...         console.print(f"You typed: {text}")
>>> console.input_text_handler(handle_input)
>>> console.print("Enter text or type 'quit' to exit.")
>>>
>>> # Keep script running to listen for input
>>> sidekick.run_forever()
>>> print("Script has finished.") # This line runs after run_forever exits
sidekick.connection.shutdown()[source]

Initiates a clean shutdown of the Sidekick connection and resources.

Call this function to gracefully disconnect from the Sidekick panel. It performs the following actions: - Signals run_forever() (if it’s currently running) to stop its waiting loop. - Attempts to send a final ‘offline’ announcement to the Sidekick server. - Attempts to send a ‘clearAll’ command to the UI (if configured via set_config). - Closes the underlying WebSocket connection. - Stops the background listener thread. - Clears internal state and message handlers.

It’s safe to call this function multiple times; subsequent calls after the first successful shutdown will have no effect.

This function is also registered automatically via atexit to be called when your Python script exits normally, ensuring cleanup happens if possible.

You might call this manually from an event handler (e.g., a “Quit” button’s on_click callback) to programmatically stop run_forever() and end the script.

Example

>>> def on_quit_button_click(control_id):
...    print("Quit button clicked. Shutting down.")
...    sidekick.shutdown()
>>> controls.add_button("quit", "Quit")
>>> controls.on_click(on_quit_button_click)
>>> sidekick.run_forever()
sidekick.connection.register_message_handler(instance_id: str, handler: Callable[[Dict[str, Any]], None])[source]

Registers a handler function for messages targeted at a specific module instance. (Internal).

This is called automatically by BaseModule.__init__ when a Sidekick module (like Grid, Console) is created. It maps the module’s unique instance_id to its _internal_message_handler method.

The listener thread uses this mapping to dispatch incoming ‘event’ and ‘error’ messages from the UI to the correct Python object.

Parameters:
  • instance_id (str) – The unique ID of the module instance (e.g., “grid-1”).

  • handler (Callable) – The function (usually an instance method) to call. It must accept one argument: the message dictionary.

Raises:

TypeError – If the provided handler is not a callable function.

sidekick.connection.unregister_message_handler(instance_id: str)[source]

Removes the message handler for a specific module instance. (Internal).

Called automatically by BaseModule.remove() when a module is explicitly removed, and also during the final close_connection cleanup to clear all handlers.

Parameters:

instance_id (str) – The ID of the module instance whose handler should be removed.

sidekick.connection.register_global_message_handler(handler: Callable[[Dict[str, Any]], None] | None)[source]

Registers a single function to receive all incoming messages from Sidekick.

Advanced Usage / Debugging: This function is primarily intended for debugging the communication protocol or building very custom, low-level integrations. The function you provide (handler) will be called by the listener thread for every message received from the Sidekick server, before the message is dispatched to any specific module instance handlers.

Warning

The structure and content of messages received here are subject to the internal Sidekick communication protocol and may change between versions. Relying on this for core application logic is generally discouraged.

Parameters:

handler (Optional[Callable[[Dict[str, Any]], None]]) – The function to call with each raw message dictionary received from the WebSocket. It should accept one argument (the message dict). Pass None to remove any currently registered global handler.

Raises:

TypeError – If the provided handler is not a callable function and not None.

sidekick.console module

Provides the Console class for displaying text output in Sidekick.

Use the sidekick.Console class to create a dedicated text area within the Sidekick panel. This acts like a separate terminal or output window specifically for your script, allowing you to display status messages, log information, or show results without cluttering the main VS Code terminal.

Key Features:

  • Text Output: Use the print() method (similar to Python’s built-in print) to append text messages to the console area.

  • Optional Text Input: Configure the console (show_input=True) to include a text input field at the bottom. Users can type text into this field and submit it back to your running Python script.

  • Input Handling: Use the on_input_text() method to register a callback function that gets executed whenever the user submits text from the input field.

  • Clearing: Use the clear() method to remove all previously displayed text.

Basic Usage:
>>> import sidekick
>>> # Create a simple console for output only
>>> console = sidekick.Console()
>>> console.print("Script starting...")
>>> for i in range(3):
...     console.print(f"Processing item {i+1}...")
>>> console.print("Script finished.")
Interactive Usage:
>>> import sidekick
>>> console = sidekick.Console(show_input=True, initial_text="Enter command:")
>>>
>>> def handle_command(user_text):
...     console.print(f"Received: '{user_text}'")
...     if user_text.lower() == 'quit':
...         sidekick.shutdown() # Example: stop the script
...
>>> console.input_text_handler(handle_command)
>>> sidekick.run_forever() # Keep script running to listen for input
class sidekick.console.Console(instance_id: str | None = None, spawn: bool = True, initial_text: str = '', show_input: bool = False)[source]

Bases: BaseModule

Represents a Console module instance in the Sidekick UI panel.

Creates a scrollable text area for displaying output from your script via its print() method. Optionally includes a text input field at the bottom to receive input from the user in the Sidekick panel.

target_id

The unique identifier for this console instance.

Type:

str

__init__(instance_id: str | None = None, spawn: bool = True, initial_text: str = '', show_input: bool = False)[source]

Initializes the Console object and optionally creates the UI element.

Sets up the console and configures its appearance (e.g., whether to show an input field). Establishes the connection to Sidekick if not already done.

Parameters:
  • instance_id (Optional[str]) –

    A specific ID for this console instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”console-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing console element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new console UI element. If False, the library assumes a console element with the given instance_id already exists, and this Python object simply connects to it. initial_text and show_input arguments are ignored if spawn=False.

  • initial_text (str) – A line of text to display immediately when the console UI element is first created. Only used if spawn=True. Defaults to an empty string “”.

  • show_input (bool) – If True, an input field and submit button are included at the bottom of the console UI, allowing the user to type and send text back to the script (handled via on_input_text()). If False (default), only the text output area is shown. Only used if spawn=True.

Raises:
  • ValueError – If spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Simple output-only console
>>> log_console = sidekick.Console()
>>> log_console.print("Program log started.")
>>>
>>> # Interactive console waiting for user input
>>> interactive_console = sidekick.Console(show_input=True, initial_text="Enter your name:")
>>> # (Requires using interactive_console.on_input_text() and sidekick.run_forever())
on_input_text(callback: Callable[[str], None] | None)[source]

Registers a function to call when the user submits text via the input field.

This method is only relevant if you created the console with show_input=True. When the user types text into the input box in the Sidekick UI panel and then presses Enter (or clicks the associated submit button), the callback function you provide here will be executed within your running Python script.

Parameters:

callback (Optional[Callable[[str], None]]) – The function to call when text is submitted. This function must accept one argument: a string containing the exact text entered and submitted by the user. Pass None to remove any previously registered callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> import sidekick
>>> console = sidekick.Console(show_input=True)
>>>
>>> def process_user_input(text_from_user):
...     console.print(f"Processing command: {text_from_user}")
...     # Add logic here based on the input...
...     if text_from_user.lower() == "quit":
...         console.print("Exiting now.")
...         sidekick.shutdown() # Stop run_forever
...
>>> console.input_text_handler(process_user_input)
>>> console.print("Enter commands below (type 'quit' to exit):")
>>>
>>> # Keep the script running to listen for the user's input!
>>> sidekick.run_forever()
print(*args: Any, sep: str = ' ', end: str = '\n')[source]

Prints messages to this console instance in the Sidekick UI.

Works very much like Python’s built-in print() function. It converts all positional arguments (args) to their string representations, joins them together using the sep string as a separator, and finally appends the end string (which defaults to a newline character n, causing each call to typically start on a new line in the console).

The resulting string is then sent to the Sidekick UI to be appended to the console’s text area.

Parameters:
  • *args (Any) – One or more objects to print. They will be automatically converted to strings using str().

  • sep (str) – The separator string inserted between multiple arguments. Defaults to a single space (’ ‘).

  • end (str) – The string appended at the very end, after all arguments and separators. Defaults to a newline character (’n’). Set this to end=’’ to print without starting a new line afterwards.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Examples

>>> console = sidekick.Console()
>>> name = "World"
>>> count = 5
>>> console.print("Hello,", name, "!") # Output: Hello, World !
>>> console.print("Count:", count, sep='=') # Output: Count:=5
>>> console.print("Processing...", end='') # Prints without a newline
>>> console.print("Done.") # Prints on the same line as "Processing..."
clear()[source]

Removes all previously printed text from this console instance in Sidekick.

This effectively empties the console’s text area in the UI panel.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> console = sidekick.Console()
>>> console.print("Message 1")
>>> console.print("Message 2")
>>> import time; time.sleep(1) # Wait a second
>>> console.clear() # The console in Sidekick becomes empty
>>> console.print("Cleared!")

sidekick.control module

Provides the Control class for adding interactive UI elements to Sidekick.

Use the sidekick.Control class to create a dedicated panel within the Sidekick UI where you can programmatically add simple user interface controls like buttons and labeled text input fields.

This allows your Python script to present interactive options to the user directly within the Sidekick panel, enabling you to:

  • Create buttons (add_button) that trigger specific Python functions when clicked in the Sidekick UI.

  • Add text input fields (add_text_input) where users can type information, which is then sent back to your Python script for processing when they click the associated submit button.

Use the on_click() and on_input_text() methods to register callback functions that define how your Python script should react to these user interactions.

Example Scenario: Imagine building a simple simulation. You could use a Control panel to add:

  • A “Start Simulation” button.

  • A “Reset Simulation” button.

  • A text input field labeled “Enter Speed:” with a “Set Speed” button.

Your Python script would then use on_click and on_input_text to run the appropriate simulation logic or update speed variables when the user interacts with these controls in Sidekick.

class sidekick.control.Control(instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents a Control panel module instance in the Sidekick UI.

This class creates a container in the Sidekick panel specifically designed to hold simple interactive controls added dynamically from your Python script. Use its methods (add_button, add_text_input) to populate the panel, and on_click / on_input_text to define the Python functions that respond to user interactions with those controls.

target_id

The unique identifier for this control panel instance.

Type:

str

__init__(instance_id: str | None = None, spawn: bool = True)[source]

Initializes the Control panel object and optionally creates the UI element.

Sets up an empty panel ready to receive controls via methods like add_button. Establishes the connection to Sidekick if not already done.

Parameters:
  • instance_id (Optional[str]) –

    A specific ID for this control panel instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”control-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing control panel element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new, empty control panel UI element. If False, the library assumes a panel with the given instance_id already exists, and this Python object simply connects to it.

Raises:
  • ValueError – If spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Create a new panel to hold buttons and inputs
>>> sim_controls = sidekick.Control(instance_id="simulation-controls")
>>> sim_controls.add_button("start_btn", "Run Simulation")
>>>
>>> # Attach to an existing panel (e.g., created by another script)
>>> existing_panel = sidekick.Control(instance_id="shared-controls", spawn=False)
on_click(callback: Callable[[str], None] | None)[source]

Registers a function to call when any button within this panel is clicked.

When a user clicks a button (previously added using add_button()) in the Sidekick UI panel that belongs to this Control instance, the callback function you provide here will be executed within your running Python script.

Parameters:

callback (Optional[Callable[[str], None]]) – The function to call when any button in this panel is clicked. This function must accept one argument: control_id (str): The unique identifier (the same ID you provided when calling add_button) of the specific button that was clicked. Pass None to remove any previously registered click callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> controls = sidekick.Control()
>>> controls.add_button("action_one", "Perform Action 1")
>>> controls.add_button("action_two", "Do Something Else")
>>>
>>> def button_handler(button_that_was_clicked):
...     print(f"Button clicked: {button_that_was_clicked}")
...     if button_that_was_clicked == "action_one":
...         # Run action 1 logic...
...         print("Running action 1...")
...     elif button_that_was_clicked == "action_two":
...         # Run action 2 logic...
...         print("Doing something else...")
...
>>> controls.on_click(button_handler)
>>> sidekick.run_forever() # Keep script running
on_input_text(callback: Callable[[str, str], None] | None)[source]

Registers a function to call when text is submitted from any text input field within this panel.

When a user types text into an input field (previously added using add_text_input()) within this Control instance’s panel and clicks its associated “Submit” button in the Sidekick UI, the callback function you provide here will be executed in your running Python script.

Parameters:

callback (Optional[Callable[[str, str], None]]) –

The function to call when text is submitted from any input field in this panel. This function must accept two arguments: 1. control_id (str): The unique identifier (the same ID you

provided when calling add_text_input) of the specific text input group whose submit button was clicked.

  1. value (str): The text string that the user had entered into the input field when they submitted it.

Pass None to remove any previously registered input text callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> controls = sidekick.Control()
>>> controls.add_text_input("param_a", placeholder="Parameter A")
>>> controls.add_text_input("param_b", placeholder="Parameter B", button_text="Set B")
>>>
>>> def input_handler(input_field_id, submitted_value):
...     print(f"Input received from '{input_field_id}': '{submitted_value}'")
...     if input_field_id == "param_a":
...         # Process parameter A...
...         print(f"Setting A to {submitted_value}")
...     elif input_field_id == "param_b":
...         # Process parameter B...
...         print(f"Setting B to {submitted_value}")
...
>>> controls.input_text_handler(input_handler)
>>> sidekick.run_forever() # Keep script running
add_button(control_id: str, button_text: str)[source]

Adds a clickable button to this control panel in the Sidekick UI.

Creates a button element within this Control instance’s panel area. Clicking this button in the Sidekick UI will trigger the function registered using on_click(), passing this button’s unique control_id to the callback function.

Parameters:
  • control_id (str) – A unique identifier string for this specific button within this control panel. This ID is chosen by you and is crucial for identifying which button was pressed in the on_click callback. It must be a non-empty string.

  • button_text (str) – The text label that will appear visibly on the button in the UI.

Raises:

Example

>>> controls = sidekick.Control()
>>> controls.add_button("start_sim", "Start Simulation")
>>> controls.add_button("reset_sim", "Reset")
>>> # Add an on_click handler to react to these buttons...
add_text_input(control_id: str, placeholder: str = '', initial_value: str = '', button_text: str = '')[source]

Adds a text input field paired with a submit button to the control panel.

Creates a combined UI element in the Sidekick panel consisting of a text entry box and an adjacent button (labeled with button_text). When the user types text into the field and clicks the associated submit button, the function registered using on_input_text() is triggered. That callback function receives both this control’s control_id and the text the user entered.

Parameters:
  • control_id (str) – A unique identifier string for this specific text input group (the field + button combination) within this control panel. This ID is passed to the on_input_text callback. Must be a non-empty string.

  • placeholder (str) – Optional text displayed faintly inside the input field when it’s empty, providing a hint to the user (e.g., “Enter name”). Defaults to “”.

  • initial_value (str) – Optional text pre-filled in the input field when it first appears in the UI. Defaults to “”.

  • button_text (str) – Optional text label displayed on the submit button that’s associated with this input field.

Raises:

Example

>>> controls = sidekick.Control()
>>> # Add a simple text input with a placeholder
>>> controls.add_text_input("search_term", placeholder="Enter search query...")
>>>
>>> # Add an input with an initial value and a custom button label
>>> controls.add_text_input("server_ip", initial_value="192.168.1.1", button_text="Set IP")
>>> # Add an on_input_text handler to react to these inputs...
remove_control(control_id: str)[source]

Removes a specific control (button or text input) from this panel in the UI.

Use this method to dynamically remove a button or text input field that was previously added using add_button() or add_text_input(). You must provide the same unique control_id that you used when adding the control.

Parameters:

control_id (str) – The unique identifier string of the control element (button or text input group) that you want to remove from the Sidekick UI panel. Must be a non-empty string matching the ID used during creation.

Raises:

Example

>>> controls = sidekick.Control()
>>> controls.add_button("temporary_task", "Run Once")
>>> # ... some logic runs ...
>>> # Now remove the button as it's no longer needed
>>> controls.remove_control("temporary_task")
>>>
>>> controls.add_text_input("user_pin", "PIN")
>>> # ... user enters PIN and it's processed ...
>>> controls.remove_control("user_pin") # Remove the input field

sidekick.grid module

Provides the Grid class for creating interactive 2D grids in Sidekick.

Use the sidekick.Grid class to create a grid of rectangular cells (like a spreadsheet, checkerboard, or pixel display) within the Sidekick panel. You can programmatically control the background color and display text within each individual cell of the grid from your Python script.

This module is particularly useful for:

  • Visualizing 2D Data: Representing game maps, matrices, or cellular automata states.

  • Simple Graphics: Creating pixel art or basic pattern displays.

  • Interactive Elements: Building simple interactive boards or simulations where the user can click on cells to trigger actions or provide input to your Python script (using on_click()).

Coordinate System:

Methods like set_color, set_text, clear_cell, and the on_click callback use (x, y) coordinates to identify specific cells within the grid:

  • x represents the column index, starting from 0 for the leftmost column.

  • y represents the row index, starting from 0 for the topmost row.

So, (0, 0) is the top-left cell.

Basic Usage:
>>> import sidekick
>>> # Create a small 4x3 grid
>>> my_grid = sidekick.Grid(num_columns=4, num_rows=3)
>>>
>>> # Set the background color of the top-left cell to blue
>>> my_grid.set_color(x=0, y=0, color='blue')
>>>
>>> # Put the text "Hi" in the cell at column 1, row 2 (bottom middle)
>>> my_grid.set_text(x=1, y=2, text='Hi')
>>>
>>> # Clear the content and color of the top-left cell
>>> my_grid.clear_cell(x=0, y=0)
Interactive Usage:
>>> import sidekick
>>> grid = sidekick.Grid(5, 5)
>>>
>>> def user_clicked_cell(x, y):
...     print(f"User clicked cell at column {x}, row {y}")
...     grid.set_text(x, y, "Clicked!") # Show feedback in the grid
...
>>> grid.on_click(user_clicked_cell)
>>> print("Click on the grid cells in the Sidekick panel!")
>>> sidekick.run_forever() # Keep script alive to listen for clicks
class sidekick.grid.Grid(num_columns: int, num_rows: int, instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents an interactive Grid module instance in the Sidekick UI.

Instantiate this class to create a grid of cells with specified dimensions in the Sidekick panel. You can then manipulate individual cells using methods like set_color(), set_text(), clear_cell(), or clear the entire grid with clear(). Use on_click() to register a function that responds to user clicks on the grid cells.

target_id

The unique identifier for this grid instance.

Type:

str

num_columns

The number of columns this grid has (read-only).

Type:

int

num_rows

The number of rows this grid has (read-only).

Type:

int

__init__(num_columns: int, num_rows: int, instance_id: str | None = None, spawn: bool = True)[source]

Initializes the Grid object and optionally creates the UI element.

Sets up the grid’s dimensions and prepares it for interaction. Establishes the connection to Sidekick if not already done (this might block).

Parameters:
  • num_columns (int) – The number of columns the grid should have. Must be a positive integer (greater than 0).

  • num_rows (int) – The number of rows the grid should have. Must be a positive integer (greater than 0).

  • instance_id (Optional[str]) –

    A specific ID for this grid instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”grid-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing grid element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new grid UI element with the specified dimensions. If False, the library assumes a grid element with the given instance_id already exists, and this Python object simply connects to it. The num_columns and num_rows arguments are still validated locally when spawn=False but are not sent in the (empty) spawn command.

Raises:
  • ValueError – If num_columns or num_rows are not positive integers, or if spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Create a standard 8x8 grid
>>> board_grid = sidekick.Grid(num_columns=8, num_rows=8)
>>>
>>> # Create a wide grid
>>> wide_grid = sidekick.Grid(20, 5)
>>>
>>> # Attach to an existing grid named "level-map" (assume it's 30x20)
>>> map_control = sidekick.Grid(instance_id="level-map", spawn=False,
...                             num_columns=30, num_rows=20)
property num_columns: int

The number of columns in the grid (read-only).

Type:

int

property num_rows: int

The number of rows in the grid (read-only).

Type:

int

on_click(callback: Callable[[int, int], None] | None)[source]

Registers a function to call when the user clicks on any cell in this grid.

When the user clicks a cell within this grid in the Sidekick UI panel, the callback function you provide here will be executed within your running Python script.

Parameters:

callback (Optional[Callable[[int, int], None]]) – The function to call when a cell is clicked. This function must accept two integer arguments: 1. x (int): The column index (0-based, from left) of the clicked cell. 2. y (int): The row index (0-based, from top) of the clicked cell. Pass None to remove any previously registered click callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> import sidekick
>>> grid = sidekick.Grid(10, 10)
>>>
>>> def paint_cell(column, row):
...     print(f"Painting cell at ({column}, {row})")
...     grid.set_color(column, row, "lightblue") # Change color on click
...
>>> grid.on_click(paint_cell)
>>> print("Click on the grid cells in Sidekick to paint them!")
>>>
>>> # Keep the script running to listen for clicks!
>>> sidekick.run_forever()
set_color(x: int, y: int, color: str | None)[source]

Sets the background color of a specific cell in the grid.

Changes the visual appearance of a single cell (square) in the grid UI. This operation does not affect any text that might be displayed in the cell.

Parameters:
  • x (int) – The column index (0 to num_columns - 1) of the target cell.

  • y (int) – The row index (0 to num_rows - 1) of the target cell.

  • color (Optional[str]) – The desired background color for the cell. This should be a string representing a color in a standard CSS format, such as: - Color names: ‘red’, ‘blue’, ‘lightgray’ - Hex codes: ‘#FF0000’, ‘#00F’, ‘#abcdef’ - RGB: ‘rgb(0, 255, 0)’, ‘rgba(0, 0, 255, 0.5)’ (for transparency) - HSL: ‘hsl(120, 100%, 50%)’, ‘hsla(240, 50%, 50%, 0.7)’ Pass None to clear any custom background color previously set for this cell, resetting it to the default grid cell background.

Raises:
  • IndexError – If the provided x or y coordinates are outside the valid range of columns (0 to num_columns-1) or rows (0 to num_rows-1).

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(4, 4)
>>> # Set cell (0, 0) (top-left) to red
>>> grid.set_color(0, 0, "red")
>>> # Set cell (3, 3) (bottom-right) to semi-transparent green
>>> grid.set_color(3, 3, "rgba(0, 255, 0, 0.5)")
>>> # Clear the color of cell (0, 0), reverting it to default
>>> grid.set_color(0, 0, None)
set_text(x: int, y: int, text: str | None)[source]

Sets the text content displayed inside a specific cell in the grid.

Places the given text string within the boundaries of the specified cell in the Sidekick UI. Setting text does not affect the cell’s background color. If the text is too long to fit, the UI might truncate it or handle overflow.

Parameters:
  • x (int) – The column index (0 to num_columns - 1) of the target cell.

  • y (int) – The row index (0 to num_rows - 1) of the target cell.

  • text (Optional[str]) – The text string to display within the cell. Any Python object provided will be converted to its string representation using str(). If you pass None or an empty string “”, any existing text currently displayed in the specified cell will be cleared.

Raises:
  • IndexError – If the provided x or y coordinates are outside the valid range of columns (0 to num_columns-1) or rows (0 to num_rows-1).

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(3, 3)
>>> # Put an "X" in the center cell (1, 1)
>>> grid.set_text(1, 1, "X")
>>> # Put a number in the top-right cell (2, 0)
>>> count = 5
>>> grid.set_text(2, 0, str(count)) # Convert number to string
>>> # Clear the text from the center cell
>>> grid.set_text(1, 1, None)
>>> # Setting empty string also clears text
>>> grid.set_text(2, 0, "")
clear_cell(x: int, y: int)[source]

Clears both the background color and the text content of a specific cell.

Resets the specified cell (x, y) back to its default visual state, removing any custom background color set by set_color() and any text set by set_text().

Parameters:
  • x (int) – The column index (0 to num_columns - 1) of the cell to clear.

  • y (int) – The row index (0 to num_rows - 1) of the cell to clear.

Raises:
  • IndexError – If the provided x or y coordinates are outside the valid grid boundaries.

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(4, 4)
>>> grid.set_color(3, 3, "purple")
>>> grid.set_text(3, 3, "Value")
>>> # Reset cell (3, 3) completely to its default state
>>> grid.clear_cell(3, 3)
clear()[source]

Clears the entire grid, resetting all cells to their default state.

Removes all custom background colors and all text content from every cell in the grid simultaneously. The grid will appear visually empty, as if it was just created.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(5, 5)
>>> grid.set_color(0, 0, "red")
>>> grid.set_text(1, 1, "Hi")
>>> grid.set_color(4, 4, "blue")
>>> # ... other manipulations ...
>>> # Now clear the entire grid back to its initial state
>>> grid.clear()

sidekick.observable_value module

Provides the ObservableValue class for making Sidekick visualizations reactive.

This module contains the ObservableValue class, a special wrapper you can use around your regular Python lists, dictionaries, or sets.

Why use it?

The main purpose of ObservableValue is to work hand-in-hand with the sidekick.Viz module. When you display an ObservableValue using viz.show(), the Viz panel in Sidekick gains a superpower: it automatically updates its display whenever you modify the data inside the ObservableValue.

How it works:

  1. Wrap your data: my_list = sidekick.ObservableValue([1, 2])

  2. Show it in Viz: viz.show(“My List”, my_list)

  3. Modify the data using the wrapper’s methods: * my_list.append(3) * my_list[0] = 99 * del my_list[1]

  4. Observe: The Viz panel in Sidekick updates instantly to show these changes, often highlighting exactly what was modified.

This makes it much easier to track how your data structures evolve during your script’s execution without needing to manually call viz.show() after every single change.

Note on Limitations:

  • Automatic updates only occur when you modify the data through the ObservableValue wrapper’s methods (like .append(), [key]=value, .add()). Changes made directly to the underlying object obtained via .get() might not be detected automatically.

  • For nested structures (e.g., a list inside a dictionary), you would need to wrap the inner mutable structures with ObservableValue as well if you want their internal changes to trigger automatic updates.

  • Changes made by directly setting attributes on a wrapped custom object are generally not detected automatically.

class sidekick.observable_value.ObservableValue(initial_value: Any)[source]

Bases: object

Wraps a Python value (list, dict, set) to notify subscribers about changes.

Use this class to make your data “reactive” when displayed using the sidekick.Viz module. By wrapping mutable data structures (lists, dictionaries, sets) in an ObservableValue, you enable the Viz panel in Sidekick to update its display automatically whenever you modify the wrapped data through this wrapper object.

This is achieved by intercepting common modification methods (like append, __setitem__, update, add, clear, etc.) and sending notifications after the operation completes.

Basic Usage:
>>> import sidekick
>>> viz = sidekick.Viz() # Assuming Viz panel is ready
>>>
>>> # Wrap a list
>>> shopping_list = sidekick.ObservableValue(['apples', 'bananas'])
>>> viz.show("Groceries", shopping_list)
>>>
>>> # Modify through the wrapper - Viz updates automatically!
>>> shopping_list.append('carrots')
>>> shopping_list[0] = 'blueberries'
>>>
>>> # Wrap a dictionary
>>> config = sidekick.ObservableValue({'theme': 'dark', 'autosave': False})
>>> viz.show("Settings", config)
>>>
>>> # Modify through the wrapper - Viz updates automatically!
>>> config['autosave'] = True
>>> config.update({'fontSize': 12})
>>> del config['theme']

Key Benefit: Simplifies tracking data changes visually in Sidekick, as you don’t need repeated calls to viz.show() after each modification to an observed value.

See the module docstring for important limitations regarding direct modification of unwrapped values or attributes of wrapped custom objects.

__init__(initial_value: Any)[source]

Initializes the ObservableValue by wrapping the provided Python value.

Parameters:

initial_value – The Python value (e.g., a list, dict, set, number, string, etc.) that you want to make observable.

subscribe(callback: Callable[[Dict[str, Any]], None]) Callable[[], None][source]

Registers a function to be called whenever the wrapped value changes. (Internal).

This method is primarily intended for internal use by the sidekick.Viz module. When viz.show() is called with an ObservableValue, Viz uses this method to register its own internal handler (_handle_observable_update).

When a change occurs to the wrapped value (triggered by methods like append(), __setitem__(), set(), etc.), the callback function provided here is executed with a dictionary containing details about that specific change (e.g., type of change, path, new value).

Parameters:

callback – A function that accepts one argument: a dictionary describing the change event (common keys include ‘type’, ‘path’, ‘value’, ‘key’, ‘old_value’, ‘length’).

Returns:

A function that, when called with no arguments,

will remove this specific callback from the subscription list. This allows Viz to clean up its listener when the variable is removed from the display or the Viz panel itself is removed.

Return type:

UnsubscribeFunction

Raises:

TypeError – If the provided callback is not a callable function.

unsubscribe(callback: Callable[[Dict[str, Any]], None])[source]

Removes a previously registered callback function. (Internal).

Typically called by the unsubscribe function returned from subscribe, or potentially directly by Viz during cleanup.

Parameters:

callback – The specific callback function instance to remove from the set of subscribers.

get() Any[source]

Returns the actual underlying Python value being wrapped by this ObservableValue.

Use this method when you need direct access to the original list, dictionary, set, or other object stored inside, without the ObservableValue wrapper’s notification logic.

Be cautious: Modifying mutable objects obtained via get() directly (e.g., my_obs_list.get().append(item)) will not trigger automatic notifications to subscribers like sidekick.Viz. For automatic updates, always modify through the ObservableValue wrapper itself (my_obs_list.append(item)).

Returns:

The actual Python object currently stored within the wrapper.

Example

>>> obs_list = sidekick.ObservableValue([10, 20, 30])
>>> raw_list = obs_list.get()
>>> print(raw_list)
[10, 20, 30]
>>> print(type(raw_list))
<class 'list'>
>>> # Modifying raw_list directly does NOT notify Viz
>>> raw_list.pop()
30
>>> # Modifying through the wrapper DOES notify Viz
>>> obs_list.pop() # Viz will update
set(new_value: Any)[source]

Replaces the currently wrapped value with a completely new value.

This method is used when you want to assign a fundamentally different object (e.g., a new list, a different dictionary, a number instead of a list) to this observable variable, rather than just modifying the contents of the existing wrapped object.

It triggers a “set” notification to all subscribers, indicating that the entire value has been replaced.

Note

If the new_value you provide is the exact same object in memory as the currently wrapped value (i.e., new_value is self.get()), this method will do nothing and send no notification, as the value hasn’t conceptually changed from the wrapper’s perspective.

Parameters:

new_value – The new Python object to wrap and observe going forward.

Example

>>> obs_data = sidekick.ObservableValue({"status": "pending"})
>>> viz.show("Job Status", obs_data) # Show initial state
>>>
>>> # Replace the entire dictionary
>>> final_status = {"status": "complete", "result": 123}
>>> obs_data.set(final_status) # Viz panel updates to show the new dict
>>>
>>> # Further modifications through obs_data now affect final_status
>>> obs_data["timestamp"] = time.time() # Viz updates again
append(item: Any)[source]

Appends an item to the end of the wrapped list/sequence and notifies subscribers.

This method mimics the behavior of list.append(). It requires the wrapped value (self.get()) to be a mutable sequence (like a standard Python list).

Raises:
  • AttributeError – If the wrapped value does not have an append method.

  • TypeError – If the wrapped value is not a list or similar mutable sequence (though usually caught by AttributeError first).

Example

>>> items = sidekick.ObservableValue(['a', 'b'])
>>> viz.show("Items", items)
>>> items.append('c') # Viz automatically updates to show ['a', 'b', 'c']
insert(index: int, item: Any)[source]

Inserts an item at a specific index in the wrapped list/sequence and notifies subscribers.

Mimics list.insert(). Requires the wrapped value to be a mutable sequence.

Parameters:
  • index (int) – The index at which to insert the item.

  • item (Any) – The item to insert.

Raises:
  • AttributeError – If the wrapped value does not have an insert method.

  • TypeError – If the wrapped value is not a list or similar.

  • IndexError – If the index is out of range for insertion (behavior matches list.insert).

pop(index: int = -1) Any[source]

Removes and returns the item at the given index (default last) and notifies subscribers.

Mimics list.pop(). Requires the wrapped value to be a mutable sequence.

Parameters:

index (int) – The index of the item to remove. Defaults to -1 (the last item).

Returns:

The item that was removed from the list.

Raises:
  • AttributeError – If the wrapped value does not have a pop method.

  • TypeError – If the wrapped value is not a list or similar.

  • IndexError – If the list is empty or the index is out of range.

remove(value: Any)[source]

Removes the first occurrence of a given value from the wrapped list/sequence and notifies subscribers.

Mimics list.remove(). Requires the wrapped value to be a mutable sequence. If the value is not found, it does nothing (and sends no notification), matching the standard list.remove() behavior.

Parameters:

value (Any) – The value to search for and remove the first instance of.

Raises:
  • AttributeError – If the wrapped value does not have a remove method.

  • TypeError – If the wrapped value is not a list or similar.

  • ValueError – While this method catches the ValueError from list.remove (when the value isn’t found) and does nothing, the underlying list.index call used for notification could raise it if the value disappears between the check and the call (highly unlikely).

clear()[source]

Removes all items from the wrapped container (list, dict, set) and notifies subscribers.

Requires the wrapped value to have a callable clear() method.

Raises:
  • AttributeError – If the wrapped object does not have a clear method.

  • TypeError – If the wrapped object’s clear attribute is not callable.

__setitem__(key: Any, value: Any)[source]

Sets the value for a key/index in the wrapped container (dict/list) and notifies subscribers.

This method intercepts the standard Python square bracket assignment syntax: my_observable[key] = value

It performs the assignment operation on the wrapped object (self._value) and then, if successful, triggers a “setitem” notification to inform subscribers (like Viz) about the change. This works for both dictionary key assignment (my_dict[key] = val) and list index assignment (my_list[index] = val).

Parameters:
  • key (Any) – The key (for dictionaries) or index (for lists) to assign to.

  • value (Any) – The new value to associate with the key/index.

Raises:
  • TypeError – If the wrapped value does not support item assignment (e.g., if it’s a set, tuple, or immutable object).

  • IndexError – If the wrapped value is a list and the index is out of range.

  • KeyError – Typically not raised by assignment itself, but underlying checks might.

__delitem__(key: Any)[source]

Deletes an item/key from the wrapped container (dict/list) and notifies subscribers.

Intercepts the standard Python del statement used with square brackets: del my_observable[key]

It performs the deletion operation on the wrapped object (self._value) and then, if successful, triggers a “delitem” notification. Works for deleting dictionary keys or list items by index.

Parameters:

key (Any) – The key (for dictionaries) or index (for lists) to delete.

Raises:
  • TypeError – If the wrapped value does not support item deletion.

  • KeyError – If the wrapped value is a dictionary and the key is not found.

  • IndexError – If the wrapped value is a list and the index is out of range.

update(other: Dict[Any, Any] | Mapping[Any, Any] | Iterable[Tuple[Any, Any]] = {}, **kwargs: Any)[source]

Updates the wrapped dictionary with key-value pairs from another mapping and/or keyword arguments, notifying subscribers for each change.

Mimics the behavior of dict.update(). It iterates through the items to be added or updated and uses the intercepted __setitem__ method for each one. This ensures that subscribers (like Viz) receive individual “setitem” notifications for every key that is added or whose value is changed, allowing for more granular UI updates compared to a single bulk update notification.

Requires the wrapped value (self.get()) to be a mutable mapping (like a standard Python dict).

Parameters:
  • other – Can be another dictionary, an object implementing the Mapping protocol, or an iterable of key-value pairs (like [(‘a’, 1), (‘b’, 2)]). Keys and values from other will be added/updated in the wrapped dict.

  • **kwargs – Keyword arguments are treated as additional key-value pairs to add or update in the wrapped dictionary.

Raises:
  • AttributeError – If the wrapped value does not behave like a dictionary (no __setitem__).

  • TypeError – If the wrapped value is not a dictionary or similar mutable mapping, or if the other argument is not a valid source for updates.

Example

>>> settings = sidekick.ObservableValue({'font': 'Arial', 'size': 10})
>>> viz.show("Settings", settings)
>>>
>>> # Update using another dictionary
>>> settings.update({'size': 12, 'theme': 'dark'}) # Sends 2 notifications
>>> # Update using keyword arguments
>>> settings.update(line_numbers=True, theme='light') # Sends 2 notifications (theme overwritten)
add(element: Any)[source]

Adds an element to the wrapped set and notifies subscribers if the element was not already present.

Mimics set.add(). Requires the wrapped value to be a mutable set. If the element is already in the set, this method does nothing (and sends no notification), matching standard set behavior.

Parameters:

element (Any) – The element to add to the set.

Raises:
  • AttributeError – If the wrapped value does not have an add method or is not a set.

  • TypeError – If the wrapped value is not a set or similar mutable set.

discard(element: Any)[source]

Removes an element from the wrapped set if it is present, and notifies subscribers if removal occurred.

Mimics set.discard(). Requires the wrapped value to be a mutable set. If the element is not found in the set, this method does nothing (and sends no notification), matching standard set behavior.

Parameters:

element (Any) – The element to remove from the set.

Raises:
  • AttributeError – If the wrapped value does not have a discard method.

  • TypeError – If the wrapped value is not a set or similar.

__getattr__(name: str) Any[source]

Delegates attribute access to the wrapped value if the attribute is not internal.

This allows you to conveniently access methods and attributes of the wrapped object directly through the ObservableValue instance. For example, if obs_list = ObservableValue([1, 2]), calling obs_list.count(1) will correctly delegate to the underlying list’s count method.

It prevents direct access to the ObservableValue’s own internal attributes (like _value, _subscribers) via this delegation mechanism to avoid conflicts.

Parameters:

name (str) – The name of the attribute being accessed.

Returns:

The value of the attribute from the wrapped object.

Return type:

Any

Raises:

AttributeError – If the attribute name refers to an internal attribute of ObservableValue itself, or if the wrapped object does not have an attribute with the given name.

__setattr__(name: str, value: Any)[source]

Sets internal attributes or delegates attribute setting to the wrapped value.

This method controls how attribute assignment works on the ObservableValue instance:

  • If the name being assigned matches one of the ObservableValue’s own internal attributes (defined in _obs_internal_attrs, e.g., _value), it sets that internal attribute directly on the wrapper instance itself.

  • Otherwise (if name is not an internal attribute), it attempts to set the attribute with the given name and value directly on the wrapped object (self._value).

Important: Delegating attribute setting to the wrapped object via this method does not automatically trigger notifications to subscribers like Viz. If you need the Viz panel to update when you change an attribute of a wrapped custom object, you have several options:

  1. Call .set(self.get()) on the ObservableValue after modifying the wrapped object’s attribute(s). This forces a full “set” notification, telling Viz to re-render the entire object display.

  2. If the attribute itself holds mutable data (like a list), consider wrapping that attribute’s value in its own ObservableValue.

  3. If the custom object has its own notification mechanism, trigger it manually.

Parameters:
  • name (str) – The name of the attribute to set.

  • value (Any) – The value to assign to the attribute.

Raises:

AttributeError – If attempting to set an attribute on a wrapped object that doesn’t support attribute assignment (e.g., built-in types like int or list, or objects without __slots__ or __dict__ allowing the assignment).

__repr__() str[source]

Returns a string representation showing it’s an ObservableValue wrapping another value.

Example: ObservableValue([1, 2, 3])

__str__() str[source]

Returns the string representation of the wrapped value.

Allows str(my_observable) to behave the same as str(my_observable.get()).

__eq__(other: Any) bool[source]

Compares the wrapped value for equality.

Allows comparing an ObservableValue directly with another value or another ObservableValue. The comparison is performed on the underlying wrapped values.

Example

>>> obs1 = ObservableValue([1, 2])
>>> obs2 = ObservableValue([1, 2])
>>> obs1 == [1, 2] # True
>>> obs1 == obs2   # True
__len__() int[source]

Returns the length of the wrapped value, if the wrapped value supports len().

Allows using len(my_observable) just like len(my_observable.get()).

Raises:

TypeError – If the wrapped object type does not have a defined length (e.g., numbers, None, objects without __len__).

__getitem__(key: Any) Any[source]

Allows accessing items/keys of the wrapped value using square bracket notation ([]).

Enables syntax like my_observable[key] or my_observable[index] if the wrapped object (self.get()) supports item access (like lists, dictionaries, or custom objects implementing __getitem__).

Parameters:

key (Any) – The key or index to access within the wrapped object.

Returns:

The value associated with the key/index in the wrapped object.

Return type:

Any

Raises:
  • TypeError – If the wrapped object does not support item access (__getitem__).

  • KeyError – If the wrapped object is a dictionary and the key is not found.

  • IndexError – If the wrapped object is a list and the index is out of range.

__iter__() Iterable[Any][source]

Allows iterating over the wrapped value (e.g., in a for loop).

Enables syntax like for item in my_observable: if the wrapped object (self.get()) is itself iterable (like lists, dictionaries, sets, strings, or custom objects implementing __iter__).

Yields:

The items produced by iterating over the wrapped value.

Raises:

TypeError – If the wrapped object is not iterable.

__contains__(item: Any) bool[source]

Allows using the in operator to check for containment in the wrapped value.

Enables syntax like element in my_observable if the wrapped object (self.get()) supports containment checks (like lists, dictionaries checking keys, sets, strings, or custom objects implementing __contains__).

Parameters:

item (Any) – The item to check for containment within the wrapped value.

Returns:

True if the item is found in the wrapped value, False otherwise.

Return type:

bool

Raises:

TypeError – If the wrapped object does not support the in operator (typically requires __contains__ or being iterable).

sidekick.utils module

Internal Utility Functions for the Sidekick Library.

This module contains helper functions used internally by other parts of the Sidekick library.

Warning

Functions and variables in this module are considered internal implementation details. You should not need to import or use them directly in your scripts, as they might change without notice in future versions.

sidekick.utils.generate_unique_id(prefix: str) str[source]

Generates a simple, sequential unique ID for a module instance.

This function is used internally by Sidekick module classes (like Grid, Console, etc.) when you create an instance without providing your own specific instance_id. It ensures that each automatically generated ID is unique within the current script run, helping the library distinguish between different modules of the same type (e.g., multiple Grids).

The generated IDs follow a simple “prefix-number” format (e.g., “grid-1”, “console-2”).

Note

This is intended for internal library use. You should not rely on the specific format of these generated IDs in your code, as it could change. Always use the target_id attribute of a module instance if you need to reference its ID.

Parameters:

prefix (str) – A descriptive prefix indicating the type of module, such as “grid”, “console”, or “canvas”.

Returns:

A unique identifier string for the new instance (e.g., “grid-1”).

Return type:

str

sidekick.viz module

Provides the Viz class for visualizing Python variables in Sidekick.

Use the sidekick.Viz class to create an interactive, tree-like display of your Python variables within the Sidekick panel in VS Code. This is incredibly helpful for understanding the state and structure of your data, especially complex objects, lists, dictionaries, and sets, as your script executes.

Key Features:

  • Variable Inspection: Display almost any Python variable (int, str, list, dict, set, custom objects) using the show() method. The Viz panel presents nested structures in a collapsible tree view.

  • Reactivity (with ObservableValue): The most powerful feature! If you wrap your mutable data (lists, dicts, sets) in sidekick.ObservableValue before showing it (viz.show(“my_data”, sidekick.ObservableValue(data))), the Viz panel will automatically update its display whenever you modify the data through the ObservableValue wrapper. Changes are often highlighted, making it easy to see exactly what happened.

  • Clear Display: Handle large collections and deep nesting by truncating the display automatically. Detect and visualize recursive references to prevent infinite loops.

  • Variable Removal: Remove variables from the display when they are no longer needed using remove_variable().

Basic Usage (Static Variable):
>>> import sidekick
>>> viz = sidekick.Viz()
>>> my_config = {"user": "Alice", "settings": {"theme": "dark", "level": 5}}
>>> viz.show("App Config", my_config)
>>> # To update, you need to call show() again if my_config changes later
>>> my_config["settings"]["level"] = 6
>>> viz.show("App Config", my_config) # Manual update needed
Reactive Usage (with ObservableValue):
>>> import sidekick
>>> viz = sidekick.Viz()
>>> reactive_list = sidekick.ObservableValue([10, 20])
>>> viz.show("Reactive List", reactive_list)
>>>
>>> # Now, changes update Viz automatically!
>>> reactive_list.append(30)
>>> reactive_list[0] = 100
>>> # No need to call viz.show() again!

Internal Details:

This module also contains the complex internal logic (_get_representation) needed to convert arbitrary Python data structures into a specific JSON-like format that the Sidekick frontend UI component can understand and render interactively. This involves handling nesting, recursion, data types, and observability tracking.

class sidekick.viz.Viz(instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents the Variable Visualizer (Viz) module instance in the Sidekick UI.

Use this class to create an interactive panel in Sidekick where you can display Python variables and data structures. It presents data like lists, dictionaries, sets, and even custom objects in a collapsible tree view, making it easy to inspect their contents and structure as your script runs.

The key feature is its integration with sidekick.ObservableValue. When you display data wrapped in an ObservableValue using viz.show(), the Viz panel will automatically update its display whenever the underlying data is modified through the wrapper. This provides a powerful live view of how your data changes over time without requiring manual refreshes.

target_id

The unique identifier for this Viz panel instance.

Type:

str

__init__(instance_id: str | None = None, spawn: bool = True)[source]

Initializes the Viz object and optionally creates the UI panel.

Sets up the Viz panel instance. Establishes the connection to Sidekick if not already done (this might block).

Parameters:
  • instance_id (Optional[str]) –

    A specific ID for this Viz panel instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”viz-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing Viz panel element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new, empty Viz panel UI element. If False, the library assumes a panel with the given instance_id already exists, and this Python object simply connects to it.

Raises:
  • ValueError – If spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Create a new Viz panel
>>> data_viewer = sidekick.Viz()
>>>
>>> # Attach to an existing panel maybe named "debug-variables"
>>> debug_vars = sidekick.Viz(instance_id="debug-variables", spawn=False)
show(name: str, value: Any)[source]

Displays or updates a variable in the Sidekick Viz panel.

This is the primary method for sending data to the Viz panel. It shows the given Python value under the specified name in an interactive tree view.

  • If `name` is new: Adds the variable to the Viz panel display.

  • If `name` already exists: Updates the display for that variable to reflect the current state of the provided value.

Reactivity with `ObservableValue`:

The key feature is how show() interacts with sidekick.ObservableValue:

  • If the value you pass is wrapped in sidekick.ObservableValue (e.g., viz.show(“My List”, sidekick.ObservableValue([1, 2]))), the Viz panel will automatically subscribe to changes within that ObservableValue. Any subsequent modifications made through the wrapper (e.g., my_obs_list.append(3)) will automatically trigger updates in the Viz UI, without needing further calls to viz.show().

  • If the value is not an ObservableValue (e.g., a regular list, dict, number, string, or custom object), the Viz panel simply displays a snapshot of the value at the moment show() is called. If the underlying data changes later, you must call `viz.show(name, updated_value)` again with the same name to refresh the display in the Sidekick panel.

Parameters:
  • name (str) – The name to display for this variable in the Viz panel header (e.g., “my_list”, “game_state”, “loop_counter”). This acts as the unique identifier for the variable within this Viz instance. Must be a non-empty string.

  • value (Any) – The Python variable or value you want to visualize. This can be almost any Python object: primitives (int, float, str, bool, None), collections (list, dict, set, tuple), custom class instances, or, importantly, an ObservableValue wrapping one of these types.

Raises:
  • ValueError – If the provided name is empty or not a string.

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the initial ‘set’ command fails.

Example

>>> import sidekick
>>> viz = sidekick.Viz()
>>>
>>> # --- Showing a non-observable dictionary ---
>>> config = {"level": 1, "active": True}
>>> viz.show("Game Config", config)
>>> # If config changes later...
>>> config["active"] = False
>>> # ...Viz panel does NOT update automatically. Need to call show() again:
>>> viz.show("Game Config", config) # Manually update the display
>>>
>>> # --- Showing an observable list ---
>>> player_scores = sidekick.ObservableValue([100, 95])
>>> viz.show("Scores", player_scores)
>>> # Now, changes through the wrapper update Viz automatically:
>>> player_scores.append(110)
>>> player_scores[0] = 105
>>> # No need to call viz.show("Scores", player_scores) again!
remove_variable(name: str)[source]

Removes a previously shown variable from the Viz panel display.

Use this method when you no longer need to see a specific variable in the Sidekick Viz panel.

If the variable currently displayed under this name was an ObservableValue, this method also automatically unsubscribes from its changes, preventing further automatic updates for this removed variable and cleaning up resources.

Parameters:

name (str) – The exact name of the variable to remove. This must match the name used in the corresponding viz.show(name, …) call.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the removal command fails.

Example

>>> viz = sidekick.Viz()
>>> temporary_data = [1, 2, 3, 4, 5]
>>> viz.show("Intermediate Result", temporary_data)
>>> # ... process the data ...
>>> # Now remove it from the display
>>> viz.remove_variable("Intermediate Result")
remove()[source]

Removes the entire Viz panel instance from the Sidekick UI and cleans up resources.

Call this method when you are completely finished with this Viz panel. It performs the following actions:

  1. Unsubscribes: Iterates through all variables currently tracked by this Viz instance and, if any are `ObservableValue`s, calls their unsubscribe functions to stop listening for changes.

  2. Calls Base `remove()`: Invokes the BaseModule.remove() method, which: a. Unregisters the internal message handler for this Viz panel. b. Resets registered callbacks (on_error) to None. c. Sends the final ‘remove’ command to the Sidekick UI to delete the

    entire Viz panel element itself.

Raises:

SidekickConnectionError (or subclass) – Can occur if sending the final ‘remove’ command fails. Cleanup of local Python resources (subscriptions, handlers) will still be attempted.

Module contents

Sidekick Python Library (sidekick-py).

Welcome to the Sidekick Python library! This is your starting point for connecting your Python code to the Sidekick visual coding buddy, which typically runs inside the Visual Studio Code editor.

Sidekick helps you see your code come alive. Instead of just imagining what loops are doing or how data structures change, you can use this library to create visual representations and interactive elements directly within the Sidekick panel in VS Code.

What can you do with it?
  • Create interactive grids for maps, games, or data display (sidekick.Grid).

  • Show text output like print(), but in a dedicated Sidekick area, and even get text input back from the user (sidekick.Console).

  • Visualize your variables (lists, dictionaries, objects) and see how they change over time, automatically (sidekick.Viz with sidekick.ObservableValue).

  • Draw shapes, lines, and text on a 2D canvas (sidekick.Canvas).

  • Add clickable buttons and text input fields to trigger actions in your Python script (sidekick.Control).

These visual modules update in real-time as your Python code executes, making it easier to understand, debug, and demonstrate programming concepts.

Getting Started:
  1. Install: Make sure you have sidekick-py installed (pip install sidekick-py) and the Sidekick VS Code extension is installed and enabled.

  2. Open Panel: In VS Code, open the Sidekick panel (Ctrl+Shift+P, search for Sidekick: Show Panel).

  3. Import: Start your Python script with import sidekick.

  4. Create: Instantiate a visual module, e.g., grid = sidekick.Grid(5, 5). The connection to Sidekick happens automatically here!

  5. Interact: Use the module’s methods, e.g., grid.set_color(0, 0, ‘blue’).

  6. Listen (if needed): If you need to react to user clicks or input (using methods like grid.on_click(…)), you must keep your script running. Add sidekick.run_forever() at the end of your script. You can stop it by pressing Ctrl+C in the terminal.

Happy visual coding!

sidekick.set_url(url: str)[source]

Sets the WebSocket URL where the Sidekick server is expected to be listening.

You must call this function before creating any Sidekick modules (like sidekick.Grid()) or calling any other Sidekick function that might trigger a connection attempt (like sidekick.clear_all()). The library uses the URL set here when it makes its first connection attempt.

Calling this after a connection attempt has already started (even if it failed) will log a warning and have no effect, unless you explicitly call sidekick.shutdown() first to completely reset the connection state.

Parameters:

url (str) – The full WebSocket URL, which must start with “ws://” or “wss://”. The default value is “ws://localhost:5163”.

Raises:

ValueError – If the provided URL does not start with “ws://” or “wss://”.

Example

>>> import sidekick
>>> # If the Sidekick server is running on a different machine or port
>>> try:
...     sidekick.set_url("ws://192.168.1.100:5163")
... except ValueError as e:
...     print(e)
>>>
>>> # Now it's safe to create Sidekick modules
>>> console = sidekick.Console()
sidekick.set_config(clear_on_connect: bool = True, clear_on_disconnect: bool = False)[source]

Configures automatic clearing behavior for the Sidekick UI panel.

Like set_url, you must call this function before the first connection attempt is made (i.e., before creating any Sidekick modules). Calling it later will have no effect unless shutdown() is called first.

Parameters:
  • clear_on_connect (bool) – If True (the default), the library will automatically send a command to clear all existing elements from the Sidekick UI panel as soon as the connection becomes fully ready (i.e., when the UI panel signals it’s online and ready). This provides a clean slate for your script. Set this to False if you want your script to potentially add to or interact with UI elements left over from a previous script run (less common).

  • clear_on_disconnect (bool) – If True (default is False), the library will attempt (on a best-effort basis) to send a command to clear the Sidekick UI when your script disconnects cleanly. This happens when shutdown() is called explicitly or when the script exits normally (due to the atexit handler). This cleanup might not happen if the connection is lost abruptly or due to an error.

sidekick.clear_all()[source]

Sends a command to remove all visual elements from the Sidekick panel.

This effectively resets the Sidekick UI, removing any Grids, Consoles, Viz panels, Canvases, or Controls that were created by this running Python script instance.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or if sending the command fails.

sidekick.register_global_message_handler(handler: Callable[[Dict[str, Any]], None] | None)[source]

Registers a single function to receive all incoming messages from Sidekick.

Advanced Usage / Debugging: This function is primarily intended for debugging the communication protocol or building very custom, low-level integrations. The function you provide (handler) will be called by the listener thread for every message received from the Sidekick server, before the message is dispatched to any specific module instance handlers.

Warning

The structure and content of messages received here are subject to the internal Sidekick communication protocol and may change between versions. Relying on this for core application logic is generally discouraged.

Parameters:

handler (Optional[Callable[[Dict[str, Any]], None]]) – The function to call with each raw message dictionary received from the WebSocket. It should accept one argument (the message dict). Pass None to remove any currently registered global handler.

Raises:

TypeError – If the provided handler is not a callable function and not None.

sidekick.run_forever()[source]

Keeps your Python script running indefinitely to handle Sidekick UI events.

If your script needs to react to interactions in the Sidekick panel (like button clicks, grid cell clicks, console input, etc.), it needs to stay alive to listen for those events. Calling sidekick.run_forever() at the end of your script achieves this.

It essentially pauses the main thread of your script in a loop, while the background listener thread (managed internally) continues to receive messages from Sidekick and trigger your registered callback functions (e.g., the functions passed to grid.on_click() or console.on_input_text()).

How to Stop run_forever():

  1. Press Ctrl+C in the terminal where your script is running.

  2. Call sidekick.shutdown() from within one of your callback functions (e.g., have a “Quit” button call sidekick.shutdown in its on_click handler).

  3. If the connection to Sidekick breaks unexpectedly, run_forever() will also stop (typically after a SidekickDisconnectedError is raised).

Raises:

SidekickConnectionError (or subclass) – If the initial connection to Sidekick cannot be established when run_forever starts. The script won’t enter the waiting loop if it can’t connect first.

Example

>>> import sidekick
>>> console = sidekick.Console(show_input=True)
>>> def handle_input(text):
...     if text.lower() == 'quit':
...         console.print("Exiting...")
...         sidekick.shutdown() # Stop run_forever from callback
...     else:
...         console.print(f"You typed: {text}")
>>> console.input_text_handler(handle_input)
>>> console.print("Enter text or type 'quit' to exit.")
>>>
>>> # Keep script running to listen for input
>>> sidekick.run_forever()
>>> print("Script has finished.") # This line runs after run_forever exits
sidekick.shutdown()[source]

Initiates a clean shutdown of the Sidekick connection and resources.

Call this function to gracefully disconnect from the Sidekick panel. It performs the following actions: - Signals run_forever() (if it’s currently running) to stop its waiting loop. - Attempts to send a final ‘offline’ announcement to the Sidekick server. - Attempts to send a ‘clearAll’ command to the UI (if configured via set_config). - Closes the underlying WebSocket connection. - Stops the background listener thread. - Clears internal state and message handlers.

It’s safe to call this function multiple times; subsequent calls after the first successful shutdown will have no effect.

This function is also registered automatically via atexit to be called when your Python script exits normally, ensuring cleanup happens if possible.

You might call this manually from an event handler (e.g., a “Quit” button’s on_click callback) to programmatically stop run_forever() and end the script.

Example

>>> def on_quit_button_click(control_id):
...    print("Quit button clicked. Shutting down.")
...    sidekick.shutdown()
>>> controls.add_button("quit", "Quit")
>>> controls.on_click(on_quit_button_click)
>>> sidekick.run_forever()
class sidekick.ObservableValue(initial_value: Any)[source]

Bases: object

Wraps a Python value (list, dict, set) to notify subscribers about changes.

Use this class to make your data “reactive” when displayed using the sidekick.Viz module. By wrapping mutable data structures (lists, dictionaries, sets) in an ObservableValue, you enable the Viz panel in Sidekick to update its display automatically whenever you modify the wrapped data through this wrapper object.

This is achieved by intercepting common modification methods (like append, __setitem__, update, add, clear, etc.) and sending notifications after the operation completes.

Basic Usage:
>>> import sidekick
>>> viz = sidekick.Viz() # Assuming Viz panel is ready
>>>
>>> # Wrap a list
>>> shopping_list = sidekick.ObservableValue(['apples', 'bananas'])
>>> viz.show("Groceries", shopping_list)
>>>
>>> # Modify through the wrapper - Viz updates automatically!
>>> shopping_list.append('carrots')
>>> shopping_list[0] = 'blueberries'
>>>
>>> # Wrap a dictionary
>>> config = sidekick.ObservableValue({'theme': 'dark', 'autosave': False})
>>> viz.show("Settings", config)
>>>
>>> # Modify through the wrapper - Viz updates automatically!
>>> config['autosave'] = True
>>> config.update({'fontSize': 12})
>>> del config['theme']

Key Benefit: Simplifies tracking data changes visually in Sidekick, as you don’t need repeated calls to viz.show() after each modification to an observed value.

See the module docstring for important limitations regarding direct modification of unwrapped values or attributes of wrapped custom objects.

__contains__(item: Any) bool[source]

Allows using the in operator to check for containment in the wrapped value.

Enables syntax like element in my_observable if the wrapped object (self.get()) supports containment checks (like lists, dictionaries checking keys, sets, strings, or custom objects implementing __contains__).

Parameters:

item (Any) – The item to check for containment within the wrapped value.

Returns:

True if the item is found in the wrapped value, False otherwise.

Return type:

bool

Raises:

TypeError – If the wrapped object does not support the in operator (typically requires __contains__ or being iterable).

__delitem__(key: Any)[source]

Deletes an item/key from the wrapped container (dict/list) and notifies subscribers.

Intercepts the standard Python del statement used with square brackets: del my_observable[key]

It performs the deletion operation on the wrapped object (self._value) and then, if successful, triggers a “delitem” notification. Works for deleting dictionary keys or list items by index.

Parameters:

key (Any) – The key (for dictionaries) or index (for lists) to delete.

Raises:
  • TypeError – If the wrapped value does not support item deletion.

  • KeyError – If the wrapped value is a dictionary and the key is not found.

  • IndexError – If the wrapped value is a list and the index is out of range.

__eq__(other: Any) bool[source]

Compares the wrapped value for equality.

Allows comparing an ObservableValue directly with another value or another ObservableValue. The comparison is performed on the underlying wrapped values.

Example

>>> obs1 = ObservableValue([1, 2])
>>> obs2 = ObservableValue([1, 2])
>>> obs1 == [1, 2] # True
>>> obs1 == obs2   # True
__getattr__(name: str) Any[source]

Delegates attribute access to the wrapped value if the attribute is not internal.

This allows you to conveniently access methods and attributes of the wrapped object directly through the ObservableValue instance. For example, if obs_list = ObservableValue([1, 2]), calling obs_list.count(1) will correctly delegate to the underlying list’s count method.

It prevents direct access to the ObservableValue’s own internal attributes (like _value, _subscribers) via this delegation mechanism to avoid conflicts.

Parameters:

name (str) – The name of the attribute being accessed.

Returns:

The value of the attribute from the wrapped object.

Return type:

Any

Raises:

AttributeError – If the attribute name refers to an internal attribute of ObservableValue itself, or if the wrapped object does not have an attribute with the given name.

__getitem__(key: Any) Any[source]

Allows accessing items/keys of the wrapped value using square bracket notation ([]).

Enables syntax like my_observable[key] or my_observable[index] if the wrapped object (self.get()) supports item access (like lists, dictionaries, or custom objects implementing __getitem__).

Parameters:

key (Any) – The key or index to access within the wrapped object.

Returns:

The value associated with the key/index in the wrapped object.

Return type:

Any

Raises:
  • TypeError – If the wrapped object does not support item access (__getitem__).

  • KeyError – If the wrapped object is a dictionary and the key is not found.

  • IndexError – If the wrapped object is a list and the index is out of range.

__init__(initial_value: Any)[source]

Initializes the ObservableValue by wrapping the provided Python value.

Parameters:

initial_value – The Python value (e.g., a list, dict, set, number, string, etc.) that you want to make observable.

__iter__() Iterable[Any][source]

Allows iterating over the wrapped value (e.g., in a for loop).

Enables syntax like for item in my_observable: if the wrapped object (self.get()) is itself iterable (like lists, dictionaries, sets, strings, or custom objects implementing __iter__).

Yields:

The items produced by iterating over the wrapped value.

Raises:

TypeError – If the wrapped object is not iterable.

__len__() int[source]

Returns the length of the wrapped value, if the wrapped value supports len().

Allows using len(my_observable) just like len(my_observable.get()).

Raises:

TypeError – If the wrapped object type does not have a defined length (e.g., numbers, None, objects without __len__).

__repr__() str[source]

Returns a string representation showing it’s an ObservableValue wrapping another value.

Example: ObservableValue([1, 2, 3])

__setattr__(name: str, value: Any)[source]

Sets internal attributes or delegates attribute setting to the wrapped value.

This method controls how attribute assignment works on the ObservableValue instance:

  • If the name being assigned matches one of the ObservableValue’s own internal attributes (defined in _obs_internal_attrs, e.g., _value), it sets that internal attribute directly on the wrapper instance itself.

  • Otherwise (if name is not an internal attribute), it attempts to set the attribute with the given name and value directly on the wrapped object (self._value).

Important: Delegating attribute setting to the wrapped object via this method does not automatically trigger notifications to subscribers like Viz. If you need the Viz panel to update when you change an attribute of a wrapped custom object, you have several options:

  1. Call .set(self.get()) on the ObservableValue after modifying the wrapped object’s attribute(s). This forces a full “set” notification, telling Viz to re-render the entire object display.

  2. If the attribute itself holds mutable data (like a list), consider wrapping that attribute’s value in its own ObservableValue.

  3. If the custom object has its own notification mechanism, trigger it manually.

Parameters:
  • name (str) – The name of the attribute to set.

  • value (Any) – The value to assign to the attribute.

Raises:

AttributeError – If attempting to set an attribute on a wrapped object that doesn’t support attribute assignment (e.g., built-in types like int or list, or objects without __slots__ or __dict__ allowing the assignment).

__setitem__(key: Any, value: Any)[source]

Sets the value for a key/index in the wrapped container (dict/list) and notifies subscribers.

This method intercepts the standard Python square bracket assignment syntax: my_observable[key] = value

It performs the assignment operation on the wrapped object (self._value) and then, if successful, triggers a “setitem” notification to inform subscribers (like Viz) about the change. This works for both dictionary key assignment (my_dict[key] = val) and list index assignment (my_list[index] = val).

Parameters:
  • key (Any) – The key (for dictionaries) or index (for lists) to assign to.

  • value (Any) – The new value to associate with the key/index.

Raises:
  • TypeError – If the wrapped value does not support item assignment (e.g., if it’s a set, tuple, or immutable object).

  • IndexError – If the wrapped value is a list and the index is out of range.

  • KeyError – Typically not raised by assignment itself, but underlying checks might.

__str__() str[source]

Returns the string representation of the wrapped value.

Allows str(my_observable) to behave the same as str(my_observable.get()).

add(element: Any)[source]

Adds an element to the wrapped set and notifies subscribers if the element was not already present.

Mimics set.add(). Requires the wrapped value to be a mutable set. If the element is already in the set, this method does nothing (and sends no notification), matching standard set behavior.

Parameters:

element (Any) – The element to add to the set.

Raises:
  • AttributeError – If the wrapped value does not have an add method or is not a set.

  • TypeError – If the wrapped value is not a set or similar mutable set.

append(item: Any)[source]

Appends an item to the end of the wrapped list/sequence and notifies subscribers.

This method mimics the behavior of list.append(). It requires the wrapped value (self.get()) to be a mutable sequence (like a standard Python list).

Raises:
  • AttributeError – If the wrapped value does not have an append method.

  • TypeError – If the wrapped value is not a list or similar mutable sequence (though usually caught by AttributeError first).

Example

>>> items = sidekick.ObservableValue(['a', 'b'])
>>> viz.show("Items", items)
>>> items.append('c') # Viz automatically updates to show ['a', 'b', 'c']
clear()[source]

Removes all items from the wrapped container (list, dict, set) and notifies subscribers.

Requires the wrapped value to have a callable clear() method.

Raises:
  • AttributeError – If the wrapped object does not have a clear method.

  • TypeError – If the wrapped object’s clear attribute is not callable.

discard(element: Any)[source]

Removes an element from the wrapped set if it is present, and notifies subscribers if removal occurred.

Mimics set.discard(). Requires the wrapped value to be a mutable set. If the element is not found in the set, this method does nothing (and sends no notification), matching standard set behavior.

Parameters:

element (Any) – The element to remove from the set.

Raises:
  • AttributeError – If the wrapped value does not have a discard method.

  • TypeError – If the wrapped value is not a set or similar.

get() Any[source]

Returns the actual underlying Python value being wrapped by this ObservableValue.

Use this method when you need direct access to the original list, dictionary, set, or other object stored inside, without the ObservableValue wrapper’s notification logic.

Be cautious: Modifying mutable objects obtained via get() directly (e.g., my_obs_list.get().append(item)) will not trigger automatic notifications to subscribers like sidekick.Viz. For automatic updates, always modify through the ObservableValue wrapper itself (my_obs_list.append(item)).

Returns:

The actual Python object currently stored within the wrapper.

Example

>>> obs_list = sidekick.ObservableValue([10, 20, 30])
>>> raw_list = obs_list.get()
>>> print(raw_list)
[10, 20, 30]
>>> print(type(raw_list))
<class 'list'>
>>> # Modifying raw_list directly does NOT notify Viz
>>> raw_list.pop()
30
>>> # Modifying through the wrapper DOES notify Viz
>>> obs_list.pop() # Viz will update
insert(index: int, item: Any)[source]

Inserts an item at a specific index in the wrapped list/sequence and notifies subscribers.

Mimics list.insert(). Requires the wrapped value to be a mutable sequence.

Parameters:
  • index (int) – The index at which to insert the item.

  • item (Any) – The item to insert.

Raises:
  • AttributeError – If the wrapped value does not have an insert method.

  • TypeError – If the wrapped value is not a list or similar.

  • IndexError – If the index is out of range for insertion (behavior matches list.insert).

pop(index: int = -1) Any[source]

Removes and returns the item at the given index (default last) and notifies subscribers.

Mimics list.pop(). Requires the wrapped value to be a mutable sequence.

Parameters:

index (int) – The index of the item to remove. Defaults to -1 (the last item).

Returns:

The item that was removed from the list.

Raises:
  • AttributeError – If the wrapped value does not have a pop method.

  • TypeError – If the wrapped value is not a list or similar.

  • IndexError – If the list is empty or the index is out of range.

remove(value: Any)[source]

Removes the first occurrence of a given value from the wrapped list/sequence and notifies subscribers.

Mimics list.remove(). Requires the wrapped value to be a mutable sequence. If the value is not found, it does nothing (and sends no notification), matching the standard list.remove() behavior.

Parameters:

value (Any) – The value to search for and remove the first instance of.

Raises:
  • AttributeError – If the wrapped value does not have a remove method.

  • TypeError – If the wrapped value is not a list or similar.

  • ValueError – While this method catches the ValueError from list.remove (when the value isn’t found) and does nothing, the underlying list.index call used for notification could raise it if the value disappears between the check and the call (highly unlikely).

set(new_value: Any)[source]

Replaces the currently wrapped value with a completely new value.

This method is used when you want to assign a fundamentally different object (e.g., a new list, a different dictionary, a number instead of a list) to this observable variable, rather than just modifying the contents of the existing wrapped object.

It triggers a “set” notification to all subscribers, indicating that the entire value has been replaced.

Note

If the new_value you provide is the exact same object in memory as the currently wrapped value (i.e., new_value is self.get()), this method will do nothing and send no notification, as the value hasn’t conceptually changed from the wrapper’s perspective.

Parameters:

new_value – The new Python object to wrap and observe going forward.

Example

>>> obs_data = sidekick.ObservableValue({"status": "pending"})
>>> viz.show("Job Status", obs_data) # Show initial state
>>>
>>> # Replace the entire dictionary
>>> final_status = {"status": "complete", "result": 123}
>>> obs_data.set(final_status) # Viz panel updates to show the new dict
>>>
>>> # Further modifications through obs_data now affect final_status
>>> obs_data["timestamp"] = time.time() # Viz updates again
subscribe(callback: Callable[[Dict[str, Any]], None]) Callable[[], None][source]

Registers a function to be called whenever the wrapped value changes. (Internal).

This method is primarily intended for internal use by the sidekick.Viz module. When viz.show() is called with an ObservableValue, Viz uses this method to register its own internal handler (_handle_observable_update).

When a change occurs to the wrapped value (triggered by methods like append(), __setitem__(), set(), etc.), the callback function provided here is executed with a dictionary containing details about that specific change (e.g., type of change, path, new value).

Parameters:

callback – A function that accepts one argument: a dictionary describing the change event (common keys include ‘type’, ‘path’, ‘value’, ‘key’, ‘old_value’, ‘length’).

Returns:

A function that, when called with no arguments,

will remove this specific callback from the subscription list. This allows Viz to clean up its listener when the variable is removed from the display or the Viz panel itself is removed.

Return type:

UnsubscribeFunction

Raises:

TypeError – If the provided callback is not a callable function.

unsubscribe(callback: Callable[[Dict[str, Any]], None])[source]

Removes a previously registered callback function. (Internal).

Typically called by the unsubscribe function returned from subscribe, or potentially directly by Viz during cleanup.

Parameters:

callback – The specific callback function instance to remove from the set of subscribers.

update(other: Dict[Any, Any] | Mapping[Any, Any] | Iterable[Tuple[Any, Any]] = {}, **kwargs: Any)[source]

Updates the wrapped dictionary with key-value pairs from another mapping and/or keyword arguments, notifying subscribers for each change.

Mimics the behavior of dict.update(). It iterates through the items to be added or updated and uses the intercepted __setitem__ method for each one. This ensures that subscribers (like Viz) receive individual “setitem” notifications for every key that is added or whose value is changed, allowing for more granular UI updates compared to a single bulk update notification.

Requires the wrapped value (self.get()) to be a mutable mapping (like a standard Python dict).

Parameters:
  • other – Can be another dictionary, an object implementing the Mapping protocol, or an iterable of key-value pairs (like [(‘a’, 1), (‘b’, 2)]). Keys and values from other will be added/updated in the wrapped dict.

  • **kwargs – Keyword arguments are treated as additional key-value pairs to add or update in the wrapped dictionary.

Raises:
  • AttributeError – If the wrapped value does not behave like a dictionary (no __setitem__).

  • TypeError – If the wrapped value is not a dictionary or similar mutable mapping, or if the other argument is not a valid source for updates.

Example

>>> settings = sidekick.ObservableValue({'font': 'Arial', 'size': 10})
>>> viz.show("Settings", settings)
>>>
>>> # Update using another dictionary
>>> settings.update({'size': 12, 'theme': 'dark'}) # Sends 2 notifications
>>> # Update using keyword arguments
>>> settings.update(line_numbers=True, theme='light') # Sends 2 notifications (theme overwritten)
class sidekick.Grid(num_columns: int, num_rows: int, instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents an interactive Grid module instance in the Sidekick UI.

Instantiate this class to create a grid of cells with specified dimensions in the Sidekick panel. You can then manipulate individual cells using methods like set_color(), set_text(), clear_cell(), or clear the entire grid with clear(). Use on_click() to register a function that responds to user clicks on the grid cells.

target_id

The unique identifier for this grid instance.

Type:

str

num_columns

The number of columns this grid has (read-only).

Type:

int

num_rows

The number of rows this grid has (read-only).

Type:

int

__init__(num_columns: int, num_rows: int, instance_id: str | None = None, spawn: bool = True)[source]

Initializes the Grid object and optionally creates the UI element.

Sets up the grid’s dimensions and prepares it for interaction. Establishes the connection to Sidekick if not already done (this might block).

Parameters:
  • num_columns (int) – The number of columns the grid should have. Must be a positive integer (greater than 0).

  • num_rows (int) – The number of rows the grid should have. Must be a positive integer (greater than 0).

  • instance_id (Optional[str]) –

    A specific ID for this grid instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”grid-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing grid element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new grid UI element with the specified dimensions. If False, the library assumes a grid element with the given instance_id already exists, and this Python object simply connects to it. The num_columns and num_rows arguments are still validated locally when spawn=False but are not sent in the (empty) spawn command.

Raises:
  • ValueError – If num_columns or num_rows are not positive integers, or if spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Create a standard 8x8 grid
>>> board_grid = sidekick.Grid(num_columns=8, num_rows=8)
>>>
>>> # Create a wide grid
>>> wide_grid = sidekick.Grid(20, 5)
>>>
>>> # Attach to an existing grid named "level-map" (assume it's 30x20)
>>> map_control = sidekick.Grid(instance_id="level-map", spawn=False,
...                             num_columns=30, num_rows=20)
clear()[source]

Clears the entire grid, resetting all cells to their default state.

Removes all custom background colors and all text content from every cell in the grid simultaneously. The grid will appear visually empty, as if it was just created.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(5, 5)
>>> grid.set_color(0, 0, "red")
>>> grid.set_text(1, 1, "Hi")
>>> grid.set_color(4, 4, "blue")
>>> # ... other manipulations ...
>>> # Now clear the entire grid back to its initial state
>>> grid.clear()
clear_cell(x: int, y: int)[source]

Clears both the background color and the text content of a specific cell.

Resets the specified cell (x, y) back to its default visual state, removing any custom background color set by set_color() and any text set by set_text().

Parameters:
  • x (int) – The column index (0 to num_columns - 1) of the cell to clear.

  • y (int) – The row index (0 to num_rows - 1) of the cell to clear.

Raises:
  • IndexError – If the provided x or y coordinates are outside the valid grid boundaries.

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(4, 4)
>>> grid.set_color(3, 3, "purple")
>>> grid.set_text(3, 3, "Value")
>>> # Reset cell (3, 3) completely to its default state
>>> grid.clear_cell(3, 3)
property num_columns: int

The number of columns in the grid (read-only).

Type:

int

property num_rows: int

The number of rows in the grid (read-only).

Type:

int

on_click(callback: Callable[[int, int], None] | None)[source]

Registers a function to call when the user clicks on any cell in this grid.

When the user clicks a cell within this grid in the Sidekick UI panel, the callback function you provide here will be executed within your running Python script.

Parameters:

callback (Optional[Callable[[int, int], None]]) – The function to call when a cell is clicked. This function must accept two integer arguments: 1. x (int): The column index (0-based, from left) of the clicked cell. 2. y (int): The row index (0-based, from top) of the clicked cell. Pass None to remove any previously registered click callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> import sidekick
>>> grid = sidekick.Grid(10, 10)
>>>
>>> def paint_cell(column, row):
...     print(f"Painting cell at ({column}, {row})")
...     grid.set_color(column, row, "lightblue") # Change color on click
...
>>> grid.on_click(paint_cell)
>>> print("Click on the grid cells in Sidekick to paint them!")
>>>
>>> # Keep the script running to listen for clicks!
>>> sidekick.run_forever()
set_color(x: int, y: int, color: str | None)[source]

Sets the background color of a specific cell in the grid.

Changes the visual appearance of a single cell (square) in the grid UI. This operation does not affect any text that might be displayed in the cell.

Parameters:
  • x (int) – The column index (0 to num_columns - 1) of the target cell.

  • y (int) – The row index (0 to num_rows - 1) of the target cell.

  • color (Optional[str]) – The desired background color for the cell. This should be a string representing a color in a standard CSS format, such as: - Color names: ‘red’, ‘blue’, ‘lightgray’ - Hex codes: ‘#FF0000’, ‘#00F’, ‘#abcdef’ - RGB: ‘rgb(0, 255, 0)’, ‘rgba(0, 0, 255, 0.5)’ (for transparency) - HSL: ‘hsl(120, 100%, 50%)’, ‘hsla(240, 50%, 50%, 0.7)’ Pass None to clear any custom background color previously set for this cell, resetting it to the default grid cell background.

Raises:
  • IndexError – If the provided x or y coordinates are outside the valid range of columns (0 to num_columns-1) or rows (0 to num_rows-1).

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(4, 4)
>>> # Set cell (0, 0) (top-left) to red
>>> grid.set_color(0, 0, "red")
>>> # Set cell (3, 3) (bottom-right) to semi-transparent green
>>> grid.set_color(3, 3, "rgba(0, 255, 0, 0.5)")
>>> # Clear the color of cell (0, 0), reverting it to default
>>> grid.set_color(0, 0, None)
set_text(x: int, y: int, text: str | None)[source]

Sets the text content displayed inside a specific cell in the grid.

Places the given text string within the boundaries of the specified cell in the Sidekick UI. Setting text does not affect the cell’s background color. If the text is too long to fit, the UI might truncate it or handle overflow.

Parameters:
  • x (int) – The column index (0 to num_columns - 1) of the target cell.

  • y (int) – The row index (0 to num_rows - 1) of the target cell.

  • text (Optional[str]) – The text string to display within the cell. Any Python object provided will be converted to its string representation using str(). If you pass None or an empty string “”, any existing text currently displayed in the specified cell will be cleared.

Raises:
  • IndexError – If the provided x or y coordinates are outside the valid range of columns (0 to num_columns-1) or rows (0 to num_rows-1).

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> grid = sidekick.Grid(3, 3)
>>> # Put an "X" in the center cell (1, 1)
>>> grid.set_text(1, 1, "X")
>>> # Put a number in the top-right cell (2, 0)
>>> count = 5
>>> grid.set_text(2, 0, str(count)) # Convert number to string
>>> # Clear the text from the center cell
>>> grid.set_text(1, 1, None)
>>> # Setting empty string also clears text
>>> grid.set_text(2, 0, "")
class sidekick.Console(instance_id: str | None = None, spawn: bool = True, initial_text: str = '', show_input: bool = False)[source]

Bases: BaseModule

Represents a Console module instance in the Sidekick UI panel.

Creates a scrollable text area for displaying output from your script via its print() method. Optionally includes a text input field at the bottom to receive input from the user in the Sidekick panel.

target_id

The unique identifier for this console instance.

Type:

str

__init__(instance_id: str | None = None, spawn: bool = True, initial_text: str = '', show_input: bool = False)[source]

Initializes the Console object and optionally creates the UI element.

Sets up the console and configures its appearance (e.g., whether to show an input field). Establishes the connection to Sidekick if not already done.

Parameters:
  • instance_id (Optional[str]) –

    A specific ID for this console instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”console-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing console element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new console UI element. If False, the library assumes a console element with the given instance_id already exists, and this Python object simply connects to it. initial_text and show_input arguments are ignored if spawn=False.

  • initial_text (str) – A line of text to display immediately when the console UI element is first created. Only used if spawn=True. Defaults to an empty string “”.

  • show_input (bool) – If True, an input field and submit button are included at the bottom of the console UI, allowing the user to type and send text back to the script (handled via on_input_text()). If False (default), only the text output area is shown. Only used if spawn=True.

Raises:
  • ValueError – If spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Simple output-only console
>>> log_console = sidekick.Console()
>>> log_console.print("Program log started.")
>>>
>>> # Interactive console waiting for user input
>>> interactive_console = sidekick.Console(show_input=True, initial_text="Enter your name:")
>>> # (Requires using interactive_console.on_input_text() and sidekick.run_forever())
clear()[source]

Removes all previously printed text from this console instance in Sidekick.

This effectively empties the console’s text area in the UI panel.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Example

>>> console = sidekick.Console()
>>> console.print("Message 1")
>>> console.print("Message 2")
>>> import time; time.sleep(1) # Wait a second
>>> console.clear() # The console in Sidekick becomes empty
>>> console.print("Cleared!")
on_input_text(callback: Callable[[str], None] | None)[source]

Registers a function to call when the user submits text via the input field.

This method is only relevant if you created the console with show_input=True. When the user types text into the input box in the Sidekick UI panel and then presses Enter (or clicks the associated submit button), the callback function you provide here will be executed within your running Python script.

Parameters:

callback (Optional[Callable[[str], None]]) – The function to call when text is submitted. This function must accept one argument: a string containing the exact text entered and submitted by the user. Pass None to remove any previously registered callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> import sidekick
>>> console = sidekick.Console(show_input=True)
>>>
>>> def process_user_input(text_from_user):
...     console.print(f"Processing command: {text_from_user}")
...     # Add logic here based on the input...
...     if text_from_user.lower() == "quit":
...         console.print("Exiting now.")
...         sidekick.shutdown() # Stop run_forever
...
>>> console.input_text_handler(process_user_input)
>>> console.print("Enter commands below (type 'quit' to exit):")
>>>
>>> # Keep the script running to listen for the user's input!
>>> sidekick.run_forever()
print(*args: Any, sep: str = ' ', end: str = '\n')[source]

Prints messages to this console instance in the Sidekick UI.

Works very much like Python’s built-in print() function. It converts all positional arguments (args) to their string representations, joins them together using the sep string as a separator, and finally appends the end string (which defaults to a newline character n, causing each call to typically start on a new line in the console).

The resulting string is then sent to the Sidekick UI to be appended to the console’s text area.

Parameters:
  • *args (Any) – One or more objects to print. They will be automatically converted to strings using str().

  • sep (str) – The separator string inserted between multiple arguments. Defaults to a single space (’ ‘).

  • end (str) – The string appended at the very end, after all arguments and separators. Defaults to a newline character (’n’). Set this to end=’’ to print without starting a new line afterwards.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the command fails.

Examples

>>> console = sidekick.Console()
>>> name = "World"
>>> count = 5
>>> console.print("Hello,", name, "!") # Output: Hello, World !
>>> console.print("Count:", count, sep='=') # Output: Count:=5
>>> console.print("Processing...", end='') # Prints without a newline
>>> console.print("Done.") # Prints on the same line as "Processing..."
class sidekick.Control(instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents a Control panel module instance in the Sidekick UI.

This class creates a container in the Sidekick panel specifically designed to hold simple interactive controls added dynamically from your Python script. Use its methods (add_button, add_text_input) to populate the panel, and on_click / on_input_text to define the Python functions that respond to user interactions with those controls.

target_id

The unique identifier for this control panel instance.

Type:

str

__init__(instance_id: str | None = None, spawn: bool = True)[source]

Initializes the Control panel object and optionally creates the UI element.

Sets up an empty panel ready to receive controls via methods like add_button. Establishes the connection to Sidekick if not already done.

Parameters:
  • instance_id (Optional[str]) –

    A specific ID for this control panel instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”control-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing control panel element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new, empty control panel UI element. If False, the library assumes a panel with the given instance_id already exists, and this Python object simply connects to it.

Raises:
  • ValueError – If spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Create a new panel to hold buttons and inputs
>>> sim_controls = sidekick.Control(instance_id="simulation-controls")
>>> sim_controls.add_button("start_btn", "Run Simulation")
>>>
>>> # Attach to an existing panel (e.g., created by another script)
>>> existing_panel = sidekick.Control(instance_id="shared-controls", spawn=False)
add_button(control_id: str, button_text: str)[source]

Adds a clickable button to this control panel in the Sidekick UI.

Creates a button element within this Control instance’s panel area. Clicking this button in the Sidekick UI will trigger the function registered using on_click(), passing this button’s unique control_id to the callback function.

Parameters:
  • control_id (str) – A unique identifier string for this specific button within this control panel. This ID is chosen by you and is crucial for identifying which button was pressed in the on_click callback. It must be a non-empty string.

  • button_text (str) – The text label that will appear visibly on the button in the UI.

Raises:

Example

>>> controls = sidekick.Control()
>>> controls.add_button("start_sim", "Start Simulation")
>>> controls.add_button("reset_sim", "Reset")
>>> # Add an on_click handler to react to these buttons...
add_text_input(control_id: str, placeholder: str = '', initial_value: str = '', button_text: str = '')[source]

Adds a text input field paired with a submit button to the control panel.

Creates a combined UI element in the Sidekick panel consisting of a text entry box and an adjacent button (labeled with button_text). When the user types text into the field and clicks the associated submit button, the function registered using on_input_text() is triggered. That callback function receives both this control’s control_id and the text the user entered.

Parameters:
  • control_id (str) – A unique identifier string for this specific text input group (the field + button combination) within this control panel. This ID is passed to the on_input_text callback. Must be a non-empty string.

  • placeholder (str) – Optional text displayed faintly inside the input field when it’s empty, providing a hint to the user (e.g., “Enter name”). Defaults to “”.

  • initial_value (str) – Optional text pre-filled in the input field when it first appears in the UI. Defaults to “”.

  • button_text (str) – Optional text label displayed on the submit button that’s associated with this input field.

Raises:

Example

>>> controls = sidekick.Control()
>>> # Add a simple text input with a placeholder
>>> controls.add_text_input("search_term", placeholder="Enter search query...")
>>>
>>> # Add an input with an initial value and a custom button label
>>> controls.add_text_input("server_ip", initial_value="192.168.1.1", button_text="Set IP")
>>> # Add an on_input_text handler to react to these inputs...
on_click(callback: Callable[[str], None] | None)[source]

Registers a function to call when any button within this panel is clicked.

When a user clicks a button (previously added using add_button()) in the Sidekick UI panel that belongs to this Control instance, the callback function you provide here will be executed within your running Python script.

Parameters:

callback (Optional[Callable[[str], None]]) – The function to call when any button in this panel is clicked. This function must accept one argument: control_id (str): The unique identifier (the same ID you provided when calling add_button) of the specific button that was clicked. Pass None to remove any previously registered click callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> controls = sidekick.Control()
>>> controls.add_button("action_one", "Perform Action 1")
>>> controls.add_button("action_two", "Do Something Else")
>>>
>>> def button_handler(button_that_was_clicked):
...     print(f"Button clicked: {button_that_was_clicked}")
...     if button_that_was_clicked == "action_one":
...         # Run action 1 logic...
...         print("Running action 1...")
...     elif button_that_was_clicked == "action_two":
...         # Run action 2 logic...
...         print("Doing something else...")
...
>>> controls.on_click(button_handler)
>>> sidekick.run_forever() # Keep script running
on_input_text(callback: Callable[[str, str], None] | None)[source]

Registers a function to call when text is submitted from any text input field within this panel.

When a user types text into an input field (previously added using add_text_input()) within this Control instance’s panel and clicks its associated “Submit” button in the Sidekick UI, the callback function you provide here will be executed in your running Python script.

Parameters:

callback (Optional[Callable[[str, str], None]]) –

The function to call when text is submitted from any input field in this panel. This function must accept two arguments: 1. control_id (str): The unique identifier (the same ID you

provided when calling add_text_input) of the specific text input group whose submit button was clicked.

  1. value (str): The text string that the user had entered into the input field when they submitted it.

Pass None to remove any previously registered input text callback.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> controls = sidekick.Control()
>>> controls.add_text_input("param_a", placeholder="Parameter A")
>>> controls.add_text_input("param_b", placeholder="Parameter B", button_text="Set B")
>>>
>>> def input_handler(input_field_id, submitted_value):
...     print(f"Input received from '{input_field_id}': '{submitted_value}'")
...     if input_field_id == "param_a":
...         # Process parameter A...
...         print(f"Setting A to {submitted_value}")
...     elif input_field_id == "param_b":
...         # Process parameter B...
...         print(f"Setting B to {submitted_value}")
...
>>> controls.input_text_handler(input_handler)
>>> sidekick.run_forever() # Keep script running
remove_control(control_id: str)[source]

Removes a specific control (button or text input) from this panel in the UI.

Use this method to dynamically remove a button or text input field that was previously added using add_button() or add_text_input(). You must provide the same unique control_id that you used when adding the control.

Parameters:

control_id (str) – The unique identifier string of the control element (button or text input group) that you want to remove from the Sidekick UI panel. Must be a non-empty string matching the ID used during creation.

Raises:

Example

>>> controls = sidekick.Control()
>>> controls.add_button("temporary_task", "Run Once")
>>> # ... some logic runs ...
>>> # Now remove the button as it's no longer needed
>>> controls.remove_control("temporary_task")
>>>
>>> controls.add_text_input("user_pin", "PIN")
>>> # ... user enters PIN and it's processed ...
>>> controls.remove_control("user_pin") # Remove the input field
class sidekick.Viz(instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents the Variable Visualizer (Viz) module instance in the Sidekick UI.

Use this class to create an interactive panel in Sidekick where you can display Python variables and data structures. It presents data like lists, dictionaries, sets, and even custom objects in a collapsible tree view, making it easy to inspect their contents and structure as your script runs.

The key feature is its integration with sidekick.ObservableValue. When you display data wrapped in an ObservableValue using viz.show(), the Viz panel will automatically update its display whenever the underlying data is modified through the wrapper. This provides a powerful live view of how your data changes over time without requiring manual refreshes.

target_id

The unique identifier for this Viz panel instance.

Type:

str

__init__(instance_id: str | None = None, spawn: bool = True)[source]

Initializes the Viz object and optionally creates the UI panel.

Sets up the Viz panel instance. Establishes the connection to Sidekick if not already done (this might block).

Parameters:
  • instance_id (Optional[str]) –

    A specific ID for this Viz panel instance. - If spawn=True (default): Optional. If None, a unique ID (e.g.,

    ”viz-1”) is generated automatically.

    • If spawn=False: Required. Must match the ID of an existing Viz panel element in the Sidekick UI to attach to.

  • spawn (bool) – If True (the default), a command is sent to Sidekick to create a new, empty Viz panel UI element. If False, the library assumes a panel with the given instance_id already exists, and this Python object simply connects to it.

Raises:
  • ValueError – If spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to Sidekick cannot be established during initialization.

Examples

>>> # Create a new Viz panel
>>> data_viewer = sidekick.Viz()
>>>
>>> # Attach to an existing panel maybe named "debug-variables"
>>> debug_vars = sidekick.Viz(instance_id="debug-variables", spawn=False)
remove()[source]

Removes the entire Viz panel instance from the Sidekick UI and cleans up resources.

Call this method when you are completely finished with this Viz panel. It performs the following actions:

  1. Unsubscribes: Iterates through all variables currently tracked by this Viz instance and, if any are `ObservableValue`s, calls their unsubscribe functions to stop listening for changes.

  2. Calls Base `remove()`: Invokes the BaseModule.remove() method, which: a. Unregisters the internal message handler for this Viz panel. b. Resets registered callbacks (on_error) to None. c. Sends the final ‘remove’ command to the Sidekick UI to delete the

    entire Viz panel element itself.

Raises:

SidekickConnectionError (or subclass) – Can occur if sending the final ‘remove’ command fails. Cleanup of local Python resources (subscriptions, handlers) will still be attempted.

remove_variable(name: str)[source]

Removes a previously shown variable from the Viz panel display.

Use this method when you no longer need to see a specific variable in the Sidekick Viz panel.

If the variable currently displayed under this name was an ObservableValue, this method also automatically unsubscribes from its changes, preventing further automatic updates for this removed variable and cleaning up resources.

Parameters:

name (str) – The exact name of the variable to remove. This must match the name used in the corresponding viz.show(name, …) call.

Raises:

SidekickConnectionError (or subclass) – If the connection is not ready or sending the removal command fails.

Example

>>> viz = sidekick.Viz()
>>> temporary_data = [1, 2, 3, 4, 5]
>>> viz.show("Intermediate Result", temporary_data)
>>> # ... process the data ...
>>> # Now remove it from the display
>>> viz.remove_variable("Intermediate Result")
show(name: str, value: Any)[source]

Displays or updates a variable in the Sidekick Viz panel.

This is the primary method for sending data to the Viz panel. It shows the given Python value under the specified name in an interactive tree view.

  • If `name` is new: Adds the variable to the Viz panel display.

  • If `name` already exists: Updates the display for that variable to reflect the current state of the provided value.

Reactivity with `ObservableValue`:

The key feature is how show() interacts with sidekick.ObservableValue:

  • If the value you pass is wrapped in sidekick.ObservableValue (e.g., viz.show(“My List”, sidekick.ObservableValue([1, 2]))), the Viz panel will automatically subscribe to changes within that ObservableValue. Any subsequent modifications made through the wrapper (e.g., my_obs_list.append(3)) will automatically trigger updates in the Viz UI, without needing further calls to viz.show().

  • If the value is not an ObservableValue (e.g., a regular list, dict, number, string, or custom object), the Viz panel simply displays a snapshot of the value at the moment show() is called. If the underlying data changes later, you must call `viz.show(name, updated_value)` again with the same name to refresh the display in the Sidekick panel.

Parameters:
  • name (str) – The name to display for this variable in the Viz panel header (e.g., “my_list”, “game_state”, “loop_counter”). This acts as the unique identifier for the variable within this Viz instance. Must be a non-empty string.

  • value (Any) – The Python variable or value you want to visualize. This can be almost any Python object: primitives (int, float, str, bool, None), collections (list, dict, set, tuple), custom class instances, or, importantly, an ObservableValue wrapping one of these types.

Raises:
  • ValueError – If the provided name is empty or not a string.

  • SidekickConnectionError (or subclass) – If the connection is not ready or sending the initial ‘set’ command fails.

Example

>>> import sidekick
>>> viz = sidekick.Viz()
>>>
>>> # --- Showing a non-observable dictionary ---
>>> config = {"level": 1, "active": True}
>>> viz.show("Game Config", config)
>>> # If config changes later...
>>> config["active"] = False
>>> # ...Viz panel does NOT update automatically. Need to call show() again:
>>> viz.show("Game Config", config) # Manually update the display
>>>
>>> # --- Showing an observable list ---
>>> player_scores = sidekick.ObservableValue([100, 95])
>>> viz.show("Scores", player_scores)
>>> # Now, changes through the wrapper update Viz automatically:
>>> player_scores.append(110)
>>> player_scores[0] = 105
>>> # No need to call viz.show("Scores", player_scores) again!
class sidekick.Canvas(width: int, height: int, instance_id: str | None = None, spawn: bool = True)[source]

Bases: BaseModule

Represents a 2D drawing canvas module instance in the Sidekick UI.

Provides a surface within the Sidekick panel where your Python script can programmatically draw shapes, lines, and text. It’s useful for visualizing algorithms, creating simple graphics or simulations, basic game displays, or educational demonstrations.

Drawing commands (like draw_line(), draw_rect()) are sent immediately to the Sidekick UI to update the visual representation. By default, these draw directly onto the visible canvas area.

For creating animations or complex scenes smoothly without flickering, use the buffer() method within a with statement. This enables “double buffering”, where drawing happens on a hidden surface first, and the result is displayed all at once (see module docstring or buffer() method documentation for examples).

You can also make the canvas interactive by responding to user clicks via the on_click() method.

target_id

The unique identifier for this canvas instance, used for communication with the Sidekick UI. Generated automatically if not provided during initialization.

Type:

str

width

The width of the canvas drawing area in pixels, set during initialization. This value is read-only after creation.

Type:

int

height

The height of the canvas drawing area in pixels, set during initialization. This value is read-only after creation.

Type:

int

ONSCREEN_BUFFER_ID = 0
__del__()[source]

Internal: Fallback attempt to clean up resources upon garbage collection. (Internal).

Warning

Relying on __del__ for cleanup is not reliable in Python. You should always explicitly call the `canvas.remove()` method when you are finished with a canvas instance to ensure proper cleanup of both Python resources and UI elements (including offscreen buffers) in Sidekick. This __del__ method primarily attempts to call the base class’s __del__ for handler unregistration as a last resort. It does not attempt to destroy offscreen buffers.

__init__(width: int, height: int, instance_id: str | None = None, spawn: bool = True)[source]

Initializes a new Canvas object and optionally creates its UI element in Sidekick.

Sets up the dimensions and prepares the canvas for drawing commands. The connection to Sidekick is established automatically during this initialization if it hasn’t been already (this might block).

Parameters:
  • width (int) – The desired width of the canvas drawing area in pixels. Must be a positive integer.

  • height (int) – The desired height of the canvas drawing area in pixels. Must be a positive integer.

  • instance_id (Optional[str]) –

    A specific ID to assign to this canvas instance. - If spawn=True (default): If provided, this ID will be used. Useful

    for deterministic identification. If None, a unique ID (e.g., “canvas-1”) will be generated automatically.

    • If spawn=False: This ID is required and must match the ID of an existing canvas element already present in the Sidekick UI that this Python object should connect to and control.

  • spawn (bool) – If True (the default), a command is sent to Sidekick (after connection) to create a new canvas UI element with the specified width and height. If False, the library assumes a canvas element with the given instance_id already exists in the UI, and this Python object will simply attach to it for sending drawing commands or receiving events. When spawn=False, the width and height arguments are still validated locally but are not sent in the (empty) spawn command.

Raises:
  • ValueError – If width or height are not positive integers, or if spawn is False and instance_id is not provided.

  • SidekickConnectionError (or subclass) – If the connection to the Sidekick UI cannot be established or fails during initialization.

Examples

>>> # Create a new 300x200 canvas in Sidekick
>>> main_canvas = sidekick.Canvas(300, 200)
>>>
>>> # Create another canvas with a specific ID
>>> mini_map = sidekick.Canvas(100, 100, instance_id="ui-mini-map")
>>>
>>> # Assume a canvas with ID "debug-overlay" already exists in Sidekick.
>>> # Attach a Python object to control it (local dimensions needed for validation).
>>> overlay_control = sidekick.Canvas(100, 50, instance_id="debug-overlay", spawn=False)
buffer() ContextManager[_CanvasBufferProxy][source]

Provides a context manager (with statement) for efficient double buffering.

Using this method enables double buffering, the standard technique for creating smooth, flicker-free animations or complex drawing sequences. Instead of drawing directly to the visible screen (which can cause tearing or flickering as elements are drawn one by one), you draw everything for the next frame onto a hidden (offscreen) buffer. When you’re finished drawing the frame, the entire content of the hidden buffer is copied to the visible screen in one go.

How it works in practice:

  1. Enter `with` block: with canvas.buffer() as buf: * An offscreen buffer is acquired (or created/reused). * This buffer is automatically cleared. * The variable buf becomes a proxy object that mirrors the canvas’s

    drawing methods (e.g., buf.draw_line()).

  2. Inside `with` block: * All drawing commands called on buf (e.g., buf.draw_circle(…))

    are sent to the hidden offscreen buffer. The visible screen remains unchanged during this time.

  3. Exit `with` block: * The visible canvas is automatically cleared. * The entire content of the hidden buffer is drawn (“blitted”) onto

    the visible canvas in a single, fast operation.

    • The hidden buffer is released back into an internal pool so it can be reused efficiently next time you enter a buffer() context.

Returns:

An object designed to be used in a with statement. The object yielded by the with statement (buf in the example) provides the drawing methods that target the hidden buffer.

Return type:

ContextManager[_CanvasBufferProxy]

Example (Simple Animation):
>>> import sidekick, time, math
>>> canvas = sidekick.Canvas(150, 100)
>>> x_pos = 10
>>> for frame in range(50):
...     with canvas.buffer() as frame_buffer: # Get the hidden buffer proxy
...         # Draw background (optional, buffer starts clear)
...         # frame_buffer.draw_rect(0, 0, canvas.width, canvas.height, fill_color='lightblue')
...         # Draw moving element on the hidden buffer
...         frame_buffer.draw_circle(x_pos, 50, 10, fill_color='red')
...         frame_buffer.draw_text(5, 15, f"Frame: {frame}")
...     # --- Screen automatically updates here when 'with' block ends ---
...     x_pos += 2 # Move for next frame
...     time.sleep(0.03) # Control animation speed
>>> print("Animation finished.")
clear(buffer_id: int | None = None)[source]

Clears the specified canvas buffer (visible screen or an offscreen buffer).

Erases all previously drawn shapes, lines, and text from the target buffer, resetting it to a blank state (typically transparent or a default background color determined by the UI theme).

Parameters:

buffer_id (Optional[int]) –

The ID of the buffer to clear. - If None (default) or 0, clears the main visible (onscreen) canvas. - If a positive integer corresponding to an offscreen buffer (usually

obtained implicitly via canvas.buffer()), clears that specific hidden buffer.

Raises:

SidekickConnectionError (or subclass) – If sending the command fails.

Examples

>>> canvas = sidekick.Canvas(100, 50)
>>> canvas.draw_rect(10, 10, 30, 30, fill_color='red')
>>> # Clear the main visible canvas
>>> canvas.clear()
>>>
>>> # Example within double buffering: Clear the offscreen buffer
>>> with canvas.buffer() as buf:
...     # buf implicitly refers to an offscreen buffer
...     # To clear *that* specific buffer (e.g., at start of frame drawing):
...     buf.clear() # This calls canvas.clear(buffer_id=buf._buffer_id) internally
...     buf.draw_circle(50, 25, 10) # Draw new content
... # On exit, screen is cleared, then buffer is drawn.
draw_circle(cx: int, cy: int, radius: int, fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a circle on the specified buffer.

The circle is defined by its center coordinates (cx, cy) and its radius.

Parameters:
  • cx (int) – The x-coordinate of the center of the circle.

  • cy (int) – The y-coordinate of the center of the circle.

  • radius (int) – The radius of the circle in pixels. Must be positive.

  • fill_color (Optional[str]) – The color to fill the inside of the circle. Accepts CSS color formats. If None (default), the circle is not filled.

  • line_color (Optional[str]) – The color of the circle’s outline. If None (default), the UI’s default outline color is used.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be positive if an outline is desired. If None (default), the UI’s default outline width (typically 1 pixel) is used. Use 0 for no outline.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If radius is not positive, or if line_width is provided but is not a non-negative integer.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(250, 150)
>>> # Draw a filled red circle
>>> canvas.draw_circle(50, 75, 40, fill_color='red')
>>> # Draw an empty circle with a thick blue outline
>>> canvas.draw_circle(150, 75, 30, line_color='blue', line_width=4)
draw_ellipse(cx: int, cy: int, radius_x: int, radius_y: int, fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws an ellipse (or oval) shape on the specified buffer.

The ellipse is defined by its center coordinates (cx, cy) and its horizontal radius (radius_x) and vertical radius (radius_y). If radius_x equals radius_y, this draws a circle.

Parameters:
  • cx (int) – The x-coordinate of the center of the ellipse.

  • cy (int) – The y-coordinate of the center of the ellipse.

  • radius_x (int) – The horizontal radius (half the total width) of the ellipse. Must be positive.

  • radius_y (int) – The vertical radius (half the total height) of the ellipse. Must be positive.

  • fill_color (Optional[str]) – The color to fill the inside of the ellipse. Accepts CSS color formats. If None (default), the ellipse is not filled.

  • line_color (Optional[str]) – The color of the ellipse’s outline. Uses UI default if None.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be non-negative. Uses UI default (typically 1) if None. Use 0 for no outline.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If radius_x or radius_y are not positive, or if line_width is provided but is negative.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(250, 150)
>>> # Draw a wide, short, filled red ellipse
>>> canvas.draw_ellipse(125, 50, 80, 30, fill_color='red')
>>> # Draw a tall, thin, empty ellipse outline
>>> canvas.draw_ellipse(125, 100, 20, 40, line_color='black', line_width=1)
draw_line(x1: int, y1: int, x2: int, y2: int, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a straight line segment between two points on the specified buffer.

Connects the start point (x1, y1) to the end point (x2, y2).

Parameters:
  • x1 (int) – The x-coordinate (pixels from left) of the line’s start point.

  • y1 (int) – The y-coordinate (pixels from top) of the line’s start point.

  • x2 (int) – The x-coordinate of the line’s end point.

  • y2 (int) – The y-coordinate of the line’s end point.

  • line_color (Optional[str]) – The color of the line. Accepts standard CSS color formats (e.g., ‘black’, ‘#FF0000’, ‘rgb(0, 255, 0)’, ‘hsl(120, 100%, 50%)’). If None, the Sidekick UI’s default line color (usually determined by the theme) is used.

  • line_width (Optional[int]) – The thickness of the line in pixels. Must be a positive integer (e.g., 1, 2, 3…). If None, the UI’s default line width (typically 1 pixel) is used.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None, which targets the main visible (onscreen) canvas (ID 0). When used inside with canvas.buffer() as buf:, drawing methods on buf automatically target the correct offscreen buffer ID.

Raises:

Example

>>> canvas = sidekick.Canvas(150, 150)
>>> # Draw line with default color/width from (10, 20) to (100, 120)
>>> canvas.draw_line(10, 20, 100, 120)
>>> # Draw a thicker, blue line
>>> canvas.draw_line(20, 30, 110, 130, line_color='blue', line_width=3)
draw_polygon(points: List[Tuple[int, int]], fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a closed polygon shape on the specified buffer.

Connects the vertices (points) in the order provided and automatically draws an additional line segment connecting the last point back to the first point to close the shape. The interior can optionally be filled.

Parameters:
  • points (List[Tuple[int, int]]) – A list containing at least three vertex tuples, where each tuple (x, y) represents the integer coordinates of a corner of the polygon.

  • fill_color (Optional[str]) – The color to fill the interior of the polygon. Accepts CSS color formats. If None (default), the polygon is not filled.

  • line_color (Optional[str]) – The color of the polygon’s outline. Uses UI default if None. Use 0 for no outline.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be non-negative. Uses UI default (typically 1) if None.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If points contains fewer than three points, or if line_width is provided but is negative.

  • TypeError – If points is not a list or contains invalid data.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(200, 200)
>>> # Draw a filled blue triangle
>>> triangle = [(50, 150), (100, 50), (150, 150)]
>>> canvas.draw_polygon(triangle, fill_color='blue')
>>> # Draw an empty hexagon outline
>>> hexagon = [(20, 10), (60, 10), (80, 50), (60, 90), (20, 90), (0, 50)]
>>> canvas.draw_polygon(hexagon, line_color='darkgreen', line_width=2)
draw_polyline(points: List[Tuple[int, int]], line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a series of connected line segments (an open path) on the specified buffer.

Connects the points in the order they appear in the points list using straight lines. It’s an “open” path because, unlike draw_polygon, it does not automatically draw a line connecting the last point back to the first.

Parameters:
  • points (List[Tuple[int, int]]) – A list containing at least two vertex tuples, where each tuple (x, y) represents the integer coordinates of a corner point of the polyline.

  • line_color (Optional[str]) – The color for all line segments in the polyline. Uses UI default if None.

  • line_width (Optional[int]) – The thickness for all line segments in pixels. Must be positive. Uses UI default (typically 1) if None.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If points contains fewer than two points, or if line_width is provided but is not positive.

  • TypeError – If points is not a list or contains non-tuple/non-numeric elements.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(200, 100)
>>> # Draw a 'W' shape
>>> w_shape_points = [(20, 80), (40, 20), (60, 80), (80, 20), (100, 80)]
>>> canvas.draw_polyline(w_shape_points, line_color='purple', line_width=3)
draw_rect(x: int, y: int, width: int, height: int, fill_color: str | None = None, line_color: str | None = None, line_width: int | None = None, buffer_id: int | None = None)[source]

Draws a rectangle on the specified buffer.

The rectangle is defined by its top-left corner coordinates (x, y) and its width and height.

Parameters:
  • x (int) – The x-coordinate of the top-left corner.

  • y (int) – The y-coordinate of the top-left corner.

  • width (int) – The width of the rectangle in pixels. Should be non-negative. (A width of 0 might not be visible).

  • height (int) – The height of the rectangle in pixels. Should be non-negative. (A height of 0 might not be visible).

  • fill_color (Optional[str]) – The color to fill the inside of the rectangle. Accepts CSS color formats. If None (default), the rectangle will not be filled (its interior will be transparent).

  • line_color (Optional[str]) – The color of the rectangle’s outline (border). If None (default), the UI’s default outline color is used. If you don’t want an outline, you might need to set line_width to 0 or line_color to the same as fill_color or a transparent color, depending on the desired effect and UI behavior.

  • line_width (Optional[int]) – The thickness of the outline in pixels. Must be positive if an outline is desired. If None (default), the UI’s default outline width (typically 1 pixel) is used. A line_width of 0 usually means no outline is drawn.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:
  • ValueError – If line_width is provided but is not a non-negative integer, or if width or height are negative.

  • SidekickConnectionError (or subclass) – If sending the command fails.

Example

>>> canvas = sidekick.Canvas(200, 150)
>>> # Draw an empty rectangle with a 2px red border
>>> canvas.draw_rect(10, 10, 50, 80, line_color='red', line_width=2)
>>> # Draw a filled green rectangle with the default 1px border
>>> canvas.draw_rect(70, 30, 40, 40, fill_color='green')
>>> # Draw a filled yellow rectangle with effectively no border
>>> canvas.draw_rect(120, 50, 30, 30, fill_color='yellow', line_width=0)
draw_text(x: int, y: int, text: str, text_color: str | None = None, text_size: int | None = None, buffer_id: int | None = None)[source]

Draws a string of text on the specified buffer.

The (x, y) coordinates typically define the position of the text. The exact alignment (e.g., whether (x, y) is the top-left corner, bottom-left baseline, or center) might depend slightly on the underlying UI implementation, but bottom-left baseline is common for canvas APIs.

Parameters:
  • x (int) – The x-coordinate for the starting position of the text.

  • y (int) – The y-coordinate for the starting position of the text.

  • text (str) – The text string you want to display. Any Python object provided will be converted to its string representation using str().

  • text_color (Optional[str]) – The color of the text. Accepts CSS color formats. Uses the UI’s default text color (usually black or white depending on theme) if None.

  • text_size (Optional[int]) – The font size in pixels. Must be positive. Uses the UI’s default font size if None.

  • buffer_id (Optional[int]) – The target buffer ID. Defaults to None (targets the visible onscreen canvas, ID 0).

Raises:

Example

>>> canvas = sidekick.Canvas(200, 100)
>>> score = 150
>>> canvas.draw_text(10, 20, f"Score: {score}") # Draw score with default style
>>> canvas.draw_text(100, 60, "GAME OVER", text_color='red', text_size=24) # Larger, red text
property height: int

The height of the canvas in pixels (read-only).

Type:

int

on_click(callback: Callable[[int, int], None] | None)[source]

Registers a function to be called when the user clicks on the canvas.

When the user clicks anywhere on this canvas’s visible area in the Sidekick UI panel, the function you provide (callback) will be executed within your running Python script.

Note

Click events are only triggered by interactions with the main, visible (onscreen) canvas (buffer ID 0). Clicks are not detected on hidden offscreen buffers used with canvas.buffer().

Parameters:

callback (Optional[Callable[[int, int], None]]) – The function to execute when a click occurs. This function must accept two integer arguments: x (the horizontal pixel coordinate of the click relative to the canvas’s left edge, starting at 0) and y (the vertical pixel coordinate relative to the canvas’s top edge, starting at 0). To remove a previously registered callback, pass None.

Raises:

TypeError – If the provided callback is not a callable function (or None).

Example

>>> def draw_dot_on_click(x, y):
...     print(f"Canvas clicked at ({x}, {y}). Drawing a small circle there.")
...     # Draw a small green circle at the click location
...     canvas.draw_circle(x, y, 5, fill_color='green')
...
>>> canvas = sidekick.Canvas(250, 250)
>>> canvas.on_click(draw_dot_on_click)
>>> print("Canvas created. Click on the canvas in the Sidekick panel!")
>>> # Keep the script running to listen for click events
>>> sidekick.run_forever()
remove()[source]

Removes the canvas from the Sidekick UI and cleans up associated resources.

This performs the necessary cleanup actions:

  1. Destroys Offscreen Buffers: Sends commands to the Sidekick UI to destroy any hidden offscreen buffers that were created for this canvas instance via canvas.buffer(), releasing their resources in the UI.

  2. Calls Base `remove()`: Invokes the BaseModule.remove() method, which: a. Unregisters the internal message handler for this canvas. b. Resets registered callbacks (on_click, on_error) to None. c. Sends the final ‘remove’ command to the Sidekick UI to delete the

    main canvas element itself.

After calling remove(), you should no longer interact with this Canvas object.

Raises:

SidekickConnectionError (or subclass) – Can occur if sending the ‘destroyBuffer’ or the final ‘remove’ command fails. Cleanup of local Python resources (callbacks, handlers, buffer pool state) will still be attempted.

property width: int

The width of the canvas in pixels (read-only).

Type:

int

exception sidekick.SidekickConnectionError[source]

Bases: Exception

Base error for all Sidekick connection-related problems.

Catch this exception type if you want to handle any issue related to establishing or maintaining the connection to the Sidekick panel.

Example

>>> try:
...     console = sidekick.Console() # Connection happens here
...     console.print("Connected!")
... except sidekick.SidekickConnectionError as e:
...     print(f"Could not connect to Sidekick: {e}")
exception sidekick.SidekickConnectionRefusedError(url: str, original_exception: Exception)[source]

Bases: SidekickConnectionError

Raised when the library fails to connect to the Sidekick server initially.

This usually means the Sidekick WebSocket server wasn’t running or couldn’t be reached at the configured URL (ws://localhost:5163 by default).

Common Causes:

  1. The Sidekick panel isn’t open and active in VS Code.

  2. The Sidekick VS Code extension isn’t running correctly or has encountered an error.

  3. The WebSocket server couldn’t start (e.g., the port is already in use by another application). Check VS Code’s “Sidekick Server” output channel for details.

  4. A firewall is blocking the connection between your script and VS Code.

  5. The URL was changed via sidekick.set_url() to an incorrect address.

url

The WebSocket URL that the connection attempt was made to.

Type:

str

original_exception

The lower-level error that caused the failure (e.g., ConnectionRefusedError from the OS, TimeoutError from the websocket library).

Type:

Exception

exception sidekick.SidekickTimeoutError(timeout: float)[source]

Bases: SidekickConnectionError

Raised when connection to the server succeeds, but the Sidekick UI panel doesn’t respond.

After successfully connecting to the WebSocket server (run by the VS Code extension), the library waits a short time (a few seconds) for the Sidekick UI panel itself (the web content inside the panel) to finish loading and send back a signal confirming it’s ready to receive commands. If this signal doesn’t arrive within the timeout period, this error is raised.

Common Causes:

  1. The Sidekick panel is open in VS Code, but it hasn’t finished loading its HTML/JavaScript content yet (e.g., due to slow system performance or network issues if loading remote resources, though usually local).

  2. There’s an error within the Sidekick UI panel’s JavaScript code preventing it from initializing correctly. Check the Webview Developer Tools in VS Code (Command Palette -> “Developer: Open Webview Developer Tools”) for errors.

timeout

The number of seconds the library waited for the UI response.

Type:

float

exception sidekick.SidekickDisconnectedError(reason: str = 'Connection lost')[source]

Bases: SidekickConnectionError

Raised when the connection is lost after it was successfully established.

This indicates that communication was working previously, but the connection broke unexpectedly. This can happen if you try to send a command or if the background listener thread detects the disconnection.

Common Causes:

  1. The Sidekick panel was closed in VS Code while your script was still running.

  2. The Sidekick VS Code extension crashed, was disabled, or VS Code was closed.

  3. A network interruption occurred between the Python script and VS Code (less common for local connections but possible).

  4. An internal error occurred while trying to send or receive a message over the established connection.

Important: The library will not automatically try to reconnect if this error occurs. Any further attempts to use Sidekick modules (like grid.set_color()) will also fail until the script is potentially restarted and a new connection is established.

reason

A short description of why the disconnection occurred or was detected.

Type:

str