Private/Console/Ui.psm1

using namespace System
using namespace System.IO
using namespace System.Threading


using module ..\Enums.psm1
using module ..\Abstracts.psm1
using module .\Internal.psm1
using module .\Colors.psm1
using module .\Rendering.psm1
using module .\Ansi.psm1
using module .\Renderer.psm1
using module .\Boxes.psm1
using module .\Widgets.psm1

class AnsiConsoleFacade : IAnsiConsole {
  hidden [object]$_renderLock
  hidden [AnsiWriter]$_writer
  hidden [AnsiMarkup]$_markup

  AnsiConsoleFacade([AnsiWriter]$writer) {
    $this._writer = $writer
    $this._markup = [AnsiMarkup]::new($writer)

    $prof = [Profile]::new()
    $prof.Capabilities = $writer.Capabilities
    $prof.Out = [AnsiConsoleOutput]::new($writer.GetOutput())
    $this.Profile = $prof

    $this.Cursor = [NoopCursor]::new()
    $this.Input = [IAnsiConsoleInput]::new()
    $this.ExclusivityMode = [NoopExclusivityMode]::new()
    $this._renderLock = [object]::new()
  }
  [void] Write([string]$string) {
    $this.Write($string, $false)
  }
  [void] Write([string]$string, [bool]$animate) {
    $this.use_animation($animate)
    $this.Write([Text]$string)
  }
  [void] Write([IRenderable]$renderable) {
    if ($null -eq $renderable) { return }
    [Monitor]::Enter($this._renderLock)
    try {
      [ConsoleRenderer]::Render($this, $renderable)
    } finally {
      [Monitor]::Exit($this._renderLock)
    }
  }
  [void] WriteLine([string]$string) {
    $this.WriteLine($string, $false)
  }
  [void] WriteLine([string]$string, [bool]$animate) {
    $this.use_animation($animate)
    $this.WriteObject($string)
    $this._writer.WriteLine()
  }
  [void] WriteObject([object]$value) {
    if ($value -is [IRenderable]) {
      $this.Write([IRenderable]$value)
      return
    }

    # try markup but if that fails then skip it - render as plain text
    if ($value -is [string] -and $value -match '\[[^\]]+\]') {
      $markupwasok = $false
      try {
        $this.Write([Markup]::new([string]$value))
        $markupwasok = $true
      } catch {
        Write-Debug "Failed to write markup. $_"
      }
      if ($markupwasok) {
        return
      }
    }

    $text = if ($null -eq $value) { [string]::Empty } else { [string]$value }
    $this.Write([Text]::new($text, [Style]::Plain))
  }

  [void] WriteAnsi([Action[AnsiWriter]]$action) {
    if ($null -eq $action) {
      return
    }

    [Monitor]::Enter($this._renderLock)
    try {
      $action.Invoke($this._writer)
    } finally {
      [Monitor]::Exit($this._renderLock)
    }
  }

  [void] Markup([string]$markup) {
    $this.Markup($markup, [Style]::Plain)
  }

  [void] Markup([string]$markup, [Style]$style) {
    [Monitor]::Enter($this._renderLock)
    try {
      $this._markup.Write($markup, $style)
    } finally {
      [Monitor]::Exit($this._renderLock)
    }
  }

  [void] MarkupLine([string]$markup) {
    $this.Markup($markup)
    $this._writer.WriteLine()
  }
  [void] Clear() {
    $this.Clear($true)
  }

  [void] Clear([bool]$CursorHome) {
    [Monitor]::Enter($this._renderLock)
    try {
      if ($this._writer.Capabilities.Ansi) {
        $this._writer.Write("`e[2J")
        if ($CursorHome) {
          $this._writer.Write("`e[H")
        }
      } else {
        [Console]::Clear()
      }
    } finally {
      [Monitor]::Exit($this._renderLock)
    }
  }
  [AnsiWriter] GetWriter() {
    return $this._writer
  }
  [void] use_animation([bool]$condition) {
    # .NOTES
    # typing animation only works when we use $host.UI.Write(..) not [console]::write(..)
    $this._writer._output.UseTypingEffect = $condition
    $this._writer._output.WriteRaw = !$condition
  }
}

class AnsiConsole {
  static [IAnsiConsole] $Console

  static AnsiConsole() {
    [AnsiConsole]::Initialize()
  }

  static [void] Markup([string]$markup) {
    ([AnsiConsoleFacade][AnsiConsole]::Console).Markup($markup)
  }

  static [void] MarkupLine([string]$markup) {
    ([AnsiConsoleFacade][AnsiConsole]::Console).MarkupLine($markup)
  }

  static [void] Clear() {
    [AnsiConsole]::Console.Clear()
  }

