diff --git a/.gitignore b/.gitignore index 08d3073..0d217ec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.a *.o *.so +*.yml .vscode diff --git a/Makefile b/Makefile deleted file mode 100644 index e69de29..0000000 diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..53be66a --- /dev/null +++ b/compile.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$(cd -- "$(dirname -- "$0")" && pwd)" + +mkdir -p "$DIR/builds" + +g++ -O2 -std=c++20 -shared -fPIC -Wall -Wextra \ + -o "$DIR/builds/C-crib.so" $DIR/src/cpp/*.cpp +strip "$DIR/builds/C-crib.so" diff --git a/crib.rb b/crib.rb new file mode 100755 index 0000000..1a402d7 --- /dev/null +++ b/crib.rb @@ -0,0 +1,53 @@ +#!/usr/bin/env ruby + +require_relative "./src/ruby/mod" + +# C.start_screen +# +# at_exit do +# C.end_screen +# puts "bye" +# end +# +# +# class Tester +# @current_row = 0 +# +# class << self +# def debug_print(text) +# text.each_char.with_index do |ch, i| +# C.update(@current_row, i, ch, 0xFFFFFE, 0x000001, 0) +# end +# @current_row += 1 +# end +# +# def reset +# @current_row = 0 +# end +# end +# end +# +# render_thread = Thread.new do +# loop do +# sleep(1.0/20) +# C.render +# end +# end +# +# loop do +# sleep 0.001 +# event = C.read_key +# break if event[:key_type] == 0 && event[:c] == 'q'.ord +# Tester.reset if event[:key_type] == 0 && event[:c] == 'r'.ord +# Tester.debug_print(C.get_size.to_s) if event[:key_type] == 0 && event[:c] == 's'.ord +# Tester.debug_print(event.to_s) +# end +# +# render_thread.kill +# render_thread.join + +require_relative("./src/ruby/editor.rb") + +test = Buffer.new("hello world\nwow") + +pp test.render_text(0, 0, 10, 10) diff --git a/src/cpp/input.cpp b/src/cpp/input.cpp new file mode 100644 index 0000000..21a3006 --- /dev/null +++ b/src/cpp/input.cpp @@ -0,0 +1,125 @@ +#include "../headers/header.hpp" + +int read_input(char *buf, size_t buflen) { + size_t i = 0; + int n; + n = read(STDIN_FILENO, &buf[i], 1); + if (n <= 0) + return -1; + i++; + if (buf[0] == '\x1b') { + while (i < buflen - 1) { + n = read(STDIN_FILENO, &buf[i], 1); + if (n <= 0) + break; + i++; + } + } + buf[i] = '\0'; + return i; +} + +void capture_mouse(char *buf, KeyEvent *ret) { + uint8_t byte = buf[3]; + ret->mouse_modifier = (byte >> 3) & 0x03; + uint8_t aa = (byte >> 5) & 0x03; + uint8_t cc = byte & 0x03; + ret->mouse_x = buf[4] - 33; + ret->mouse_y = buf[5] - 33; + ret->mouse_direction = 4; + if (aa == 1 && cc == 3) { + ret->mouse_state = RELEASE; + ret->mouse_button = NONE_BTN; + } else if (aa == 1) { + ret->mouse_state = PRESS; + ret->mouse_button = cc; + } else if (aa == 2) { + ret->mouse_state = DRAG; + ret->mouse_button = cc; + } else if (aa == 3) { + ret->mouse_button = SCROLL_BTN; + ret->mouse_state = SCROLL; + ret->mouse_direction = cc; + } else { + ret->mouse_state = RELEASE; + ret->mouse_button = NONE_BTN; + } +} + +KeyEvent read_key_nonblock() { + KeyEvent ret; + char buf[7]; + int n = read_input(buf, sizeof(buf)); + if (n <= 0) { + ret.key_type = KEY_NONE; + ret.c = '\0'; + return ret; + } + if (n == 1) { + ret.key_type = KEY_CHAR; + ret.c = buf[0]; + } else if (buf[0] == '\x1b' && buf[1] == '[' && buf[2] == 'M') { + ret.key_type = KEY_MOUSE; + capture_mouse(buf, &ret); + } else { + ret.key_type = KEY_SPECIAL; + if (buf[0] == '\x1b' && buf[1] == '[') { + int using_modifiers = buf[3] == ';'; + int pos; + if (!using_modifiers) { + pos = 2; + ret.special_modifier = 0; + } else { + pos = 4; + switch (buf[3]) { + case '2': + ret.special_modifier = SHIFT; + break; + case '3': + ret.special_modifier = ALT; + break; + case '5': + ret.special_modifier = CNTRL; + break; + case '7': + ret.special_modifier = CNTRL_ALT; + break; + default: + ret.special_modifier = 0; + break; + } + } + switch (buf[pos]) { + case 'A': + ret.special_key = KEY_UP; + break; + case 'B': + ret.special_key = KEY_DOWN; + break; + case 'C': + ret.special_key = KEY_RIGHT; + break; + case 'D': + ret.special_key = KEY_LEFT; + break; + case '3': + ret.special_key = KEY_DELETE; + break; + default: + ret.special_key = 99; + break; + } + } + } + return ret; +} + +KeyEvent read_key() { + KeyEvent ret; + while (1) { + ret = read_key_nonblock(); + if (ret.key_type != KEY_NONE) + return ret; + usleep(2500); + } +} diff --git a/src/cpp/renderer.cpp b/src/cpp/renderer.cpp new file mode 100644 index 0000000..8692e0d --- /dev/null +++ b/src/cpp/renderer.cpp @@ -0,0 +1,260 @@ +// includes +#include "../../libs/libgrapheme/grapheme.h" +#include "../../libs/unicode_width/unicode_width.h" +#include "../headers/header.hpp" + +struct termios orig_termios; + +int rows, cols; +bool show_cursor = false; +std::vector screen; +std::vector old_screen; +std::mutex screen_mutex; + +int real_width(std::string str) { + if (!str.size()) + return 0; + const char *p = str.c_str(); + if (str[0] == '\t') + return 4; + unicode_width_state_t state; + unicode_width_init(&state); + int width = 0; + for (size_t j = 0; j < str.size(); j++) { + unsigned char c = str[j]; + if (c < 128) { + int char_width = unicode_width_process(&state, c); + if (char_width > 0) + width += char_width; + } else { + uint_least32_t cp; + size_t bytes = grapheme_decode_utf8(p + j, str.size() - j, &cp); + if (bytes > 1) { + int char_width = unicode_width_process(&state, cp); + if (char_width > 0) + width += char_width; + j += bytes - 1; + } + } + } + return width; +} + +void get_terminal_size() { + struct winsize w; + ioctl(0, TIOCGWINSZ, &w); + rows = w.ws_row; + cols = w.ws_col; +} + +void die(const char *s) { + perror(s); + disable_raw_mode(); + exit(EXIT_FAILURE); +} + +void enable_raw_mode() { + if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) + die("tcgetattr"); + atexit(disable_raw_mode); + + struct termios raw = orig_termios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) + die("tcsetattr"); + + std::string os = "\x1b[?1049h\x1b[2 q\x1b[?1002h\x1b[?25l"; + write(STDOUT_FILENO, os.c_str(), os.size()); +} + +void disable_raw_mode() { + std::string os = "\x1b[?1049l\x1b[?25h"; + write(STDOUT_FILENO, os.c_str(), os.size()); + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) { + perror("tcsetattr"); + exit(EXIT_FAILURE); + } +} + +void start_screen() { + enable_raw_mode(); + get_terminal_size(); + screen.assign(rows * cols, {}); // allocate & zero-init + old_screen.assign(rows * cols, {}); // allocate & zero-init +} + +void end_screen() { disable_raw_mode(); } + +void update(int row, int col, const char *utf8, uint32_t fg, uint32_t bg, + uint8_t flags) { + if (row < 0 || row >= rows || col < 0 || col >= cols) + return; + + int idx = row * cols + col; + std::lock_guard lock(screen_mutex); + + screen[idx].utf8 = utf8 ? utf8 : ""; // nullptr => empty string + screen[idx].fg = fg; + screen[idx].bg = bg; + screen[idx].flags = flags; +} + +coords get_size() { return {rows, cols}; } + +void render() { + static bool first_render = true; + uint32_t current_fg = 0; + uint32_t current_bg = 0; + bool current_italic = false; + bool current_bold = false; + bool current_underline = false; + + std::lock_guard lock(screen_mutex); + + std::string out; + // reserve a conservative amount to avoid repeated reallocs + out.reserve(static_cast(rows) * static_cast(cols) * 4 + 256); + + // save cursor + hide + out += "\x1b[s\x1b[?25l"; + + if (first_render) { + out += "\x1b[2J\x1b[H"; + first_render = false; + } + + for (int row = 0; row < rows; ++row) { + int first_change_col = -1; + int last_change_col = -1; + + // detect change span in this row + for (int col = 0; col < cols; ++col) { + int idx = row * cols + col; + ScreenCell &old_cell = old_screen[idx]; + ScreenCell &new_cell = screen[idx]; + + bool old_empty = old_cell.utf8.empty(); + bool new_empty = new_cell.utf8.empty(); + + bool content_changed = + (old_empty && !new_empty) || (!old_empty && new_empty) || + (!old_empty && !new_empty && old_cell.utf8 != new_cell.utf8); + + bool style_changed = + (old_cell.fg != new_cell.fg) || (old_cell.bg != new_cell.bg) || + ((old_cell.flags & CF_ITALIC) != (new_cell.flags & CF_ITALIC)) || + ((old_cell.flags & CF_BOLD) != (new_cell.flags & CF_BOLD)) || + ((old_cell.flags & CF_UNDERLINE) != (new_cell.flags & CF_UNDERLINE)); + + if (content_changed || style_changed) { + if (first_change_col == -1) + first_change_col = col; + last_change_col = col; + } + } + + if (first_change_col == -1) + continue; + + // move cursor once to the start of change region + char buf[32]; + int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, + first_change_col + 1); + out.append(buf, n); + + // render changed region + for (int col = first_change_col; col <= last_change_col; ++col) { + int idx = row * cols + col; + ScreenCell &old_cell = old_screen[idx]; + ScreenCell &new_cell = screen[idx]; + + // foreground change + if (current_fg != new_cell.fg) { + if (new_cell.fg) { + char fb[64]; + int m = snprintf( + fb, sizeof(fb), "\x1b[38;2;%d;%d;%dm", (new_cell.fg >> 16) & 0xFF, + (new_cell.fg >> 8) & 0xFF, (new_cell.fg >> 0) & 0xFF); + out.append(fb, m); + } else { + out += "\x1b[39m"; + } + current_fg = new_cell.fg; + } + + // background change + if (current_bg != new_cell.bg) { + if (new_cell.bg) { + char bb[64]; + int m = snprintf( + bb, sizeof(bb), "\x1b[48;2;%d;%d;%dm", (new_cell.bg >> 16) & 0xFF, + (new_cell.bg >> 8) & 0xFF, (new_cell.bg >> 0) & 0xFF); + out.append(bb, m); + } else { + out += "\x1b[49m"; + } + current_bg = new_cell.bg; + } + + // italic + bool italic = (new_cell.flags & CF_ITALIC) != 0; + if (italic != current_italic) { + out += italic ? "\x1b[3m" : "\x1b[23m"; + current_italic = italic; + } + + // bold + bool bold = (new_cell.flags & CF_BOLD) != 0; + if (bold != current_bold) { + out += bold ? "\x1b[1m" : "\x1b[22m"; + current_bold = bold; + } + + // underline + bool underline = (new_cell.flags & CF_UNDERLINE) != 0; + if (underline != current_underline) { + out += underline ? "\x1b[4m" : "\x1b[24m"; + current_underline = underline; + } + + // content + if (!new_cell.utf8.empty()) { + if (new_cell.utf8[0] == '\t') + out.append(" "); + else + out.append(new_cell.utf8); + } else { + out.append(1, ' '); + } + + // copy new -> old (std::string assignment, no strdup/free) + old_cell.utf8 = new_cell.utf8; + old_cell.fg = new_cell.fg; + old_cell.bg = new_cell.bg; + old_cell.flags = new_cell.flags; + } + } + + // final reset + restore cursor + show cursor + out += "\x1b[0m"; + out += "\x1b[u"; + if (show_cursor) + out += "\x1b[?25h"; + + // single syscall to write the whole frame + ssize_t written = write(STDOUT_FILENO, out.data(), out.size()); + (void)written; // you may check for errors in debug builds +} + +void set_cursor(int row, int col, bool show_cursor_param) { + char buf[32]; + int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, col + 1); + show_cursor = show_cursor_param; + write(STDOUT_FILENO, buf, n); +} diff --git a/src/headers/header.hpp b/src/headers/header.hpp new file mode 100644 index 0000000..e456d50 --- /dev/null +++ b/src/headers/header.hpp @@ -0,0 +1,102 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#define KEY_CHAR 0 +#define KEY_SPECIAL 1 +#define KEY_MOUSE 2 +#define KEY_NONE 3 + +#define KEY_UP 0 +#define KEY_DOWN 1 +#define KEY_LEFT 2 +#define KEY_RIGHT 3 +#define KEY_DELETE 4 + +#define KEY_ESC '\x1b' + +#define PRESS 0 +#define RELEASE 1 +#define DRAG 2 +#define SCROLL 3 + +#define LEFT_BTN 0 +#define MIDDLE_BTN 1 +#define RIGHT_BTN 2 +#define SCROLL_BTN 3 +#define NONE_BTN 4 + +#define SCROLL_UP 0 +#define SCROLL_DOWN 1 +#define SCROLL_LEFT 2 +#define SCROLL_RIGHT 3 + +#define ALT 1 +#define CNTRL 2 +#define CNTRL_ALT 3 +#define SHIFT 4 + +enum CellFlags : uint8_t { + CF_NONE = 0, + CF_ITALIC = 1 << 0, + CF_BOLD = 1 << 1, + CF_UNDERLINE = 1 << 2, +}; + +struct ScreenCell { + std::string utf8; // empty => no content + uint32_t fg = 0; + uint32_t bg = 0; + uint8_t flags = CF_NONE; +}; + +struct KeyEvent { + uint8_t key_type; + + char c; + + uint8_t special_key; + uint8_t special_modifier; + + uint8_t mouse_x; + uint8_t mouse_y; + uint8_t mouse_button; + uint8_t mouse_state; + uint8_t mouse_direction; + uint8_t mouse_modifier; +}; + +extern int rows, cols; +extern std::vector screen; // size rows*cols +extern std::vector old_screen; +extern std::mutex screen_mutex; + +struct coords { + int row; + int col; +}; + +extern "C" { +void get_terminal_size(); +void die(const char *s); +void enable_raw_mode(); +void disable_raw_mode(); +void start_screen(); +void end_screen(); +void update(int row, int col, const char *utf8, uint32_t fg, uint32_t bg, + uint8_t flags); +void set_cursor(int row, int col, bool show_cursor_param); +void render(); +coords get_size(); + +int real_width(std::string str); + +int read_input(char *buf, size_t buflen); +KeyEvent read_key_nonblock(); +KeyEvent read_key(); +} diff --git a/src/ruby/editor.rb b/src/ruby/editor.rb new file mode 100644 index 0000000..293c80e --- /dev/null +++ b/src/ruby/editor.rb @@ -0,0 +1,70 @@ +class Buffer + # Simple structs for clarity + Diagnostic = Struct.new(:x0, :y0, :x1, :y1, :message) + Highlight = Struct.new(:x0, :y0, :x1, :y1, :fg, :bg) + VirtualText = Struct.new(:x, :y, :lines) + Cursor = Struct.new(:x, :y) + + attr_accessor :text, :cursor, :selection_start, + :diagnostics, :highlights, :virtual_texts + + def initialize(initial_text = "") + @text = initial_text + @cursor = Cursor.new(0, 0) + @selection_start = Cursor.new(0, 0) + @diagnostics = [] + @highlights = [] + @virtual_texts = [] + end + + # Utility methods + def lines + @text.split("\n") + end + + def line_count + lines.size + end + + def line(y) + lines[y] || "" + end + + def insert(x, y, str) + current_line = lines[y] || "" + before = current_line[0...x] || "" + after = current_line[x..-1] || "" + lines_arr = lines + lines_arr[y] = before + str + after + @text = lines_arr.join("\n") + end + + def erase(x0, y0, x1, y1) + lines_arr = lines + if y0 == y1 + line = lines_arr[y0] || "" + lines_arr[y0] = line[0...x0] + line[x1..-1].to_s + else + first = lines_arr[y0][0...x0] + last = lines_arr[y1][x1..-1].to_s + lines_arr[y0..y1] = [first + last] + end + @text = lines_arr.join("\n") + end + + # Add overlays + def add_diagnostic(x0, y0, x1, y1, message) + @diagnostics << Diagnostic.new(x0, y0, x1, y1, message) + end + + def add_highlight(x0, y0, x1, y1, fg: 0xFFFFFF, bg: 0x000000) + @highlights << Highlight.new(x0, y0, x1, y1, fg, bg) + end + + def add_virtual_text(x, y, lines) + @virtual_texts << VirtualText.new(x, y, lines) + end + + def render() + end +end diff --git a/src/ruby/mod.rb b/src/ruby/mod.rb new file mode 100644 index 0000000..0383cd3 --- /dev/null +++ b/src/ruby/mod.rb @@ -0,0 +1,57 @@ +require "ffi" +require_relative "utils" + +module C + extend FFI::Library + ffi_lib File.join(__dir__, "../../builds/C-crib.so") + + class KeyEvent < FFI::Struct + layout :key_type, :uint8, + :c, :char, + :special_key, :uint8, + :special_modifier, :uint8, + :mouse_x, :uint8, + :mouse_y, :uint8, + :mouse_button, :uint8, + :mouse_state, :uint8, + :mouse_direction, :uint8, + :mouse_modifier, :uint8 + + def to_s + case KEY_TYPE[self[:key_type]] + when :char + "#" + when :special + "#" + when :mouse + "#" + else + "#" + end + end + end + + class Coords < FFI::Struct + layout :x, :int, + :y, :int + + def to_ary + [self[:x], self[:y]] + end + + def to_s + "#" + end + end + + attach_function :start_screen, [], :void + attach_function :end_screen, [], :void + attach_function :update, [:int, :int, :string, :uint32, :uint32, :uint8], :void + attach_function :render, [], :void + attach_function :set_cursor, [:int, :int, :int], :void + attach_function :read_key, [], KeyEvent.by_value + attach_function :get_size, [], Coords.by_value + attach_function :real_width, [:string], :int +end diff --git a/src/ruby/utils.rb b/src/ruby/utils.rb new file mode 100644 index 0000000..3d0fcd3 --- /dev/null +++ b/src/ruby/utils.rb @@ -0,0 +1,56 @@ +def ctrl_key(k) + k.ord & 0x1F +end + +# Key types +KEY_TYPE = { + 0 => :char, + 1 => :special, + 2 => :mouse +} + +# Special keys +SPECIAL_KEY = { + 0 => :up, + 1 => :down, + 2 => :left, + 3 => :right, + 4 => :delete +} + +# Control key +KEY_ESC = "\x1b" + +# Mouse states +MOUSE_STATE = { + 0 => :press, + 1 => :release, + 2 => :drag, + 3 => :scroll +} + +# Mouse buttons +MOUSE_BUTTON = { + 0 => :left, + 1 => :middle, + 2 => :right, + 3 => :scroll, + 4 => :none +} + +# Scroll directions +SCROLL_DIR = { + 0 => :up, + 1 => :down, + 2 => :left, + 3 => :right, + 4 => :none +} + +# Modifiers +MODIFIER = { + 1 => :alt, + 2 => :cntrl, + 3 => :cntrl_alt, + 4 => :shift +}