Ir al contenido

Apps y utilidades

Programas de todos los días, cada uno un único archivo de Python en examples/ que funciona tal cual. Son cortos a propósito: lee uno de arriba abajo y habrás visto el estado, una vista, eventos y una suscripción trabajando juntos. Varios son versiones en Pyzed de las demos clásicas de Iced, la biblioteca de interfaces de Rust con la que dibuja Pyzed.

Añade, edita, completa, filtra y borra tareas; se guardan como JSON en tu carpeta de usuario, así que siguen ahí la próxima vez que abras la app.

La app Todos con unas tareas, una de ellas hecha
  • Un dataclass por tarea y una lista en el estado; el filtro es un control ui.segmented.
  • Haz doble clic en una tarea para editarla en el sitio; las tareas se escriben en disco en cada cambio.
  • Una versión más simple se construye paso a paso en el tutorial de la Lista de tareas.
Windows, macOS y Linux
uv run --with pyzed-ui python examples/todos.py
Código completo: examples/todos.py
examples/todos.py
"""Todos, after Iced's ``todos`` demo: add, edit, complete, filter and persist tasks.
python examples/todos.py
Tasks are saved as JSON next to your user config, so they survive restarts.
"""
import json
from dataclasses import asdict, dataclass, field
from itertools import count
from pathlib import Path
import pyzed as pz
from pyzed import ui
STORE = Path.home() / ".config" / "pyzed-todos.json"
_ids = count()
@dataclass
class Task:
text: str
done: bool = False
id: int = field(default_factory=lambda: next(_ids))
class Todos(pz.App):
title = "Todos"
size = (560, 680)
def __init__(self, store=STORE):
self.store = store
self.draft = ""
self.filter = "All"
self.editing = None # (task id, edited text)
self.tasks = self.load()
# -- persistence --------------------------------------------------------
def load(self):
try:
return [Task(t["text"], t["done"]) for t in json.loads(self.store.read_text())]
except (OSError, ValueError, KeyError, TypeError):
return [Task("Read the Pyzed examples"), Task("Build something nice", done=True)]
def save(self):
try:
self.store.parent.mkdir(parents=True, exist_ok=True)
self.store.write_text(json.dumps([{"text": t.text, "done": t.done} for t in self.tasks], indent=2))
except OSError:
pass
# -- actions --------------------------------------------------------------
def add(self):
if self.draft.strip():
self.tasks.append(Task(self.draft.strip()))
self.draft = ""
self.save()
def toggle(self, task):
task.done = not task.done
self.save()
def remove(self, task):
self.tasks.remove(task)
self.save()
def start_edit(self, task):
self.editing = (task.id, task.text)
return pz.Command.focus(f"edit-{task.id}")
def finish_edit(self):
task_id, text = self.editing
for task in self.tasks:
if task.id == task_id and text.strip():
task.text = text.strip()
self.editing = None
self.save()
def clear_done(self):
self.tasks = [t for t in self.tasks if not t.done]
self.save()
def on_key(self, combo):
if combo == "escape":
self.editing = None
# -- view -------------------------------------------------------------------
def visible(self):
if self.filter == "Active":
return [t for t in self.tasks if not t.done]
if self.filter == "Completed":
return [t for t in self.tasks if t.done]
return self.tasks
def task_row(self, task):
t = self.resolved_theme()
if self.editing and self.editing[0] == task.id:
return pz.row([
ui.input(self.editing[1], lambda text: setattr(self, "editing", (task.id, text)),
on_submit=self.finish_edit, id=f"edit-{task.id}"),
ui.icon_button("check", self.finish_edit, tooltip="Save"),
ui.icon_button("trash-2", lambda: self.remove(task), tooltip="Delete", color=t.error),
], spacing=6, align="center")
return pz.row([
ui.checkbox(task.done, lambda _: self.toggle(task)),
pz.mouse_area(
ui.label(task.text, color="text_muted" if task.done else "text", width="fill"),
on_double_click=lambda: self.start_edit(task),
),
ui.icon_button("pencil", lambda: self.start_edit(task), tooltip="Edit"),
], spacing=10, align="center")
def view(self):
t = self.resolved_theme()
left = sum(not task.done for task in self.tasks)
tasks = self.visible()
body = (pz.column([self.task_row(task) for task in tasks], spacing=6)
if tasks else ui.empty_state("Nothing here", f"No {self.filter.lower()} tasks."))
return pz.container(
pz.column([
ui.heading("todos"),
ui.input(self.draft, lambda text: setattr(self, "draft", text), on_submit=self.add,
placeholder="What needs to be done?", icon="plus"),
pz.row([
ui.muted(f"{left} {'task' if left == 1 else 'tasks'} left"),
pz.hspace(),
ui.segmented(["All", "Active", "Completed"], self.filter, lambda f: setattr(self, "filter", f)),
], align="center"),
pz.scrollable(body, height="fill"),
pz.row([pz.hspace(), ui.button("Clear completed", self.clear_done, variant="ghost", size="sm")]),
], spacing=14, max_width=520),
padding=24, width="fill", height="fill", align_x="center", bg=t.editor,
)
if __name__ == "__main__":
pz.run(Todos)

Un editor de Markdown en vivo: escribe a la izquierda, mira el resultado con formato a la derecha y arrastra el divisor entre los dos.

Texto Markdown a la izquierda y su vista previa a la derecha
  • ui.editor con on_change guarda el texto en el estado; ui.markdown lo muestra con formato.
  • ui.split hace que los dos paneles se puedan redimensionar; los enlaces de la vista previa se abren en tu navegador.
Windows, macOS y Linux
uv run --with pyzed-ui python examples/markdown_viewer.py
Código completo: examples/markdown_viewer.py
examples/markdown_viewer.py
"""A live Markdown editor: code editor on the left, rendered preview on the right.
python examples/markdown_viewer.py
Shows ``ui.editor`` with ``on_change``, ``ui.markdown`` and a draggable ``ui.split``.
Links in the preview open in your browser.
"""
import webbrowser
import pyzed as pz
from pyzed import ui
SAMPLE = """# Pyzed
Build **fast** desktop apps in *Python*, rendered by [Iced](https://iced.rs).
## Features
- Zed-style components in `pyzed.ui`
- Themes, including the ones from [Zed](https://zed.dev)
- `async def` everywhere
```python
import pyzed as pz
class Hello(pz.App):
def view(self):
return pz.text("Hello!")
```
> Tip: drag the divider to resize the panes.
"""
class MarkdownViewer(pz.App):
title = "Markdown"
size = (1100, 720)
def __init__(self):
self.source = SAMPLE
self.ratio = 0.5
self.wrap = True
def view(self):
t = self.resolved_theme()
editor = pz.column([
pz.container(
pz.row([ui.label("Markdown", size="sm", color="text_muted"), pz.hspace(),
ui.checkbox(self.wrap, lambda v: setattr(self, "wrap", v), label="Wrap")], align="center"),
padding=(6, 12), bg=t.tab_bar, width="fill",
),
ui.editor("source", self.source, language="md", wrap=self.wrap,
on_change=lambda text: setattr(self, "source", text)),
])
preview = pz.container(
pz.scrollable(pz.container(ui.markdown(self.source, on_link=webbrowser.open), padding=24, max_width=760),
height="fill"),
width="fill", height="fill", bg=t.background,
)
return ui.split(editor, preview, ratio=self.ratio, on_resize=lambda r: setattr(self, "ratio", r))
if __name__ == "__main__":
pz.run(MarkdownViewer)

Muestra una imagen con pz.image y en un pz.image_viewer con zoom: la rueda hace zoom y arrastrar la mueve. Pasa una ruta para abrir tu propia imagen; sin ella dibuja una imagen a partir de píxeles generados en Python puro.

Una imagen a tamaño normal y en un visor con zoom
Windows, macOS y Linux
uv run --with pyzed-ui python examples/image_viewer.py
Código completo: examples/image_viewer.py
examples/image_viewer.py
"""Bitmap images: ``pz.image`` and the zoomable ``pz.image_viewer``.
python examples/image_viewer.py [picture.png]
Without an argument it shows a picture generated in pure Python (raw RGBA pixels
passed with ``size=(w, h)``). Scroll to zoom the viewer and drag to pan.
"""
import math
import sys
import pyzed as pz
from pyzed import ui
W, H = 480, 320
def generated_pixels():
"""A colorful plasma pattern as raw RGBA bytes."""
out = bytearray()
for y in range(H):
for x in range(W):
v = math.sin(x / 23) + math.sin(y / 17) + math.sin((x + y) / 31) + math.sin(math.hypot(x - W / 2, y - H / 2) / 19)
out += bytes((int(128 + 127 * math.sin(v * math.pi)), int(128 + 127 * math.sin(v * math.pi + 2)),
int(128 + 127 * math.sin(v * math.pi + 4)), 255))
return bytes(out)
class Viewer(pz.App):
title = "Image viewer"
size = (1000, 640)
def __init__(self, path=None):
self.path = path
self.pixels = None if path else generated_pixels()
self.fit = "contain"
self.radius = 12.0
self.opacity = 1.0
def source(self, **kwargs):
if self.path:
return dict(source=self.path, **kwargs)
return dict(source=self.pixels, size=(W, H), **kwargs)
def view(self):
t = self.resolved_theme()
thumbs = pz.column([
ui.label("pz.image", weight="semibold"),
ui.field("Fit", ui.select(["contain", "cover", "fill", "none", "scale_down"], self.fit,
lambda v: setattr(self, "fit", v), width=130)),
ui.field("Radius", pz.container(ui.slider(self.radius, lambda v: setattr(self, "radius", v), min=0, max=60),
width=130)),
ui.field("Opacity", pz.container(ui.slider(self.opacity, lambda v: setattr(self, "opacity", v)),
width=130)),
pz.container(pz.image(**self.source(width="fill", height=200, content_fit=self.fit, radius=self.radius,
opacity=self.opacity)),
width="fill", height=200, bg=t.surface, radius=self.radius),
pz.row([pz.image(**self.source(width=64, height=64, content_fit="cover", radius=32)),
pz.image(**self.source(width=64, height=64, content_fit="cover", radius=8, rotation=15)),
pz.image(**self.source(width=64, height=64, content_fit="cover", filter="nearest", scale=2))],
spacing=12),
], spacing=12, width=320)
viewer = pz.column([
ui.label("pz.image_viewer (scroll to zoom, drag to pan)", weight="semibold"),
pz.container(pz.image_viewer(**self.source(width="fill", height="fill")), width="fill", height="fill",
bg=t.surface, radius=t.radius, clip=True),
], spacing=12, width="fill")
return pz.container(pz.row([thumbs, viewer], spacing=20), padding=20, bg=t.background,
width="fill", height="fill")
if __name__ == "__main__":
pz.run(Viewer(sys.argv[1] if len(sys.argv) > 1 else None))

