Skip to content

Instantly share code, notes, and snippets.

@pjambet
Last active August 24, 2022 12:55
Show Gist options
  • Save pjambet/660b18a3f6b5a056325cc414e8f55f48 to your computer and use it in GitHub Desktop.
Save pjambet/660b18a3f6b5a056325cc414e8f55f48 to your computer and use it in GitHub Desktop.
A very basic text editor in ruby, copied from Kilo and adapted in Ruby
require 'termios'
s = [0, 0, 0, 0].pack("S_S_S_S_")
STDOUT.ioctl(Termios::TIOCGWINSZ, s)
HEIGHT, WIDTH, _, _ = s.unpack("S_S_S_S_")
# Raw mode
current = Termios.tcgetattr(STDIN)
t = current.dup
t.c_iflag &= ~(Termios::BRKINT | Termios::ICRNL | Termios::INPCK | Termios::ISTRIP | Termios::IXON)
t.c_oflag &= ~(Termios::OPOST)
t.c_cflag |= (Termios::CS8)
t.c_lflag &= ~(Termios::ECHO | Termios::ICANON | Termios::IEXTEN | Termios::ISIG)
t.c_cc[Termios::VMIN] = 1 # Setting 0 as in Kilo raises EOF errors
Termios.tcsetattr(STDIN, Termios::TCSANOW, t)
trap("INT") {
Termios.tcsetattr(STDIN, Termios::TCSANOW, current)
exit(0)
}
TEXT_CONTENT = [""]
CURSOR_POSITION = [1, 1]
def refresh
append_buffer = ""
append_buffer << "\x1b[?25l" # Hide cursor
append_buffer << "\x1b[H"
# stderr_log TEXT_CONTENT
HEIGHT.times do |row_index|
if row_index >= TEXT_CONTENT.count
append_buffer << "~\x1b[0K\r\n"
# stderr_log("Row index: #{row_index}")
# stderr_log("Line count: #{TEXT_CONTENT.count}")
next
end
row = TEXT_CONTENT[row_index] || ""
# stderr_log "'#{row}'"
append_buffer << row
append_buffer << "\x1b[39m"
append_buffer << "\x1b[0K"
append_buffer << "\r\n"
end
append_buffer.strip!
append_buffer << "\x1b[H"
x, y = CURSOR_POSITION
# x += 1 if x > 0
append_buffer << "\x1b[#{y};#{x}H"
# append_buffer << "\x1b[;1H"
append_buffer << "\x1b[?25h" # Show cursor
stderr_log("'#{append_buffer}'".inspect)
stderr_log("Cursor postition: x: #{CURSOR_POSITION[0]}, y: #{CURSOR_POSITION[1]}: #{y};#{x}H")
STDOUT.write(append_buffer)
end
def current_row
TEXT_CONTENT[CURSOR_POSITION[1] - 1]
end
def stderr_log(message)
unless STDERR.tty? # true when not redirecting to a file, a little janky but works for what I want
STDERR.puts(message)
end
end
loop do
refresh
c = STDIN.readpartial(1)
if c == "q"
Process.kill("INT", Process.pid)
end
# stderr_log("ord: #{c.ord}")
if c.ord == 13 # enter
if current_row.length > (CURSOR_POSITION[0] - 1)
carry = current_row[(CURSOR_POSITION[0] - 1)..-1]
current_row.slice!((CURSOR_POSITION[0] - 1)..-1)
else
carry = ""
end
TEXT_CONTENT.insert(CURSOR_POSITION[1], carry)
CURSOR_POSITION[0] = 1
CURSOR_POSITION[1] += 1
elsif c.ord == 127 # backspace
next if CURSOR_POSITION[0] == 1 && CURSOR_POSITION[1] == 1
if CURSOR_POSITION[0] == 1
if current_row.nil?
TEXT_CONTENT.delete_at(CURSOR_POSITION[1] - 1)
CURSOR_POSITION[1] -= 1
CURSOR_POSITION[0] = current_row.length + 1
elsif current_row.empty?
TEXT_CONTENT.delete_at(CURSOR_POSITION[1] - 1)
CURSOR_POSITION[1] -= 1
CURSOR_POSITION[0] = current_row.length + 1
else
previous_row = TEXT_CONTENT[CURSOR_POSITION[1] - 2]
CURSOR_POSITION[0] = previous_row.length + 1
TEXT_CONTENT[CURSOR_POSITION[1] - 2] = previous_row + current_row
TEXT_CONTENT.delete_at(CURSOR_POSITION[1] - 1)
CURSOR_POSITION[1] -= 1
end
else
deletion_index = CURSOR_POSITION[0] - 2
current_row.slice!(deletion_index)
CURSOR_POSITION[0] -= 1
end
elsif c.ord == 27 # ESC
second_char = STDIN.read_nonblock(1, exception: false)
next if second_char == :wait_readable
third_char = STDIN.read_nonblock(1, exception: false)
next if third_char == :wait_readable
if second_char == "["
case third_char
when "A" # Up
CURSOR_POSITION[1] -= 1 unless CURSOR_POSITION[1] == 1
if current_row && CURSOR_POSITION[0] > current_row.length + 1
CURSOR_POSITION[0] = current_row.length + 1
end
when "B" # Down
if CURSOR_POSITION[1] == TEXT_CONTENT.length
CURSOR_POSITION[0] = 1
end
CURSOR_POSITION[1] += 1 unless CURSOR_POSITION[1] == TEXT_CONTENT.length + 1
if current_row && CURSOR_POSITION[0] > current_row.length + 1
CURSOR_POSITION[0] = current_row.length + 1
end
when "C" # Right
# stderr_log("Current row: #{current_row}\n")
if current_row && CURSOR_POSITION[0] > current_row.length
if CURSOR_POSITION[1] <= TEXT_CONTENT.length + 1
CURSOR_POSITION[0] = 1
CURSOR_POSITION[1] += 1
end
elsif current_row
CURSOR_POSITION[0] += 1
end
when "D" # Left
if CURSOR_POSITION[0] == 1
if CURSOR_POSITION[1] > 1
CURSOR_POSITION[1] -= 1
CURSOR_POSITION[0] = current_row.length + 1
end
else
CURSOR_POSITION[0] -= 1
end
when "H" then "H" # Home
when "F" then "F" # End
end
end
# TEXT_CONTENT.last << third_char
elsif c.ord >= 32 && c.ord <= 126
if current_row.nil?
TEXT_CONTENT << ""
end
current_row.insert(CURSOR_POSITION[0] - 1, c)
CURSOR_POSITION[0] += 1
end
end
require 'termios'
s = [0, 0, 0, 0].pack("S_S_S_S_")
STDOUT.ioctl(Termios::TIOCGWINSZ, s)
HEIGHT, WIDTH, _, _ = s.unpack("S_S_S_S_")
TEXT_CONTENT = [""]
current = Termios.tcgetattr(STDIN)
t = current.dup
# https://man7.org/linux/man-pages/man3/tcflush.3.html
t.c_lflag &= ~(Termios::ICANON);
Termios.tcsetattr(STDIN, Termios::TCSANOW, t)
def refresh
append_buffer = ""
append_buffer << "\x1b[?25l" # Hide cursor
append_buffer << "\x1b[H"
HEIGHT.times do |row_index|
if row_index >= TEXT_CONTENT.count
append_buffer << "~\x1b[0K\r\n"
next
end
end
append_buffer << "\x1b[H"
append_buffer << TEXT_CONTENT.join("")
STDOUT.write(append_buffer)
end
loop do
refresh
c = STDIN.readpartial(1)
exit(0) if c == "q"
if c.ord >= 32 && c.ord <= 126
TEXT_CONTENT.last << c
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment