// Port of rich/console.py.

///|
/// A file-like object the console writes to (upstream `IO[str]`).
pub(open) trait ConsoleFile {
  fn write(Self, String) -> Unit raise
  fn flush(Self) -> Unit raise = _
  /// Whether the file is a terminal.
  fn isatty(Self) -> Bool = _
  /// Encoding of the file, e.g. `"utf-8"`.
  fn encoding(Self) -> String = _
}

///|
impl ConsoleFile with fn flush(_self) {
  ()
}

///|
impl ConsoleFile with fn isatty(_self) {
  false
}

///|
impl ConsoleFile with fn encoding(_self) {
  "utf-8"
}

///|
/// Standard output or standard error.
pub struct StdStream {
  fd : Int
}

///|
/// The standard output stream.
pub fn StdStream::stdout() -> StdStream {
  { fd: 1, }
}

///|
/// The standard error stream.
pub fn StdStream::stderr() -> StdStream {
  { fd: 2, }
}

///|
pub impl ConsoleFile for StdStream with fn write(self, text) {
  if !@term.write_fd(self.fd, text) {
    raise WriteError("failed to write to file descriptor \{self.fd}")
  }
}

///|
pub impl ConsoleFile for StdStream with fn flush(self) {
  @term.flush_fd(self.fd)
}

///|
pub impl ConsoleFile for StdStream with fn isatty(self) {
  @term.isatty(self.fd)
}

///|
/// An in-memory text file (upstream `io.StringIO`).
pub struct StringIO {
  priv buffer : StringBuilder
  priv tty : Bool
}

///|
/// Create an in-memory file. With `isatty`, it claims to be a terminal.
pub fn StringIO::new(isatty? : Bool = false) -> StringIO {
  { buffer: StringBuilder(), tty: isatty, }
}

///|
/// Everything written so far.
pub fn StringIO::getvalue(self : StringIO) -> String {
  self.buffer.to_string()
}

///|
/// Discard the contents.
pub fn StringIO::reset(self : StringIO) -> Unit {
  self.buffer.reset()
}

///|
pub impl ConsoleFile for StringIO with fn write(self, text) {
  self.buffer.write_string(text)
}

///|
pub impl ConsoleFile for StringIO with fn isatty(self) {
  self.tty
}

///|
/// A file that discards everything (upstream `NullFile`).
pub struct NullFile {}

///|
pub fn NullFile::new() -> NullFile {
  NullFile::{ }
}

///|
pub impl ConsoleFile for NullFile with fn write(_self, _text) {
  ()
}

///|
/// Hook that may process renderables before they are rendered.
pub(open) trait RenderHook {
  fn process_renderables(Self, Array[&Renderable]) -> Array[&Renderable] raise
}

///|
/// A renderable to generate new line(s).
pub struct NewLine {
  count : Int
}

///|
pub fn NewLine::new(count? : Int = 1) -> NewLine {
  { count, }
}

///|
pub impl Renderable for NewLine with fn rich_console(self, _console, _options) {
  [Seg(Segment::new("\n".repeat(self.count)))]
}

///|
/// Render a list of lines at a given offset.
pub struct ScreenUpdate {
  lines : Array[Array[Segment]]
  x : Int
  y : Int
}

///|
pub fn ScreenUpdate::new(
  lines : Array[Array[Segment]],
  x : Int,
  y : Int,
) -> ScreenUpdate {
  { lines, x, y, }
}

///|
pub impl Renderable for ScreenUpdate with fn rich_console(
  self,
  _console,
  _options,
) {
  let out = []
  for i, line in self.lines {
    out.push(Seg(Control::move_to(self.x, self.y + i).segment))
    for seg in line {
      out.push(Seg(seg))
    }
  }
  out
}

///|
/// Takes a group of renderables and returns a renderable object that
/// renders the group.
pub struct Group {
  renderables : Array[&Renderable]
  fit : Bool
}

///|
pub fn Group::new(
  renderables : Array[&Renderable],
  fit? : Bool = true,
) -> Group {
  { renderables, fit, }
}

///|
pub impl Renderable for Group with fn rich_measure(self, console, options) {
  if self.fit {
    Some(measure_renderables(console, options, self.renderables))
  } else {
    Some({ minimum: options.max_width, maximum: options.max_width, })
  }
}

///|
pub impl Renderable for Group with fn rich_console(self, _console, _options) {
  self.renderables.map(r => Child(r))
}

///|
/// The console color system names.
fn color_system_from_name(name : String) -> @color.ColorSystem? raise RichError {
  match name {
    "standard" => Some(Standard)
    "256" => Some(EightBit)
    "truecolor" => Some(Truecolor)
    "windows" => Some(Windows)
    _ => raise ValueError("invalid color system \{py_repr(name)}")
  }
}

///|
fn color_system_name(system : @color.ColorSystem) -> String {
  match system {
    Standard => "standard"
    EightBit => "256"
    Truecolor => "truecolor"
    Windows => "windows"
  }
}

