# Console

> A dev-tools style log with levels, filter, copy and clear. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API.

Source: https://albakore.github.io/pyzed/library/console/

:::tip[In plain words]
A list of messages from the app (info, warnings, errors) that you can filter and copy.
:::

`ui.Log` collects entries (thread-safe, capped at `limit`, repeated lines folded) and can capture `print` and tracebacks. `ui.console` shows them like a browser's dev tools console: a filter box, one toggle per level with its count, copy and clear. Levels are `debug`, `info`, `event`, `stdout`, `stderr`, `warning` and `error` (`ui.LOG_LEVELS`). With a `ui.LogSelection` the lines can be selected (click, Shift+click, Ctrl+click) and a selected line shows its `detail` (a traceback); `on_context` opens your menu on right click, and `sources` adds a dropdown to show the lines of one source.

`ui.console` · Category: [Code and tools](https://albakore.github.io/pyzed/library/#code-and-tools) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#console)

**Code:**

```python
log = ui.Log()
log.write("info", "Loaded ui/main.pzform")
log.write("event", "button1.click")
log.write("stdout", "Saving 3 files…")
log.write("warning", "settings.json not found, using defaults")
log.write("error", "ValueError: invalid literal for int()")
pz.container(ui.console(log.entries, on_clear=on_clear, on_copy=on_copy, on_query=on_query),
             width=600, height=220)
```

## When to use

- For a debug panel in your app, or to show what a background job is doing.

## Usage

```python
import pyzed as pz
from pyzed import ui

class Debug(pz.App):
    def __init__(self):
        self.log = ui.Log(on_write=lambda: pz.post("log"))  # redraw when a line arrives
        self.log.capture()                                   # also collect print() and stderr
        self.query, self.hidden = "", set()

    def view(self):
        return ui.console(self.log.entries, query=self.query,
                          on_query=lambda q: setattr(self, "query", q),
                          hidden=self.hidden, on_toggle_level=lambda lvl: self.hidden.symmetric_difference_update({lvl}),
                          on_clear=self.log.clear, on_copy=lambda: pz.Command.copy(self.log.text()))
```

## Variants

:::note[About these captures]
Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks.
:::

### Options

```python
log = ui.Log()
for level in ui.LOG_LEVELS:
    log.write(level, f"{level} message")
pz.container(ui.console(log.entries, on_clear=on_clear), width=520, height=220)
```

```python
log = ui.Log()
for level in ui.LOG_LEVELS:
    log.write(level, f"{level} message")
pz.container(ui.console(log.entries, hidden={"info", "event"}, on_toggle_level=on_toggle),
             width=520, height=200)
```

```python
log = ui.Log()
log.write("info", "started")
log.write("error", "ValueError: bad input")
pz.container(ui.console(log.entries, query="error", on_query=on_query), width=520, height=140)
```

## Events

Console has **8 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/).

| Event | Of | Handler gets | When |
| --- | --- | --- | --- |
| `on_query` | `ui.console` | `(text)` | Receives each edit of the filter. |
| `on_toggle_level` | `ui.console` | `(level)` | Called with the level of the clicked toggle. |
| `on_clear` | `ui.console` | `()` | Adds a Clear button. |
| `on_copy` | `ui.console` | `()` | Adds a Copy button. |
| `on_context` | `ui.console` | `(line)` | Called with the line right-clicked (`None` under the lines): open a menu there (copy, go to source, clear...). |
| `on_open` | `ui.console` | `(line)` | Called with the line double-clicked. |
| `on_source` | `ui.console` | `(source)` | Receives the source picked in the dropdown (`None` for all). |
| `on_write` | `ui.Log` | `()` | Called after every write (from any thread). |

**Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`.

### Handling the events

The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/).

**Imperative:**

```python
class Debug(pz.App):
    def __init__(self):
        self.log = ui.Log()
        self.log.write("info", "Started")
        self.log.write("error", "ValueError: bad input")
        self.log.on_write = lambda: pz.post("log")          # a line arrived (from any thread)
        self.query, self.hidden = "", set()

    def update(self, message):
        pass                             # "log" just redraws

    def view(self):
        return ui.console(self.log.entries, query=self.query, hidden=self.hidden,
                          on_query=lambda q: setattr(self, "query", q),               # the filter
                          on_toggle_level=lambda lvl: self.hidden.symmetric_difference_update({lvl}),
                          on_clear=self.log.clear,                                    # adds Clear
                          on_copy=lambda: pz.Command.copy(self.log.text()),           # adds Copy
                          on_open=lambda entry: print("open", entry.text))            # double click
```

**Declarative:**

```python
from dataclasses import dataclass

@dataclass
class Filter:
    query: str

@dataclass
class ToggleLevel:
    level: str

