/* * Copyright (c) 2017, 2022 Jonas 'Sortie' Termansen. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF7 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * terminal.c * Terminal emulator. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(TTY_NAME_MAX) #include #endif #include #include #include #include #include "palette.h" struct kbkey_sequence { const char* sequence; int kbkey; int flags; }; #define MODIFIER_ALT (1 << 0) #define MODIFIER_LSHIFT (1 << 1) #define MODIFIER_RSHIFT (1 << 2) #define MODIFIER_LCONTROL (1 << 3) #define MODIFIER_RCONTROL (1 << 4) #define SEQUENCE_1IFMOD (1 << 0) #define SEQUENCE_OSHORT (1 << 1) static const struct kbkey_sequence kbkey_sequences[] = { { "\e[A", KBKEY_UP, SEQUENCE_1IFMOD }, { "\e[B", KBKEY_DOWN, SEQUENCE_1IFMOD}, { "\e[C", KBKEY_RIGHT, SEQUENCE_1IFMOD }, { "\e[D", KBKEY_LEFT, SEQUENCE_1IFMOD }, { "\e[F", KBKEY_END, SEQUENCE_1IFMOD }, { "\e[H", KBKEY_HOME, SEQUENCE_1IFMOD }, { "\e[2~", KBKEY_INSERT, 0 }, { "\e[3~", KBKEY_DELETE, 0 }, { "\e[5~", KBKEY_PGUP, 0 }, { "\e[6~", KBKEY_PGDOWN, 0 }, { "\e[1P", KBKEY_F1, SEQUENCE_OSHORT }, { "\e[1Q", KBKEY_F2, SEQUENCE_OSHORT }, { "\e[1R", KBKEY_F3, SEQUENCE_OSHORT }, { "\e[1S", KBKEY_F4, SEQUENCE_OSHORT }, { "\e[15~", KBKEY_F5, 0 }, { "\e[17~", KBKEY_F6, 0 }, { "\e[18~", KBKEY_F7, 0 }, { "\e[19~", KBKEY_F8, 0 }, { "\e[20~", KBKEY_F9, 0 }, { "\e[21~", KBKEY_F10, 0 }, { "\e[23~", KBKEY_F11, 0 }, { "\e[24~", KBKEY_F12, 0 }, }; static inline const struct kbkey_sequence* lookup_keystroke_sequence(int kbkey) { size_t count = sizeof(kbkey_sequences) / sizeof(kbkey_sequences[0]); for ( size_t i = 0; i < count; i++ ) if ( kbkey_sequences[i].kbkey == kbkey ) return &kbkey_sequences[i]; return NULL; } static uint32_t WINDOW_ID = 0; static uint32_t WINDOW_WIDTH = 0; static uint32_t WINDOW_HEIGHT = 0; static bool need_redraw = true; static bool need_show = true; static bool need_exit = false; static bool redraw_pipe_written = false; static int redraw_pipe[2]; struct entry { uint32_t attr; uint32_t fgcolor; uint32_t bgcolor; wchar_t wc; }; #define ATTR_INVERSE (1 << 0) #define ATTR_BOLD (1 << 1) #define ATTR_UNDERLINE (1 << 2) static struct entry* scrollback; static size_t column = 0; static size_t row = 0; static size_t columns = 0; static size_t rows = 0; static int modifiers = 0; static mbstate_t in_ps; static mbstate_t out_ps; static uint32_t default_fgcolor; static uint32_t default_bgcolor; static uint32_t current_fgcolor; static uint32_t current_bgcolor; static uint32_t attr; static uint32_t next_attr; static unsigned ansisavedposx; static unsigned ansisavedposy; static enum { NONE = 0, CSI, CHARSET, COMMAND, GREATERTHAN, } ansimode; #define ANSI_NUM_PARAMS 16 static unsigned ansiusedparams; static unsigned ansiparams[ANSI_NUM_PARAMS]; static bool ignore_sequence; static bool draw_cursor = true; static void scrollback_resize(size_t new_rows, size_t new_columns) { // TODO: Recover gracefully if the scrollback fails. // TODO: Overflow. struct entry* new_scrollback = calloc(sizeof(struct entry), new_rows * new_columns); if ( !new_scrollback ) err(1, "malloc"); size_t src_y_after_cursor = rows ? row + 1 : 0; size_t src_y_count = new_rows < src_y_after_cursor ? new_rows : src_y_after_cursor; size_t src_y_from = src_y_after_cursor - src_y_count; size_t new_row = row; size_t new_column = column; new_row += src_y_from; for ( size_t dst_y = 0; dst_y < new_rows; dst_y++ ) { size_t src_y = src_y_from + dst_y; for ( size_t dst_x = 0; dst_x < new_columns; dst_x++ ) { size_t src_x = dst_x; struct entry tc; if ( src_x < columns && src_y < rows ) tc = scrollback[src_y * columns + src_x]; else if ( columns && rows ) { size_t templ_x = src_y < columns ? src_y : src_x - 1; size_t templ_y = src_y < rows ? src_y : rows - 1; tc = scrollback[templ_y * columns + templ_x]; tc.wc = 0; tc.attr = 0; } else tc = (struct entry) { 0 }; new_scrollback[dst_y * new_columns + dst_x] = tc; if ( src_x == column && src_y == row ) { new_row = dst_y; new_column = dst_x; } } } if ( new_columns <= new_column ) new_column = new_columns ? new_column - 1 : 0; if ( new_rows <= new_row ) new_row = new_rows ? new_row - 1 : 0; free(scrollback); scrollback = new_scrollback; rows = new_rows; columns = new_columns; row = new_row; column = new_column; } static void fill(size_t from_x, size_t from_y, size_t to_x, size_t to_y, struct entry with) { // TODO: Assert within bounds? size_t from = from_y * columns + from_x; size_t to = to_y * columns + to_x; for ( size_t i = from; i <= to; i++ ) scrollback[i] = with; } static void scroll(ssize_t offsigned, struct entry with) { if ( 0 < offsigned ) { size_t off = offsigned; if ( rows < off ) off = rows; size_t dist = off * columns; size_t end = rows * columns - dist; for ( size_t i = 0; i < end; i++ ) scrollback[i] = scrollback[i + dist]; for ( size_t i = end; i < end + dist; i++ ) scrollback[i] = with; } else if ( offsigned < 0 ) { size_t off = -offsigned; // TODO: Negation overflow. if ( rows < off ) off = rows; size_t dist = off * columns; size_t end = rows * columns; for ( size_t i = end-1; dist <= i; i-- ) scrollback[i] = scrollback[i - dist]; for ( size_t i = 0; i < dist; i++ ) scrollback[i] = with; } } static void newline(void) { if ( row + 1 < rows ) { row++; return; } struct entry entry; entry.attr = 0; entry.fgcolor = current_fgcolor; entry.bgcolor = current_bgcolor; entry.wc = 0; scroll(1, entry); } static void run_ansi_command(char c) { switch ( c ) { case 'A': // Cursor up { unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1; if ( row < dist ) row = 0; else row -= dist; } break; case 'B': // Cursor down { unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1; if ( rows <= row + dist ) row = rows-1; else row += dist; } break; case 'C': // Cursor forward { unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1; if ( columns <= column + dist ) column = columns-1; else column += dist; } break; case 'D': // Cursor backward { unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1; if ( column < dist ) column = 0; else column -= dist; } break; case 'E': // Move to beginning of line N lines down. { column = 0; unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1; if ( rows <= row + dist ) row = rows-1; else row += dist; } break; case 'F': // Move to beginning of line N lines up. { column = 0; unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1; if ( row < dist ) row = 0; else row -= dist; } break; case 'G': // Move the cursor to column N. { unsigned pos = 0 < ansiusedparams ? ansiparams[0]-1 : 0; if ( columns <= pos ) pos = columns-1; column = pos; } break; case 'H': // Move the cursor to line Y, column X. case 'f': { unsigned posy = 0 < ansiusedparams ? ansiparams[0]-1 : 0; unsigned posx = 1 < ansiusedparams ? ansiparams[1]-1 : 0; if ( columns <= posx ) posx = columns-1; if ( rows <= posy ) posy = rows-1; column = posx; row = posy; } break; case 'J': // Erase parts of the screen. { unsigned mode = 0 < ansiusedparams ? ansiparams[0] : 0; size_t from_x = 0, from_y = 0; size_t to_x = 0, to_y = 0; if ( mode == 0 ) // From cursor to end. { from_x = column; from_y = row; // TODO: Ensure the number of rows and columns are always non-zero. to_x = columns - 1; to_y = rows - 1; } if ( mode == 1 ) // From start to cursor. { from_x = 0; from_y = 0; to_x = columns - 1; to_y = rows - 1; } if ( mode == 2 ) // Everything. { from_x = 0; from_y = 0; to_x = columns - 1; to_y = rows - 1; } struct entry with; with.attr = 0; with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor; with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor; with.wc = 0; fill(from_x, from_y, to_x, to_y, with); } break; case 'K': // Erase parts of the current line. { unsigned mode = 0 < ansiusedparams ? ansiparams[0] : 0; size_t from_x = 0, from_y = row; size_t to_x = 0, to_y = row; if ( mode == 0 ) // From cursor to end. { from_x = column; to_x = columns - 1; } if ( mode == 1 ) // From start to cursor. { from_x = 0; to_x = column; } if ( mode == 2 ) // Everything. { from_x = 0; to_x = columns - 1; } struct entry with; with.attr = 0; with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor; with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor; with.wc = 0; fill(from_x, from_y, to_x, to_y, with); } break; // TODO: CSI Ps M Delete Ps Line(s) (default = 1) (DL). // (delete those lines and move the rest of the lines upwards). // TODO: CSI Ps P Delete Ps Character(s) (default = 1) (DCH). // (delete those characters and move the rest of the line leftward). case 'S': // Scroll a line up and place a new line at the buttom. { struct entry with; with.attr = 0; with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor; with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor; with.wc = 0; scroll(1, with); row = rows - 1; } break; case 'T': // Scroll a line up and place a new line at the top. { struct entry with; with.attr = 0; with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor; with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor; with.wc = 0; scroll(-1, with); row = 0; } break; case 'd': // Move the cursor to line N. { unsigned posy = 0 < ansiusedparams ? ansiparams[0]-1 : 0; if ( rows <= posy ) posy = rows-1; row = posy; } break; case 'm': // Change how the text is rendered. { if ( ansiusedparams == 0 ) { ansiparams[0] = 0; ansiusedparams++; } for ( size_t i = 0; i < ansiusedparams; i++ ) { unsigned cmd = ansiparams[i]; // Turn all attributes off. if ( cmd == 0 ) { attr = 0; current_fgcolor = default_fgcolor; current_bgcolor = default_bgcolor; } // Boldness. else if ( cmd == 1 ) attr |= ATTR_BOLD; // TODO: 2, Faint // TODO: 3, Italicized // Underline. else if ( cmd == 4 ) attr |= ATTR_UNDERLINE; // TODO: 5, Blink (appears as Bold) // Inverse. else if ( cmd == 7 ) attr |= ATTR_INVERSE; // TODO: 8, Invisible // TODO: 9, Crossed-out // TODO: 21, Doubly-underlined // Normal (neither bold nor faint). else if ( cmd == 22 ) attr &= ~ATTR_BOLD; // TODO: 23, Not italicized // Not underlined. else if ( cmd == 24 ) attr &= ~ATTR_UNDERLINE; // TODO: 25, Steady (not blinking) // Positive (not inverse). else if ( cmd == 27 ) attr &= ~ATTR_INVERSE; // TODO: 28, Visible (not hidden) // Set text color. else if ( 30 <= cmd && cmd <= 37 ) { unsigned val = cmd - 30; current_fgcolor = palette[val] | 0xFF000000; } // Set text color. else if ( cmd == 38 ) { if ( 5 <= ansiusedparams - i && ansiparams[i+1] == 2 ) { uint8_t r = ansiparams[i+2]; uint8_t g = ansiparams[i+3]; uint8_t b = ansiparams[i+4]; i += 5 - 1; current_fgcolor = make_color(r, g, b); } else if ( 3 <= ansiusedparams - i && ansiparams[i+1] == 5 ) { uint8_t index = ansiparams[i+2]; i += 3 - 1; current_fgcolor = palette[index] | 0xFF000000; } } // Set default text color. else if ( cmd == 39 ) { current_fgcolor = default_fgcolor; } // Set background color. else if ( 40 <= cmd && cmd <= 47 ) { unsigned val = cmd - 40; current_bgcolor = palette[val] | 0xFF000000; } // Set background color. else if ( cmd == 48 ) { if ( 5 <= ansiusedparams - i && ansiparams[i+1] == 2 ) { uint8_t r = ansiparams[i+2]; uint8_t g = ansiparams[i+3]; uint8_t b = ansiparams[i+4]; i += 5 - 1; current_bgcolor = make_color(r, g, b); } else if ( 3 <= ansiusedparams - i && ansiparams[i+1] == 5 ) { uint8_t index = ansiparams[i+2]; i += 3 - 1; current_bgcolor = palette[index] | 0xFF000000; } } // Set default background color. else if ( cmd == 49 ) { current_bgcolor = default_bgcolor; } // Set text color. else if ( 90 <= cmd && cmd <= 97 ) { unsigned val = cmd - 90 + 8; current_fgcolor = palette[val] | 0xFF000000; } // Set background color. else if ( 100 <= cmd && cmd <= 107 ) { unsigned val = cmd - 100 + 8; current_bgcolor = palette[val] | 0xFF000000; } else { ansimode = NONE; } } } break; case 'n': // Request special information from terminal. { ansimode = NONE; // TODO: Handle this code. } break; case 's': // Save cursor position. { ansisavedposx = column; ansisavedposy = row; } break; case 'u': // Restore cursor position. { column = ansisavedposx; row = ansisavedposy; if ( columns <= column ) column = columns-1; if ( rows <= row ) row = rows-1; } break; case 'l': // Hide cursor. { // TODO: This is somehow related to the special char '?'. if ( 0 < ansiusedparams && ansiparams[0] == 25 ) draw_cursor = false; if ( 0 < ansiusedparams && ansiparams[0] == 1049 ) {}; // TODO: Save scrollback. } break; case 'h': // Show cursor. { // TODO: This is somehow related to the special char '?'. if ( 0 < ansiusedparams && ansiparams[0] == 25 ) draw_cursor = true; if ( 0 < ansiusedparams && ansiparams[0] == 1049 ) {}; // TODO: Restore scrollback. } break; default: { ansimode = NONE; } // TODO: Handle other cases. } ansimode = NONE; } static void put_ansi_escaped(char c) { // Check the proper prefixes are used. if ( ansimode == CSI ) { if ( c == '[' ) ansimode = COMMAND; else if ( c == '(' || c == ')' || c == '*' || c == '+' || c == '-' || c == '.' || c == '/' ) ansimode = CHARSET; // TODO: Enter and exit alternatve keypad mode. else if ( c == '=' || c == '>' ) ansimode = NONE; else { ansimode = NONE; } return; } if ( ansimode == CHARSET ) { ansimode = NONE; return; } // Read part of a parameter. if ( '0' <= c && c <= '9' ) { if ( ansiusedparams == 0 ) ansiusedparams++; unsigned val = c - '0'; ansiparams[ansiusedparams-1] *= 10; ansiparams[ansiusedparams-1] += val; } // Parameter delimiter. else if ( c == ';' ) { if ( ansiusedparams == ANSI_NUM_PARAMS ) { ansimode = NONE; return; } ansiparams[ansiusedparams++] = 0; } // Left for future standardization, so discard this sequence. else if ( c == ':' ) { ignore_sequence = true; } else if ( c == '>' ) { ansimode = GREATERTHAN; } // Run a command. else if ( 64 <= c && c <= 126 ) { if ( !ignore_sequence ) { if ( ansimode == COMMAND ) run_ansi_command(c); else if ( ansimode == GREATERTHAN ) { // Send Device Attributes if ( c == 'c' ) { // TODO: Send an appropriate response through the terminal. } else { ansimode = NONE; return; } ansimode = NONE; } } else ansimode = NONE; } // Something I don't understand, and ignore intentionally. else if ( c == '?' ) { //ansimode = NONE; } // TODO: There are some rare things that should be supported here. // Ignore unknown input. else { ansimode = NONE; } } static uint32_t boldify(uint32_t color) { int b = color >> 0 & 0xFF; int g = color >> 8 & 0xFF; int r = color >> 16 & 0xFF; int a = color >> 24 & 0xFF; b += 63; if ( 255 < b ) b = 255; g += 63; if ( 255 < g ) g = 255; r += 63; if ( 255 < r ) r = 255; return make_color_a(r, g, b, a); } static void outwc(wchar_t wc) { if ( wc == L'\a' ) { } else if ( wc == L'\n' ) { newline(); } else if ( wc == L'\r' ) { column = 0; } else if ( wc == L'\b' ) { if ( column ) { column--; struct entry* entry = &scrollback[row * columns + column]; next_attr = entry->attr & (ATTR_BOLD | ATTR_UNDERLINE); if ( entry->wc == L'_' ) next_attr |= ATTR_UNDERLINE; else if ( entry->wc == L' ' ) next_attr &= ~ATTR_BOLD; else next_attr |= ATTR_BOLD; } } else if ( wc == L'\t' ) { if ( column == columns ) { newline(); column = 0; } column++; column = -(-column & ~((size_t)0x7)); if ( columns <= column ) column = columns; } else if ( wc == L'\e' ) { next_attr = 0; ansiusedparams = 0; ansiparams[0] = 0; ignore_sequence = false; ansimode = CSI; } else { if ( column == columns ) { newline(); column = 0; } struct entry* entry = &scrollback[row * columns + column++]; entry->attr = attr | next_attr; if ( !(entry->attr & ATTR_INVERSE) ) { entry->fgcolor = current_fgcolor; entry->bgcolor = current_bgcolor; } else { entry->fgcolor = current_bgcolor; entry->bgcolor = current_fgcolor; } if ( entry->attr & ATTR_BOLD ) entry->fgcolor = boldify(entry->fgcolor); entry->wc = wc; next_attr = 0; } } static void outc(char c) { if ( ansimode != NONE ) { put_ansi_escaped(c); return; } wchar_t wc; size_t amount = mbrtowc(&wc, &c, 1, &out_ps); if ( amount == (size_t) -2 ) return; if ( amount == (size_t) -1 ) { memset(&out_ps, 0, sizeof(out_ps)); wc = 0xFFFD; /* REPLACEMENT CHARACTER */; } if ( amount == (size_t) 0 ) wc = L' '; outwc(wc); } static pthread_mutex_t scrollback_mutex = PTHREAD_MUTEX_INITIALIZER; static int master_fd; static void* outgoing_thread(void* ctx) { (void) ctx; const char* getcursor = "\e[6n"; size_t i = 0; char c; ssize_t amount = 0; while ( 0 < (amount = read(master_fd, &c, 1)) ) { // TODO: Do escape code handling in the escape code parsing. if ( c == getcursor[i] ) { i++; if ( !getcursor[i] ) { i = 0; char buf[64]; snprintf(buf, sizeof(buf), "\e[%zu;%zuR", (size_t) (row + 1), (size_t) (column + 1)); for ( size_t n = 0; buf[n]; n++ ) { if ( write(master_fd, &buf[n], 1) <= 0 ) { warn("incoming write"); break; // TODO: This break is incorrect. } } } continue; } pthread_mutex_lock(&scrollback_mutex); for ( size_t j = 0; j < i; j++ ) outc(getcursor[j]); i = 0; outc(c); if ( !redraw_pipe_written ) { char c = 'X'; if ( write(redraw_pipe[1], &c, 1) < 0 ) warn("write: redraw_pipe"); else redraw_pipe_written = true; } pthread_mutex_unlock(&scrollback_mutex); } if ( amount < 0 ) warn("outgoing read"); return NULL; } void on_disconnect(void* ctx) { (void) ctx; need_exit = true; } void on_quit(void* ctx, uint32_t window_id) { (void) ctx; if ( window_id != WINDOW_ID ) return; need_exit = true; } void on_resize(void* ctx, uint32_t window_id, uint32_t width, uint32_t height) { (void) ctx; if ( window_id != WINDOW_ID ) return; // TODO: The resolution 0x0 gets sent for newly created windows that hasn't // been shown yet, fix the compositor to not do this. if ( width == 0 && height == 0 ) return; pthread_mutex_lock(&scrollback_mutex); size_t new_rows = height / FONT_HEIGHT; size_t new_columns = width / FONT_WIDTH; scrollback_resize(new_rows, new_columns); struct winsize ws; ws.ws_row = rows; ws.ws_col = columns; if ( ioctl(master_fd, TIOCSWINSZ, &ws) < 0 ) warn("TIOCSWINSZ"); pthread_mutex_unlock(&scrollback_mutex); need_redraw = true; WINDOW_WIDTH = width; WINDOW_HEIGHT = height; } void inuc(unsigned char uc) { write(master_fd, &uc, 1); } void on_keycode(int kbkey) { if ( kbkey < 0 ) return; if ( kbkey == KBKEY_ESC ) { inuc('\e'); return; } const struct kbkey_sequence* seq = lookup_keystroke_sequence(kbkey); if ( !seq ) return; const char* str = seq->sequence; size_t len = strlen(str); int mods = 0; if ( modifiers & (MODIFIER_LSHIFT | MODIFIER_RSHIFT) ) mods |= 1; if ( modifiers & MODIFIER_ALT ) mods |= 2; if ( modifiers & (MODIFIER_LCONTROL | MODIFIER_RCONTROL) ) mods |= 4; if ( (seq->flags & SEQUENCE_OSHORT) && mods == 0 ) { inuc('\e'); inuc('O'); inuc((unsigned char) str[len-1]); return; } for ( size_t i = 0; i < len - 1; i++ ) inuc((unsigned char) str[i]); if ( seq->flags & SEQUENCE_1IFMOD && mods != 0 ) inuc('1'); if ( mods ) { inuc(';'); inuc('1' + mods); } inuc(str[len-1]); } void on_keyboard(void* ctx, uint32_t window_id, uint32_t codepoint) { (void) ctx; if ( window_id != WINDOW_ID ) return; int kbkey = KBKEY_DECODE(codepoint); if ( kbkey != 0 ) { // TODO: Don't do this here, let the compositor do this. if ( kbkey == KBKEY_LALT ) modifiers |= MODIFIER_ALT; else if ( kbkey == -KBKEY_LALT ) modifiers &= ~MODIFIER_ALT; else if ( kbkey == KBKEY_LSHIFT ) modifiers |= MODIFIER_LSHIFT; else if ( kbkey == -KBKEY_LSHIFT ) modifiers &= ~MODIFIER_LSHIFT; else if ( kbkey == KBKEY_RSHIFT ) modifiers |= MODIFIER_RSHIFT; else if ( kbkey == -KBKEY_RSHIFT ) modifiers &= ~MODIFIER_RSHIFT; else if ( kbkey == KBKEY_LCTRL ) modifiers |= MODIFIER_LCONTROL; else if ( kbkey == -KBKEY_LCTRL ) modifiers &= ~MODIFIER_LCONTROL; else if ( kbkey == KBKEY_RCTRL ) modifiers |= MODIFIER_RCONTROL; else if ( kbkey == -KBKEY_RCTRL ) modifiers &= ~MODIFIER_RCONTROL; on_keycode(kbkey); return; } if ( codepoint == '\n' ) codepoint = '\r'; bool control = modifiers & (MODIFIER_LCONTROL | MODIFIER_RCONTROL); if ( codepoint == '\b' ) codepoint = 127; if ( modifiers & MODIFIER_ALT ) inuc('\e'); if ( control && codepoint == L' ' ) inuc(0); else if ( control && (L'`' <= codepoint && codepoint <= L'}') ) inuc(codepoint - L'`'); else if ( control && (L'@' <= codepoint && codepoint <= L'_') ) inuc(codepoint - L'@'); else if ( control && codepoint == L'?' ) inuc(127); else { wchar_t wc = codepoint; char mb[MB_CUR_MAX]; size_t amount = wcrtomb(mb, wc, &in_ps); if ( amount == (size_t) -1 ) memset(&in_ps, 0, sizeof(in_ps)); else { for ( size_t i = 0; i < amount; i++ ) inuc((unsigned char) mb[i]); } } } void draw(struct display_connection* connection) { uint32_t* framebuffer = (uint32_t*) calloc(sizeof(uint32_t), WINDOW_WIDTH * WINDOW_HEIGHT); assert(framebuffer); struct framebuffer fb; fb.pitch = WINDOW_WIDTH; fb.buffer = framebuffer; fb.xres = WINDOW_WIDTH; fb.yres = WINDOW_HEIGHT; pthread_mutex_lock(&scrollback_mutex); size_t draw_rows = WINDOW_HEIGHT / FONT_HEIGHT; size_t draw_columns = WINDOW_WIDTH / FONT_WIDTH; if ( rows < draw_rows ) draw_rows = rows; if ( columns < draw_columns ) draw_rows = columns; for ( size_t y = 0; y < rows; y++ ) { size_t yoff = FONT_HEIGHT * y; for ( size_t x = 0; x < columns; x++ ) { size_t xoff = FONT_WIDTH * x; size_t cell_width = FONT_WIDTH; size_t cell_height = FONT_HEIGHT; if ( x + 1 == columns ) cell_width = WINDOW_WIDTH - xoff; if ( y + 1 == rows ) cell_height = WINDOW_HEIGHT - yoff; struct framebuffer charfb = framebuffer_crop(fb, xoff, yoff, cell_width, cell_height); struct entry* entry = &scrollback[y * columns + x]; for ( size_t py = 0; py < charfb.yres; py++ ) { uint32_t* line = charfb.buffer + py * charfb.pitch; for ( size_t px = 0; px < charfb.xres; px++ ) line[px] = entry->bgcolor; } render_char(charfb, entry->wc, entry->fgcolor); size_t entry_width = FONT_WIDTH; size_t entry_height = FONT_HEIGHT; if ( charfb.xres < entry_width ) entry_width = charfb.xres; if ( charfb.yres < entry_height ) entry_height = charfb.yres; size_t underlines = 0; if ( draw_cursor && y == row && x == column ) underlines = 2; else if ( entry->attr & ATTR_UNDERLINE ) underlines = 1; size_t start_underlines = FONT_HEIGHT - underlines; for ( size_t py = start_underlines; py < entry_height; py++ ) { uint32_t* line = charfb.buffer + py * charfb.pitch; for ( size_t x = 0; x < entry_width; x++ ) line[x] = blend_pixel(line[x], entry->fgcolor); } } } pthread_mutex_unlock(&scrollback_mutex); display_render_window(connection, WINDOW_ID, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, framebuffer); free(framebuffer); } static void signal_handler(int signum) { (void) signum; } int main(int argc, char* argv[]) { struct display_connection* connection = display_connect_default(); if ( !connection && errno == ECONNREFUSED ) display_spawn(argc, argv); if ( !connection ) err(1, "Could not connect to display server"); int opt; while ( (opt = getopt(argc, argv, "")) != -1 ) { switch ( opt ) { default: return 1; } } load_font(); if ( pipe(redraw_pipe) < 0 ) err(1, "pipe"); rows = 25; columns = 80; // TODO: Overflow. scrollback = calloc(sizeof(struct entry), rows * columns); if ( !scrollback ) err(1, "malloc"); WINDOW_WIDTH = columns * FONT_WIDTH; WINDOW_HEIGHT = rows * FONT_HEIGHT; default_bgcolor = make_color_a(0, 0, 0, 220); default_fgcolor = palette[7] | 0xFF000000; current_bgcolor = default_bgcolor; current_fgcolor = default_fgcolor; for ( size_t y = 0; y < rows; y++ ) { for ( size_t x = 0; x < columns; x++ ) { struct entry* entry = &scrollback[y * columns + x]; entry->attr = 0; entry->fgcolor = current_fgcolor; entry->bgcolor = current_bgcolor; entry->wc = 0; } } display_create_window(connection, WINDOW_ID); display_resize_window(connection, WINDOW_ID, WINDOW_WIDTH, WINDOW_HEIGHT); display_title_window(connection, WINDOW_ID, "Terminal"); struct winsize ws; ws.ws_row = rows; ws.ws_col = columns; char path[TTY_NAME_MAX + 1]; int slave_fd; if ( openpty(&master_fd, &slave_fd, path, NULL, &ws) < 0 ) err(1, "openpty"); sigset_t saved_mask, sigchld_mask; sigemptyset(&sigchld_mask); sigaddset(&sigchld_mask, SIGCHLD); sigprocmask(SIG_BLOCK, &sigchld_mask, &saved_mask); struct sigaction sa = { .sa_handler = signal_handler }; struct sigaction old_sa; sigaction(SIGCHLD, &sa, &old_sa); pid_t child_pid = fork(); if ( child_pid < 0 ) err(1, "fork"); if ( !child_pid ) { sigprocmask(SIG_SETMASK, &saved_mask, NULL); if ( setsid() < 0 ) { warn("setsid"); _exit(1); } if ( ioctl(slave_fd, TIOCSCTTY) < 0 ) { warn("ioctl: TIOCSCTTY"); _exit(1); } if ( close(0) < 0 || close(1) < 0 || close(2) < 0 ) { warn("close"); _exit(1); } if ( dup2(slave_fd, 0) != 0 || dup2(slave_fd, 1) != 1 || dup2(slave_fd, 2) != 2 ) { warn("dup"); _exit(1); } if ( closefrom(3) < 0 ) { warn("closefrom"); _exit(1); } const char* program; if ( argc <= optind ) { program = "sh"; uid_t uid = getuid(); struct passwd* pwd = getpwuid(uid); if ( !pwd ) warn("getpwuid: %ju", (uintmax_t) uid); else program = pwd->pw_shell; char* login_program; if ( asprintf(&login_program, "-%s", program) < 0 ) { warn("malloc"); _exit(1); } execlp(program, login_program, (const char*) NULL); } else { // Support explicit login shells using a leading dash. program = argv[optind]; if ( program[0] == '-' ) program++; execvp(program, argv + optind); } warn("%s", program); _exit(127); } int errnum; pthread_t outthread; if ( (errnum = pthread_create(&outthread, NULL, outgoing_thread, NULL)) ) { errno = errnum; err(1, "pthread_create"); } struct display_event_handlers handlers; memset(&handlers, 0, sizeof(handlers)); handlers.disconnect_handler = on_disconnect; handlers.quit_handler = on_quit; handlers.resize_handler = on_resize; handlers.keyboard_handler = on_keyboard; const nfds_t nfds = 2; struct pollfd pfds[nfds]; pfds[0].fd = redraw_pipe[0]; pfds[0].events = POLLIN; pfds[0].revents = 0; pfds[1].fd = display_connection_fd(connection); pfds[1].events = POLLIN; pfds[1].revents = 0; while ( !need_exit ) { int status; if ( 0 < waitpid(child_pid, &status, WNOHANG) ) break; if ( need_redraw ) { draw(connection); need_redraw = false; } if ( need_show ) { display_show_window(connection, WINDOW_ID); need_show = false; } if ( ppoll(pfds, nfds, NULL, &saved_mask) < 0 ) { if ( errno == EINTR ) continue; err(1, "poll"); } if ( pfds[0].revents ) { pthread_mutex_lock(&scrollback_mutex); char c; ssize_t amount = read(redraw_pipe[0], &c, 1); if ( amount < 0 ) err(1, "read: redraw_pipe"); if ( 0 < amount ) { redraw_pipe_written = false; need_redraw = true; } pthread_mutex_unlock(&scrollback_mutex); } if ( pfds[1].revents ) { while ( display_poll_event(connection, &handlers) == 0 ); } } display_disconnect(connection); return 0; }