1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
| #### 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
|