#### 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