Un reloj analógico dibujado en un pz.canvas. Una suscripción, pz.every(1, ...), lo redibuja cada segundo, y las agujas son trazos girados alrededor del centro.

Un reloj analógico
Windows, macOS y Linux
uv run --with pyzed-ui python examples/clock.py
Código completo: examples/clock.py
examples/clock.py
"""An analog clock drawn on a canvas, after Iced's ``clock`` demo.
python examples/clock.py
``pz.every(1, ...)`` redraws it every second; the hands are rotated paths.
"""
import math
import time
import pyzed as pz
from pyzed import draw
SIZE = 320
def hand(cx, cy, angle, length, width, color):
a = math.radians(angle - 90)
return draw.line(cx, cy, cx + length * math.cos(a), cy + length * math.sin(a), stroke=color, width=width,
cap="round")
class Clock(pz.App):
title = "Clock"
size = (SIZE + 40, SIZE + 80)
def __init__(self):
self.now = time.localtime()
def subscriptions(self):
return [pz.every(1.0, lambda: setattr(self, "now", time.localtime()))]
def view(self):
t = self.resolved_theme()
c = SIZE / 2
r = c - 8
shapes = [draw.circle(c, c, r, fill=t.surface, stroke=t.border, width=2)]
for i in range(60):
a = math.radians(i * 6)
outer, inner = r - 6, r - (18 if i % 5 == 0 else 11)
shapes.append(draw.line(c + inner * math.sin(a), c - inner * math.cos(a),
c + outer * math.sin(a), c - outer * math.cos(a),
stroke=t.text if i % 5 == 0 else t.text_muted, width=2.5 if i % 5 == 0 else 1))
for h in range(1, 13):
a = math.radians(h * 30)
shapes.append(draw.text(c + (r - 38) * math.sin(a), c - (r - 38) * math.cos(a), h, color=t.text,
size=16, align_x="center", align_y="center"))
hours, minutes, seconds = self.now.tm_hour % 12, self.now.tm_min, self.now.tm_sec
shapes += [
hand(c, c, hours * 30 + minutes / 2, r * 0.5, 6, t.text),
hand(c, c, minutes * 6 + seconds / 10, r * 0.75, 4, t.text),
hand(c, c, seconds * 6, r * 0.82, 2, t.error),
draw.circle(c, c, 5, fill=t.error),
]
return pz.container(
pz.column([pz.canvas(shapes, width=SIZE, height=SIZE),
pz.text(time.strftime("%H:%M:%S", self.now), size=20, font="mono", color=t.text)],
spacing=12, align="center"),
center=True, bg=t.background,
)
if __name__ == "__main__":
pz.run(Clock)

Un cronómetro con vueltas. Espacio lo arranca y lo para, L guarda una vuelta y R lo reinicia.

Un cronómetro con una lista de vueltas
  • La suscripción del temporizador solo existe mientras el cronómetro corre: si no, subscriptions() devuelve una lista vacía, así que una app en reposo no usa CPU.
Windows, macOS y Linux
uv run --with pyzed-ui python examples/stopwatch.py
Código completo: examples/stopwatch.py
examples/stopwatch.py
"""A stopwatch with laps, after Iced's ``stopwatch`` demo.
python examples/stopwatch.py
The timer subscription only exists while the watch runs, so an idle app uses no CPU.
Space starts/stops, ``l`` records a lap, ``r`` resets.
"""
import time
import pyzed as pz
from pyzed import ui
def fmt(seconds):
minutes, rest = divmod(seconds, 60)
return f"{int(minutes):02d}:{rest:05.2f}"
class Stopwatch(pz.App):
title = "Stopwatch"
size = (420, 520)
def __init__(self):
self.elapsed = 0.0
self.started = None
self.laps = []
@property
def running(self):
return self.started is not None
def now(self):
return self.elapsed + (time.monotonic() - self.started if self.running else 0.0)
def toggle(self):
if self.running:
self.elapsed = self.now()
self.started = None
else:
self.started = time.monotonic()
def reset(self):
self.elapsed, self.started, self.laps = 0.0, None, []
def lap(self):
if self.running:
self.laps.insert(0, self.now())
def subscriptions(self):
# An empty message still triggers a redraw; ``now()`` does the rest.
return [pz.every(0.03, lambda: None)] if self.running else []
def on_key(self, combo):
{"space": self.toggle, "l": self.lap, "r": self.reset}.get(combo, lambda: None)()
def view(self):
t = self.resolved_theme()
laps = [
pz.row([ui.muted(f"Lap {len(self.laps) - i}"), pz.hspace(), pz.text(fmt(v), font="mono", color=t.text)])
for i, v in enumerate(self.laps)
]
return pz.container(
pz.column([
pz.text(fmt(self.now()), size=56, font="mono", color=t.text),
pz.row([
ui.button("Stop" if self.running else "Start", self.toggle, variant="solid",
tint="error" if self.running else "success", keys="space"),
ui.button("Lap", self.lap, disabled=not self.running, keys="l"),
ui.button("Reset", self.reset, variant="ghost", keys="r"),
], spacing=8),
pz.scrollable(pz.column(laps, spacing=6, width="fill"), height="fill"),
], spacing=20, align="center", max_width=340),
padding=28, width="fill", height="fill", align_x="center", bg=t.editor,
)
if __name__ == "__main__":
pz.run(Stopwatch)

Escribe un texto o un enlace y obtén su código QR, con los colores del tema o en blanco y negro. Toda la app son unas 30 líneas: pz.qr_code(data, cell_size=7) hace el trabajo.

Un código QR generado a partir de un enlace
Windows, macOS y Linux
uv run --with pyzed-ui python examples/qr_code.py
Código completo: examples/qr_code.py
examples/qr_code.py
"""Type something and get its QR code, after Iced's ``qr_code`` demo."""
import pyzed as pz
from pyzed import ui
class QR(pz.App):
title = "QR code"
size = (420, 520)
def __init__(self):
self.data = "https://github.com/albakore/pyzed"
self.dark = True
def view(self):
t = self.resolved_theme()
fg, bg = (t.text, t.surface) if self.dark else ("#000000", "#ffffff")
return pz.container(
pz.column([
ui.heading("QR code", level=2),
ui.input(self.data, lambda v: setattr(self, "data", v), placeholder="Text or URL"),
ui.checkbox(self.dark, lambda v: setattr(self, "dark", v), label="Use theme colors"),
pz.container(pz.qr_code(self.data, cell_size=7, color=fg, bg=bg) if self.data else ui.muted("Empty"),
padding=16, bg=bg, radius=t.radius),
], spacing=14, align="center"),
padding=24, width="fill", height="fill", align_x="center", bg=t.background,
)
if __name__ == "__main__":
pz.run(QR)

Un temporizador de concentración con periodos de trabajo y descanso: la app que la guía Cómo pedir construye con un asistente de IA, un pedido cada vez.

Un temporizador Pomodoro en cuenta atrás
Windows, macOS y Linux
uv run --with pyzed-ui python examples/pomodoro.py
Código completo: examples/pomodoro.py
examples/pomodoro.py
"""A Pomodoro timer: the app built step by step in the AI prompting guide."""
import pyzed as pz
from pyzed import ui
WORK, BREAK = 25 * 60, 5 * 60
class Pomodoro(pz.App):
title = "Pomodoro"
size = (420, 380)
def __init__(self):
self.mode = "Focus"
self.left = WORK
self.running = False
self.done = 0
def subscriptions(self):
return [pz.every(1, self.tick, key="timer")] if self.running else []
def tick(self):
self.left -= 1
if self.left <= 0:
if self.mode == "Focus":
self.done += 1
self.mode = "Break" if self.mode == "Focus" else "Focus"
self.left = BREAK if self.mode == "Break" else WORK
def switch(self, mode):
self.mode, self.running = mode, False
self.left = WORK if mode == "Focus" else BREAK
def toggle(self):
self.running = not self.running
def reset(self):
self.running, self.left = False, WORK if self.mode == "Focus" else BREAK
def view(self):
total = WORK if self.mode == "Focus" else BREAK
return pz.center(pz.column([
ui.segmented(["Focus", "Break"], self.mode, self.switch),
pz.text(f"{self.left // 60:02d}:{self.left % 60:02d}", size=64, weight="bold"),
ui.progress(1 - self.left / total, height=8, width=280),
pz.row([
ui.button("Pause" if self.running else "Start", self.toggle,
icon="pause" if self.running else "play", variant="solid", size="lg"),
ui.icon_button("rotate-ccw", self.reset, tooltip="Reset"),
], spacing=8, align="center"),
ui.muted(f"{self.done} sessions today", size="sm"),
], spacing=18, align="center")).style(background="editor")
if __name__ == "__main__":
pz.run(Pomodoro)

ui.ColorPicker en sus tres modos: un color con transparencia, un degradado lineal y un relleno con máscara de opacidad. Devuelve un texto "#rrggbbaa", un pz.Gradient o un pz.Paint, y los tres sirven como background.

El selector de color en modo degradado: dos paradas, un ángulo, las barras de tono y alfa, y el degradado que produce
Windows, macOS y Linux
uv run --with pyzed-ui python examples/color_picker.py
Código completo: examples/color_picker.py
examples/color_picker.py
"""Color picker: a color with alpha, a linear gradient, or a fill with an opacity mask.
python examples/color_picker.py
``ui.ColorPicker`` reports a ``"#rrggbbaa"`` string, a ``pz.Gradient`` or a ``pz.Paint``;
all of them work as a ``background`` in ``widget.style``.
"""
import pyzed as pz
from pyzed import ui
class ColorPickerDemo(pz.App):
title = "Color picker"
size = (640, 560)
def __init__(self):
self.fill = pz.Gradient(["#6366f1", "#ec4899"], angle=135)
def pick(self, event):
self.fill = event.value
def view(self):
preview = pz.container(None, width="fill", height="fill").style(background=self.fill)
return pz.row([
ui.ColorPicker(value=self.fill, on_change=self.pick),
pz.column([
pz.stack([ui.checkerboard(320, 360, 12), preview], width=320, height=360).style(border=(1, "border")),
pz.text(repr(self.fill)).style(font="mono", size=12, color="text_muted"),
], spacing=10, width="fill"),
], spacing=20).style(padding=24, width="fill", height="fill")
if __name__ == "__main__":
pz.run(ColorPickerDemo)

