#### 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/>.
require "./remicharms/*"
# RemiCharms is an interface to libcurses for Crystal. It provides both a raw,
# low-level interface to libcurses via C bindings, and a more higher-level
# interface.
#
# It is intended to be a bit easier to use than straight-up ncurses bindings.
# It is based on [cl-charms](https://github.com/HiTECNOLOGYs/cl-charms/), a
# similar interface to ncurses for Common Lisp.
#
# Currently, the low-level interface (the `NCurses` module) is pretty close to
# ncurses itself. The high-level interface can be used directly alongside the
# low-level interface for those instances where the high level interface is
# missing functionality.
module RemiCharms
# The current version of the library.
VERSION = "0.1.0"
# Defines how character input is handled by ncurses.
enum InputMode
# The default mode (that is, the mode that ncurses is in when it starts up).
Default
# Raw input mode.
Raw
# Raw mode, but with control characters like Ctrl-C still interpreted as
# usual.
CBreak
end
enum Visibility : Int32
Invisible = 0
Normal = 1
VeryVisible = 2
end
alias Acs = NCurses::Acs
alias Color = NCurses::Color
alias KeyCode = NCurses::KeyCode
alias Attribute = NCurses::Attribute
@@standardWindow : Window?
# The equivalent to `stdscr` in ncurses.
class_getter! standardWindow
@@inputMode : InputMode = InputMode::Default
# The current `InputMode`.
class_getter inputMode
# Initializes ncurses and the terminal for drawing. This then returns the
# standard window (i.e. `RemiCharms.standardWindow`).
#
# This function must be called before using curses functions. Consider using
# `RemiCharms.withCurses` instead to ensure proper initialization and cleanup.
def self.init : Window
stdscr = NCurses.initscr
raise Error.new("RemiCharms.init failed") if stdscr.null?
# Query the standard window the normal way.
win = Window.standardWindow
win.refresh
win
end
# Finalizes ncurses
#
# This function must be called before exiting. Consider using
# `RemiCharms.withCurses` instead to ensure proper initialization and cleanup.
def self.deinit : Nil
NCurses.endwin
end
# Flushes standard input, output, standard error, initializes ncurses via
# `RemiCharms.init`, then yields the standard window. This ensures
# `RemiCharms.deinit` is called before returning.
def self.withCurses(&) : Nil
STDIN.flush
STDOUT.flush
STDERR.flush
init
@@standardWindow = Window.standardWindow
yield @@standardWindow.not_nil!
ensure
deinit
end
# Enables or disables the echoing of input.
def self.echoing=(value : Bool) : Nil
if value
RemiCharms.checkStatus(NCurses.echo)
else
RemiCharms.checkStatus(NCurses.noecho)
end
end
# Changes the input mode. See `RemiCharms.enableRawInput` and
# `RemiCharms.disableRawInput` for more information.
def self.inputMode=(mode : InputMode) : Nil
case mode
in .default? then disableRawInput
in .raw? then enableRawInput(false)
in .c_break? then enableRawInput(true)
end
end
# Enables raw input mode. This disables line buffering and will make
# characters available as soon as they're typed.
#
# If *interpretControlChars* is `true`, then control characters like Ctrl-C
# will be interpreted as usual.
def self.enableRawInput(interpretControlChars : Bool = false) : Nil
# Ensure mutual exclusion of Raw and CBreak
disableRawInput
# Enable and remember the mode
if interpretControlChars
checkStatus(NCurses.cbreak)
@@inputMode = InputMode::CBreak
else
checkStatus(NCurses.raw)
@@inputMode = InputMode::Raw
end
end
# Disables raw input mode. This undoes the action of
# `RemiCharms.enableRawInput`.
def self.disableRawInput : Nil
case @@inputMode
in .default?
checkStatus(NCurses.nocbreak)
checkStatus(NCurses.noraw)
in .raw?
checkStatus(NCurses.noraw)
in .c_break?
checkStatus(NCurses.nocbreak)
end
end
# Audibly beep to alert the user.
def self.beep : Nil
NCurses.beep
end
# Visually flash the console.
def self.flash : Nil
NCurses.flash
end
# Return a string representing the version of the underlying curses
# implementation.
def self.cursesVersion : String
data = NCurses.curses_version
if data.null?
""
else
String.new(data)
end
end
# Sets the visibility of the cursor.
def self.cursorVisibility=(vis : Visibility) : Nil
checkStatus(NCurses.curs_set(vis.value))
end
# Attempts to enable color support. On success, this returns `true`,
# otherwise it returns `false` if the terminal does not support color.
def self.enableColors : Bool
if NCurses.has_colors == NCurses::FALSE
false
else
checkStatus(NCurses.start_color)
true
end
end
# Initializes a color pair that will be represented by *pair*.
def self.initColorPair(pair : Int16, fg : Color, bg : Color) : Nil
checkStatus(NCurses.init_pair(pair, fg.value, bg.value))
end
# Attempts to change the definition of the color represented by *color* to the
# given RGB values. On success, this returns `true`, otherwise if the
# terminal does not support the changing of colors, this returns `false`.
#
# When `#initColor` is called, all occurences of *color* are immediately
# changed.
def self.initColor(color : Int16, r : Int16, g : Int16, b : Int16) : Bool
if NCurses.can_change_color == NCurses::FALSE
false
else
checkStatus(NCurses.init_color(color, r, g, b))
true
end
end
# :ditto:
@[AlwaysInline]
def self.initColor(color : Int16, rgb : Tuple(Int16, Int16, Int16)) : Bool
initColor(color, rgb[0], rgb[1], rgb[2])
end
# Given a color pair represented by *num*, returns a new integer that can be
# used with an attribute function.
def self.getColorPair(num : Int16) : Int16
# Remi: hand-expanded the macro from ncurses.h
((num.to_u32! << 8) & 65280).to_i16!
end
@[AlwaysInline]
protected def self.combineAttrs(*attrs : Attribute|Color|Int16|UInt32) : UInt32
val : UInt32 = 0u32
attrs.each do |attr|
case attr
in Attribute, Color then val |= attr.value
in Int16, UInt32 then val |= attr
end
end
val
end
# Turn on the attributes for this window without affecting any others.
def self.attributeOn(*attrs : Attribute|Color|Int16|UInt32) : Nil
attributeOn(RemiCharms.combineAttrs(*attrs))
end
# :ditto:
def self.attributeOn(attrs : Attribute|Color|Int16|UInt32) : Nil
checkStatus(NCurses.wattron(RemiCharms.combineAttrs(attrs)))
end
# Turn off the attributes for this window without affecting any others.
def self.attributeOff(*attrs : Attribute|Color|Int16|UInt32) : Nil
attributeOff(RemiCharms.combineAttrs(*attrs))
end
# :ditto:
def self.attributeOff(attrs : Attribute|Color|Int16|UInt32) : Nil
checkStatus(NCurses.wattroff(RemiCharms.combineAttrs(attrs)))
end
# Temporarily turns on the given attributes for this window in the block,
# then turns them off before returning.
def self.withAttributes(attrs : Attribute|UInt32, &) : Nil
attributeOn(attrs)
yield
attributeOff(attrs)
end
# Controlls whether the underlying display device translates the return key
# into newline on input.
def self.useEnterAsNewline=(value : Bool) : Nil
if value
checkStatus(NCurses.nl)
else
checkStatus(NCurses.nonl)
end
end
# Records the current cursor position in `#standardWindow`, then yields. This
# will then ensure that the cursor is restored to its original position before
# calling this function.
@[AlwaysInline]
def self.withRestoredCursor(&block) : Nil
standardWindow.withRestoredCursor(block)
end
end