Artifact 0055fde15683878e97cbfe9e2359a7d531cfa5d26b929bea5b2ee8a836cd2990:

  • File src/uis/orig/orig.cr — part of check-in [6d09a61a85] at 2024-08-17 03:43:13 on branch relocate-input-handling — Use S-Lang for input. Properly handle Ctrl-Z. (user: alexa size: 48702)

#### Benben
#### Copyright (C) 2023-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or
#### modify it under the terms of the GNU Affero General Public
#### License as published by the Free Software Foundation, either
#### version 3 of the License, or (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#### Affero General Public License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program.  If not, see <https://www.gnu.org/licenses/>.
require "remislang"
require "./theme"
require "../../ui"
require "../../banners"
require "./vumeter"
require "./tagfield"
require "./banner"

Signal::WINCH.trap do
  # This could be nil if we're rendering
  Benben.ui?.try do |rawUI|
    if ui = rawUI.as?(Benben::OrigUI)
      # Reinitialize S-Lang and then update the screen size
      ui.uiLock.synchronize do
        LibSLang.SLsmg_init_smg()
        RemiSlang::Screen.updateScreenSize
      end
    end
  end
end

Signal::TSTP.trap do
  Benben.dlog!("Suspending")

  # Let the UI know we need to suspend
  Benben.ui?.try do |rawUI|
    if ui = rawUI.as?(Benben::OrigUI)
      ui.uiLock.synchronize do
        # The OrigUI needs to remember its current TTY state so it can properly
        # restore it later.  Without this, input handling goes wonky and starts
        # needing Enter to be pressed after each key input.
        LibC.tcgetattr(STDOUT.fd, out currentAttrs)
        ui.currentAttrs = currentAttrs
      end
    end
  end

  # Send SIGSTOP
  Process.signal(Signal::STOP, Process.pid)
end

Signal::CONT.trap do
  Benben.dlog!("Unsuspending")

  # This could be nil if we're rendering
  Benben.ui?.try do |rawUI|
    if ui = rawUI.as?(Benben::OrigUI)
      ui.uiLock.synchronize do
        # The OrigUI needs to restore its TTY state from before the suspend so
        # it can properly restore it later.  Without this, input handling goes
        # wonky and starts needing Enter to be pressed after each key input.
        ui.currentAttrs.try do |attrs|
          LibC.tcsetattr(STDOUT.fd, Termios::AttributeSelection::TCSANOW, pointerof(attrs))
        end
      end

      # Redraw the screen
      ui.queueRedrawWholeScreen
    end
  end
end

module Benben
  alias Slang = RemiSlang
  alias SLScreen = RemiSlang::Screen
  alias SLInput = RemiSlang::Input

  class OrigUI < UI
    property currentAttrs : LibC::Termios?

    # How often the CPU usage is refreshed, in milliseconds.
    CPU_USAGE_UPDATE_FREQ = 1000

    # How long a message is displayed.
    MESSAGE_TIME = 5.seconds

    # 30 fps, should be more than plenty for a TUI.
    FPS_CAP = 1000.0 / 30.0

    ##
    ## These are messages that are displayed when `h` is pressed.  These must be
    ## 4-5 lines each after the call to String#strip.

    HELP_MESSAGE = %|
