diff --git a/src/editor.c b/src/editor.c index aa20cbe..2a35de1 100644 --- a/src/editor.c +++ b/src/editor.c @@ -218,6 +218,7 @@ void editor_free(editor_t *ed) clear_edit_stack(&ed->redo_stack, &ed->redo_count); free(ed->undo_stack); free(ed->redo_stack); + free(ed->clipboard); buffer_free(&ed->buf); } @@ -748,3 +749,76 @@ void editor_redo(editor_t *ed) rec.old_text, rec.old_len, rec.new_text, rec.new_len); free_edit_record(&rec); } + +void editor_copy(editor_t *ed) +{ + if (!editor_has_selection(ed)) { + return; + } + size_t r1, c1, r2, c2; + editor_selection_range(ed, &r1, &c1, &r2, &c2); + + free(ed->clipboard); + ed->clipboard = buffer_extract_text(&ed->buf, r1, c1, r2, c2, &ed->clipboard_len); +} + +void editor_cut(editor_t *ed) +{ + if (!editor_has_selection(ed)) { + return; + } + editor_copy(ed); + + size_t r1, c1, r2, c2; + editor_selection_range(ed, &r1, &c1, &r2, &c2); + + edit_capture_t cap; + capture_before(ed, r1, c1, r2, c2, &cap); + + buffer_delete_range(&ed->buf, r1, c1, r2, c2); + ed->cy = r1; + ed->cx = c1; + ed->sel.active = false; + ed->goal_rx = editor_cx_to_rx(&ed->buf.lines[ed->cy], ed->cx); + + commit_edit(ed, &cap, ed->cy, ed->cx); +} + +static void insert_text_replacing_selection(editor_t *ed, const char *text, size_t len) +{ + size_t r1, c1, r2, c2; + if (editor_has_selection(ed)) { + editor_selection_range(ed, &r1, &c1, &r2, &c2); + } else { + r1 = r2 = ed->cy; + c1 = c2 = ed->cx; + } + + edit_capture_t cap; + capture_before(ed, r1, c1, r2, c2, &cap); + + if (editor_has_selection(ed)) { + delete_selection(ed); + } + buffer_insert_text(&ed->buf, ed->cy, ed->cx, text, len); + text_end_position(ed->cy, ed->cx, text, len, &ed->cy, &ed->cx); + ed->goal_rx = editor_cx_to_rx(&ed->buf.lines[ed->cy], ed->cx); + + commit_edit(ed, &cap, ed->cy, ed->cx); +} + +void editor_paste(editor_t *ed) +{ + if (ed->clipboard == NULL || ed->clipboard_len == 0) { + return; + } + insert_text_replacing_selection(ed, ed->clipboard, ed->clipboard_len); +} + +void editor_paste_text(editor_t *ed, const char *text, size_t len) +{ + if (len == 0) { + return; + } + insert_text_replacing_selection(ed, text, len); +} diff --git a/src/editor.h b/src/editor.h index e893232..437dfcd 100644 --- a/src/editor.h +++ b/src/editor.h @@ -62,6 +62,9 @@ typedef struct { size_t redo_count; size_t redo_cap; + char *clipboard; + size_t clipboard_len; + bool quit_confirm_pending; bool should_quit; } editor_t; @@ -107,4 +110,9 @@ void editor_scroll_view(editor_t *ed, int delta_lines); void editor_undo(editor_t *ed); void editor_redo(editor_t *ed); +void editor_copy(editor_t *ed); +void editor_cut(editor_t *ed); +void editor_paste(editor_t *ed); +void editor_paste_text(editor_t *ed, const char *text, size_t len); + #endif /* DAF_EDITOR_H */ diff --git a/src/input.c b/src/input.c index e288396..34dd4f4 100644 --- a/src/input.c +++ b/src/input.c @@ -201,6 +201,12 @@ void input_process_key(editor_t *ed, key_event_t key) return; } + if (key.type == KEY_PASTE) { + editor_paste_text(ed, key.paste_text, key.paste_len); + editor_set_status(ed, "Pasted from terminal"); + return; + } + bool is_ctrl_key = key.type == KEY_CHAR && (key.mods & MOD_CTRL) != 0; if (!(is_ctrl_key && key.codepoint == CTRL_KEY('q'))) { ed->quit_confirm_pending = false; @@ -219,6 +225,27 @@ void input_process_key(editor_t *ed, key_event_t key) editor_undo(ed); } else if (key.codepoint == CTRL_KEY('y')) { editor_redo(ed); + } else if (key.codepoint == CTRL_KEY('c')) { + if (editor_has_selection(ed)) { + editor_copy(ed); + terminal_set_clipboard(ed->clipboard, ed->clipboard_len); + editor_set_status(ed, "Copied"); + } else { + editor_set_status(ed, "Nothing selected"); + } + } else if (key.codepoint == CTRL_KEY('x')) { + if (editor_has_selection(ed)) { + editor_cut(ed); + terminal_set_clipboard(ed->clipboard, ed->clipboard_len); + editor_set_status(ed, "Cut"); + } else { + editor_set_status(ed, "Nothing selected"); + } + } else if (key.codepoint == CTRL_KEY('v')) { + if (ed->clipboard != NULL) { + editor_paste(ed); + editor_set_status(ed, "Pasted"); + } } return; } diff --git a/src/terminal.c b/src/terminal.c index 7d991e2..bf57e1e 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -14,10 +14,16 @@ #include #include +#include "common.h" + static struct termios g_orig_termios; static int g_raw_mode_active = 0; static volatile sig_atomic_t g_resized = 0; +static char *g_paste_buf = NULL; +static size_t g_paste_len = 0; +static size_t g_paste_cap = 0; + static void handle_sigwinch(int signum) { (void)signum; @@ -33,7 +39,7 @@ static void write_str(const char *s) void terminal_disable_raw_mode(void) { if (g_raw_mode_active) { - write_str("\x1b[?1006l\x1b[?1002l"); + write_str("\x1b[?2004l\x1b[?1006l\x1b[?1002l"); tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_orig_termios); g_raw_mode_active = 0; } @@ -60,7 +66,7 @@ void terminal_enable_raw_mode(void) exit(EXIT_FAILURE); } g_raw_mode_active = 1; - write_str("\x1b[?1002h\x1b[?1006h"); + write_str("\x1b[?1002h\x1b[?1006h\x1b[?2004h"); struct sigaction sa; memset(&sa, 0, sizeof(sa)); @@ -250,6 +256,51 @@ static key_event_t read_sgr_mouse_sequence(void) return ev; } +static void paste_buf_append(char c) +{ + if (g_paste_len + 1 > g_paste_cap) { + size_t new_cap = g_paste_cap == 0 ? 1024 : g_paste_cap * 2; + g_paste_buf = xrealloc(g_paste_buf, new_cap); + g_paste_cap = new_cap; + } + g_paste_buf[g_paste_len++] = c; +} + +static key_event_t read_bracketed_paste(void) +{ + static const char terminator[5] = {'[', '2', '0', '1', '~'}; + g_paste_len = 0; + + for (;;) { + char c; + if (read_raw_byte(&c) != 0) { + break; + } + if ((unsigned char)c != 0x1b) { + paste_buf_append(c); + continue; + } + + char seq[5]; + size_t got = 0; + for (; got < sizeof(seq); got++) { + if (try_read_byte(&seq[got], 30) != 0) { + break; + } + } + if (got == sizeof(seq) && memcmp(seq, terminator, sizeof(terminator)) == 0) { + break; + } + + paste_buf_append(0x1b); + for (size_t i = 0; i < got; i++) { + paste_buf_append(seq[i]); + } + } + + return (key_event_t){.type = KEY_PASTE, .paste_text = g_paste_buf, .paste_len = g_paste_len}; +} + static key_event_t read_csi_rest(char first_byte) { char body[16]; @@ -269,6 +320,10 @@ static key_event_t read_csi_rest(char first_byte) } body[idx] = '\0'; + if (strcmp(body, "200~") == 0) { + return read_bracketed_paste(); + } + return decode_csi_body(body); } @@ -352,3 +407,53 @@ key_event_t terminal_read_key(void) } return decode_utf8_char(uc); } + +static const char k_base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static size_t base64_encode(const unsigned char *data, size_t len, char *out) +{ + size_t out_len = 0; + size_t i = 0; + + for (; i + 3 <= len; i += 3) { + unsigned int n = ((unsigned int)data[i] << 16) | ((unsigned int)data[i + 1] << 8) | + (unsigned int)data[i + 2]; + out[out_len++] = k_base64_chars[(n >> 18) & 0x3F]; + out[out_len++] = k_base64_chars[(n >> 12) & 0x3F]; + out[out_len++] = k_base64_chars[(n >> 6) & 0x3F]; + out[out_len++] = k_base64_chars[n & 0x3F]; + } + + size_t rem = len - i; + if (rem == 1) { + unsigned int n = (unsigned int)data[i] << 16; + out[out_len++] = k_base64_chars[(n >> 18) & 0x3F]; + out[out_len++] = k_base64_chars[(n >> 12) & 0x3F]; + out[out_len++] = '='; + out[out_len++] = '='; + } else if (rem == 2) { + unsigned int n = ((unsigned int)data[i] << 16) | ((unsigned int)data[i + 1] << 8); + out[out_len++] = k_base64_chars[(n >> 18) & 0x3F]; + out[out_len++] = k_base64_chars[(n >> 12) & 0x3F]; + out[out_len++] = k_base64_chars[(n >> 6) & 0x3F]; + out[out_len++] = '='; + } + + return out_len; +} + +void terminal_set_clipboard(const char *text, size_t len) +{ + size_t cap = ((len + 2) / 3) * 4 + 1; + char *encoded = xmalloc(cap); + size_t encoded_len = base64_encode((const unsigned char *)text, len, encoded); + + write_str("\x1b]52;c;"); + ssize_t written = write(STDOUT_FILENO, encoded, encoded_len); + (void)written; + write_str("\x07"); + + free(encoded); +} diff --git a/src/terminal.h b/src/terminal.h index e33ccbb..2094e07 100644 --- a/src/terminal.h +++ b/src/terminal.h @@ -5,6 +5,8 @@ #ifndef DAF_TERMINAL_H #define DAF_TERMINAL_H +#include + enum key_type { KEY_NONE = 0, KEY_CHAR, @@ -22,6 +24,7 @@ enum key_type { KEY_TAB, KEY_ESCAPE, KEY_MOUSE, + KEY_PASTE, }; enum key_mod { @@ -48,6 +51,9 @@ typedef struct { enum mouse_button mouse_button; int mouse_row; int mouse_col; + + const char *paste_text; + size_t paste_len; } key_event_t; void terminal_enable_raw_mode(void); @@ -56,4 +62,6 @@ int terminal_get_window_size(int *rows, int *cols); int terminal_consume_resize(void); key_event_t terminal_read_key(void); +void terminal_set_clipboard(const char *text, size_t len); + #endif /* DAF_TERMINAL_H */ diff --git a/tests/meson.build b/tests/meson.build index 7e9f251..42db3ee 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,4 +1,4 @@ -test_names = ['buffer', 'search', 'encoding', 'filetype', 'editor', 'undo'] +test_names = ['buffer', 'search', 'encoding', 'filetype', 'editor', 'undo', 'clipboard'] foreach name : test_names test_exe = executable( diff --git a/tests/test_clipboard.c b/tests/test_clipboard.c new file mode 100644 index 0000000..4ea7af8 --- /dev/null +++ b/tests/test_clipboard.c @@ -0,0 +1,139 @@ +/* SPDX-FileCopyrightText: AnmiTaliDev + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include + +#include "editor.h" + +static void set_line(editor_t *ed, size_t row, const char *text) +{ + buffer_insert_bytes(&ed->buf, row, 0, text, strlen(text)); +} + +static void select_range(editor_t *ed, size_t row, size_t from, size_t to) +{ + ed->cy = row; + ed->cx = from; + ed->sel.active = true; + ed->sel.anchor_row = row; + ed->sel.anchor_col = from; + ed->cx = to; +} + +static void test_copy_paste(void) +{ + editor_t ed; + editor_init(&ed, 24, 80); + set_line(&ed, 0, "hello world"); + + select_range(&ed, 0, 0, 5); /* "hello" */ + editor_copy(&ed); + assert(ed.clipboard_len == 5); + assert(memcmp(ed.clipboard, "hello", 5) == 0); + assert(ed.buf.lines[0].len == 11); + + ed.cy = 0; + ed.cx = 11; + ed.sel.active = false; + editor_insert_codepoint(&ed, ' '); + editor_paste(&ed); + assert(ed.buf.lines[0].len == 17); + assert(memcmp(ed.buf.lines[0].chars, "hello world hello", 17) == 0); + assert(ed.cx == 17); + + editor_free(&ed); +} + +static void test_cut_is_undoable(void) +{ + editor_t ed; + editor_init(&ed, 24, 80); + set_line(&ed, 0, "hello world"); + + select_range(&ed, 0, 0, 6); /* "hello " */ + editor_cut(&ed); + assert(ed.buf.lines[0].len == 5); + assert(memcmp(ed.buf.lines[0].chars, "world", 5) == 0); + assert(ed.clipboard_len == 6); + assert(memcmp(ed.clipboard, "hello ", 6) == 0); + + editor_undo(&ed); + assert(ed.buf.lines[0].len == 11); + assert(memcmp(ed.buf.lines[0].chars, "hello world", 11) == 0); + + editor_free(&ed); +} + +static void test_paste_replaces_selection(void) +{ + editor_t ed; + editor_init(&ed, 24, 80); + set_line(&ed, 0, "hello world"); + + select_range(&ed, 0, 0, 5); + editor_copy(&ed); + + select_range(&ed, 0, 6, 11); /* "world" */ + editor_paste(&ed); + assert(memcmp(ed.buf.lines[0].chars, "hello hello", 11) == 0); + + editor_undo(&ed); + assert(memcmp(ed.buf.lines[0].chars, "hello world", 11) == 0); + + editor_free(&ed); +} + +static void test_paste_multiline(void) +{ + editor_t ed; + editor_init(&ed, 24, 80); + set_line(&ed, 0, "ab"); + buffer_split_line(&ed.buf, 0, 2); + set_line(&ed, 1, "cd"); + + ed.sel.active = true; + ed.sel.anchor_row = 0; + ed.sel.anchor_col = 0; + ed.cy = 1; + ed.cx = 2; + editor_copy(&ed); + assert(ed.clipboard_len == 5); + assert(memcmp(ed.clipboard, "ab\ncd", 5) == 0); + + ed.sel.active = false; + ed.cy = 1; + ed.cx = 2; + editor_paste(&ed); + assert(ed.buf.num_lines == 3); + assert(memcmp(ed.buf.lines[1].chars, "cdab", 4) == 0); + assert(memcmp(ed.buf.lines[2].chars, "cd", 2) == 0); + + editor_free(&ed); +} + +static void test_copy_without_selection_is_noop(void) +{ + editor_t ed; + editor_init(&ed, 24, 80); + set_line(&ed, 0, "text"); + + editor_copy(&ed); + assert(ed.clipboard == NULL); + + editor_paste(&ed); + assert(ed.buf.lines[0].len == 4); + + editor_free(&ed); +} + +int main(void) +{ + test_copy_paste(); + test_cut_is_undoable(); + test_paste_replaces_selection(); + test_paste_multiline(); + test_copy_without_selection_is_noop(); + return 0; +}