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