diff --git a/sh/Makefile b/sh/Makefile index b327b0e1..bd240c53 100644 --- a/sh/Makefile +++ b/sh/Makefile @@ -12,6 +12,12 @@ CXXFLAGS:=$(CXXFLAGS) -Wall -Wextra -fno-exceptions -fno-rtti BINARIES:=sh sortix-sh +SORTIX_SH_SRCS=\ +editline.cpp \ +sh.cpp \ +showline.cpp \ +util.cpp + all: $(BINARIES) .PHONY: all install clean @@ -20,8 +26,8 @@ install: all mkdir -p $(DESTDIR)$(BINDIR) install $(BINARIES) $(DESTDIR)$(BINDIR) -sortix-sh: sh.cpp - $(CXX) -std=gnu++11 $(CPPFLAGS) $(CXXFLAGS) $< -o $@ +sortix-sh: $(SORTIX_SH_SRCS) *.h + $(CXX) -std=gnu++11 $(CPPFLAGS) $(CXXFLAGS) $(SORTIX_SH_SRCS) -o $@ sh: proper-sh.cpp $(CXX) -std=gnu++11 $(CPPFLAGS) $(CXXFLAGS) $< -o $@ diff --git a/sh/editline.cpp b/sh/editline.cpp new file mode 100644 index 00000000..59cf522c --- /dev/null +++ b/sh/editline.cpp @@ -0,0 +1,621 @@ +/******************************************************************************* + + Copyright(C) Jonas 'Sortie' Termansen 2011, 2012, 2013, 2014, 2015. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . + + editline.h + Read a line from the terminal. + +*******************************************************************************/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "editline.h" +#include "showline.h" + +static const unsigned int NORMAL_TERMMODE = + TERMMODE_UNICODE | + TERMMODE_SIGNAL | + TERMMODE_UTF8 | + TERMMODE_LINEBUFFER | + TERMMODE_ECHO; + +void edit_line_show(struct edit_line* edit_state) +{ + size_t line_length = 0; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + line_length += strlen(edit_state->ps1); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + { + char mb[MB_CUR_MAX]; + line_length += wcrtomb(mb, edit_state->line[i], &ps); + if ( edit_state->line[i] == L'\n' ) + line_length += strlen(edit_state->ps2); + } + + char* line = (char*) malloc(line_length + 1); + assert(line); + + size_t cursor = 0; + size_t line_offset = 0; + memset(&ps, 0, sizeof(ps)); + + strcpy(line + line_offset, edit_state->ps1); + line_offset += strlen(edit_state->ps1); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + { + if ( edit_state->line_offset == i ) + cursor = line_offset; + line_offset += wcrtomb(line + line_offset, edit_state->line[i], &ps); + if ( edit_state->line[i] == L'\n' ) + { + strcpy(line + line_offset, edit_state->ps2); + line_offset += strlen(edit_state->ps2); + } + } + + if ( edit_state->line_offset == edit_state->line_used ) + cursor = line_offset; + + line[line_offset] = '\0'; + + show_line(&edit_state->show_state, line, cursor); + + free(line); +} + +char* edit_line_result(struct edit_line* edit_state) +{ + size_t result_length = 0; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + { + char mb[MB_CUR_MAX]; + result_length += wcrtomb(mb, edit_state->line[i], &ps); + } + + char* result = (char*) malloc(result_length + 1); + if ( !result ) + return NULL; + size_t result_offset = 0; + + memset(&ps, 0, sizeof(ps)); + + for ( size_t i = 0; i < edit_state->line_used; i++ ) + result_offset += wcrtomb(result + result_offset, edit_state->line[i], &ps); + + result[result_offset] = '\0'; + + return result; +} + +bool edit_line_can_finish(struct edit_line* edit_state) +{ + if ( !edit_state->check_input_incomplete ) + return true; + char* line = edit_line_result(edit_state); + assert(line); + bool result = !edit_state->check_input_incomplete( + edit_state->check_input_incomplete_context, line); + free(line); + return result; +} + +void edit_line_append_history(struct edit_line* edit_state, const char* line) +{ + if ( edit_state->history_used == edit_state->history_length ) + { + size_t new_length = 2 * edit_state->history_length; + if ( new_length == 0 ) + new_length = 16; + // TODO: Use reallocarray instead of realloc. + size_t new_size = sizeof(char*) * new_length; + char** new_history = (char**) realloc(edit_state->history, new_size); + assert(new_history); + edit_state->history = new_history; + edit_state->history_length = new_length; + } + + size_t history_index = edit_state->history_used++; + edit_state->history[history_index] = strdup(line); + assert(edit_state->history[history_index]); +} + +void edit_line_type_use_record(struct edit_line* edit_state, const char* record) +{ + free(edit_state->line); + edit_state->line_offset = 0; + edit_state->line_used = 0; + edit_state->line_length = 0; + + size_t line_length; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + size_t record_offset = 0; + for ( line_length = 0; true; line_length++ ) + { + size_t num_bytes = mbrtowc(NULL, record + record_offset, SIZE_MAX, &ps); + assert(num_bytes != (size_t) -2); + assert(num_bytes != (size_t) -1); + if ( num_bytes == 0 ) + break; + record_offset += num_bytes; + } + + // TODO: Avoid multiplication overflow. + wchar_t* line = (wchar_t*) malloc(sizeof(wchar_t) * line_length); + assert(line); + size_t line_used; + + memset(&ps, 0, sizeof(ps)); + + record_offset = 0; + for ( line_used = 0; line_used < line_length; line_used++ ) + { + size_t num_bytes = mbrtowc(&line[line_used], record + record_offset, SIZE_MAX, &ps); + assert(num_bytes != (size_t) -2); + assert(num_bytes != (size_t) -1); + assert(num_bytes != (size_t) 0); + record_offset += num_bytes; + } + + edit_state->line = line; + edit_state->line_offset = line_used; + edit_state->line_used = line_used; + edit_state->line_length = line_length; +} + +void edit_line_type_history_save_at(struct edit_line* edit_state, size_t index) +{ + assert(index <= edit_state->history_used); + + char* saved_line = edit_line_result(edit_state); + assert(saved_line); + if ( index == edit_state->history_used ) + { + edit_line_append_history(edit_state, saved_line); + free(saved_line); + } + else + { + free(edit_state->history[index]); + edit_state->history[index] = saved_line; + } +} + +void edit_line_type_history_save_current(struct edit_line* edit_state) +{ + edit_line_type_history_save_at(edit_state, edit_state->history_offset); +} + +void edit_line_type_history_prev(struct edit_line* edit_state) +{ + if ( edit_state->history_offset == 0 ) + return; + + edit_line_type_history_save_current(edit_state); + + const char* record = edit_state->history[--edit_state->history_offset]; + assert(record); + edit_line_type_use_record(edit_state, record); +} + +void edit_line_type_history_next(struct edit_line* edit_state) +{ + if ( edit_state->history_used - edit_state->history_offset <= 1 ) + return; + + edit_line_type_history_save_current(edit_state); + + const char* record = edit_state->history[++edit_state->history_offset]; + assert(record); + edit_line_type_use_record(edit_state, record); +} + +void edit_line_type_codepoint(struct edit_line* edit_state, wchar_t wc) +{ + if ( wc == L'\n' && edit_line_can_finish(edit_state)) + { + if ( edit_state->line_used ) + edit_line_type_history_save_at(edit_state, edit_state->history_target); + edit_state->editing = false; + return; + } + + if ( edit_state->line_used == edit_state->line_length ) + { + size_t new_length = 2 * edit_state->line_length; + if ( !new_length ) + new_length = 16; + // TODO: Use reallocarray instead of realloc. + size_t new_size = sizeof(wchar_t) * new_length; + wchar_t* new_line = (wchar_t*) realloc(edit_state->line, new_size); + assert(new_line); + edit_state->line = new_line; + edit_state->line_length = new_length; + } + + assert(edit_state->line_offset <= edit_state->line_used); + assert(edit_state->line_used <= edit_state->line_length); + + for ( size_t i = edit_state->line_used; i != edit_state->line_offset; i-- ) + edit_state->line[i] = edit_state->line[i-1]; + + edit_state->line[edit_state->line_used++, edit_state->line_offset++] = wc; + + assert(edit_state->line_offset <= edit_state->line_used); + assert(edit_state->line_used <= edit_state->line_length); +} + +void line_edit_type_home(struct edit_line* edit_state) +{ + edit_state->line_offset = 0; +} + +void line_edit_type_left(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == 0 ) + return; + edit_state->line_offset--; +} + +void line_edit_type_right(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == edit_state->line_used ) + return; + edit_state->line_offset++; +} + +void line_edit_type_end(struct edit_line* edit_state) +{ + edit_state->line_offset = edit_state->line_used; +} + +void line_edit_type_backspace(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == 0 ) + return; + edit_state->line_used--; + edit_state->line_offset--; + for ( size_t i = edit_state->line_offset; i < edit_state->line_used; i++ ) + edit_state->line[i] = edit_state->line[i+1]; +} + +void line_edit_type_previous_word(struct edit_line* edit_state) +{ + while ( edit_state->line_offset && + iswspace(edit_state->line[edit_state->line_offset-1]) ) + edit_state->line_offset--; + while ( edit_state->line_offset && + !iswspace(edit_state->line[edit_state->line_offset-1]) ) + edit_state->line_offset--; +} + +void line_edit_type_next_word(struct edit_line* edit_state) +{ + while ( edit_state->line_offset != edit_state->line_used && + iswspace(edit_state->line[edit_state->line_offset]) ) + edit_state->line_offset++; + while ( edit_state->line_offset != edit_state->line_used && + !iswspace(edit_state->line[edit_state->line_offset]) ) + edit_state->line_offset++; +} + +void line_edit_type_delete(struct edit_line* edit_state) +{ + if ( edit_state->line_offset == edit_state->line_used ) + return; + edit_state->line_used--; + for ( size_t i = edit_state->line_offset; i < edit_state->line_used; i++ ) + edit_state->line[i] = edit_state->line[i+1]; +} + +void line_edit_type_eof_or_delete(struct edit_line* edit_state) +{ + if ( edit_state->line_used ) + return line_edit_type_delete(edit_state); + edit_state->editing = false; + edit_state->eof_condition = true; + if ( edit_state->trap_eof_opportunity ) + edit_state->trap_eof_opportunity(edit_state->trap_eof_opportunity_context); +} + +void edit_line_type_interrupt(struct edit_line* edit_state) +{ + dprintf(edit_state->out_fd, "^C\n"); + edit_state->editing = false; + edit_state->abort_editing = true; +} + +void edit_line_type_kill_after(struct edit_line* edit_state) +{ + while ( edit_state->line_offset < edit_state->line_used ) + line_edit_type_delete(edit_state); +} + +void edit_line_type_kill_before(struct edit_line* edit_state) +{ + while ( edit_state->line_offset ) + line_edit_type_backspace(edit_state); +} + +void edit_line_type_clear(struct edit_line* edit_state) +{ + show_line_clear(&edit_state->show_state); +} + +void edit_line_type_delete_word_before(struct edit_line* edit_state) +{ + while ( edit_state->line_offset && + iswspace(edit_state->line[edit_state->line_offset-1]) ) + line_edit_type_backspace(edit_state); + while ( edit_state->line_offset && + !iswspace(edit_state->line[edit_state->line_offset-1]) ) + line_edit_type_backspace(edit_state); +} + +int edit_line_completion_sort(const void* a_ptr, const void* b_ptr) +{ + const char* a = *(const char**) a_ptr; + const char* b = *(const char**) b_ptr; + return strcmp(a, b); +} + +void edit_line_type_complete(struct edit_line* edit_state) +{ + if ( !edit_state->complete ) + return; + + char* partial = edit_line_result(edit_state); + if ( !partial ) + return; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + size_t complete_at = 0; + for ( size_t i = 0; i < edit_state->line_offset; i++ ) + { + char mb[MB_CUR_MAX]; + size_t num_bytes = wcrtomb(mb, edit_state->line[i], &ps); + assert(num_bytes != (size_t) -1); + assert(num_bytes != (size_t) 0); + complete_at += num_bytes; + } + + char** completions; + size_t used_before; + size_t used_after; + size_t num_completions = edit_state->complete( + &completions, + &used_before, + &used_after, + edit_state->complete_context, + partial, + complete_at); + + qsort(completions, num_completions, sizeof(char*), edit_line_completion_sort); + + size_t lcp = 0; + bool similar = true; + while ( num_completions && similar ) + { + char c = completions[0][lcp]; + if ( c == '\0' ) + break; + for ( size_t i = 1; similar && i < num_completions; i++ ) + { + if ( completions[i][lcp] != c ) + similar = false; + } + if ( similar ) + lcp++; + } + + bool prefix_ends_with_slash = false; + memset(&ps, 0, sizeof(ps)); + for ( size_t i = 0; i < lcp; ) + { + const char* completion = completions[0]; + wchar_t wc; + size_t num_bytes = mbrtowc(&wc, completion + i, lcp - i, &ps); + if ( num_bytes == (size_t) -2 ) + break; + assert(num_bytes != (size_t) -1); + assert(num_bytes != (size_t) 0); + edit_line_type_codepoint(edit_state, wc); + prefix_ends_with_slash = wc == L'/'; + i += num_bytes; + } + + if ( num_completions == 1 && !prefix_ends_with_slash ) + { + edit_line_type_codepoint(edit_state, ' '); + } + + if ( 2 <= num_completions && lcp == 0 && edit_state->double_tab ) + { + bool first = true; + for ( size_t i = 0; i < num_completions; i++ ) + { + const char* completion = completions[i]; + size_t length = used_before + strlen(completion) + used_after; + if ( !length ) + continue; + if ( first ) + show_line_finish(&edit_state->show_state); + // TODO: Use a reliable write. + if ( !first ) + write(edit_state->out_fd, " ", 1); + write(edit_state->out_fd, partial + complete_at - used_before, used_before); + write(edit_state->out_fd, completion, strlen(completion)); + write(edit_state->out_fd, partial + complete_at, used_after); + first = false; + } + if ( !first) + { + write(edit_state->out_fd, "\n", 1); + show_line_begin(&edit_state->show_state, edit_state->out_fd); + edit_line_show(edit_state); + } + } + + edit_state->double_tab = true; + + (void) used_before; + (void) used_after; + + for ( size_t i = 0; i < num_completions; i++ ) + free(completions[i]); + free(completions); + + free(partial); +} + +void edit_line_kbkey(struct edit_line* edit_state, int kbkey) +{ + if ( kbkey != KBKEY_TAB && kbkey != -KBKEY_TAB ) + edit_state->double_tab = false; + + if ( edit_state->left_control || edit_state->right_control ) + { + switch ( kbkey ) + { + case KBKEY_LEFT: line_edit_type_previous_word(edit_state); return; + case KBKEY_RIGHT: line_edit_type_next_word(edit_state); return; + }; + } + + switch ( kbkey ) + { + case KBKEY_HOME: line_edit_type_home(edit_state); return; + case KBKEY_LEFT: line_edit_type_left(edit_state); return; + case KBKEY_RIGHT: line_edit_type_right(edit_state); return; + case KBKEY_UP: edit_line_type_history_prev(edit_state); return; + case KBKEY_DOWN: edit_line_type_history_next(edit_state); return; + case KBKEY_END: line_edit_type_end(edit_state); return; + case KBKEY_BKSPC: line_edit_type_backspace(edit_state); return; + case KBKEY_DELETE: line_edit_type_delete(edit_state); return; + case KBKEY_TAB: edit_line_type_complete(edit_state); return; + case -KBKEY_LCTRL: edit_state->left_control = false; return; + case +KBKEY_LCTRL: edit_state->left_control = true; return; + case -KBKEY_RCTRL: edit_state->right_control = false; return; + case +KBKEY_RCTRL: edit_state->right_control = true; return; + }; +} + +void edit_line_codepoint(struct edit_line* edit_state, wchar_t wc) +{ + if ( (edit_state->left_control || edit_state->right_control) && + ((L'a' <= wc && wc <= L'z') || (L'A' <= wc && wc <= L'Z')) ) + { + if ( wc == L'a' || wc == L'A' ) + line_edit_type_home(edit_state); + if ( wc == L'b' || wc == L'B' ) + line_edit_type_left(edit_state); + if ( wc == L'c' || wc == L'C' ) + edit_line_type_interrupt(edit_state); + if ( wc == L'd' || wc == L'D' ) + line_edit_type_eof_or_delete(edit_state); + if ( wc == L'e' || wc == L'E' ) + line_edit_type_end(edit_state); + if ( wc == L'f' || wc == L'F' ) + line_edit_type_right(edit_state); + if ( wc == L'k' || wc == L'K' ) + edit_line_type_kill_after(edit_state); + if ( wc == L'l' || wc == L'L' ) + show_line_clear(&edit_state->show_state); + if ( wc == L'u' || wc == L'U' ) + edit_line_type_kill_before(edit_state); + if ( wc == L'w' || wc == L'W' ) + edit_line_type_delete_word_before(edit_state); + return; + } + + if ( wc == L'\b' ) + return; + if ( wc == L'\t' ) + return; + + edit_line_type_codepoint(edit_state, wc); +} + +void edit_line(struct edit_line* edit_state) +{ + edit_state->editing = true; + edit_state->abort_editing = false; + edit_state->eof_condition = false; + edit_state->double_tab = false; + + free(edit_state->line); + edit_state->line = NULL; + edit_state->line_offset = 0; + edit_state->line_used = 0; + edit_state->line_length = 0; + edit_state->history_offset = edit_state->history_used; + edit_state->history_target = edit_state->history_used; + + settermmode(edit_state->in_fd, TERMMODE_KBKEY | TERMMODE_UNICODE); + + show_line_begin(&edit_state->show_state, edit_state->out_fd); + + while ( edit_state->editing ) + { + edit_line_show(edit_state); + + uint32_t codepoint; + if ( read(0, &codepoint, sizeof(codepoint)) != sizeof(codepoint) ) + { + edit_state->eof_condition = true; + edit_state->abort_editing = true; + break; + } + + if ( int kbkey = KBKEY_DECODE(codepoint) ) + edit_line_kbkey(edit_state, kbkey); + else + edit_line_codepoint(edit_state, (wchar_t) codepoint); + } + + if ( edit_state->abort_editing ) + show_line_abort(&edit_state->show_state); + else + { + edit_line_show(edit_state); + show_line_finish(&edit_state->show_state); + } + + settermmode(edit_state->in_fd, NORMAL_TERMMODE); +} diff --git a/sh/editline.h b/sh/editline.h new file mode 100644 index 00000000..b53814ce --- /dev/null +++ b/sh/editline.h @@ -0,0 +1,91 @@ +/******************************************************************************* + + Copyright(C) Jonas 'Sortie' Termansen 2011, 2012, 2013, 2014, 2015. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . + + editline.h + Read a line from the terminal. + +*******************************************************************************/ + +#ifndef EDITLINE_H +#define EDITLINE_H + +#include + +#include "showline.h" + +struct edit_line +{ + const char* ps1; + const char* ps2; + struct show_line show_state; + wchar_t* line; + size_t line_offset; + size_t line_used; + size_t line_length; + char** history; + size_t history_offset; + size_t history_used; + size_t history_length; + size_t history_target; + void* check_input_incomplete_context; + bool (*check_input_incomplete)(void*, const char*); + void* trap_eof_opportunity_context; + void (*trap_eof_opportunity)(void*); + void* complete_context; + size_t (*complete)(char***, size_t*, size_t*, void*, const char*, size_t); + int in_fd; + int out_fd; + bool editing; + bool abort_editing; + bool eof_condition; + bool double_tab; + // TODO: Should these be stored here, or outside the line editing context? + bool left_control; + bool right_control; +}; + +void edit_line_show(struct edit_line* edit_state); +char* edit_line_result(struct edit_line* edit_state); +bool edit_line_can_finish(struct edit_line* edit_state); +void edit_line_append_history(struct edit_line* edit_state, const char* line); +void edit_line_type_use_record(struct edit_line* edit_state, const char* record); +void edit_line_type_history_save_at(struct edit_line* edit_state, size_t index); +void edit_line_type_history_save_current(struct edit_line* edit_state); +void edit_line_type_history_prev(struct edit_line* edit_state); +void edit_line_type_history_next(struct edit_line* edit_state); +void edit_line_type_codepoint(struct edit_line* edit_state, wchar_t wc); +void line_edit_type_home(struct edit_line* edit_state); +void line_edit_type_left(struct edit_line* edit_state); +void line_edit_type_right(struct edit_line* edit_state); +void line_edit_type_end(struct edit_line* edit_state); +void line_edit_type_backspace(struct edit_line* edit_state); +void line_edit_type_previous_word(struct edit_line* edit_state); +void line_edit_type_next_word(struct edit_line* edit_state); +void line_edit_type_delete(struct edit_line* edit_state); +void line_edit_type_eof_or_delete(struct edit_line* edit_state); +void edit_line_type_interrupt(struct edit_line* edit_state); +void edit_line_type_kill_after(struct edit_line* edit_state); +void edit_line_type_kill_before(struct edit_line* edit_state); +void edit_line_type_clear(struct edit_line* edit_state); +void edit_line_type_delete_word_before(struct edit_line* edit_state); +int edit_line_completion_sort(const void* a_ptr, const void* b_ptr); +void edit_line_type_complete(struct edit_line* edit_state); +void edit_line_kbkey(struct edit_line* edit_state, int kbkey); +void edit_line_codepoint(struct edit_line* edit_state, wchar_t wc); +void edit_line(struct edit_line* edit_state); + +#endif diff --git a/sh/sh.cpp b/sh/sh.cpp index 469eee9f..fa09c547 100644 --- a/sh/sh.cpp +++ b/sh/sh.cpp @@ -20,8 +20,6 @@ *******************************************************************************/ -#include -#include #include #include @@ -43,17 +41,14 @@ #include #include +#include "editline.h" +#include "showline.h" +#include "util.h" + #if !defined(VERSIONSTR) #define VERSIONSTR "unknown version" #endif -static const unsigned int NORMAL_TERMMODE = - TERMMODE_UNICODE | - TERMMODE_SIGNAL | - TERMMODE_UTF8 | - TERMMODE_LINEBUFFER | - TERMMODE_ECHO; - const char* builtin_commands[] = { "cd", @@ -63,991 +58,8 @@ const char* builtin_commands[] = (const char*) NULL, }; -// TODO: Predict the terminal colors as well! -struct cursor_predict -{ - bool escaped; -}; - -struct wincurpos predict_cursor(struct cursor_predict* cursor_predict, - struct wincurpos wcp, - struct winsize ws, - wchar_t c) -{ - if ( c == L'\0' ) - return wcp; - - if ( cursor_predict->escaped ) - { - if ( (L'a' <= c && c <= L'z') || (L'A' <= c && c <= L'Z') ) - cursor_predict->escaped = false; - return wcp; - } - - if ( c == L'\e' ) - { - cursor_predict->escaped = true; - return wcp; - } - - if ( c == L'\n' || ws.ws_col <= wcp.wcp_col + 1 ) - { - wcp.wcp_col = 0; - if ( wcp.wcp_row + 1 < ws.ws_row ) - wcp.wcp_row++; - } - else - { - wcp.wcp_col++; - } - - return wcp; -} - -bool predict_will_scroll(struct cursor_predict cursor_predict, - struct wincurpos wcp, - struct winsize ws, - wchar_t c) -{ - if ( c == L'\0' ) - return false; - if ( cursor_predict.escaped ) - return false; - return (c == L'\n' || ws.ws_col <= wcp.wcp_col + 1) && - !(wcp.wcp_row + 1 < ws.ws_row); -} - -struct show_line -{ - struct wincurpos wcp_start; - struct wincurpos wcp_current; - struct winsize ws; - int out_fd; - char* current_line; - size_t current_cursor; - bool invalidated; -}; - -void show_line_begin(struct show_line* show_state, int out_fd) -{ - memset(show_state, 0, sizeof(*show_state)); - show_state->out_fd = out_fd; - show_state->current_line = NULL; - show_state->current_cursor = 0; - tcgetwincurpos(out_fd, &show_state->wcp_start); - show_state->wcp_current = show_state->wcp_start; - tcgetwinsize(show_state->out_fd, &show_state->ws); -} - -bool show_line_is_weird(const char* line) -{ - for ( size_t i = 0; line[i]; i++ ) - { - if ( line[i] == '\e' ) - { - i++; - if ( line[i] != '[' ) - return true; - i++; - while ( ('0' <= line[i] && line[i] <= '9') || line[i] == ';' ) - i++; - switch ( line[i] ) - { - case 'm': break; - default: return true; - } - continue; - } - - switch ( line[i] ) - { - case '\a': return true; - case '\b': return true; - case '\f': return true; - case '\r': return true; - case '\t': return true; // TODO: This isn't weird. - case '\v': return true; - default: break; - } - } - - return false; -} - -void show_line_change_cursor(struct show_line* show_state, struct wincurpos wcp) -{ - if ( wcp.wcp_col == show_state->wcp_current.wcp_col && - wcp.wcp_row == show_state->wcp_current.wcp_row ) - return; - - if ( wcp.wcp_col == 0 ) - dprintf(show_state->out_fd, "\e[%zuH", wcp.wcp_row + 1); - else - dprintf(show_state->out_fd, "\e[%zu;%zuH", wcp.wcp_row + 1, wcp.wcp_col+ 1); - - show_state->wcp_current = wcp; -} - -bool show_line_optimized(struct show_line* show_state, const char* line, size_t cursor) -{ - struct winsize ws = show_state->ws; - - mbstate_t old_ps; - mbstate_t new_ps; - memset(&old_ps, 0, sizeof(old_ps)); - memset(&new_ps, 0, sizeof(new_ps)); - struct wincurpos old_wcp = show_state->wcp_start; - struct wincurpos new_wcp = show_state->wcp_start; - struct cursor_predict old_cursor_predict; - struct cursor_predict new_cursor_predict; - memset(&old_cursor_predict, 0, sizeof(old_cursor_predict)); - memset(&new_cursor_predict, 0, sizeof(new_cursor_predict)); - size_t old_line_offset = 0; - size_t new_line_offset = 0; - const char* old_line = show_state->current_line; - const char* new_line = line; - - struct wincurpos cursor_wcp = show_state->wcp_start; - - while ( true ) - { - if ( cursor == new_line_offset ) - cursor_wcp = new_wcp; - - wchar_t old_wc; - wchar_t new_wc; - - size_t old_num_bytes = mbrtowc(&old_wc, old_line + old_line_offset, SIZE_MAX, &old_ps); - size_t new_num_bytes = mbrtowc(&new_wc, new_line + new_line_offset, SIZE_MAX, &new_ps); - assert(old_num_bytes != (size_t) -2); - assert(new_num_bytes != (size_t) -2); - assert(old_num_bytes != (size_t) -1); - assert(new_num_bytes != (size_t) -1); - if ( old_num_bytes == 0 && new_num_bytes == 0 ) - break; - - bool will_scroll = predict_will_scroll(new_cursor_predict, new_wcp, ws, new_wc); - bool can_scroll = show_state->wcp_start.wcp_row != 0; - - if ( will_scroll && !can_scroll ) - { - if ( new_line_offset < cursor ) - cursor_wcp = new_wcp; - break; - } - - if ( predict_will_scroll(old_cursor_predict, old_wcp, ws, old_wc) ) - break; - - struct wincurpos next_old_wcp = predict_cursor(&old_cursor_predict, old_wcp, ws, old_wc); - struct wincurpos next_new_wcp = predict_cursor(&new_cursor_predict, new_wcp, ws, new_wc); - - if ( old_wc != new_wc || - old_wcp.wcp_row != new_wcp.wcp_row || - old_wcp.wcp_col != new_wcp.wcp_col ) - { - // TODO: Use a reliable write instead! - - if ( old_wc == L'\n' && new_wc == L'\n' ) - { - // Good enough as newlines are invisible. - } - else if ( old_wc == L'\n' && new_wc != L'\0' ) - { - show_line_change_cursor(show_state, new_wcp); - write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); - show_state->wcp_current = next_new_wcp; - old_num_bytes = 0; - } - else if ( old_wc != L'\0' && new_wc == '\n' ) - { - show_line_change_cursor(show_state, old_wcp); - write(show_state->out_fd, " ", 1); - show_state->wcp_current = next_old_wcp; - new_num_bytes = 0; - } - else if ( old_wc == L'\n' && new_wc == L'\0' ) - { - // No need to do anything here as newlines are visible. - } - else if ( old_wc == L'\0' && new_wc == L'\n' ) - { - show_line_change_cursor(show_state, new_wcp); - write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); - show_state->wcp_current = next_new_wcp; - } - else if ( old_wcp.wcp_row != new_wcp.wcp_row || - old_wcp.wcp_col != new_wcp.wcp_col ) - return false; - else if ( new_wc == L'\0' && old_wc != L'\0' ) - { - show_line_change_cursor(show_state, old_wcp); - write(show_state->out_fd, " ", 1); - show_state->wcp_current = next_old_wcp; - } - else if ( new_wc != L'\0' ) - { - show_line_change_cursor(show_state, new_wcp); - write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); - show_state->wcp_current = next_new_wcp; - } - } - - if ( will_scroll && can_scroll ) - { - cursor_wcp.wcp_row--; - next_old_wcp.wcp_row--; - show_state->wcp_start.wcp_row--; - } - - old_wcp = next_old_wcp; - new_wcp = next_new_wcp; - - old_line_offset += old_num_bytes; - new_line_offset += new_num_bytes; - } - - show_line_change_cursor(show_state, cursor_wcp); - - free(show_state->current_line); - show_state->current_line = strdup(line); - assert(show_state->current_line); - show_state->current_cursor = cursor; - - return true; -} - -void show_line(struct show_line* show_state, const char* line, size_t cursor) -{ - // TODO: We don't currently invalidate on SIGWINCH. - struct winsize ws; - tcgetwinsize(show_state->out_fd, &ws); - if ( ws.ws_col != show_state->ws.ws_col || - ws.ws_row != show_state->ws.ws_row ) - { - // TODO: What if wcp_start isn't inside the window any longer? - show_state->invalidated = true; - show_state->ws = ws; - } - - // Attempt to do an optimized line re-rendering reusing the characters - // already present on the console. Bail out if this turns out to be harder - // than expected and re-render everything from scratch instead. - if ( !show_state->invalidated && - show_state->current_line && - !show_line_is_weird(show_state->current_line) && - !show_line_is_weird(line) ) - { - if ( show_line_optimized(show_state, line, cursor) ) - return; - show_state->invalidated = true; - } - - show_line_change_cursor(show_state, show_state->wcp_start); - - dprintf(show_state->out_fd, "\e[m"); - - if ( show_state->invalidated || show_state->current_line ) - dprintf(show_state->out_fd, "\e[0J"); - - struct cursor_predict cursor_predict; - memset(&cursor_predict, 0, sizeof(cursor_predict)); - struct wincurpos wcp = show_state->wcp_start; - struct wincurpos cursor_wcp = wcp; - - mbstate_t ps; - memset(&ps, 0, sizeof(ps)); - for ( size_t i = 0; true; ) - { - if ( cursor == i ) - cursor_wcp = wcp; - wchar_t wc; - size_t num_bytes = mbrtowc(&wc, line + i, SIZE_MAX, &ps); - assert(num_bytes != (size_t) -2); - assert(num_bytes != (size_t) -1); - if ( num_bytes == 0 ) - break; - bool will_scroll = predict_will_scroll(cursor_predict, wcp, ws, wc); - bool can_scroll = show_state->wcp_start.wcp_row != 0; - if ( will_scroll && !can_scroll ) - { - if ( i < cursor ) - cursor_wcp = wcp; - break; - } - // TODO: Use a reliable write. - write(show_state->out_fd, line + i, num_bytes); - if ( will_scroll && can_scroll ) - { - cursor_wcp.wcp_row--; - show_state->wcp_start.wcp_row--; - } - wcp = predict_cursor(&cursor_predict, wcp, ws, wc); - i += num_bytes; - } - - dprintf(show_state->out_fd, "\e[%zu;%zuH", - cursor_wcp.wcp_row + 1, - cursor_wcp.wcp_col + 1); - - show_state->wcp_current = wcp; - - free(show_state->current_line); - show_state->current_line = strdup(line); - assert(show_state->current_line); - show_state->current_cursor = cursor; - - show_state->invalidated = false; -} - -void show_line_clear(struct show_line* show_state) -{ - dprintf(show_state->out_fd, "\e[H\e[2J"); - - show_state->wcp_start.wcp_row = 0; - show_state->wcp_start.wcp_col = 0; - show_state->invalidated = true; - - show_line(show_state, show_state->current_line, strlen(show_state->current_line)); -} - -void show_line_abort(struct show_line* show_state) -{ - free(show_state->current_line); - show_state->current_line = NULL; - show_state->current_cursor = 0; -} - -void show_line_finish(struct show_line* show_state) -{ - show_line(show_state, show_state->current_line, strlen(show_state->current_line)); - dprintf(show_state->out_fd, "\n"); - - show_line_abort(show_state); -} - -struct edit_line -{ - const char* ps1; - const char* ps2; - struct show_line show_state; - wchar_t* line; - size_t line_offset; - size_t line_used; - size_t line_length; - char** history; - size_t history_offset; - size_t history_used; - size_t history_length; - size_t history_target; - void* check_input_incomplete_context; - bool (*check_input_incomplete)(void*, const char*); - void* trap_eof_opportunity_context; - void (*trap_eof_opportunity)(void*); - void* complete_context; - size_t (*complete)(char***, size_t*, size_t*, void*, const char*, size_t); - int in_fd; - int out_fd; - bool editing; - bool abort_editing; - bool eof_condition; - bool double_tab; - // TODO: Should these be stored here, or outside the line editing context? - bool left_control; - bool right_control; -}; - -void edit_line_show(struct edit_line* edit_state) -{ - size_t line_length = 0; - - mbstate_t ps; - memset(&ps, 0, sizeof(ps)); - - line_length += strlen(edit_state->ps1); - - for ( size_t i = 0; i < edit_state->line_used; i++ ) - { - char mb[MB_CUR_MAX]; - line_length += wcrtomb(mb, edit_state->line[i], &ps); - if ( edit_state->line[i] == L'\n' ) - line_length += strlen(edit_state->ps2); - } - - char* line = (char*) malloc(line_length + 1); - assert(line); - - size_t cursor = 0; - size_t line_offset = 0; - memset(&ps, 0, sizeof(ps)); - - strcpy(line + line_offset, edit_state->ps1); - line_offset += strlen(edit_state->ps1); - - for ( size_t i = 0; i < edit_state->line_used; i++ ) - { - if ( edit_state->line_offset == i ) - cursor = line_offset; - line_offset += wcrtomb(line + line_offset, edit_state->line[i], &ps); - if ( edit_state->line[i] == L'\n' ) - { - strcpy(line + line_offset, edit_state->ps2); - line_offset += strlen(edit_state->ps2); - } - } - - if ( edit_state->line_offset == edit_state->line_used ) - cursor = line_offset; - - line[line_offset] = '\0'; - - show_line(&edit_state->show_state, line, cursor); - - free(line); -} - -char* edit_line_result(struct edit_line* edit_state) -{ - size_t result_length = 0; - - mbstate_t ps; - memset(&ps, 0, sizeof(ps)); - - for ( size_t i = 0; i < edit_state->line_used; i++ ) - { - char mb[MB_CUR_MAX]; - result_length += wcrtomb(mb, edit_state->line[i], &ps); - } - - char* result = (char*) malloc(result_length + 1); - if ( !result ) - return NULL; - size_t result_offset = 0; - - memset(&ps, 0, sizeof(ps)); - - for ( size_t i = 0; i < edit_state->line_used; i++ ) - result_offset += wcrtomb(result + result_offset, edit_state->line[i], &ps); - - result[result_offset] = '\0'; - - return result; -} - -bool edit_line_can_finish(struct edit_line* edit_state) -{ - if ( !edit_state->check_input_incomplete ) - return true; - char* line = edit_line_result(edit_state); - assert(line); - bool result = !edit_state->check_input_incomplete( - edit_state->check_input_incomplete_context, line); - free(line); - return result; -} - -void edit_line_append_history(struct edit_line* edit_state, const char* line) -{ - if ( edit_state->history_used == edit_state->history_length ) - { - size_t new_length = 2 * edit_state->history_length; - if ( new_length == 0 ) - new_length = 16; - // TODO: Use reallocarray instead of realloc. - size_t new_size = sizeof(char*) * new_length; - char** new_history = (char**) realloc(edit_state->history, new_size); - assert(new_history); - edit_state->history = new_history; - edit_state->history_length = new_length; - } - - size_t history_index = edit_state->history_used++; - edit_state->history[history_index] = strdup(line); - assert(edit_state->history[history_index]); -} - -void edit_line_type_use_record(struct edit_line* edit_state, const char* record) -{ - free(edit_state->line); - edit_state->line_offset = 0; - edit_state->line_used = 0; - edit_state->line_length = 0; - - size_t line_length; - - mbstate_t ps; - memset(&ps, 0, sizeof(ps)); - - size_t record_offset = 0; - for ( line_length = 0; true; line_length++ ) - { - size_t num_bytes = mbrtowc(NULL, record + record_offset, SIZE_MAX, &ps); - assert(num_bytes != (size_t) -2); - assert(num_bytes != (size_t) -1); - if ( num_bytes == 0 ) - break; - record_offset += num_bytes; - } - - // TODO: Avoid multiplication overflow. - wchar_t* line = (wchar_t*) malloc(sizeof(wchar_t) * line_length); - assert(line); - size_t line_used; - - memset(&ps, 0, sizeof(ps)); - - record_offset = 0; - for ( line_used = 0; line_used < line_length; line_used++ ) - { - size_t num_bytes = mbrtowc(&line[line_used], record + record_offset, SIZE_MAX, &ps); - assert(num_bytes != (size_t) -2); - assert(num_bytes != (size_t) -1); - assert(num_bytes != (size_t) 0); - record_offset += num_bytes; - } - - edit_state->line = line; - edit_state->line_offset = line_used; - edit_state->line_used = line_used; - edit_state->line_length = line_length; -} - -void edit_line_type_history_save_at(struct edit_line* edit_state, size_t index) -{ - assert(index <= edit_state->history_used); - - char* saved_line = edit_line_result(edit_state); - assert(saved_line); - if ( index == edit_state->history_used ) - { - edit_line_append_history(edit_state, saved_line); - free(saved_line); - } - else - { - free(edit_state->history[index]); - edit_state->history[index] = saved_line; - } -} - -void edit_line_type_history_save_current(struct edit_line* edit_state) -{ - edit_line_type_history_save_at(edit_state, edit_state->history_offset); -} - -void edit_line_type_history_prev(struct edit_line* edit_state) -{ - if ( edit_state->history_offset == 0 ) - return; - - edit_line_type_history_save_current(edit_state); - - const char* record = edit_state->history[--edit_state->history_offset]; - assert(record); - edit_line_type_use_record(edit_state, record); -} - -void edit_line_type_history_next(struct edit_line* edit_state) -{ - if ( edit_state->history_used - edit_state->history_offset <= 1 ) - return; - - edit_line_type_history_save_current(edit_state); - - const char* record = edit_state->history[++edit_state->history_offset]; - assert(record); - edit_line_type_use_record(edit_state, record); -} - -void edit_line_type_codepoint(struct edit_line* edit_state, wchar_t wc) -{ - if ( wc == L'\n' && edit_line_can_finish(edit_state)) - { - if ( edit_state->line_used ) - edit_line_type_history_save_at(edit_state, edit_state->history_target); - edit_state->editing = false; - return; - } - - if ( edit_state->line_used == edit_state->line_length ) - { - size_t new_length = 2 * edit_state->line_length; - if ( !new_length ) - new_length = 16; - // TODO: Use reallocarray instead of realloc. - size_t new_size = sizeof(wchar_t) * new_length; - wchar_t* new_line = (wchar_t*) realloc(edit_state->line, new_size); - assert(new_line); - edit_state->line = new_line; - edit_state->line_length = new_length; - } - - assert(edit_state->line_offset <= edit_state->line_used); - assert(edit_state->line_used <= edit_state->line_length); - - for ( size_t i = edit_state->line_used; i != edit_state->line_offset; i-- ) - edit_state->line[i] = edit_state->line[i-1]; - - edit_state->line[edit_state->line_used++, edit_state->line_offset++] = wc; - - assert(edit_state->line_offset <= edit_state->line_used); - assert(edit_state->line_used <= edit_state->line_length); -} - -void line_edit_type_home(struct edit_line* edit_state) -{ - edit_state->line_offset = 0; -} - -void line_edit_type_left(struct edit_line* edit_state) -{ - if ( edit_state->line_offset == 0 ) - return; - edit_state->line_offset--; -} - -void line_edit_type_right(struct edit_line* edit_state) -{ - if ( edit_state->line_offset == edit_state->line_used ) - return; - edit_state->line_offset++; -} - -void line_edit_type_end(struct edit_line* edit_state) -{ - edit_state->line_offset = edit_state->line_used; -} - -void line_edit_type_backspace(struct edit_line* edit_state) -{ - if ( edit_state->line_offset == 0 ) - return; - edit_state->line_used--; - edit_state->line_offset--; - for ( size_t i = edit_state->line_offset; i < edit_state->line_used; i++ ) - edit_state->line[i] = edit_state->line[i+1]; -} - -void line_edit_type_previous_word(struct edit_line* edit_state) -{ - while ( edit_state->line_offset && - iswspace(edit_state->line[edit_state->line_offset-1]) ) - edit_state->line_offset--; - while ( edit_state->line_offset && - !iswspace(edit_state->line[edit_state->line_offset-1]) ) - edit_state->line_offset--; -} - -void line_edit_type_next_word(struct edit_line* edit_state) -{ - while ( edit_state->line_offset != edit_state->line_used && - iswspace(edit_state->line[edit_state->line_offset]) ) - edit_state->line_offset++; - while ( edit_state->line_offset != edit_state->line_used && - !iswspace(edit_state->line[edit_state->line_offset]) ) - edit_state->line_offset++; -} - -void line_edit_type_delete(struct edit_line* edit_state) -{ - if ( edit_state->line_offset == edit_state->line_used ) - return; - edit_state->line_used--; - for ( size_t i = edit_state->line_offset; i < edit_state->line_used; i++ ) - edit_state->line[i] = edit_state->line[i+1]; -} - -void line_edit_type_eof_or_delete(struct edit_line* edit_state) -{ - if ( edit_state->line_used ) - return line_edit_type_delete(edit_state); - edit_state->editing = false; - edit_state->eof_condition = true; - if ( edit_state->trap_eof_opportunity ) - edit_state->trap_eof_opportunity(edit_state->trap_eof_opportunity_context); -} - -void edit_line_type_interrupt(struct edit_line* edit_state) -{ - dprintf(edit_state->out_fd, "^C\n"); - edit_state->editing = false; - edit_state->abort_editing = true; -} - -void edit_line_type_kill_after(struct edit_line* edit_state) -{ - while ( edit_state->line_offset < edit_state->line_used ) - line_edit_type_delete(edit_state); -} - -void edit_line_type_kill_before(struct edit_line* edit_state) -{ - while ( edit_state->line_offset ) - line_edit_type_backspace(edit_state); -} - -void edit_line_type_clear(struct edit_line* edit_state) -{ - show_line_clear(&edit_state->show_state); -} - -void edit_line_type_delete_word_before(struct edit_line* edit_state) -{ - while ( edit_state->line_offset && - iswspace(edit_state->line[edit_state->line_offset-1]) ) - line_edit_type_backspace(edit_state); - while ( edit_state->line_offset && - !iswspace(edit_state->line[edit_state->line_offset-1]) ) - line_edit_type_backspace(edit_state); -} - -int edit_line_completion_sort(const void* a_ptr, const void* b_ptr) -{ - const char* a = *(const char**) a_ptr; - const char* b = *(const char**) b_ptr; - return strcmp(a, b); -} - -void edit_line_type_complete(struct edit_line* edit_state) -{ - if ( !edit_state->complete ) - return; - - char* partial = edit_line_result(edit_state); - if ( !partial ) - return; - - mbstate_t ps; - memset(&ps, 0, sizeof(ps)); - - size_t complete_at = 0; - for ( size_t i = 0; i < edit_state->line_offset; i++ ) - { - char mb[MB_CUR_MAX]; - size_t num_bytes = wcrtomb(mb, edit_state->line[i], &ps); - assert(num_bytes != (size_t) -1); - assert(num_bytes != (size_t) 0); - complete_at += num_bytes; - } - - char** completions; - size_t used_before; - size_t used_after; - size_t num_completions = edit_state->complete( - &completions, - &used_before, - &used_after, - edit_state->complete_context, - partial, - complete_at); - - qsort(completions, num_completions, sizeof(char*), edit_line_completion_sort); - - size_t lcp = 0; - bool similar = true; - while ( num_completions && similar ) - { - char c = completions[0][lcp]; - if ( c == '\0' ) - break; - for ( size_t i = 1; similar && i < num_completions; i++ ) - { - if ( completions[i][lcp] != c ) - similar = false; - } - if ( similar ) - lcp++; - } - - bool prefix_ends_with_slash = false; - memset(&ps, 0, sizeof(ps)); - for ( size_t i = 0; i < lcp; ) - { - const char* completion = completions[0]; - wchar_t wc; - size_t num_bytes = mbrtowc(&wc, completion + i, lcp - i, &ps); - if ( num_bytes == (size_t) -2 ) - break; - assert(num_bytes != (size_t) -1); - assert(num_bytes != (size_t) 0); - edit_line_type_codepoint(edit_state, wc); - prefix_ends_with_slash = wc == L'/'; - i += num_bytes; - } - - if ( num_completions == 1 && !prefix_ends_with_slash ) - { - edit_line_type_codepoint(edit_state, ' '); - } - - if ( 2 <= num_completions && lcp == 0 && edit_state->double_tab ) - { - bool first = true; - for ( size_t i = 0; i < num_completions; i++ ) - { - const char* completion = completions[i]; - size_t length = used_before + strlen(completion) + used_after; - if ( !length ) - continue; - if ( first ) - show_line_finish(&edit_state->show_state); - // TODO: Use a reliable write. - if ( !first ) - write(edit_state->out_fd, " ", 1); - write(edit_state->out_fd, partial + complete_at - used_before, used_before); - write(edit_state->out_fd, completion, strlen(completion)); - write(edit_state->out_fd, partial + complete_at, used_after); - first = false; - } - if ( !first) - { - write(edit_state->out_fd, "\n", 1); - show_line_begin(&edit_state->show_state, edit_state->out_fd); - edit_line_show(edit_state); - } - } - - edit_state->double_tab = true; - - (void) used_before; - (void) used_after; - - for ( size_t i = 0; i < num_completions; i++ ) - free(completions[i]); - free(completions); - - free(partial); -} - -void edit_line_kbkey(struct edit_line* edit_state, int kbkey) -{ - if ( kbkey != KBKEY_TAB && kbkey != -KBKEY_TAB ) - edit_state->double_tab = false; - - if ( edit_state->left_control || edit_state->right_control ) - { - switch ( kbkey ) - { - case KBKEY_LEFT: line_edit_type_previous_word(edit_state); return; - case KBKEY_RIGHT: line_edit_type_next_word(edit_state); return; - }; - } - - switch ( kbkey ) - { - case KBKEY_HOME: line_edit_type_home(edit_state); return; - case KBKEY_LEFT: line_edit_type_left(edit_state); return; - case KBKEY_RIGHT: line_edit_type_right(edit_state); return; - case KBKEY_UP: edit_line_type_history_prev(edit_state); return; - case KBKEY_DOWN: edit_line_type_history_next(edit_state); return; - case KBKEY_END: line_edit_type_end(edit_state); return; - case KBKEY_BKSPC: line_edit_type_backspace(edit_state); return; - case KBKEY_DELETE: line_edit_type_delete(edit_state); return; - case KBKEY_TAB: edit_line_type_complete(edit_state); return; - case -KBKEY_LCTRL: edit_state->left_control = false; return; - case +KBKEY_LCTRL: edit_state->left_control = true; return; - case -KBKEY_RCTRL: edit_state->right_control = false; return; - case +KBKEY_RCTRL: edit_state->right_control = true; return; - }; -} - -void edit_line_codepoint(struct edit_line* edit_state, wchar_t wc) -{ - if ( (edit_state->left_control || edit_state->right_control) && - ((L'a' <= wc && wc <= L'z') || (L'A' <= wc && wc <= L'Z')) ) - { - if ( wc == L'a' || wc == L'A' ) - line_edit_type_home(edit_state); - if ( wc == L'b' || wc == L'B' ) - line_edit_type_left(edit_state); - if ( wc == L'c' || wc == L'C' ) - edit_line_type_interrupt(edit_state); - if ( wc == L'd' || wc == L'D' ) - line_edit_type_eof_or_delete(edit_state); - if ( wc == L'e' || wc == L'E' ) - line_edit_type_end(edit_state); - if ( wc == L'f' || wc == L'F' ) - line_edit_type_right(edit_state); - if ( wc == L'k' || wc == L'K' ) - edit_line_type_kill_after(edit_state); - if ( wc == L'l' || wc == L'L' ) - show_line_clear(&edit_state->show_state); - if ( wc == L'u' || wc == L'U' ) - edit_line_type_kill_before(edit_state); - if ( wc == L'w' || wc == L'W' ) - edit_line_type_delete_word_before(edit_state); - return; - } - - if ( wc == L'\b' ) - return; - if ( wc == L'\t' ) - return; - - edit_line_type_codepoint(edit_state, wc); -} - -void edit_line(struct edit_line* edit_state) -{ - edit_state->editing = true; - edit_state->abort_editing = false; - edit_state->eof_condition = false; - edit_state->double_tab = false; - - free(edit_state->line); - edit_state->line = NULL; - edit_state->line_offset = 0; - edit_state->line_used = 0; - edit_state->line_length = 0; - edit_state->history_offset = edit_state->history_used; - edit_state->history_target = edit_state->history_used; - - settermmode(edit_state->in_fd, TERMMODE_KBKEY | TERMMODE_UNICODE); - - show_line_begin(&edit_state->show_state, edit_state->out_fd); - - while ( edit_state->editing ) - { - edit_line_show(edit_state); - - uint32_t codepoint; - if ( read(0, &codepoint, sizeof(codepoint)) != sizeof(codepoint) ) - { - edit_state->eof_condition = true; - edit_state->abort_editing = true; - break; - } - - if ( int kbkey = KBKEY_DECODE(codepoint) ) - edit_line_kbkey(edit_state, kbkey); - else - edit_line_codepoint(edit_state, (wchar_t) codepoint); - } - - if ( edit_state->abort_editing ) - show_line_abort(&edit_state->show_state); - else - { - edit_line_show(edit_state); - show_line_finish(&edit_state->show_state); - } - - settermmode(edit_state->in_fd, NORMAL_TERMMODE); -} - int status = 0; -char* strdup_safe(const char* string) -{ - return string ? strdup(string) : NULL; -} - -const char* getenv_safe(const char* name, const char* def = "") -{ - const char* ret = getenv(name); - return ret ? ret : def; -} - static bool is_proper_absolute_path(const char* path) { if ( path[0] == '\0' ) diff --git a/sh/sh.h b/sh/sh.h new file mode 100644 index 00000000..e69de29b diff --git a/sh/showline.cpp b/sh/showline.cpp new file mode 100644 index 00000000..d0f2f4cf --- /dev/null +++ b/sh/showline.cpp @@ -0,0 +1,377 @@ +/******************************************************************************* + + Copyright(C) Jonas 'Sortie' Termansen 2011, 2012, 2013, 2014, 2015. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . + + showline.cpp + Display a line on the terminal. + +*******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#include "showline.h" + +struct wincurpos predict_cursor(struct cursor_predict* cursor_predict, + struct wincurpos wcp, + struct winsize ws, + wchar_t c) +{ + if ( c == L'\0' ) + return wcp; + + if ( cursor_predict->escaped ) + { + if ( (L'a' <= c && c <= L'z') || (L'A' <= c && c <= L'Z') ) + cursor_predict->escaped = false; + return wcp; + } + + if ( c == L'\e' ) + { + cursor_predict->escaped = true; + return wcp; + } + + if ( c == L'\n' || ws.ws_col <= wcp.wcp_col + 1 ) + { + wcp.wcp_col = 0; + if ( wcp.wcp_row + 1 < ws.ws_row ) + wcp.wcp_row++; + } + else + { + wcp.wcp_col++; + } + + return wcp; +} + +bool predict_will_scroll(struct cursor_predict cursor_predict, + struct wincurpos wcp, + struct winsize ws, + wchar_t c) +{ + if ( c == L'\0' ) + return false; + if ( cursor_predict.escaped ) + return false; + return (c == L'\n' || ws.ws_col <= wcp.wcp_col + 1) && + !(wcp.wcp_row + 1 < ws.ws_row); +} + +void show_line_begin(struct show_line* show_state, int out_fd) +{ + memset(show_state, 0, sizeof(*show_state)); + show_state->out_fd = out_fd; + show_state->current_line = NULL; + show_state->current_cursor = 0; + tcgetwincurpos(out_fd, &show_state->wcp_start); + show_state->wcp_current = show_state->wcp_start; + tcgetwinsize(show_state->out_fd, &show_state->ws); +} + +bool show_line_is_weird(const char* line) +{ + for ( size_t i = 0; line[i]; i++ ) + { + if ( line[i] == '\e' ) + { + i++; + if ( line[i] != '[' ) + return true; + i++; + while ( ('0' <= line[i] && line[i] <= '9') || line[i] == ';' ) + i++; + switch ( line[i] ) + { + case 'm': break; + default: return true; + } + continue; + } + + switch ( line[i] ) + { + case '\a': return true; + case '\b': return true; + case '\f': return true; + case '\r': return true; + case '\t': return true; // TODO: This isn't weird. + case '\v': return true; + default: break; + } + } + + return false; +} + +void show_line_change_cursor(struct show_line* show_state, struct wincurpos wcp) +{ + if ( wcp.wcp_col == show_state->wcp_current.wcp_col && + wcp.wcp_row == show_state->wcp_current.wcp_row ) + return; + + if ( wcp.wcp_col == 0 ) + dprintf(show_state->out_fd, "\e[%zuH", wcp.wcp_row + 1); + else + dprintf(show_state->out_fd, "\e[%zu;%zuH", wcp.wcp_row + 1, wcp.wcp_col+ 1); + + show_state->wcp_current = wcp; +} + +bool show_line_optimized(struct show_line* show_state, const char* line, size_t cursor) +{ + struct winsize ws = show_state->ws; + + mbstate_t old_ps; + mbstate_t new_ps; + memset(&old_ps, 0, sizeof(old_ps)); + memset(&new_ps, 0, sizeof(new_ps)); + struct wincurpos old_wcp = show_state->wcp_start; + struct wincurpos new_wcp = show_state->wcp_start; + struct cursor_predict old_cursor_predict; + struct cursor_predict new_cursor_predict; + memset(&old_cursor_predict, 0, sizeof(old_cursor_predict)); + memset(&new_cursor_predict, 0, sizeof(new_cursor_predict)); + size_t old_line_offset = 0; + size_t new_line_offset = 0; + const char* old_line = show_state->current_line; + const char* new_line = line; + + struct wincurpos cursor_wcp = show_state->wcp_start; + + while ( true ) + { + if ( cursor == new_line_offset ) + cursor_wcp = new_wcp; + + wchar_t old_wc; + wchar_t new_wc; + + size_t old_num_bytes = mbrtowc(&old_wc, old_line + old_line_offset, SIZE_MAX, &old_ps); + size_t new_num_bytes = mbrtowc(&new_wc, new_line + new_line_offset, SIZE_MAX, &new_ps); + assert(old_num_bytes != (size_t) -2); + assert(new_num_bytes != (size_t) -2); + assert(old_num_bytes != (size_t) -1); + assert(new_num_bytes != (size_t) -1); + if ( old_num_bytes == 0 && new_num_bytes == 0 ) + break; + + bool will_scroll = predict_will_scroll(new_cursor_predict, new_wcp, ws, new_wc); + bool can_scroll = show_state->wcp_start.wcp_row != 0; + + if ( will_scroll && !can_scroll ) + { + if ( new_line_offset < cursor ) + cursor_wcp = new_wcp; + break; + } + + if ( predict_will_scroll(old_cursor_predict, old_wcp, ws, old_wc) ) + break; + + struct wincurpos next_old_wcp = predict_cursor(&old_cursor_predict, old_wcp, ws, old_wc); + struct wincurpos next_new_wcp = predict_cursor(&new_cursor_predict, new_wcp, ws, new_wc); + + if ( old_wc != new_wc || + old_wcp.wcp_row != new_wcp.wcp_row || + old_wcp.wcp_col != new_wcp.wcp_col ) + { + // TODO: Use a reliable write instead! + + if ( old_wc == L'\n' && new_wc == L'\n' ) + { + // Good enough as newlines are invisible. + } + else if ( old_wc == L'\n' && new_wc != L'\0' ) + { + show_line_change_cursor(show_state, new_wcp); + write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); + show_state->wcp_current = next_new_wcp; + old_num_bytes = 0; + } + else if ( old_wc != L'\0' && new_wc == '\n' ) + { + show_line_change_cursor(show_state, old_wcp); + write(show_state->out_fd, " ", 1); + show_state->wcp_current = next_old_wcp; + new_num_bytes = 0; + } + else if ( old_wc == L'\n' && new_wc == L'\0' ) + { + // No need to do anything here as newlines are visible. + } + else if ( old_wc == L'\0' && new_wc == L'\n' ) + { + show_line_change_cursor(show_state, new_wcp); + write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); + show_state->wcp_current = next_new_wcp; + } + else if ( old_wcp.wcp_row != new_wcp.wcp_row || + old_wcp.wcp_col != new_wcp.wcp_col ) + return false; + else if ( new_wc == L'\0' && old_wc != L'\0' ) + { + show_line_change_cursor(show_state, old_wcp); + write(show_state->out_fd, " ", 1); + show_state->wcp_current = next_old_wcp; + } + else if ( new_wc != L'\0' ) + { + show_line_change_cursor(show_state, new_wcp); + write(show_state->out_fd, new_line + new_line_offset, new_num_bytes); + show_state->wcp_current = next_new_wcp; + } + } + + if ( will_scroll && can_scroll ) + { + cursor_wcp.wcp_row--; + next_old_wcp.wcp_row--; + show_state->wcp_start.wcp_row--; + } + + old_wcp = next_old_wcp; + new_wcp = next_new_wcp; + + old_line_offset += old_num_bytes; + new_line_offset += new_num_bytes; + } + + show_line_change_cursor(show_state, cursor_wcp); + + free(show_state->current_line); + show_state->current_line = strdup(line); + assert(show_state->current_line); + show_state->current_cursor = cursor; + + return true; +} + +void show_line(struct show_line* show_state, const char* line, size_t cursor) +{ + // TODO: We don't currently invalidate on SIGWINCH. + struct winsize ws; + tcgetwinsize(show_state->out_fd, &ws); + if ( ws.ws_col != show_state->ws.ws_col || + ws.ws_row != show_state->ws.ws_row ) + { + // TODO: What if wcp_start isn't inside the window any longer? + show_state->invalidated = true; + show_state->ws = ws; + } + + // Attempt to do an optimized line re-rendering reusing the characters + // already present on the console. Bail out if this turns out to be harder + // than expected and re-render everything from scratch instead. + if ( !show_state->invalidated && + show_state->current_line && + !show_line_is_weird(show_state->current_line) && + !show_line_is_weird(line) ) + { + if ( show_line_optimized(show_state, line, cursor) ) + return; + show_state->invalidated = true; + } + + show_line_change_cursor(show_state, show_state->wcp_start); + + dprintf(show_state->out_fd, "\e[m"); + + if ( show_state->invalidated || show_state->current_line ) + dprintf(show_state->out_fd, "\e[0J"); + + struct cursor_predict cursor_predict; + memset(&cursor_predict, 0, sizeof(cursor_predict)); + struct wincurpos wcp = show_state->wcp_start; + struct wincurpos cursor_wcp = wcp; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + for ( size_t i = 0; true; ) + { + if ( cursor == i ) + cursor_wcp = wcp; + wchar_t wc; + size_t num_bytes = mbrtowc(&wc, line + i, SIZE_MAX, &ps); + assert(num_bytes != (size_t) -2); + assert(num_bytes != (size_t) -1); + if ( num_bytes == 0 ) + break; + bool will_scroll = predict_will_scroll(cursor_predict, wcp, ws, wc); + bool can_scroll = show_state->wcp_start.wcp_row != 0; + if ( will_scroll && !can_scroll ) + { + if ( i < cursor ) + cursor_wcp = wcp; + break; + } + // TODO: Use a reliable write. + write(show_state->out_fd, line + i, num_bytes); + if ( will_scroll && can_scroll ) + { + cursor_wcp.wcp_row--; + show_state->wcp_start.wcp_row--; + } + wcp = predict_cursor(&cursor_predict, wcp, ws, wc); + i += num_bytes; + } + + dprintf(show_state->out_fd, "\e[%zu;%zuH", + cursor_wcp.wcp_row + 1, + cursor_wcp.wcp_col + 1); + + show_state->wcp_current = wcp; + + free(show_state->current_line); + show_state->current_line = strdup(line); + assert(show_state->current_line); + show_state->current_cursor = cursor; + + show_state->invalidated = false; +} + +void show_line_clear(struct show_line* show_state) +{ + dprintf(show_state->out_fd, "\e[H\e[2J"); + + show_state->wcp_start.wcp_row = 0; + show_state->wcp_start.wcp_col = 0; + show_state->invalidated = true; + + show_line(show_state, show_state->current_line, strlen(show_state->current_line)); +} + +void show_line_abort(struct show_line* show_state) +{ + free(show_state->current_line); + show_state->current_line = NULL; + show_state->current_cursor = 0; +} + +void show_line_finish(struct show_line* show_state) +{ + show_line(show_state, show_state->current_line, strlen(show_state->current_line)); + dprintf(show_state->out_fd, "\n"); + + show_line_abort(show_state); +} diff --git a/sh/showline.h b/sh/showline.h new file mode 100644 index 00000000..f06f826e --- /dev/null +++ b/sh/showline.h @@ -0,0 +1,63 @@ +/******************************************************************************* + + Copyright(C) Jonas 'Sortie' Termansen 2011, 2012, 2013, 2014, 2015. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . + + showline.h + Display a line on the terminal. + +*******************************************************************************/ + +#ifndef SHOWLINE_H +#define SHOWLINE_H + +#include +#include + +// TODO: Predict the terminal colors as well! +struct cursor_predict +{ + bool escaped; +}; + +struct show_line +{ + struct wincurpos wcp_start; + struct wincurpos wcp_current; + struct winsize ws; + int out_fd; + char* current_line; + size_t current_cursor; + bool invalidated; +}; + +struct wincurpos predict_cursor(struct cursor_predict* cursor_predict, + struct wincurpos wcp, + struct winsize ws, + wchar_t c); +bool predict_will_scroll(struct cursor_predict cursor_predict, + struct wincurpos wcp, + struct winsize ws, + wchar_t c); +void show_line_begin(struct show_line* show_state, int out_fd); +bool show_line_is_weird(const char* line); +void show_line_change_cursor(struct show_line* show_state, struct wincurpos wcp); +bool show_line_optimized(struct show_line* show_state, const char* line, size_t cursor); +void show_line(struct show_line* show_state, const char* line, size_t cursor); +void show_line_clear(struct show_line* show_state); +void show_line_abort(struct show_line* show_state); +void show_line_finish(struct show_line* show_state); + +#endif diff --git a/sh/util.cpp b/sh/util.cpp new file mode 100644 index 00000000..9070cb02 --- /dev/null +++ b/sh/util.cpp @@ -0,0 +1,37 @@ +/******************************************************************************* + + Copyright(C) Jonas 'Sortie' Termansen 2011, 2012, 2013, 2014, 2015. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . + + util.cpp + Utility functions. + +*******************************************************************************/ + +#include +#include + +#include "util.h" + +char* strdup_safe(const char* string) +{ + return string ? strdup(string) : NULL; +} + +const char* getenv_safe(const char* name, const char* def) +{ + const char* ret = getenv(name); + return ret ? ret : def; +} diff --git a/sh/util.h b/sh/util.h new file mode 100644 index 00000000..d28b5a47 --- /dev/null +++ b/sh/util.h @@ -0,0 +1,29 @@ +/******************************************************************************* + + Copyright(C) Jonas 'Sortie' Termansen 2011, 2012, 2013, 2014, 2015. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see . + + util.h + Utility functions. + +*******************************************************************************/ + +#ifndef UTIL_H +#define UTIL_H + +char* strdup_safe(const char* string); +const char* getenv_safe(const char* name, const char* def = ""); + +#endif