Zalgorithm

First Textual application

Docs:

The goal #

Launch Processing and an OSC server from a Textual app . Display Processing and OSC logs within the app. Control some aspect of the Processing sketch via OSC commands. Make the app match the operating system’s theme.

Multiplying complex numbers #

Related to what I’ve been focused on lately the app will multiply complex numbers in their polar form

Running processes that are normally started in separate terminals in a Textual app #

Helpful docs:

The Textual documentation about Thread workers is relavant to my use case:

In previous examples we used run_worker or the work decorator in conjunction with coroutines. This works well if you are using an async API like httpx, but if your API doesn’t support async you may need to use threads.

Avoid calling methods directly on the UI from a threaded worker. Instead, use App.call_from_thread. I’m doing that with the write_callback arg in the OSC server constructor:

class TextualOscServer(ServerThread):
    def __init__(self, port, write_callback):
        ServerThread.__init__(self, port)
        self.write = write_callback

    @make_method("/complex/response", "s")
    def response_callback(self, path, args):
        response = args
        self.write(f"Received response from '{path}': {response}")

    @make_method(None, None)
    def fallback(self, path, args):
        self.write(f"Received response '{path}' {args}")

# ...

class OscApp(App):  # the Textual app
    def __init__(self):
        super().__init__()
        self.__osc_server = None
        self.a = ComplexPolar(0.0, 0.0)
        self.b = ComplexPolar(0.0, 0.0)

    def on_mount(self):
        osc_log = self.query_one("#osc-log", Log)

        def write_to_log(message):
            self.call_from_thread(osc_log.write_line, message)

        self._osc_server = TextualOscServer(9000, write_to_log)
        self._osc_server.start()
        osc_log.write_line("OSC Server listening on port 9000")

Spending hours writing code that an LLM could spit out in 30 seconds #

I’m assuming an LLM could have spit this out in 30 seconds. notes / Why write code that could be written by an LLM?

Processing #

complex-polar-multiplication.pde:

The ComplexPolar number class is a little contrived. Multiplying complex numbers in the polar form is very efficient. Adding complex numbers in the polar form doesn’t really work — they need to be converted back to the rectangular form first.

ComplexPolar(float r, float theta) {
  this.r = r;
  this.theta = theta;
  float normalizedTheta = theta % TWO_PI;
  if (normalizedTheta < 0) normalizedTheta += TWO_PI;
  this.hue = (int)(normalizedTheta * 180/PI);
}

ComplexPolar mult(ComplexPolar other) {
  return new ComplexPolar(
    r * other.r, theta + other.theta
    );
}

ComplexPolar sum(ComplexPolar other) {
  float real = this.re() + other.re();
  float imag = this.im() + other.im();
  float magnitude = sqrt(real*real + imag*imag);
  float theta = atan2(imag, real);

  return new ComplexPolar(
    magnitude, theta
    );
}

float magnitude() {
  return r;
}

float re() {
  return r * cos(theta);
}

float im() {
  return r * sin(theta);
}

Full Processing code:

import oscP5.*;
import netP5.*;

OscP5 oscP5;
NetAddress myLocation;

ComplexPolar a, b, c;

float windowScale;
boolean clearBg = false;

boolean closeBA, closeCA, closeCB = false;

void setup() {
  size(600, 600);
  windowScale = width / 3;
  colorMode(HSB, 360, 100, 100);
  background(48, 6, 100);
  stroke(217, 4, 56);
  textSize(18);

  noFill();
  oscP5 = new OscP5(this, 12000);
  myLocation = new NetAddress("127.0.0.1", 9000);
}

void draw() {
  translate(width/2, height/2);
  scale(1, -1);

  background(48, 6, 100);

  stroke(230, 1, 36);
  strokeWeight(1);
  noFill();
  ellipse(0, 0, 1*windowScale, 1*windowScale);
  line(0, 0, width/2, 0);
  fill(230, 1, 36);
  stroke(230, 1, 36);
  strokeWeight(8);
  point(0, 0);
  triangle(width/2 -8, 0, width/2-16, 4, width/2-16, -4);

  noFill();

  if (a != null) {
    float re = a.re() * windowScale * 0.5;
    float im = a.im() * windowScale * 0.5;
    strokeWeight(12);
    stroke(a.hue, 74, 100);
    point(re, im);

    pushMatrix();
    scale(1, -1);
    fill(0, 0, 0);
    text("A", re + 10, -im);
    text("Point 'A', r: " + a.r + " theta: " + a.theta, -width/2 + 10, -height/2 + 20);
    popMatrix();
  }
  if (b != null) {
    float re = b.re() * windowScale * 0.5;
    float im = b.im() * windowScale * 0.5;
    strokeWeight(12);
    stroke(b.hue, 74, 100);
    point(re, im);

    pushMatrix();
    scale(1, -1);
    String label = "B";
    int labelOffset = 10;
    if (closeBA) labelOffset += 12;
    text(label, re + labelOffset, -im);

    fill(0, 0, 0);
    text("Point 'B', r: " + b.r + " theta: " + b.theta, -width/2 + 10, -height/2 + 42);
    popMatrix();
  }
  if (c != null) {
    float re = c.re() * windowScale * 0.5;
    float im = c.im() * windowScale * 0.5;
    strokeWeight(12);
    stroke(c.hue, 74, 100);
    point(re, im);

    pushMatrix();
    scale(1, -1);
    String label = "C";
    int labelOffset = 10;
    if (closeCA) labelOffset += 12;
    if (closeCB) labelOffset += 12;
    text(label, re + labelOffset, -im);
    fill(0, 0, 0);
    text("Point 'C', r: " + c.r + " theta: " + c.theta, -width/2 + 10, -height/2 + 64);
    popMatrix();
  }

  if (a != null && b != null) {
    closeBA = isClose(b, a);
    if (c != null) {
      closeCA = isClose(c, a);
      closeCB = isClose(c, b);
    }
  }
}