///|
/// A high level console interface.
pub(all) struct Console {
  priv mut file_ : &ConsoleFile?
  /// Use stderr rather than stdout if no file is set.
  mut stderr : Bool
  /// Suppress all output.
  mut quiet : Bool
  /// Number of spaces used to replace a tab character.
  mut tab_size : Int
  /// Record terminal output (required for the export methods).
  mut record : Bool
  /// Default soft wrap mode for `print`.
  mut soft_wrap : Bool
  /// Legacy Windows mode.
  mut legacy_windows : Bool
  /// Restrict box options that don't render on legacy Windows.
  mut safe_box : Bool
  /// Disable colors.
  mut no_color : Bool
  /// Whether the console is interactive.
  mut is_interactive : Bool
  /// Whether running in Jupyter (always false in MoonBit).
  is_jupyter : Bool
  /// Style applied to all output.
  mut style : StyleType?
  /// Default highlighter.
  mut highlighter : &Highlighter
  /// Get the current time (seconds since the epoch) for `log`.
  mut get_datetime : () -> Double
  /// Monotonic clock in seconds.
  mut get_time : () -> Double
  priv markup_ : Bool
  priv emoji_ : Bool
  priv emoji_variant : EmojiVariant?
  priv highlight_ : Bool
  priv mut width_ : Int?
  priv mut height_ : Int?
  priv color_system_ : @color.ColorSystem?
  priv force_terminal : Bool?
  priv environ : Map[String, String]?
  priv log_render : LogRender
  priv buffer : Array[Segment]
  priv mut buffer_index : Int
  priv theme_stack : ThemeStack
  priv record_buffer : Array[Segment]
  priv render_hooks : Array[&RenderHook]
  priv live_stack : Array[Int]
  priv mut is_alt_screen_ : Bool
}

///|
/// Create a console.
///
/// `color_system` is `Some("auto")` (detect), `Some("standard")`,
/// `Some("256")`, `Some("truecolor")`, `Some("windows")`, or `None` for no
/// color. `environ` replaces the process environment (for testing).
/// `log_time_formatter` (upstream: a callable `log_time_format`) formats
/// the time of `log` instead of `log_time_format`.
pub fn Console::new(
  color_system? : String? = Some("auto"),
  force_terminal? : Bool,
  force_jupyter? : Bool,
  force_interactive? : Bool,
  soft_wrap? : Bool = false,
  theme? : Theme,
  stderr? : Bool = false,
  file? : &ConsoleFile,
  quiet? : Bool = false,
  width? : Int,
  height? : Int,
  style? : &IntoStyle,
  no_color? : Bool,
  tab_size? : Int = 8,
  record? : Bool = false,
  markup? : Bool = true,
  emoji? : Bool = true,
  emoji_variant? : EmojiVariant,
  highlight? : Bool = true,
  log_time? : Bool = true,
  log_path? : Bool = true,
  log_time_format? : String = "[%X]",
  log_time_formatter? : (Double) -> Text,
  highlighter? : &Highlighter,
  legacy_windows? : Bool,
  safe_box? : Bool = true,
  get_datetime? : () -> Double,
  get_time? : () -> Double,
  environ? : Map[String, String],
) -> Console raise RichError {
  let is_jupyter = force_jupyter.unwrap_or(false)
  fn getenv(name : String) -> String? {
    match environ {
      Some(env) => env.get(name)
      None => @env.get_env_var(name)
    }
  }

  let legacy_windows = legacy_windows.unwrap_or(false)
  let mut width = width
  let mut height = height
  if width is None {
    if getenv("COLUMNS") is Some(columns) && is_digits(columns) {
      width = Some(
        parse_int_or(columns, 80) - (if legacy_windows { 1 } else { 0 }),
      )
    }
  }
  if height is None {
    if getenv("LINES") is Some(lines) && is_digits(lines) {
      height = Some(parse_int_or(lines, 25))
    }
  }
  let console : Console = {
    file_: file,
    stderr,
    quiet,
    tab_size,
    record,
    soft_wrap,
    legacy_windows,
    safe_box,
    no_color: false,
    is_interactive: false,
    is_jupyter,
    style: style_opt(style),
    highlighter: match highlighter {
      Some(h) => h
      None => ReprHighlighter::new()
    },
    get_datetime: match get_datetime {
      Some(f) => f
      None => @term.time
    },
    get_time: match get_time {
      Some(f) => f
      None => @term.monotonic
    },
    markup_: markup,
    emoji_: emoji,
    emoji_variant,
    highlight_: highlight,
    width_: width,
    height_: height,
    color_system_: None,
    force_terminal,
    environ,
    log_render: LogRender::new(
      show_time=log_time,
      show_path=log_path,
      time_format=log_time_format,
      time_formatter?=log_time_formatter,
    ),
    buffer: [],
    buffer_index: 0,
    theme_stack: ThemeStack::new(theme.unwrap_or(default_theme)),
    record_buffer: [],
    render_hooks: [],
    live_stack: [],
    is_alt_screen_: false,
  }
  let color_system = match color_system {
    None => None
    Some("auto") => console.detect_color_system()
    Some(name) => color_system_from_name(name)
  }
  let console = { ..console, color_system_: color_system, }
  console.no_color = match no_color {
    Some(b) => b
    None => console.getenv("NO_COLOR").unwrap_or("") != ""
  }
  let mut force_interactive = force_interactive
  if force_interactive is None {
    match console.getenv("TTY_INTERACTIVE") {
      Some("0") => force_interactive = Some(false)
      Some("1") => force_interactive = Some(true)
      _ => ()
    }
  }
  console.is_interactive = match force_interactive {
    Some(b) => b
    None => console.is_terminal() && !console.is_dumb_terminal()
  }
  console
}

///|
fn is_digits(s : String) -> Bool {
  s != "" && s.iter().all(c => c >= '0' && c <= '9')
}

///|
fn parse_int_or(s : String, default : Int) -> Int {
  @string.parse_int(s) catch {
    _ => default
  }
}

///|
fn Console::getenv(self : Console, name : String) -> String? {
  match self.environ {
    Some(env) => env.get(name)
    None => @env.get_env_var(name)
  }
}

///|
/// Upstream `repr(console)`.
pub fn Console::repr(self : Console) -> String {
  let cs = match self.color_system_ {
    Some(c) => c.to_string()
    None => "None"
  }
  ""
}

///|
/// The file the console writes to.
pub fn Console::file(self : Console) -> &ConsoleFile {
  match self.file_ {
    Some(f) => f
    None => if self.stderr { StdStream::stderr() } else { StdStream::stdout() }
  }
}

