Login
Artifact [35c6fbe740]
Login

Artifact 35c6fbe740b42b99f3ea91d0cda4773dc6fef95943f26f74a439acde7beedf6d:


#### RemiCharms
#### Based on CL-Charms
#### Copyright (C) 2023 Remilia Scarlet <remilia@posteo.jp>
#### Copyright (c) 2014 Robert Smith <quad@symbo1ics.com>
####
#### 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/>.

module RemiCharms
  # A high-level representation of a curses window.
  #
  # You can still call the lower-level bindings by using the `#pointer` field
  # where necessary.
  class Window
    @@standardWindow : Window?
    @@cachedWindow : Window = Window.new

    # Pointer to the underlying representation of a window pointer.  You can use
    # this in calls to the lower-level bindings.
    getter pointer : NCurses::WindowPtr

    protected def initialize
      @pointer = NCurses::WindowPtr.null
    end

    protected def initialize(@pointer : NCurses::WindowPtr)
    end

    # Creates a new `Window` with the given *width* and *height*, starting at
    # the coordinate (*startX*, *startY*).
    #
    # Note that windows may not overlap.
    def initialize(width : Int, height : Int, startX : Int, startY : Int)
      # FIXME: The behavior of this function is special if WIDTH or HEIGHT are
      # 0. Either document this behavior or disallow it.
      #
      # We should check if this window overlaps with another, and appropriately
      # warn.
      @pointer = NCurses.newwin(height, width, startY, startX)
      if @pointer.null?
        raise Error.new("Failed to allocate a new window")
      end
    end

    # Cleans up the internal pointer if `#destroy` hasn't yet been called when
    # this instance is garbage collected.  If `#destroy` has been called, this
    # does nothing.
    #
    # See
    # [https://crystal-lang.org/reference/1.9/syntax_and_semantics/finalize.html]()
    # for more details
    def finalize
      unless @pointer.null?
        RemiCharms.checkStatus(NCurses.delwin(@pointer))
        @pointer = NCurses::WindowPtr.null
      end
    end

    protected def pointer=(@pointer : NCurses::WindowPtr) : Nil
    end

    # Destroys this window.
    def destroy : Nil
      RemiCharms.checkStatus(NCurses.delwin(@pointer))
      @pointer = NCurses::WindowPtr.null
    end

    # Creates a copy of this window.
    def dup : Window
      newPtr = NCurses.dupwin(@pointer)
      if newPtr.null?
        raise Error.new("Failed to copy the window")
      else
        Window.new(newPtr)
      end
    end

    # Returns the width and height of this window, in that order.
    @[AlwaysInline]
    def size : Tuple(Int32, Int32)
      y, x = NCurses.getmaxyx(@pointer)
      {x, y}
    end

    # Returns the `stdscr` in curses.
    def self.standardWindow : Window
      if @@standardWindow.nil?
        stdscr = LibNCurses.stdscr

        # Update the cached window if necessary.
        unless stdscr.address == @@cachedWindow.pointer.address
          if stdscr.null?
            raise Error.new("No standard window exists.  Did you initialize correctly?")
          else
            @@cachedWindow.pointer = stdscr
          end
        end

        @@standardWindow = @@cachedWindow
      end

      @@standardWindow.not_nil!
    end

    # Refresh the display of this window.
    def refresh : Nil
      RemiCharms.checkStatus(NCurses.wrefresh(@pointer))
    end

    # Forces the entire window to be cleared and repainted on the next call to
    # `#refresh`.
    def forceRepaint : Nil
      RemiCharms.checkStatus(NCurses.clearok(@pointer, NCurses::TRUE))
    end

    # Blank out the contents of this window. If *repaint* is `true`, then the
    # window will be repainted entirely in the next refresh.  Using this option
    # can be more optimally performant than calling `#forceRepaint` manually.
    def clear(repaint : Bool = false) : Nil
      if repaint
        RemiCharms.checkStatus(NCurses.wclear(@pointer))
      else
        RemiCharms.checkStatus(NCurses.werase(@pointer))
      end
    end

    # Clear the rest of the window after the cursor.
    def clearAfterCursor : Nil
      # XXX: Man page says "returns an error if the cursor position is about to
      # wrap
      RemiCharms.checkStatus(NCurses.wclrtobot(@pointer))
    end

    # Clear the rest of the line after the cursor.
    def clearLineAfterCursor : Nil
      RemiCharms.checkStatus(NCurses.wclrtoeol(@pointer))
    end

    # Returns the character at the cursor in this window.
    def charAt : Char
      NCurses.winch(@pointer).chr
    end

    # Returns the character at the point (*x*, *y*) in this window.
    def charAt(x : Int, y : Int) : Char
      NCurses.mvwinch(@pointer, y, x).chr
    end

    # Get a character from this window.  In the event a character is not ready
    # or could not be returned, this will raise an `Error` if *ignoreError* is
    # `false`, or return `nil` otherwise.
    @[AlwaysInline]
    def getChar(ignoreError : Bool = false) : Char?
      ch = NCurses.wgetch(@pointer)
      case
      when ch != NCurses::ERR then ch.chr
      when ignoreError then nil
      else raise Error.new("Error getting character")
      end
    end

    # Same as `#getChar`, but the *ignoreError* parameter for it is always `true`.
    def getChar! : Char?
      getChar(true)
    end

    # Insert the character *ch* at the cursor within this window, advancing the
    # rest of the line, without moving the cursor.  This is akin to pressing the
    # 'insert' key and typing a character.
    def insert(ch : Char) : Nil
      RemiCharms.checkStatus(NCurses.winsch(@pointer, ch.ord))
    end

    # Insert the character *ch* at the coordinates (*x*, *y*) within this
    # window, advancing the rest of the line, without moving the cursor.  This
    # is akin to pressing the 'insert' key and typing a character.
    def insert(ch : Char, x : Int, y : Int) : Nil
      RemiCharms.checkStatus(NCurses.winsch(@pointer, ch.ord))
    end

    # Returns `true` if the coordinate (*x*, *y*) is at the last position within
    # this window, or `false` otherwise.
    def lastPos?(x : Int, y : Int) : Bool
      width, height = size
      x == width - 1 && y == height - 1
    end

    # Writes *ch* at the last position of this window.  This assumes the width
    # of the window is at least 2.
    def writeAtLastPos(ch : Char) : Nil
      width, height = size
      insert(ch, width - 1, height - 1)
    end

    # Writes the character *ch* in this window at the cursor.
    @[AlwaysInline]
    def write(ch : Char) : Nil
      x, y = cursorPos
      if lastPos?(x, y)
        writeAtLastPos(ch)
      else
        RemiCharms.checkStatus(NCurses.waddch(@pointer, ch.ord))
      end
    end

    # Writes the string *str* in this window at the cursor.
    def write(str : String) : Nil
      RemiCharms.checkStatus(NCurses.waddstr(@pointer, str.to_unsafe))
    end

    # Writes the character *ch* in this window at the coordinates (*x*, *y*).
    @[AlwaysInline]
    def write(ch : Char, x : Int, y : Int) : Nil
      if lastPos?(x, y)
        writeAtLastPos(ch)
      else
        RemiCharms.checkStatus(NCurses.mvwaddch(@pointer, y, x, ch.ord))
      end
    end

    # Writes the string *str* in this window at the coordinates (*x*, *y*).
    def write(str : String, x : Int, y : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwaddstr(@pointer, y, x, str.to_unsafe))
    end

    def write(ch : Acs) : Nil
      RemiCharms.checkStatus(NCurses.waddch(@pointer, ch.fromMap))
    end

    def write(ch : Acs, x : Int, y : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwaddch(@pointer, y, x, ch.fromMap))
    end

    # Returns the X and Y coordinates of the cursor in this window, in that
    # order.
    @[AlwaysInline]
    def cursorPos : Tuple(Int32, Int32)
      y, x = NCurses.getyx(@pointer)
      {x, y}
    end

    # Moves the cursor in this window to the coordinates (*x*, *y*).
    def moveCursor(x : Int, y : Int) : Nil
      RemiCharms.checkStatus(NCurses.wmove(@pointer, y, x))
    end

    # Records the current cursor position in this window, then yields.  This
    # will then ensure that the cursor is restored to its original position
    # before calling this function.
    def withRestoredCursor(&) : Nil
      oldX, oldY = cursorPos
      yield
      moveCursor(oldX, oldY)
    end

    # Moves the cursor in this window up by *delta* characters.  A negative
    # *delta* moves the cursor down instead.
    @[AlwaysInline]
    def cursorUp(delta : Int32 = 1) : Nil
      x, y = cursorPos
      moveCursor(x, Math.max(0, y - delta))
    end

    # Moves the cursor in this window down by *delta* characters.  A negative
    # *delta* moves the cursor up instead.
    @[AlwaysInline]
    def cursorDown(delta : Int32 = 1) : Nil
      x, y = cursorPos
      moveCursor(x, Math.max(0, y + delta)) # Remi: 0?
    end

    # Moves the cursor in this window left by *delta* characters.  A negative
    # *delta* moves the cursor right instead.
    @[AlwaysInline]
    def cursorLeft(delta : Int32 = 1) : Nil
      x, y = cursorPos
      moveCursor(Math.max(0, x - delta), y)
    end

    # Moves the cursor in this window right by *delta* characters.  A negative
    # *delta* moves the cursor left instead.
    @[AlwaysInline]
    def cursorRight(delta : Int32 = 1) : Nil
      x, y = cursorPos
      moveCursor(Math.max(0, x + delta), y) # Remi: 0?
    end

    # Enable or disable extra keys, such as arrow and function keys, in this
    # window.
    def extraKeys=(value : Bool) : Nil
      if value
        RemiCharms.checkStatus(NCurses.keypad(@pointer, NCurses::TRUE))
      else
        RemiCharms.checkStatus(NCurses.keypad(@pointer, NCurses::FALSE))
      end
    end

    # Enables or disables non-blocking mode for this window. When enabled, this
    # will cause character input functions to not block and error (or return
    # `nil`)."
    def nonBlocking=(value : Bool) : Nil
      if value
        RemiCharms.checkStatus(NCurses.nodelay(@pointer, NCurses::TRUE))
      else
        RemiCharms.checkStatus(NCurses.nodelay(@pointer, NCurses::FALSE))
      end
    end

    # Turn on the attributes for this window without affecting any others.
    def attributeOn(*attrs : Attribute|Color|Int16|UInt32) : Nil
      attributeOn(RemiCharms.combineAttrs(*attrs))
    end

    # :ditto:
    def attributeOn(attrs : Attribute|Color|Int16|UInt32) : Nil
      RemiCharms.checkStatus(NCurses.wattron(@pointer, RemiCharms.combineAttrs(attrs)))
    end

    # Turn off the attributes for this window without affecting any others.
    def attributeOff(*attrs : Attribute|Color|Int16|UInt32) : Nil
      attributeOff(RemiCharms.combineAttrs(*attrs))
    end

    # :ditto:
    def attributeOff(attrs : Attribute|Color|Int16|UInt32) : Nil
      RemiCharms.checkStatus(NCurses.wattroff(@pointer, RemiCharms.combineAttrs(attrs)))
    end

    # Temporarily turns on the given attributes for this window in the block,
    # then turns them off before returning.
    def withAttributes(attrs : Attribute|UInt32, &) : Nil
      attributeOn(attrs)
      yield
      attributeOff(attrs)
    end

    # Draws a border around this window using the default ACS characters.
    def border : Nil
      RemiCharms.checkStatus(NCurses.wborder(@pointer,
                                             Acs::Vline, Acs::Vline, # Left, right
                                             Acs::Hline, Acs::Hline, # Top, bottom
                                             Acs::Ulcorner, Acs::Urcorner,
                                             Acs::Llcorner, Acs::Lrcorner))
    end

    # Draws a border around this window using the given ACS characters.  The
    # same character is used for the top as the bottom, and the same for the
    # left as the right.
    @[AlwaysInline]
    def border(topAndBottom : Acs, leftAndRight, upperLeft : Acs, upperRight : Acs,
               lowerLeft : Acs, lowerRight : Acs) : Nil
      border(topAndBottom, topAndBottom, leftAndRight, leftAndRight,
             upperLeft, upperRight, lowerLeft, lowerRight)
    end

    # Draws a border around this window using the given ACS characters.
    def border(topSide : Acs, bottomSide : Acs, leftSide : Acs, rightSide : Acs,
               upperLeft : Acs, upperRight : Acs, lowerLeft : Acs, lowerRight : Acs) : Nil
      RemiCharms.checkStatus(NCurses.wborder(@pointer,
                                             leftSide, rightSide, topSide, bottomSide,
                                             upperLeft, upperRight, lowerLeft, lowerRight))
    end

    # Drawa a border around this window using the given character.  The same
    # character is used for the top as the bottom, and the same for the left as
    # the right.  The characters must be ASCII-based.
    @[AlwaysInline]
    def border(topAndBottom : Char, leftAndRight, upperLeft : Char, upperRight : Char,
               lowerLeft : Char, lowerRight : Char) : Nil
      border(topAndBottom, topAndBottom, leftAndRight, leftAndRight,
             upperLeft, upperRight, lowerLeft, lowerRight)
    end

    # Drawa a border around this window using the given characters.  The
    # characters must be ASCII-based.
    def border(topSide : Char, bottomSide : Char, leftSide : Char, rightSide : Char,
               upperLeft : Char, upperRight : Char, lowerLeft : Char, lowerRight : Char) : Nil
      unless topSide.ascii? && bottomSide.ascii? && leftSide.ascii? && rightSide.ascii? &&
             upperLeft.ascii? && upperRight.ascii? && lowerLeft.ascii? && lowerRight.ascii?
        raise Error.new("Only ASCII characters are accepted")
      end
      RemiCharms.checkStatus(NCurses.wborder(@pointer,
                                             leftSide.ord, rightSide.ord,
                                             topSide.ord, bottomSide.ord,
                                             upperLeft.ord, upperRight.ord,
                                             lowerLeft.ord, lowerRight.ord))
    end

    # Draws a horizontal line at the cursor that is *count* characters wide
    # using `Acs::Hline` for the character.
    def hline(count : Int) : Nil
      RemiCharms.checkStatus(NCurses.whline(@pointer, Acs::Hline, count))
    end

    # Draws a horizontal line at the coordinates that is *count* characters wide
    # using `Acs::Hline` for the character.
    def hline(x : Int, y : Int, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwhline(@pointer, y, x, Acs::Hline, count))
    end

    # Draws a vertical line at the cursor that is *count* characters wide
    # using `Acs::Vline` for the character.
    def vline(count : Int) : Nil
      RemiCharms.checkStatus(NCurses.wvline(@pointer, Acs::Vline, count))
    end

    # Draws a vertical line at the coordinates that is *count* characters wide
    # using `Acs::Vline` for the character.
    def vline(x : Int, y : Int, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwvline(@pointer, y, x, Acs::Vline, count))
    end

    # Draws a horizontal line at the cursor that is *count* characters wide
    # using the given ACS character.
    def hline(ch : Acs, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.whline(@pointer, ch, count))
    end

    # Draws a horizontal line at the coordinates that is *count* characters wide
    # using the given ACS character.
    def hline(ch : Acs, x : Int, y : Int, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwhline(@pointer, y, x, ch, count))
    end

    # Draws a vertical line at the cursor that is *count* characters wide
    # using the given ACS character.
    def vline(ch : Acs, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.wvline(@pointer, ch, count))
    end

    # Draws a vertical line at the coordinates that is *count* characters wide
    # using the given ACS character.
    def vline(ch : Acs, x : Int, y : Int, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwvline(@pointer, y, x, ch, count))
    end

    # Draws a horizontal line at the cursor that is *count* characters wide
    # using the given character.  The character must be ASCII-based.
    def hline(ch : Char, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.whline(@pointer, ch.ord, count))
    end

    # Draws a horizontal line at the coordinates that is *count* characters wide
    # using the given character.  The character must be ASCII-based.
    def hline(ch : Char, x : Int, y : Int, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwhline(@pointer, y, x, ch.ord, count))
    end

    # Draws a vertical line at the cursor that is *count* characters wide
    # using the given character.  The character must be ASCII-based.
    def vline(ch : Char, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.wvline(@pointer, ch.ord, count))
    end

    # Draws a vertical line at the coordinates that is *count* characters wide
    # using the given character.  The character must be ASCII-based.
    def vline(ch : Char, x : Int, y : Int, count : Int) : Nil
      RemiCharms.checkStatus(NCurses.mvwvline(@pointer, y, x, ch.ord, count))
    end

    # TODO: scrollok, idlok, idcok
  end
end