Login
Artifact [e455e62d78]
Login

Artifact e455e62d780c3e33ed7fb0a8c3829382248f26782eef6d11fbe4ed39cea3e3bb:


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