https://github.com/akkartik/mu/blob/main/browse-slack/environment.mu
   1 type environment {
   2   search-terms: (handle gap-buffer)
   3   tabs: (handle array tab)
   4   current-tab-index: int  # index into tabs
   5   dirty?: boolean
   6   # search mode
   7   cursor-in-search?: boolean
   8   # channel mode
   9   cursor-in-channels?: boolean
  10   channel-cursor-index: int
  11 }
  12 
  13 type tab {
  14   type: int
  15       # type 0: all items
  16       # type 1: items in a channel
  17       # type 2: search for a term
  18       # type 3: comments in a single thread
  19   item-index: int  # what item in the corresponding list we start rendering
  20                    # the current page at
  21   # only for type 0, 1
  22   hidden-items: (handle stream int)
  23   # only for type 1
  24   channel-index: int
  25   # only for type 2
  26   search-terms: (handle gap-buffer)
  27   search-items: (handle array int)
  28   search-items-first-free: int
  29   # only for type 3
  30   root-index: int
  31 }
  32 
  33 # static buffer sizes in this file:
  34 #   main-panel-hor            # in characters
  35 #   item-padding-hor          # in pixels
  36 #   item-padding-ver          # in characters
  37 #   avatar-side               # in pixels
  38 #   avatar-space-hor          # in characters
  39 #   avatar-space-ver          # in characters
  40 #   search-position-x         # in characters
  41 #   search-space-ver          # in characters
  42 #   author-name-padding-ver   # in characters
  43 #   post-right-coord          # in characters
  44 #   channel-offset-x          # in characters
  45 #   menu-space-ver            # in characters
  46 #   max-search-results
  47 
  48 fn initialize-environment _self: (addr environment), _items: (addr item-list) {
  49   var self/esi: (addr environment) <- copy _self
  50   var search-terms-ah/eax: (addr handle gap-buffer) <- get self, search-terms
  51   allocate search-terms-ah
  52   var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
  53   initialize-gap-buffer search-terms, 0x30/search-capacity
  54   var items/eax: (addr item-list) <- copy _items
  55   var items-data-first-free-a/eax: (addr int) <- get items, data-first-free
  56   var final-item/edx: int <- copy *items-data-first-free-a
  57   final-item <- decrement
  58   var tabs-ah/ecx: (addr handle array tab) <- get self, tabs
  59   populate tabs-ah, 0x10/max-history
  60   # current-tab-index implicitly set to 0
  61   var tabs/eax: (addr array tab) <- lookup *tabs-ah
  62   var first-tab/eax: (addr tab) <- index tabs, 0/current-tab-index
  63   var dest/edi: (addr int) <- get first-tab, item-index
  64   copy-to *dest, final-item
  65 }
  66 
  67 ### Render
  68 
  69 fn render-environment screen: (addr screen), _env: (addr environment), users: (addr array user), channels: (addr array channel), items: (addr item-list) {
  70   var env/esi: (addr environment) <- copy _env
  71   {
  72     var dirty?/eax: (addr boolean) <- get env, dirty?
  73     compare *dirty?, 0/false
  74     break-if-!=
  75     # minimize repaints when typing into the search bar
  76     {
  77       var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
  78       compare *cursor-in-search?, 0/false
  79       break-if-=
  80       render-search-input screen, env
  81       clear-rect screen, 0/x 0x2f/y, 0x80/x 0x30/y, 0/bg
  82       render-search-menu screen, env
  83       return
  84     }
  85     # minimize repaints when focus in channel nav
  86     {
  87       var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
  88       compare *cursor-in-channels?, 0/false
  89       break-if-=
  90       render-channels screen, env, channels
  91       clear-rect screen, 0/x 0x2f/y, 0x80/x 0x30/y, 0/bg
  92       render-channels-menu screen, env
  93       return
  94     }
  95   }
  96   # full repaint
  97   clear-screen screen
  98   render-search-input screen, env
  99   render-channels screen, env, channels
 100   render-item-list screen, env, users, channels, items
 101   render-menu screen, env
 102   var dirty?/eax: (addr boolean) <- get env, dirty?
 103   copy-to *dirty?, 0/false
 104 }
 105 
 106 fn render-channels screen: (addr screen), _env: (addr environment), _channels: (addr array channel) {
 107   var env/esi: (addr environment) <- copy _env
 108   var cursor-index/edi: int <- copy -1
 109   {
 110     var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
 111     compare *cursor-in-search?, 0/false
 112     break-if-!=
 113     var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
 114     compare *cursor-in-channels?, 0/false
 115     break-if-=
 116     var cursor-index-addr/eax: (addr int) <- get env, channel-cursor-index
 117     cursor-index <- copy *cursor-index-addr
 118   }
 119   var channels/esi: (addr array channel) <- copy _channels
 120   var y/ebx: int <- copy 2/search-space-ver
 121   y <- add 1/item-padding-ver
 122   var i/ecx: int <- copy 0
 123   var max/edx: int <- length channels
 124   {
 125     compare i, max
 126     break-if->=
 127     var offset/eax: (offset channel) <- compute-offset channels, i
 128     var curr/eax: (addr channel) <- index channels, offset
 129     var name-ah/eax: (addr handle array byte) <- get curr, name
 130     var name/eax: (addr array byte) <- lookup *name-ah
 131     compare name, 0
 132     break-if-=
 133     set-cursor-position screen, 2/x y
 134     {
 135       compare cursor-index, i
 136       break-if-=
 137       draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "#", 7/grey 0/black
 138       draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, name, 7/grey 0/black
 139     }
 140     {
 141       compare cursor-index, i
 142       break-if-!=
 143       # cursor; reverse video
 144       draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "#", 0/black 0xf/white
 145       draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, name, 0/black 0xf/white
 146     }
 147     y <- add 2/channel-padding
 148     i <- increment
 149     loop
 150   }
 151 }
 152 
 153 fn render-item-list screen: (addr screen), _env: (addr environment), users: (addr array user), channels: (addr array channel), items: (addr item-list) {
 154   var env/esi: (addr environment) <- copy _env
 155   var tmp-width/eax: int <- copy 0
 156   var tmp-height/ecx: int <- copy 0
 157   tmp-width, tmp-height <- screen-size screen
 158   var screen-width: int
 159   copy-to screen-width, tmp-width
 160   var screen-height: int
 161   copy-to screen-height, tmp-height
 162   #
 163   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
 164   var _tabs/eax: (addr array tab) <- lookup *tabs-ah
 165   var tabs/edx: (addr array tab) <- copy _tabs
 166   var current-tab-index-a/eax: (addr int) <- get env, current-tab-index
 167   var current-tab-index/eax: int <- copy *current-tab-index-a
 168   var current-tab-offset/eax: (offset tab) <- compute-offset tabs, current-tab-index
 169   var current-tab/edx: (addr tab) <- index tabs, current-tab-offset
 170   var show-cursor?: boolean
 171   {
 172     var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
 173     compare *cursor-in-search?, 0/false
 174     break-if-!=
 175     var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
 176     compare *cursor-in-channels?, 0/false
 177     break-if-!=
 178     copy-to show-cursor?, 1/true
 179   }
 180   render-tab screen, current-tab, show-cursor?, users, channels, items, screen-height
 181   var top/eax: int <- copy screen-height
 182   top <- subtract 2/menu-space-ver
 183   clear-rect screen, 0 top, screen-width screen-height, 0/bg
 184 }
 185 
 186 fn render-tab screen: (addr screen), _current-tab: (addr tab), show-cursor?: boolean, users: (addr array user), channels: (addr array channel), items: (addr item-list), screen-height: int {
 187   var current-tab/esi: (addr tab) <- copy _current-tab
 188   var current-tab-type/eax: (addr int) <- get current-tab, type
 189   compare *current-tab-type, 0/all-items
 190   {
 191     break-if-!=
 192     render-all-items screen, current-tab, show-cursor?, items, users, screen-height
 193     return
 194   }
 195   compare *current-tab-type, 1/channel
 196   {
 197     break-if-!=
 198     render-channel-tab screen, current-tab, show-cursor?, users, channels, items, screen-height
 199     return
 200   }
 201   compare *current-tab-type, 2/search
 202   {
 203     break-if-!=
 204     render-search-tab screen, current-tab, show-cursor?, users, channels, items, screen-height
 205     return
 206   }
 207   compare *current-tab-type, 3/thread
 208   {
 209     break-if-!=
 210     render-thread-tab screen, current-tab, show-cursor?, users, channels, items, screen-height
 211     return
 212   }
 213 }
 214 
 215 fn render-all-items screen: (addr screen), _current-tab: (addr tab), show-cursor?: boolean, _items: (addr item-list), users: (addr array user), screen-height: int {
 216   var current-tab/esi: (addr tab) <- copy _current-tab
 217   var items/edi: (addr item-list) <- copy _items
 218   var newest-item/eax: (addr int) <- get current-tab, item-index
 219   var i/ebx: int <- copy *newest-item
 220   var items-data-first-free-addr/eax: (addr int) <- get items, data-first-free
 221   render-progress screen, i, *items-data-first-free-addr
 222   var items-data-ah/eax: (addr handle array item) <- get items, data
 223   var _items-data/eax: (addr array item) <- lookup *items-data-ah
 224   var items-data/edi: (addr array item) <- copy _items-data
 225   var y/ecx: int <- copy 2/search-space-ver
 226   y <- add 1/item-padding-ver
 227   $render-all-items:loop: {
 228     compare i, 0
 229     break-if-<
 230     {
 231       var hide?/eax: boolean <- should-hide? current-tab, i, _items
 232       compare hide?, 0/false
 233       break-if-=
 234       i <- decrement
 235       loop $render-all-items:loop
 236     }
 237     compare y, screen-height
 238     break-if->=
 239     var offset/eax: (offset item) <- compute-offset items-data, i
 240     var curr-item/eax: (addr item) <- index items-data, offset
 241     y <- render-item screen, curr-item, users, show-cursor?, y, screen-height
 242     # cursor always at top item
 243     copy-to show-cursor?, 0/false
 244     i <- decrement
 245     loop
 246   }
 247 }
 248 
 249 fn render-channel-tab screen: (addr screen), _current-tab: (addr tab), show-cursor?: boolean, users: (addr array user), _channels: (addr array channel), _items: (addr item-list), screen-height: int {
 250   var current-tab/esi: (addr tab) <- copy _current-tab
 251   var items/edi: (addr item-list) <- copy _items
 252   var channels/ebx: (addr array channel) <- copy _channels
 253   var channel-index-addr/eax: (addr int) <- get current-tab, channel-index
 254   var channel-index/eax: int <- copy *channel-index-addr
 255   var channel-offset/eax: (offset channel) <- compute-offset channels, channel-index
 256   var current-channel/ecx: (addr channel) <- index channels, channel-offset
 257   var current-channel-posts-ah/eax: (addr handle array int) <- get current-channel, posts
 258   var _current-channel-posts/eax: (addr array int) <- lookup *current-channel-posts-ah
 259   var current-channel-posts/edx: (addr array int) <- copy _current-channel-posts
 260   var current-channel-first-channel-item-addr/eax: (addr int) <- get current-tab, item-index
 261   var i/ebx: int <- copy *current-channel-first-channel-item-addr
 262   var current-channel-posts-first-free-addr/eax: (addr int) <- get current-channel, posts-first-free
 263   set-cursor-position 0/screen, 0x68/x 0/y
 264   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "channel", 7/fg 0/bg
 265   render-progress screen, i, *current-channel-posts-first-free-addr
 266   var items-data-ah/eax: (addr handle array item) <- get items, data
 267   var _items-data/eax: (addr array item) <- lookup *items-data-ah
 268   var items-data/edi: (addr array item) <- copy _items-data
 269   var y/ecx: int <- copy 2/search-space-ver
 270   y <- add 1/item-padding-ver
 271   {
 272     compare i, 0
 273     break-if-<
 274     compare y, screen-height
 275     break-if->=
 276     var item-index-addr/eax: (addr int) <- index current-channel-posts, i
 277     var item-index/eax: int <- copy *item-index-addr
 278     var item-offset/eax: (offset item) <- compute-offset items-data, item-index
 279     var curr-item/eax: (addr item) <- index items-data, item-offset
 280     y <- render-item screen, curr-item, users, show-cursor?, y, screen-height
 281     # cursor always at top item
 282     copy-to show-cursor?, 0/false
 283     i <- decrement
 284     loop
 285   }
 286 }
 287 
 288 fn render-search-tab screen: (addr screen), _current-tab: (addr tab), show-cursor?: boolean, users: (addr array user), channels: (addr array channel), _items: (addr item-list), screen-height: int {
 289   var current-tab/esi: (addr tab) <- copy _current-tab
 290   var items/edi: (addr item-list) <- copy _items
 291   var current-tab-search-items-ah/eax: (addr handle array int) <- get current-tab, search-items
 292   var _current-tab-search-items/eax: (addr array int) <- lookup *current-tab-search-items-ah
 293   var current-tab-search-items/ebx: (addr array int) <- copy _current-tab-search-items
 294   var current-tab-top-item-addr/eax: (addr int) <- get current-tab, item-index
 295   var i/edx: int <- copy *current-tab-top-item-addr
 296   var current-tab-search-items-first-free-addr/eax: (addr int) <- get current-tab, search-items-first-free
 297   set-cursor-position 0/screen, 0x68/x 0/y
 298   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "search", 7/fg 0/bg
 299   render-progress screen, i, *current-tab-search-items-first-free-addr
 300   {
 301     compare *current-tab-search-items-first-free-addr, 0x100/max-search-results
 302     break-if-<
 303     set-cursor-position 0/screen, 0x68/x 1/y
 304     draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "too many results", 4/fg 0/bg
 305   }
 306   var items-data-ah/eax: (addr handle array item) <- get items, data
 307   var _items-data/eax: (addr array item) <- lookup *items-data-ah
 308   var items-data/edi: (addr array item) <- copy _items-data
 309   var y/ecx: int <- copy 2/search-space-ver
 310   y <- add 1/item-padding-ver
 311   {
 312     compare i, 0
 313     break-if-<
 314     compare y, screen-height
 315     break-if->=
 316     var item-index-addr/eax: (addr int) <- index current-tab-search-items, i
 317     var item-index/eax: int <- copy *item-index-addr
 318     var item-offset/eax: (offset item) <- compute-offset items-data, item-index
 319     var curr-item/eax: (addr item) <- index items-data, item-offset
 320     y <- render-item screen, curr-item, users, show-cursor?, y, screen-height
 321     # cursor always at top item
 322     copy-to show-cursor?, 0/false
 323     i <- decrement
 324     loop
 325   }
 326 }
 327 
 328 fn render-thread-tab screen: (addr screen), _current-tab: (addr tab), show-cursor?: boolean, users: (addr array user), channels: (addr array channel), _items: (addr item-list), screen-height: int {
 329   var current-tab/esi: (addr tab) <- copy _current-tab
 330   var items/eax: (addr item-list) <- copy _items
 331   var items-data-ah/eax: (addr handle array item) <- get items, data
 332   var _items-data/eax: (addr array item) <- lookup *items-data-ah
 333   var items-data/edi: (addr array item) <- copy _items-data
 334   var post-index-addr/eax: (addr int) <- get current-tab, root-index
 335   var post-index/eax: int <- copy *post-index-addr
 336   var post-offset/eax: (offset item) <- compute-offset items-data, post-index
 337   var post/ebx: (addr item) <- index items-data, post-offset
 338   var current-tab-top-item-addr/eax: (addr int) <- get current-tab, item-index
 339   var i/edx: int <- copy *current-tab-top-item-addr
 340   var post-comments-first-free-addr/eax: (addr int) <- get post, comments-first-free
 341   set-cursor-position 0/screen, 0x68/x 0/y
 342   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "thread", 7/fg 0/bg
 343   render-progress screen, i, *post-comments-first-free-addr
 344   var post-comments-ah/eax: (addr handle array int) <- get post, comments
 345   var post-comments/eax: (addr array int) <- lookup *post-comments-ah
 346   var y/ecx: int <- copy 2/search-space-ver
 347   y <- add 1/item-padding-ver
 348   {
 349     compare i, 0
 350     break-if-<
 351     compare y, screen-height
 352     break-if->=
 353     var item-index-addr/eax: (addr int) <- index post-comments, i
 354     var item-index/eax: int <- copy *item-index-addr
 355     var item-offset/eax: (offset item) <- compute-offset items-data, item-index
 356     var curr-item/eax: (addr item) <- index items-data, item-offset
 357     y <- render-item screen, curr-item, users, show-cursor?, y, screen-height
 358     # cursor always at top item
 359     copy-to show-cursor?, 0/false
 360     i <- decrement
 361     loop
 362   }
 363   # finally render the parent -- though we'll never focus on it
 364   y <- render-item screen, post, users, 0/no-cursor, y, screen-height
 365 }
 366 
 367 # side-effect: mutates cursor position
 368 fn render-progress screen: (addr screen), curr: int, max: int {
 369   set-cursor-position 0/screen, 0x70/x 0/y
 370   var top-index/eax: int <- copy max
 371   top-index <- subtract curr  # happy accident: 1-based
 372   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, top-index, 7/fg 0/bg
 373   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "/", 7/fg 0/bg
 374   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen screen, max, 7/fg 0/bg
 375 }
 376 
 377 fn render-search-input screen: (addr screen), _env: (addr environment) {
 378   var env/esi: (addr environment) <- copy _env
 379   var cursor-in-search?/ecx: (addr boolean) <- get env, cursor-in-search?
 380   set-cursor-position 0/screen, 0x22/x=search-position-x 1/y
 381   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "search ", 7/fg 0/bg
 382   var search-terms-ah/eax: (addr handle gap-buffer) <- get env, search-terms
 383   var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
 384   rewind-gap-buffer search-terms
 385   var x/eax: int <- render-gap-buffer screen, search-terms, 0x2a/x 1/y, *cursor-in-search?, 0xf/fg 0/bg
 386   {
 387     compare x, 0x4a/end-search
 388     break-if->
 389     var y/ecx: int <- copy 0
 390     x, y <- render-code-point screen, 0x5f/underscore, 0/xmin 1/ymin, 0x80/xmax, 1/ymax, x, 1/y, 0xf/fg 0/bg
 391     loop
 392   }
 393 }
 394 
 395 # not used in search mode
 396 fn render-menu screen: (addr screen), _env: (addr environment) {
 397   var env/edi: (addr environment) <- copy _env
 398   {
 399     var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
 400     compare *cursor-in-search?, 0/false
 401     break-if-=
 402     render-search-menu screen, env
 403     return
 404   }
 405   var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
 406   compare *cursor-in-channels?, 0/false
 407   {
 408     break-if-=
 409     render-channels-menu screen, env
 410     return
 411   }
 412   render-main-menu screen, env
 413 }
 414 
 415 fn render-main-menu screen: (addr screen), _env: (addr environment) {
 416   var width/eax: int <- copy 0
 417   var y/ecx: int <- copy 0
 418   width, y <- screen-size screen
 419   y <- decrement
 420   set-cursor-position screen, 2/x, y
 421   {
 422     var env/edi: (addr environment) <- copy _env
 423     var num-tabs/edi: (addr int) <- get env, current-tab-index
 424     compare *num-tabs, 0
 425     break-if-<=
 426     draw-text-rightward-from-cursor screen, " Esc ", width, 0/fg 0xf/bg
 427     draw-text-rightward-from-cursor screen, " go back  ", width, 0xf/fg, 0/bg
 428   }
 429   draw-text-rightward-from-cursor screen, " / ", width, 0/fg 0xf/bg
 430   draw-text-rightward-from-cursor screen, " search  ", width, 0xf/fg, 0/bg
 431   draw-text-rightward-from-cursor screen, " Tab ", width, 0/fg 0xf/bg
 432   draw-text-rightward-from-cursor screen, " go to channels  ", width, 0xf/fg, 0/bg
 433   draw-text-rightward-from-cursor screen, " Enter ", width, 0/fg 0xf/bg
 434   draw-text-rightward-from-cursor screen, " go to thread  ", width, 0xf/fg, 0/bg
 435   {
 436     {
 437       var is-all-items-or-channel?/eax: boolean <- current-tab-is-all-items-or-channel? _env
 438       compare is-all-items-or-channel?, 0/false
 439     }
 440     break-if-=
 441     draw-text-rightward-from-cursor screen, " ^h ", width, 0/fg 0xf/bg
 442     draw-text-rightward-from-cursor screen, " hide thread  ", width, 0xf/fg, 0/bg
 443     draw-text-rightward-from-cursor screen, " ^u ", width, 0/fg 0xf/bg
 444     draw-text-rightward-from-cursor screen, " unhide all  ", width, 0xf/fg, 0/bg
 445   }
 446   draw-text-rightward-from-cursor screen, " ^b ", width, 0/fg 0xf/bg
 447   draw-text-rightward-from-cursor screen, " << page  ", width, 0xf/fg, 0/bg
 448   draw-text-rightward-from-cursor screen, " ^f ", width, 0/fg 0xf/bg
 449   draw-text-rightward-from-cursor screen, " page >>  ", width, 0xf/fg, 0/bg
 450 }
 451 
 452 fn render-channels-menu screen: (addr screen), _env: (addr environment) {
 453   var width/eax: int <- copy 0
 454   var y/ecx: int <- copy 0
 455   width, y <- screen-size screen
 456   y <- decrement
 457   set-cursor-position screen, 2/x, y
 458   {
 459     var env/edi: (addr environment) <- copy _env
 460     var num-tabs/edi: (addr int) <- get env, current-tab-index
 461     compare *num-tabs, 0
 462     break-if-<=
 463     draw-text-rightward-from-cursor screen, " Esc ", width, 0/fg 0xf/bg
 464     draw-text-rightward-from-cursor screen, " go back  ", width, 0xf/fg, 0/bg
 465   }
 466   draw-text-rightward-from-cursor screen, " / ", width, 0/fg 0xf/bg
 467   draw-text-rightward-from-cursor screen, " search  ", width, 0xf/fg, 0/bg
 468   draw-text-rightward-from-cursor screen, " Tab ", width, 0/fg 0xf/bg
 469   draw-text-rightward-from-cursor screen, " go to items  ", width, 0xf/fg, 0/bg
 470   draw-text-rightward-from-cursor screen, " Enter ", width, 0/fg 0xf/bg
 471   draw-text-rightward-from-cursor screen, " select  ", width, 0xf/fg, 0/bg
 472 }
 473 
 474 fn render-search-menu screen: (addr screen), _env: (addr environment) {
 475   var width/eax: int <- copy 0
 476   var y/ecx: int <- copy 0
 477   width, y <- screen-size screen
 478   y <- decrement
 479   set-cursor-position screen, 2/x, y
 480   draw-text-rightward-from-cursor screen, " Esc ", width, 0/fg 0xf/bg
 481   draw-text-rightward-from-cursor screen, " cancel  ", width, 0xf/fg, 0/bg
 482   draw-text-rightward-from-cursor screen, " Enter ", width, 0/fg 0xf/bg
 483   draw-text-rightward-from-cursor screen, " select  ", width, 0xf/fg, 0/bg
 484   draw-text-rightward-from-cursor screen, " ^a ", width, 0/fg, 0xf/bg
 485   draw-text-rightward-from-cursor screen, " <<  ", width, 0xf/fg, 0/bg
 486   draw-text-rightward-from-cursor screen, " ^b ", width, 0/fg, 0xf/bg
 487   draw-text-rightward-from-cursor screen, " <word  ", width, 0xf/fg, 0/bg
 488   draw-text-rightward-from-cursor screen, " ^f ", width, 0/fg, 0xf/bg
 489   draw-text-rightward-from-cursor screen, " word>  ", width, 0xf/fg, 0/bg
 490   draw-text-rightward-from-cursor screen, " ^e ", width, 0/fg, 0xf/bg
 491   draw-text-rightward-from-cursor screen, " >>  ", width, 0xf/fg, 0/bg
 492   draw-text-rightward-from-cursor screen, " ^u ", width, 0/fg, 0xf/bg
 493   draw-text-rightward-from-cursor screen, " clear  ", width, 0xf/fg, 0/bg
 494 }
 495 
 496 fn render-item screen: (addr screen), _item: (addr item), _users: (addr array user), show-cursor?: boolean, y: int, screen-height: int -> _/ecx: int {
 497   var item/esi: (addr item) <- copy _item
 498   var users/edi: (addr array user) <- copy _users
 499   var author-index-addr/ecx: (addr int) <- get item, by
 500   var author-index/ecx: int <- copy *author-index-addr
 501   var author-offset/ecx: (offset user) <- compute-offset users, author-index
 502   var author/ecx: (addr user) <- index users, author-offset
 503   # author avatar
 504   var author-avatar-ah/eax: (addr handle image) <- get author, avatar
 505   var _author-avatar/eax: (addr image) <- lookup *author-avatar-ah
 506   var author-avatar/ebx: (addr image) <- copy _author-avatar
 507   {
 508     compare author-avatar, 0
 509     break-if-=
 510     var y/edx: int <- copy y
 511     y <- shift-left 4/log2font-height
 512     var x/eax: int <- copy 0x20/main-panel-hor
 513     x <- shift-left 3/log2font-width
 514     x <- add 0x18/item-padding-hor
 515     render-image screen, author-avatar, x, y, 0x50/avatar-side, 0x50/avatar-side
 516   }
 517   # channel
 518   var channel-name-ah/eax: (addr handle array byte) <- get item, channel
 519   var channel-name/eax: (addr array byte) <- lookup *channel-name-ah
 520   {
 521     var x/eax: int <- copy 0x20/main-panel-hor
 522     x <- add 0x40/channel-offset-x
 523     set-cursor-position screen, x y
 524   }
 525   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, "#", 7/grey 0/black
 526   draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, channel-name, 7/grey 0/black
 527   # author name
 528   {
 529     var author-real-name-ah/eax: (addr handle array byte) <- get author, real-name
 530     var author-real-name/eax: (addr array byte) <- lookup *author-real-name-ah
 531     var x/ecx: int <- copy 0x20/main-panel-hor
 532     x <- add 0x10/avatar-space-hor
 533     set-cursor-position screen, x y
 534     draw-text-wrapping-right-then-down-from-cursor-over-full-screen screen, author-real-name, 0xf/white 0/black
 535   }
 536   increment y
 537   # text
 538   var text-ah/eax: (addr handle array byte) <- get item, text
 539   var _text/eax: (addr array byte) <- lookup *text-ah
 540   var text/edx: (addr array byte) <- copy _text
 541   var text-y/eax: int <- render-slack-message screen, text, show-cursor?, y, screen-height
 542   # flush
 543   add-to y, 6/avatar-space-ver
 544   compare y, text-y
 545   {
 546     break-if-<
 547     return y
 548   }
 549   return text-y
 550 }
 551 
 552 fn render-slack-message screen: (addr screen), text: (addr array byte), highlight?: boolean, ymin: int, ymax: int -> _/eax: int {
 553   var x/eax: int <- copy 0x20/main-panel-hor
 554   x <- add 0x10/avatar-space-hor
 555   var y/ecx: int <- copy ymin
 556   y <- add 1/author-name-padding-ver
 557   $render-slack-message:draw: {
 558     compare highlight?, 0/false
 559     {
 560       break-if-=
 561       x, y <- draw-json-text-wrapping-right-then-down screen, text, x y, 0x70/xmax=post-right-coord ymax, x y, 0/fg 7/bg
 562       break $render-slack-message:draw
 563     }
 564     x, y <- draw-json-text-wrapping-right-then-down screen, text, x y, 0x70/xmax=post-right-coord ymax, x y, 7/fg 0/bg
 565   }
 566   y <- add 2/item-padding-ver
 567   return y
 568 }
 569 
 570 # draw text in the rectangle from (xmin, ymin) to (xmax, ymax), starting from (x, y), wrapping as necessary
 571 # return the next (x, y) coordinate in raster order where drawing stopped
 572 # that way the caller can draw more if given the same min and max bounding-box.
 573 # if there isn't enough space, truncate
 574 fn draw-json-text-wrapping-right-then-down screen: (addr screen), _text: (addr array byte), xmin: int, ymin: int, xmax: int, ymax: int, _x: int, _y: int, color: int, background-color: int -> _/eax: int, _/ecx: int {
 575   var stream-storage: (stream byte 0x4000/print-buffer-size)
 576   var stream/edi: (addr stream byte) <- address stream-storage
 577   var text/esi: (addr array byte) <- copy _text
 578   var len/eax: int <- length text
 579   compare len, 0x4000/print-buffer-size
 580   {
 581     break-if-<
 582     write stream, "ERROR: stream too small in draw-text-wrapping-right-then-down"
 583   }
 584   compare len, 0x4000/print-buffer-size
 585   {
 586     break-if->=
 587     write stream, text
 588   }
 589   var x/eax: int <- copy _x
 590   var y/ecx: int <- copy _y
 591   x, y <- draw-json-stream-wrapping-right-then-down screen, stream, xmin, ymin, xmax, ymax, x, y, color, background-color
 592   return x, y
 593 }
 594 
 595 # draw a stream in the rectangle from (xmin, ymin) to (xmax, ymax), starting from (x, y), wrapping as necessary
 596 # return the next (x, y) coordinate in raster order where drawing stopped
 597 # that way the caller can draw more if given the same min and max bounding-box.
 598 # if there isn't enough space, truncate
 599 fn draw-json-stream-wrapping-right-then-down screen: (addr screen), stream: (addr stream byte), xmin: int, ymin: int, xmax: int, ymax: int, x: int, y: int, color: int, background-color: int -> _/eax: int, _/ecx: int {
 600   var xcurr/ecx: int <- copy x
 601   var ycurr/edx: int <- copy y
 602   var c/ebx: code-point <- copy 0
 603   var next-c/esi: code-point <- copy 0
 604   {
 605     # read c from either next-c or stream
 606     $draw-json-stream-wrapping-right-then-down:read-base: {
 607       compare next-c, 0
 608       {
 609         break-if-=
 610         c <- copy next-c
 611         next-c <- copy 0
 612         break $draw-json-stream-wrapping-right-then-down:read-base
 613       }
 614       c <- read-json-code-point stream
 615     }
 616     compare c, 0xffffffff/end-of-file
 617     break-if-=
 618     $draw-json-stream-wrapping-right-then-down:render-grapheme: {
 619       compare c, 0x5c/backslash
 620       {
 621         break-if-!=
 622         xcurr, ycurr <- render-json-escaped-code-point screen, stream, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 623         break $draw-json-stream-wrapping-right-then-down:render-grapheme
 624       }
 625       compare c, 0xa/newline
 626       {
 627         break-if-!=
 628         # minimum effort to clear cursor
 629         var dummy/eax: int <- draw-code-point screen, 0x20/space, xcurr, ycurr, color, background-color
 630         xcurr <- copy xmin
 631         ycurr <- increment
 632         break $draw-json-stream-wrapping-right-then-down:render-grapheme
 633       }
 634       var offset/eax: int <- draw-code-point screen, c, xcurr, ycurr, color, background-color
 635       # overlay a combining character if necessary
 636       $draw-json-stream-wrapping-right-then-down:read-combiner: {
 637         var done?/eax: boolean <- stream-empty? stream
 638         compare done?, 0/false
 639         break-if-!=
 640         # read a character
 641         # no combining character allowed here
 642         var g/eax: grapheme <- read-grapheme stream
 643         var c/eax: code-point <- to-code-point g
 644         # if not a combining character, save for next iteration and loop
 645         {
 646           var combining-code-point?/eax: boolean <- combining-code-point? c
 647           compare combining-code-point?, 0/false
 648         }
 649         {
 650           break-if-!=
 651           next-c <- copy c
 652           break $draw-json-stream-wrapping-right-then-down:read-combiner
 653         }
 654         # otherwise overlay it without saving its width
 655         # This means strange results if a base and its combiner have different
 656         # widths. We'll always follow the base width.
 657         var dummy/eax: int <- overlay-code-point screen, c, xcurr, ycurr, color, background-color
 658       }
 659       xcurr <- add offset
 660       compare xcurr, xmax
 661       {
 662         break-if-<
 663         xcurr <- copy xmin
 664         ycurr <- increment
 665       }
 666     }
 667     loop
 668   }
 669   set-cursor-position screen, xcurr, ycurr
 670   return xcurr, ycurr
 671 }
 672 
 673 # just return a different register
 674 fn read-json-code-point stream: (addr stream byte) -> _/ebx: code-point {
 675   var g/eax: grapheme <- read-grapheme stream
 676   var result/eax: code-point <- to-code-point g
 677   return result
 678 }
 679 
 680 # '\' encountered
 681 # https://www.json.org/json-en.html
 682 fn render-json-escaped-code-point screen: (addr screen), stream: (addr stream byte), xmin: int, ymin: int, xmax: int, ymax: int, xcurr: int, ycurr: int, color: int, background-color: int -> _/ecx: int, _/edx: int {
 683   var g/ebx: code-point <- read-json-code-point stream
 684   compare g, 0xffffffff/end-of-file
 685   {
 686     break-if-!=
 687     return xcurr, ycurr
 688   }
 689   # \n = newline
 690   compare g, 0x6e/n
 691   var x/eax: int <- copy xcurr
 692   {
 693     break-if-!=
 694     increment ycurr
 695     return xmin, ycurr
 696   }
 697   # ignore \t \r \f \b
 698   {
 699     compare g, 0x74/t
 700     break-if-!=
 701     return xcurr, ycurr
 702   }
 703   {
 704     compare g, 0x72/r
 705     break-if-!=
 706     return xcurr, ycurr
 707   }
 708   {
 709     compare g, 0x66/f
 710     break-if-!=
 711     return xcurr, ycurr
 712   }
 713   {
 714     compare g, 0x62/b
 715     break-if-!=
 716     return xcurr, ycurr
 717   }
 718   var y/ecx: int <- copy 0
 719   # \u = Unicode
 720   {
 721     compare g, 0x75/u
 722     break-if-!=
 723     x, y <- render-json-escaped-unicode-code-point screen, stream, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 724     var y/edx: int <- copy y
 725     return x, y
 726   }
 727   # most characters escape to themselves
 728   # combining characters not supported after backslash
 729   x, y <- render-code-point screen, g, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 730   var y/edx: int <- copy y
 731   return x, y
 732 }
 733 
 734 # '\u' encountered
 735 fn render-json-escaped-unicode-code-point screen: (addr screen), stream: (addr stream byte), xmin: int, ymin: int, xmax: int, ymax: int, xcurr: int, ycurr: int, color: int, background-color: int -> _/eax: int, _/ecx: int {
 736   var hex-digits-storage: (array byte 4)
 737   var hex-digits/esi: (addr array byte) <- address hex-digits-storage
 738   # slurp 4 bytes exactly
 739   var src/eax: byte <- read-byte stream
 740   var dest/ecx: (addr byte) <- index hex-digits, 0
 741   copy-byte-to *dest, src
 742   src <- read-byte stream
 743   dest <- index hex-digits, 1
 744   copy-byte-to *dest, src
 745   src <- read-byte stream
 746   dest <- index hex-digits, 2
 747   copy-byte-to *dest, src
 748   src <- read-byte stream
 749   dest <- index hex-digits, 3
 750   copy-byte-to *dest, src
 751   # \u2013 = -
 752   {
 753     var endash?/eax: boolean <- string-equal? hex-digits, "2013"
 754     compare endash?, 0/false
 755     break-if-=
 756     var x/eax: int <- copy 0
 757     var y/ecx: int <- copy 0
 758     x, y <- render-code-point screen, 0x2d/dash, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 759     return x, y
 760   }
 761   # \u2014 = -
 762   {
 763     var emdash?/eax: boolean <- string-equal? hex-digits, "2014"
 764     compare emdash?, 0/false
 765     break-if-=
 766     var x/eax: int <- copy 0
 767     var y/ecx: int <- copy 0
 768     x, y <- render-code-point screen, 0x2d/dash, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 769     return x, y
 770   }
 771   # \u2018 = '
 772   {
 773     var left-quote?/eax: boolean <- string-equal? hex-digits, "2018"
 774     compare left-quote?, 0/false
 775     break-if-=
 776     var x/eax: int <- copy 0
 777     var y/ecx: int <- copy 0
 778     x, y <- render-code-point screen, 0x27/quote, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 779     return x, y
 780   }
 781   # \u2019 = '
 782   {
 783     var right-quote?/eax: boolean <- string-equal? hex-digits, "2019"
 784     compare right-quote?, 0/false
 785     break-if-=
 786     var x/eax: int <- copy 0
 787     var y/ecx: int <- copy 0
 788     x, y <- render-code-point screen, 0x27/quote, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 789     return x, y
 790   }
 791   # \u201c = "
 792   {
 793     var left-dquote?/eax: boolean <- string-equal? hex-digits, "201c"
 794     compare left-dquote?, 0/false
 795     break-if-=
 796     var x/eax: int <- copy 0
 797     var y/ecx: int <- copy 0
 798     x, y <- render-code-point screen, 0x22/dquote, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 799     return x, y
 800   }
 801   # \u201d = "
 802   {
 803     var right-dquote?/eax: boolean <- string-equal? hex-digits, "201d"
 804     compare right-dquote?, 0/false
 805     break-if-=
 806     var x/eax: int <- copy 0
 807     var y/ecx: int <- copy 0
 808     x, y <- render-code-point screen, 0x22/dquote, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 809     return x, y
 810   }
 811   # \u2022 = *
 812   {
 813     var bullet?/eax: boolean <- string-equal? hex-digits, "2022"
 814     compare bullet?, 0/false
 815     break-if-=
 816     var x/eax: int <- copy 0
 817     var y/ecx: int <- copy 0
 818     x, y <- render-code-point screen, 0x2a/asterisk, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 819     return x, y
 820   }
 821   # \u2026 = ...
 822   {
 823     var ellipses?/eax: boolean <- string-equal? hex-digits, "2026"
 824     compare ellipses?, 0/false
 825     break-if-=
 826     var x/eax: int <- copy 0
 827     var y/ecx: int <- copy 0
 828     x, y <- draw-text-wrapping-right-then-down screen, "...", xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 829     return x, y
 830   }
 831   var n/eax: int <- parse-hex-int hex-digits
 832   var c/edx: code-point <- copy n
 833   var x/eax: int <- copy 0
 834   var y/ecx: int <- copy 0
 835   x, y <- render-code-point screen, c, xmin, ymin, xmax, ymax, xcurr, ycurr, color, background-color
 836   return x, y
 837 }
 838 
 839 ### Edit
 840 
 841 fn update-environment _env: (addr environment), key: byte, users: (addr array user), channels: (addr array channel), items: (addr item-list) {
 842   var env/edi: (addr environment) <- copy _env
 843   # first dispatch to search mode if necessary
 844   {
 845     var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
 846     compare *cursor-in-search?, 0/false
 847     break-if-=
 848     update-search env, key, users, channels, items
 849     return
 850   }
 851   {
 852     compare key, 0x2f/slash
 853     break-if-!=
 854     # enter search mode
 855     var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
 856     copy-to *cursor-in-search?, 1/true
 857     # do one more repaint
 858     var dirty?/eax: (addr boolean) <- get env, dirty?
 859     copy-to *dirty?, 1/true
 860     return
 861   }
 862   {
 863     compare key, 0x1b/esc
 864     break-if-!=
 865     # back in history
 866     previous-tab env
 867     return
 868   }
 869   var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
 870   {
 871     compare key, 9/tab
 872     break-if-!=
 873     # toggle cursor between main panel and channels nav
 874     not *cursor-in-channels?  # bitwise NOT; only works if you never assign 1/true to this variable
 875     # do one more repaint
 876     var dirty?/eax: (addr boolean) <- get env, dirty?
 877     copy-to *dirty?, 1/true
 878     return
 879   }
 880   {
 881     compare *cursor-in-channels?, 0/false
 882     break-if-!=
 883     update-main-panel env, key, users, channels, items
 884     return
 885   }
 886   {
 887     compare *cursor-in-channels?, 0/false
 888     break-if-=
 889     update-channels-nav env, key, users, channels, items
 890     return
 891   }
 892 }
 893 
 894 fn update-main-panel env: (addr environment), key: byte, users: (addr array user), channels: (addr array channel), items: (addr item-list) {
 895   {
 896     compare key, 0xa/newline
 897     break-if-!=
 898     new-thread-tab env, users, channels, items
 899     return
 900   }
 901   {
 902     compare key, 8/ctrl-h
 903     break-if-!=
 904     var is-all-items-or-channel?/eax: boolean <- current-tab-is-all-items-or-channel? env
 905     compare is-all-items-or-channel?, 0/false
 906     break-if-=
 907     hide-thread env, users, channels, items
 908     return
 909   }
 910   {
 911     compare key, 0x15/ctrl-u
 912     break-if-!=
 913     var is-all-items-or-channel?/eax: boolean <- current-tab-is-all-items-or-channel? env
 914     compare is-all-items-or-channel?, 0/false
 915     break-if-=
 916     new-all-items-tab env, users, channels, items
 917     return
 918   }
 919   {
 920     compare key, 0x81/down-arrow
 921     break-if-!=
 922     next-item env, users, channels, items
 923     return
 924   }
 925   {
 926     compare key, 0x82/up-arrow
 927     break-if-!=
 928     previous-item env, users, channels, items
 929     return
 930   }
 931   {
 932     compare key, 6/ctrl-f
 933     break-if-!=
 934     page-down env, users, channels, items
 935     return
 936   }
 937   {
 938     compare key, 2/ctrl-b
 939     break-if-!=
 940     page-up env, users, channels, items
 941     return
 942   }
 943 }
 944 
 945 fn current-tab-is-all-items-or-channel? _env: (addr environment) -> _/eax: boolean {
 946   var env/esi: (addr environment) <- copy _env
 947   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
 948   var _tabs/eax: (addr array tab) <- lookup *tabs-ah
 949   var tabs/edx: (addr array tab) <- copy _tabs
 950   var current-tab-index-a/eax: (addr int) <- get env, current-tab-index
 951   var current-tab-index/eax: int <- copy *current-tab-index-a
 952   var current-tab-offset/eax: (offset tab) <- compute-offset tabs, current-tab-index
 953   var current-tab/edx: (addr tab) <- index tabs, current-tab-offset
 954   var current-tab-type/eax: (addr int) <- get current-tab, type
 955   {
 956     compare *current-tab-type, 0/all-items
 957     break-if-!=
 958     return 1/true
 959   }
 960   {
 961     compare *current-tab-type, 1/channel
 962     break-if-!=
 963     return 1/true
 964   }
 965   return 0/false
 966 }
 967 
 968 # TODO: clamp cursor within bounds
 969 fn update-channels-nav _env: (addr environment), key: byte, users: (addr array user), channels: (addr array channel), items: (addr item-list) {
 970   var env/edi: (addr environment) <- copy _env
 971   var channel-cursor-index/eax: (addr int) <- get env, channel-cursor-index
 972   {
 973     compare key, 0x81/down-arrow
 974     break-if-!=
 975     increment *channel-cursor-index
 976     return
 977   }
 978   {
 979     compare key, 0x82/up-arrow
 980     break-if-!=
 981     decrement *channel-cursor-index
 982     return
 983   }
 984   {
 985     compare key, 0xa/newline
 986     break-if-!=
 987     new-channel-tab env, *channel-cursor-index, channels
 988     var cursor-in-channels?/eax: (addr boolean) <- get env, cursor-in-channels?
 989     copy-to *cursor-in-channels?, 0/false
 990     return
 991   }
 992 }
 993 
 994 fn update-search _env: (addr environment), key: byte, users: (addr array user), channels: (addr array channel), items: (addr item-list) {
 995   var env/edi: (addr environment) <- copy _env
 996   var cursor-in-search?/eax: (addr boolean) <- get env, cursor-in-search?
 997   {
 998     compare key 0x1b/esc
 999     break-if-!=
1000     # get out of search mode
1001     copy-to *cursor-in-search?, 0/false
1002     return
1003   }
1004   {
1005     compare key, 0xa/newline
1006     break-if-!=
1007     # perform a search, then get out of search mode
1008     new-search-tab env, items
1009     copy-to *cursor-in-search?, 0/false
1010     return
1011   }
1012   # otherwise delegate
1013   var search-terms-ah/eax: (addr handle gap-buffer) <- get env, search-terms
1014   var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
1015   var g/ecx: grapheme <- copy key
1016   edit-gap-buffer search-terms, g
1017 }
1018 
1019 fn new-all-items-tab _env: (addr environment), users: (addr array user), channels: (addr array channel), _items: (addr item-list) {
1020   var env/edi: (addr environment) <- copy _env
1021   var current-tab-index-addr/ecx: (addr int) <- get env, current-tab-index
1022   increment *current-tab-index-addr
1023   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1024   var tabs/eax: (addr array tab) <- lookup *tabs-ah
1025   var max-tabs/ebx: int <- length tabs
1026   {
1027     compare *current-tab-index-addr, max-tabs
1028     break-if-<
1029     abort "history overflow; grow max-history (we should probably improve this)"
1030   }
1031   var current-tab-index/ecx: int <- copy *current-tab-index-addr
1032   var current-tab-offset/ecx: (offset tab) <- compute-offset tabs, current-tab-index
1033   var current-tab/ecx: (addr tab) <- index tabs, current-tab-offset
1034   clear-object current-tab
1035   var items/eax: (addr item-list) <- copy _items
1036   var items-data-first-free-a/eax: (addr int) <- get items, data-first-free
1037   var final-item/edx: int <- copy *items-data-first-free-a
1038   final-item <- decrement
1039   var dest/edi: (addr int) <- get current-tab, item-index
1040   copy-to *dest, final-item
1041 }
1042 
1043 fn new-thread-tab _env: (addr environment), users: (addr array user), channels: (addr array channel), _items: (addr item-list) {
1044   var env/edi: (addr environment) <- copy _env
1045   var current-tab-index-addr/ecx: (addr int) <- get env, current-tab-index
1046   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1047   var tabs/eax: (addr array tab) <- lookup *tabs-ah
1048   var current-tab-index/ecx: int <- copy *current-tab-index-addr
1049   var current-tab-offset/ecx: (offset tab) <- compute-offset tabs, current-tab-index
1050   var current-tab/ecx: (addr tab) <- index tabs, current-tab-offset
1051   var item-index/esi: int <- item-index env, _items, channels
1052   var post-index/ecx: int <- post-index _items, item-index
1053   var current-tab-index-addr/eax: (addr int) <- get env, current-tab-index
1054   increment *current-tab-index-addr
1055   var current-tab-index/edx: int <- copy *current-tab-index-addr
1056   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1057   var tabs/eax: (addr array tab) <- lookup *tabs-ah
1058   var max-tabs/ebx: int <- length tabs
1059   compare current-tab-index, max-tabs
1060   {
1061     compare current-tab-index, max-tabs
1062     break-if-<
1063     abort "history overflow; grow max-history (we should probably improve this)"
1064   }
1065   var current-tab-offset/edi: (offset tab) <- compute-offset tabs, current-tab-index
1066   var current-tab/edi: (addr tab) <- index tabs, current-tab-offset
1067   clear-object current-tab
1068   var current-tab-type/eax: (addr int) <- get current-tab, type
1069   copy-to *current-tab, 3/thread
1070   var current-tab-root-index/eax: (addr int) <- get current-tab, root-index
1071   copy-to *current-tab-root-index, post-index
1072   var items/eax: (addr item-list) <- copy _items
1073   var items-data-ah/eax: (addr handle array item) <- get items, data
1074   var items-data/eax: (addr array item) <- lookup *items-data-ah
1075   var offset/ecx: (offset item) <- compute-offset items-data, post-index
1076   var post/eax: (addr item) <- index items-data, offset
1077   var post-comments-first-free-addr/ecx: (addr int) <- get post, comments-first-free
1078   # terminology:
1079   #   post-comment-index = index of a comment in a post's comment array
1080   #   comment-index = index of a comment in the global item list
1081   var final-post-comment-index/ecx: int <- copy *post-comments-first-free-addr
1082   final-post-comment-index <- decrement
1083   var post-comments-ah/eax: (addr handle array int) <- get post, comments
1084   var post-comments/eax: (addr array int) <- lookup *post-comments-ah
1085   # look for item-index in post-comments[0..final-post-comment-index]
1086   var curr-post-comment-index/edx: int <- copy final-post-comment-index
1087   {
1088     compare curr-post-comment-index, 0
1089     {
1090       break-if->=
1091       # if we didn't find the current item in a post's comments, it must be
1092       # the parent post itself which isn't in the comment list but hackily
1093       # rendered at the bottom. Just render the whole comment list.
1094       var tab-item-index-addr/edi: (addr int) <- get current-tab, item-index
1095       copy-to *tab-item-index-addr, curr-post-comment-index
1096       return
1097     }
1098     var curr-comment-index/ecx: (addr int) <- index post-comments, curr-post-comment-index
1099     compare *curr-comment-index, item-index
1100     {
1101       break-if-!=
1102       # item-index found
1103       var tab-item-index-addr/edi: (addr int) <- get current-tab, item-index
1104       copy-to *tab-item-index-addr, curr-post-comment-index
1105       return
1106     }
1107     curr-post-comment-index <- decrement
1108     loop
1109   }
1110   abort "new-thread-tab: should never leave previous loop without returning"
1111 }
1112 
1113 # hide a thread in a (channel or all-items) tab
1114 fn hide-thread _env: (addr environment), users: (addr array user), channels: (addr array channel), items: (addr item-list) {
1115   var env/edi: (addr environment) <- copy _env
1116   var current-tab-index-addr/eax: (addr int) <- get env, current-tab-index
1117   var current-tab-index/ecx: int <- copy *current-tab-index-addr
1118   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1119   var tabs/eax: (addr array tab) <- lookup *tabs-ah
1120   var current-tab-offset/ecx: (offset tab) <- compute-offset tabs, current-tab-index
1121   var current-tab/ebx: (addr tab) <- index tabs, current-tab-offset
1122   var current-tab-hidden-items-ah/edx: (addr handle stream int) <- get current-tab, hidden-items
1123   var current-tab-hidden-items/eax: (addr stream int) <- lookup *current-tab-hidden-items-ah
1124   {
1125     compare current-tab-hidden-items, 0
1126     break-if-!=
1127     populate-stream current-tab-hidden-items-ah, 0x10/max-hidden-threads
1128     current-tab-hidden-items <- lookup *current-tab-hidden-items-ah
1129   }
1130   {
1131     var too-many-hidden-items?/eax: boolean <- stream-full? current-tab-hidden-items
1132     compare too-many-hidden-items?, 0/false
1133     break-if-=
1134     abort "too many hidden threads in this tab"  # TODO: create a space for flash error messages on screen
1135     return
1136   }
1137   var current-item-index/esi: int <- item-index env, items, channels
1138   var current-post-index-value/ecx: int <- post-index items, current-item-index
1139   # . turn current-post-index into an addr
1140   var current-post-index-storage: int
1141   copy-to current-post-index-storage, current-post-index-value
1142   var current-post-index-addr/ecx: (addr int) <- address current-post-index-storage
1143   #
1144   write-to-stream current-tab-hidden-items, current-post-index-addr
1145   # current-tab's item-index is now on a hidden item
1146   # try to position it on a visible item
1147   var item-index-addr/esi: (addr int) <- get current-tab, item-index
1148   var old-item-index/eax: int <- copy *item-index-addr
1149   next-item env, users, channels, items
1150   compare *item-index-addr, old-item-index
1151   break-if-!=
1152   previous-item env, users, channels, items
1153 }
1154 
1155 fn should-hide? _tab: (addr tab), item-index: int, items: (addr item-list) -> _/eax: boolean {
1156   var post-index/ecx: int <- post-index items, item-index
1157   var tab/esi: (addr tab) <- copy _tab
1158   var tab-hidden-items-ah/edx: (addr handle stream int) <- get tab, hidden-items
1159   var tab-hidden-items/eax: (addr stream int) <- lookup *tab-hidden-items-ah
1160   compare tab-hidden-items, 0
1161   {
1162     break-if-!=
1163     # either we haven't hidden anything, or we're in a tab type that doesn't
1164     # support hiding
1165     return 0/false
1166   }
1167   rewind-stream tab-hidden-items
1168   {
1169     {
1170       var done?/eax: boolean <- stream-empty? tab-hidden-items
1171       compare done?, 0/false
1172     }
1173     break-if-!=
1174     var curr-item: int
1175     var curr-item-addr/edx: (addr int) <- address curr-item
1176     read-from-stream tab-hidden-items, curr-item-addr
1177     # if curr-item == post-index, return true
1178     compare curr-item, post-index
1179     {
1180       break-if-!=
1181       return 1/true
1182     }
1183     loop
1184   }
1185   return 0/false
1186 }
1187 
1188 # what index in the global items list is the cursor at in the current tab?
1189 fn item-index _env: (addr environment), _items: (addr item-list), _channels: (addr array channel) -> _/esi: int {
1190   var env/eax: (addr environment) <- copy _env
1191   var current-tab-index-addr/esi: (addr int) <- get env, current-tab-index
1192   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1193   var tabs/eax: (addr array tab) <- lookup *tabs-ah
1194   var tab-index/esi: int <- copy *current-tab-index-addr
1195   var tab-offset/esi: (offset tab) <- compute-offset tabs, tab-index
1196   var tab/esi: (addr tab) <- index tabs, tab-offset
1197   var tab-type/eax: (addr int) <- get tab, type
1198   {
1199     compare *tab-type, 0/all-items
1200     break-if-!=
1201     var tab-item-index/eax: (addr int) <- get tab, item-index
1202     return *tab-item-index
1203   }
1204   {
1205     compare *tab-type, 1/channel
1206     break-if-!=
1207     var channel-index-addr/eax: (addr int) <- get tab, channel-index
1208     var channel-index/eax: int <- copy *channel-index-addr
1209     var channels/ecx: (addr array channel) <- copy _channels
1210     var channel-offset/eax: (offset channel) <- compute-offset channels, channel-index
1211     var current-channel/eax: (addr channel) <- index channels, channel-offset
1212     var current-channel-posts-ah/eax: (addr handle array int) <- get current-channel, posts
1213     var current-channel-posts/eax: (addr array int) <- lookup *current-channel-posts-ah
1214     var channel-item-index-addr/ecx: (addr int) <- get tab, item-index
1215     var channel-item-index/ecx: int <- copy *channel-item-index-addr
1216     var channel-item-index/eax: (addr int) <- index current-channel-posts, channel-item-index
1217     return *channel-item-index
1218   }
1219   {
1220     compare *tab-type, 2/search
1221     break-if-!=
1222     var tab-search-items-ah/eax: (addr handle array int) <- get tab, search-items
1223     var tab-search-items/eax: (addr array int) <- lookup *tab-search-items-ah
1224     var tab-search-items-index-addr/ecx: (addr int) <- get tab, item-index
1225     var tab-search-items-index/ecx: int <- copy *tab-search-items-index-addr
1226     var src/eax: (addr int) <- index tab-search-items, tab-search-items-index
1227     return *src
1228   }
1229   {
1230     compare *tab-type, 3/thread
1231     break-if-!=
1232     var items/eax: (addr item-list) <- copy _items
1233     var items-data-ah/eax: (addr handle array item) <- get items, data
1234     var _items-data/eax: (addr array item) <- lookup *items-data-ah
1235     var items-data/edi: (addr array item) <- copy _items-data
1236     var tab-root-index-addr/eax: (addr int) <- get tab, root-index
1237     var tab-root-index/eax: int <- copy *tab-root-index-addr
1238     var tab-root-offset/eax: (offset item) <- compute-offset items-data, tab-root-index
1239     var post/eax: (addr item) <- index items-data, tab-root-offset
1240     var post-comments-ah/eax: (addr handle array int) <- get post, comments
1241     var post-comments/eax: (addr array int) <- lookup *post-comments-ah
1242     var tab-item-index-addr/ecx: (addr int) <- get tab, item-index
1243     var tab-item-index/ecx: int <- copy *tab-item-index-addr
1244     var src/eax: (addr int) <- index post-comments, tab-item-index
1245     return *src
1246   }
1247   abort "item-index: unknown tab type"
1248   return -1
1249 }
1250 
1251 # go from a comment item to its parent post
1252 fn post-index _items: (addr item-list), item-index: int -> _/ecx: int {
1253   var items/eax: (addr item-list) <- copy _items
1254   var items-data-ah/eax: (addr handle array item) <- get items, data
1255   var items-data/eax: (addr array item) <- lookup *items-data-ah
1256   var index/ecx: int <- copy item-index
1257   var offset/ecx: (offset item) <- compute-offset items-data, index
1258   var item/eax: (addr item) <- index items-data, offset
1259   var parent/eax: (addr int) <- get item, parent
1260   compare *parent, 0
1261   {
1262     break-if-=
1263     return *parent
1264   }
1265   return item-index
1266 }
1267 
1268 fn new-channel-tab _env: (addr environment), channel-index: int, _channels: (addr array channel) {
1269   var env/edi: (addr environment) <- copy _env
1270   var current-tab-index-addr/eax: (addr int) <- get env, current-tab-index
1271   increment *current-tab-index-addr
1272   var current-tab-index/ecx: int <- copy *current-tab-index-addr
1273   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1274   var tabs/eax: (addr array tab) <- lookup *tabs-ah
1275   var max-tabs/edx: int <- length tabs
1276   compare current-tab-index, max-tabs
1277   {
1278     compare current-tab-index, max-tabs
1279     break-if-<
1280     abort "history overflow; grow max-history (we should probably improve this)"
1281   }
1282   var current-tab-offset/ecx: (offset tab) <- compute-offset tabs, current-tab-index
1283   var current-tab/ecx: (addr tab) <- index tabs, current-tab-offset
1284   clear-object current-tab
1285   var current-tab-type/eax: (addr int) <- get current-tab, type
1286   copy-to *current-tab, 1/channel
1287   var current-tab-channel-index/eax: (addr int) <- get current-tab, channel-index
1288   var curr-channel-index/edx: int <- copy channel-index
1289   copy-to *current-tab-channel-index, curr-channel-index
1290   var channels/esi: (addr array channel) <- copy _channels
1291   var curr-channel-offset/eax: (offset channel) <- compute-offset channels, curr-channel-index
1292   var curr-channel/eax: (addr channel) <- index channels, curr-channel-offset
1293   var curr-channel-posts-first-free-addr/eax: (addr int) <- get curr-channel, posts-first-free
1294   var curr-channel-final-post-index/eax: int <- copy *curr-channel-posts-first-free-addr
1295   curr-channel-final-post-index <- decrement
1296   var dest/edi: (addr int) <- get current-tab, item-index
1297   copy-to *dest, curr-channel-final-post-index
1298 }
1299 
1300 fn new-search-tab _env: (addr environment), items: (addr item-list) {
1301   var env/edi: (addr environment) <- copy _env
1302   var current-tab-index-addr/eax: (addr int) <- get env, current-tab-index
1303   increment *current-tab-index-addr
1304   var current-tab-index/ecx: int <- copy *current-tab-index-addr
1305   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1306   var tabs/eax: (addr array tab) <- lookup *tabs-ah
1307   var max-tabs/edx: int <- length tabs
1308   compare current-tab-index, max-tabs
1309   {
1310     compare current-tab-index, max-tabs
1311     break-if-<
1312     abort "history overflow; grow max-history (we should probably improve this)"
1313   }
1314   var current-tab-offset/ecx: (offset tab) <- compute-offset tabs, current-tab-index
1315   var current-tab/ecx: (addr tab) <- index tabs, current-tab-offset
1316   clear-object current-tab
1317   var current-tab-type/eax: (addr int) <- get current-tab, type
1318   copy-to *current-tab, 2/search
1319   var current-tab-search-terms-ah/edx: (addr handle gap-buffer) <- get current-tab, search-terms
1320   allocate current-tab-search-terms-ah
1321   var current-tab-search-terms/eax: (addr gap-buffer) <- lookup *current-tab-search-terms-ah
1322   initialize-gap-buffer current-tab-search-terms, 0x30/search-capacity
1323   var search-terms-ah/ebx: (addr handle gap-buffer) <- get env, search-terms
1324   copy-gap-buffer search-terms-ah, current-tab-search-terms-ah
1325   var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
1326   search-items current-tab, items, search-terms
1327 }
1328 
1329 fn search-items _tab: (addr tab), _items: (addr item-list), search-terms: (addr gap-buffer) {
1330   var tab/edi: (addr tab) <- copy _tab
1331   var tab-items-first-free-addr/esi: (addr int) <- get tab, search-items-first-free
1332   var tab-items-ah/eax: (addr handle array int) <- get tab, search-items
1333   populate tab-items-ah, 0x100/max-search-results
1334   var _tab-items/eax: (addr array int) <- lookup *tab-items-ah
1335   var tab-items/edi: (addr array int) <- copy _tab-items
1336   # preprocess search-terms
1337   var search-terms-stream-storage: (stream byte 0x100)
1338   var search-terms-stream-addr/ecx: (addr stream byte) <- address search-terms-stream-storage
1339   emit-gap-buffer search-terms, search-terms-stream-addr
1340   var search-terms-text-h: (handle array byte)
1341   var search-terms-text-ah/eax: (addr handle array byte) <- address search-terms-text-h
1342   stream-to-array search-terms-stream-addr, search-terms-text-ah
1343   var tmp/eax: (addr array byte) <- lookup *search-terms-text-ah
1344   var search-terms-text: (addr array byte)
1345   copy-to search-terms-text, tmp
1346   #
1347   var items/ecx: (addr item-list) <- copy _items
1348   var items-data-ah/eax: (addr handle array item) <- get items, data
1349   var _items-data/eax: (addr array item) <- lookup *items-data-ah
1350   var items-data/ebx: (addr array item) <- copy _items-data
1351   var items-data-first-free-a/edx: (addr int) <- get items, data-first-free
1352   var i/ecx: int <- copy 0
1353   {
1354     compare i, *items-data-first-free-a
1355     break-if->=
1356     var curr-offset/eax: (offset item) <- compute-offset items-data, i
1357     var curr-item/eax: (addr item) <- index items-data, curr-offset
1358     var found?/eax: boolean <- search-terms-match? curr-item, search-terms-text
1359     compare found?, 0/false
1360     {
1361       break-if-=
1362       var tab-items-first-free/eax: int <- copy *tab-items-first-free-addr
1363       compare tab-items-first-free, 0x100/max-search-results
1364       break-if->=
1365       var dest/eax: (addr int) <- index tab-items, tab-items-first-free
1366       copy-to *dest, i
1367       increment *tab-items-first-free-addr
1368     }
1369     i <- increment
1370     loop
1371   }
1372   var tab/edi: (addr tab) <- copy _tab
1373   var tab-item-index-addr/edi: (addr int) <- get tab, item-index
1374   var tab-items-first-free/eax: int <- copy *tab-items-first-free-addr
1375   tab-items-first-free <- decrement
1376   copy-to *tab-item-index-addr, tab-items-first-free
1377 }
1378 
1379 fn search-terms-match? _item: (addr item), search-terms: (addr array byte) -> _/eax: boolean {
1380   var item/esi: (addr item) <- copy _item
1381   var item-text-ah/eax: (addr handle array byte) <- get item, text
1382   var item-text/eax: (addr array byte) <- lookup *item-text-ah
1383   var i/ecx: int <- copy 0
1384   var max/edx: int <- length item-text
1385   var search-terms2/ebx: (addr array byte) <- copy search-terms
1386   var slen/ebx: int <- length search-terms2
1387   max <- subtract slen
1388   {
1389     compare i, max
1390     break-if->
1391     var found?/eax: boolean <- substring-match? item-text, search-terms, i
1392     compare found?, 0/false
1393     {
1394       break-if-=
1395       return 1/true
1396     }
1397     i <- increment
1398     loop
1399   }
1400   return 0/false
1401 }
1402 
1403 fn substring-match? _s: (addr array byte), _pat: (addr array byte), start: int -> _/eax: boolean {
1404   var s/esi: (addr array byte) <- copy _s
1405   var pat/edi: (addr array byte) <- copy _pat
1406   var s-idx/edx: int <- copy start
1407   var pat-idx/ebx: int <- copy 0
1408   var pat-len: int
1409   var tmp/eax: int <- length pat
1410   copy-to pat-len, tmp
1411   {
1412     compare pat-idx, pat-len
1413     break-if->=
1414     var s-ab/eax: (addr byte) <- index s, s-idx
1415     var s-b/eax: byte <- copy-byte *s-ab
1416     var pat-ab/ecx: (addr byte) <- index pat, pat-idx
1417     var pat-b/ecx: byte <- copy-byte *pat-ab
1418     compare s-b, pat-b
1419     {
1420       break-if-=
1421       return 0/false
1422     }
1423     s-idx <- increment
1424     pat-idx <- increment
1425     loop
1426   }
1427   return 1/true
1428 }
1429 
1430 fn previous-tab _env: (addr environment) {
1431   var env/edi: (addr environment) <- copy _env
1432   var current-tab-index-addr/ecx: (addr int) <- get env, current-tab-index
1433   compare *current-tab-index-addr, 0
1434   {
1435     break-if-<=
1436     decrement *current-tab-index-addr
1437     # if necessary restore search state
1438     var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1439     var tabs/eax: (addr array tab) <- lookup *tabs-ah
1440     var current-tab-index/ecx: int <- copy *current-tab-index-addr
1441     var current-tab-offset/ecx: (offset tab) <- compute-offset tabs, current-tab-index
1442     var current-tab/ecx: (addr tab) <- index tabs, current-tab-offset
1443     var current-tab-type/eax: (addr int) <- get current-tab, type
1444     compare *current-tab-type, 2/search
1445     break-if-!=
1446     var current-tab-search-terms-ah/ecx: (addr handle gap-buffer) <- get current-tab, search-terms
1447     var search-terms-ah/edx: (addr handle gap-buffer) <- get env, search-terms
1448     var search-terms/eax: (addr gap-buffer) <- lookup *search-terms-ah
1449     clear-gap-buffer search-terms
1450     copy-gap-buffer current-tab-search-terms-ah, search-terms-ah
1451   }
1452 }
1453 
1454 fn next-item _env: (addr environment), users: (addr array user), _channels: (addr array channel), _items: (addr item-list) {
1455   var env/edi: (addr environment) <- copy _env
1456   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1457   var _tabs/eax: (addr array tab) <- lookup *tabs-ah
1458   var tabs/edx: (addr array tab) <- copy _tabs
1459   var current-tab-index-a/eax: (addr int) <- get env, current-tab-index
1460   var current-tab-index/eax: int <- copy *current-tab-index-a
1461   var current-tab-offset/eax: (offset tab) <- compute-offset tabs, current-tab-index
1462   var current-tab/edx: (addr tab) <- index tabs, current-tab-offset
1463   var dest/ebx: (addr int) <- get current-tab, item-index
1464   # if current-tab isn't all-items or channel, no need to worry about hidden items
1465   var current-tab-type/eax: (addr int) <- get current-tab, type
1466   {
1467     compare *current-tab-type, 0/all-items
1468     break-if-=
1469     compare *current-tab-type, 1/channel
1470     break-if-=
1471     {
1472       compare *dest, 0
1473       break-if-<=
1474       decrement *dest
1475     }
1476     return
1477   }
1478   var old-value/ecx: int <- copy *dest
1479   # do { --*dest } while *dest > 0 and should-hide?(current-tab, *dest)
1480   {
1481     compare *dest, 0
1482     break-if-<=
1483     decrement *dest
1484     # if current item is not hidden, return
1485     var current-item-index/esi: int <- item-index env, _items, _channels
1486     var should-hide?/eax: boolean <- should-hide? current-tab, current-item-index, _items
1487     compare should-hide?, 0/false
1488     loop-if-!=
1489     return
1490   }
1491   # couldn't find a visible item. Restore.
1492   copy-to *dest, old-value
1493 }
1494 
1495 fn previous-item _env: (addr environment), users: (addr array user), _channels: (addr array channel), _items: (addr item-list) {
1496   var env/edi: (addr environment) <- copy _env
1497   var tabs-ah/eax: (addr handle array tab) <- get env, tabs
1498   var _tabs/eax: (addr array tab) <- lookup *tabs-ah
1499   var tabs/edx: (addr array tab) <- copy _tabs
1500   var current-tab-index-a/eax: (addr int) <- get env, current-tab-index
1501   var current-tab-index/eax: int <- copy *current-tab-index-a
1502   var current-tab-offset/eax: (offset tab) <- compute-offset tabs, current-tab-index
1503   var current-tab/edx: (addr tab) <- index tabs, current-tab-offset
1504   var current-tab-type/eax: (addr int) <- get current-tab, type
1505   compare *current-tab-type, 0/all-items
1506   {
1507     break-if-!=
1508     var items/esi: (addr item-list) <- copy _items
1509     var items-data-first-free-a/ecx: (addr int) <- get items, data-first-free
1510     var final-item-index/ecx: int <- copy *items-data-first-free-a
1511     final-item-index <- decrement
1512     var dest/ebx: (addr int) <- get current-tab, item-index
1513     var old-value/eax: int <- copy *dest
1514     # do { ++*dest } while *dest < final-index and should-hide?(current-tab, *dest)
1515     {
1516       compare *dest, final-item-index
1517       break-if->=
1518       increment *dest
1519       # if current item is not hidden, return
1520       var current-item-index/esi: int <- item-index env, _items, _channels
1521       var should-hide?/eax: boolean <- should-hide? current-tab, current-item-index, _items
1522       compare should-hide?, 0/false
1523       loop-if-!=
1524       return
1525     }
1526     # couldn't find a visible item. Restore.
1527     copy-to *dest, old-value
1528     return
1529   }
1530   compare *current-tab-type, 1/channel
1531   {
1532     break-if-!=
1533     var current-channel-index-addr/eax: (addr int) <- get current-tab, channel-index
1534     var current-channel-index/eax: int <- copy *current-channel-index-addr
1535     var channels/esi: (addr array channel) <- copy _channels
1536     var current-channel-offset/eax: (offset channel) <- compute-offset channels, current-channel-index
1537     var current-channel/eax: (addr channel) <- index channels, current-channel-offset
1538     var current-channel-posts-first-free-addr/eax: (addr int) <- get current-channel, posts-first-free
1539     var final-item-index/ecx: int <- copy *current-channel-posts-first-free-addr
1540     final-item-index <- decrement
1541     var dest/ebx: (addr int) <- get current-tab, item-index
1542     var old-value/eax: int <- copy *dest
1543     # do { ++*dest } while *dest < final-index and should-hide?(current-tab, *dest)
1544     {
1545       compare *dest, final-item-index
1546       break-if->=
1547       increment *dest
1548       # if current item is not hidden, return
1549       var current-item-index/esi: int <- item-index env, _items, _channels
1550       var should-hide?/eax: boolean <- should-hide? current-tab, current-item-index, _items
1551       compare should-hide?, 0/false
1552       loop-if-!=
1553       return
1554     }
1555     # couldn't find a visible item. Restore.
1556     copy-to *dest, old-value
1557     return
1558   }
1559   compare *current-tab-type, 2/search
1560   {
1561     break-if-!=
1562     var current-tab-search-items-first-free-addr/eax: (addr int) <- get current-tab, search-items-first-free
1563     var final-item-index/ecx: int <- copy *current-tab-search-items-first-free-addr
1564     final-item-index <- decrement
1565     var dest/eax: (addr int) <- get current-tab, item-index
1566     compare *dest, final-item-index
1567     break-if->=
1568     increment *dest
1569     return
1570   }
1571   compare *current-tab-type, 3/thread
1572   {
1573     break-if-!=
1574     var items/eax: (addr item-list) <- copy _items
1575     var items-data-ah/eax: (addr handle array item) <- get items, data
1576     var _items-data/eax: (addr array item) <- lookup *items-data-ah
1577     var items-data/esi: (addr array item) <- copy _items-data
1578     var current-tab-root-index-addr/eax: (addr int) <- get current-tab, root-index
1579     var current-tab-root-index/eax: int <- copy *current-tab-root-index-addr
1580     var current-tab-root-offset/eax: (offset item) <- compute-offset items-data, current-tab-root-index
1581     var post/eax: (addr item) <- index items-data, current-tab-root-offset
1582     var post-comments-first-free-addr/ecx: (addr int) <- get post, comments-first-free
1583     var final-item-index/ecx: int <- copy *post-comments-first-free-addr
1584     final-item-index <- decrement
1585     var dest/eax: (addr int) <- get current-tab, item-index
1586     compare *dest, final-item-index
1587     break-if->=
1588     increment *dest
1589     return
1590   }
1591 }
1592 
1593 fn page-down _env: (addr environment), users: (addr array user), channels: (addr array channel), _items: (addr item-list) {
1594   var env/edi: (addr environment) <- copy _env
1595   var items/eax: (addr item-list) <- copy _items
1596   var items-data-ah/eax: (addr handle array item) <- get items, data
1597   var _items-data/eax: (addr array item) <- lookup *items-data-ah
1598   var items-data/ebx: (addr array item) <- copy _items-data
1599   var _old-item-index/esi: int <- item-index env, _items, channels
1600   var old-item-index/ecx: int <- copy _old-item-index
1601   var y/edx: int <- copy 2
1602   {
1603     compare y, 0x28/screen-height-minus-menu
1604     break-if->=
1605     var item-index/esi: int <- item-index env, _items, channels
1606     {
1607       compare y, 2
1608       break-if-=  # skip this condition on first iteration
1609       compare item-index, old-item-index
1610       break-if-!=
1611       # no forward progress; we're at the bottom of the current tab
1612       return
1613     }
1614     var item-offset/eax: (offset item) <- compute-offset items-data, item-index
1615     var item/eax: (addr item) <- index items-data, item-offset
1616     var item-text-ah/eax: (addr handle array byte) <- get item, text
1617     var item-text/eax: (addr array byte) <- lookup *item-text-ah
1618     var h/eax: int <- estimate-height item-text
1619     y <- add h
1620     next-item env, users, channels, _items
1621     loop
1622   }
1623   # we're past the end of the screen now, so bounce back for some continuity
1624   previous-item env, users, channels, _items
1625   {
1626     # HACK: make sure we make forward progress even if a single post takes up
1627     # the whole screen.
1628     # We can't see the rest of that single post at the moment. But at least we
1629     # can go past it.
1630     var old-item-index/eax: int <- copy old-item-index
1631     var item-index/esi: int <- item-index env, _items, channels
1632     compare item-index, old-item-index
1633     break-if-!=
1634     next-item env, users, channels, _items
1635   }
1636 }
1637 
1638 fn page-up _env: (addr environment), users: (addr array user), channels: (addr array channel), _items: (addr item-list) {
1639   var env/edi: (addr environment) <- copy _env
1640   var items/eax: (addr item-list) <- copy _items
1641   var items-data-ah/eax: (addr handle array item) <- get items, data
1642   var _items-data/eax: (addr array item) <- lookup *items-data-ah
1643   var items-data/ebx: (addr array item) <- copy _items-data
1644   var _old-item-index/esi: int <- item-index env, _items, channels
1645   var old-item-index/ecx: int <- copy _old-item-index
1646   var y/edx: int <- copy 2
1647   {
1648     compare y, 0x28/screen-height-minus-menu
1649     break-if->=
1650     var item-index/esi: int <- item-index env, _items, channels
1651     {
1652       compare y, 2
1653       break-if-=  # skip this condition on first iteration
1654       compare item-index, old-item-index
1655       break-if-!=
1656       # no forward progress; we're at the bottom of the current tab
1657       return
1658     }
1659     var item-offset/eax: (offset item) <- compute-offset items-data, item-index
1660     var item/eax: (addr item) <- index items-data, item-offset
1661     var item-text-ah/eax: (addr handle array byte) <- get item, text
1662     var item-text/eax: (addr array byte) <- lookup *item-text-ah
1663     var h/eax: int <- estimate-height item-text
1664     y <- add h
1665     previous-item env, users, channels, _items
1666     loop
1667   }
1668 }
1669 
1670 # keep sync'd with render-item
1671 fn estimate-height _message-text: (addr array byte) -> _/eax: int {
1672   var message-text/esi: (addr array byte) <- copy _message-text
1673   var result/eax: int <- length message-text
1674   var remainder/edx: int <- copy 0
1675   result, remainder <- integer-divide result, 0x40/post-width
1676   compare remainder, 0
1677   {
1678     break-if-=
1679     result <- increment
1680   }
1681   result <- add 2/item-padding-ver
1682   compare result, 6/avatar-space-ver
1683   {
1684     break-if->
1685     return 6/avatar-space-ver
1686   }
1687   return result
1688 }