///|
#cfg(not(platform="windows"))
extern "C" fn tui_is_tty(fd : Int) -> Int = "moonbit_tui_is_tty"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_enable_raw(fd : Int) -> Int = "moonbit_tui_enable_raw"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_restore(fd : Int) -> Int = "moonbit_tui_restore"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_width(fd : Int) -> Int = "moonbit_tui_width"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_height(fd : Int) -> Int = "moonbit_tui_height"

///|
#cfg(not(platform="windows"))
#borrow(bytes)
extern "C" fn tui_write(
  fd : Int,
  bytes : Bytes,
  offset : Int,
  len : Int,
) -> Int = "moonbit_tui_write"

///|
#cfg(not(platform="windows"))
extern "C" fn tui_install_resize_handler() -> Unit = "moonbit_tui_install_resize_handler"

///|
#cfg(not(platform="windows"))
#borrow(command, args)
extern "C" fn tui_exec_process(
  command : Bytes,
  command_offset : Int,
  command_len : Int,
  args : Bytes,
  args_offset : Int,
  args_len : Int,
  argc : Int,
) -> Int = "moonbit_tui_exec_process"

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn is_tty(fd : Int) -> Bool {
  tui_is_tty(fd) == 1
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn is_tty(fd : Int) -> Bool {
  ignore(fd)
  false
}

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn terminal_size() -> Size raise TerminalError {
  let width = tui_width(1)
  let height = tui_height(1)
  guard width > 0 && height > 0 else {
    raise NativeError("failed to read terminal size")
  }
  { width, height, }
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn terminal_size() -> Size raise TerminalError {
  raise UnsupportedPlatform
}

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn enter_raw_mode() -> Unit raise TerminalError {
  guard is_tty(0) && is_tty(1) else { raise NotATty("stdin/stdout") }
  guard tui_enable_raw(0) == 0 else {
    raise NativeError("failed to enable raw mode")
  }
  tui_install_resize_handler()
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn enter_raw_mode() -> Unit raise TerminalError {
  raise UnsupportedPlatform
}

///|
#coverage.skip
#cfg(not(platform="windows"))
pub fn restore_terminal() -> Unit {
  ignore(tui_restore(0))
}

///|
#coverage.skip
#cfg(platform="windows")
pub fn restore_terminal() -> Unit {
  ()
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run(
  self : Program[Model, Msg],
) -> Unit raise TerminalError {
  ignore(self.run_with_options_returning_model(ProgramOptions::default()))
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_options(
  self : Program[Model, Msg],
  options : ProgramOptions,
) -> Unit raise TerminalError {
  ignore(self.run_with_options_returning_model(options))
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_returning_model(
  self : Program[Model, Msg],
) -> Model raise TerminalError {
  self.run_with_options_returning_model(ProgramOptions::default())
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_options_returning_model(
  self : Program[Model, Msg],
  options : ProgramOptions,
) -> Model raise TerminalError {
  self.run_with_runtime_control(options, None, 0).model
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_cancel_token(
  self : Program[Model, Msg],
  options : ProgramOptions,
  token : CancelToken,
) -> Model raise TerminalError {
  self.run_with_cancel_token_result(options, token).model
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_timeout(
  self : Program[Model, Msg],
  options : ProgramOptions,
  milliseconds : Int,
) -> Model raise TerminalError {
  self.run_with_timeout_result(options, milliseconds).model
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_options_result(
  self : Program[Model, Msg],
  options : ProgramOptions,
) -> ProgramRuntimeResult[Model] raise TerminalError {
  self.run_with_runtime_control(options, None, 0)
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_cancel_token_result(
  self : Program[Model, Msg],
  options : ProgramOptions,
  token : CancelToken,
) -> ProgramRuntimeResult[Model] raise TerminalError {
  self.run_with_runtime_control(options, Some(token), 0)
}

///|
#coverage.skip
pub async fn[Model, Msg] Program::run_with_timeout_result(
  self : Program[Model, Msg],
  options : ProgramOptions,
  milliseconds : Int,
) -> ProgramRuntimeResult[Model] raise TerminalError {
  self.run_with_runtime_control(options, None, milliseconds)
}

///|
#coverage.skip
priv struct RuntimeState {
  mut previous : StyledFrame?
  mut current_size : Size
  mut exit : ProgramExit
}

///|
#coverage.skip
async fn[Model, Msg] Program::run_with_runtime_control(
  self : Program[Model, Msg],
  options : ProgramOptions,
  cancel_token : CancelToken?,
  timeout_millis : Int,
) -> ProgramRuntimeResult[Model] raise TerminalError {
  enter_raw_mode()
  defer restore_terminal()
  let state : RuntimeState = {
    previous: None,
    current_size: terminal_size(),
    exit: ProgramQuit,
  }
  let queue = @aqueue.Queue::new(kind=Unbounded)
  let emit = Emit::new(msg => {
    ignore(queue.try_put(RuntimeMsg(msg)) catch { _ => false })
  })
  for cmd in self.handle_event(emit, Resize(state.current_size)) {
    cmd.run_with_terminal(command => {
      ignore(queue.try_put(RuntimeTerminal(command)) catch { _ => false })
    }) catch {
      err => raise NativeError(err.to_string())
    }
  }
  self
  .init(emit)
  .run_with_terminal(command => {
    ignore(queue.try_put(RuntimeTerminal(command)) catch { _ => false })
  }) catch {
    err => raise NativeError(err.to_string())
  }
  // Undo the start-program output (alternate screen, hidden cursor, mouse,
  // bracketed paste, focus events, autowrap) when leaving by error or
  // cancellation. `catch` no longer observes cancellation, so this cannot
  // live in the task-group handler below. `write_terminal` is a synchronous
  // FFI write, so it completes even while the task is being cancelled.
  errdefer (write_terminal(stop_program_output(options)) catch { _ => () })
  if options.renderer_enabled {
    let initial_view = self.view()
    let initial_frame = StyledFrame::from_node(initial_view, state.current_size)
    write_terminal(
      start_program_output(
        options,
        initial_view,
        state.current_size,
        initial_frame,
      ),
    )
    state.previous = Some(initial_frame)
  } else {
    write_terminal(start_program_control_output(options))
  }
  self.mark_clean()
  @async.with_task_group(group => {
    self.subscriptions().start(emit, group)
    group.spawn_bg(no_wait=true, allow_failure=true, () => {
      read_terminal_input(self, queue, group)
    })
    group.spawn_bg(no_wait=true, allow_failure=true, () => {
      watch_terminal_resize(
        queue,
        state.current_size,
        options.resize_poll_millis,
      )
    })
    if options.renderer_enabled && options.renderer_fps > 0 {
      let interval = Int::max(1, 1000 / options.renderer_fps)
      group.spawn_bg(no_wait=true, allow_failure=true, () => {
        while true {
          @async.sleep(interval)
          queue.put(RuntimeRender)
        }
      })
    }
    match cancel_token {
      Some(token) =>
        group.spawn_bg(no_wait=true, allow_failure=true, () => {
          while !token.is_cancelled() {
            @async.sleep(16)
          }
          queue.put(RuntimeCancel)
        })
      None => ()
    }
    if timeout_millis > 0 {
      group.spawn_bg(no_wait=true, allow_failure=true, () => {
        @async.sleep(timeout_millis)
        queue.put(RuntimeTimeout)
      })
    }
    while true {
      let item = queue.get()
      let mut render_due = options.renderer_fps <= 0 ||
        item is RuntimeRender ||
        item is RuntimeResize(_)
      match
        handle_runtime_item(self, emit, queue, group, item, options, state) {
        Some(exit) => {
          state.exit = exit
          group.return_immediately(())
          break
        }
        None => ()
      }
      let mut drained = 0
      while drained < options.max_messages_per_frame {
        match (queue.try_get() catch { _ => None }) {
          Some(item) => {
            if item is RuntimeRender || item is RuntimeResize(_) {
              render_due = true
            }
            match
              handle_runtime_item(
                self, emit, queue, group, item, options, state,
              ) {
              Some(exit) => {
                state.exit = exit
                group.return_immediately(())
                break
              }
              None => ()
            }
            drained += 1
          }
          None => break
        }
      }
      if self.is_dirty() && render_due {
        if options.renderer_enabled {
          let next_size = terminal_size()
          let resized = next_size != state.current_size
          state.current_size = next_size
          let next = StyledFrame::from_node(self.view(), state.current_size)
          let output = if resized {
            repaint_resized_frame(next)
          } else {
            frame_update_output(options, state.previous, next)
          }
          write_terminal(output)
          state.previous = Some(next)
        } else {
          state.current_size = terminal_size() catch { _ => state.current_size }
        }
        self.mark_clean()
      }
    }
  }) catch {
    err => raise NativeError(err.to_string())
  }
  write_terminal(stop_program_output(options))
  { model: self.model(), exit: state.exit, }
}

///|
#coverage.skip
priv enum RuntimeItem[Msg] {
  RuntimeMsg(Msg)
  RuntimeTerminal(TerminalCommand)
  RuntimeRender
  RuntimeResize(Size)
  RuntimeCancel
  RuntimeTimeout
}

///|
#coverage.skip
fn start_program_output(
  options : ProgramOptions,
  node : Node,
  size : Size,
  frame : StyledFrame,
) -> String {
  if !options.renderer_enabled {
    return start_program_control_output(options)
  }
  if options.alternate_screen {
    ansi_start_program(options, node, size)
  } else {
    let out = StringBuilder::new()
    out.write_string(ansi_disable_autowrap())
    if options.hide_cursor {
      out.write_string(ansi_hide_cursor())
    }
    if options.mouse_mode != MouseOff {
      out.write_string(ansi_enable_mouse(options.mouse_mode))
    }
    if options.bracketed_paste {
      out.write_string(ansi_enable_bracketed_paste())
    }
    if options.focus_events {
      out.write_string(ansi_enable_focus_events())
    }
    out.write_string(paint_styled_frame_inline(frame))
    out.to_string()
  }
}

///|
#coverage.skip
fn start_program_control_output(options : ProgramOptions) -> String {
  let out = StringBuilder::new()
  if options.alternate_screen {
    out.write_string(ansi_enter_alternate_screen())
  }
  out.write_string(ansi_disable_autowrap())
  if options.hide_cursor {
    out.write_string(ansi_hide_cursor())
  }
  if options.mouse_mode != MouseOff {
    out.write_string(ansi_enable_mouse(options.mouse_mode))
  }
  if options.bracketed_paste {
    out.write_string(ansi_enable_bracketed_paste())
  }
  if options.focus_events {
    out.write_string(ansi_enable_focus_events())
  }
  out.to_string()
}

///|
#coverage.skip
fn frame_update_output(
  options : ProgramOptions,
  previous : StyledFrame?,
  next : StyledFrame,
) -> String {
  if options.alternate_screen {
    match previous {
      None => paint_styled_frame_ansi(next)
      Some(prev) => diff_styled_frame(prev, next)
    }
  } else {
    match previous {
      None => repaint_styled_frame_inline(next.lines.length(), next)
      Some(prev) => diff_styled_frame_inline(prev, next)
    }
  }
}

///|
#coverage.skip
fn stop_program_output(options : ProgramOptions) -> String {
  let out = StringBuilder::new()
  out.write_string(ansi_stop_program(options))
  if !options.alternate_screen {
    out.write_string("\r\n")
  }
  out.to_string()
}

///|
#coverage.skip
fn repaint_resized_frame(frame : StyledFrame) -> String {
  let out = StringBuilder::new()
  out.write_string(ansi_clear_screen())
  out.write_string(ansi_move_cursor(1, 1))
  out.write_string(paint_styled_frame_ansi(frame))
  out.to_string()
}

///|
#coverage.skip
fn safe_terminal_command_output(
  command : TerminalCommand,
  options : ProgramOptions,
  state : RuntimeState,
  fd~ : Int,
) -> String {
  if fd == 2 && !is_tty(2) {
    return raw_terminal_command_output(command)
  }
  match command {
    Print(value) => safe_print_output(value, newline=false, options, state)
    PrintLine(value) => safe_print_output(value, newline=true, options, state)
    PrintErr(value) => safe_print_output(value, newline=false, options, state)
    PrintErrLine(value) =>
      safe_print_output(value, newline=true, options, state)
    _ => ansi_terminal_command(command)
  }
}

///|
#coverage.skip
fn raw_terminal_command_output(command : TerminalCommand) -> String {
  match command {
    Print(value) | PrintErr(value) => value
    PrintLine(value) | PrintErrLine(value) => "\{value}\n"
    _ => ansi_terminal_command(command)
  }
}

///|
#coverage.skip
fn terminal_command_fd(command : TerminalCommand) -> Int {
  match command {
    PrintErr(_) | PrintErrLine(_) => 2
    _ => 1
  }
}

///|
#coverage.skip
fn safe_print_output(
  value : String,
  newline~ : Bool,
  options : ProgramOptions,
  state : RuntimeState,
) -> String {
  if !options.renderer_enabled {
    return if newline { "\{value}\r\n" } else { value }
  }
  match state.previous {
    None => if newline { "\{value}\r\n" } else { value }
    Some(frame) => {
      let out = StringBuilder::new()
      if options.alternate_screen {
        out.write_string(ansi_leave_alternate_screen())
        write_safe_print_text(out, value, newline)
        out.write_string(ansi_enter_alternate_screen())
        out.write_string(
          start_program_control_output({ ..options, alternate_screen: false, }),
        )
        out.write_string(paint_styled_frame_ansi(frame))
        return out.to_string()
      } else if frame.lines.length() > 0 {
        out.write_string(ansi_move_cursor_up(frame.lines.length() - 1))
      }
      out.write_string("\r")
      write_safe_print_text(out, value, newline)
      if options.alternate_screen {
        out.write_string(paint_styled_frame_ansi(frame))
      } else {
        out.write_string(paint_styled_frame_inline(frame))
      }
      out.to_string()
    }
  }
}

///|
#coverage.skip
fn write_safe_print_text(
  out : StringBuilder,
  value : String,
  newline : Bool,
) -> Unit {
  let lines = split_safe_print(value)
  if lines.is_empty() {
    if newline {
      out.write_string(ansi_clear_line())
      out.write_string("\r\n")
    }
    return
  }
  for index, line in lines {
    out.write_string(line)
    out.write_string(ansi_clear_line())
    if index + 1 < lines.length() || newline {
      out.write_string("\r\n")
    }
  }
}

///|
#coverage.skip
fn split_safe_print(value : String) -> Array[String] {
  let lines : Array[String] = []
  let mut start = 0
  let mut index = 0
  while index < value.length() {
    if value.code_unit_at(index) == Int::to_uint16('\n'.to_int()) {
      lines.push(trim_safe_print_cr(value[start:index].to_owned()))
      start = index + 1
    }
    index += 1
  }
  if start < value.length() {
    lines.push(trim_safe_print_cr(value[start:].to_owned()))
  }
  lines
}

///|
#coverage.skip
fn trim_safe_print_cr(value : String) -> String {
  if value.length() > 0 &&
    value.code_unit_at(value.length() - 1) == Int::to_uint16('\r'.to_int()) {
    value[:value.length() - 1].to_owned()
  } else {
    value
  }
}

///|
#coverage.skip
async fn[Model, Msg] handle_runtime_item(
  program : Program[Model, Msg],
  emit : Emit[Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  item : RuntimeItem[Msg],
  options : ProgramOptions,
  state : RuntimeState,
) -> ProgramExit? {
  match item {
    RuntimeMsg(msg) => {
      handle_runtime_msg(program, emit, queue, group, msg, options, state)
      None
    }
    RuntimeTerminal(command) =>
      handle_terminal_command(command, program, options, state)
    RuntimeRender => None
    RuntimeResize(size) => {
      state.current_size = size
      program.mark_dirty()
      for msg in program.subscriptions().map_event(Resize(size)) {
        queue.put(RuntimeMsg(msg))
      }
      None
    }
    RuntimeCancel => Some(ProgramCancelled)
    RuntimeTimeout => Some(ProgramTimedOut)
  }
}

///|
#coverage.skip
async fn[Model, Msg] handle_runtime_msg(
  program : Program[Model, Msg],
  emit : Emit[Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  msg : Msg,
  options : ProgramOptions,
  state : RuntimeState,
) -> Unit {
  let cmd = program.step(emit, msg)
  if cmd_needs_suspension(cmd) {
    run_runtime_cmd(program, cmd, queue, group, options, state)
  } else {
    group.spawn_bg(no_wait=true, allow_failure=true, () => {
      run_runtime_cmd(program, cmd, queue, group, options, state)
    })
  }
}

///|
#coverage.skip
fn cmd_needs_suspension(cmd : Cmd) -> Bool {
  match cmd {
    CmdSuspend(_) | CmdExecProcess(_, _, _) => true
    CmdBatch(cmds) | CmdSequence(cmds) => {
      for child in cmds {
        if cmd_needs_suspension(child) {
          return true
        }
      }
      false
    }
    _ => false
  }
}

///|
#coverage.skip
async fn[Model, Msg] run_runtime_cmd(
  program : Program[Model, Msg],
  cmd : Cmd,
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  options : ProgramOptions,
  state : RuntimeState,
) -> Unit {
  match cmd {
    NoCmd => ()
    CmdMessage(send) => send()
    CmdBatch(cmds) =>
      @async.with_task_group(batch => {
        for child in cmds {
          batch.spawn_bg(() => {
            run_runtime_cmd(program, child, queue, group, options, state)
          })
        }
      })
    CmdSequence(cmds) =>
      for child in cmds {
        run_runtime_cmd(program, child, queue, group, options, state)
      }
    CmdDelay(milliseconds, cmd) => {
      @async.sleep(Int::max(1, milliseconds))
      run_runtime_cmd(program, cmd, queue, group, options, state)
    }
    CmdEvery(milliseconds, cmd) => {
      let interval = Int::max(1, milliseconds)
      while true {
        @async.sleep(interval)
        run_runtime_cmd(program, cmd, queue, group, options, state)
      }
    }
    CmdTask(task) => {
      let next = task()
      run_runtime_cmd(program, next, queue, group, options, state)
    }
    CmdSuspend(task) => {
      let next = run_suspended_task(program, options, state, task)
      run_runtime_cmd(program, next, queue, group, options, state)
    }
    CmdExecProcess(command, args, done) => {
      let status = run_suspended_process(program, options, state, command, args)
      run_runtime_cmd(program, done(status), queue, group, options, state)
    }
    CmdTerminal(command) => queue.put(RuntimeTerminal(command))
  }
}

///|
#coverage.skip
async fn[Model, Msg] run_suspended_task(
  program : Program[Model, Msg],
  options : ProgramOptions,
  state : RuntimeState,
  task : async () -> Cmd,
) -> Cmd raise TerminalError {
  suspend_terminal(options, state)
  let msg = try {
    // Re-enter the TUI on error *and* cancellation so the caller's cleanup
    // (stop-program output + restore) sees a consistent terminal state.
    // `resume_terminal` is synchronous, so no cancellation protection needed.
    errdefer (resume_terminal(program, options, state) catch { _ => () })
    task()
  } catch {
    err => raise NativeError(err.to_string())
  }
  resume_terminal(program, options, state)
  msg
}

///|
#coverage.skip
fn[Model, Msg] run_suspended_process(
  program : Program[Model, Msg],
  options : ProgramOptions,
  state : RuntimeState,
  command : String,
  args : Array[String],
) -> Int raise TerminalError {
  suspend_terminal(options, state)
  let status = run_process(command, args) catch {
    err => {
      resume_terminal(program, options, state) catch {
        _ => ()
      }
      raise err
    }
  }
  resume_terminal(program, options, state)
  status
}

///|
#coverage.skip
fn suspend_terminal(
  options : ProgramOptions,
  state : RuntimeState,
) -> Unit raise TerminalError {
  let output = StringBuilder::new()
  match state.previous {
    Some(frame) if options.renderer_enabled =>
      if !options.alternate_screen && frame.lines.length() > 0 {
        output.write_string(
          "\{ansi_move_cursor_up(frame.lines.length() - 1)}\r",
        )
      }
    _ => ()
  }
  output.write_string(stop_program_output(options))
  write_terminal(output.to_string()) catch {
    err => {
      restore_terminal()
      raise err
    }
  }
  restore_terminal()
}

///|
#coverage.skip
fn[Model, Msg] resume_terminal(
  program : Program[Model, Msg],
  options : ProgramOptions,
  state : RuntimeState,
) -> Unit raise TerminalError {
  enter_raw_mode()
  if options.renderer_enabled {
    let size = terminal_size()
    let view = program.view()
    let frame = StyledFrame::from_node(view, size)
    write_terminal(start_program_output(options, view, size, frame))
    state.current_size = size
    state.previous = Some(frame)
  } else {
    write_terminal(start_program_control_output(options))
    state.current_size = terminal_size() catch { _ => state.current_size }
    state.previous = None
  }
}

///|
#coverage.skip
#cfg(not(platform="windows"))
fn run_process(
  command : String,
  args : Array[String],
) -> Int raise TerminalError {
  let bytes = @encoding/utf8.encode(command)
  let packed_args = pack_exec_args(args)
  let status = tui_exec_process(
    bytes,
    0,
    bytes.length(),
    packed_args,
    0,
    packed_args.length(),
    args.length(),
  )
  if status < 0 {
    raise NativeError("failed to execute process: \{command}")
  }
  status
}

///|
#coverage.skip
#cfg(platform="windows")
fn run_process(
  command : String,
  args : Array[String],
) -> Int raise TerminalError {
  ignore(command)
  ignore(args)
  raise UnsupportedPlatform
}

///|
#coverage.skip
fn pack_exec_args(args : Array[String]) -> Bytes {
  let out = StringBuilder::new()
  for index, arg in args {
    if index > 0 {
      out.write_char('\u{00}')
    }
    out.write_string(arg)
  }
  @encoding/utf8.encode(out.to_string())
}

///|
#coverage.skip
fn[Model, Msg] handle_terminal_command(
  command : TerminalCommand,
  program : Program[Model, Msg],
  options : ProgramOptions,
  state : RuntimeState,
) -> ProgramExit? raise TerminalError {
  match command {
    QuitProgram => Some(ProgramQuit)
    Repaint => {
      program.mark_dirty()
      None
    }
    _ => {
      let fd = terminal_command_fd(command)
      write_terminal_fd(
        fd,
        safe_terminal_command_output(command, options, state, fd~),
      )
      if command is ClearScreen {
        program.mark_dirty()
      }
      None
    }
  }
}

///|
#coverage.skip
async fn[Msg] watch_terminal_resize(
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  initial_size : Size,
  poll_millis : Int,
) -> Unit {
  let mut last = initial_size
  while true {
    @async.sleep(poll_millis)
    let next = terminal_size() catch { _ => last }
    if next != last {
      last = next
      queue.put(RuntimeResize(next))
    }
  }
}

///|
#coverage.skip
async fn[Model, Msg] read_terminal_input(
  program : Program[Model, Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
) -> Unit {
  let mut decoder = InputDecoder::new()
  let mut pending_idle = 0
  while true {
    match @stdio.stdin.read_some(max_len=32) {
      None => {
        if decoder.has_pending() && decoder.pending_is_lone_escape() {
          pending_idle += 1
          if pending_idle >= 2 {
            let (events, next) = decoder.flush()
            decoder = next
            pending_idle = 0
            dispatch_input_events(program, queue, group, events)
          }
        }
        @async.sleep(16)
      }
      Some(bytes) => {
        pending_idle = 0
        let (events, next) = decoder.feed(bytes)
        decoder = next
        dispatch_input_events(program, queue, group, events)
      }
    }
  }
}

///|
#coverage.skip
async fn[Model, Msg] dispatch_input_events(
  program : Program[Model, Msg],
  queue : @aqueue.Queue[RuntimeItem[Msg]],
  group : @async.TaskGroup[Unit],
  events : Array[Event],
) -> Unit {
  ignore(group)
  for event in events {
    for msg in program.subscriptions().map_event(event) {
      queue.put(RuntimeMsg(msg))
    }
  }
}

///|
#coverage.skip
fn[Msg, X] Sub::start(
  self : Sub[Msg],
  emit : Emit[Msg],
  group : @async.TaskGroup[X],
) -> Unit {
  match self {
    NoSub => ()
    SubBatch(subs) =>
      for sub in subs {
        sub.start(emit, group)
      }
    TickSub(milliseconds, msg) =>
      group.spawn_bg(no_wait=true, allow_failure=true, () => {
        while true {
          @async.sleep(milliseconds)
          emit.send(msg)
        }
      })
    EventSub(_)
    | Keyboard(_)
    | KeySub(_)
    | MouseSub(_)
    | MouseEventSub(_)
    | ResizeSub(_)
    | PasteSub(_)
    | FocusSub(_)
    | FocusChangesSub(_) => ()
  }
}

///|
#coverage.skip
#cfg(not(platform="windows"))
fn write_terminal(output : String) -> Unit raise TerminalError {
  write_terminal_fd(1, output)
}

///|
#coverage.skip
#cfg(not(platform="windows"))
fn write_terminal_fd(fd : Int, output : String) -> Unit raise TerminalError {
  let bytes = @encoding/utf8.encode(output)
  let mut offset = 0
  while offset < bytes.length() {
    let written = tui_write(fd, bytes, offset, bytes.length() - offset)
    if written <= 0 {
      raise NativeError("failed to write terminal output")
    } else {
      offset += written
    }
  }
}

///|
#coverage.skip
#cfg(platform="windows")
fn write_terminal(output : String) -> Unit raise TerminalError {
  ignore(output)
  raise UnsupportedPlatform
}

///|
#coverage.skip
#cfg(platform="windows")
fn write_terminal_fd(fd : Int, output : String) -> Unit raise TerminalError {
  ignore(fd)
  ignore(output)
  raise UnsupportedPlatform
}