crummy.cr
· 3.7 KiB · Crystal
Ham
Playground
require "./event"
module Crummy
# Enum holding various ASCII control codes. Not all code are present in this
# enum.
enum ControlChar
Null
StartHeading
StartText
EndText
Enquiry
Acknowledgement
Bell
Backspace
HorizontalTab
LineFeed
VerticalTab
FormFeed
CarriageReturn
ShiftOut
ShiftIn
Cancel
Substitute
Escape
Unknown
end
# Enum for the various CSI operations.
enum ControlSequence
CursorUp
CursorDown
CursorForwards
CursorBackwards
CursorNextLine
CursorPreviousLine
CursorHorizontalAbsolute
CursorPosition
EraseDisplay
EraseLine
InsertLines
DeleteLines
DeleteChars
EraseChars
ScrollUp
ScrollDown
HorizontalVerticalPosition
SelectGraphicRendition
MouseEvent
KeyCode
F1
F2
F3
F4
Unknown
end
end
module Crummy
# Sends a control sequence to clear the screen to the console
def self.clear
print "\e[1;1H\e[2J"
end
# Sends a control sequence to enable full mouse tracking to the console
def self.init_cursor
print "\e[?1003h\e[?1015h\e[?1006h"
end
def self.deinit_cursor
print "\e[?1003l\e[?1015l\e[?1006l"
end
# Reads a character in raw mode. If the read times out, then nil is returned
#
# Useage:
# ```
# buffer = [] of Char
# while (byte = read_char)
# buffer << byte
# end
# ```
def self.read_char
STDIN.raw { |io|
begin
io.read_char
rescue
nil
end
}
end
def self.parse_mouse
KeyEvent.new(SpecialKey::Unknown, Modifier::None)
end
def self.parse_csi
str = String.build do |str|
while (char = read_char)
str << char
next if char.number?
next if char == ';'
next if char == '<'
break
end
end
if str[0]? == '<'&& (res = /<(\d+);(\d+);(\d+)(m|M)/.match str)
pressed = $4 == "M"
button = $1
x = $2.to_i32
y = $3.to_i32
MouseEvent.new(pressed, MouseButton.from_s(button), {x: x, y: y})
else
KeyEvent.new(SpecialKey::Unknown, Modifier::None)
end
end
def self.parse_ss3
KeyEvent.new(SpecialKey::Unknown, Modifier::None)
end
def self.parse_code
KeyEvent.new(SpecialKey::Unknown, Modifier::None)
end
# Reads input from the console. It converts all escape sequences into
# `String`s.
def self.read_raw_input
raw_input = [] of Event
while (char = read_char)
if char == '\e'
raw_input << case read_char
when nil
KeyEvent.new(SpecialKey::Escape, Modifier::None)
when '['
parse_csi
when 'O'
parse_ss3
when .number?
parse_code
else
KeyEvent.new(SpecialKey::Unknown, Modifier::None)
end
else
raw_input << KeyEvent.new(char, Modifier::None)
end
end
raw_input
end
def self.process_input(raw_input) : Array(Event)
[] of Event
end
# Reads a series of inputs from the command line
def self.listen
raw_input = read_raw_input
end
end
at_exit {
STDIN.cooked!
Crummy.deinit_cursor
Crummy.clear
}
STDIN.read_timeout = 0.milliseconds
Crummy.init_cursor
Signal::INT.trap do
exit
end
pos = {x: 1, y: 1}
def draw(pos, offset, str)
x, y = pos[:x] + offset[:x], pos[:y] + offset[:y]
"\e[#{y};#{x}H#{str}"
end
loop do
input = Crummy.read_raw_input
input.each do |event|
if event.is_a? Crummy::MouseEvent
pos = event.location
end
end
Crummy.clear
print <<-RENDER
#{draw(pos, {x: -2, y: -1}, "⬛😃⬛")}
#{draw(pos, {x: -2, y: 0}, "😃⬛😃")}
#{draw(pos, {x: -2, y: 1}, "⬛😃⬛")}
RENDER
print "\e[1;1H"
sleep 32.milliseconds
end
| 1 | require "./event" |
| 2 | |
| 3 | module Crummy |
| 4 | # Enum holding various ASCII control codes. Not all code are present in this |
| 5 | # enum. |
| 6 | enum ControlChar |
| 7 | Null |
| 8 | StartHeading |
| 9 | StartText |
| 10 | EndText |
| 11 | Enquiry |
| 12 | Acknowledgement |
| 13 | Bell |
| 14 | Backspace |
| 15 | HorizontalTab |
| 16 | LineFeed |
| 17 | VerticalTab |
| 18 | FormFeed |
| 19 | CarriageReturn |
| 20 | ShiftOut |
| 21 | ShiftIn |
| 22 | Cancel |
| 23 | Substitute |
| 24 | Escape |
| 25 | Unknown |
| 26 | |
| 27 | end |
| 28 | |
| 29 | # Enum for the various CSI operations. |
| 30 | enum ControlSequence |
| 31 | CursorUp |
| 32 | CursorDown |
| 33 | CursorForwards |
| 34 | CursorBackwards |
| 35 | CursorNextLine |
| 36 | CursorPreviousLine |
| 37 | CursorHorizontalAbsolute |
| 38 | CursorPosition |
| 39 | EraseDisplay |
| 40 | EraseLine |
| 41 | InsertLines |
| 42 | DeleteLines |
| 43 | DeleteChars |
| 44 | EraseChars |
| 45 | ScrollUp |
| 46 | ScrollDown |
| 47 | HorizontalVerticalPosition |
| 48 | SelectGraphicRendition |
| 49 | MouseEvent |
| 50 | KeyCode |
| 51 | F1 |
| 52 | F2 |
| 53 | F3 |
| 54 | F4 |
| 55 | Unknown |
| 56 | end |
| 57 | end |
| 58 | |
| 59 | module Crummy |
| 60 | # Sends a control sequence to clear the screen to the console |
| 61 | def self.clear |
| 62 | print "\e[1;1H\e[2J" |
| 63 | end |
| 64 | |
| 65 | # Sends a control sequence to enable full mouse tracking to the console |
| 66 | def self.init_cursor |
| 67 | print "\e[?1003h\e[?1015h\e[?1006h" |
| 68 | end |
| 69 | |
| 70 | def self.deinit_cursor |
| 71 | print "\e[?1003l\e[?1015l\e[?1006l" |
| 72 | end |
| 73 | |
| 74 | # Reads a character in raw mode. If the read times out, then nil is returned |
| 75 | # |
| 76 | # Useage: |
| 77 | # ``` |
| 78 | # buffer = [] of Char |
| 79 | # while (byte = read_char) |
| 80 | # buffer << byte |
| 81 | # end |
| 82 | # ``` |
| 83 | def self.read_char |
| 84 | STDIN.raw { |io| |
| 85 | begin |
| 86 | io.read_char |
| 87 | rescue |
| 88 | nil |
| 89 | end |
| 90 | } |
| 91 | end |
| 92 | |
| 93 | def self.parse_mouse |
| 94 | |
| 95 | |
| 96 | KeyEvent.new(SpecialKey::Unknown, Modifier::None) |
| 97 | end |
| 98 | |
| 99 | def self.parse_csi |
| 100 | str = String.build do |str| |
| 101 | while (char = read_char) |
| 102 | str << char |
| 103 | |
| 104 | next if char.number? |
| 105 | next if char == ';' |
| 106 | next if char == '<' |
| 107 | break |
| 108 | end |
| 109 | end |
| 110 | |
| 111 | if str[0]? == '<'&& (res = /<(\d+);(\d+);(\d+)(m|M)/.match str) |
| 112 | pressed = $4 == "M" |
| 113 | button = $1 |
| 114 | x = $2.to_i32 |
| 115 | y = $3.to_i32 |
| 116 | MouseEvent.new(pressed, MouseButton.from_s(button), {x: x, y: y}) |
| 117 | else |
| 118 | KeyEvent.new(SpecialKey::Unknown, Modifier::None) |
| 119 | end |
| 120 | end |
| 121 | |
| 122 | def self.parse_ss3 |
| 123 | KeyEvent.new(SpecialKey::Unknown, Modifier::None) |
| 124 | end |
| 125 | |
| 126 | |
| 127 | def self.parse_code |
| 128 | KeyEvent.new(SpecialKey::Unknown, Modifier::None) |
| 129 | end |
| 130 | # Reads input from the console. It converts all escape sequences into |
| 131 | # `String`s. |
| 132 | def self.read_raw_input |
| 133 | raw_input = [] of Event |
| 134 | while (char = read_char) |
| 135 | if char == '\e' |
| 136 | raw_input << case read_char |
| 137 | when nil |
| 138 | KeyEvent.new(SpecialKey::Escape, Modifier::None) |
| 139 | when '[' |
| 140 | parse_csi |
| 141 | when 'O' |
| 142 | parse_ss3 |
| 143 | when .number? |
| 144 | parse_code |
| 145 | else |
| 146 | KeyEvent.new(SpecialKey::Unknown, Modifier::None) |
| 147 | end |
| 148 | else |
| 149 | raw_input << KeyEvent.new(char, Modifier::None) |
| 150 | end |
| 151 | end |
| 152 | |
| 153 | raw_input |
| 154 | end |
| 155 | |
| 156 | def self.process_input(raw_input) : Array(Event) |
| 157 | [] of Event |
| 158 | end |
| 159 | |
| 160 | # Reads a series of inputs from the command line |
| 161 | def self.listen |
| 162 | raw_input = read_raw_input |
| 163 | |
| 164 | end |
| 165 | end |
| 166 | |
| 167 | at_exit { |
| 168 | STDIN.cooked! |
| 169 | Crummy.deinit_cursor |
| 170 | Crummy.clear |
| 171 | } |
| 172 | |
| 173 | STDIN.read_timeout = 0.milliseconds |
| 174 | Crummy.init_cursor |
| 175 | |
| 176 | Signal::INT.trap do |
| 177 | exit |
| 178 | end |
| 179 | |
| 180 | pos = {x: 1, y: 1} |
| 181 | |
| 182 | def draw(pos, offset, str) |
| 183 | x, y = pos[:x] + offset[:x], pos[:y] + offset[:y] |
| 184 | |
| 185 | "\e[#{y};#{x}H#{str}" |
| 186 | end |
| 187 | |
| 188 | loop do |
| 189 | input = Crummy.read_raw_input |
| 190 | |
| 191 | input.each do |event| |
| 192 | if event.is_a? Crummy::MouseEvent |
| 193 | pos = event.location |
| 194 | end |
| 195 | end |
| 196 | |
| 197 | Crummy.clear |
| 198 | |
| 199 | print <<-RENDER |
| 200 | #{draw(pos, {x: -2, y: -1}, "⬛😃⬛")} |
| 201 | #{draw(pos, {x: -2, y: 0}, "😃⬛😃")} |
| 202 | #{draw(pos, {x: -2, y: 1}, "⬛😃⬛")} |
| 203 | RENDER |
| 204 | print "\e[1;1H" |
| 205 | sleep 32.milliseconds |
| 206 | end |
| 207 |