///|
/// Set a new file to write to.
pub fn Console::set_file(self : Console, file : &ConsoleFile) -> Unit {
  self.file_ = Some(file)
}

///|
fn Console::detect_color_system(self : Console) -> @color.ColorSystem? {
  if self.is_jupyter {
    return Some(Truecolor)
  }
  if !self.is_terminal() || self.is_dumb_terminal() {
    return None
  }
  let color_term = py_strip(self.getenv("COLORTERM").unwrap_or("")).to_lower()
  if color_term == "truecolor" || color_term == "24bit" {
    return Some(Truecolor)
  }
  let term = py_strip(self.getenv("TERM").unwrap_or("")).to_lower()
  let colors = match term.rev_find("-") {
    Some(i) => py_slice(term, i + 1, term.length())
    None => term
  }
  match colors {
    "kitty" | "256color" => Some(EightBit)
    "16color" => Some(Standard)
    _ => Some(Standard)
  }
}

///|
/// Enter a buffer context: all output is buffered until the matching
/// `exit_buffer`.
pub fn Console::enter_buffer(self : Console) -> Unit {
  self.buffer_index += 1
}

///|
/// Leave a buffer context, and render content if required.
pub fn Console::exit_buffer(self : Console) -> Unit raise {
  self.buffer_index -= 1
  self.check_buffer()
}

///|
/// Run `f` with output buffered (upstream `with console:`).
pub fn Console::buffered(self : Console, f : () -> Unit raise) -> Unit raise {
  self.enter_buffer()
  {
    errdefer self.exit_buffer()
    f()
  }
  self.exit_buffer()
}

///|
/// Register a live display. Returns `true` if it is the top-most one.
pub fn Console::set_live(self : Console, live_id : Int) -> Bool {
  self.live_stack.push(live_id)
  self.live_stack.length() == 1
}

///|
/// Remove the top-most live display.
pub fn Console::clear_live(self : Console) -> Unit {
  ignore(self.live_stack.pop())
}

///|
/// The ids of the active live displays (innermost last).
pub fn Console::live_stack(self : Console) -> Array[Int] {
  self.live_stack.copy()
}

///|
/// Add a new render hook to the stack.
pub fn Console::push_render_hook(self : Console, hook : &RenderHook) -> Unit {
  self.render_hooks.push(hook)
}

///|
/// Pop the last render hook from the stack.
pub fn Console::pop_render_hook(self : Console) -> Unit {
  ignore(self.render_hooks.pop())
}

///|
/// Begin capturing console output. Call `end_capture` to exit capture mode
/// and return output.
pub fn Console::begin_capture(self : Console) -> Unit {
  self.enter_buffer()
}

///|
/// End capture mode and return captured string.
pub fn Console::end_capture(self : Console) -> String raise {
  let render_result = self.render_buffer(self.buffer)
  self.buffer.clear()
  self.exit_buffer()
  render_result
}

///|
/// Capture the output of `f` instead of writing it to the console.
pub fn Console::capture(self : Console, f : () -> Unit raise) -> String raise {
  self.begin_capture()
  {
    errdefer {
      self.buffer.clear()
      self.buffer_index -= 1
    }
    f()
  }
  self.end_capture()
}

///|
/// Push a new theme on to the top of the stack.
pub fn Console::push_theme(
  self : Console,
  theme : Theme,
  inherit_styles? : Bool = true,
) -> Unit {
  self.theme_stack.push_theme(theme, inherit_styles~)
}

///|
/// Remove theme from top of stack, restoring previous theme.
pub fn Console::pop_theme(self : Console) -> Unit raise RichError {
  self.theme_stack.pop_theme()
}

///|
/// Use a different theme for the duration of `f`.
pub fn Console::use_theme(
  self : Console,
  theme : Theme,
  f : () -> Unit raise,
  inherit_styles? : Bool = true,
) -> Unit raise {
  self.push_theme(theme, inherit_styles~)
  {
    errdefer self.pop_theme()
    f()
  }
  self.pop_theme()
}

///|
/// The color system name: `"standard"`, `"256"`, `"truecolor"`,
/// `"windows"`, or `None`.
pub fn Console::color_system(self : Console) -> String? {
  self.color_system_.map(color_system_name)
}

///|
/// The detected or configured color system.
pub fn Console::color_system_enum(self : Console) -> @color.ColorSystem? {
  self.color_system_
}

///|
/// The encoding of the console file, e.g. `"utf-8"`.
pub fn Console::encoding(self : Console) -> String {
  let e = self.file().encoding()
  if e == "" {
    "utf-8"
  } else {
    e.to_lower()
  }
}

///|
/// Check if the console is writing to a terminal.
pub fn Console::is_terminal(self : Console) -> Bool {
  if self.force_terminal is Some(f) {
    return f
  }
  if self.is_jupyter {
    return false
  }
  match self.getenv("TTY_COMPATIBLE") {
    Some("0") => return false
    Some("1") => return true
    _ => ()
  }
  if self.getenv("FORCE_COLOR") is Some(force_color) {
    return force_color != ""
  }
  self.file().isatty()
}

///|
/// Detect dumb terminal.
pub fn Console::is_dumb_terminal(self : Console) -> Bool {
  let term = self.getenv("TERM").unwrap_or("").to_lower()
  let is_dumb = term == "dumb" || term == "unknown"
  self.is_terminal() && is_dumb
}

///|
/// Default console options.
pub fn Console::options(self : Console) -> ConsoleOptions {
  let size = self.size()
  {
    max_height: size.height,
    size,
    legacy_windows: self.legacy_windows,
    min_width: 1,
    max_width: size.width,
    encoding: self.encoding(),
    is_terminal: self.is_terminal(),
    justify: None,
    overflow: None,
    no_wrap: Some(false),
    highlight: None,
    markup: None,
    height: None,
  }
}

