about summary refs log tree commit diff stats
path: root/linux/405screen.mu
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-03-03 22:09:50 -0800
committerKartik K. Agaram <vc@akkartik.com>2021-03-03 22:21:03 -0800
commit71e4f3812982dba2efb471283d310224e8db363e (patch)
treeea111a1acb8b8845dbda39c0e1b4bac1d198143b /linux/405screen.mu
parentc6b928be29ac8cdb4e4d6e1eaa20420ff03e5a4c (diff)
downloadmu-71e4f3812982dba2efb471283d310224e8db363e.tar.gz
7842 - new directory organization
Baremetal is now the default build target and therefore has its sources
at the top-level. Baremetal programs build using the phase-2 Mu toolchain
that requires a Linux kernel. This phase-2 codebase which used to be at
the top-level is now under the linux/ directory. Finally, the phase-2 toolchain,
while self-hosting, has a way to bootstrap from a C implementation, which
is now stored in linux/bootstrap. The bootstrap C implementation uses some
literate programming tools that are now in linux/bootstrap/tools.

So the whole thing has gotten inverted. Each directory should build one
artifact and include the main sources (along with standard library). Tools
used for building it are relegated to sub-directories, even though those
tools are often useful in their own right, and have had lots of interesting
programs written using them.

A couple of things have gotten dropped in this process:
  - I had old ways to run on just a Linux kernel, or with a Soso kernel.
    No more.
  - I had some old tooling for running a single test at the cursor. I haven't
    used that lately. Maybe I'll bring it back one day.

The reorg isn't done yet. Still to do:
  - redo documentation everywhere. All the README files, all other markdown,
    particularly vocabulary.md.
  - clean up how-to-run comments at the start of programs everywhere
  - rethink what to do with the html/ directory. Do we even want to keep
    supporting it?