  static [void] Write([object]$value) {
    ([AnsiConsoleFacade][AnsiConsole]::Console).WriteObject($value)
  }

  static [void] WriteLine([string]$text) {
    ([AnsiConsoleFacade][AnsiConsole]::Console).WriteLine($text)
  }

  static [void] Initialize() {
    [AnsiConsole]::Console = [AnsiConsoleFactory]::Create([AnsiConsoleSettings]::new())
  }
}

class AnsiConsoleFactory {
  static [IAnsiConsole] Create([AnsiConsoleSettings]$settings) {
    if ($null -eq $settings) {
      $settings = [AnsiConsoleSettings]::new()
    }

    if ($null -eq $settings.Out) {
      $settings.Out = [AnsiConsoleOutput]::new([ConsoleWriter]::new())
    }

    $writer = [AnsiWriter]::new($settings.Out.Writer)
    $writer.Capabilities = [AnsiConsoleFactory]::ResolveCapabilities($settings)
    $IAnsiConsole = [AnsiConsoleFacade]::new($writer)
    $IAnsiConsole.PsObject.Properties.Add([PSscriptProperty]::new("Writer", { return $this.GetWriter() }))
    return $IAnsiConsole
  }

  static hidden [AnsiCapabilities] ResolveCapabilities([AnsiConsoleSettings]$settings) {
    $capabilities = [AnsiCapabilities]::new()
    $capabilities.Ansi = [AnsiConsoleFactory]::ResolveAnsiSupport($settings.Ansi)
    $capabilities.ColorSystem = [AnsiConsoleFactory]::ResolveColorSystem($settings.ColorSystem)
    $capabilities.Links = $capabilities.Ansi
    $capabilities.AlternateBuffer = $capabilities.Ansi
    return $capabilities
  }

  static hidden [bool] ResolveAnsiSupport([AnsiSupport]$support) {
    switch ($support) {
      ([AnsiSupport]::Yes) { return $true }
      ([AnsiSupport]::No) { return $false }
      default {
        $term = [Environment]::GetEnvironmentVariable('TERM')
        return -not [string]::IsNullOrWhiteSpace($term) -or -not [Console]::IsOutputRedirected
      }
    }

    return $false
  }

  static hidden [ColorSystem] ResolveColorSystem([ColorSystemSupport]$support) {
    switch ($support) {
      ([ColorSystemSupport]::NoColors) { return [ColorSystem]::NoColors }
      ([ColorSystemSupport]::Legacy) { return [ColorSystem]::Legacy }
      ([ColorSystemSupport]::Standard) { return [ColorSystem]::Standard }
      ([ColorSystemSupport]::EightBit) { return [ColorSystem]::EightBit }
      ([ColorSystemSupport]::TrueColor) { return [ColorSystem]::TrueColor }
      default {
        $colorterm = [Environment]::GetEnvironmentVariable('COLORTERM')
        if ($colorterm -match 'truecolor|24bit') {
          return [ColorSystem]::TrueColor
        }

        return [Console]::IsOutputRedirected ? [ColorSystem]::NoColors : [ColorSystem]::TrueColor
      }
    }

    return [ColorSystem]::NoColors
  }
}

class AnsiConsoleOutput {
  [ConsoleWriter]$Writer
  AnsiConsoleOutput([ConsoleWriter]$writer) {
    $this.Writer = $writer
  }
}

class AnsiConsoleSettings {
  [AnsiSupport]$Ansi = [AnsiSupport]::Detect
  [ColorSystemSupport]$ColorSystem = [ColorSystemSupport]::Detect
  [AnsiConsoleOutput]$Out = [AnsiConsoleOutput]::new([ConsoleWriter]::new())
}

class Profile {
  [AnsiCapabilities]$Capabilities
  [AnsiConsoleOutput]$Out
  [int]$Width
  [int]$Height

  Profile() {
    $this.Capabilities = [AnsiCapabilities]::new()
    $this.Out = [AnsiConsoleOutput]::new([ConsoleWriter]::new())
    $this.Width = 0
    $this.Height = 0
  }

  [int] GetWidth() {
    if ($this.Width -gt 0) {
      return $this.Width
    }

    try {
      return [Math]::Max(1, [Console]::WindowWidth)
    } catch {
      return 80
    }
  }

  [int] GetHeight() {
    if ($this.Height -gt 0) {
      return $this.Height
    }

    try {
      return [Math]::Max(1, [Console]::WindowHeight)
    } catch {
      return 25
    }
  }

  [RenderOptions] CreateRenderOptions() {
    $options = [RenderOptions]::new()
    $options.ColorSystem = $this.Capabilities.ColorSystem
    $options.Ansi = $this.Capabilities.Ansi
    return $options
  }
}