///|
/// The size of the console.
pub fn Console::size(self : Console) -> ConsoleDimensions {
  let lw = if self.legacy_windows { 1 } else { 0 }
  if self.width_ is Some(w) && self.height_ is Some(h) {
    return { width: w - lw, height: h, }
  }
  if self.is_dumb_terminal() {
    return { width: 80, height: 25, }
  }
  let mut width : Int? = None
  let mut height : Int? = None
  for fd in [0, 1, 2] {
    if @term.terminal_size(fd) is Some((w, h)) {
      width = Some(w)
      height = Some(h)
      break
    }
  }
  if self.getenv("COLUMNS") is Some(columns) && is_digits(columns) {
    width = Some(parse_int_or(columns, 80))
  }
  if self.getenv("LINES") is Some(lines) && is_digits(lines) {
    height = Some(parse_int_or(lines, 25))
  }
  let width = match width {
    Some(w) if w != 0 => w
    _ => 80
  }
  let height = match height {
    Some(h) if h != 0 => h
    _ => 25
  }
  {
    width: match self.width_ {
      None => width - lw
      Some(w) => w
    },
    height: match self.height_ {
      None => height
      Some(h) => h
    },
  }
}

///|
/// Set a new size for the terminal.
pub fn Console::set_size(self : Console, width : Int, height : Int) -> Unit {
  self.width_ = Some(width)
  self.height_ = Some(height)
}

///|
/// The width of the console.
pub fn Console::width(self : Console) -> Int {
  self.size().width
}

///|
/// Set the width of the console.
pub fn Console::set_width(self : Console, width : Int) -> Unit {
  self.width_ = Some(width)
}

///|
/// The height of the console.
pub fn Console::height(self : Console) -> Int {
  self.size().height
}

///|
/// Set the height of the console.
pub fn Console::set_height(self : Console, height : Int) -> Unit {
  self.height_ = Some(height)
}

///|
/// Play a 'bell' sound (if supported by the terminal).
pub fn Console::bell(self : Console) -> Unit raise {
  self.control([Control::bell()])
}

///|
/// Write new line(s).
pub fn Console::line(self : Console, count? : Int = 1) -> Unit raise {
  guard count >= 0 else { raise ValueError("count must be >= 0") }
  self.print(NewLine::new(count~))
}

///|
/// Clear the screen.
pub fn Console::clear(self : Console, home? : Bool = true) -> Unit raise {
  if home {
    self.control([Control::clear(), Control::home()])
  } else {
    self.control([Control::clear()])
  }
}

///|
/// Show or hide the cursor.
pub fn Console::show_cursor(self : Console, show? : Bool = true) -> Bool raise {
  if self.is_terminal() {
    self.control([Control::show_cursor(show)])
    true
  } else {
    false
  }
}

///|
/// Enable or disable the alternative screen mode. Returns `true` if the
/// control codes were written.
pub fn Console::set_alt_screen(
  self : Console,
  enable? : Bool = true,
) -> Bool raise {
  if self.is_terminal() && !self.legacy_windows {
    self.control([Control::alt_screen(enable)])
    self.is_alt_screen_ = enable
    true
  } else {
    false
  }
}

///|
/// Check if the alt screen was enabled.
pub fn Console::is_alt_screen(self : Console) -> Bool {
  self.is_alt_screen_
}

///|
/// Set the title of the console terminal window.
pub fn Console::set_window_title(self : Console, title : String) -> Bool raise {
  if self.is_terminal() {
    self.control([Control::title(title)])
    true
  } else {
    false
  }
}

///|
/// A context that enables the alternate screen (upstream `ScreenContext`,
/// returned by `console.screen()`).
pub struct ScreenContext {
  console : Console
  hide_cursor : Bool
  screen : Screen
  priv mut changed : Bool
}

///|
/// Create a screen context; call `enter` / `exit`, or use `Console::screen`.
pub fn Console::screen_context(
  self : Console,
  hide_cursor? : Bool = true,
  style? : &IntoStyle,
) -> ScreenContext {
  {
    console: self,
    hide_cursor,
    screen: Screen::new([], style=style_or_empty(style)),
    changed: false,
  }
}

///|
/// Update the screen with new renderables (and / or a new style).
pub fn ScreenContext::update(
  self : ScreenContext,
  renderables? : Array[&Renderable] = [],
  style? : &IntoStyle,
) -> Unit raise {
  if renderables.length() > 0 {
    self.screen.renderable = if renderables.length() > 1 {
      (Group::new(renderables) : &Renderable)
    } else {
      renderables[0]
    }
  }
  if style is Some(style) {
    self.screen.style = Some(style.into_style_type())
  }
  self.console.print(self.screen, end="")
}

///|
/// Enable the alternate screen (and hide the cursor).
pub fn ScreenContext::enter(self : ScreenContext) -> Unit raise {
  self.changed = self.console.set_alt_screen(enable=true)
  if self.changed && self.hide_cursor {
    self.console.show_cursor(show=false) |> ignore
  }
}

///|
/// Restore the normal screen (and the cursor).
pub fn ScreenContext::exit(self : ScreenContext) -> Unit raise {
  if self.changed {
    self.console.set_alt_screen(enable=false) |> ignore
    if self.hide_cursor {
      self.console.show_cursor(show=true) |> ignore
    }
  }
}

///|
/// Run `f` in the alternate screen (upstream `with console.screen() as
/// screen:`).
pub fn Console::screen(
  self : Console,
  f : (ScreenContext) -> Unit raise,
  hide_cursor? : Bool = true,
  style? : &IntoStyle,
) -> Unit raise {
  let context = self.screen_context(hide_cursor~, style?)
  context.enter()
  {
    errdefer context.exit()
    f(context)
  }
  context.exit()
}

///|
/// Measure a renderable.
pub fn Console::measure(
  self : Console,
  renderable : &Renderable,
  options? : ConsoleOptions,
) -> Measurement raise {
  Measurement::get(
    self,
    options.unwrap_or_else(() => self.options()),
    renderable,
  )
}