boolean isClose(ComplexPolar a, ComplexPolar b) {
  float thetaA = normalizeTheta(a.theta);
  float thetaB = normalizeTheta(b.theta);
  if (abs(thetaA - thetaB) < PI/16 && abs(a.r - b.r) < 0.01) {
    return true;
  }
  return false;
}

float normalizeTheta(float theta) {
  float normalizedTheta = theta % TWO_PI;
  if (normalizedTheta < 0) normalizedTheta += TWO_PI;
  return normalizedTheta;
}

void oscEvent(OscMessage message) {
  println("### OSC message received:");
  println("    addrpattern: "+message.addrPattern());
  println("    typetag:"+message.typetag());

  if (message.checkAddrPattern("/complex/a")) {
    if (message.checkTypetag("ff")) {
      float r = message.get(0).floatValue();
      float theta = message.get(1).floatValue();
      a = new ComplexPolar(r, theta);
    }
  }

  if (message.checkAddrPattern("/complex/b")) {
    if (message.checkTypetag("ff")) {
      float r = message.get(0).floatValue();
      float theta = message.get(1).floatValue();
      b = new ComplexPolar(r, theta);
    }
  }

  // TODO: send 'c' back to the Textual app
  if (message.checkAddrPattern("/complex/multiply/ab")) {
    if (a != null && b != null) {
      c = a.mult(b);
    } else {
      // this isn't the best way to go about it, but...
      OscMessage warningMessage = new OscMessage("/complex/response");
      String msg;
      if (a == null && b == null) {
        msg = "Set values for polar coordinates \"a\" and \"b\"";
      } else if (a == null) {
        msg = "Set a value for the polar coordinate \"a\"";
      } else {
        msg = "Set a value for the polar coordinate \"b\"";
      }
      warningMessage.add(msg);
      oscP5.send(warningMessage, myLocation);
    }
  }

  if (message.checkAddrPattern("/complex/add/ab")) {
    if (a != null && b != null) {
      c = a.sum(b);
    }
  }

  if (message.checkAddrPattern("/complex/clear")) {
    a = null;
    b = null;
    c = null;
    OscMessage responseMessage = new OscMessage("/complex/response");
    String msg = "Number values cleared";
    responseMessage.add(msg);
    oscP5.send(responseMessage, myLocation);
  }
}

class ComplexPolar {
  float r, theta;
  int hue;

  ComplexPolar(float r, float theta) {
    this.r = r;
    this.theta = theta;
    float normalizedTheta = theta % TWO_PI;
    if (normalizedTheta < 0) normalizedTheta += TWO_PI;
    this.hue = (int)(normalizedTheta * 180/PI);
  }

  ComplexPolar mult(ComplexPolar other) {
    return new ComplexPolar(
      r * other.r, theta + other.theta
      );
  }

  ComplexPolar sum(ComplexPolar other) {
    float real = this.re() + other.re();
    float imag = this.im() + other.im();
    float magnitude = sqrt(real*real + imag*imag);
    float theta = atan2(imag, real);

    return new ComplexPolar(
      magnitude, theta
      );
  }

  float magnitude() {
    return r;
  }

  float re() {
    return r * cos(theta);
  }

  float im() {
    return r * sin(theta);
  }
}

Python/Textual #

This went OK. I spent a lot of time messing with the layout.

from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Button, Log, Input, Label, Header
from textual.theme import Theme
from textual import work, on

from pyliblo3 import ServerThread, Address, make_method, send
import numpy as np

import asyncio

processing = Address("localhost", 12000)

flexoki_light_theme = Theme(
    name="flexoki_light",
    primary="#100F0F",
    secondary="#D14D41",
    foreground="#100F0F",
    background="#FFFCF0",
    surface="#FFFCF0",
    success="#879A39",
    accent="#4385BE",
    warning="#D14D41",
)


# The OSC server is a threaded process, not an asychronous process;
# I'm not sure this is the only way to do it.
class TextualOscServer(ServerThread):
    def __init__(self, port, write_callback):
        ServerThread.__init__(self, port)
        self.write = write_callback

    @make_method("/complex/response", "s")
    def response_callback(self, path, args):
        response = args
        self.write(f"\u2190 Received response from '{path}': {response}")

    @make_method(None, None)
    def fallback(self, path, args):
        self.write(f"\u2090 Received response from '{path}' {args}")