n - Next  p - Prev  t - Tag lang          a - Vol up    ] - Loop up
q - Quit  e - EQ    c - Toggle soft clip  z - Vol down  [ - Loop down
s - Toggle stereo enhancer                Space - Pause/Unpause
R - Redraw screen   S - Stop after song   C - Reload song configs
> - Seek forward    < - Seek backward
    |.strip.lines

    HELP_MESSAGE_MODULES = %|
n - Next  p - Prev  i - Toggle Interp.    a - Vol up
q - Quit  e - EQ    c - Toggle soft clip  z - Vol down
s - Toggle stereo enhancer                Space - Pause/Unpause
R - Redraw screen   S - Stop after song   C - Reload song configs
> - Next pattern    < - Prev pattern
    |.strip.lines

    HELP_MESSAGE_MIDI = %|
n - Next  p - Prev  x - Toggle Chorus     a - Vol up
q - Quit  e - EQ    c - Toggle soft clip  z - Vol down
s - Toggle stereo enhancer                Space - Pause/Unpause
R - Redraw screen   S - Stop after song   C - Reload song configs
    |.strip.lines

    enum Mode
      Playing
      Paused
      PlayingStopAfterCurrent
      PausedStopAfterCurrent
      Undefined
    end

    ##
    ## Theme
    ##

    @@theme : Theme?
    class_getter! theme

    ##
    ## Headers and Tags
    ##

    INFO_LINE_HEADER = "Info: "

    TAG_FIELD_TRACK = 0
    TAG_FIELD_GAME = 1
    TAG_FIELD_AUTHOR = 2
    TAG_FIELD_SYSTEM = 3
    TAG_FIELD_RELEASE_DATE = 4
    TAG_FIELD_CREATOR = 5
    TAG_FIELD_RVA = 6
    TAG_FIELD_MODE = 7
    @tagFields : Array(TagField) = [] of TagField

    getter uiLock : Mutex = Mutex.new

    # Screen info
    getter lastWidth : Int32 = 0
    getter lastHeight : Int32 = 0

    # Whether or not the UI is running
    @running : AtomicBool = AtomicBool.new(false)

    # Whether or not to use animations.
    getter? animations : Bool = true

    # Sample rates
    @sampleRate : UInt32
    @invSampleRate : Float64

    # Last time the tag language was swapped (only when auto-swapping tag
    # languages).
    @lastTagSwap : Time = Time.local

    # Current volume
    @mainVolume : Float64 = -1.0
    @lastShownVolume : Float64 = -1.0

    # Most recent messages
    @lastMsg : String = ""
    @lastErrMsg : String = ""

    # Whether or not we're currently showing help
    @helpTimeout : Time?

    # Current playback mode
    @mode : Mode = Mode::Playing
    @lastMode : Mode = Mode::Undefined

    # The VU meter.
    private getter! vu : VUMeter?

    # Banner stuff
    @banner : Banner

    # Tag display info
    @playingFile : PlayableFile?
    @tag : TagInfo?
    @gd3TagLang : Gd3TagLang

    # Information Line
    @infoLine : String = ""

    # Progress bar information.
    @progressMax : UInt64 = 1u64
    @progressPos : UInt64 = 1u64
    @totalTracks : UInt32 = 0
    @curTrack : UInt32 = 0
    @currentLoop : UInt32 = 0
    @maxLoops : Int32 = 0
    @progressBarLabel : String = ""
    @progressBarLabelWidth : Int32 = 0
    @eqEnabled : Bool = false
    @stereoEnhancerEnabled : Bool = false
    @softClippingEnabled : Bool = true
    @reverbEnabled : Bool = false
    @hasSongConfig : Bool = false
    @progressBarCurTime : String = ""
    @progressBarTotalTime : String = ""

    # When the CPU time was last drawn
    @cpuLastUpdated : Time::Span = Time.monotonic - CPU_USAGE_UPDATE_FREQ.seconds

    # Local copy of the "stop after current song" functionality so we can
    # compare it to what the player has.
    @stopAfterCurrent : Bool = false

    # Where the banner ends and the main content area begins.
    @mainAreaStartRow : Int32 = 0

    # Communication channels
    protected getter keyChan : Channel(KeyCommand) = Channel(KeyCommand).new(10)

    # Song queue total time
    @totalTime : Time::Span? = nil
    @totalTimeMut : Mutex = Mutex.new
    @totalTimeStr : String = ""

    def initialize
      @@theme = Theme.new(Benben.theme)
      @animations = Benben.config.uiConfig.animationsEnabled?
      @gd3TagLang = case Benben.config.vgm.preferredGd3Lang
                    when .toggle_prefer_japanese?
                      Gd3TagLang::Japanese
                    when .toggle_prefer_english?
                      Gd3TagLang::English
                    else
                      Benben.config.vgm.preferredGd3Lang
                    end

      @sampleRate = Benben.config.sampleRate
      @invSampleRate = 1.0 / @sampleRate

      @tagFields << TagField.new # Track
      @tagFields << TagField.new # Game/Album
      @tagFields << TagField.new # Author/Artist
      @tagFields << TagField.new # System/Genre
      @tagFields << TagField.new # Release Date
      @tagFields << TagField.new # Creator/Composer
      @tagFields << TagField.new # RVA
      @tagFields << TagField.new # Mode

      @banner = Banner.getBanner(@animations)
      @mainAreaStartRow = @banner.size
    end

    ###
    ### Abstract Method Implementations
    ###

    def apply(settings : EphemeralConfig) : Nil
      @stereoEnhancerEnabled = settings.enableStereoEnhancer?
      @eqEnabled = settings.equalizerEnabled?
      @softClippingEnabled = settings.enableSoftClipping?
      @reverbEnabled = settings.reverbEnabled?
      @hasSongConfig = settings.songConfigApplied?
      @mainVolume = settings.mainVolume
      Benben.dlog!("Initial main volume from settings application: #{@mainVolume}")
      updateProgressBarInfo
    end

    ###
    ### Messages
    ###

    @messageTimeout : Time?
    @errorQueue : Deque(String) = Deque(String).new(1)
    @messageQueue : Deque(String) = Deque(String).new(1)

    # Updates the messages display.
    private def updateMessages : Nil
      forceShowError : Bool = false
      forceShow : Bool = false
      offset = @lastHeight - 2

      # First, dequeue any messages that we've received.
      select
      when msg = @errMsgChan.receive
        if forceShowError = msg[1]
          @errorQueue.insert(0, msg[0])
        else
          @errorQueue << msg[0]
        end
        Benben.dlog!("Error message queued (immediate? #{msg[1]}): #{msg[0]}")
      when msg = @msgChan.receive
        if forceShow = msg[1]
          @messageQueue.insert(0, msg[0])
        else
          @messageQueue << msg[0]
        end
        Benben.dlog!("Message queued (immediate? #{msg[1]}): #{msg[0]}")
      else nil
      end

      # Now show the messages.  The ones that are forced to the front of the
      # queue get priority.  Errors are always favored over normal messages.
      if forceShowError
        Benben.dlog!("Showing immediate error message")
        SLScreen.goto(0, offset)
        SLScreen.clearEol
        OrigUI.theme.writeError("Error: ", @errorQueue.pop)
        @messageTimeout = Time.local + MESSAGE_TIME
      elsif forceShow
        Benben.dlog!("Showing immediate message")
        SLScreen.goto(0, offset)
        SLScreen.clearEol
        SLScreen.write(@messageQueue.pop)
        @messageTimeout = Time.local + MESSAGE_TIME
      else
        # No priority messages.  Check the timeout.
        if timeout = @messageTimeout
          if timeout < Time.local
            # Timeout is done, we can clear the messages line.
            Benben.dlog!("Message timeout finished")
            SLScreen.goto(0, offset)
            SLScreen.clearEol
            @messageTimeout = nil
          end
        else
          # Timeout isn't set, which means we can do a new message if we have
          # any.  Again, we favor errors over normal messages.
          if !@errorQueue.empty?
            Benben.dlog!("Displaying new error message")
            SLScreen.goto(0, offset)
            SLScreen.clearEol
            OrigUI.theme.writeError("Error: ", @errorQueue.pop)
            @messageTimeout = Time.local + MESSAGE_TIME
          elsif !@messageQueue.empty?
            Benben.dlog!("Displaying new message")
            SLScreen.goto(0, offset)
            SLScreen.clearEol
            SLScreen.write(@messageQueue.pop)
            @messageTimeout = Time.local + MESSAGE_TIME
          end
        end
      end
    end

    # Updates the help display.
    private def updateHelp : Nil
      offset = @mainAreaStartRow + 8

      # First, dequeue any messages that we've received.
      select
      when msg = @helpChan.receive
        if msg
          # Clear the drawing area, then draw the help message
          SLScreen.fill(0, offset,
                        @lastWidth.to_u32!,
                        Math.max(@lastHeight - offset - 2, 6).to_u32,
                        ' ')
          case @playingFile
          when ModuleFile
            HELP_MESSAGE_MODULES.each_with_index do |line, idx|
              SLScreen.write(0, offset + idx + 1, line)
            end
          when MidiFile
            HELP_MESSAGE_MIDI.each_with_index do |line, idx|
              SLScreen.write(0, offset + idx + 1, line)
            end
          else
            HELP_MESSAGE.each_with_index do |line, idx|
              SLScreen.write(0, offset + idx + 1, line)
            end
          end
          # Set the timeout
          @helpTimeout = Time.local + MESSAGE_TIME
        else
          # Immediately stop showing help
          @helpTimeout = Time.local
        end
      else nil
      end

      # Check to see if we need to stop showing help.
      if timeout = @helpTimeout
        if Time.local >= timeout
          @helpTimeout = nil
          # Clear the drawing area
          SLScreen.fill(0, offset, (@lastWidth - 2).to_u32!, 6u32, ' ')
          updateSongQueue
        end
      end
    end

    ###
    ### Tag Handling
    ###

    private def updateTagFields : Nil
      tag = @tag
      case tag
      in VgmTagInfo
        @gd3TagLang = case Benben.config.vgm.preferredGd3Lang
                      in .toggle_prefer_japanese?, .japanese?
                        Gd3TagLang::English # Backwards - about to switch anyway
                      in .toggle_prefer_english?, .english?
                        Gd3TagLang::Japanese # Backwards - about to switch anyway
                      end
        toggleGD3Language
        @tagFields[TAG_FIELD_RELEASE_DATE].set("Release Date: ", tag.date)
        @tagFields[TAG_FIELD_CREATOR     ].set("VGM Creator: ",  tag.creator)
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          "N/A")

      in ModuleTagInfo
        @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        tag.title)
        @tagFields[TAG_FIELD_GAME        ].set("Type: ",         tag.type)
        @tagFields[TAG_FIELD_AUTHOR      ].set("Tracks: ",       tag.tracks)
        @tagFields[TAG_FIELD_SYSTEM      ].set("Patterns: ",     tag.patterns)
        @tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ",     tag.tracksPerPat)
        @tagFields[TAG_FIELD_CREATOR     ].set("Instruments: ",  tag.instruments)
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          "N/A")

      in VorbisTagInfo
        @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        tag.title)
        @tagFields[TAG_FIELD_GAME        ].set("Album: ",        tag.album)
        @tagFields[TAG_FIELD_AUTHOR      ].set("Artist: ",       tag.artist)
        @tagFields[TAG_FIELD_SYSTEM      ].set("Genre: ",        tag.genre)
        @tagFields[TAG_FIELD_RELEASE_DATE].set("Release Date: ", tag.date)
        if !tag.composer.empty?
          @tagFields[TAG_FIELD_CREATOR].set("Composer: ", tag.composer)
        else
          @tagFields[TAG_FIELD_CREATOR].set("Performer: ", tag.performer)
        end
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          Benben.config.replayGain.to_s)

      in Id3TagInfo
        @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        tag.title)
        @tagFields[TAG_FIELD_GAME        ].set("Album: ",        tag.album)
        @tagFields[TAG_FIELD_AUTHOR      ].set("Artist: ",       tag.artist)
        @tagFields[TAG_FIELD_SYSTEM      ].set("Genre: ",        tag.genre)
        @tagFields[TAG_FIELD_RELEASE_DATE].set("Release Date: ", tag.date)
        if !tag.composer.empty?
          @tagFields[TAG_FIELD_CREATOR].set("Composer: ", tag.composer)
        else
          @tagFields[TAG_FIELD_CREATOR].set("Performer: ", tag.performer)
        end
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          Benben.config.replayGain.to_s)

      in MidiTagInfo
        # MIDI files don't have tag information, so we repurpose the fields to
        # show other information.
        if midi = Benben.player.curFile.as?(MidiFile)
          @tagFields[TAG_FIELD_TRACK       ].set("File: ",       Path[midi.filename].basename)
          @tagFields[TAG_FIELD_GAME        ].set("Reverb: ",     Benben.config.midi.reverbType.to_s)
          @tagFields[TAG_FIELD_AUTHOR      ].set("Reverb Amt: ", Benben.config.midi.reverbAmount.to_s)
          @tagFields[TAG_FIELD_SYSTEM      ].set("Chorus Amt: ", Benben.config.midi.chorusAmount.to_s)
          @tagFields[TAG_FIELD_RELEASE_DATE].set("V. Filter: ",  Benben.config.midi.voiceFilterType.to_s)
          @tagFields[TAG_FIELD_CREATOR     ].set("C. Filter: ",  Benben.config.midi.channelFilterType.to_s)
        else
          RemiLib.log.warn("UI has MidiTagInfo, but the player does not have a MidiFile")
          @tagFields[TAG_FIELD_TRACK       ].set("File: ",       "")
          @tagFields[TAG_FIELD_GAME        ].set("Reverb: ",     "")
          @tagFields[TAG_FIELD_AUTHOR      ].set("Reverb Amt: ", "")
          @tagFields[TAG_FIELD_SYSTEM      ].set("Chorus Amt: ", "")
          @tagFields[TAG_FIELD_RELEASE_DATE].set("V. Filter: ",  "")
          @tagFields[TAG_FIELD_CREATOR     ].set("C. Filter: ",  "")
        end
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          "N/A")

      in PcmTagInfo
        # PCM files don't have tag information, so we repurpose the fields to
        # show other information.
        if pcm = Benben.player.curFile.as?(PcmFile)
          @tagFields[TAG_FIELD_TRACK       ].set("File: ",        Path[pcm.filename].basename)
          @tagFields[TAG_FIELD_GAME        ].set("Sample Rate: ", pcm.sampleRate.to_s)
          @tagFields[TAG_FIELD_AUTHOR      ].set("Samples: ",     formatStr("~:d", pcm.totalSamples))
          @tagFields[TAG_FIELD_SYSTEM      ].set("Type: ",        pcm.type)
          @tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ",    pcm.channels.to_s)
          @tagFields[TAG_FIELD_CREATOR     ].set("Resampling: ",   (Benben.config.sampleRate == pcm.sampleRate ? "No" : "Yes"))
        else
          RemiLib.log.warn("UI has PcmTagInfo, but the player does not have a PcmFile")
          @tagFields[TAG_FIELD_TRACK       ].set("File: ",       "")
          @tagFields[TAG_FIELD_GAME        ].set("Sample Rate: ",     "")
          @tagFields[TAG_FIELD_AUTHOR      ].set("Samples: ", "")
          @tagFields[TAG_FIELD_SYSTEM      ].set("Typ: ", "")
          @tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ",  "")
          @tagFields[TAG_FIELD_RELEASE_DATE].set("Resampling: ",  "")
        end
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          "N/A")

      in QoaTagInfo
        # QOA files don't have tag information, so we repurpose the fields to
        # show other information.
        if qoa = Benben.player.curFile.as?(QoaFile)
          @tagFields[TAG_FIELD_TRACK       ].set("File: ",        Path[qoa.filename].basename)
          @tagFields[TAG_FIELD_GAME        ].set("Sample Rate: ", qoa.sampleRate.to_s)
          @tagFields[TAG_FIELD_AUTHOR      ].set("Samples: ",     formatStr("~:d", qoa.totalSamples))
          @tagFields[TAG_FIELD_SYSTEM      ].set("Type: ",        "QOA")
          @tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ",    qoa.channels.to_s)
          @tagFields[TAG_FIELD_CREATOR     ].set("Resampling: ",  (Benben.config.sampleRate == qoa.sampleRate ? "No" : "Yes"))
        else
          RemiLib.log.warn("UI has QoaTagInfo, but the player does not have a QoaFile")
          @tagFields[TAG_FIELD_TRACK       ].set("File: ",       "")
          @tagFields[TAG_FIELD_GAME        ].set("Sample Rate: ",     "")
          @tagFields[TAG_FIELD_AUTHOR      ].set("Samples: ", "")
          @tagFields[TAG_FIELD_SYSTEM      ].set("Typ: ", "")
          @tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ",  "")
          @tagFields[TAG_FIELD_RELEASE_DATE].set("Resampling: ",  "")
        end
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          "N/A")

      in TagInfo, Nil
        @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        "")
        @tagFields[TAG_FIELD_GAME        ].set("Game: ",         "")
        @tagFields[TAG_FIELD_AUTHOR      ].set("Author: ",       "")
        @tagFields[TAG_FIELD_SYSTEM      ].set("System: ",       "")
        @tagFields[TAG_FIELD_RELEASE_DATE].set("Release Date: ", "")
        @tagFields[TAG_FIELD_CREATOR     ].set("VGM Creator: ",  "")
        @tagFields[TAG_FIELD_RVA         ].set("RVA: ",          "N/A")
      end
    end

    # Toggles the displayed GD3 tag language.
    private def toggleGD3Language : Nil
      if (tag = @tag).is_a?(VgmTagInfo)
        case @gd3TagLang
        when .japanese?
          if tag.title.empty? && tag.album.empty? &&
             tag.artist.empty? && tag.systemEn.empty?
            # Just stay on Japanese, no information in the English tag.
            @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        tag.titleJp)
            @tagFields[TAG_FIELD_GAME        ].set("Game: ",         tag.gameJp)
            @tagFields[TAG_FIELD_AUTHOR      ].set("Author: ",       tag.artistJp)
            @tagFields[TAG_FIELD_SYSTEM      ].set("System: ",       tag.systemJp)
            @gd3TagLang = Gd3TagLang::Japanese
          else
            # Switch
            @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        tag.title)
            @tagFields[TAG_FIELD_GAME        ].set("Game: ",         tag.album)
            @tagFields[TAG_FIELD_AUTHOR      ].set("Author: ",       tag.artist)
            @tagFields[TAG_FIELD_SYSTEM      ].set("System: ",       tag.systemEn)
            @gd3TagLang =  Gd3TagLang::English
          end
        when .english?
          if tag.titleJp.empty? && tag.gameJp.empty? &&
             tag.artistJp.empty? && tag.systemJp.empty?
            # Just stay on English, no information in the Japanese tag.
            @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        tag.title)
            @tagFields[TAG_FIELD_GAME        ].set("Game: ",         tag.album)
            @tagFields[TAG_FIELD_AUTHOR      ].set("Author: ",       tag.artist)
            @tagFields[TAG_FIELD_SYSTEM      ].set("System: ",       tag.systemEn)
            @gd3TagLang = Gd3TagLang::English
          else
            # Switch
            @tagFields[TAG_FIELD_TRACK       ].set("Title: ",        tag.titleJp)
            @tagFields[TAG_FIELD_GAME        ].set("Game: ",         tag.gameJp)
            @tagFields[TAG_FIELD_AUTHOR      ].set("Author: ",       tag.artistJp)
            @tagFields[TAG_FIELD_SYSTEM      ].set("System: ",       tag.systemJp)
            @gd3TagLang = Gd3TagLang::Japanese
          end
        else raise "UI's current GD3 tag language is unexpected"
        end
      end
    end

    # Displays the tag information.
    private def updateTagDisplay(force : Bool = false) : Nil
      half = @lastWidth.tdiv(2)
      @tagFields[TAG_FIELD_TRACK].move(0, @mainAreaStartRow)
      @tagFields[TAG_FIELD_GAME].move(half, @mainAreaStartRow)
      @tagFields[TAG_FIELD_AUTHOR].move(0, @mainAreaStartRow + 1)
      @tagFields[TAG_FIELD_SYSTEM].move(half, @mainAreaStartRow + 1)
      @tagFields[TAG_FIELD_RELEASE_DATE].move(0, @mainAreaStartRow + 2)
      @tagFields[TAG_FIELD_CREATOR].move(half, @mainAreaStartRow + 2)
      @tagFields[TAG_FIELD_RVA].move(0, @mainAreaStartRow + 3)
      @tagFields[TAG_FIELD_MODE].move(half, @mainAreaStartRow + 3)

      unless force
        @tagFields.each(&.update)
      else
        @tagFields.each(&.redraw)
      end
    end

    ###
    ### Info Line
    ###

    # Displays the Info Line
    private def updateInfoLine : Nil
      @infoLine = Benben.player.infoLine
      SLScreen.goto(0, @mainAreaStartRow + 4)
      SLScreen.clearEol
      OrigUI.theme.writeField(INFO_LINE_HEADER, Utils.clipLine(@infoLine))
    end

    ###
    ### Mode Field
    ###

    private def updateModeField : Nil
      unless @mode == @lastMode
        @tagFields[TAG_FIELD_MODE].set("State: ", (case @mode
                                                   in .playing?
                                                     "Playing"
                                                   in .paused?
                                                     "Paused"
                                                   in .playing_stop_after_current?
                                                     "Playing (stopping after current)"
                                                   in .paused_stop_after_current?
                                                     "Paused (stopping after current)"
                                                   in .undefined?
                                                     "Undefined"
                                                   end))
        @tagFields[TAG_FIELD_MODE].redraw
        @lastMode = @mode
      end
    end

    ###
    ### Progress Bar
    ###

    # Updates the information that is on the same line as the progress bar.
    private def updateProgressBarInfo : Nil
      @progressBarLabel = if @maxLoops < 0
                            sprintf("[%c%c%c%c%c] %i/%i, %i of *",
                                    @eqEnabled ? 'E' : 'e',
                                    @stereoEnhancerEnabled ? 'S' : 's',
                                    @softClippingEnabled ? 'C' : 'c',
                                    @reverbEnabled ? 'R' : 'r',
                                    @hasSongConfig ? '+' : '-',
                                    @curTrack + 1u32,
                                    @totalTracks,
                                    @currentLoop + 1u32)
                          elsif @currentLoop > @maxLoops
                            sprintf("[%c%c%c%c%c] %i/%i, %i of %i",
                                    @eqEnabled ? 'E' : 'e',
                                    @stereoEnhancerEnabled ? 'S' : 's',
                                    @softClippingEnabled ? 'C' : 'c',
                                    @reverbEnabled ? 'R' : 'r',
                                    @hasSongConfig ? '+' : '-',
                                    @curTrack + 1u32,
                                    @totalTracks,
                                    @maxLoops + 1,
                                    @maxLoops + 1)
                          else
                            sprintf("[%c%c%c%c%c] %i/%i, %i of %i",
                                    @eqEnabled ? 'E' : 'e',
                                    @stereoEnhancerEnabled ? 'S' : 's',
                                    @softClippingEnabled ? 'C' : 'c',
                                    @reverbEnabled ? 'R' : 'r',
                                    @hasSongConfig ? '+' : '-',
                                    @curTrack + 1u32,
                                    @totalTracks,
                                    @currentLoop + 1u32,
                                    @maxLoops + 1)
                          end
      @progressBarLabelWidth = @progressBarLabel.size
    end

    # This is based on the progress bar in RemiLib, but is leaner and more
    # suited for music playback.
    private def updateProgress : Nil
      SLScreen.goto(0, @mainAreaStartRow + 7)
      SLScreen.clearEol

      conWidth = @lastWidth
      return if conWidth < 10
      width = conWidth.to_i32 - 1

      # The -5 is to account for the percent display.  The -2 is to account for
      # the two vertical bars.  The -1 is to account for a space between the
      # label and bar.
      #
      # -3 to account for the [, ], and space for the time display.
      barWidth = width - @progressBarLabelWidth - 5 - 2 - 1 -
                 @progressBarCurTime.size - @progressBarTotalTime.size - 3 - 1

      # Write label
      SLScreen.write("#{@progressBarLabel}: ")

      # Write bar
      percent : Float32 = Math.max(0.0f32, (@progressPos.to_f32! / @progressMax.to_f32!) * 100_f32)
      percent = 100_f32 if percent > 100
      progress : Int32 = Math.max(0, ((percent * barWidth) / 100_f32).to_i32!)

      SLScreen.write('|')
      OrigUI.theme.resetBarColor
      progress.times do |i|
        SLScreen.write(OrigUI.theme.pbarChar, color: OrigUI.theme.getBarColor(progress))
      end
      SLScreen.defaultColor
      SLScreen.write("#{OrigUI.theme.pbarSpaceChar * (barWidth - progress)}|")

      # Write percent
      SLScreen.write("#{sprintf("%4i", percent.to_i)}%")

      # Write time
      SLScreen.write(" [#{@progressBarCurTime}/#{@progressBarTotalTime}]")
    end

    ###
    ### VU meters
    ###

    # Initializes the VU meters
    protected def initVu : Nil
      @vu = VUMeter.new(@mainAreaStartRow + 5, Benben.theme)
    end

    ###
    ### Volume
    ###

    # Updates the volume display.
    private def updateVolume : Nil
      xoffset : Int32 = @lastWidth - 10
      yoffset : Int32 = @lastHeight - 1
      SLScreen.write(xoffset, yoffset, sprintf("Vol: %.2f", @mainVolume))
      @lastShownVolume = @mainVolume
    end

    ###
    ### Volume
    ###

    # Updates the CPU usage display.
    @[AlwaysInline]
    private def updateCpu : Nil
      xoffset : Int32 = @lastWidth - 25
      yoffset : Int32 = @lastHeight - 1

      {% if flag?(:linux) %}
        SLScreen.write(xoffset, yoffset, sprintf("%-12s", sprintf("CPU: %.1f%%", Utils.getCpu)))
      {% else %}
        SLScreen.write(xoffset, yoffset, "CPU: ?")
      {% end %}
    end

    ###
    ### Play Queue Time Handling
    ###

    # Calculates the total time of the song queue, returning the length of time
    # as a `Time::Span`.  This method is thread safe.
    private def calcTotalTime : Time::Span
      @totalTimeMut.synchronize do
        # Determine total time
        newTotal = Time::Span.new
        Benben.fileHandler.each do |file|
          newTotal += file.timeLength
          Fiber.yield
        end

        Benben.dlog!("Total play queue time calculated: #{newTotal}")
        newTotal
      end
    end

    # Gets the string that will display the total amount of time.  The value is
    # memoized, but if *force* is `true`, then it is always recalculated.
    private def totalTimeStr(force : Bool = false) : String
      if force || @totalTime.nil?
        newTime = calcTotalTime
        @totalTime = newTime

        @totalTimeStr = if newTime.hours > 0
                          sprintf("Total time: %02i:%02i:%02i", newTime.total_hours, newTime.minutes, newTime.seconds)
                        else
                          sprintf("Total time: %02i:%02i", newTime.total_minutes, newTime.seconds)
                        end
      end

      @totalTimeStr
    end

    ###
    ### Song Queue
    ###

    @[AlwaysInline]
    private def drawSongQueueName(offset : Int, name : String, isCurrent : Bool = false) : Nil
      maxWidth = @lastWidth - 6
      if isCurrent
        SLScreen.write(1, offset, "> ")
      else
        SLScreen.write(1, offset, "  ")
      end
      SLScreen.write(3, offset, Utils.clipField(name, maxWidth))
    end

    private def updateSongQueue : Nil
      numTracks : Int32 = Benben.fileHandler.size
      offset : Int32 = @mainAreaStartRow + 8
      boxStart : Int32 = offset
      maxWidth : Int32 = @lastWidth - 4
      boxHeight : UInt32 = Math.max(@lastHeight - offset - 2, 6).to_u32

      # Prepare the drawing area
      OrigUI.theme.withSongQueueBoxColors do
        SLScreen.drawBox(0, offset, @lastWidth.to_u32!, boxHeight)
        SLScreen.fill(1, offset + 1, maxWidth.to_u32!, boxHeight - 2, ' ')
      end
      OrigUI.theme.withSongQueueHeaderColors do
        SLScreen.write(2, offset, "Song Queue")
        timeStr = totalTimeStr
        SLScreen.write((@lastWidth - timeStr.size - 2), offset + boxHeight - 1, timeStr)
      end

      # Start drawing on the line immediately after the top of the box.
      offset += 1

      case Benben.fileHandler.size
      when 0
        # Consistency check
        raise "How did Benben start?"

      when 1
        # Simple case: 1 track
        curName : String = Benben.fileHandler.currentFileBasename
        OrigUI.theme.startCurSongColor
        drawSongQueueName(offset, curName, true)

      else
        # 2 or more tracks.
        curName = Benben.fileHandler.currentFileBasename

        if @curTrack == 0
          # We're starting on the very first track in the queue.
          OrigUI.theme.startCurSongColor
          drawSongQueueName(offset, curName, true)
        else
          # We're past the first track of the queue, so draw the previous track,
          # advance to the next line, then draw the current track.
          otherTrack : String = Benben.fileHandler.getFileBasename(@curTrack - 1)
          OrigUI.theme.startPrevSongColor
          drawSongQueueName(offset, otherTrack)
          offset += 1
          OrigUI.theme.startCurSongColor
          drawSongQueueName(offset, curName, true)
        end

        # Advance to the next line, then draw all remaining tracks.
        offset += 1
        trackNum : UInt32 = @curTrack + 1

        colorNum : Int32 = 0
        unless trackNum >= numTracks
          (trackNum...numTracks).each do |idx|
            OrigUI.theme.startNextSongColor(colorNum)
            otherTrack = Benben.fileHandler.getFileBasename(idx)
            drawSongQueueName(offset, otherTrack)
            offset += 1
            break if offset == (boxStart + boxHeight - 1)
            if (colorNum += 1) == OrigUI.theme.numNextSongColors
              colorNum -= 1
            end
          end
        end
      end
    ensure
      SLScreen.defaultColor
    end

    ###
    ### State Updates
    ###

    # Updates the internal state by getting information from the player and
    # other places.
    private def updateState : Nil
      showUpdateMsgs : Bool = @initialUpdateFinished.lazyGet
      updatePbarInfo : Bool = false

      # Check to see if we've switched to a new song.
      if !(newFile = Benben.player.curFile).same?(@playingFile)
        # Update the tag information since we have a new song.
        if @playingFile = newFile
          @tag = newFile.taginfo
        else
          @tag = nil
        end
        updateTagFields

        # Stop showing the help
        @helpTimeout = nil

        # Update the current track number.
        newI32 = Benben.fileHandler.currentTrack.get
        if newI32 >= 0
          @curTrack = newI32.to_u32!
          updateSongQueue
          updatePbarInfo = true
        end

        # Reset the VU's clips
        vu.resetClips

        # Set mode to Playing
        @lastMode = @mode
        @mode = Mode::Playing
      end

      if (newProgressMax = Benben.player.totalSamples.get) != @progressMax
        # Update the maximum value of the progress bar.
        @progressMax = newProgressMax
        ts = Time::Span.new(nanoseconds: (@progressMax * @invSampleRate).to_i64! * 1000000000)
        @progressBarTotalTime = sprintf("%02i:%02i", ts.total_minutes, ts.seconds)
        updatePbarInfo = true
      end

      # Maybe update the maximum play loops.
      if (newI32 = Benben.player.maxLoops) != @maxLoops
        if showUpdateMsgs
          if newI32 > @maxLoops
            # Loops increased
            if @maxLoops < 0
              queueMessage("Increased max loops, no longer looping indefinitely")
            else
              queueMessage("Increased max loops")
            end
          else
            # Loops decreased
            if newI32 < 0
              queueMessage("Decreased max loops, now looping indefinitely")
            else
              queueMessage("Decreased max loops")
            end
          end
        end

        @maxLoops = newI32
        updatePbarInfo = true
      end

      # Maybe update the current loop.
      if (newU32 = Benben.player.currentLoop) != @currentLoop
        @currentLoop = newU32
        updatePbarInfo = true
      end

      # Maybe update the total number of tracks.
      if (newU32 = Benben.fileHandler.size.to_u32) != @totalTracks
        @totalTracks = newU32
        updatePbarInfo = true
      end

      # Maybe update the progress bar's current position.
      if (newU64 = Benben.player.samplesPlayed.get) != @progressPos
        @progressPos = newU64
        ts = Time::Span.new(nanoseconds: (@progressPos * @invSampleRate).to_i64! * 1000000000)
        @progressBarCurTime = sprintf("%02i:%02i", ts.total_minutes, ts.seconds)
        updatePbarInfo = true
      end

      # Maybe update whether or not the EQ is enabled.
      if (newBool = Benben.player.eqEnabled?) != @eqEnabled
        @eqEnabled = newBool
        if showUpdateMsgs
          queueMessage("Equalizer #{@eqEnabled ? "enabled" : "disabled"}")
        end
        updatePbarInfo = true
      end

      # Maybe update whether or not soft clipping is enabled.
      if (newBool = Benben.player.softClippingEnabled?) != @softClippingEnabled
        @softClippingEnabled = newBool
        if showUpdateMsgs
          queueMessage("Soft clipping #{@softClippingEnabled ? "enabled" : "disabled"}")
        end
        updatePbarInfo = true
      end

      # Maybe update whether or not the stereo enhancer is enabled.
      if (newBool = Benben.player.stereoEnhancerEnabled?) != @stereoEnhancerEnabled
        @stereoEnhancerEnabled = newBool
        if showUpdateMsgs
          queueMessage("Stereo enhancer #{@stereoEnhancerEnabled ? "enabled" : "disabled"}")
        end
        updatePbarInfo = true
      end

      # Maybe update whether or not the reverb is enabled.
      if (newBool = Benben.player.reverbEnabled?) != @reverbEnabled
        if showUpdateMsgs
          if @reverbEnabled = newBool
            queueMessage("Reverb enabled") #sprintf("Reverb enabled, amount: %.2f", @reverb.amount))
          else
            queueMessage("Reverb disabled")
          end
        end
        updatePbarInfo = true
      end

      # KLUDGE workaround displaying the wrong volume until vol up/vol down is
      # pressed.
      updateVolume if @lastShownVolume != @mainVolume

      # Maybe change the GD3 tag language that's shown.
      if (newLang = Benben.config.vgm.preferredGd3Lang) && !newLang.togglable? && newLang != @gd3TagLang
        Benben.dlog!("Manually toggling the GD3 language")
        @gd3TagLang = newLang
        updateTagDisplay
      end

      # Toggle the GD3 language if needed.
      if Benben.config.vgm.preferredGd3Lang.togglable? &&
         (Time.local - @lastTagSwap).total_seconds > GD3_TOGGLE_INTERVAL
        toggleGD3Language
        updateTagDisplay
        @lastTagSwap = Time.local
      end

      # Update info line
      if (newInfoLine = Benben.player.infoLine) && @infoLine != newInfoLine
        @infoLine = newInfoLine
        updateInfoLine
      end

      # If anything above wanted to update the progress bar display, do so now.
      updateProgressBarInfo if updatePbarInfo

      # This is always reset to true.
      @initialUpdateFinished.set(true)
    end

    ###
    ### Other Initializations
    ###

    # Initializes the colors.
    protected def initColors : Nil
      if SLScreen.hasAnsiColors?
        OrigUI.theme.apply
        Banner.initColors
      end
    end

    ###
    ### Entry point and updating
    ###

    # Updates the screen.
    @[AlwaysInline]
    protected def update : Nil
      updateState
      if @banner.done?
        @banner.animateFadeDown
      else
        @banner.refreshBanner
      end
      @banner.refreshBanner unless @banner.done?
      @banner.refreshBorderLines if @animations
      vu.refresh
      updateProgress
      updateMessages
      updateHelp
      updateTagDisplay
      updateModeField

      if (Time.monotonic - @cpuLastUpdated).total_milliseconds >= CPU_USAGE_UPDATE_FREQ
        updateCpu
        @cpuLastUpdated = Time.monotonic
      end

      SLScreen.refresh
    end

    # Purposely unhygenic macro because Crystal has no macrolet
    private macro doFps
      Fiber.yield
      sleep (FPS_CAP - (Time.monotonic - lastUpdate).total_milliseconds).milliseconds
      lastUpdate = Time.monotonic
    end

    @[AlwaysInline]
    private def drawVersionLine : Nil
      SLScreen.write(0, @lastHeight - 1, "v#{VERSION} - Press 'q' to quit, 'h' for help")
    end

    def redrawWholeScreen : Nil
      @helpTimeout = nil
      LibSLang.SLsmg_init_smg()
      RemiSlang::Screen.updateScreenSize
      SLScreen.clear
      updateTagFields
      updateInfoLine
      updateTagDisplay(true)
      vu.refresh
      updateVolume
      updateCpu
      updateProgressBarInfo
      @banner.refreshBanner(true)
      @banner.refreshBorderLines(true)
      drawVersionLine
      updateSongQueue
      updateProgress
      updateMessages
      updateModeField
    end

    def queueRedrawWholeScreen : Nil
      Benben.manager.sendToAllKeyChans(KeyCommand::ForceRedraw)
    end

    private def startInputLoop : Nil
      Benben.dlog!("Input loop fiber is starting")
      #STDIN.read_timeout = 500.milliseconds
      spawn do
        Benben.dlog!("Input loop is starting")
        #keyBuf : Bytes = Bytes.new(1)
        #haveEscape : Bool = false
        #escaped : UInt8 = 0u8

        until Benben.manager.state.get.quit?
          begin
            #STDIN.raw &.read(keyBuf)

            # case keyBuf[0]
            # when '\e'.ord
            #   haveEscape = true

            # when 26 # Emulate Ctrl+Z
            #   haveEscape = false
            #   Benben.dlog!("Suspending")
            #   Process.signal(Signal::TSTP, Process.pid)

            # when '['.ord
            #   if haveEscape
            #     escaped = keyBuf[0]
            #   else
            #     dispatchKey(keyBuf[0].chr)
            #   end

            # else
            #   # Normal key, send it.
            #   if haveEscape
            #     dispatchKey(keyBuf[0].chr, escaped.chr)
            #   else
            #     dispatchKey(keyBuf[0].chr)
            #   end
            #   haveEscape = false
            # end

            # Remi: This code does not like Ctrl-Z.  When this code is used
            # and Ctrl-Z happens, then all further input requires the enter
            # key to be pressed after each stroke.  So don't use it.
            #
            if key = RemiSlang::Input.getKey?(-50)
              case key.ord
              when 26 # Emulate Ctrl+Z
                Benben.dlog!("Suspending")
                Process.signal(Signal::TSTP, Process.pid)
              else
              # Normal key, send it.
                dispatchKey(key)
              end
            end
          rescue IO::TimeoutError
            nil
          rescue err : Exception
            Benben.dlog!("Input loop had an exception: #{err} (#{err.backtrace})")
          end

          Fiber.yield
        end

        begin
          Benben.dlog!("Input loop has finished")
        rescue err : Exception
          Benben.manager.submitError(BenbenError.new("Fatal error in input loop while exiting: #{err}"))
        end
      end
    end

    private def dispatchKey(c : Char, escaped : Char? = nil) : Nil
      {% begin %}
        Benben.dlog!("Dispatching key: #{c} (#{c.ord}), escape: #{escaped} (#{escaped.try(&.ord)||0})")
        case c
        when ' ' then Benben.manager.sendToAllKeyChans(KeyCommand::Pause)
        when 'n' then Benben.manager.sendToAllKeyChans(KeyCommand::Next)
        when 'p' then Benben.manager.sendToAllKeyChans(KeyCommand::Previous)
        when '>' then Benben.manager.sendToAllKeyChans(KeyCommand::SeekForward)
        when '<' then Benben.manager.sendToAllKeyChans(KeyCommand::SeekBackward)
        when 'a' then Benben.manager.sendToAllKeyChans(KeyCommand::VolUp)
        when 'z' then Benben.manager.sendToAllKeyChans(KeyCommand::VolDown)
        when 'e' then Benben.manager.sendToAllKeyChans(KeyCommand::EqToggle)
        when 'c' then Benben.manager.sendToAllKeyChans(KeyCommand::SoftClipToggle)
        when 's' then Benben.manager.sendToAllKeyChans(KeyCommand::StereoEnhanceToggle)
        when 'r' then Benben.manager.sendToAllKeyChans(KeyCommand::ReverbToggle)
        when 't' then Benben.manager.sendToAllKeyChans(KeyCommand::LangToggle)
        when ']' then Benben.manager.sendToAllKeyChans(KeyCommand::LoopUp)
        when '[' then Benben.manager.sendToAllKeyChans(KeyCommand::LoopDown)
        when 'S' then Benben.manager.sendToAllKeyChans(KeyCommand::StopAfterCurrent)
        when 'C' then Benben.manager.sendToAllKeyChans(KeyCommand::ReloadSongConfigs)
        when 'i' then Benben.manager.sendToAllKeyChans(KeyCommand::ToggleInterpolation)
        when 'x' then Benben.manager.sendToAllKeyChans(KeyCommand::ToggleChorus)
        when 'R' then Benben.manager.sendToAllKeyChans(KeyCommand::ForceRedraw)
        when 'q' then Benben.manager.sendToAllKeyChans(KeyCommand::Exit)
        when 'h' then Benben.manager.sendToAllKeyChans(KeyCommand::Help)

        {% if flag?(:benben_manual_gc) %}
        when 'G' then GC.collect
        {% end %}
        end
      {% end %}
    end

    # Runs the main UI loop.
    def run : Nil
      Benben.dlog!("Starting Original UI")
      raise "Original UI started twice" if @running.lazyGet
      @running.set(true)
      lastUpdate : Time::Span = Time.monotonic

      # Initialize S-Lang
      @uiLock.synchronize do
        # Initialize some basic things.  Order here is important.
        LibSLang.slang_tt_read_fd = STDIN.fd
        Slang.init
        SLScreen.clear
        SLScreen.hideCursor
        SLScreen.updateScreenSize
        @lastWidth, @lastHeight = SLScreen.size

        startInputLoop
        initColors
        @banner.initOffsets
        initVu
        drawVersionLine

        # Immediately draw the banner if animations are disabled
        @banner.refreshBanner(true) unless @animations
      end

      # Start the loop.
      Benben.dlog!("Original UI main loop starting")
      width : Int32 = 0
      height : Int32 = 0
      while @running.lazyGet
        # Update the screen
        @uiLock.synchronize do
          width, height = SLScreen.size
          if width < 80 || height < 24
            SLScreen.clear
            SLScreen.write(0, 0, "Terminal too small (#{width}x#{height}), 80x24 or larger needed")
            SLScreen.refresh
            doFps
            next
          elsif width != @lastWidth || height != @lastHeight
            @lastWidth = width
            @lastHeight = height
            Benben.dlog!("Redrawing entire screen, new size is #{@lastWidth}x#{@lastHeight}")
            redrawWholeScreen
          end

          update
        end

        # Check for commands.
        select
        when cmd = @keyChan.receive
          case cmd
          when .exit?
            Benben.dlog!("Original UI has received an exit command")
            @running.set(false)
          when .help?
            queueHelp

          when .pause?
            @lastMode = @mode
            @mode = case @mode
                    when .playing? then Mode::Paused
                    when .playing_stop_after_current? then Mode::PausedStopAfterCurrent
                    when .paused? then Mode::Playing
                    when .paused_stop_after_current? then Mode::PlayingStopAfterCurrent
                    else Mode::Undefined
                    end

          when .lang_toggle?
            Benben.dlog!("UI is telling the settings to toggle the GD3 language")
            Benben.config.vgm.preferredGd3LangToggle

          when .vol_up?
            newVol = (@mainVolume + VOL_STEP).clamp(MIN_VOL, MAX_VOL)
            Benben.dlog!("Main volume changed from #{@mainVolume} to #{newVol}")
            @mainVolume = newVol
            vu.resetClips
            updateVolume

          when .vol_down?
            newVol = (@mainVolume - VOL_STEP).clamp(MIN_VOL, MAX_VOL)
            Benben.dlog!("Main volume changed from #{@mainVolume} to #{newVol}")
            @mainVolume = newVol
            vu.resetClips
            updateVolume

          when .reload_song_configs?
            queueMessage("Reloading song configs")
            Benben.config.loadSongConfigs

          when .force_redraw?
            Benben.dlog!("Redrawing entire screen due to request")
            redrawWholeScreen

          when .stop_after_current?
            @stopAfterCurrent = !@stopAfterCurrent
            if @initialUpdateFinished.get
              if @stopAfterCurrent
                queueMessage("Stopping after current song")
              else
                queueMessage("Not stopping after current song")
              end
            end

            @lastMode = @mode
            @mode = case @mode
                    when .playing? then Mode::PlayingStopAfterCurrent
                    when .paused? then Mode::PausedStopAfterCurrent
                    when .playing_stop_after_current? then Mode::Playing
                    when .paused_stop_after_current? then Mode::Paused
                    else Mode::Undefined
                    end


          else Benben.dlog!("Original UI is ignoring command: #{cmd}")
          end # case cmd
        else nil
        end # select

        # Sleep
        doFps
      end

      Benben.dlog!("Original UI has quit")
    end

    # Shuts down the UI.
    def deinit : Nil
      Benben.dlog!("Running Original UI deinit")
      @running.set(false)
      Slang.deinit
      Benben.dlog!("Original UI has been deinitialized")
    end
  end
end