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