///|
/// Render an object in to segments.
pub fn Console::render(
  self : Console,
  renderable : &Renderable,
  options? : ConsoleOptions,
) -> Array[Segment] raise {
  let out = []
  self.render_into(
    renderable,
    options.unwrap_or_else(() => self.options()),
    out,
    0,
  )
  out
}

///|
fn Console::render_into(
  self : Console,
  renderable : &Renderable,
  options : ConsoleOptions,
  out : Array[Segment],
  depth : Int,
) -> Unit raise {
  if options.max_width < 1 {
    return
  }
  if depth > 1000 {
    raise NotRenderableError("maximum render depth exceeded")
  }
  // Strings and Text (whose `text_kind` is not `NotText`, see
  // `Renderable::text_kind`) render straight into `out`.
  match renderable.text_kind() {
    Text(t) => {
      t.render_console_into(self, options, out)
      return
    }
    Str(s) => {
      self
      .render_str(s, highlight=options.highlight, markup=options.markup)
      .render_console_into(self, options, out)
      return
    }
    NotText => ()
  }
  let items = renderable.rich_console(self, options)
  // one copy shared by all the children, made when the first one is seen
  let mut child_options = None
  for item in items {
    match item {
      Seg(s) => out.push(s)
      Child(r) => {
        let opts = match child_options {
          Some(o) => o
          None => {
            let o = options.reset_height()
            child_options = Some(o)
            o
          }
        }
        self.render_into(r, opts, out, depth + 1)
      }
    }
  }
}