class ComplexPolar:
    def __init__(self, r=0.0, theta=0.0):
        self.r = r
        self.theta = theta


class OscApp(App):
    CSS_PATH = "styles.tcss"

    def __init__(self):
        super().__init__()
        self.__osc_server = None
        self.a = ComplexPolar(0.0, 0.0)
        self.b = ComplexPolar(0.0, 0.0)

    def on_mount(self):
        self.register_theme(flexoki_light_theme)
        self.theme = "flexoki_light"
        self.title = "Complex math visualization"

        osc_log = self.query_one("#osc-log", Log)

        def write_to_log(message):
            self.call_from_thread(osc_log.write_line, message)

        self._osc_server = TextualOscServer(9000, write_to_log)
        self._osc_server.start()
        osc_log.write_line("OSC Server listening on port 9000")

    def on_unmount(self):
        if self._osc_server:
            self._osc_server.stop()
            self._osc_server.free()

    def compose(self) -> ComposeResult:
        yield Header()
        yield Button("Launch Processing", id="launch-processing")
        yield Horizontal(
            Vertical(
                Label("magnitude:"),
                Input(
                    type="number",
                    id="r-a",
                ),
                classes="component-container",
            ),
            Vertical(
                Label("angle:"),
                Input(
                    type="number",
                    id="theta-a",
                ),
                classes="component-container",
            ),
            classes="number-container",
        )
        yield Horizontal(
            Vertical(
                Label("magnitude:"),
                Input(
                    type="number",
                    id="r-b",
                ),
                classes="component-container",
            ),
            Vertical(
                Label("angle:"),
                Input(
                    type="number",
                    id="theta-b",
                ),
                classes="component-container",
            ),
            classes="number-container",
        )
        yield Horizontal(
            Button("Add", id="add-ab"),
            Button("Multiply", id="multiply-ab"),
            Button("Clear", id="complex-clear"),
            classes="math-operations-container",
        )
        yield Label("OSC log:")
        yield Log(id="osc-log")
        yield Label("Processing log:")
        yield Log(id="processing-log")

    @on(Input.Submitted)
    def input_changed(self, event: Input.Submitted) -> None:
        if event.input.id == "r-a":
            self.a.r = float(event.input.value)
            self.osc("/complex/a", self.a.r, self.a.theta)
        if event.input.id == "theta-a":
            self.a.theta = float(event.input.value) * np.pi / 180
            self.osc("/complex/a", self.a.r, self.a.theta)
        if event.input.id == "r-b":
            self.b.r = float(event.input.value)
            self.osc("/complex/b", self.b.r, self.b.theta)
        if event.input.id == "theta-b":
            self.b.theta = float(event.input.value) * np.pi / 180
            self.osc("/complex/b", self.b.r, self.b.theta)

    # this could use the '@on(Button.Pressed)' decorator
    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "launch-processing":
            self.launch_processing()
        if event.button.id == "add-ab":
            self.osc("/complex/add/ab")
        if event.button.id == "multiply-ab":
            self.osc("/complex/multiply/ab")
        if event.button.id == "complex-clear":
            self.osc("/complex/clear")

    @work(exclusive=True)  # exclusive: cancel previous workers before starting new one
    async def launch_processing(self) -> None:
        output_widget = self.query_one("#processing-log", Log)

        proc = await asyncio.create_subprocess_exec(
            "processing-java",
            "--sketch=/home/me/sketchbook/complex_polar_multiplication/",
            "--run",
            stdout=asyncio.subprocess.PIPE,
        )

        while True:
            line = await proc.stdout.readline()
            if not line:
                break
            output_widget.write_line(line.decode().strip())

        await proc.wait()

    def osc(self, address, *values):
        osc_log = self.query_one("#osc-log", Log)
        osc_log.write_line(f"\u2192 {address} {values}")
        send(processing, address, *values)


if __name__ == "__main__":
    app = OscApp()
    app.run()

styles.tcss:

Header {
  dock: top;
  content-align: center middle;
  color: $primary;
}

Button {
  margin: 1;
  padding: 0 1;
  background: $accent;
}
#complex-clear {
  background: $secondary;
}

Label {
  padding: 0 1;
}

.component-container {
  height: 6;
}

.number-container {
  height: auto;
  border: heavy $primary;
}

.math-operations-container {
  height: auto;
}

Log {
  border: heavy $primary;
}

Input {
  border: solid $primary;
}

Images #

Multiplying 1(cos 45 + i sin 45) by 1(cos 90 + i sin 90)
Multiplying 1(cos 45 + i sin 45) by 1(cos 90 + i sin 90)

Multiplying 1(cos 45 + i sin 45) by 2(cos -45 + i sin -45)
Multiplying 1(cos 45 + i sin 45) by 2(cos -45 + i sin -45)

References #

textual.textualize.io. “Textual.” Accessed on: January 31, 2026. https://textual.textualize.io/ .

github.com/Textualize/rich. “Rich.” Accessed on: January 31, 2026. https://github.com/Textualize/rich .