class Debug(pz.App):
    log = ui.Log()
    query, hidden = "", frozenset()

    def update(self, message):
        match message:
            case Filter(query):
                self.query = query
            case ToggleLevel(level):
                self.hidden = self.hidden ^ {level}
            case "clear":
                self.log.clear()

    def view(self):
        return ui.console(self.log.entries, query=self.query, hidden=self.hidden, on_query=Filter,
                          on_toggle_level=ToggleLevel, on_clear="clear")
```

**Functional:**

```python
from dataclasses import dataclass

LOG = ui.Log()
LOG.write("info", "Started")

@dataclass(frozen=True)
class State:
    query: str = ""
    hidden: frozenset = frozenset()

def view(state, set_state):
    return ui.console(LOG.entries, query=state.query, hidden=state.hidden,
                      on_query=lambda q: set_state(query=q),
                      on_toggle_level=lambda lvl: set_state(hidden=state.hidden ^ {lvl}))
```

## Properties and API

### `ui.console`

```python
console(
    entries,
    *,
    query='',
    on_query=None,
    hidden=(),
    on_toggle_level=None,
    on_clear=None,
    on_copy=None,
    actions=(),
    selection=None,
    on_context=None,
    on_open=None,
    sources=(),
    source=None,
    on_source=None,
    show_source=False,
    placeholder=None,
    empty=None,
    font_size=12.5,
    id=None,
    theme=None,
)
```

A log view like a browser's dev tools console: a filter box, one toggle per level with its count, copy and clear, and the entries in a monospace list that keeps scrolling to the newest one.

| Property | Default | Description |
| --- | --- | --- |
| `entries` | *required* | `ui.LogEntry` items (`log.entries`). |
| `query` | `""` | Filter text. |
| `hidden` | `()` | Levels not shown. |
| `actions` | `()` | Extra widgets at the end of the toolbar. |
| `selection` | `None` | A `ui.LogSelection`: the lines can be selected with the mouse, and a selected line with `detail` shows it below. |
| `sources` | `()` | `(value, label)` pairs: adds a dropdown that shows the lines of one `source`. |
| `source` | `None` | The source shown (`None` for all). |
| `show_source` | `False` | Tags each line with its source. |
| `placeholder` | `None` | Text of the empty filter box. |
| `empty` | `None` | What shows while there are no lines. |
| `font_size` | `12.5` | Size of the entries. |
| `id` | `None` | Scrollable id. |
| `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). |

Events: `on_query`, `on_toggle_level`, `on_clear`, `on_copy`, `on_context`, `on_open`, `on_source` ([events](#events)).

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#console)

### `ui.Log`

```python
class Log
```

The entries of a `ui.console`: thread-safe, capped at `limit`, with repeated lines folded into one. `write(level, text)` adds a line; `capture()` (or `with log:`) copies `print` output and tracebacks into it; `pipe(stream)` reads a subprocess's output in a background thread; `text()` returns everything for copying (`text(ids)` only those lines). `write` also takes the line's `source`, `detail` (a traceback) and `origin` (`(path, line)`) and returns the entry; `find(id)` looks one up.

| Property | Default | Description |
| --- | --- | --- |
| `limit` | `5000` | Most entries kept. |

Events: `on_write` ([events](#events)).

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#log)

### `ui.LogEntry`

```python
@dataclass
class LogEntry(level, text, time=time.time(), count=1, source='', detail='', origin=None, id=0)
```

One line of a `ui.Log`: `level`, `text`, `time`, `count` (identical lines in a row), `source`, `detail`, `origin`, its number `id`, `clock` (`HH:MM:SS`) and `line()` (the entry as plain text).

| Property | Default | Description |
| --- | --- | --- |
| `level` | *required* | Level name. |
| `text` | *required* | The line. |
| `time` | `time()` | When it was written (`time.time()`). |
| `count` | `1` | Identical lines in a row. |
| `source` | `""` | What it comes from (`"project"`, `"agent"`...); `ui.console` can show and filter it. |
| `detail` | `""` | More text shown under the line while it is selected (a traceback). |
| `origin` | `None` | `(path, line)` of the code it is about. |
| `id` | `0` | Its number in the log (what a `ui.LogSelection` keeps). |

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#logentry)

### `ui.LogSelection`

```python
class LogSelection
```

The selected lines of a `ui.console`, like in a file list: a click selects one line, Shift+click a range from the last one clicked and Ctrl+click (Cmd on macOS) adds or removes one. Keep one in your app and pass it to `ui.console(selection=...)`; `select(ids)` selects exactly those (Ctrl+A), `clear()` none, `keep(entries)` forgets lines that are gone, and `ids` are the selected entries' `id`s for `log.text(ids)`.

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#logselection)

### `ui.LOG_LEVELS`

The levels `ui.console` colors, in the order of its filter buttons: debug, info, event, stdout, stderr, warning, error.

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#log_levels)

## Related

- [Terminal](https://albakore.github.io/pyzed/library/terminal/): A read-only terminal view with ANSI colors.
- [Editor](https://albakore.github.io/pyzed/library/editor/): A full code editor: highlighting, folding, minimap, git markers.