In spite of these shortcomings, all the scripts at the top-level, linux/
and linux/bootstrap are working. The names of the scripts also feel reasonable.
This is a good milestone to take stock at.
Diffstat (limited to 'linux/405screen.mu')
-rw-r--r--linux/405screen.mu1445
1 files changed, 1445 insertions, 0 deletions
diff --git a/linux/405screen.mu b/linux/405screen.mu
new file mode 100644
index 00000000..c850df2a
--- /dev/null
+++ b/linux/405screen.mu
@@ -0,0 +1,1445 @@
+# Wrappers for real screen primitives that can be passed a fake screen.
+# The tests here have been painstakingly validated against a real terminal
+# emulator. I believe functionality here is broadly portable across terminal
+# emulators.
+#
+# Remember: fake screen co-ordinates are 1-based, just like in real terminal
+# emulators.
+
+type screen {
+  num-rows: int
+  num-cols: int
+  data: (handle array screen-cell)
+  top-index: int  # 0-indexed
+  cursor-row: int  # 1-indexed
+  cursor-col: int  # 1-indexed
+  cursor-hide?: boolean
+  curr-attributes: screen-cell
+}
+
+type screen-cell {
+  data: grapheme
+  color: int
+  background-color: int
+  bold?: boolean
+  underline?: boolean
+  reverse?: boolean
+  blink?: boolean
+}
+
+fn initialize-screen screen: (addr screen), nrows: int, ncols: int {
+  var screen-addr/esi: (addr screen) <- copy screen
+  var tmp/eax: int <- copy 0
+  var dest/edi: (addr int) <- copy 0
+  # screen->num-rows = nrows
+  dest <- get screen-addr, num-rows
+  tmp <- copy nrows
+  copy-to *dest, tmp
+  # screen->num-cols = ncols
+  dest <- get screen-addr, num-cols
+  tmp <- copy ncols
+  copy-to *dest, tmp
+  # screen->data = new screen-cell[nrows*ncols]
+  {
+    var data-addr/edi: (addr handle array screen-cell) <- get screen-addr, data
+    tmp <- multiply nrows
+    populate data-addr, tmp
+  }
+  # screen->cursor-row = 1
+  dest <- get screen-addr, cursor-row
+  copy-to *dest, 1
+  # screen->cursor-col = 1
+  dest <- get screen-addr, cursor-col
+  copy-to *dest, 1
+  # screen->curr-attributes->background-color = 7  (simulate light background)
+  var tmp2/eax: (addr screen-cell) <- get screen-addr, curr-attributes
+  dest <- get tmp2, background-color
+  copy-to *dest, 7
+}
+
+fn screen-size screen: (addr screen) -> _/eax: int, _/ecx: int {
+  var nrows/eax: int <- copy 0
+  var ncols/ecx: int <- copy 0
+  compare screen, 0
+  {
+    break-if-!=
+    nrows, ncols <- real-screen-size
+    return nrows, ncols
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var tmp/edx: (addr int) <- get screen-addr, num-rows
+  nrows <- copy *tmp
+  tmp <- get screen-addr, num-cols
+  ncols <- copy *tmp
+  return nrows, ncols
+}
+
+fn clear-screen screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    clear-real-screen
+    return
+  }
+  # fake screen
+  var space/edi: grapheme <- copy 0x20
+  move-cursor screen, 1, 1
+  var screen-addr/esi: (addr screen) <- copy screen
+  var i/eax: int <- copy 1
+  var nrows/ecx: (addr int) <- get screen-addr, num-rows
+  {
+    compare i, *nrows
+    break-if->
+    var j/edx: int <- copy 1
+    var ncols/ebx: (addr int) <- get screen-addr, num-cols
+    {
+      compare j, *ncols
+      break-if->
+      print-grapheme screen, space
+      j <- increment
+      loop
+    }
+    i <- increment
+    loop
+  }
+  move-cursor screen, 1, 1
+}
+
+fn move-cursor screen: (addr screen), row: int, column: int {
+  compare screen, 0
+  {
+    break-if-!=
+    move-cursor-on-real-screen row, column
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  # row < 0 is ignored
+  {
+    compare row, 0
+    break-if->=
+    return
+  }
+  # row = 0 is treated same as 1
+  {
+    compare row, 0
+    break-if-!=
+    copy-to row, 1
+  }
+  # row > num-rows saturates to num-rows
+  {
+    var nrows-addr/eax: (addr int) <- get screen-addr, num-rows
+    var nrows/eax: int <- copy *nrows-addr
+    compare row, nrows
+    break-if-<=
+    copy-to row, nrows
+  }
+  # column < 0 is ignored
+  {
+    compare column, 0
+    break-if->=
+    return
+  }
+  # column = 0 is treated same as 1
+  {
+    compare column, 0
+    break-if-!=
+    copy-to column, 1
+  }
+  # column > num-cols saturates to num-cols+1 (so wrapping to next row)
+  {
+    var ncols-addr/eax: (addr int) <- get screen-addr, num-cols
+    var ncols/eax: int <- copy *ncols-addr
+    compare column, ncols
+    break-if-<=
+    copy-to column, ncols
+    increment column
+  }
+  # screen->cursor-row = row
+  var dest/edi: (addr int) <- get screen-addr, cursor-row
+  var src/eax: int <- copy row
+  copy-to *dest, src
+  # screen->cursor-col = column
+  dest <- get screen-addr, cursor-col
+  src <- copy column
+  copy-to *dest, src
+}
+
+fn print-string screen: (addr screen), s: (addr array byte) {
+  compare screen, 0
+  {
+    break-if-!=
+    print-string-to-real-screen s
+    return
+  }
+  # fake screen
+  var stream-storage: (stream byte 0x100)
+  var stream/esi: (addr stream byte) <- address stream-storage
+  write stream, s
+  print-stream screen, stream
+}
+
+fn print-stream _screen: (addr screen), s: (addr stream byte) {
+  var screen/edi: (addr screen) <- copy _screen
+  {
+    var done?/eax: boolean <- stream-empty? s
+    compare done?, 0
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme s
+    print-grapheme screen, g
+    loop
+  }
+}
+
+fn print-array-of-ints-in-decimal screen: (addr screen), _a: (addr array int) {
+  var a/esi: (addr array int) <- copy _a
+  var max/ecx: int <- length a
+  var i/eax: int <- copy 0
+  {
+    compare i, max
+    break-if->=
+    {
+      compare i, 0
+      break-if-=
+      print-string screen, " "
+    }
+    var x/ecx: (addr int) <- index a, i
+    print-int32-decimal screen, *x
+    i <- increment
+    loop
+  }
+}
+
+fn print-grapheme screen: (addr screen), c: grapheme {
+  compare screen, 0
+  {
+    break-if-!=
+    print-grapheme-to-real-screen c
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var cursor-col-addr/edx: (addr int) <- get screen-addr, cursor-col
+  # adjust cursor if necessary
+  # to avoid premature scrolling it's important to do this lazily, at the last possible time
+  {
+    # next row
+    var num-cols-addr/ecx: (addr int) <- get screen-addr, num-cols
+    var num-cols/ecx: int <- copy *num-cols-addr
+    compare *cursor-col-addr, num-cols
+    break-if-<=
+    copy-to *cursor-col-addr, 1
+    var cursor-row-addr/ebx: (addr int) <- get screen-addr, cursor-row
+    increment *cursor-row-addr
+    # scroll
+    var num-rows-addr/eax: (addr int) <- get screen-addr, num-rows
+    var num-rows/eax: int <- copy *num-rows-addr
+    compare *cursor-row-addr, num-rows
+    break-if-<=
+    copy-to *cursor-row-addr, num-rows
+    # if (top-index > data size) top-index = 0, otherwise top-index += num-cols
+    $print-grapheme:perform-scroll: {
+      var top-index-addr/ebx: (addr int) <- get screen-addr, top-index
+      var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+      var data/eax: (addr array screen-cell) <- lookup *data-ah
+      var max-index/edi: int <- length data
+      compare *top-index-addr, max-index
+      {
+        break-if->=
+        add-to *top-index-addr, num-cols
+        break $print-grapheme:perform-scroll
+      }
+      {
+        break-if-<
+        copy-to *top-index-addr, 0
+      }
+    }
+  }
+  var idx/ecx: int <- current-screen-cell-index screen-addr
+#?   print-string-to-real-screen "printing grapheme at screen index "
+#?   print-int32-hex-to-real-screen idx
+#?   print-string-to-real-screen ": "
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var dest-cell/ecx: (addr screen-cell) <- index data, offset
+  var src-cell/eax: (addr screen-cell) <- get screen-addr, curr-attributes
+  copy-object src-cell, dest-cell
+  var dest/eax: (addr grapheme) <- get dest-cell, data
+  var c2/ecx: grapheme <- copy c
+#?   print-grapheme-to-real-screen c2
+#?   print-string-to-real-screen "\n"
+  copy-to *dest, c2
+  increment *cursor-col-addr
+}
+
+fn current-screen-cell-index screen-on-stack: (addr screen) -> _/ecx: int {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var cursor-row-addr/ecx: (addr int) <- get screen, cursor-row
+  var cursor-col-addr/eax: (addr int) <- get screen, cursor-col
+  var result/ecx: int <- screen-cell-index screen, *cursor-row-addr, *cursor-col-addr
+  return result
+}
+
+fn screen-cell-index screen-on-stack: (addr screen), row: int, col: int -> _/ecx: int {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var num-cols-addr/eax: (addr int) <- get screen, num-cols
+  var num-cols/eax: int <- copy *num-cols-addr
+  var result/ecx: int <- copy row
+  result <- subtract 1
+  result <- multiply num-cols
+  result <- add col
+  result <- subtract 1
+  # result = (result + top-index) % data length
+  var top-index-addr/eax: (addr int) <- get screen, top-index
+  result <- add *top-index-addr
+  var data-ah/eax: (addr handle array screen-cell) <- get screen, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var max-index/eax: int <- length data
+  compare result, max-index
+  {
+    break-if-<
+    result <- subtract max-index
+  }
+  return result
+}
+
+fn screen-grapheme-at screen-on-stack: (addr screen), row: int, col: int -> _/eax: grapheme {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen-addr, row, col
+  var result/eax: grapheme <- screen-grapheme-at-idx screen-addr, idx
+  return result
+}
+
+fn screen-grapheme-at-idx screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: grapheme {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var idx/ecx: int <- copy idx-on-stack
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var cell/eax: (addr screen-cell) <- index data, offset
+  var src/eax: (addr grapheme) <- get cell, data
+  return *src
+}
+
+fn screen-color-at screen-on-stack: (addr screen), row: int, col: int -> _/eax: int {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen-addr, row, col
+  var result/eax: int <- screen-color-at-idx screen-addr, idx
+  return result
+}
+
+fn screen-color-at-idx screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: int {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var idx/ecx: int <- copy idx-on-stack
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var cell/eax: (addr screen-cell) <- index data, offset
+  var src/eax: (addr int) <- get cell, color
+  var result/eax: int <- copy *src
+  return result
+}
+
+fn screen-background-color-at screen-on-stack: (addr screen), row: int, col: int -> _/eax: int {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen-addr, row, col
+  var result/eax: int <- screen-background-color-at-idx screen-addr, idx
+  return result
+}
+
+fn screen-background-color-at-idx screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: int {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var idx/ecx: int <- copy idx-on-stack
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var cell/eax: (addr screen-cell) <- index data, offset
+  var src/eax: (addr int) <- get cell, background-color
+  return *src
+}
+
+fn screen-bold-at? screen-on-stack: (addr screen), row: int, col: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen-addr, row, col
+  var result/eax: boolean <- screen-bold-at-idx? screen-addr, idx
+  return result
+}
+
+fn screen-bold-at-idx? screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var idx/ecx: int <- copy idx-on-stack
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var cell/eax: (addr screen-cell) <- index data, offset
+  var src/eax: (addr boolean) <- get cell, bold?
+  return *src
+}
+
+fn screen-underline-at? screen-on-stack: (addr screen), row: int, col: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen-addr, row, col
+  var result/eax: boolean <- screen-underline-at-idx? screen-addr, idx
+  return result
+}
+
+fn screen-underline-at-idx? screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var idx/ecx: int <- copy idx-on-stack
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var cell/eax: (addr screen-cell) <- index data, offset
+  var src/eax: (addr boolean) <- get cell, underline?
+  return *src
+}
+
+fn screen-reverse-at? screen-on-stack: (addr screen), row: int, col: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen-addr, row, col
+  var result/eax: boolean <- screen-reverse-at-idx? screen-addr, idx
+  return result
+}
+
+fn screen-reverse-at-idx? screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var idx/ecx: int <- copy idx-on-stack
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var cell/eax: (addr screen-cell) <- index data, offset
+  var src/eax: (addr boolean) <- get cell, reverse?
+  return *src
+}
+
+fn screen-blink-at? screen-on-stack: (addr screen), row: int, col: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen-addr, row, col
+  var result/eax: boolean <- screen-blink-at-idx? screen-addr, idx
+  return result
+}
+
+fn screen-blink-at-idx? screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: boolean {
+  var screen-addr/esi: (addr screen) <- copy screen-on-stack
+  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
+  var data/eax: (addr array screen-cell) <- lookup *data-ah
+  var idx/ecx: int <- copy idx-on-stack
+  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
+  var cell/eax: (addr screen-cell) <- index data, offset
+  var src/eax: (addr boolean) <- get cell, blink?
+  return *src
+}
+
+fn print-code-point screen: (addr screen), c: code-point {
+  var g/eax: grapheme <- to-grapheme c
+  print-grapheme screen, g
+}
+
+fn print-int32-hex screen: (addr screen), n: int {
+  compare screen, 0
+  {
+    break-if-!=
+    print-int32-hex-to-real-screen n
+    return
+  }
+  # fake screen
+  var s2: (stream byte 0x100)
+  var s2-addr/esi: (addr stream byte) <- address s2
+  write-int32-hex s2-addr, n
+  var screen-addr/edi: (addr screen) <- copy screen
+  {
+    var done?/eax: boolean <- stream-empty? s2-addr
+    compare done?, 0
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme s2-addr
+    print-grapheme screen, g
+    loop
+  }
+}
+
+fn print-int32-hex-bits screen: (addr screen), n: int, bits: int {
+  compare screen, 0
+  {
+    break-if-!=
+    print-int32-hex-bits-to-real-screen n, bits
+    return
+  }
+  # fake screen
+  var s2: (stream byte 0x100)
+  var s2-addr/esi: (addr stream byte) <- address s2
+  write-int32-hex-bits s2-addr, n, bits
+  var screen-addr/edi: (addr screen) <- copy screen
+  {
+    var done?/eax: boolean <- stream-empty? s2-addr
+    compare done?, 0
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme s2-addr
+    print-grapheme screen, g
+    loop
+  }
+}
+
+fn print-int32-decimal screen: (addr screen), n: int {
+  compare screen, 0
+  {
+    break-if-!=
+    print-int32-decimal-to-real-screen n
+    return
+  }
+  # fake screen
+  var s2: (stream byte 0x100)
+  var s2-addr/esi: (addr stream byte) <- address s2
+  write-int32-decimal s2-addr, n
+  var screen-addr/edi: (addr screen) <- copy screen
+  {
+    var done?/eax: boolean <- stream-empty? s2-addr
+    compare done?, 0
+    break-if-!=
+    var g/eax: grapheme <- read-grapheme s2-addr
+    print-grapheme screen, g
+    loop
+  }
+}
+
+fn reset-formatting screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    reset-formatting-on-real-screen
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var dest/ecx: (addr screen-cell) <- get screen-addr, curr-attributes
+  var default-cell: screen-cell
+  var bg/eax: (addr int) <- get default-cell, background-color
+  copy-to *bg, 7
+  var default-cell-addr/eax: (addr screen-cell) <- address default-cell
+  copy-object default-cell-addr, dest
+}
+
+fn start-color screen: (addr screen), fg: int, bg: int {
+  compare screen, 0
+  {
+    break-if-!=
+    start-color-on-real-screen fg, bg
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var attr/ecx: (addr screen-cell) <- get screen-addr, curr-attributes
+  var dest/edx: (addr int) <- get attr, color
+  var src/eax: int <- copy fg
+  copy-to *dest, src
+  var dest/edx: (addr int) <- get attr, background-color
+  var src/eax: int <- copy bg
+  copy-to *dest, src
+}
+
+fn start-bold screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    start-bold-on-real-screen
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var attr/ecx: (addr screen-cell) <- get screen-addr, curr-attributes
+  var dest/edx: (addr boolean) <- get attr, bold?
+  copy-to *dest, 1
+}
+
+fn start-underline screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    start-underline-on-real-screen
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var attr/ecx: (addr screen-cell) <- get screen-addr, curr-attributes
+  var dest/edx: (addr boolean) <- get attr, underline?
+  copy-to *dest, 1
+}
+
+fn start-reverse-video screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    start-reverse-video-on-real-screen
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var attr/ecx: (addr screen-cell) <- get screen-addr, curr-attributes
+  var dest/edx: (addr boolean) <- get attr, reverse?
+  copy-to *dest, 1
+}
+
+fn start-blinking screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    start-blinking-on-real-screen
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var attr/ecx: (addr screen-cell) <- get screen-addr, curr-attributes
+  var dest/edx: (addr boolean) <- get attr, blink?
+  copy-to *dest, 1
+}
+
+fn hide-cursor screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    hide-cursor-on-real-screen
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var hide?/ecx: (addr boolean) <- get screen-addr, cursor-hide?
+  copy-to *hide?, 1
+}
+
+fn show-cursor screen: (addr screen) {
+  compare screen, 0
+  {
+    break-if-!=
+    show-cursor-on-real-screen
+    return
+  }
+  # fake screen
+  var screen-addr/esi: (addr screen) <- copy screen
+  var hide?/ecx: (addr boolean) <- get screen-addr, cursor-hide?
+  copy-to *hide?, 0
+}
+
+# validate data on screen regardless of attributes (color, bold, etc.)
+# Mu doesn't have multi-line strings, so we provide functions for rows or portions of rows.
+# Tab characters (that translate into multiple screen cells) not supported.
+
+fn check-screen-row screen: (addr screen), row-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  check-screen-row-from screen, row-idx, 1, expected, msg
+}
+
+fn check-screen-row-from screen-on-stack: (addr screen), row-idx: int, col-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen, row-idx, col-idx
+  # compare 'expected' with the screen contents starting at 'idx', grapheme by grapheme
+  var e: (stream byte 0x100)
+  var e-addr/edx: (addr stream byte) <- address e
+  write e-addr, expected
+  {
+    var done?/eax: boolean <- stream-empty? e-addr
+    compare done?, 0
+    break-if-!=
+    var _g/eax: grapheme <- screen-grapheme-at-idx screen, idx
+    var g/ebx: grapheme <- copy _g
+    var expected-grapheme/eax: grapheme <- read-grapheme e-addr
+    # compare graphemes
+    $check-screen-row-from:compare-graphemes: {
+      # if expected-grapheme is space, null grapheme is also ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        compare g, 0
+        break-if-= $check-screen-row-from:compare-graphemes
+      }
+      # if (g == expected-grapheme) print "."
+      compare g, expected-grapheme
+      {
+        break-if-!=
+        print-string-to-real-screen "."
+        break $check-screen-row-from:compare-graphemes
+      }
+      # otherwise print an error
+      print-string-to-real-screen msg
+      print-string-to-real-screen ": expected '"
+      print-grapheme-to-real-screen expected-grapheme
+      print-string-to-real-screen "' at ("
+      print-int32-hex-to-real-screen row-idx
+      print-string-to-real-screen ", "
+      print-int32-hex-to-real-screen col-idx
+      print-string-to-real-screen ") but observed '"
+      print-grapheme-to-real-screen g
+      print-string-to-real-screen "'\n"
+    }
+    idx <- increment
+    increment col-idx
+    loop
+  }
+}
+
+# various variants by screen-cell attribute; spaces in the 'expected' data should not match the attribute
+
+fn check-screen-row-in-color screen: (addr screen), fg: int, row-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  check-screen-row-in-color-from screen, fg, row-idx, 1, expected, msg
+}
+
+fn check-screen-row-in-color-from screen-on-stack: (addr screen), fg: int, row-idx: int, col-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen, row-idx, col-idx
+  # compare 'expected' with the screen contents starting at 'idx', grapheme by grapheme
+  var e: (stream byte 0x100)
+  var e-addr/edx: (addr stream byte) <- address e
+  write e-addr, expected
+  {
+    var done?/eax: boolean <- stream-empty? e-addr
+    compare done?, 0
+    break-if-!=
+    var _g/eax: grapheme <- screen-grapheme-at-idx screen, idx
+    var g/ebx: grapheme <- copy _g
+    var _expected-grapheme/eax: grapheme <- read-grapheme e-addr
+    var expected-grapheme/edi: grapheme <- copy _expected-grapheme
+    $check-screen-row-in-color-from:compare-cells: {
+      # if expected-grapheme is space, null grapheme is also ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        compare g, 0
+        break-if-= $check-screen-row-in-color-from:compare-cells
+      }
+      # if expected-grapheme is space, a different color is ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        var color/eax: int <- screen-color-at-idx screen, idx
+        compare color, fg
+        break-if-!= $check-screen-row-in-color-from:compare-cells
+      }
+      # compare graphemes
+      $check-screen-row-in-color-from:compare-graphemes: {
+        # if (g == expected-grapheme) print "."
+        compare g, expected-grapheme
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-color-from:compare-graphemes
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") but observed '"
+        print-grapheme-to-real-screen g
+        print-string-to-real-screen "'\n"
+      }
+      $check-screen-row-in-color-from:compare-colors: {
+        var color/eax: int <- screen-color-at-idx screen, idx
+        compare fg, color
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-color-from:compare-colors
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") in color "
+        print-int32-hex-to-real-screen fg
+        print-string-to-real-screen " but observed color "
+        print-int32-hex-to-real-screen color
+        print-string-to-real-screen "\n"
+      }
+    }
+    idx <- increment
+    increment col-idx
+    loop
+  }
+}
+
+# background color is visible even for spaces, so 'expected' behaves as an array of booleans.
+# non-space = given background must match; space = background must not match
+fn check-screen-row-in-background-color screen: (addr screen), bg: int, row-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  check-screen-row-in-background-color-from screen, bg, row-idx, 1, expected, msg
+}
+
+fn check-screen-row-in-background-color-from screen-on-stack: (addr screen), bg: int, row-idx: int, col-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen, row-idx, col-idx
+  # compare 'expected' with the screen contents starting at 'idx', grapheme by grapheme
+  var e: (stream byte 0x100)
+  var e-addr/edx: (addr stream byte) <- address e
+  write e-addr, expected
+  {
+    var done?/eax: boolean <- stream-empty? e-addr
+    compare done?, 0
+    break-if-!=
+    var _g/eax: grapheme <- screen-grapheme-at-idx screen, idx
+    var g/ebx: grapheme <- copy _g
+    var _expected-grapheme/eax: grapheme <- read-grapheme e-addr
+    var expected-grapheme/edx: grapheme <- copy _expected-grapheme
+    $check-screen-row-in-background-color-from:compare-cells: {
+      # if expected-grapheme is space, null grapheme is also ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        compare g, 0
+        break-if-= $check-screen-row-in-background-color-from:compare-cells
+      }
+      # if expected-grapheme is space, a different color is ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        var color/eax: int <- screen-background-color-at-idx screen, idx
+        compare color, bg
+        break-if-!= $check-screen-row-in-background-color-from:compare-cells
+      }
+      # compare graphemes
+      $check-screen-row-in-background-color-from:compare-graphemes: {
+        # if (g == expected-grapheme) print "."
+        compare g, expected-grapheme
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-background-color-from:compare-graphemes
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") but observed '"
+        print-grapheme-to-real-screen g
+        print-string-to-real-screen "'\n"
+      }
+      $check-screen-row-in-background-color-from:compare-colors: {
+        var color/eax: int <- screen-background-color-at-idx screen, idx
+        compare bg, color
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-background-color-from:compare-colors
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") in background color "
+        print-int32-hex-to-real-screen bg
+        print-string-to-real-screen " but observed color "
+        print-int32-hex-to-real-screen color
+        print-string-to-real-screen "\n"
+      }
+    }
+    idx <- increment
+    increment col-idx
+    loop
+  }
+}
+
+fn check-screen-row-in-bold screen: (addr screen), row-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  check-screen-row-in-bold-from screen, row-idx, 1, expected, msg
+}
+
+fn check-screen-row-in-bold-from screen-on-stack: (addr screen), row-idx: int, col-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen, row-idx, col-idx
+  # compare 'expected' with the screen contents starting at 'idx', grapheme by grapheme
+  var e: (stream byte 0x100)
+  var e-addr/edx: (addr stream byte) <- address e
+  write e-addr, expected
+  {
+    var done?/eax: boolean <- stream-empty? e-addr
+    compare done?, 0
+    break-if-!=
+    var _g/eax: grapheme <- screen-grapheme-at-idx screen, idx
+    var g/ebx: grapheme <- copy _g
+    var _expected-grapheme/eax: grapheme <- read-grapheme e-addr
+    var expected-grapheme/edx: grapheme <- copy _expected-grapheme
+    $check-screen-row-in-bold-from:compare-cells: {
+      # if expected-grapheme is space, null grapheme is also ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        compare g, 0
+        break-if-= $check-screen-row-in-bold-from:compare-cells
+      }
+      # if expected-grapheme is space, non-bold is ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        var bold?/eax: boolean <- screen-bold-at-idx? screen, idx
+        compare bold?, 1
+        break-if-!= $check-screen-row-in-bold-from:compare-cells
+      }
+      # compare graphemes
+      $check-screen-row-in-bold-from:compare-graphemes: {
+        # if (g == expected-grapheme) print "."
+        compare g, expected-grapheme
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-bold-from:compare-graphemes
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") but observed '"
+        print-grapheme-to-real-screen g
+        print-string-to-real-screen "'\n"
+      }
+      $check-screen-row-in-bold-from:compare-bold: {
+        var bold?/eax: boolean <- screen-bold-at-idx? screen, idx
+        compare bold?, 1
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-bold-from:compare-bold
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") to be in bold\n"
+      }
+    }
+    idx <- increment
+    increment col-idx
+    loop
+  }
+}
+
+fn check-screen-row-in-underline screen: (addr screen), row-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  check-screen-row-in-underline-from screen, row-idx, 1, expected, msg
+}
+
+fn check-screen-row-in-underline-from screen-on-stack: (addr screen), row-idx: int, col-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen, row-idx, col-idx
+  # compare 'expected' with the screen contents starting at 'idx', grapheme by grapheme
+  var e: (stream byte 0x100)
+  var e-addr/edx: (addr stream byte) <- address e
+  write e-addr, expected
+  {
+    var done?/eax: boolean <- stream-empty? e-addr
+    compare done?, 0
+    break-if-!=
+    var _g/eax: grapheme <- screen-grapheme-at-idx screen, idx
+    var g/ebx: grapheme <- copy _g
+    var _expected-grapheme/eax: grapheme <- read-grapheme e-addr
+    var expected-grapheme/edx: grapheme <- copy _expected-grapheme
+    $check-screen-row-in-underline-from:compare-cells: {
+      # if expected-grapheme is space, null grapheme is also ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        compare g, 0
+        break-if-= $check-screen-row-in-underline-from:compare-cells
+      }
+      # if expected-grapheme is space, non-underline is ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        var underline?/eax: boolean <- screen-underline-at-idx? screen, idx
+        compare underline?, 1
+        break-if-!= $check-screen-row-in-underline-from:compare-cells
+      }
+      # compare graphemes
+      $check-screen-row-in-underline-from:compare-graphemes: {
+        # if (g == expected-grapheme) print "."
+        compare g, expected-grapheme
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-underline-from:compare-graphemes
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") but observed '"
+        print-grapheme-to-real-screen g
+        print-string-to-real-screen "'\n"
+      }
+      $check-screen-row-in-underline-from:compare-underline: {
+        var underline?/eax: boolean <- screen-underline-at-idx? screen, idx
+        compare underline?, 1
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-underline-from:compare-underline
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") to be underlined\n"
+      }
+    }
+    idx <- increment
+    increment col-idx
+    loop
+  }
+}
+
+fn check-screen-row-in-reverse screen: (addr screen), row-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  check-screen-row-in-reverse-from screen, row-idx, 1, expected, msg
+}
+
+fn check-screen-row-in-reverse-from screen-on-stack: (addr screen), row-idx: int, col-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen, row-idx, col-idx
+  # compare 'expected' with the screen contents starting at 'idx', grapheme by grapheme
+  var e: (stream byte 0x100)
+  var e-addr/edx: (addr stream byte) <- address e
+  write e-addr, expected
+  {
+    var done?/eax: boolean <- stream-empty? e-addr
+    compare done?, 0
+    break-if-!=
+    var _g/eax: grapheme <- screen-grapheme-at-idx screen, idx
+    var g/ebx: grapheme <- copy _g
+    var _expected-grapheme/eax: grapheme <- read-grapheme e-addr
+    var expected-grapheme/edx: grapheme <- copy _expected-grapheme
+    $check-screen-row-in-reverse-from:compare-cells: {
+      # if expected-grapheme is space, null grapheme is also ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        compare g, 0
+        break-if-= $check-screen-row-in-reverse-from:compare-cells
+      }
+      # if expected-grapheme is space, non-reverse is ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        var reverse?/eax: boolean <- screen-reverse-at-idx? screen, idx
+        compare reverse?, 1
+        break-if-!= $check-screen-row-in-reverse-from:compare-cells
+      }
+      # compare graphemes
+      $check-screen-row-in-reverse-from:compare-graphemes: {
+        # if (g == expected-grapheme) print "."
+        compare g, expected-grapheme
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-reverse-from:compare-graphemes
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") but observed '"
+        print-grapheme-to-real-screen g
+        print-string-to-real-screen "'\n"
+      }
+      $check-screen-row-in-reverse-from:compare-reverse: {
+        var reverse?/eax: boolean <- screen-reverse-at-idx? screen, idx
+        compare reverse?, 1
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-reverse-from:compare-reverse
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") to be in reverse-video\n"
+      }
+    }
+    idx <- increment
+    increment col-idx
+    loop
+  }
+}
+
+fn check-screen-row-in-blinking screen: (addr screen), row-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  check-screen-row-in-blinking-from screen, row-idx, 1, expected, msg
+}
+
+fn check-screen-row-in-blinking-from screen-on-stack: (addr screen), row-idx: int, col-idx: int, expected: (addr array byte), msg: (addr array byte) {
+  var screen/esi: (addr screen) <- copy screen-on-stack
+  var idx/ecx: int <- screen-cell-index screen, row-idx, col-idx
+  # compare 'expected' with the screen contents starting at 'idx', grapheme by grapheme
+  var e: (stream byte 0x100)
+  var e-addr/edx: (addr stream byte) <- address e
+  write e-addr, expected
+  {
+    var done?/eax: boolean <- stream-empty? e-addr
+    compare done?, 0
+    break-if-!=
+    var _g/eax: grapheme <- screen-grapheme-at-idx screen, idx
+    var g/ebx: grapheme <- copy _g
+    var _expected-grapheme/eax: grapheme <- read-grapheme e-addr
+    var expected-grapheme/edx: grapheme <- copy _expected-grapheme
+    $check-screen-row-in-blinking-from:compare-cells: {
+      # if expected-grapheme is space, null grapheme is also ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        compare g, 0
+        break-if-= $check-screen-row-in-blinking-from:compare-cells
+      }
+      # if expected-grapheme is space, non-blinking is ok
+      {
+        compare expected-grapheme, 0x20
+        break-if-!=
+        var blinking?/eax: boolean <- screen-blink-at-idx? screen, idx
+        compare blinking?, 1
+        break-if-!= $check-screen-row-in-blinking-from:compare-cells
+      }
+      # compare graphemes
+      $check-screen-row-in-blinking-from:compare-graphemes: {
+        # if (g == expected-grapheme) print "."
+        compare g, expected-grapheme
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-blinking-from:compare-graphemes
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") but observed '"
+        print-grapheme-to-real-screen g
+        print-string-to-real-screen "'\n"
+      }
+      $check-screen-row-in-blinking-from:compare-blinking: {
+        var blinking?/eax: boolean <- screen-blink-at-idx? screen, idx
+        compare blinking?, 1
+        {
+          break-if-!=
+          print-string-to-real-screen "."
+          break $check-screen-row-in-blinking-from:compare-blinking
+        }
+        # otherwise print an error
+        print-string-to-real-screen msg
+        print-string-to-real-screen ": expected '"
+        print-grapheme-to-real-screen expected-grapheme
+        print-string-to-real-screen "' at ("
+        print-int32-hex-to-real-screen row-idx
+        print-string-to-real-screen ", "
+        print-int32-hex-to-real-screen col-idx
+        print-string-to-real-screen ") to be blinking\n"
+      }
+    }
+    idx <- increment
+    increment col-idx
+
+    loop
+  }
+}
+
+fn test-print-single-grapheme {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  check-screen-row screen, 1/row, "a", "F - test-print-single-grapheme"  # top-left corner of the screen
+}
+
+fn test-print-multiple-graphemes {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  print-string screen, "Hello, 世界"
+  check-screen-row screen, 1/row, "Hello, 世界", "F - test-print-multiple-graphemes"
+}
+
+fn test-move-cursor {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  move-cursor screen, 1, 4
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  check-screen-row screen, 1/row, "   a", "F - test-move-cursor"  # top row
+}
+
+fn test-move-cursor-zeroes {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  move-cursor screen, 0, 0
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  check-screen-row screen, 1/row, "a", "F - test-move-cursor-zeroes"  # top-left corner of the screen
+}
+
+fn test-move-cursor-zero-row {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  move-cursor screen, 0, 2
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  check-screen-row screen, 1/row, " a", "F - test-move-cursor-zero-row"  # top row
+}
+
+fn test-move-cursor-zero-column {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  move-cursor screen, 4, 0
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  check-screen-row screen, 4/row, "a", "F - test-move-cursor-zero-column"
+}
+
+fn test-move-cursor-negative-row {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 3
+  move-cursor screen, -1/row, 2/col
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  # no move
+  check-screen-row screen, 1/row, "a", "F - test-move-cursor-negative-row"
+}
+
+fn test-move-cursor-negative-column {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5, 3
+  move-cursor screen, 2/row, -1/col
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  # no move
+  check-screen-row screen, 1/row, "a", "F - test-move-cursor-negative-column"
+}
+
+fn test-move-cursor-column-too-large {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 3/cols
+  move-cursor screen, 1/row, 4/col
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  # top row is empty
+  check-screen-row screen, 1/row, "   ", "F - test-move-cursor-column-too-large"
+  # character shows up on next row
+  check-screen-row screen, 2/row, "a", "F - test-move-cursor-column-too-large"
+}
+
+fn test-move-cursor-column-too-large-saturates {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 3/cols
+  move-cursor screen, 1/row, 6/col
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  # top row is empty
+  check-screen-row screen, 1/row, "   ", "F - test-move-cursor-column-too-large-saturates"  # top-left corner of the screen
+  # character shows up at the start of next row
+  check-screen-row screen, 2/row, "a", "F - test-move-cursor-column-too-large-saturates"  # top-left corner of the screen
+}
+
+fn test-move-cursor-row-too-large {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 3/cols
+  move-cursor screen, 6/row, 2/col
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  # bottom row shows the character
+  check-screen-row screen, 5/row, " a", "F - test-move-cursor-row-too-large"
+}
+
+fn test-move-cursor-row-too-large-saturates {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 3/cols
+  move-cursor screen, 9/row, 2/col
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  # bottom row shows the character
+  check-screen-row screen, 5/row, " a", "F - test-move-cursor-row-too-large-saturates"
+}
+
+fn test-check-screen-row-from {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  move-cursor screen, 1, 4
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  check-screen-row screen, 1/row, "   a", "F - test-check-screen-row-from/baseline"
+  check-screen-row-from screen, 1/row, 4/col, "a", "F - test-check-screen-row-from"
+}
+
+fn test-print-string-overflows-to-next-row {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  print-string screen, "abcdefg"
+  check-screen-row screen, 1/row, "abcd", "F - test-print-string-overflows-to-next-row"
+  check-screen-row screen, 2/row, "efg", "F - test-print-string-overflows-to-next-row"
+}
+
+fn test-check-screen-scrolls-on-overflow {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  # single character starting at bottom right
+  move-cursor screen, 5/rows, 4/cols
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  check-screen-row-from screen, 5/row, 4/col, "a", "F - test-check-screen-scrolls-on-overflow/baseline"  # bottom-right corner of the screen
+  # multiple characters starting at bottom right
+  move-cursor screen, 5, 4
+  print-string screen, "ab"
+  # screen scrolled up one row
+#?   check-screen-row screen, 1/row, "    ", "F - test-check-screen-scrolls-on-overflow/x1"
+#?   check-screen-row screen, 2/row, "    ", "F - test-check-screen-scrolls-on-overflow/x2"
+#?   check-screen-row screen, 3/row, "    ", "F - test-check-screen-scrolls-on-overflow/x3"
+#?   check-screen-row screen, 4/row, "   a", "F - test-check-screen-scrolls-on-overflow/x4"
+#?   check-screen-row screen, 5/row, "b   ", "F - test-check-screen-scrolls-on-overflow/x5"
+  check-screen-row-from screen, 4/row, 4/col, "a", "F - test-check-screen-scrolls-on-overflow/1"
+  check-screen-row-from screen, 5/row, 1/col, "b", "F - test-check-screen-scrolls-on-overflow/2"
+}
+
+fn test-check-screen-color {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  start-color screen, 1/fg, 0/bg
+  c <- copy 0x62/b
+  print-grapheme screen, c
+  start-color screen, 0/fg, 7/bg
+  c <- copy 0x63/c
+  print-grapheme screen, c
+  check-screen-row-in-color screen, 0/fg, 1/row, "a c", "F - test-check-screen-color"
+}
+
+fn test-check-screen-background-color {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  start-color screen, 0/fg, 1/bg
+  c <- copy 0x62/b
+  print-grapheme screen, c
+  start-color screen, 0/fg, 7/bg
+  c <- copy 0x63/c
+  print-grapheme screen, c
+  check-screen-row-in-background-color screen, 7/bg, 1/row, "a c", "F - test-check-screen-background-color"
+}
+
+fn test-check-screen-bold {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  start-bold screen
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  reset-formatting screen
+  c <- copy 0x62/b
+  print-grapheme screen, c
+  start-bold screen
+  c <- copy 0x63/c
+  print-grapheme screen, c
+  check-screen-row-in-bold screen, 1/row, "a c", "F - test-check-screen-bold"
+}
+
+fn test-check-screen-underline {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  start-underline screen
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  reset-formatting screen
+  c <- copy 0x62/b
+  print-grapheme screen, c
+  start-underline screen
+  c <- copy 0x63/c
+  print-grapheme screen, c
+  check-screen-row-in-underline screen, 1/row, "a c", "F - test-check-screen-underline"
+}
+
+fn test-check-screen-reverse {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  start-reverse-video screen
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  reset-formatting screen
+  c <- copy 0x62/b
+  print-grapheme screen, c
+  start-reverse-video screen
+  c <- copy 0x63/c
+  print-grapheme screen, c
+  check-screen-row-in-reverse screen, 1/row, "a c", "F - test-check-screen-reverse"
+}
+
+fn test-check-screen-blinking {
+  var screen-on-stack: screen
+  var screen/esi: (addr screen) <- address screen-on-stack
+  initialize-screen screen, 5/rows, 4/cols
+  start-blinking screen
+  var c/eax: grapheme <- copy 0x61/a
+  print-grapheme screen, c
+  reset-formatting screen
+  c <- copy 0x62/b
+  print-grapheme screen, c
+  start-blinking screen
+  c <- copy 0x63/c
+  print-grapheme screen, c
+  check-screen-row-in-blinking screen, 1/row, "a c", "F - test-check-screen-blinking"
+}
+
+#? fn main -> _/ebx: int {
+#? #?   test-check-screen-color
+#?   run-tests
+#?   return 0
+#? }