///|
/// Render objects in to a list of lines.
pub fn Console::render_lines(
  self : Console,
  renderable : &Renderable,
  options? : ConsoleOptions,
  style? : Style,
  pad? : Bool = true,
  new_lines? : Bool = false,
) -> Array[Array[Segment]] raise {
  let render_options = options.unwrap_or_else(() => self.options())
  let mut rendered = self.render(renderable, options=render_options)
  if style is Some(s) && s.to_bool() {
    rendered = Segment::apply_style(rendered, style=s)
  }
  let mut lines = Segment::split_and_crop_lines(
    rendered,
    render_options.max_width,
    include_new_lines=new_lines,
    pad~,
    style?,
  )
  if render_options.height is Some(h) {
    let h = @cmp.maximum(0, h)
    if lines.length() > h {
      lines = lines[0:h].to_owned()
    }
    let extra_lines = h - lines.length()
    if extra_lines > 0 {
      let pad_line = if new_lines {
        [
          Segment::new(space_string(render_options.max_width), style?),
          Segment::new("\n"),
        ]
      } else {
        [Segment::new(space_string(render_options.max_width), style?)]
      }
      for _ in 0.. Text raise {
  let emoji = emoji.unwrap_or(None)
  let markup = markup.unwrap_or(None)
  let highlight = highlight.unwrap_or(None)
  let emoji_enabled = emoji == Some(true) || (emoji is None && self.emoji_)
  let markup_enabled = markup == Some(true) || (markup is None && self.markup_)
  let highlight_enabled = highlight == Some(true) ||
    (highlight is None && self.highlight_)
  let style = style_or_empty(style)
  let rich_text = if markup_enabled {
    let t = render_markup(
      text,
      style~,
      emoji=emoji_enabled,
      emoji_variant?=self.emoji_variant,
    )
    t.justify = justify
    t.overflow = overflow
    t
  } else {
    Text::new(
      text=if emoji_enabled {
        @emoji.replace(
          text,
          default_variant?=self.emoji_variant.map(v => v.to_string()),
        )
      } else {
        text
      },
      justify?,
      overflow?,
      style~,
    )
  }
  if highlight_enabled {
    let h = match highlighter {
      Some(h) => h
      None => self.highlighter
    }
    let highlight_text = h.apply(rich_text.plain())
    highlight_text.copy_styles(rich_text)
    return highlight_text
  }
  rich_text
}

///|
/// Get a Style instance by its theme name or parse a definition.
pub fn Console::get_style(
  self : Console,
  name : &IntoStyle,
  default? : StyleType,
) -> Style raise RichError {
  match name.into_style_type() {
    Style(s) => s
    Def(name) => {
      let result = match self.theme_stack.get(name) {
        Some(s) => s
        None =>
          Style::parse(name) catch {
            error =>
              match default {
                Some(d) => return self.get_style(d)
                None =>
                  raise MissingStyle(
                    "Failed to get style \{py_repr(name)}; \{error}",
                  )
              }
          }
      }
      if result.link() is Some(link) && link != "" {
        result.copy()
      } else {
        result
      }
    }
  }
}

///|
/// Combine a number of renderables and text into a list of renderables.
fn Console::collect_renderables(
  self : Console,
  objects : ArrayView[&Renderable],
  sep : String,
  end : String,
  justify? : JustifyMethod,
  emoji? : Bool,
  markup? : Bool,
  highlight? : Bool,
) -> Array[&Renderable] raise {
  let renderables : Array[&Renderable] = []
  let text : Array[Text] = []
  fn append(renderable : &Renderable) -> Unit {
    match justify {
      Some(Left) => renderables.push(Align::new(renderable, Left))
      Some(Center) => renderables.push(Align::new(renderable, Center))
      Some(Right) => renderables.push(Align::new(renderable, Right))
      _ => renderables.push(renderable)
    }
  }

  let highlighter : &Highlighter = if highlight == Some(true) ||
    (highlight is None && self.highlight_) {
    self.highlighter
  } else {
    NullHighlighter::new()
  }
  fn check_text() {
    if !text.is_empty() {
      let sep_text = Text::new(text=sep, justify?, end~)
      append(sep_text.join(text))
      text.clear()
    }
  }

  for renderable in objects {
    match renderable.text_kind() {
      Str(s) =>
        text.push(self.render_str(s, emoji~, markup~, highlight~, highlighter~))
      Text(t) => text.push(t)
      NotText => {
        check_text()
        append(renderable)
      }
    }
  }
  check_text()
  if self.style is Some(style) {
    let style = self.get_style(style)
    return renderables.map(r => (Styled::new(r, style) : &Renderable))
  }
  renderables
}

///|
/// Draw a line with an optional centered title.
pub fn Console::rule(
  self : Console,
  title? : &IntoText,
  characters? : String = "─",
  style? : &IntoStyle,
  align? : AlignMethod = Center,
) -> Unit raise {
  let rule = Rule::new(
    title=match title {
      Some(t) => t
      None => ""
    },
    characters~,
    style=match style {
      Some(s) => s
      None => "rule.line"
    },
    align~,
  )
  self.print(rule)
}

///|
/// Insert non-printing control codes.
pub fn Console::control(
  self : Console,
  controls : ArrayView[Control],
) -> Unit raise {
  if !self.is_dumb_terminal() {
    self.enter_buffer()
    for c in controls {
      self.buffer.push(c.segment)
    }
    self.exit_buffer()
  }
}

///|
/// Low-level output: no pretty printing, wrapping or markup, but optional
/// highlighting and a basic style.
pub fn Console::out(
  self : Console,
  text : String,
  end? : String = "\n",
  style? : &IntoStyle,
  highlight? : Bool,
) -> Unit raise {
  self.out_many([text], end~, style?, highlight?)
}

///|
/// Low-level output of several strings joined with `sep` (upstream
/// `out(*objects, sep=...)`).
pub fn Console::out_many(
  self : Console,
  objects : ArrayView[String],
  sep? : String = " ",
  end? : String = "\n",
  style? : &IntoStyle,
  highlight? : Bool,
) -> Unit raise {
  let raw_output = objects.iter().join(sep)
  self.print(
    raw_output,
    style?,
    highlight?,
    emoji=false,
    markup=false,
    no_wrap=true,
    overflow=Ignore,
    crop=false,
    end~,
  )
}

///|
/// Print to the console.
pub fn Console::print(
  self : Console,
  object : &Renderable,
  sep? : String = " ",
  end? : String = "\n",
  style? : &IntoStyle,
  justify? : JustifyMethod,
  overflow? : OverflowMethod,
  no_wrap? : Bool,
  emoji? : Bool,
  markup? : Bool,
  highlight? : Bool,
  width? : Int,
  height? : Int,
  crop? : Bool = true,
  soft_wrap? : Bool,
  new_line_start? : Bool = false,
) -> Unit raise {
  self.print_many(
    [object],
    sep~,
    end~,
    style?,
    justify?,
    overflow?,
    no_wrap?,
    emoji?,
    markup?,
    highlight?,
    width?,
    height?,
    crop~,
    soft_wrap?,
    new_line_start~,
  )
}

///|
/// Print several objects to the console. Adjacent strings and Texts are
/// joined with `sep`; other renderables are printed on their own.
pub fn Console::print_many(
  self : Console,
  objects : ArrayView[&Renderable],
  sep? : String = " ",
  end? : String = "\n",
  style? : &IntoStyle,
  justify? : JustifyMethod,
  overflow? : OverflowMethod,
  no_wrap? : Bool,
  emoji? : Bool,
  markup? : Bool,
  highlight? : Bool,
  width? : Int,
  height? : Int,
  crop? : Bool = true,
  soft_wrap? : Bool,
  new_line_start? : Bool = false,
) -> Unit raise {
  let objects : ArrayView[&Renderable] = if objects.length() == 0 {
    if end == "\n" {
      [NewLine::new()]
    } else {
      [""]
    }
  } else {
    objects
  }
  let soft_wrap = soft_wrap.unwrap_or(self.soft_wrap)
  let mut no_wrap = no_wrap
  let mut overflow = overflow
  let mut crop = crop
  if soft_wrap {
    if no_wrap is None {
      no_wrap = Some(true)
    }
    if overflow is None {
      overflow = Some(Ignore)
    }
    crop = false
  }
  let render_hooks = self.render_hooks.copy()
  self.enter_buffer()
  {
    errdefer self.exit_buffer()
    let mut renderables = self.collect_renderables(
      objects,
      sep,
      end,
      justify?,
      emoji?,
      markup?,
      highlight?,
    )
    for hook in render_hooks {
      renderables = hook.process_renderables(renderables)
    }
    let render_options = self
      .options()
      .update(
        justify~,
        overflow~,
        width?=width.map(w => @cmp.minimum(w, self.width())),
        height~,
        no_wrap~,
        markup~,
        highlight~,
      )
    let new_segments : Array[Segment] = []
    match style {
      None =>
        for renderable in renderables {
          self.render_into(renderable, render_options, new_segments, 0)
        }
      Some(style) => {
        let render_style = self.get_style(style)
        let new_line = Segment::line()
        for renderable in renderables {
          for
            item in Segment::split_lines_terminator(
              self.render(renderable, options=render_options),
            ) {
            let (line, add_new_line) = item
            new_segments.append(Segment::apply_style(line, style=render_style))
            if add_new_line {
              new_segments.push(new_line)
            }
          }
        }
      }
    }
    if new_line_start {
      let joined = new_segments.map(s => s.text).join("")
      if py_splitlines(joined).length() > 1 {
        new_segments.insert(0, Segment::line())
      }
    }
    if crop {
      for
        line in Segment::split_and_crop_lines(
          new_segments,
          self.width(),
          pad=false,
        ) {
        self.buffer.append(line)
      }
    } else {
      self.buffer.append(new_segments)
    }
  }
  self.exit_buffer()
}

///|
/// Pretty print JSON. `json` is a JSON string; otherwise `data` is encoded.
pub fn Console::print_json(
  self : Console,
  json? : String,
  data? : Json,
  indent? : Int? = Some(2),
  indent_str? : String,
  highlight? : Bool = true,
  ensure_ascii? : Bool = false,
  sort_keys? : Bool = false,
) -> Unit raise {
  let renderable = match json {
    Some(j) =>
      JSON::new(j, indent~, indent_str?, highlight~, ensure_ascii~, sort_keys~)
    None =>
      JSON::from_data(
        data.unwrap_or(Json::null()),
        indent~,
        indent_str?,
        highlight~,
        ensure_ascii~,
        sort_keys~,
      )
  }
  self.print(renderable, soft_wrap=true)
}

///|
/// Update the screen at a given offset (alt screen only).
pub fn Console::update_screen(
  self : Console,
  renderable : &Renderable,
  region? : Region,
  options? : ConsoleOptions,
) -> Unit raise {
  if !self.is_alt_screen() {
    raise NoAltScreen("Alt screen must be enabled to call update_screen")
  }
  let render_options = options.unwrap_or_else(() => self.options())
  let (x, y, render_options) = match region {
    None =>
      (
        0,
        0,
        render_options.update_dimensions(
          render_options.max_width,
          match render_options.height {
            Some(h) if h != 0 => h
            _ => self.height()
          },
        ),
      )
    Some(r) => (r.x, r.y, render_options.update_dimensions(r.width, r.height))
  }
  let lines = self.render_lines(renderable, options=render_options)
  self.update_screen_lines(lines, x~, y~)
}

///|
/// Update lines of the screen at a given offset (alt screen only).
pub fn Console::update_screen_lines(
  self : Console,
  lines : Array[Array[Segment]],
  x? : Int = 0,
  y? : Int = 0,
) -> Unit raise {
  if !self.is_alt_screen() {
    raise NoAltScreen("Alt screen must be enabled to call update_screen")
  }
  let screen_update = ScreenUpdate::new(lines, x, y)
  let segments = self.render(screen_update)
  self.buffer.append(segments)
  self.check_buffer()
}

///|
/// Log rich content to the terminal, with the time and the caller location.
#callsite(autofill(loc))
pub fn Console::log(
  self : Console,
  object : &Renderable,
  sep? : String = " ",
  end? : String = "\n",
  style? : &IntoStyle,
  justify? : JustifyMethod,
  emoji? : Bool,
  markup? : Bool,
  highlight? : Bool,
  loc~ : SourceLoc,
) -> Unit raise {
  self.log_many(
    [object],
    sep~,
    end~,
    style?,
    justify?,
    emoji?,
    markup?,
    highlight?,
    loc~,
  )
}

///|
/// Split a `SourceLoc` (`path:line:col-line:col`) into `(path, line)`.
fn parse_source_loc(loc : SourceLoc) -> (String, Int) {
  let s = loc.to_string()
  let parts : Array[String] = s.split(":").map(p => p.to_owned()).collect()
  if parts.length() >= 4 {
    let path = parts[0:parts.length() - 3].to_owned().join(":")
    (path, parse_int_or(parts[parts.length() - 3], 0))
  } else {
    (s, 0)
  }
}

///|
/// Log several objects (see `log`).
#callsite(autofill(loc))
pub fn Console::log_many(
  self : Console,
  objects : ArrayView[&Renderable],
  sep? : String = " ",
  end? : String = "\n",
  style? : &IntoStyle,
  justify? : JustifyMethod,
  emoji? : Bool,
  markup? : Bool,
  highlight? : Bool,
  loc~ : SourceLoc,
) -> Unit raise {
  let objects : ArrayView[&Renderable] = if objects.length() == 0 {
    [NewLine::new()]
  } else {
    objects
  }
  let render_hooks = self.render_hooks.copy()
  self.enter_buffer()
  {
    errdefer self.exit_buffer()
    let mut renderables = self.collect_renderables(
      objects,
      sep,
      end,
      justify?,
      emoji?,
      markup?,
      highlight?,
    )
    if style is Some(style) {
      let style = style.into_style_type()
      renderables = renderables.map(r => (Styled::new(r, style) : &Renderable))
    }
    let (filename, line_no) = parse_source_loc(loc)
    let link_path = if filename.has_prefix("<") { None } else { Some(filename) }
    let path = match filename.rev_find("/") {
      Some(i) => py_slice(filename, i + 1, filename.length())
      None => filename
    }
    renderables = [
      self.log_render.render(
        self,
        renderables,
        log_time=(self.get_datetime)(),
        path~,
        line_no~,
        link_path?,
      ),
    ]
    for hook in render_hooks {
      renderables = hook.process_renderables(renderables)
    }
    let new_segments = []
    let render_options = self.options()
    for renderable in renderables {
      new_segments.append(self.render(renderable, options=render_options))
    }
    for
      line in Segment::split_and_crop_lines(
        new_segments,
        self.width(),
        pad=false,
      ) {
      self.buffer.append(line)
    }
  }
  self.exit_buffer()
}

///|
/// Check if the buffer may be rendered, and render it if it can.
fn Console::check_buffer(self : Console) -> Unit raise {
  if self.quiet {
    self.buffer.clear()
    return
  }
  self.write_buffer()
}

///|
/// Write the buffer to the output file.
fn Console::write_buffer(self : Console) -> Unit raise {
  if self.record && self.buffer_index == 0 {
    self.record_buffer.append(self.buffer)
  }
  if self.buffer_index == 0 {
    let text = self.render_buffer(self.buffer)
    self.buffer.clear()
    let file = self.file()
    if text != "" {
      file.write(text)
    }
    file.flush()
  }
}

///|
/// Render buffered output to a string.
fn Console::render_buffer(self : Console, buffer : Array[Segment]) -> String {
  let output = StringBuilder()
  let color_system = self.color_system_
  let legacy_windows = self.legacy_windows
  let not_terminal = !self.is_terminal()
  let buffer = if self.no_color && color_system is Some(_) {
    Segment::remove_color(buffer)
  } else {
    buffer
  }
  for segment in buffer {
    match segment.style {
      Some(style) if style.to_bool() =>
        style.render_to(output, segment.text, color_system, legacy_windows)
      _ =>
        if !(not_terminal && segment.has_codes()) {
          output.write_string(segment.text)
        }
    }
  }
  output.to_string()
}

///|
/// Display a prompt and wait for input from the user.
///
/// `stream` replaces stdin: it is called once and returns the next line like
/// Python's `stream.readline()` (with its newline, `""` at end of input);
/// as upstream, the result is then returned as is.
pub fn Console::input(
  self : Console,
  prompt? : &IntoText,
  markup? : Bool = true,
  emoji? : Bool = true,
  password? : Bool = false,
  stream? : () -> String raise,
) -> String raise {
  if prompt is Some(prompt) && prompt.into_text_type().to_bool() {
    match prompt.into_text_type() {
      Str(s) => self.print(s, markup~, emoji~, end="")
      Text(t) => self.print(t, markup~, emoji~, end="")
    }
  }
  if stream is Some(readline) {
    return readline()
  }
  match @term.read_line(echo=!password) {
    Some(line) => {
      if password {
        // the newline was not echoed
        self.file().write("\n")
      }
      match line.strip_suffix("\n") {
        Some(l) => l.to_owned()
        None => line
      }
    }
    None => raise ValueError("EOF when reading a line")
  }
}

///|
/// Generate text from console contents (requires `record=true`).
pub fn Console::export_text(
  self : Console,
  clear? : Bool = true,
  styles? : Bool = false,
) -> String {
  guard self.record else {
    abort(
      "To export console contents set record=True in the constructor or instance",
    )
  }
  let buf = StringBuilder()
  if styles {
    for segment in self.record_buffer {
      match segment.style {
        Some(style) if style.to_bool() =>
          buf.write_string(style.render(text=segment.text))
        _ => buf.write_string(segment.text)
      }
    }
  } else {
    for segment in self.record_buffer {
      if !segment.has_codes() {
        buf.write_string(segment.text)
      }
    }
  }
  if clear {
    self.record_buffer.clear()
  }
  buf.to_string()
}

///|
/// Generate text from console contents and save it to a file.
pub fn Console::save_text(
  self : Console,
  path : String,
  clear? : Bool = true,
  styles? : Bool = false,
) -> Unit raise {
  let text = self.export_text(clear~, styles~)
  if !@term.write_file(path, text) {
    raise WriteError("unable to write \{path}")
  }
}

///|
/// The default HTML template.
pub let console_html_format : String =
  #|
  #|
  #|
  #|
  #|
  #|
  #|
  #|    
{code}
#| #| #| ///| /// Python `html.escape(text)`. pub fn html_escape(text : String) -> String { @pystr.html_escape(text) } ///| /// Generate HTML from console contents (requires `record=true`). pub fn Console::export_html( self : Console, theme? : @color.TerminalTheme, clear? : Bool = true, code_format? : String, inline_styles? : Bool = false, ) -> String { guard self.record else { abort( "To export console contents set record=True in the constructor or instance", ) } let fragments = StringBuilder() let theme = theme.unwrap_or(@color.default_terminal_theme) let mut stylesheet = "" let render_code_format = code_format.unwrap_or(console_html_format) let segments = Segment::filter_control(Segment::simplify(self.record_buffer)) if inline_styles { for segment in segments { let mut text = html_escape(segment.text) match segment.style { Some(style) if style.to_bool() => { let rule = style.get_html_style(theme~) if style.link() is Some(link) && link != "" { text = "\{text}" } if rule != "" { text = "\{text}" } } _ => () } fragments.write_string(text) } } else { let styles : Map[String, Int] = Map([]) for segment in segments { let mut text = html_escape(segment.text) match segment.style { Some(style) if style.to_bool() => { let rule = style.get_html_style(theme~) let style_number = match styles.get(rule) { Some(n) => n None => { let n = styles.length() + 1 styles[rule] = n n } } if style.link() is Some(link) && link != "" { text = "\{text}" } else { text = "\{text}" } } _ => () } fragments.write_string(text) } let rules = [] for rule, number in styles { if rule != "" { rules.push(".r\{number} {\{rule}}") } } stylesheet = rules.join("\n") } let rendered_code = py_format(render_code_format, { "code": fragments.to_string(), "stylesheet": stylesheet, "foreground": theme.foreground_color.hex(), "background": theme.background_color.hex(), }) if clear { self.record_buffer.clear() } rendered_code } ///| /// Generate HTML from console contents and write it to a file. pub fn Console::save_html( self : Console, path : String, theme? : @color.TerminalTheme, clear? : Bool = true, code_format? : String, inline_styles? : Bool = false, ) -> Unit raise { let html = self.export_html(theme?, clear~, code_format?, inline_styles~) if !@term.write_file(path, html) { raise WriteError("unable to write \{path}") } } ///| /// Run `f`, then send everything it printed to a pager. pub fn Console::pager( self : Console, f : () -> Unit raise, pager? : &Pager, styles? : Bool = false, links? : Bool = false, ) -> Unit raise { let pager : &Pager = match pager { Some(p) => p None => SystemPager::new() } self.enter_buffer() { errdefer { self.buffer.clear() self.buffer_index -= 1 } f() } let mut buffer = self.buffer.copy() if !styles { buffer = Segment::strip_styles(buffer) } else if !links { buffer = Segment::strip_links(buffer) } let content = self.render_buffer(buffer) self.buffer.clear() self.buffer_index -= 1 pager.show(content) }