From 4fc1a6ade8b6f3cf4466bf1a19bafb2e62df9abb Mon Sep 17 00:00:00 2001 From: Max Wash Date: Mon, 18 Nov 2024 09:53:55 +0000 Subject: [PATCH] frontend: add a line editor for shell input --- frontend/line-ed/buffer.c | 40 +++ frontend/line-ed/buffer.h | 14 ++ frontend/line-ed/cursor.c | 27 ++ frontend/line-ed/cursor.h | 10 + frontend/line-ed/history.c | 67 +++++ frontend/line-ed/history.h | 12 + frontend/line-ed/hl-range.c | 287 +++++++++++++++++++++ frontend/line-ed/hl-range.h | 41 +++ frontend/line-ed/hook.c | 37 +++ frontend/line-ed/hook.h | 16 ++ frontend/line-ed/hooks/highlight.c | 131 ++++++++++ frontend/line-ed/hooks/highlight.h | 20 ++ frontend/line-ed/input.c | 204 +++++++++++++++ frontend/line-ed/input.h | 13 + frontend/line-ed/line-ed.c | 296 ++++++++++++++++++++++ frontend/line-ed/line-ed.h | 96 ++++++++ frontend/line-ed/prompt.c | 27 ++ frontend/line-ed/prompt.h | 12 + frontend/line-ed/refresh.c | 242 ++++++++++++++++++ frontend/line-ed/refresh.h | 21 ++ frontend/line-ed/tty.c | 384 +++++++++++++++++++++++++++++ frontend/line-ed/tty.h | 140 +++++++++++ 22 files changed, 2137 insertions(+) create mode 100644 frontend/line-ed/buffer.c create mode 100644 frontend/line-ed/buffer.h create mode 100644 frontend/line-ed/cursor.c create mode 100644 frontend/line-ed/cursor.h create mode 100644 frontend/line-ed/history.c create mode 100644 frontend/line-ed/history.h create mode 100644 frontend/line-ed/hl-range.c create mode 100644 frontend/line-ed/hl-range.h create mode 100644 frontend/line-ed/hook.c create mode 100644 frontend/line-ed/hook.h create mode 100644 frontend/line-ed/hooks/highlight.c create mode 100644 frontend/line-ed/hooks/highlight.h create mode 100644 frontend/line-ed/input.c create mode 100644 frontend/line-ed/input.h create mode 100644 frontend/line-ed/line-ed.c create mode 100644 frontend/line-ed/line-ed.h create mode 100644 frontend/line-ed/prompt.c create mode 100644 frontend/line-ed/prompt.h create mode 100644 frontend/line-ed/refresh.c create mode 100644 frontend/line-ed/refresh.h create mode 100644 frontend/line-ed/tty.c create mode 100644 frontend/line-ed/tty.h diff --git a/frontend/line-ed/buffer.c b/frontend/line-ed/buffer.c new file mode 100644 index 0000000..14eaf7d --- /dev/null +++ b/frontend/line-ed/buffer.c @@ -0,0 +1,40 @@ +#include "buffer.h" +#include "line-ed.h" + +const char *line_start(struct line_ed *ed, unsigned int y) +{ + const char *line = ed->l_buf; + + for (unsigned int i = 0; i < y; i++) { + line += strcspn(line, "\n"); + + if (*line == '\n') { + line++; + } + } + + return line; +} + +unsigned int line_length(struct line_ed *ed, unsigned int y) +{ + const char *line = ed->l_buf; + + for (unsigned int i = 0; i < y; i++) { + line += strcspn(line, "\n"); + if (*line == '\n') { + line++; + } + } + + if (*line == '\0') { + return 0; + } + + unsigned int len = strcspn(line, "\n"); + if (line[len] == '\n') { + len++; + } + + return len; +} diff --git a/frontend/line-ed/buffer.h b/frontend/line-ed/buffer.h new file mode 100644 index 0000000..757246c --- /dev/null +++ b/frontend/line-ed/buffer.h @@ -0,0 +1,14 @@ +#ifndef LINE_ED_BUFFER_H_ +#define LINE_ED_BUFFER_H_ + +struct line_ed; + +/* returns a pointer to the start of the line based on the given `y` + * coordinate */ +extern const char *line_start(struct line_ed *ed, unsigned int y); +/* returns the length of the line based on the given `y` coordinate. + * for any line other than the last line in the buffer, this length + * INCLUDES the trailing linefeed. */ +extern unsigned int line_length(struct line_ed *ed, unsigned int y); + +#endif diff --git a/frontend/line-ed/cursor.c b/frontend/line-ed/cursor.c new file mode 100644 index 0000000..d3a47b4 --- /dev/null +++ b/frontend/line-ed/cursor.c @@ -0,0 +1,27 @@ +#include "line-ed.h" +#include "cursor.h" +#include "prompt.h" + +void line_ed_coords_to_physical_coords(struct line_ed *ed, + unsigned int x, unsigned int y, + unsigned int *out_x, unsigned int *out_y) +{ + unsigned int prompt_len = 0; + if (ed->l_cursor_y == 0) { + prompt_len = prompt_length(ed, PROMPT_MAIN); + } else if (ed->l_cursor_y <= ed->l_continuations) { + prompt_len = prompt_length(ed, PROMPT_CONT); + } + + if (y == 0) { + x += prompt_len; + } + + if (out_x) { + *out_x = x; + } + + if (out_y) { + *out_y = y; + } +} diff --git a/frontend/line-ed/cursor.h b/frontend/line-ed/cursor.h new file mode 100644 index 0000000..4aed6f0 --- /dev/null +++ b/frontend/line-ed/cursor.h @@ -0,0 +1,10 @@ +#ifndef LINE_ED_CURSOR_H_ +#define LINE_ED_CURSOR_H_ + +struct line_ed; + +extern void line_ed_coords_to_physical_coords(struct line_ed *ed, + unsigned int x, unsigned int y, + unsigned int *out_x, unsigned int *out_y); + +#endif diff --git a/frontend/line-ed/history.c b/frontend/line-ed/history.c new file mode 100644 index 0000000..d57a104 --- /dev/null +++ b/frontend/line-ed/history.c @@ -0,0 +1,67 @@ +#include "line-ed.h" + +#include +#include + +void alloc_empty_history_entry(struct line_ed *ed) +{ + b_string *str = (b_string *)b_array_at( + ed->l_history, b_array_size(ed->l_history) - 1); + if (!str || !b_string_get_size(str, B_STRLEN_NORMAL)) { + str = b_string_create(); + b_array_append(ed->l_history, (b_object *)str); + } + + ed->l_history_pos = b_array_size(ed->l_history) - 1; +} + +void save_buf_to_history(struct line_ed *ed) +{ + b_string *cur = (b_string *)b_array_get(ed->l_history, ed->l_history_pos); + b_string_replace_all(cur, ed->l_buf); +} + +void append_buf_to_history(struct line_ed *ed) +{ + b_string *cur = (b_string *)b_array_get(ed->l_history, ed->l_history_pos); + char s[] = {'\n', 0}; + b_string_append_cstr(cur, s); + b_string_append_cstr(cur, ed->l_buf); +} + +void load_buf_from_history(struct line_ed *ed) +{ + b_string *cur = (b_string *)b_array_at(ed->l_history, ed->l_history_pos); + size_t len + = MIN(ed->l_buf_end - ed->l_buf - 1, + b_string_get_size(cur, B_STRLEN_NORMAL)); + + memcpy(ed->l_buf, b_string_ptr(cur), len); + ed->l_buf[len] = '\0'; + + unsigned int x = 0, y = 0; + for (size_t i = 0; ed->l_buf[i]; i++) { + if (ed->l_buf[i] == '\n') { + x = 0; + y++; + } else { + x++; + } + } + + ed->l_buf_ptr = ed->l_buf + len; + ed->l_line_end = ed->l_buf_ptr; + ed->l_cursor_x = x; + ed->l_cursor_y = y; +} + +const char *last_history_line(struct line_ed *ed) +{ + unsigned long nlines = b_array_size(ed->l_history); + if (nlines < 2) { + return NULL; + } + + b_string *last = (b_string *)b_array_at(ed->l_history, nlines - 2); + return b_string_ptr(last); +} diff --git a/frontend/line-ed/history.h b/frontend/line-ed/history.h new file mode 100644 index 0000000..d166aa2 --- /dev/null +++ b/frontend/line-ed/history.h @@ -0,0 +1,12 @@ +#ifndef LINE_ED_HISTORY_H_ +#define LINE_ED_HISTORY_H_ + +struct line_ed; + +extern void alloc_empty_history_entry(struct line_ed *ed); +extern void save_buf_to_history(struct line_ed *ed); +extern void append_buf_to_history(struct line_ed *ed); +extern void load_buf_from_history(struct line_ed *ed); +extern const char *last_history_line(struct line_ed *ed); + +#endif diff --git a/frontend/line-ed/hl-range.c b/frontend/line-ed/hl-range.c new file mode 100644 index 0000000..04201ea --- /dev/null +++ b/frontend/line-ed/hl-range.c @@ -0,0 +1,287 @@ +#include "hl-range.h" + +#include "line-ed.h" +#include "prompt.h" + +#include +#include +#include + +int compare_coords( + unsigned int ax, unsigned int ay, unsigned int bx, unsigned int by) +{ + if (ay > by) { + return 1; + } + + if (ay < by) { + return -1; + } + + if (ax > bx) { + return 1; + } + + if (ax < bx) { + return -1; + } + + return 0; +} + +struct hl_range *get_hl_range(struct line_ed *ed, unsigned int x, unsigned int y) +{ + if (b_queue_empty(&ed->l_hl_ranges)) { + return NULL; + } + + struct hl_range *best_match = NULL; + + b_queue_iterator it; + b_queue_foreach (&it, &ed->l_hl_ranges) { + struct hl_range *cur = b_unbox(struct hl_range, it.entry, h_entry); + int cmp_end = compare_coords(x, y, cur->h_end_x, cur->h_end_y); + + if (cmp_end == 1) { + continue; + } + + return cur; + } + + return NULL; +} + +struct hl_range *get_next_hl_range(struct hl_range *range) +{ + b_queue_entry *entry = &range->h_entry; + entry = b_queue_next(entry); + if (!entry) { + return NULL; + } + + range = b_unbox(struct hl_range, entry, h_entry); + return range; +} + +int apply_hl_range( + struct hl_range *range, struct s_tty *tty, unsigned int x, unsigned int y) +{ + if (!range) { + s_tty_reset_vmode(tty); + return 0; + } + + int cmp_start = compare_coords(x, y, range->h_start_x, range->h_start_y); + int cmp_end = compare_coords(x, y, range->h_end_x, range->h_end_y); + + if (cmp_start < 0) { + s_tty_reset_vmode(tty); + } + + if (cmp_start >= 0 && cmp_end <= 0) { + s_tty_set_vmode(tty, &range->h_vmode); + } + + if (cmp_end == 1) { + return -1; + } + + return 0; +} + +struct hl_range *create_highlight( + unsigned long start_x, unsigned long start_y, unsigned long end_x, + unsigned long end_y, const struct s_tty_vmode *vmode) +{ + struct hl_range *out = malloc(sizeof *out); + if (!out) { + return NULL; + } + + memset(out, 0x0, sizeof *out); + + out->h_start_x = start_x; + out->h_start_y = start_y; + out->h_end_x = end_x; + out->h_end_y = end_y; + memcpy(&out->h_vmode, vmode, sizeof *vmode); + + return out; +} + +enum hl_range_comparison compare_hl_ranges( + const struct hl_range *a, const struct hl_range *b) +{ + int cmp_a_end_b_start = compare_coords( + a->h_end_x, a->h_end_y, b->h_start_x, b->h_start_y); + if (cmp_a_end_b_start == -1) { + return HL_RANGE_LESS; + } + + int cmp_b_end_a_start = compare_coords( + b->h_end_x, b->h_end_y, a->h_start_x, a->h_start_y); + if (cmp_b_end_a_start == -1) { + return HL_RANGE_GREATER; + } + + /* A end IS greater than B start + * B end IS greater than A start */ + + int cmp_a_start_b_start = compare_coords( + a->h_start_x, a->h_start_y, b->h_start_x, b->h_start_y); + int cmp_a_end_b_end + = compare_coords(a->h_end_x, a->h_end_y, b->h_end_x, b->h_end_y); + + switch (cmp_a_start_b_start) { + case 1: + return cmp_a_end_b_end <= 0 ? HL_RANGE_A_IN_B + : HL_RANGE_GREATER_OVERLAP; + case 0: + switch (cmp_a_end_b_end) { + case 1: + return HL_RANGE_B_IN_A; + case 0: + return HL_RANGE_EQUAL; + case -1: + return HL_RANGE_A_IN_B; + default: + break; + } + case -1: + return cmp_a_end_b_end >= 0 ? HL_RANGE_B_IN_A + : HL_RANGE_LESS_OVERLAP; + default: + break; + } + + /* should never reach here */ + return HL_RANGE_EQUAL; +} + +static void move_end_to_meet_start( + struct hl_range *move_end_of, struct hl_range *to_meet_start_of) +{ + move_end_of->h_end_y = to_meet_start_of->h_start_y; + + if (to_meet_start_of->h_start_x == 0) { + move_end_of->h_end_y--; + move_end_of->h_end_x = (unsigned int)-1; + } else { + move_end_of->h_end_x = to_meet_start_of->h_start_x - 1; + } +} + +static void move_start_to_meet_end( + struct hl_range *move_start_of, struct hl_range *to_meet_end_of) +{ + move_start_of->h_end_y = to_meet_end_of->h_end_y; + move_start_of->h_start_x = to_meet_end_of->h_end_x + 1; +} + +void line_ed_put_highlight( + struct line_ed *ed, unsigned long start_x, unsigned long start_y, + unsigned long end_x, unsigned long end_y, const struct s_tty_vmode *vmode) +{ + struct hl_range *highlight + = create_highlight(start_x, start_y, end_x, end_y, vmode); + if (!highlight) { + return; + } + + struct hl_range *h2 = NULL; + + b_queue_entry *entry = NULL; + entry = b_queue_first(&ed->l_hl_ranges); + + if (!entry) { + b_queue_push_back(&ed->l_hl_ranges, &highlight->h_entry); + return; + } + + struct hl_range *cur = NULL; + enum hl_range_comparison prev_cmp = -1; + + b_queue_entry *insert_before = NULL; + b_queue_entry *insert_after = NULL; + + bool end = false; + while (entry) { + b_queue_entry *next = b_queue_next(entry); + cur = b_unbox(struct hl_range, entry, h_entry); + + enum hl_range_comparison cmp = compare_hl_ranges(cur, highlight); + + switch (cmp) { + case HL_RANGE_A_IN_B: + b_queue_delete(&ed->l_hl_ranges, entry); + free(cur); + break; + case HL_RANGE_B_IN_A: + move_end_to_meet_start(cur, highlight); + h2 = create_highlight( + 0, 0, cur->h_end_x, cur->h_end_y, &cur->h_vmode); + move_start_to_meet_end(h2, highlight); + b_queue_insert_after( + &ed->l_hl_ranges, &highlight->h_entry, + &cur->h_entry); + b_queue_insert_after( + &ed->l_hl_ranges, &h2->h_entry, + &highlight->h_entry); + insert_before = insert_after = NULL; + end = true; + break; + case HL_RANGE_LESS_OVERLAP: + move_end_to_meet_start(cur, highlight); + case HL_RANGE_LESS: + insert_after = entry; + break; + case HL_RANGE_GREATER_OVERLAP: + move_start_to_meet_end(cur, highlight); + insert_before = entry; + break; + case HL_RANGE_GREATER: + b_queue_insert_before( + &ed->l_hl_ranges, &highlight->h_entry, entry); + insert_before = insert_after = NULL; + end = true; + break; + default: + break; + } + + entry = next; + if (end) { + break; + } + } + + if (insert_before) { + b_queue_insert_before( + &ed->l_hl_ranges, &highlight->h_entry, insert_before); + } else if (insert_after) { + b_queue_insert_after( + &ed->l_hl_ranges, &highlight->h_entry, insert_after); + } +} + +void line_ed_clear_highlights(struct line_ed *ed) +{ + b_queue_entry *entry = b_queue_pop_front(&ed->l_hl_ranges); + while (entry) { + struct hl_range *range = b_unbox(struct hl_range, entry, h_entry); + free(range); + + entry = b_queue_pop_front(&ed->l_hl_ranges); + } +} + +void line_ed_print_highlights(struct line_ed *ed) +{ + b_queue_iterator it; + b_queue_foreach (&it, &ed->l_hl_ranges) { + struct hl_range *h = b_unbox(struct hl_range, it.entry, h_entry); + printf("(%u, %u) -> (%u, %u)\n", h->h_start_x, h->h_start_y, + h->h_end_x, h->h_end_y); + } +} diff --git a/frontend/line-ed/hl-range.h b/frontend/line-ed/hl-range.h new file mode 100644 index 0000000..b455930 --- /dev/null +++ b/frontend/line-ed/hl-range.h @@ -0,0 +1,41 @@ +#ifndef LINE_ED_HL_RANGE_H_ +#define LINE_ED_HL_RANGE_H_ + +#include "tty.h" + +#include + +struct line_ed; + +enum hl_range_comparison { + HL_RANGE_LESS, + HL_RANGE_LESS_OVERLAP, + HL_RANGE_EQUAL, + HL_RANGE_A_IN_B, + HL_RANGE_B_IN_A, + HL_RANGE_GREATER_OVERLAP, + HL_RANGE_GREATER, +}; + +struct hl_range { + unsigned int h_start_x, h_start_y; + unsigned int h_end_x, h_end_y; + struct s_tty_vmode h_vmode; + b_queue_entry h_entry; +}; + +extern int compare_coords( + unsigned int ax, unsigned int ay, unsigned int bx, unsigned int by); +extern struct hl_range *get_hl_range( + struct line_ed *ed, unsigned int x, unsigned int y); +extern struct hl_range *get_next_hl_range(struct hl_range *range); +extern int apply_hl_range( + struct hl_range *range, struct s_tty *tty, unsigned int x, unsigned int y); + +extern struct hl_range *create_highlight( + unsigned long start_x, unsigned long start_y, unsigned long end_x, + unsigned long end_y, const struct s_tty_vmode *vmode); +extern enum hl_range_comparison compare_hl_ranges( + const struct hl_range *a, const struct hl_range *b); + +#endif diff --git a/frontend/line-ed/hook.c b/frontend/line-ed/hook.c new file mode 100644 index 0000000..3e48253 --- /dev/null +++ b/frontend/line-ed/hook.c @@ -0,0 +1,37 @@ +#include "hook.h" + +#include "line-ed.h" + +void line_ed_add_hook(struct line_ed *ed, struct line_ed_hook *hook) +{ + b_queue_push_back(&ed->l_hooks, &hook->hook_entry); +} + +void line_ed_remove_hook(struct line_ed *ed, struct line_ed_hook *hook) +{ + b_queue_delete(&ed->l_hooks, &hook->hook_entry); +} + +void hook_keypress(struct line_ed *ed, s_keycode key) +{ + b_queue_iterator it; + b_queue_foreach (&it, &ed->l_hooks) { + struct line_ed_hook *hook + = b_unbox(struct line_ed_hook, it.entry, hook_entry); + if (hook->hook_keypress) { + hook->hook_keypress(ed, hook, key); + } + } +} + +void hook_buffer_modified(struct line_ed *ed) +{ + b_queue_iterator it; + b_queue_foreach (&it, &ed->l_hooks) { + struct line_ed_hook *hook + = b_unbox(struct line_ed_hook, it.entry, hook_entry); + if (hook->hook_buffer_modified) { + hook->hook_buffer_modified(ed, hook); + } + } +} diff --git a/frontend/line-ed/hook.h b/frontend/line-ed/hook.h new file mode 100644 index 0000000..44098d7 --- /dev/null +++ b/frontend/line-ed/hook.h @@ -0,0 +1,16 @@ +#ifndef LINE_ED_HOOK_H_ +#define LINE_ED_HOOK_H_ + +#include "tty.h" + +enum hook_id { + HOOK_KEYPRESS, + HOOK_BEFORE_PAINT, +}; + +struct line_ed; + +extern void hook_keypress(struct line_ed *ed, s_keycode key); +extern void hook_buffer_modified(struct line_ed *ed); + +#endif diff --git a/frontend/line-ed/hooks/highlight.c b/frontend/line-ed/hooks/highlight.c new file mode 100644 index 0000000..9f64f94 --- /dev/null +++ b/frontend/line-ed/hooks/highlight.c @@ -0,0 +1,131 @@ +#include "highlight.h" + +#include +#include +#include + +#define LEX_SOURCE_TO_HIGHLIGHT(p) \ + ((struct highlight_hook *)((char *)p \ + - offsetof(struct highlight_hook, hook_src))) +#define HOOK_TO_HIGHLIGHT(p) \ + ((struct highlight_hook *)((char *)p \ + - offsetof(struct highlight_hook, hook_base))) + +static enum ivy_status readline( + struct ivy_line_source *source, char *out, size_t max, size_t *read, + const char *scope_type) +{ + struct highlight_hook *highlight = LEX_SOURCE_TO_HIGHLIGHT(source); + const char *bufp = highlight->hook_bufp; + + size_t r = 0; + for (size_t i = 0; i < max; i++) { + char c = *bufp; + + out[i] = c; + + if (c == '\0') { + break; + } + + r++; + bufp++; + + if (c == '\n') { + break; + } + } + + highlight->hook_bufp = bufp; + *read = r; + return r > 0 ? IVY_OK : IVY_ERR_EOF; +} + +static void highlight_token( + struct line_ed *ed, struct ivy_token *tok, struct s_tty_vmode *vmode) +{ + /* TODO re-enable this once tokens contain positional info */ +#if 0 + line_ed_put_highlight( + ed, tok->t_start_col - 1, tok->t_start_row - 1, + tok->t_end_col - 1, tok->t_end_row - 1, vmode); +#endif +} + +static void buffer_modified(struct line_ed *ed, struct line_ed_hook *hook) +{ +#if 0 + struct highlight_hook *highlight = HOOK_TO_HIGHLIGHT(hook); + line_ed_clear_highlights(ed); + lex_reset(highlight->hook_lex); + highlight->hook_bufp = ed->l_buf; + + struct s_tty_vmode vmode_number = MAKE_VMODE( + MAKE_COLOUR_16(TTY_COLOUR16_CYAN), MAKE_COLOUR_DEFAULT(), + TTY_ATTRIB_NORMAL); + struct s_tty_vmode vmode_keyword = MAKE_VMODE( + MAKE_COLOUR_16(TTY_COLOUR16_BRIGHT_YELLOW), + MAKE_COLOUR_DEFAULT(), TTY_ATTRIB_BOLD); + struct s_tty_vmode vmode_var = MAKE_VMODE( + MAKE_COLOUR_16(TTY_COLOUR16_MAGENTA), MAKE_COLOUR_DEFAULT(), + TTY_ATTRIB_ITALIC); + + struct lex_token *tok; + while (1) { + tok = lex_advance(highlight->hook_lex); + if (!tok) { + break; + } + + if (tok->t_start_col == 0) { + lex_token_destroy(tok); + continue; + } + + switch (tok->t_type) { + case TOK_NUMBER: + highlight_token(ed, tok, &vmode_number); + break; + case TOK_KEYWORD: + highlight_token(ed, tok, &vmode_keyword); + break; + case TOK_VAR: + highlight_token(ed, tok, &vmode_var); + break; + default: + break; + } + + lex_token_destroy(tok); + } +#endif +} + +extern struct highlight_hook *highlight_hook_create(void) +{ +#if 0 + struct highlight_hook *out = malloc(sizeof *out); + if (!out) { + return NULL; + } + + memset(out, 0x0, sizeof *out); + + out->hook_lex = lex_create(LEX_MODE_SPECULATIVE); + out->hook_base.hook_buffer_modified = buffer_modified; + out->hook_src.s_readline = readline; + + lex_set_source(out->hook_lex, &out->hook_src); + + return out; +#endif + return NULL; +} + +extern void highlight_hook_destroy(struct highlight_hook *hook) +{ +#if 0 + lex_destroy(hook->hook_lex); + free(hook); +#endif +} diff --git a/frontend/line-ed/hooks/highlight.h b/frontend/line-ed/hooks/highlight.h new file mode 100644 index 0000000..a01ea1f --- /dev/null +++ b/frontend/line-ed/hooks/highlight.h @@ -0,0 +1,20 @@ +#ifndef LINE_ED_HOOKS_HIGHLIGHT_H_ +#define LINE_ED_HOOKS_HIGHLIGHT_H_ + +#include "../line-ed.h" + +#include + +struct lex; + +struct highlight_hook { + struct line_ed_hook hook_base; + struct ivy_line_source hook_src; + const char *hook_bufp; + struct lex *hook_lex; +}; + +extern struct highlight_hook *highlight_hook_create(void); +extern void highlight_hook_destroy(struct highlight_hook *hook); + +#endif diff --git a/frontend/line-ed/input.c b/frontend/line-ed/input.c new file mode 100644 index 0000000..db576cb --- /dev/null +++ b/frontend/line-ed/input.c @@ -0,0 +1,204 @@ +#include "buffer.h" +#include "cursor.h" +#include "history.h" +#include "hook.h" +#include "line-ed.h" +#include "prompt.h" +#include "refresh.h" + +#include + +void put_char(struct line_ed *ed, char c) +{ + if (ed->l_buf_ptr > ed->l_line_end + 1) { + return; + } + + struct refresh_state state = { + .r_prev_cursor_x = ed->l_cursor_x, + .r_prev_cursor_y = ed->l_cursor_y, + }; + + unsigned int prev_cursor = ed->l_buf_ptr - ed->l_buf; + + char *dest = ed->l_buf_ptr; + unsigned int len = ed->l_line_end - ed->l_buf_ptr + 1; + + if (dest < ed->l_line_end) { + memmove(dest + 1, dest, len); + } + + ed->l_cursor_x++; + ed->l_line_end++; + ed->l_buf_ptr++; + *dest = c; + + if (ed->l_buf_ptr == ed->l_line_end) { + *ed->l_buf_ptr = '\0'; + } + + hook_buffer_modified(ed); + + put_refresh(ed, &state); +} + +static void backspace_simple(struct line_ed *ed) +{ + if (ed->l_buf_ptr == ed->l_buf) { + return; + } + + struct refresh_state state = { + .r_prev_cursor_x = ed->l_cursor_x, + .r_prev_cursor_y = ed->l_cursor_y, + }; + + unsigned int prev_cursor = ed->l_buf_ptr - ed->l_buf; + + char *dest = ed->l_buf_ptr; + unsigned int len = ed->l_line_end - ed->l_buf_ptr + 1; + memmove(dest - 1, dest, len); + + ed->l_cursor_x--; + ed->l_line_end--; + ed->l_buf_ptr--; + + hook_buffer_modified(ed); + + backspace_simple_refresh(ed, &state); +} + +static void backspace_nl(struct line_ed *ed) +{ + unsigned int prev_line_len = line_length(ed, ed->l_cursor_y - 1); + + struct refresh_state state = { + .r_prev_cursor_x = ed->l_cursor_x, + .r_prev_cursor_y = ed->l_cursor_y, + .r_prev_line_len = prev_line_len, + }; + + char *dest = ed->l_buf_ptr; + unsigned int len = ed->l_line_end - ed->l_buf_ptr + 1; + memmove(dest - 1, dest, len); + + ed->l_cursor_x = prev_line_len - 1; + ed->l_cursor_y--; + ed->l_buf_ptr--; + ed->l_line_end--; + + hook_buffer_modified(ed); + + backspace_nl_refresh(ed, &state); +} + +void backspace(struct line_ed *ed) +{ + if (ed->l_buf_ptr == ed->l_buf) { + return; + } + + if (ed->l_cursor_x == 0 && ed->l_cursor_y <= ed->l_continuations) { + return; + } + + if (ed->l_cursor_x == 0 && ed->l_cursor_y > 0) { + backspace_nl(ed); + } else { + backspace_simple(ed); + } +} + +void cursor_left(struct line_ed *ed) +{ + if (ed->l_cursor_x != 0) { + fputs("\010", stdout); + fflush(stdout); + ed->l_cursor_x--; + ed->l_buf_ptr--; + return; + } + + if (ed->l_cursor_y <= ed->l_continuations || ed->l_buf_ptr <= ed->l_buf) { + return; + } + + ed->l_cursor_y--; + ed->l_buf_ptr--; + unsigned int prompt_len = 0; + if (ed->l_cursor_y == 0) { + prompt_len = prompt_length(ed, PROMPT_MAIN); + } + + unsigned int len = line_length(ed, ed->l_cursor_y); + ed->l_cursor_x = len - 1; + + printf("\033[A\033[%dG", len + prompt_len); + fflush(stdout); +} + +void cursor_right(struct line_ed *ed) +{ + if (ed->l_buf_ptr >= ed->l_line_end) { + return; + } + + if (*ed->l_buf_ptr != '\n') { + ed->l_cursor_x++; + ed->l_buf_ptr++; + fputs("\033[C", stdout); + fflush(stdout); + return; + } + + if (ed->l_buf_ptr >= ed->l_line_end) { + return; + } + + ed->l_cursor_y++; + ed->l_cursor_x = 0; + ed->l_buf_ptr++; + + printf("\033[B\033[G"); + fflush(stdout); +} + +void arrow_up(struct line_ed *ed) +{ + if (ed->l_history_pos == 0) { + return; + } + + if (ed->l_cursor_y > 0) { + printf("\033[%uA", ed->l_cursor_y); + } + + printf("\033[%zuG\033[J", prompt_length(ed, PROMPT_MAIN) + 1); + + save_buf_to_history(ed); + ed->l_history_pos--; + load_buf_from_history(ed); + + print_buffer(ed); + fflush(stdout); +} + +void arrow_down(struct line_ed *ed) +{ + if (ed->l_history_pos == b_array_size(ed->l_history) - 1) { + return; + } + + if (ed->l_cursor_y > 0) { + printf("\033[%uA", ed->l_cursor_y); + } + + printf("\033[%zuG\033[J", prompt_length(ed, PROMPT_MAIN) + 1); + + save_buf_to_history(ed); + ed->l_history_pos++; + load_buf_from_history(ed); + + print_buffer(ed); + fflush(stdout); +} diff --git a/frontend/line-ed/input.h b/frontend/line-ed/input.h new file mode 100644 index 0000000..bbecf93 --- /dev/null +++ b/frontend/line-ed/input.h @@ -0,0 +1,13 @@ +#ifndef LINE_ED_INPUT_H_ +#define LINE_ED_INPUT_H_ + +struct line_ed; + +extern void put_char(struct line_ed *ed, char c); +extern void backspace(struct line_ed *ed); +extern void cursor_left(struct line_ed *ed); +extern void cursor_right(struct line_ed *ed); +extern void arrow_up(struct line_ed *ed); +extern void arrow_down(struct line_ed *ed); + +#endif diff --git a/frontend/line-ed/line-ed.c b/frontend/line-ed/line-ed.c new file mode 100644 index 0000000..b6e74e6 --- /dev/null +++ b/frontend/line-ed/line-ed.c @@ -0,0 +1,296 @@ +#include "line-ed.h" + +#include "history.h" +#include "hook.h" +#include "input.h" +#include "prompt.h" +#include "tty.h" + +#include +#include +#include +#include + +#define LINE_ED_FROM_LEX_SOURCE(p) \ + ((struct line_ed *)((char *)p - offsetof(struct line_ed, l_line_source))) + +static enum ivy_status readline( + struct ivy_line_source *src, char *out, size_t max, size_t *read, + const char *scope_type) +{ + struct line_ed *ed = LINE_ED_FROM_LEX_SOURCE(src); + + line_ed_set_scope_type(ed, scope_type); + long r = line_ed_readline(ed, out, max); + line_ed_set_scope_type(ed, NULL); + + if (r < 0) { + return IVY_ERR_EOF; + } + + *read = r; + return IVY_OK; +} + +struct line_ed *line_ed_create(void) +{ + struct line_ed *out = malloc(sizeof *out); + if (!out) { + return NULL; + } + + memset(out, 0x0, sizeof *out); + + out->l_buf = malloc(LINE_MAX); + if (!out->l_buf) { + free(out); + return NULL; + } + + out->l_history = b_array_create(); + if (!out->l_history) { + free(out->l_buf); + free(out); + return NULL; + } + + out->l_buf_end = out->l_buf + LINE_MAX; + out->l_buf_ptr = out->l_buf; + out->l_line_end = out->l_buf; + + out->l_prompt[PROMPT_MAIN] = ">>> "; + out->l_prompt[PROMPT_CONT] = "> "; + + out->l_line_source.s_readline = readline; + + return out; +} + +void line_ed_destroy(struct line_ed *ed) +{ + b_array_release(ed->l_history); + line_ed_clear_highlights(ed); + free(ed->l_buf); + free(ed); +} + +void line_ed_set_flags(struct line_ed *ed, enum line_ed_flags flags) +{ + ed->l_flags |= flags; +} + +void line_ed_set_scope_type(struct line_ed *ed, const char *scope_type) +{ + ed->l_scope_type = scope_type; +} + +static void clear_buffer(struct line_ed *ed) +{ + memset(ed->l_buf, 0x0, ed->l_buf_end - ed->l_buf); + + ed->l_buf_ptr = ed->l_buf; + ed->l_line_end = ed->l_buf; + + ed->l_cursor_x = 0; + ed->l_cursor_y = 0; + + ed->l_continuations = 0; +} + +static void convert_continuation_feeds(char *s, size_t max) +{ + char *end = s + max; + unsigned int len = strlen(s); + + while (1) { + unsigned int r = strcspn(s, "\\"); + if (s + r > end) { + break; + } + + s += r; + len -= r; + + if (*s == '\0') { + break; + } + + if (*(s + 1) != '\n') { + s++; + continue; + } + + memmove(s, s + 1, len); + s += 1; + } +} + +static void remove_continuation_feeds(char *s, size_t max) +{ + char *end = s + max; + unsigned int len = strlen(s); + + while (1) { + unsigned int r = strcspn(s, "\\"); + if (s + r > end) { + break; + } + + s += r; + len -= r; + + if (*s == '\0') { + break; + } + + if (*(s + 1) != '\n') { + s++; + continue; + } + + memmove(s, s + 2, len); + s += 2; + } +} + +static bool input_is_empty(struct line_ed *ed) +{ + const char *p = ed->l_buf; + while (p < ed->l_line_end) { + if (!isspace(*p)) { + return false; + } + } + + return true; +} + +long line_ed_readline(struct line_ed *ed, char *out, size_t max) +{ + clear_buffer(ed); + + bool append_history = false; + unsigned int append_to_index = (unsigned int)-1; + if (!ed->l_scope_type) { + alloc_empty_history_entry(ed); + } else { + append_history = true; + append_to_index = ed->l_history_pos; + } + + struct s_tty *tty = s_get_tty(); + s_tty_set_raw(tty); + show_prompt(ed); + + for (int i = 0; ed->l_buf[i]; i++) { + if (ed->l_buf[i] == '\n') { + fputc('\r', stdout); + } + + fputc(ed->l_buf[i], stdout); + } + fflush(stdout); + + bool end = false; + bool eof = false; + + while (!end) { + s_keycode key = s_tty_read_key(tty); + hook_keypress(ed, key); + + if (key == S_TTY_CTRL_KEY('d')) { + if (!input_is_empty(ed)) { + continue; + } + + eof = true; + break; + } + + switch (key) { + case S_KEY_RETURN: + s_tty_reset_vmode(tty); + if (ed->l_line_end > ed->l_buf + && *(ed->l_line_end - 1) != '\\') { + end = true; + break; + } + + if (input_is_empty(ed)) { + fputc('\r', stdout); + fputc('\n', stdout); + clear_buffer(ed); + show_prompt(ed); + break; + } + + *ed->l_line_end = '\n'; + ed->l_line_end++; + ed->l_buf_ptr = ed->l_line_end; + + ed->l_cursor_x = 0; + ed->l_cursor_y++; + ed->l_continuations++; + fputs("\033[G\n", stdout); + show_prompt(ed); + break; + case S_KEY_BACKSPACE: + backspace(ed); + break; + case S_KEY_ARROW_LEFT: + cursor_left(ed); + break; + case S_KEY_ARROW_RIGHT: + cursor_right(ed); + break; + case S_KEY_ARROW_UP: + arrow_up(ed); + break; + case S_KEY_ARROW_DOWN: + arrow_down(ed); + break; + default: + if (isgraph(key) || key == ' ') { + put_char(ed, key); + } + break; + } + } + + s_tty_set_canon(tty); + printf("\n"); + + if (*ed->l_buf == '\0') { + return eof ? -1 : 0; + } + + if (append_history) { + ed->l_history_pos = append_to_index; + append_buf_to_history(ed); + } else { + ed->l_history_pos = b_array_size(ed->l_history) - 1; + + const char *last = last_history_line(ed); + if (!last || strcmp(last, ed->l_buf) != 0) { + save_buf_to_history(ed); + } + } + + size_t sz = ed->l_line_end - ed->l_buf + 1; + sz = MIN(max - 1, sz); + memcpy(out, ed->l_buf, sz); + out[sz - 1] = '\n'; + out[sz] = '\0'; + + if ((ed->l_flags & LINE_ED_REMOVE_CONTINUATIONS) + == LINE_ED_REMOVE_CONTINUATIONS) { + remove_continuation_feeds(out, max); + } else if ( + (ed->l_flags & LINE_ED_CONVERT_CONTINUATIONS) + == LINE_ED_CONVERT_CONTINUATIONS) { + convert_continuation_feeds(out, max); + } + sz = strlen(out); + + return sz; +} diff --git a/frontend/line-ed/line-ed.h b/frontend/line-ed/line-ed.h new file mode 100644 index 0000000..5a6b556 --- /dev/null +++ b/frontend/line-ed/line-ed.h @@ -0,0 +1,96 @@ +#ifndef LINE_ED_H_ +#define LINE_ED_H_ + +#define LINE_MAX 4096 + +#include "tty.h" + +#include +#include +#include +#include + +struct s_tty_vmode; + +struct line_ed; + +struct line_ed_hook { + void (*hook_keypress)(struct line_ed *, struct line_ed_hook *, s_keycode); + void (*hook_buffer_modified)(struct line_ed *, struct line_ed_hook *); + b_queue_entry hook_entry; +}; + +enum line_ed_flags { + /* always reprint an entire line when a character is added/deleted. + * without this flag, only the modified character any subsequent + * characters are reprinted. */ + LINE_ED_FULL_REPRINT = 0x01u, + + /* keep line continuation (backslash-newline) tokens in the output + * buffer (default behaviour) */ + LINE_ED_KEEP_CONTINUATIONS = 0x00u, + /* convert line continuation tokens in the output buffer to simple + * linefeeds. */ + LINE_ED_CONVERT_CONTINUATIONS = 0x02u, + /* remove line continuation tokens from the output buffer, so that all + * chars are on a single line */ + LINE_ED_REMOVE_CONTINUATIONS = 0x06u, +}; + +struct line_ed { + enum line_ed_flags l_flags; + + /* array of basic prompt strings */ + const char *l_prompt[2]; + /* input buffer, pointer to the buffer cell that corresponds to + * the current cursor position, and pointer to the byte AFTER the last + * usable byte in the buffer */ + char *l_buf, *l_buf_ptr, *l_buf_end; + /* pointer to the byte AFTER the last byte of the user's input */ + char *l_line_end; + /* 2-dimensional coordinates of the current cursor position. + * this does NOT include any prompts that are visible on the terminal */ + unsigned int l_cursor_x, l_cursor_y; + /* the number of line continuations that have been inputted */ + unsigned int l_continuations; + + struct ivy_line_source l_line_source; + + /* the lexical scope that we are currently in. + * this is provided by components further up the input pipeline, + * for example, when we are inside a string or if-statement. */ + const char *l_scope_type; + + /* array of previously entered commands */ + b_array *l_history; + /* index of the currently selected history entry */ + unsigned int l_history_pos; + + /* list of defined highlight ranges */ + b_queue l_hl_ranges; + /* list of installed hooks */ + b_queue l_hooks; +}; + +extern struct line_ed *line_ed_create(void); +extern void line_ed_destroy(struct line_ed *ed); +extern void line_ed_set_flags(struct line_ed *ed, enum line_ed_flags flags); +extern void line_ed_set_scope_type(struct line_ed *ed, const char *scope_type); + +extern void line_ed_put_highlight( + struct line_ed *ed, unsigned long start_x, unsigned long start_y, + unsigned long end_x, unsigned long end_y, const struct s_tty_vmode *vmode); +extern void line_ed_clear_highlights(struct line_ed *ed); +extern void line_ed_print_highlights(struct line_ed *ed); + +extern void line_ed_add_hook(struct line_ed *ed, struct line_ed_hook *hook); +extern void line_ed_remove_hook(struct line_ed *ed, struct line_ed_hook *hook); + +extern long line_ed_readline(struct line_ed *ed, char *out, size_t max); + +static inline struct ivy_line_source *line_ed_to_line_source(struct line_ed *ed) +{ + return &ed->l_line_source; +} + +#endif diff --git a/frontend/line-ed/prompt.c b/frontend/line-ed/prompt.c new file mode 100644 index 0000000..75a34e9 --- /dev/null +++ b/frontend/line-ed/prompt.c @@ -0,0 +1,27 @@ +#include +#include "line-ed.h" +#include "prompt.h" + +void show_prompt(struct line_ed *ed) +{ + int type = PROMPT_MAIN; + if (ed->l_scope_type) { + type = PROMPT_CONT; + + /* this is a temporary solution to show the current + * scope type, until prompts are implemented properly. */ + fputs(ed->l_scope_type, stdout); + } + + if (ed->l_continuations > 0) { + type = PROMPT_CONT; + } + + fputs(ed->l_prompt[type], stdout); + fflush(stdout); +} + +unsigned long prompt_length(struct line_ed *ed, int prompt_id) +{ + return strlen(ed->l_prompt[prompt_id]); +} diff --git a/frontend/line-ed/prompt.h b/frontend/line-ed/prompt.h new file mode 100644 index 0000000..cdc4204 --- /dev/null +++ b/frontend/line-ed/prompt.h @@ -0,0 +1,12 @@ +#ifndef LINE_ED_PROMPT_H_ +#define LINE_ED_PROMPT_H_ + +#define PROMPT_MAIN 0 +#define PROMPT_CONT 1 + +struct line_ed; + +extern void show_prompt(struct line_ed *ed); +extern unsigned long prompt_length(struct line_ed *ed, int prompt_id); + +#endif diff --git a/frontend/line-ed/refresh.c b/frontend/line-ed/refresh.c new file mode 100644 index 0000000..89780f7 --- /dev/null +++ b/frontend/line-ed/refresh.c @@ -0,0 +1,242 @@ +#include +#include +#include "line-ed.h" +#include "refresh.h" +#include "buffer.h" +#include "cursor.h" +#include "hl-range.h" + +/* prints the provided string to the terminal, applying any relevant highlight ranges. + * this function prints all characters in `s` until it encounters a null char (\0) or + * linefeed (\n). + * + * the (x, y) coordinates provided should be the data coordinates of the + * first character in `s`. + */ +void print_text(struct line_ed *ed, unsigned int x, unsigned int y, const char *s) +{ + struct s_tty *tty = s_get_tty(); + struct hl_range *cur_range = get_hl_range(ed, x, y); + + for (unsigned int i = 0; s[i] != '\n' && s[i] != '\0'; i++) { + if (!cur_range) { + s_tty_reset_vmode(tty); + fputc(s[i], stdout); + continue; + } + + while (1) { + if (!cur_range) { + break; + } + + int hl_result = apply_hl_range(cur_range, tty, x + i, y); + if (hl_result == 0) { + break; + } + + cur_range = get_next_hl_range(cur_range); + apply_hl_range(cur_range, tty, x + i, y); + } + + fputc(s[i], stdout); + } +} + +void print_buffer(struct line_ed *ed) +{ + const char *line_buf = ed->l_buf; + size_t line_len = strcspn(line_buf, "\n"); + + unsigned int y = 0; + + while (1) { + print_text(ed,0, y, line_buf); + + line_buf += line_len; + if (*line_buf == '\n') { + line_buf++; + } + + if (*line_buf == '\0') { + break; + } + + y++; + line_len = strcspn(line_buf, "\n"); + + fputc('\r', stdout); + fputc('\n', stdout); + } +} + +/* this function is called after a character is inserted into the line_ed buffer. + * the function performs the following steps: + * 1. get a pointer to the start of the line that was modified. + * 2. determine the first character in the line that needs to be redrawn. + * this may result in an append, a partial reprint, or a full reprint. + * 3. re-print the relevant portion of the buffer: + * for an append (a character added to the end of the line): + * * write the inserted char to the terminal. + * * done. + * for a partial reprint: + * * clear all printed chars from the logical cursor position to the end of the line. + * * print the portion of the line corresponding to the cleared section. + * * move the physical (terminal) cursor backwards until its position + * matches the logical (line_ed) cursor. + * for a full reprint: + * * same as a partial reprint except that, rather than reprinting + * from the logical cursor position, the entire line is reprinted. + */ +void put_refresh(struct line_ed *ed, struct refresh_state *state) +{ + /* get the data for the line that is being refreshed */ + const char *line_buf = line_start(ed, ed->l_cursor_y); + size_t line_len = strcspn(line_buf, "\n"); + + /* the index of the first char in line_buf that needs to be reprinted */ + unsigned int start_x = state->r_prev_cursor_x; + + /* the distance between the first char to be reprinted and the end + * of the line. + * the physical cursor will be moved back by this amount after the + * line is reprinted. */ + unsigned int cursor_rdelta = line_len - start_x; + + if (ed->l_flags & LINE_ED_FULL_REPRINT) { + if (start_x) { + fprintf(stdout, "\033[%uD", start_x); + } + + start_x = 0; + } + + print_text(ed, start_x, ed->l_cursor_y, line_buf + start_x); + + /* decrement the rdelta (move the cursor back one fewer cells), + * so that the physical cursor will be placed AFTER the character that + * was just inserted. */ + cursor_rdelta--; + + for (unsigned int i = 0; i < cursor_rdelta; i++) { + fputs("\010", stdout); + } + + fflush(stdout); +} + +/* this function is called after a character is removed from the line_ed buffer. + * IF the character was a linefeed. + * + * this is separate from backspace_simple_refresh because, in this situation, + * the cursor position depends on the length of the previous line before + * the linefeed was deleted, and we have to reprint every line following the + * two that were combined. + */ +void backspace_nl_refresh(struct line_ed *ed, struct refresh_state *state) +{ + /* get the data for the line that is being refreshed. + * relative to where the cursor was before the linefeed was deleted, + * this is the PREVIOUS line. */ + const char *line_buf = line_start(ed, ed->l_cursor_y); + size_t line_len = strcspn(line_buf, "\n"); + + /* the index of the first char in line_buf that needs to be reprinted. + * subtract one to account for the linefeed that has since been deleted. */ + unsigned int start_x = state->r_prev_line_len - 1; + + /* the column to move the physical cursor to after it has been moved + * to the previous line. + * NOTE that this number includes the length of the prompt! + * we add 1 to start_x to ensure that the cursor is moved to the cell + * AFTER the last char of the line. */ + unsigned int new_x; + line_ed_coords_to_physical_coords(ed, start_x + 1, ed->l_cursor_y, &new_x, NULL); + + /* the physical cursor is currently at the beginning of the line that + * has just been moved up. from here, clear this line and the rest + * from the screen. */ + fputs("\033[J", stdout); + + if (ed->l_flags & LINE_ED_FULL_REPRINT) { + /* next, move the physical cursor up and to the beginning of the previous line */ + unsigned int tmp_x; + line_ed_coords_to_physical_coords(ed, 0, ed->l_cursor_y, &tmp_x, NULL); + fprintf(stdout, "\033[A\033[%uG", tmp_x + 1); + start_x = 0; + } else { + /* next, move the physical cursor up and to the end of the previous line */ + fprintf(stdout, "\033[A\033[%uG", new_x); + } + + /* now reprint all of the buffer lines, starting with the first of the + * two lines that were concatenated. */ + unsigned int ydiff = 0; + while (1) { + print_text(ed, start_x, ed->l_cursor_y + ydiff, line_buf + start_x); + + line_buf += line_len + 1; + line_len = strcspn(line_buf, "\n"); + start_x = 0; + + if (*line_buf == '\0') { + break; + } + + fputc('\r', stdout); + fputc('\n', stdout); + ydiff++; + } + + /* finally, move the cursor BACK to the point where the two lines + * were concatenated. */ + if (ydiff) { + fprintf(stdout, "\033[%uA", ydiff); + } + + fprintf(stdout, "\033[%uG", new_x); + fflush(stdout); +} + +/* this function is called after a character is removed from the line_ed buffer. + * IF the character was not a linefeed. + */ +void backspace_simple_refresh(struct line_ed *ed, struct refresh_state *state) +{ + /* get the data for the line that is being refreshed */ + const char *line_buf = line_start(ed, ed->l_cursor_y); + size_t line_len = strcspn(line_buf, "\n"); + + /* the index of the first char in line_buf that needs to be reprinted */ + unsigned int start_x = ed->l_cursor_x; + //get_data_cursor_position(ed, &start_x, NULL); + + /* the distance between the first char to be reprinted and the end + * of the line. + * the physical cursor will be moved back by this amount after the + * line is reprinted. */ + unsigned int cursor_rdelta = line_len - start_x; + + if (ed->l_flags & LINE_ED_FULL_REPRINT) { + if (start_x) { + fprintf(stdout, "\033[%uD", start_x); + } + + start_x = 0; + } + + fputc('\010', stdout); + print_text(ed, start_x, ed->l_cursor_y, line_buf + start_x); + fputc(' ', stdout); + + /* increment the rdelta (move the cursor back one more cell), so + * that the cursor will appear to move back one cell (to accord with + * the fact that backspace was just pressed) */ + cursor_rdelta++; + + for (unsigned int i = 0; i < cursor_rdelta; i++) { + fputs("\010", stdout); + } + + fflush(stdout); +} diff --git a/frontend/line-ed/refresh.h b/frontend/line-ed/refresh.h new file mode 100644 index 0000000..4efda08 --- /dev/null +++ b/frontend/line-ed/refresh.h @@ -0,0 +1,21 @@ +#ifndef LINE_ED_REFRESH_H_ +#define LINE_ED_REFRESH_H_ + +struct line_ed; + +struct refresh_state { + /* cursor position before the update was performed (excluding the prompt) */ + unsigned int r_prev_cursor_x, r_prev_cursor_y; + /* when a backspace results in two separate lines being combined, + * this property contains the length of the first of the two combined + * lines BEFORE the concotenation was performed */ + unsigned int r_prev_line_len; +}; + +extern void print_text(struct line_ed *ed, unsigned int x, unsigned int y, const char *s); +extern void print_buffer(struct line_ed *ed); +extern void put_refresh(struct line_ed *ed, struct refresh_state *state); +extern void backspace_nl_refresh(struct line_ed *ed, struct refresh_state *state); +extern void backspace_simple_refresh(struct line_ed *ed, struct refresh_state *state); + +#endif diff --git a/frontend/line-ed/tty.c b/frontend/line-ed/tty.c new file mode 100644 index 0000000..0368583 --- /dev/null +++ b/frontend/line-ed/tty.c @@ -0,0 +1,384 @@ +#include +#include +#include +#include +#include +#include "tty.h" + +#define ANSI_BOLD_ON "1" +#define ANSI_BOLD_OFF "22" +#define ANSI_ITALIC_ON "3" +#define ANSI_ITALIC_OFF "23" +#define ANSI_UNDERLINE_ON "4" +#define ANSI_UNDERLINE_OFF "24" + +#define ANSI_DEFAULTCOLOUR_FG "39" +#define ANSI_DEFAULTCOLOUR_BG "49" + +#define ANSI_256COLOUR_FG "38;5" +#define ANSI_256COLOUR_BG "48;5" + +#define ANSI_TRUECOLOUR_FG "38;2" +#define ANSI_TRUECOLOUR_BG "48;2" + +enum tty_flags { + TTY_INIT = 0x01u, + TTY_INTERACTIVE = 0x02u, +}; + +struct s_tty { + FILE *t_in, *t_out; + enum tty_flags t_flags; + struct termios t_ios_raw, t_ios_default; + struct s_tty_vmode t_vmode; + unsigned char t_mcount; +}; + +static struct s_tty tty = {}; + +static const char *ansi_colour16_fg[] = { + [TTY_COLOUR16_BLACK] = "30", + [TTY_COLOUR16_RED] = "31", + [TTY_COLOUR16_GREEN] = "32", + [TTY_COLOUR16_YELLOW] = "33", + [TTY_COLOUR16_BLUE] = "34", + [TTY_COLOUR16_MAGENTA] = "35", + [TTY_COLOUR16_CYAN] = "36", + [TTY_COLOUR16_WHITE] = "37", + [TTY_COLOUR16_GREY] = "90", + [TTY_COLOUR16_BRIGHT_RED] = "91", + [TTY_COLOUR16_BRIGHT_GREEN] = "92", + [TTY_COLOUR16_BRIGHT_YELLOW] = "93", + [TTY_COLOUR16_BRIGHT_BLUE] = "94", + [TTY_COLOUR16_BRIGHT_MAGENTA] = "95", + [TTY_COLOUR16_BRIGHT_CYAN] = "96", + [TTY_COLOUR16_BRIGHT_WHITE] = "97", +}; + +static const char *ansi_colour16_bg[] = { + [TTY_COLOUR16_BLACK] = "40", + [TTY_COLOUR16_RED] = "41", + [TTY_COLOUR16_GREEN] = "42", + [TTY_COLOUR16_YELLOW] = "43", + [TTY_COLOUR16_BLUE] = "44", + [TTY_COLOUR16_MAGENTA] = "45", + [TTY_COLOUR16_CYAN] = "46", + [TTY_COLOUR16_WHITE] = "47", + [TTY_COLOUR16_GREY] = "100", + [TTY_COLOUR16_BRIGHT_RED] = "101", + [TTY_COLOUR16_BRIGHT_GREEN] = "102", + [TTY_COLOUR16_BRIGHT_YELLOW] = "103", + [TTY_COLOUR16_BRIGHT_BLUE] = "104", + [TTY_COLOUR16_BRIGHT_MAGENTA] = "105", + [TTY_COLOUR16_BRIGHT_CYAN] = "106", + [TTY_COLOUR16_BRIGHT_WHITE] = "107", +}; + +static void init_tty(struct s_tty *tty, FILE *in, FILE *out) +{ + tty->t_in = in; + tty->t_out = out; + + int fd = fileno(in); + + if (isatty(fd)) { + tty->t_flags |= TTY_INTERACTIVE; + } + + tcgetattr(fd, &tty->t_ios_default); + memcpy(&tty->t_ios_raw, &tty->t_ios_default, sizeof tty->t_ios_raw); + + tty->t_ios_raw.c_iflag &= ~(ICRNL | IXON); + tty->t_ios_raw.c_oflag &= ~(OPOST); + tty->t_ios_raw.c_lflag &= ~(ECHO | ICANON | IEXTEN); + + tty->t_flags |= TTY_INIT; +} + +struct s_tty *s_get_tty(void) +{ + if (!(tty.t_flags & TTY_INIT)) { + init_tty(&tty, stdin, stdout); + } + + return &tty; +} + +bool s_tty_is_interactive(const struct s_tty *tty) +{ + return (tty->t_flags & TTY_INTERACTIVE) == TTY_INTERACTIVE; +} + +void s_tty_set_raw(struct s_tty *tty) +{ + tcsetattr(STDIN_FILENO, TCSAFLUSH, &tty->t_ios_raw); +} + +void s_tty_set_canon(struct s_tty *tty) +{ + tcsetattr(STDIN_FILENO, TCSAFLUSH, &tty->t_ios_default); +} + +void s_tty_reset_vmode(struct s_tty *tty) +{ + if (tty->t_vmode.v_fg.c_mode == TTY_COLOUR_NONE + && tty->t_vmode.v_bg.c_mode == TTY_COLOUR_NONE + && tty->t_vmode.v_attrib == TTY_ATTRIB_NORMAL) { + return; + } + + fprintf(tty->t_out, "\033[0;39;49m"); + + memset(&tty->t_vmode, 0x0, sizeof tty->t_vmode); + + tty->t_vmode.v_fg.c_mode = TTY_COLOUR_NONE; + tty->t_vmode.v_bg.c_mode = TTY_COLOUR_NONE; + tty->t_vmode.v_attrib = TTY_ATTRIB_NORMAL; +} + +static int compare_colour(const struct s_tty_colour *a, const struct s_tty_colour *b) +{ + if (a->c_mode != b->c_mode) { + return -1; + } + + switch (a->c_mode) { + case TTY_COLOUR_16: + if (a->c_16.value != b->c_16.value) { + return -1; + } + break; + case TTY_COLOUR_256: + if (a->c_256.value != b->c_256.value) { + return -1; + } + break; + case TTY_COLOUR_TRUE: + if (a->c_true.r != b->c_true.r) { + return -1; + } + + if (a->c_true.g != b->c_true.g) { + return -1; + } + + if (a->c_true.b != b->c_true.b) { + return -1; + } + break; + default: + break; + } + + return 0; +} + +static int compare_vmode(const struct s_tty_vmode *a, const struct s_tty_vmode *b) +{ + if (a->v_attrib != b->v_attrib) { + return -1; + } + + if (compare_colour(&a->v_fg, &b->v_fg) != 0) { + return -1; + } + + if (compare_colour(&a->v_bg, &b->v_bg) != 0) { + return -1; + } + + return 0; +} + +static void put_ansi_attrib(struct s_tty *tty, const char *s) +{ + if (tty->t_mcount > 0) { + fputs(";", tty->t_out); + } + + fputs(s, tty->t_out); + tty->t_mcount++; +} + +static void set_attrib(struct s_tty *tty, enum s_tty_attrib old, enum s_tty_attrib new) +{ + if (old == new) { + return; + } + + /* Bold ON */ + if (!(old & TTY_ATTRIB_BOLD) && new & TTY_ATTRIB_BOLD) { + put_ansi_attrib(tty, ANSI_BOLD_ON); + } + + /* Bold OFF */ + if (old & TTY_ATTRIB_BOLD && !(new & TTY_ATTRIB_BOLD)) { + put_ansi_attrib(tty, ANSI_BOLD_OFF); + } + + /* Underline ON */ + if (!(old & TTY_ATTRIB_UNDERLINE) && new & TTY_ATTRIB_UNDERLINE) { + put_ansi_attrib(tty, ANSI_UNDERLINE_ON); + } + + /* Underline OFF */ + if (old & TTY_ATTRIB_UNDERLINE && !(new & TTY_ATTRIB_UNDERLINE)) { + put_ansi_attrib(tty, ANSI_UNDERLINE_OFF); + } + + /* Italic ON */ + if (!(old & TTY_ATTRIB_ITALIC) && new & TTY_ATTRIB_ITALIC) { + put_ansi_attrib(tty, ANSI_ITALIC_ON); + } + + /* Italic OFF */ + if (old & TTY_ATTRIB_ITALIC && !(new & TTY_ATTRIB_ITALIC)) { + put_ansi_attrib(tty, ANSI_ITALIC_OFF); + } +} + +static void set_fg(struct s_tty *tty, const struct s_tty_colour *old, const struct s_tty_colour *new) +{ + if (compare_colour(old, new) == 0) { + return; + } + + char buf[8]; + + switch (new->c_mode) { + case TTY_COLOUR_NONE: + put_ansi_attrib(tty, ANSI_DEFAULTCOLOUR_FG); + break; + case TTY_COLOUR_16: + put_ansi_attrib(tty, ansi_colour16_fg[new->c_16.value]); + break; + case TTY_COLOUR_256: + put_ansi_attrib(tty, ANSI_256COLOUR_FG); + snprintf(buf, sizeof buf, "%u", new->c_256.value); + put_ansi_attrib(tty, buf); + break; + case TTY_COLOUR_TRUE: + put_ansi_attrib(tty, ANSI_TRUECOLOUR_FG); + snprintf(buf, sizeof buf, "%u", new->c_true.r); + put_ansi_attrib(tty, buf); + snprintf(buf, sizeof buf, "%u", new->c_true.g); + put_ansi_attrib(tty, buf); + snprintf(buf, sizeof buf, "%u", new->c_true.b); + put_ansi_attrib(tty, buf); + break; + default: + break; + } +} + +static void set_bg(struct s_tty *tty, const struct s_tty_colour *old, const struct s_tty_colour *new) +{ + if (compare_colour(old, new) == 0) { + return; + } + + char buf[8]; + + switch (new->c_mode) { + case TTY_COLOUR_NONE: + put_ansi_attrib(tty, ANSI_DEFAULTCOLOUR_BG); + break; + case TTY_COLOUR_16: + put_ansi_attrib(tty, ansi_colour16_bg[new->c_16.value]); + break; + case TTY_COLOUR_256: + put_ansi_attrib(tty, ANSI_256COLOUR_BG); + snprintf(buf, sizeof buf, "%u", new->c_256.value); + put_ansi_attrib(tty, buf); + break; + case TTY_COLOUR_TRUE: + put_ansi_attrib(tty, ANSI_TRUECOLOUR_BG); + snprintf(buf, sizeof buf, "%u", new->c_true.r); + put_ansi_attrib(tty, buf); + snprintf(buf, sizeof buf, "%u", new->c_true.g); + put_ansi_attrib(tty, buf); + snprintf(buf, sizeof buf, "%u", new->c_true.b); + put_ansi_attrib(tty, buf); + break; + default: + break; + } +} + +void s_tty_set_vmode(struct s_tty *tty, const struct s_tty_vmode *vmode) +{ + if (compare_vmode(&tty->t_vmode, vmode) == 0) { + return; + } + + tty->t_mcount = 0; + + fprintf(tty->t_out, "\033["); + + set_attrib(tty, tty->t_vmode.v_attrib, vmode->v_attrib); + set_fg(tty, &tty->t_vmode.v_fg, &vmode->v_fg); + set_bg(tty, &tty->t_vmode.v_bg, &vmode->v_bg); + + fprintf(tty->t_out, "m"); + + memcpy(&tty->t_vmode, vmode, sizeof *vmode); +} + +s_keycode s_tty_read_key(struct s_tty *tty) +{ + char c; + int v; + int fd = fileno(tty->t_in); + + while (1) { + v = read(fd, &c, 1); + if (v < 1) { + return S_KEY_EOF; + } + + if (c == '\r' || c == '\n') { + return S_KEY_RETURN; + } + + if (c == '\b' || c == 0x7F) { + return S_KEY_BACKSPACE; + } + + if (c >= 1 && c <= 26) { + return S_TTY_CTRL_KEY(c + 'a' - 1); + } + + if (c != 0x1b) { + return c; + } + + v = read(fd, &c, 1); + if (v < 1) { + return S_KEY_EOF; + } + + if (c != '[') { + continue; + } + + v = read(fd, &c, 1); + if (v < 1) { + return S_KEY_EOF; + } + + switch (c) { + case 'A': + return S_KEY_ARROW_UP; + case 'B': + return S_KEY_ARROW_DOWN; + case 'C': + return S_KEY_ARROW_RIGHT; + case 'D': + return S_KEY_ARROW_LEFT; + default: + continue; + } + } + + return c; +} diff --git a/frontend/line-ed/tty.h b/frontend/line-ed/tty.h new file mode 100644 index 0000000..717ec98 --- /dev/null +++ b/frontend/line-ed/tty.h @@ -0,0 +1,140 @@ +#ifndef DARWIN_TTY_H_ +#define DARWIN_TTY_H_ + +#include +#include + +#define S_TTY_CTRL_KEY(c) ((c) | S_MOD_CTRL) + +struct s_tty; + +#define MAKE_VMODE(fg, bg, a) \ + { \ + .v_fg = fg, \ + .v_bg = bg, \ + .v_attrib = (a) \ + } + +#define MAKE_COLOUR_DEFAULT(v) \ + { \ + .c_mode = TTY_COLOUR_NONE, \ + } + +#define MAKE_COLOUR_16(v) \ + { \ + .c_mode = TTY_COLOUR_16, \ + .c_16 = { \ + .value = (v) \ + } \ + } + +#define MAKE_COLOUR_256(v) \ + { \ + .c_mode = TTY_COLOUR_256, \ + .c_256 = { \ + .value = (v) \ + } \ + } + +#define MAKE_COLOUR_TRUE(cr, cg, cb) \ + { \ + .c_mode = TTY_COLOUR_TRUE, \ + .c_true = { \ + .r = (cr), \ + .g = (cg), \ + .b = (cb) \ + } \ + } + +typedef uint32_t s_keycode; + +/* codepoints U+F0000 to U+FFFFD are reserved areas in Unicode, free for + * application use. store special keycodes here */ +enum s_keys { + S_KEY_ARROW_LEFT = 0xF0000, + S_KEY_ARROW_RIGHT, + S_KEY_ARROW_UP, + S_KEY_ARROW_DOWN, + S_KEY_BACKSPACE, + S_KEY_RETURN, + + S_MOD_CTRL = 0x10000000, + + S_KEY_EOF = 0xFFFFFFFF, +}; + +enum s_tty_colour16 { + TTY_COLOUR16_BLACK = 0, + TTY_COLOUR16_RED, + TTY_COLOUR16_GREEN, + TTY_COLOUR16_YELLOW, + TTY_COLOUR16_BLUE, + TTY_COLOUR16_MAGENTA, + TTY_COLOUR16_CYAN, + TTY_COLOUR16_WHITE, + TTY_COLOUR16_GREY, + TTY_COLOUR16_BRIGHT_RED, + TTY_COLOUR16_BRIGHT_GREEN, + TTY_COLOUR16_BRIGHT_YELLOW, + TTY_COLOUR16_BRIGHT_BLUE, + TTY_COLOUR16_BRIGHT_MAGENTA, + TTY_COLOUR16_BRIGHT_CYAN, + TTY_COLOUR16_BRIGHT_WHITE, +}; + +enum s_tty_colour_mode { + TTY_COLOUR_NONE = 0, + TTY_COLOUR_16, + TTY_COLOUR_256, + TTY_COLOUR_TRUE, +}; + +enum s_tty_attrib { + TTY_ATTRIB_NORMAL = 0x00u, + TTY_ATTRIB_BOLD = 0x01u, + TTY_ATTRIB_UNDERLINE = 0x02u, + TTY_ATTRIB_ITALIC = 0x04u, +}; + +struct s_tty_colour { + enum s_tty_colour_mode c_mode; + + union { + struct { + uint8_t value; + } c_16; + + struct { + uint8_t value; + } c_256; + + struct { + uint8_t r; + uint8_t g; + uint8_t b; + } c_true; + }; +}; + +struct s_tty_vmode { + enum s_tty_attrib v_attrib; + struct s_tty_colour v_fg; + struct s_tty_colour v_bg; +}; + +static inline unsigned int s_keycode_get_key(s_keycode v) +{ + return v & 0x0FFFFFFF; +} + +extern struct s_tty *s_get_tty(void); +extern bool s_tty_is_interactive(const struct s_tty *tty); + +extern void s_tty_set_raw(struct s_tty *tty); +extern void s_tty_set_canon(struct s_tty *tty); +extern void s_tty_reset_vmode(struct s_tty *tty); +extern void s_tty_set_vmode(struct s_tty *tty, const struct s_tty_vmode *vmode); + +extern s_keycode s_tty_read_key(struct s_tty *tty); + +#endif