Stream: ideas

Topic: Improve roc-ansi drawing


view this post on Zulip Luke Boswell (Jun 23 2024 at 01:49):

@Ian McLerran you mentioned you would be interested in looking at roc-ansi. Just writing this down while I'm thinking of it. Don't feel like you need to do anything with this, just sharing my thoughts in the hope that it might be interesting to you.

One thing I've been thinking about for a while, but haven't had a crack at yet is to make the drawing of the screen far more efficient (I think).

roc-ansi currently clears and redraws the whole terminal window every frame, even when nothing has changed (which is most of the time).

I think the following are the key parts for how it works, inspired by my poor man's understanding of a fragment shader. Basically the DrawFn's are functions which take a (fake) cursor and current pixel and return a Pixel character and color to render. It stops at the first function which returns a Pixel, so it doesn't call all of the DrawFn's for every row and col.

Position : { row : I32, col : I32 }
Pixel : { char : Str, fg : Color, bg : Color, styles : List Style }
DrawFn : Position, Position -> Result Pixel {}

## Loop through each pixel in screen and build up a single string to write to stdout
drawScreen : { cursor : Position, screen : ScreenSize }*, List DrawFn -> Str
drawScreen = \{ cursor, screen }, drawFns ->
    pixels =
        row <- List.range { start: At 0, end: Before screen.height } |> List.map
        col <- List.range { start: At 0, end: Before screen.width } |> List.map

        List.walkUntil
            drawFns
            { char: " ", fg: Default, bg: Default, styles: [] }
            \defaultPixel, drawFn ->
                when drawFn cursor { row, col } is
                    Ok pixel -> Break pixel
                    Err _ -> Continue defaultPixel

    pixels
    |> joinAllPixels

joinAllPixels : List (List Pixel) -> Str

I'm not sure why I included the fake cursor at the time, I don't think we need that anymore.

Anyway, the rough idea I had was to keep a copy of the screen List (List Pixel) between draws, diff against that, and then only redraw the parts that have changed.

Instead of returning a Str that sets the (real) cursor to 0,0 and then renders the whole screen in one go, it would be a series of smaller renders. Where nothing changed, this would just be an empty Str and we could even skip the Stdout.write.

view this post on Zulip Ian McLerran (Jun 23 2024 at 03:21):

Thanks Luke! I’m pretty much checked out for the evening, but definitely interested in taking a look at this! I’ll definitely be following up with you on this over the next week.


Last updated: Jun 16 2026 at 16:19 UTC