El Juego de la vida de Conway: haz clic o arrastra para pintar celdas, Espacio para reproducir o pausar, N para avanzar una generación. Miles de celdas se dibujan como una sola figura en un pz.canvas, y pz.every mueve la simulación.

La simulación en marcha: un canvas, miles de celdas.
Windows, macOS y Linux
uv run --with pyzed-ui python examples/game_of_life.py
Código completo: examples/game_of_life.py
examples/game_of_life.py
"""Conway's Game of Life, after Iced's ``game_of_life`` demo.
python examples/game_of_life.py
Click or drag on the grid to paint cells, Space to play/pause, ``n`` to step once.
Shows ``pz.canvas`` drawing thousands of cells in one shape and ``pz.every`` driving
the simulation.
"""
import random
import pyzed as pz
from pyzed import draw, ui
CELL = 10
COLS, ROWS = 80, 56
def step(alive):
counts = {}
for x, y in alive:
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
if dx or dy:
key = ((x + dx) % COLS, (y + dy) % ROWS)
counts[key] = counts.get(key, 0) + 1
return {cell for cell, n in counts.items() if n == 3 or (n == 2 and cell in alive)}
class Life(pz.App):
title = "Game of Life"
size = (COLS * CELL + 48, ROWS * CELL + 120)
def __init__(self):
self.alive = set()
self.playing = False
self.speed = 10
self.generation = 0
self.painting = None # True paints cells, False erases them
self.randomize()
def randomize(self):
self.alive = {(x, y) for x in range(COLS) for y in range(ROWS) if random.random() < 0.18}
self.generation = 0
def clear(self):
self.alive.clear()
self.generation = 0
def tick(self):
self.alive = step(self.alive)
self.generation += 1
def subscriptions(self):
return [pz.every(1 / self.speed, self.tick, key="life")] if self.playing else []
def on_key(self, combo):
if combo == "space":
self.playing = not self.playing
elif combo == "n":
self.tick()
def cell_at(self, x, y):
return int(x // CELL) % COLS, int(y // CELL) % ROWS
def press(self, x, y):
cell = self.cell_at(x, y)
self.painting = cell not in self.alive
self.paint(x, y)
def paint(self, x, y):
if self.painting is None:
return
cell = self.cell_at(x, y)
(self.alive.add if self.painting else self.alive.discard)(cell)
def view(self):
t = self.resolved_theme()
grid = [draw.background(t.editor)]
grid.append(draw.rects(((x * CELL, y * CELL, CELL - 1, CELL - 1) for x, y in self.alive), fill=t.accent))
board = pz.canvas(
grid,
width=COLS * CELL,
height=ROWS * CELL,
on_press=self.press,
on_move=self.paint,
on_release=lambda: setattr(self, "painting", None),
cursor="crosshair",
)
controls = pz.row([
ui.button("Pause" if self.playing else "Play", lambda: setattr(self, "playing", not self.playing),
icon="pause" if self.playing else "play", variant="solid", keys="space"),
ui.button("Step", self.tick, icon="step-forward", disabled=self.playing),
ui.button("Random", self.randomize, icon="shuffle"),
ui.button("Clear", self.clear, icon="eraser"),
pz.hspace(),
ui.muted(f"Speed {self.speed}/s"),
pz.container(ui.slider(self.speed, lambda v: setattr(self, "speed", int(v)), min=1, max=60, step=1),
width=160),
ui.muted(f"Generation {self.generation} · {len(self.alive)} alive"),
], spacing=8, align="center")
return pz.container(pz.column([controls, board], spacing=12), padding=16, bg=t.background,
width="fill", height="fill")
if __name__ == "__main__":
pz.run(Life)

Varias descargas a la vez, cada una con su barra de progreso, y una ventana que nunca se congela. Cada descarga es un generador async def: cada valor que hace yield va a update mientras sigue corriendo en el loop de eventos de Pyzed. Cambia la transferencia simulada por streaming con httpx para descargar archivos reales.

Cuatro descargas, dos en curso con sus barras y una terminada, con un aviso
Windows, macOS y Linux
uv run --with pyzed-ui python examples/download_progress.py
Código completo: examples/download_progress.py
examples/download_progress.py
"""Concurrent downloads with live progress, after Iced's ``download_progress`` demo.
python examples/download_progress.py
Each download is an ``async def`` generator: every value it yields goes to ``update``
while it keeps running on Pyzed's asyncio loop, so the UI never blocks. Swap the
simulated transfer for ``aiohttp``/``httpx`` streaming to download real files.
"""
import asyncio
import random
import pyzed as pz
from pyzed import ui
async def download(index, size):
"""Simulates a transfer that sometimes fails; yields ``(index, event, value)``."""
received = 0
while received < size:
await asyncio.sleep(random.uniform(0.02, 0.08))
received = min(size, received + random.randint(20_000, 120_000))
if random.random() < 0.004:
yield index, "failed", "connection reset"
return
yield index, "progress", received / size
yield index, "finished", size
class Download:
def __init__(self, index):
self.index = index
self.size = random.randint(2, 12) * 1_000_000
self.state = "idle" # idle | running | finished | failed
self.progress = 0.0
self.error = ""
class Downloads(pz.App):
title = "Downloads"
size = (560, 520)
def __init__(self):
self.downloads = [Download(0)]
def start(self, item):
item.state, item.progress = "running", 0.0
# Returning an async generator runs it in the background.
return download(item.index, item.size)
def add(self):
self.downloads.append(Download(len(self.downloads)))
def start_all(self):
return [pz.Command.perform(self.start(d)) for d in self.downloads if d.state != "running"]
def update(self, message):
index, event, value = message
item = self.downloads[index]
if event == "progress":
item.progress = value
elif event == "finished":
item.state, item.progress = "finished", 1.0
elif event == "failed":
item.state, item.error = "failed", value
def row(self, item):
t = self.resolved_theme()
mb = item.size / 1_000_000
if item.state == "idle":
status, action = ui.muted(f"{mb:.0f} MB"), ui.button("Start", lambda: self.start(item), size="sm")
elif item.state == "running":
status, action = ui.muted(f"{item.progress * mb:.1f} / {mb:.0f} MB"), ui.spinner(size=14)
elif item.state == "finished":
status = ui.label("Done", color="success", size="sm")
action = ui.icon_button("rotate-ccw", lambda: self.start(item), tooltip="Again")
else:
status = ui.label(f"Failed: {item.error}", color="error", size="sm")
action = ui.button("Retry", lambda: self.start(item), size="sm", tint="error")
return ui.card(pz.column([
pz.row([ui.label(f"file-{item.index + 1}.bin", weight="medium"), pz.hspace(), status, action],
spacing=10, align="center"),
ui.progress(item.progress, tint="error" if item.state == "failed" else "accent",
theme=t),
], spacing=8), padding=12)
def subscriptions(self):
# Keep the spinners turning while something is downloading.
busy = any(d.state == "running" for d in self.downloads)
return [pz.every(1 / 30, lambda: None)] if busy else []
def view(self):
t = self.resolved_theme()
return pz.container(
pz.column([
pz.row([ui.heading("Downloads", level=2), pz.hspace(),
ui.button("Add", self.add, icon="plus"),
ui.button("Start all", self.start_all, icon="download", variant="solid")], spacing=8,
align="center"),
pz.scrollable(pz.column([self.row(d) for d in self.downloads], spacing=8), height="fill"),
], spacing=16),
padding=24, width="fill", height="fill", bg=t.background,
)
if __name__ == "__main__":
pz.run(Downloads)

La misma idea con hilos (para código bloqueante) se explica en el tutorial Tareas en segundo plano.

Un panel de cotizaciones alimentado por pz.listen: una suscripción respaldada por un generador async. Corre mientras subscriptions() la devuelva y envía cada valor que produce a update.

Cuatro símbolos con su precio, su variación y un pequeño gráfico
async def feed(): # una app real leería aquí un websocket
prices = {s: random.uniform(50, 150) for s in SYMBOLS}
while True:
await asyncio.sleep(0.25)
symbol = random.choice(SYMBOLS)
prices[symbol] *= 1 + random.gauss(0, 0.01)
yield symbol, prices[symbol] # cada valor va a update()
class Ticker(pz.App):
def subscriptions(self):
return [pz.listen(feed)] if self.connected else [] # el interruptor pausa el flujo
def update(self, message):
symbol, price = message
self.history[symbol] = (self.history[symbol] + [price])[-60:]
Windows, macOS y Linux
uv run --with pyzed-ui python examples/websocket_stream.py
Código completo: examples/websocket_stream.py
examples/websocket_stream.py
"""Live data with ``pz.listen``: a subscription backed by an async generator.
python examples/websocket_stream.py
``listen`` runs the generator for as long as it is returned from ``subscriptions()``
and sends every value it yields to ``update``. Here it simulates a price feed; replace
the body with ``async for message in websocket:`` to consume a real socket.
"""
import asyncio
import random
import pyzed as pz
from pyzed import ui
SYMBOLS = ["PYZ", "ICE", "ZED", "RST"]
async def feed():
prices = {s: random.uniform(50, 150) for s in SYMBOLS}
while True:
await asyncio.sleep(0.25)
symbol = random.choice(SYMBOLS)
prices[symbol] *= 1 + random.gauss(0, 0.01)
yield symbol, prices[symbol]
class Ticker(pz.App):
title = "Live feed"
size = (720, 460)
def __init__(self):
self.connected = True
self.history = {s: [] for s in SYMBOLS}
def subscriptions(self):
return [pz.listen(feed)] if self.connected else []
def update(self, message):
symbol, price = message
self.history[symbol] = (self.history[symbol] + [price])[-60:]
def view(self):
t = self.resolved_theme()
cards = []
for symbol, values in self.history.items():
if len(values) < 2:
cards.append(ui.stat(symbol, "…", width="fill"))
continue
delta = (values[-1] / values[0] - 1) * 100
cards.append(ui.stat(symbol, f"{values[-1]:.2f}", delta=delta, trend=values, width="fill"))
return pz.container(
pz.column([
pz.row([ui.heading("Live feed", level=2), pz.hspace(),
ui.switch(self.connected, lambda v: setattr(self, "connected", v)),
ui.muted("connected" if self.connected else "paused")], spacing=8, align="center"),
pz.row(cards[:2], spacing=12),
pz.row(cards[2:], spacing=12),
], spacing=16),
padding=24, width="fill", height="fill", bg=t.background,
)
if __name__ == "__main__":
pz.run(Ticker)

Un chat cuyas respuestas aparecen palabra a palabra. Habla con cualquier servidor compatible con OpenAI (Ollama, LM Studio, OpenAI…) a través de httpx, y tiene un modo demo con respuestas predefinidas, así que funciona sin conexión.

Una ventana de chat con una respuesta en streaming
Windows, macOS y Linux
uv run --with pyzed-ui --with httpx python examples/ai_chat.py

Para usar un modelo real, define tres variables de entorno antes de ejecutarlo, por ejemplo con Ollama en tu máquina: CHAT_BASE_URL=http://localhost:11434/v1 y CHAT_MODEL=llama3.2 (más CHAT_API_KEY para un servicio en la nube). La sección de IA explica el resto.

Código completo: examples/ai_chat.py
examples/ai_chat.py
"""A streaming AI chat: any OpenAI-compatible server through httpx, with a built-in demo stream.
python examples/ai_chat.py # demo: canned answers, no network
CHAT_BASE_URL=http://localhost:11434/v1 CHAT_MODEL=llama3.2 python examples/ai_chat.py # Ollama
CHAT_BASE_URL=https://api.openai.com/v1 CHAT_API_KEY=sk-... CHAT_MODEL=<model> python examples/ai_chat.py
The answer streams token by token: ``send`` returns an async generator, Pyzed runs it on its asyncio
loop and every value it yields goes to ``update``, so the window keeps responding while the model writes.
"""
import asyncio
import json
import os
import time
import pyzed as pz
from pyzed import ui
BASE_URL = os.environ.get("CHAT_BASE_URL", "") # empty: demo mode
API_KEY = os.environ.get("CHAT_API_KEY", "")
MODEL = os.environ.get("CHAT_MODEL", "demo" if not BASE_URL else "llama3.2")
SYSTEM = "You are a concise assistant inside a desktop app. Answer in Markdown."
SUGGESTIONS = [
("code", "Write a Python function that removes duplicates but keeps the order"),
("lightbulb", "Explain async/await like I'm new to Python"),
("table", "Compare SQLite, PostgreSQL and DuckDB in a table"),
]
DEMO_ANSWER = """Sure! A `dict` keeps insertion order, so it does the work for you:
```python
def unique(items):
\"\"\"Removes duplicates and keeps the first occurrence of each item.\"\"\"
return list(dict.fromkeys(items))
unique([3, 1, 3, 2, 1]) # [3, 1, 2]
```
- **O(n)**: one pass, no nested loops.
- Works with any *hashable* item (numbers, strings, tuples).
- For unhashable items such as lists, keep a `seen` list instead.
"""
# -- the model ---------------------------------------------------------------------------------
async def openai_stream(messages, *, model=MODEL, base_url=BASE_URL, api_key=API_KEY):
"""Yields the answer's text pieces from an OpenAI-compatible ``/chat/completions`` endpoint."""
import httpx # only needed when talking to a real server
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
body = {"model": model, "messages": messages, "stream": True}
async with httpx.AsyncClient(timeout=httpx.Timeout(60, connect=10)) as client:
async with client.stream("POST", f"{base_url.rstrip('/')}/chat/completions", json=body,
headers=headers) as response:
if response.status_code >= 400:
detail = (await response.aread()).decode(errors="replace")[:300]
raise RuntimeError(f"HTTP {response.status_code}: {detail}")
async for line in response.aiter_lines(): # server-sent events: "data: {...}"
if not line.startswith("data:"):
continue
data = line[5:].strip()
if data == "[DONE]":
break
delta = json.loads(data)["choices"][0].get("delta", {}).get("content")
if delta:
yield delta
async def demo_stream(messages, *, delay=0.015):
"""A stand-in for the model: streams a canned answer a few characters at a time."""
text = DEMO_ANSWER
for i in range(0, len(text), 4):
await asyncio.sleep(delay)
yield text[i:i + 4]
def model_stream(messages):
return demo_stream(messages) if not BASE_URL else openai_stream(messages)
# -- the app -----------------------------------------------------------------------------------
class AIChat(pz.App):
title = "AI chat"
size = (980, 720)
def __init__(self, stream=model_stream):
self.stream = stream # a function: messages -> async iterator of text
self.messages = [] # [{"role": "user" | "assistant", "content": str}]
self.draft = ""
self.busy = False
self.error = ""
self.turn = 0 # tags each reply, so pieces of a stopped one are ignored
self.started = 0.0
self.tokens = 0
# -- actions --------------------------------------------------------------------------------
def send(self, text=None):
text = (text if text is not None else self.draft).strip()
if not text or self.busy:
return None
self.messages += [{"role": "user", "content": text}, {"role": "assistant", "content": ""}]
self.draft, self.error, self.busy = "", "", True
self.turn += 1
self.started, self.tokens = time.monotonic(), 0
history = [{"role": "system", "content": SYSTEM}] + self.messages[:-1]
return [self.reply(self.turn, history), pz.Command.scroll_to_end("thread")]
async def reply(self, turn, history):
"""Runs on Pyzed's asyncio loop; each yield goes to update() on the UI thread."""
try:
async for piece in self.stream(history):
if turn != self.turn: # stopped or superseded
return
yield ("delta", turn, piece)
except Exception as error: # network errors, bad keys... shown in the thread
yield ("error", turn, str(error) or type(error).__name__)
yield ("done", turn, None)
def stop(self):
self.turn += 1 # the running reply sees a new turn and ends
self.busy = False
def clear(self):
self.stop()
self.messages, self.error = [], ""
def update(self, message):
kind, turn, value = message
if turn != self.turn:
return None
if kind == "delta":
self.messages[-1]["content"] += value
self.tokens += 1
return pz.Command.scroll_to_end("thread")
if kind == "error":
self.error = value
elif kind == "done":
self.busy = False
return None
# -- view -----------------------------------------------------------------------------------
def bubble(self, index, message):
last = index == len(self.messages) - 1
if message["role"] == "user":
return pz.row([pz.hspace(), pz.container(ui.label(message["content"]), padding=(10, 14),
max_width=560).style(background="element", radius=14)])
content = message["content"]
if last and self.busy:
content += " ▍"
body = [ui.markdown(content) if content.strip() else
pz.row([ui.spinner(size=14), ui.muted("Thinking…")], spacing=8, align="center")]
if not (last and self.busy) and content:
body.append(pz.row([
ui.icon_button("copy", pz.Command.copy(message["content"]), size="sm", tooltip="Copy"),
ui.icon_button("refresh-cw", self.retry, size="sm", tooltip="Regenerate") if last else None,
ui.icon_button("thumbs-up", None, size="sm"), ui.icon_button("thumbs-down", None, size="sm"),
], spacing=2))
avatar = pz.container(pz.icon("sparkles", size=15, color="#ffffff"), width=30, height=30, center=True).style(
background=pz.Gradient(["#8b5cf6", "#ec4899"], angle=135), radius=15)
return pz.row([avatar, pz.column(body, spacing=6, width="fill")], spacing=12)
def retry(self):
if self.messages and not self.busy:
question = self.messages[-2]["content"]
self.messages = self.messages[:-2]
return self.send(question)
return None
def empty(self):
cards = [pz.button(pz.row([pz.icon(icon, size=16), ui.label(text, size="sm")], spacing=10, align="center"),
lambda text=text: self.send(text), width="fill", padding=(12, 14), radius=10,
bg=self.resolved_theme().surface, hover_bg=self.resolved_theme().element_hover,
border_color=self.resolved_theme().border_variant, border_width=1)
for icon, text in SUGGESTIONS]
return pz.container(pz.column([
pz.container(pz.icon("sparkles", size=26, color="#ffffff"), width=56, height=56, center=True).style(
background=pz.Gradient(["#8b5cf6", "#ec4899"], angle=135), radius=16, glow=("#8b5cf6", 18)),
ui.heading("How can I help?"),
ui.muted(f"Model: {MODEL}" + (" · demo stream, no network" if not BASE_URL else "")),
pz.space(height=8),
pz.column(cards, spacing=8, width=480),
], spacing=10, align="center"), width="fill", height="fill", center=True)
def composer(self):
action = (ui.icon_button("square", self.stop, variant="solid", tooltip="Stop") if self.busy else
ui.icon_button("arrow-up", self.send, variant="solid" if self.draft.strip() else "filled",
tooltip="Send", disabled=not self.draft.strip()))
return pz.container(pz.row([
ui.input(self.draft, lambda v: setattr(self, "draft", v), placeholder="Ask anything…",
on_submit=self.send, borderless=True, id="prompt", size="lg"),
action,
], spacing=8, align="center"), padding=(6, 8, 6, 12), max_width=760).style(
background="surface", border=(1, "border"), radius=16, shadow=("#00000026", (0, 4), 16))
def view(self):
speed = ""
if self.tokens and self.busy:
speed = f"{self.tokens / max(time.monotonic() - self.started, 0.001):.0f} chunks/s"
header = ui.toolbar(
[pz.icon("sparkles", size=14),
ui.label("Assistant", weight="semibold"), ui.badge(MODEL, tint="info", variant="soft")],
[ui.muted(speed, size="sm") if speed else None,
ui.icon_button("trash-2", self.clear, size="sm", tooltip="New chat")], height=44)
if self.messages:
thread = pz.column([self.bubble(i, m) for i, m in enumerate(self.messages)], spacing=22, max_width=760,
padding=(24, 24, 8, 24))
if self.error:
thread = pz.column([thread, pz.container(ui.callout("The model failed", self.error, tint="error",
icon="circle-alert"), max_width=760,
padding=(0, 24))])
middle = pz.scrollable(pz.container(thread, width="fill", align_x="center"), height="fill", id="thread",
anchor_bottom=True)
else:
middle = self.empty()
return pz.column([
header, ui.divider(), middle,
pz.container(self.composer(), width="fill", align_x="center", padding=(8, 24, 20, 24)),
], height="fill").style(background="editor")
if __name__ == "__main__":
pz.run(AIChat)

Abre tantas ventanas como quieras: cada pz.Window tiene su propia vista, título, tamaño y tema, pero todas comparten el estado de la app, así que el contador de clics es el mismo en todas. Cerrar la ventana principal cierra la app.

La ventana principal y dos ventanas más, una en Ayu Mirage y otra en Gruvbox Light, que comparten el contador de clics
Windows, macOS y Linux
uv run --with pyzed-ui python examples/multi_window.py
Código completo: examples/multi_window.py
examples/multi_window.py
"""Several windows in one app, after Iced's ``multi_window`` demo.
python examples/multi_window.py
Every ``pz.Window`` has its own view, title, size and theme, but they all share the
app's state: the counter below is the same in every window. Closing the main window
quits the app.
"""
import pyzed as pz
from pyzed import ui
class Note(pz.Window):
size = (360, 260)
def __init__(self, number, theme):
self.number = number
self.theme = theme
self.title = f"Window {number}"
self.text = ""
def on_close(self):
# The user clicked the window's close button.
self.app.windows.remove(self)
def dismiss(self):
self.app.windows.remove(self)
return self.close()
def view(self):
t = self.resolved_theme()
return pz.container(
pz.column([
ui.heading(self.title, level=3),
ui.muted(f"Theme: {t.name}"),
ui.input(self.text, lambda v: setattr(self, "text", v), placeholder="Type something…"),
pz.row([
ui.button(f"Shared clicks: {self.app.clicks}", self.app.click, icon="mouse-pointer-click"),
pz.hspace(),
ui.button("Close", self.dismiss, variant="ghost"),
], align="center"),
], spacing=12),
padding=20, width="fill", height="fill", bg=t.background,
)
class Main(pz.App):
title = "Multi window"
size = (480, 360)
def __init__(self):
self.clicks = 0
self.windows = []
self.opened = 0
def click(self):
self.clicks += 1
def open_window(self):
self.opened += 1
themes = pz.themes.all()
window = Note(self.opened, themes[self.opened % len(themes)])
self.windows.append(window)
return window.open()
def close_all(self):
commands = [w.close() for w in self.windows]
self.windows = []
return commands
def view(self):
t = self.resolved_theme()
items = [ui.list_item(w.title, None, icon="app-window", end=ui.muted(w.text or "empty")) for w in self.windows]
return pz.container(
pz.column([
ui.heading("Windows", level=2),
pz.row([ui.button("Open window", self.open_window, icon="plus", variant="solid"),
ui.button("Close all", self.close_all, disabled=not self.windows),
ui.button(f"Clicks: {self.clicks}", self.click)], spacing=8),
pz.column(items) if items else ui.muted("No extra windows yet."),
], spacing=14),
padding=24, width="fill", height="fill", bg=t.background,
)
if __name__ == "__main__":
pz.run(Main)

Paneles redimensionables como los de un editor: divide cualquier panel a la derecha o hacia abajo, arrastra los divisores y cierra paneles. La disposición es un pequeño árbol de listas guardado en el estado y dibujado con ui.split anidados.

Tres paneles: uno a la izquierda y dos apilados a la derecha
Windows, macOS y Linux
uv run --with pyzed-ui python examples/panes.py
Código completo: examples/panes.py
examples/panes.py
"""Resizable panes, after Iced's ``pane_grid`` demo, using nested ``ui.split``.
python examples/panes.py
Drag the dividers; each pane can be split again or closed. The layout is a small
tree of ``("h" | "v", ratio, first, second)`` tuples kept in your state.
"""
from itertools import count
import pyzed as pz
from pyzed import ui
_ids = count(1)
class Panes(pz.App):
title = "Panes"
size = (1000, 680)
def __init__(self):
self.layout = next(_ids) # a leaf is a pane number
self.focus = self.layout
def replace(self, node, target, new):
if node == target:
return new
if isinstance(node, list):
node[2] = self.replace(node[2], target, new)
node[3] = self.replace(node[3], target, new)
return node
def remove(self, node, target):
if isinstance(node, list):
if node[2] == target:
return node[3]
if node[3] == target:
return node[2]
node[2] = self.remove(node[2], target)
node[3] = self.remove(node[3], target)
return node
def split(self, pane, vertical):
new = next(_ids)
self.layout = self.replace(self.layout, pane, ["v" if vertical else "h", 0.5, pane, new])
self.focus = new
def close(self, pane):
if self.layout != pane:
self.layout = self.remove(self.layout, pane)
self.focus = None
def pane(self, number):
t = self.resolved_theme()
focused = number == self.focus
header = pz.container(
pz.row([
ui.label(f"Pane {number}", weight="medium", color="text" if focused else "text_muted"),
pz.hspace(),
ui.icon_button("columns-2", lambda: self.split(number, False), size="sm", tooltip="Split right"),
ui.icon_button("rows-2", lambda: self.split(number, True), size="sm", tooltip="Split down"),
ui.icon_button("x", lambda: self.close(number), size="sm", tooltip="Close",
disabled=self.layout == number),
], spacing=2, align="center"),
padding=(4, 8), width="fill", bg=t.tab_active if focused else t.tab_inactive,
)
body = pz.mouse_area(pz.center(ui.muted("Click to focus")), on_press=lambda: setattr(self, "focus", number))
return pz.container(pz.column([header, body]), width="fill", height="fill", bg=t.editor,
border_color=t.border_focused if focused else t.border_variant, border_width=1)
def render(self, node):
if not isinstance(node, list):
return self.pane(node)
direction, ratio, first, second = node
def resize(r, node=node):
node[1] = r
return ui.split(self.render(first), self.render(second), ratio=ratio, on_resize=resize,
vertical=direction == "v", min_size=120)
def view(self):
return pz.container(self.render(self.layout), padding=8, width="fill", height="fill",
bg=self.resolved_theme().background)
if __name__ == "__main__":
pz.run(Panes)

Un menú de clic derecho hecho con ui.popover y ui.menu. mouse_area(on_move=...) recuerda dónde está el puntero, así que el menú se abre justo ahí; hacer clic en otro sitio o pulsar Esc lo cierra.

Un menú contextual abierto sobre un panel
Windows, macOS y Linux
uv run --with pyzed-ui python examples/context_menu.py
Código completo: examples/context_menu.py
examples/context_menu.py
"""A right-click context menu built from ``ui.popover`` and ``ui.menu``.
python examples/context_menu.py
The pointer position is remembered with ``mouse_area(on_move=...)`` so the menu opens
where you clicked. Clicking anywhere else (or pressing Escape) closes it.
"""
import pyzed as pz
from pyzed import ui
class ContextMenu(pz.App):
title = "Context menu"
size = (640, 420)
def __init__(self):
self.pointer = (0, 0)
self.menu_at = None
self.log = ["Right-click anywhere in the box."]
self.soft_wrap = True
def act(self, what):
def run():
self.log.append(what)
self.menu_at = None
return run
def toggle_wrap(self):
self.soft_wrap = not self.soft_wrap
self.menu_at = None
def on_key(self, combo):
if combo == "escape":
self.menu_at = None
def view(self):
t = self.resolved_theme()
area = pz.mouse_area(
pz.container(pz.column([ui.muted(line) for line in self.log[-12:]], spacing=4), padding=16,
width="fill", height="fill", bg=t.editor, border_color=t.border_variant, border_width=1,
radius=t.radius),
on_move=lambda x, y: setattr(self, "pointer", (x, y)),
on_right_press=lambda: setattr(self, "menu_at", self.pointer),
)
menu = None
if self.menu_at:
menu = ui.menu([
ui.MenuItem("Cut", self.act("cut"), icon="scissors", keys="ctrl-x"),
ui.MenuItem("Copy", self.act("copy"), icon="copy", keys="ctrl-c"),
ui.MenuItem("Paste", self.act("paste"), icon="clipboard", keys="ctrl-v"),
ui.SEPARATOR,
ui.MenuItem("Soft wrap", self.toggle_wrap, checked=self.soft_wrap),
ui.SEPARATOR,
ui.MenuItem("Delete", self.act("delete"), icon="trash-2", danger=True),
], width=220)
x, y = self.menu_at or (0, 0)
return pz.container(
ui.popover(area, menu, x=x + 16, y=y + 16, on_dismiss=lambda: setattr(self, "menu_at", None)),
padding=16, width="fill", height="fill", bg=t.background,
)
if __name__ == "__main__":
pz.run(ContextMenu)

Tres piezas reutilizables escritas en código: una valoración con estrellas y un editor de etiquetas (clases pz.Component con propiedades, su propio estado y sus propios eventos), y una nota que puedes arrastrar, a la que puedes hacer doble clic y escribirle, que registra los eventos comunes de todos los widgets.

Una valoración con estrellas, un editor de etiquetas, una nota fijada y el registro de eventos
Windows, macOS y Linux
uv run --with pyzed-ui python examples/components.py
Código completo: examples/components.py
examples/components.py
"""Custom components and events: a star rating, a tag editor and a draggable note.
python examples/components.py
- ``Rating`` and ``TagInput`` are ``pz.Component`` classes: props, their own state,
their own events (``change``, ``add``, ``remove``), reusable anywhere.
- The note uses the common events every widget has: drag it around, double-click to
pin it, hover it, click it and press keys (they show up in the log).
"""
import pyzed as pz
from pyzed import ui
class Rating(pz.Component):
"""Stars the user can click; shows a preview while hovering."""
value: int = 0
max: int = 5
events = {"change": "A star was picked (``event.value`` is the new rating)."}
icon = "star"
size = (140, 28)
def setup(self):
self.hover = None # state: which star is under the pointer
def render(self):
t = pz.theme.current()
shown = self.hover or self.value
stars = []
for i in range(1, self.max + 1):
color = t.warning if i <= shown else t.text_disabled
stars.append(
pz.icon("star", size=20, color=color).on(
click=lambda i=i: self.emit("change", i),
mouse_enter=lambda i=i: setattr(self, "hover", i),
mouse_leave=lambda: setattr(self, "hover", None),
)
)
return pz.row(stars, spacing=2)
class TagInput(pz.Component):
"""A list of tags with a box to add more."""
tags: list = ()
placeholder: str = "Add a tag…"
events = {"add": "Enter was pressed (``event.value`` is the text).",
"remove": "A tag's x was clicked (``event.value`` is the tag)."}
icon = "tags"
size = (280, 64)
def setup(self):
self.draft = ""
def submit(self):
text, self.draft = self.draft.strip(), ""
return self.emit("add", text) if text else None
def render(self):
chips = [ui.chip(tag, on_remove=lambda tag=tag: self.emit("remove", tag)) for tag in self.tags]
return pz.column([
pz.row(chips, spacing=6, wrap=True, width="fill") if chips
else ui.label("No tags yet", color="text_muted", size="sm"),
ui.input(self.draft, lambda text: setattr(self, "draft", text), placeholder=self.placeholder,
on_submit=self.submit, icon="tag"),
], spacing=8)
class Demo(pz.App):
title = "Components and events"
size = (720, 460)
def __init__(self):
self.rating = 3
self.tags = ["python", "rust"]
self.note = (20, 40)
self.pinned = False
self.log = ["Drag the note, double-click it, click it and type."]
def record(self, event):
detail = event.key or (f"{event.dx:+.0f}, {event.dy:+.0f}" if event.type == "drag" else "")
self.log = [f"{event.type} {detail}".strip(), *self.log][:9]
def move(self, event):
if not self.pinned:
x, y = self.note
self.note = (x + event.dx, y + event.dy)
def view(self):
t = self.resolved_theme()
form = ui.card(pz.column([
ui.field("Rating", Rating(value=self.rating, on_change=lambda e: setattr(self, "rating", e.value))),
ui.label(f"{self.rating} of 5", color="text_muted", size="sm"),
ui.label("Tags"),
TagInput(
tags=self.tags,
on_add=lambda e: setattr(self, "tags", [*self.tags, e.value]),
on_remove=lambda e: setattr(self, "tags", [x for x in self.tags if x != e.value]),
),
], spacing=14), title="Components", width=320)
note = pz.container(
pz.column([
pz.row([pz.icon("pin" if self.pinned else "grip", size=14, color=t.text_muted),
ui.label("Pinned" if self.pinned else "Drag me", weight="semibold")], spacing=6, align="center"),
ui.label("Double-click to pin", size="sm", color="text_muted"),
], spacing=4),
width=180, padding=12, bg=t.elevated, border_color=t.accent if self.pinned else t.border, border_width=1,
radius=t.radius,
).on(
drag=self.move, drag_end=self.record, double_click=lambda: setattr(self, "pinned", not self.pinned),
click=self.record, key_down=self.record, focus=self.record, blur=self.record,
cursor="pointer" if self.pinned else "grab",
)
log = ui.card(pz.column([ui.label(line, mono=True, size="sm") for line in self.log], spacing=2),
title="Event log", width=320)
x, y = self.note
canvas = pz.stack([
pz.container(width="fill", height="fill"),
pz.pin(note, x=x, y=y),
], width="fill", height="fill")
return pz.row([pz.column([form, log], spacing=16), canvas], spacing=24, padding=24)
if __name__ == "__main__":
pz.run(Demo)

Para dibujar componentes con el mouse, sigue Un componente propio.

Un recorrido por los componentes listos de pyzed.ui: botones, campos, pestañas, un editor de código, menús, árboles, notificaciones, formularios, avisos y datos, con un selector de tema para verlos en cualquier tema.

La galería de componentes: botones, campos, pestañas, menús, árboles y notificaciones
Windows, macOS y Linux
uv run --with pyzed-ui python examples/gallery.py
Código completo: examples/gallery.py
examples/gallery.py
"""A tour of the ready-made components in ``pyzed.ui``.
python examples/gallery.py
"""
import pyzed as pz
from pyzed import ui
SNIPPET = '''from pyzed import ui
def view(self):
return ui.button("Deploy", self.deploy)
'''
class Gallery(pz.App):
title = "Pyzed components"
size = (1440, 900)
def __init__(self, theme=pz.themes.ONE_DARK):
self.theme = theme
self.name = "Pyzed"
self.query = ""
self.format_on_save = True
self.vim_mode = False
self.telemetry = True
self.font_size = 0.55
self.view_mode = "Split"
self.active_tab = "main.py"
self.selected_file = "src/app.py"
self.expanded = {"src"}
self.language = "Python"
self.size_choice = "Medium"
self.tab_size = 4
self.tags = {"rust", "python", "ui"}
self.advanced_open = True
self.sort_by = "Size"
self.descending = True
self.selected_row = None
def set(self, field):
return lambda value: setattr(self, field, value)
def section(self, title, *children, spacing=10):
return ui.card(pz.column(list(children), spacing=spacing), title=title, width="fill")
def view(self):
t = self.theme
buttons = self.section(
"Buttons",
pz.row([
ui.button("Filled", lambda: None),
ui.button("Ghost", lambda: None, variant="ghost"),
ui.button("Outline", lambda: None, variant="outline"),
ui.button("Solid", lambda: None, variant="solid"),
ui.button("Disabled", disabled=True),
], spacing=8),
pz.row([
ui.button("Error", lambda: None, variant="error", icon="circle-x"),
ui.button("Warning", lambda: None, variant="warning", icon="triangle-alert"),
ui.button("Success", lambda: None, variant="success", icon="circle-check"),
ui.button("Save", lambda: None, icon="save", keys="ctrl-s"),
], spacing=8),
pz.row([
ui.button("XS", lambda: None, size="xs"),
ui.button("Small", lambda: None, size="sm"),
ui.button("Medium", lambda: None, size="md"),
ui.button("Large", lambda: None, size="lg", icon="rocket", variant="solid"),
], spacing=8, align="center"),
pz.row([
ui.icon_button("files", lambda: None, tooltip="Project"),
ui.icon_button("search", lambda: None, selected=True),
ui.icon_button("git-branch", lambda: None),
ui.icon_button("bug", lambda: None),
ui.icon_button("settings", lambda: None, variant="filled"),
pz.space(width=12),
ui.avatar("KV", index=0), ui.avatar("AL", index=3), ui.avatar("MZ", index=7),
pz.space(width=12),
ui.indicator("success"), ui.indicator("warning"), ui.indicator("error"),
], spacing=6, align="center"),
)
badges = self.section(
"Badges & key bindings",
pz.row([
ui.badge("New"),
ui.badge("Beta", tint="warning"),
ui.badge("Stable", tint="success", icon="check"),
ui.badge("Deprecated", tint="error", variant="outline"),
ui.badge("v0.1.0", tint="accent", variant="solid"),
], spacing=6),
pz.row([ui.kbd("ctrl-shift-p"), ui.kbd("ctrl-k ctrl-s"), ui.kbd("alt-enter"), ui.kbd("escape")],
spacing=12, align="center"),
)
inputs = self.section(
"Inputs",
ui.input(self.name, self.set("name"), placeholder="Project name"),
ui.search_input(self.query, self.set("query"), placeholder="Search symbols…"),
ui.field("Format on save", ui.switch(self.format_on_save, self.set("format_on_save")),
description="Run the formatter whenever a buffer is saved"),
ui.field("Vim mode", ui.switch(self.vim_mode, self.set("vim_mode"))),
ui.checkbox(self.telemetry, self.set("telemetry"), label="Send anonymous usage data"),
pz.row([ui.label("Font size", size="sm", color="text_muted"),
ui.slider(self.font_size, self.set("font_size"))], spacing=12, align="center"),
pz.row([ui.label("Indexing", size="sm", color="text_muted"), ui.progress(0.68)],
spacing=12, align="center"),
ui.segmented(["Unified", "Split", "Inline"], self.view_mode, self.set("view_mode")),
spacing=12,
)
menu = self.section(
"Context menu",
ui.menu([
ui.MenuItem("Cut", lambda: None, icon="scissors", keys="ctrl-x"),
ui.MenuItem("Copy", lambda: None, icon="copy", keys="ctrl-c"),
ui.MenuItem("Paste", lambda: None, icon="clipboard", keys="ctrl-v"),
ui.SEPARATOR,
ui.MenuItem("Soft wrap", lambda: None, checked=True),
ui.MenuItem("Show minimap", lambda: None, checked=False),
ui.SEPARATOR,
ui.MenuItem("Delete file", lambda: None, icon="trash-2", danger=True),
], width=280),
)
files = ui.TreeNode.from_paths(
["src/app.py", "src/views/editor.py", "src/views/panel.py", "tests/test_app.py", "pyproject.toml", "README.md"],
{"src/app.py": "modified", "tests/test_app.py": "created"},
)
tree = self.section(
"Tree & list",
ui.tree(files, expanded=self.expanded, selected=self.selected_file,
on_toggle=lambda f: self.expanded.symmetric_difference_update({f}),
on_select=self.set("selected_file")),
ui.divider(),
ui.list_item("Recent projects", icon="history", end=ui.badge("3"), on_press=lambda: None),
ui.list_item("Extensions", icon="blocks", end=ui.kbd("ctrl-shift-x"), on_press=lambda: None),
spacing=6,
)
tabs = self.section(
"Tabs, breadcrumbs & editor",
pz.container(
pz.column([
ui.tab_bar(
[ui.Tab.for_file("main.py"), ui.Tab.for_file("theme.rs", dirty=True),
ui.Tab.for_file("README.md", preview=True)],
self.active_tab,
on_select=self.set("active_tab"),
on_close=lambda _: None,
end=[ui.icon_button("plus", lambda: None, size="sm")],
),
ui.toolbar([ui.breadcrumbs(["examples", self.active_tab, "def view"])]),
pz.container(ui.editor("gallery-snippet", SNIPPET, language="py"), height=150),
]),
border_color=t.border, border_width=1, radius=t.radius, clip=True,
),
)
toasts = self.section(
"Notifications",
ui.toast("Update available", "Pyzed 0.2 is ready to install.", kind="info",
actions=[ui.button("Restart", lambda: None, size="sm", variant="solid"),
ui.button("Later", lambda: None, size="sm", variant="ghost")], width=None),
ui.toast("Saved 3 files", kind="success", on_dismiss=lambda: None, width=None),
ui.toast("Language server crashed", "rust-analyzer exited with code 101.", kind="error",
on_dismiss=lambda: None, width=None),
)
forms = self.section(
"Forms",
ui.field("Language", ui.select(["Python", "Rust", "TypeScript", "Go"], self.language,
self.set("language"), width=160)),
ui.field("Tab size", ui.number_field(self.tab_size, self.set("tab_size"), min=1, max=8)),
ui.radio_group(["Small", "Medium", "Large"], self.size_choice, self.set("size_choice"), horizontal=True),
pz.row([ui.chip(tag, on_remove=lambda tag=tag: self.tags.discard(tag)) for tag in sorted(self.tags)],
spacing=6),
ui.disclosure("Advanced", ui.muted("Settings that most people never need."), open=self.advanced_open,
on_toggle=lambda: setattr(self, "advanced_open", not self.advanced_open)),
pz.row([ui.split_button("Run", lambda: None, lambda: None, icon="play"),
ui.toggle_group([("list", "list"), ("grid", "layout-grid")], "list", lambda v: None)],
spacing=8),
)
feedback = self.section(
"Feedback",
ui.callout("Update available", "Pyzed 0.2 is ready.", tint="info",
actions=[ui.button("Update", lambda: None, size="sm", variant="solid")]),
ui.callout("Build failed", "3 errors in src/main.rs", tint="error"),
pz.row([ui.count_badge(3), ui.count_badge(128, tint="error"), ui.facepile(["KV", "AL", "MZ", "JD", "RS", "TT"]),
ui.diff_stat(42, 7)], spacing=12, align="center"),
ui.keybinding_hint("Open file", "ctrl-p"),
)
files = [dict(name="main.rs", size=1200, lang="Rust"), dict(name="app.py", size=840, lang="Python"),
dict(name="README.md", size=310, lang="Markdown"), dict(name="theme.json", size=96, lang="JSON")]
def sort(column):
self.descending = not self.descending if self.sort_by == column else False
self.sort_by = column
data = self.section(
"Data",
ui.table([ui.Column("Name", "name"), ui.Column("Size", "size", width=70, align="end"),
ui.Column("Language", "lang")], files, sort_by=self.sort_by, descending=self.descending,
on_sort=sort, on_row_press=self.set("selected_row"), selected=self.selected_row, height=156),
pz.row([ui.stat("Requests", "12.4k", delta=4.2, trend=[3, 5, 4, 6, 8, 7, 9]),
ui.donut([5, 3, 2], label="62%", size=88)], spacing=12, align="center"),
ui.bar_chart([4, 8, 6, 9, 3], labels=["Mon", "Tue", "Wed", "Thu", "Fri"], width=300, height=120),
)
theme_picker = ui.select(pz.themes.names(), t.name, lambda name: setattr(self, "theme", pz.themes.get(name)),
width=200)
header = pz.row([
pz.icon("layers", size=22, color=t.accent),
ui.heading("Pyzed components", level=2),
pz.hspace(),
theme_picker,
], spacing=10, align="center")
grid = pz.row([
pz.column([buttons, badges, menu], spacing=16, width="fill"),
pz.column([inputs, tree], spacing=16, width="fill"),
pz.column([tabs, toasts], spacing=16, width="fill"),
], spacing=16)
more = pz.row([
pz.column([forms], width="fill"),
pz.column([feedback], width="fill"),
pz.column([data], width="fill"),
], spacing=16)
return pz.container(
pz.scrollable(pz.column([header, grid, more], spacing=20, padding=24), height="fill"),
width="fill",
height="fill",
bg=t.background if not t.is_dark else t.editor,
color=t.text,
)
if __name__ == "__main__":
pz.run(Gallery)

La app de Pyzed más pequeña: un número y dos botones. Es el mejor archivo para empezar cuando haces algo nuevo.

Un contador con botones de más y menos
Windows, macOS y Linux
uv run --with pyzed-ui python examples/counter.py
Código completo: examples/counter.py
examples/counter.py
"""The smallest Pyzed app: state + update + view."""
import pyzed as pz
from pyzed import ui
class Counter(pz.App):
title = "Counter"
size = (360, 220)
def __init__(self):
self.count = 0
def update(self, message):
if message == "increment":
self.count += 1
elif message == "decrement":
self.count -= 1
def view(self):
return pz.center(
pz.column(
[
ui.heading(str(self.count)),
pz.row(
[
ui.button("1", "decrement", icon="minus"),
ui.button("1", "increment", icon="plus", variant="solid"),
ui.icon_button("rotate-ccw", lambda: setattr(self, "count", 0), tooltip="Reset"),
],
spacing=8,
),
],
spacing=16,
align="center",
),
bg=self.theme.editor,
)
if __name__ == "__main__":
pz.run(Counter)

Un workspace de editor de código completo en unas 450 líneas: un panel de proyecto con los archivos de una carpeta, pestañas, un editor con resaltado de sintaxis (editar, deshacer, guardar con Ctrl+S), colores de estado de git, un panel de estructura, cambio de tema, una paleta de comandos (Ctrl+Shift+P) y un buscador de archivos (Ctrl+P). Su aspecto recrea el del editor Zed, cuya interfaz inspiró el tema por defecto de Pyzed. El panel de terminal muestra una salida de ejemplo; no ejecuta una shell.

Un editor de código con panel de proyecto, pestañas y estructuraUn editor de código con panel de proyecto, pestañas y estructura
Windows, macOS y Linux
uv run --with pyzed-ui python examples/zed_editor.py

Pasa una carpeta para abrirla: python examples/zed_editor.py ruta/al/proyecto. Una versión más pequeña se construye paso a paso en el tutorial del mini editor de código.

Código completo: examples/zed_editor.py
examples/zed_editor.py
"""A recreation of the Zed editor written with Pyzed.
python examples/zed_editor.py [folder]
Real features: project panel over the folder's files, tabs, a syntax-highlighted
editor (edit, undo, save with ctrl-s), git status colors, an outline panel, theme
switching, the command palette (ctrl-shift-p) and the file finder (ctrl-p).
The terminal panel shows static sample output; it does not run a shell.
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import pyzed as pz
from pyzed import Command, ui
IGNORED_SUFFIXES = (".so", ".pyd", ".dll", ".dylib", ".pyc")
IGNORED = {".git", "target", ".venv", "venv", "__pycache__", "node_modules", ".pytest_cache", "dist", ".mypy_cache"}
MAX_FILES = 3000
TERMINAL_SAMPLE = (
"\x1b[32m~/pyzed\x1b[0m \x1b[34mmain\x1b[0m $ maturin develop --release\n"
"🔗 Found pyo3 bindings with abi3 support\n"
"\x1b[32m Compiling\x1b[0m pyzed v0.1.0 (~/pyzed)\n"
"\x1b[32m Finished\x1b[0m `release` profile [optimized] target(s) in 41.20s\n"
"📦 Built wheel for abi3 Python ≥ 3.9\n"
"🛠 Installed pyzed-0.1.0\n"
"\x1b[32m~/pyzed\x1b[0m \x1b[34mmain\x1b[0m $ python examples/zed_editor.py\n"
)
# ---------------------------------------------------------------------------
# Project helpers
# ---------------------------------------------------------------------------
def scan(root: Path) -> List[str]:
files: List[str] = []
for path in sorted(root.rglob("*")):
rel = path.relative_to(root)
if any(part in IGNORED for part in rel.parts) or path.suffix in IGNORED_SUFFIXES or not path.is_file():
continue
files.append(rel.as_posix())
if len(files) >= MAX_FILES:
break
return files
def git(root: Path, *args: str) -> str:
try:
return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, timeout=5).stdout
except (OSError, subprocess.SubprocessError):
return ""
def git_status(root: Path) -> Dict[str, str]:
status = {}
for line in git(root, "status", "--porcelain").splitlines():
code, path = line[:2], line[3:].strip()
if "D" in code:
status[path] = "deleted"
elif "?" in code or "A" in code:
status[path] = "created"
elif code.strip():
status[path] = "modified"
return status
def git_markers(root: Path, path: str) -> Dict[int, str]:
markers: Dict[int, str] = {}
for hunk in re.finditer(r"^@@ -\d+(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", git(root, "diff", "-U0", "--", path), re.M):
removed, start, added = int(hunk.group(1) or 1), int(hunk.group(2)), int(hunk.group(3) or 1)
if added == 0:
markers[max(start, 1)] = "deleted"
for line in range(start, start + added):
markers[line] = "created" if removed == 0 else "modified"
return markers
SYMBOLS = re.compile(
r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?P<kind>fn|struct|enum|trait|impl|mod|def|class)\s+(?P<name>[A-Za-z_]\w*)",
)
def outline(text: str) -> List[Tuple[int, str, str]]:
out = []
for number, line in enumerate(text.splitlines(), start=1):
match = SYMBOLS.match(line)
if match:
out.append((number, match["kind"], match["name"].strip()))
return out
# ---------------------------------------------------------------------------
# The app
# ---------------------------------------------------------------------------
class Zed(pz.App):
title = "Pyzed — a Zed-like editor in Python"
size = (1440, 900)
min_size = (800, 500)
def __init__(self, root: str = ".", *, status: Optional[Dict[str, str]] = None):
self.root = Path(root).resolve()
self.files = scan(self.root)
self.status = git_status(self.root) if status is None else status
self.nodes = [ui.TreeNode(self.root.name, children=ui.TreeNode.from_paths(self.files, self.status), id="")]
self.expanded = {""}
self.selected: Optional[str] = None
self.tabs: List[str] = []
self.active: Optional[str] = None
self.texts: Dict[str, str] = {}
self.markers: Dict[str, Dict[int, str]] = {}
self.dirty: set = set()
self.cursor = (1, 1)
self.show_left, self.show_bottom, self.show_right = True, False, False
self.palette: Optional[str] = None # "commands" | "files"
self.query, self.choice = "", 0
self.notices: List[Tuple[str, str]] = []
self.theme = pz.themes.ONE_DARK
def on_start(self):
commands = []
if self.active and self.cursor != (1, 1):
commands.append(Command.set_editor_cursor(self.active, *self.cursor))
if self.palette:
commands.append(Command.focus("palette"))
return commands
# -- files -------------------------------------------------------------
def open_file(self, path: str):
if path not in self.texts:
try:
self.texts[path] = (self.root / path).read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
self.notify("Can't open binary file", "warning")
return
self.markers.setdefault(path, git_markers(self.root, path))
if path not in self.tabs:
self.tabs.append(path)
self.active = self.selected = path
parts = path.split("/")
self.expanded.update("/".join(parts[:i]) for i in range(1, len(parts)))
self.cursor = (1, 1)
def close_tab(self, path: str):
if path in self.tabs:
index = self.tabs.index(path)
self.tabs.remove(path)
if self.active == path:
self.active = self.tabs[min(index, len(self.tabs) - 1)] if self.tabs else None
def edited(self, path: str, text: str):
self.texts[path] = text
self.dirty.add(path)
def save(self):
if self.active:
(self.root / self.active).write_text(self.texts[self.active], encoding="utf-8")
self.dirty.discard(self.active)
self.notify(f"Saved {self.active}", "success")
def toggle_folder(self, folder: str):
self.expanded.symmetric_difference_update({folder})
self.selected = folder
def notify(self, text: str, kind: str = "info"):
self.notices = (self.notices + [(text, kind)])[-3:]
# -- palette -----------------------------------------------------------
def commands(self) -> List[ui.PaletteItem]:
def run(action):
def handler():
self.palette = None
return action()
return handler
def set_theme(theme):
return run(lambda: setattr(self, "theme", theme))
return [
ui.PaletteItem("file finder: toggle", run(self.open_finder), keys="ctrl-p"),
ui.PaletteItem("workspace: toggle left dock", run(self.toggle_left), keys="ctrl-b"),
ui.PaletteItem("workspace: toggle bottom dock", run(self.toggle_bottom), keys="ctrl-j"),
ui.PaletteItem("workspace: toggle right dock", run(self.toggle_right), keys="ctrl-alt-b"),
ui.PaletteItem("workspace: save", run(self.save), keys="ctrl-s"),
ui.PaletteItem("pane: close active item", run(lambda: self.active and self.close_tab(self.active)), keys="ctrl-w"),
ui.PaletteItem("theme selector: one dark", set_theme(pz.themes.ONE_DARK)),
ui.PaletteItem("theme selector: one light", set_theme(pz.themes.ONE_LIGHT)),
ui.PaletteItem("theme selector: gruvbox dark", set_theme(pz.themes.GRUVBOX_DARK)),
ui.PaletteItem("terminal panel: toggle focus", run(self.toggle_bottom), keys="ctrl-`"),
ui.PaletteItem("outline panel: toggle", run(self.toggle_right)),
ui.PaletteItem("zed: about", run(lambda: self.notify("Pyzed 0.1 · Iced 0.14 · Python " + sys.version.split()[0]))),
ui.PaletteItem("zed: quit", run(Command.exit), keys="ctrl-q"),
]
def palette_matches(self):
if self.palette == "files":
items = [
ui.PaletteItem(path, (lambda p=path: self.pick_file(p)), icon=pz.icons.for_file(path)[0])
for path in self.files
]
return ui.fuzzy_filter(self.query, items, key=lambda i: i.label)[:200]
return ui.fuzzy_filter(self.query, self.commands(), key=lambda i: i.label)
def pick_file(self, path: str):
self.palette = None
self.open_file(path)
def open_palette(self, kind: str = "commands"):
self.palette, self.query, self.choice = kind, "", 0
return Command.focus("palette")
def open_finder(self):
return self.open_palette("files")
def set_query(self, query: str):
self.query, self.choice = query, 0
def submit(self):
matches = self.palette_matches()
if matches:
return matches[min(self.choice, len(matches) - 1)][0].on_press()
def toggle_left(self):
self.show_left = not self.show_left
def toggle_bottom(self):
self.show_bottom = not self.show_bottom
def toggle_right(self):
self.show_right = not self.show_right
def on_key(self, combo: str):
combo = combo.replace("cmd-", "ctrl-")
if combo == "ctrl-shift-p":
if self.palette == "commands":
self.palette = None
return None
return self.open_palette()
if combo == "ctrl-p":
return self.open_finder()
if self.palette:
if combo == "escape":
self.palette = None
elif combo in ("down", "ctrl-n"):
self.choice += 1
elif combo in ("up", "ctrl-p"):
self.choice = max(0, self.choice - 1)
return None
actions = {
"ctrl-b": self.toggle_left,
"ctrl-j": self.toggle_bottom,
"ctrl-`": self.toggle_bottom,
"ctrl-alt-b": self.toggle_right,
"ctrl-s": self.save,
"ctrl-w": lambda: self.active and self.close_tab(self.active),
"ctrl-q": Command.exit,
}
if combo in actions:
return actions[combo]()
# -- view --------------------------------------------------------------
def view(self):
t = self.theme
layout = ui.workspace(
self.center(),
title_bar=ui.title_bar(
[
ui.title_button(self.root.name, chevron=True),
ui.title_button("main", icon="git-branch", muted=True),
],
[
ui.icon_button("bell", tooltip="Notifications"),
ui.icon_button("sparkles", tooltip="Agent Panel"),
pz.space(width=4),
ui.avatar("KV", index=0),
pz.space(width=4),
],
),
left=self.project_panel() if self.show_left else None,
right=self.outline_panel() if self.show_right else None,
bottom=self.terminal_panel() if self.show_bottom else None,
status_bar=self.status_bar(),
left_width=250,
right_width=260,
)
if self.notices:
layout = ui.toasts(layout, [
ui.toast(text, kind=kind, on_dismiss=(lambda i=i: self.notices.pop(i)))
for i, (text, kind) in enumerate(self.notices)
])
if self.palette:
matches = self.palette_matches()
self.choice = min(self.choice, max(len(matches) - 1, 0))
overlay = ui.picker(
self.query,
self.set_query,
matches,
selected=self.choice,
input_id="palette",
on_submit=self.submit,
placeholder="Execute a command…" if self.palette == "commands" else "Search project files…",
)
layout = ui.layer(layout, overlay, on_dismiss=lambda: setattr(self, "palette", None),
backdrop=pz.theme.alpha("#000000", 0.12 if t.is_dark else 0.04))
return layout
def project_panel(self):
return pz.scrollable(
pz.container(
ui.tree(self.nodes, expanded=self.expanded, selected=self.selected,
on_toggle=self.toggle_folder, on_select=self.open_file),
padding=(4, 0),
),
height="fill",
scroller=self.theme.scrollbar,
scrollbar_width=6,
)
def outline_panel(self):
symbols = outline(self.texts.get(self.active or "", ""))
rows = [
ui.list_item(
pz.row([ui.label(kind, color=self.theme.syntax["keyword"], size="sm", mono=True),
ui.label(name, mono=True, size="sm")], spacing=6),
lambda: None,
height=24,
indent=8,
)
for _, kind, name in symbols
]
return pz.column([
ui.panel_header("Outline", [ui.icon_button("x", self.toggle_right, size="sm")], icon="list-tree"),
pz.scrollable(pz.column(rows or [ui.muted("No symbols", size="sm")], padding=(0, 0, 8, 0)), height="fill"),
])
def terminal_panel(self):
tabs = pz.row([
pz.container(
pz.row([pz.icon("terminal", size=14), ui.label("zsh", size="sm")], spacing=6, align="center"),
padding=(3, 10), bg=self.theme.editor, radius=self.theme.radius_sm,
),
pz.hspace(),
ui.icon_button("plus", size="sm"),
ui.icon_button("maximize-2", size="sm"),
ui.icon_button("x", self.toggle_bottom, size="sm"),
], align="center", padding=(4, 6))
return pz.column([tabs, ui.terminal(TERMINAL_SAMPLE)])
def center(self):
if not self.active:
return self.welcome()
symbols = outline(self.texts.get(self.active, ""))
scope = [f"{kind} {name}" for line, kind, name in symbols if line <= self.cursor[0]][-1:]
tabs = ui.tab_bar(
[ui.Tab.for_file(p, dirty=p in self.dirty, status=self.status.get(p)) for p in self.tabs],
self.active,
on_select=self.select_tab,
on_close=self.close_tab,
start=[ui.icon_button("arrow-left", size="sm"), ui.icon_button("arrow-right", size="sm")],
end=[ui.icon_button("plus", size="sm"), ui.icon_button("columns-2", size="sm")],
)
toolbar = ui.toolbar(
[ui.breadcrumbs([self.active, *scope])],
[
ui.icon_button("sparkles", size="sm", tooltip="Inline Assist"),
ui.icon_button("text-search", size="sm", tooltip="Buffer Search"),
ui.icon_button("ellipsis", size="sm"),
],
)
editor = ui.editor(
self.active,
self.texts[self.active],
path=self.active,
markers=self.markers.get(self.active),
on_change=lambda text, p=self.active: self.edited(p, text),
on_cursor=self.move_cursor,
)
return pz.column([tabs, toolbar, editor])
def select_tab(self, path: str):
self.active = self.selected = path
def move_cursor(self, line: int, column: int):
self.cursor = (line, column)
def welcome(self):
rows = [
("Open a File", "file", "ctrl-p", self.open_finder),
("Command Palette", "command", "ctrl-shift-p", self.open_palette),
("Toggle Project Panel", "panel-left", "ctrl-b", self.toggle_left),
("Toggle Terminal", "terminal", "ctrl-j", self.toggle_bottom),
]
return pz.center(
pz.column(
[
pz.row([pz.icon("zap", size=30, color=self.theme.accent), ui.heading("Pyzed")], spacing=10, align="center"),
ui.muted("Code at the speed of thought — now in Python"),
pz.space(height=18),
*[
ui.button(label, action, icon=icon, variant="ghost", full_width=True, keys=keys)
for label, icon, keys, action in rows
],
],
spacing=4,
width=340,
align="center",
)
)
def status_bar(self):
language = pz.icons.language_for(self.active) if self.active else None
return ui.status_bar(
[
ui.status_item(icon="panel-left", on_press=self.toggle_left, selected=self.show_left,
tooltip="Project Panel", keys="ctrl-b"),
ui.status_item(icon="git-branch", on_press=lambda: None),
ui.status_item(icon="search", on_press=self.open_finder, tooltip="File Finder", keys="ctrl-p"),
pz.space(width=6),
ui.status_item("0", icon="circle-x", on_press=lambda: None),
ui.status_item("0", icon="triangle-alert", on_press=lambda: None),
],
[
ui.status_item(f"{self.cursor[0]}:{self.cursor[1]}", on_press=lambda: None) if self.active else None,
ui.status_item(language, on_press=lambda: None) if language else None,
ui.status_item(icon="sparkles", on_press=lambda: None),
ui.status_item(icon="bell", on_press=lambda: None),
ui.status_item(icon="panel-bottom", on_press=self.toggle_bottom, selected=self.show_bottom,
tooltip="Terminal", keys="ctrl-j"),
ui.status_item(icon="panel-right", on_press=self.toggle_right, selected=self.show_right,
tooltip="Outline", keys="ctrl-alt-b"),
],
)
if __name__ == "__main__":
Zed(sys.argv[1] if len(sys.argv) > 1 else ".").run()