/* vim: tabstop=4 shiftwidth=4 noexpandtab * This file is part of ToaruOS and is released under the terms * of the NCSA / University of Illinois License - see LICENSE.md * Copyright (C) 2013-2018 K. Lange * * Terminal Emulator * * Graphical terminal emulator. * * Provides a number of features: * - Windowed and full screen modes * - Antialiased fonts * - Built-in fallback bitmap font * - ANSI escape support * - 256 colors */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TRACE_APP_NAME "terminal" #include #include #include #include #include #include #include #include #include #include #include /* 16- and 256-color palette */ #include "terminal-palette.h" /* Bitmap font */ #include "terminal-font.h" /* Show help text */ static void usage(char * argv[]) { printf( "Terminal Emulator\n" "\n" "usage: %s [-Fbxn] [-s SCALE] [-g WIDTHxHEIGHT] [COMMAND...]\n" "\n" " -F --fullscreen \033[3mRun in fullscreen (background) mode.\033[0m\n" " -b --bitmap \033[3mUse the integrated bitmap font.\033[0m\n" " -s --scale \033[3mScale the font in SDF mode by a given amount.\033[0m\n" " -h --help \033[3mShow this help message.\033[0m\n" " -x --grid \033[3mMake resizes round to nearest match for character cell size.\033[0m\n" " -n --no-frame \033[3mDisable decorations.\033[0m\n" " -g --geometry \033[3mSet requested terminal size WIDTHxHEIGHT\033[0m\n" "\n" " This terminal emulator provides basic support for VT220 escapes and\n" " XTerm extensions, including 256 color support and font effects.\n", argv[0]); } /* master and slave pty descriptors */ static int fd_master, fd_slave; static FILE * terminal; static pid_t child_pid = 0; static int scale_fonts = 0; /* Whether fonts should be scaled */ static float font_scaling = 1.0; /* How much they should be scaled by */ static float font_gamma = 1.7; /* Gamma to use for SDF library */ static uint16_t term_width = 0; /* Width of the terminal (in cells) */ static uint16_t term_height = 0; /* Height of the terminal (in cells) */ static uint16_t font_size = 16; /* Font size according to SDF library */ static uint16_t char_width = 9; /* Width of a cell in pixels */ static uint16_t char_height = 17; /* Height of a cell in pixels */ static int csr_x = 0; /* Cursor X */ static int csr_y = 0; /* Cursor Y */ static uint32_t current_fg = 7; /* Current foreground color */ static uint32_t current_bg = 0; /* Current background color */ static term_cell_t * term_buffer = NULL; /* The terminal cell buffer */ static term_cell_t * term_buffer_a = NULL; static term_cell_t * term_buffer_b = NULL; static term_state_t * ansi_state = NULL; /* ANSI parser library state */ static int active_buffer = 0; static int _orig_x = 0; static int _orig_y = 0; static uint32_t _orig_fg = 7; static uint32_t _orig_bg = 0; static bool cursor_on = 1; /* Whether or not the cursor should be rendered */ static bool _fullscreen = 0; /* Whether or not we are running in fullscreen mode (GUI only) */ static bool _no_frame = 0; /* Whether to disable decorations or not */ static bool _use_sdf = 1; /* Whether or not to use SDF text rendering */ static bool _hold_out = 0; /* state indicator on last cell ignore \n */ static bool _free_size = 1; /* Disable rounding when resized */ static list_t * images_list = NULL; static int menu_bar_height = 24; /* Text selection information */ static int selection = 0; static int selection_start_x = 0; static int selection_start_y = 0; static int selection_end_x = 0; static int selection_end_y = 0; static char * selection_text = NULL; static int _selection_count = 0; static int _selection_i = 0; /* Mouse state */ static int last_mouse_x = -1; static int last_mouse_y = -1; static int button_state = 0; static unsigned long long mouse_ticks = 0; static yutani_window_t * window = NULL; /* GUI window */ static yutani_t * yctx = NULL; /* Window flip bounds */ static int32_t l_x = INT32_MAX; static int32_t l_y = INT32_MAX; static int32_t r_x = -1; static int32_t r_y = -1; static uint32_t window_width = 640; static uint32_t window_height = 480; #define TERMINAL_TITLE_SIZE 512 static char terminal_title[TERMINAL_TITLE_SIZE]; static size_t terminal_title_length = 0; static gfx_context_t * ctx; static struct MenuList * menu_right_click = NULL; static void render_decors(void); static void term_clear(); static void reinit(); static void term_redraw_cursor(); static int decor_left_width = 0; static int decor_top_height = 0; static int decor_right_width = 0; static int decor_bottom_height = 0; static int decor_width = 0; static int decor_height = 0; struct scrollback_row { unsigned short width; term_cell_t cells[]; }; #define MAX_SCROLLBACK 10240 static list_t * scrollback_list = NULL; static int scrollback_offset = 0; /* Menu bar entries */ struct menu_bar terminal_menu_bar = {0}; struct menu_bar_entries terminal_menu_entries[] = { {"File", "file"}, {"Edit", "edit"}, {"View", "view"}, {"Help", "help"}, {NULL, NULL}, }; /* Trigger to exit the terminal when the child process dies or * we otherwise receive an exit signal */ static volatile int exit_application = 0; static void cell_redraw(uint16_t x, uint16_t y); static void cell_redraw_inverted(uint16_t x, uint16_t y); static uint64_t get_ticks(void) { struct timeval now; gettimeofday(&now, NULL); return (uint64_t)now.tv_sec * 1000000LL + (uint64_t)now.tv_usec; } static void display_flip(void) { if (l_x != INT32_MAX && l_y != INT32_MAX) { flip(ctx); yutani_flip_region(yctx, window, l_x, l_y, r_x - l_x, r_y - l_y); l_x = INT32_MAX; l_y = INT32_MAX; r_x = -1; r_y = -1; } } /* Returns the lower of two shorts */ static int32_t min(int32_t a, int32_t b) { return (a < b) ? a : b; } /* Returns the higher of two shorts */ static int32_t max(int32_t a, int32_t b) { return (a > b) ? a : b; } /* * Convert codepoint to UTF-8 * * Returns length of byte sequence written. */ static int to_eight(uint32_t codepoint, char * out) { memset(out, 0x00, 7); if (codepoint < 0x0080) { out[0] = (char)codepoint; } else if (codepoint < 0x0800) { out[0] = 0xC0 | (codepoint >> 6); out[1] = 0x80 | (codepoint & 0x3F); } else if (codepoint < 0x10000) { out[0] = 0xE0 | (codepoint >> 12); out[1] = 0x80 | ((codepoint >> 6) & 0x3F); out[2] = 0x80 | (codepoint & 0x3F); } else if (codepoint < 0x200000) { out[0] = 0xF0 | (codepoint >> 18); out[1] = 0x80 | ((codepoint >> 12) & 0x3F); out[2] = 0x80 | ((codepoint >> 6) & 0x3F); out[3] = 0x80 | ((codepoint) & 0x3F); } else if (codepoint < 0x4000000) { out[0] = 0xF8 | (codepoint >> 24); out[1] = 0x80 | (codepoint >> 18); out[2] = 0x80 | ((codepoint >> 12) & 0x3F); out[3] = 0x80 | ((codepoint >> 6) & 0x3F); out[4] = 0x80 | ((codepoint) & 0x3F); } else { out[0] = 0xF8 | (codepoint >> 30); out[1] = 0x80 | ((codepoint >> 24) & 0x3F); out[2] = 0x80 | ((codepoint >> 18) & 0x3F); out[3] = 0x80 | ((codepoint >> 12) & 0x3F); out[4] = 0x80 | ((codepoint >> 6) & 0x3F); out[5] = 0x80 | ((codepoint) & 0x3F); } return strlen(out); } /* Set the terminal title string */ static void set_title(char * c) { int len = min(TERMINAL_TITLE_SIZE, strlen(c)+1); memcpy(terminal_title, c, len); terminal_title[len-1] = '\0'; terminal_title_length = len - 1; render_decors(); } /* Call a function for each selected cell */ static void iterate_selection(void (*func)(uint16_t x, uint16_t y)) { if (selection_end_y < selection_start_y) { for (int x = selection_end_x; x < term_width; ++x) { func(x, selection_end_y); } for (int y = selection_end_y + 1; y < selection_start_y; ++y) { for (int x = 0; x < term_width; ++x) { func(x, y); } } for (int x = 0; x <= selection_start_x; ++x) { func(x, selection_start_y); } } else if (selection_start_y == selection_end_y) { if (selection_start_x > selection_end_x) { for (int x = selection_end_x; x <= selection_start_x; ++x) { func(x, selection_start_y); } } else { for (int x = selection_start_x; x <= selection_end_x; ++x) { func(x, selection_start_y); } } } else { for (int x = selection_start_x; x < term_width; ++x) { func(x, selection_start_y); } for (int y = selection_start_y + 1; y < selection_end_y; ++y) { for (int x = 0; x < term_width; ++x) { func(x, y); } } for (int x = 0; x <= selection_end_x; ++x) { func(x, selection_end_y); } } } /* Redraw original cells in selected text */ static void unredraw_selection(void) { iterate_selection(cell_redraw); } /* Redraw the selection with the selection hint (inversion) */ static void redraw_selection(void) { iterate_selection(cell_redraw_inverted); } /* Figure out how long the UTF-8 selection string should be. */ static void count_selection(uint16_t x, uint16_t y) { term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); if (!(cell->flags & ANSI_EXT_IMG)) { if (((uint32_t *)cell)[0] != 0x00000000) { char tmp[7]; _selection_count += to_eight(cell->c, tmp); } } if (x == term_width - 1) { _selection_count++; } } /* Fill the selection text buffer with the selected text. */ void write_selection(uint16_t x, uint16_t y) { term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); if (!(cell->flags & ANSI_EXT_IMG)) { if (((uint32_t *)cell)[0] != 0x00000000) { char tmp[7]; int count = to_eight(cell->c, tmp); for (int i = 0; i < count; ++i) { selection_text[_selection_i] = tmp[i]; _selection_i++; } } } if (x == term_width - 1) { selection_text[_selection_i] = '\n';; _selection_i++; } } /* Copy the selection text to the clipboard. */ static char * copy_selection(void) { _selection_count = 0; iterate_selection(count_selection); fprintf(stderr, "Selection length is %d\n", _selection_count); if (selection_text) { free(selection_text); } if (_selection_count == 0) { return NULL; } selection_text = malloc(_selection_count + 1); selection_text[_selection_count] = '\0'; _selection_i = 0; iterate_selection(write_selection); if (selection_text[_selection_count-1] == '\n') { /* Don't end on a line feed */ selection_text[_selection_count-1] = '\0'; } yutani_set_clipboard(yctx, selection_text); return selection_text; } /* Stuffs a string into the stdin of the terminal's child process * Useful for things like the ANSI DSR command. */ static void input_buffer_stuff(char * str) { size_t s = strlen(str) + 1; write(fd_master, str, s); } /* Redraw the decorations */ static void render_decors(void) { /* Don't draw decorations or bother advertising the window if in "fullscreen mode" */ if (_fullscreen) return; if (!_no_frame) { /* Draw the decorations */ render_decorations(window, ctx, terminal_title_length ? terminal_title : "Terminal"); /* Update menu bar position and size */ terminal_menu_bar.x = decor_left_width; terminal_menu_bar.y = decor_top_height; terminal_menu_bar.width = window_width; terminal_menu_bar.window = window; /* Redraw the menu bar */ menu_bar_render(&terminal_menu_bar, ctx); } /* Advertise the window icon to the panel. */ yutani_window_advertise_icon(yctx, window, terminal_title_length ? terminal_title : "Terminal", "utilities-terminal"); /* * Flip the whole window * We do this regardless of whether we drew decorations to catch * a case where decorations are toggled. */ l_x = 0; l_y = 0; r_x = window->width; r_y = window->height; display_flip(); } /* Set a pixel in the terminal cell area */ static inline void term_set_point(uint16_t x, uint16_t y, uint32_t color ) { if (_fullscreen) { /* In full screen mode, pre-blend the color over black. */ color = alpha_blend_rgba(premultiply(rgba(0,0,0,0xFF)), color); } if (!_no_frame) { GFX(ctx, (x+decor_left_width),(y+decor_top_height+menu_bar_height)) = color; } else { GFX(ctx, x,y) = color; } } /* Draw a partial block character. */ static void draw_semi_block(int c, int x, int y, uint32_t fg, uint32_t bg) { int height; bg = premultiply(bg); fg = premultiply(fg); if (c == 0x2580) { uint32_t t = bg; bg = fg; fg = t; c = 0x2584; for (uint8_t i = 0; i < char_height; ++i) { for (uint8_t j = 0; j < char_width; ++j) { term_set_point(x+j,y+i,bg); } } } c -= 0x2580; height = char_height - ((c * char_height) / 8); for (uint8_t i = height; i < char_height; ++i) { for (uint8_t j = 0; j < char_width; ++j) { term_set_point(x+j, y+i,fg); } } } /* Convert unicode codepoint to fallback codepage codepoint */ static uint32_t ununicode(uint32_t c) { switch (c) { case L'☺': return 1; case L'☻': return 2; case L'♥': return 3; case L'♦': return 4; case L'♣': return 5; case L'♠': return 6; case L'•': return 7; case L'◘': return 8; case L'○': return 9; case L'◙': return 10; case L'♂': return 11; case L'♀': return 12; case L'♪': return 13; case L'♫': return 14; case L'☼': return 15; case L'►': return 16; case L'◄': return 17; case L'↕': return 18; case L'‼': return 19; case L'¶': return 20; case L'§': return 21; case L'▬': return 22; case L'↨': return 23; case L'↑': return 24; case L'↓': return 25; case L'→': return 26; case L'←': return 27; case L'∟': return 28; case L'↔': return 29; case L'▲': return 30; case L'▼': return 31; /* ASCII text */ case L'⌂': return 127; case L'Ç': return 128; case L'ü': return 129; case L'é': return 130; case L'â': return 131; case L'ä': return 132; case L'à': return 133; case L'å': return 134; case L'ç': return 135; case L'ê': return 136; case L'ë': return 137; case L'è': return 138; case L'ï': return 139; case L'î': return 140; case L'ì': return 141; case L'Ä': return 142; case L'Å': return 143; case L'É': return 144; case L'æ': return 145; case L'Æ': return 146; case L'ô': return 147; case L'ö': return 148; case L'ò': return 149; case L'û': return 150; case L'ù': return 151; case L'ÿ': return 152; case L'Ö': return 153; case L'Ü': return 154; case L'¢': return 155; case L'£': return 156; case L'¥': return 157; case L'₧': return 158; case L'ƒ': return 159; case L'á': return 160; case L'í': return 161; case L'ó': return 162; case L'ú': return 163; case L'ñ': return 164; case L'Ñ': return 165; case L'ª': return 166; case L'º': return 167; case L'¿': return 168; case L'⌐': return 169; case L'¬': return 170; case L'½': return 171; case L'¼': return 172; case L'¡': return 173; case L'«': return 174; case L'»': return 175; case L'░': return 176; case L'▒': return 177; case L'▓': return 178; case L'│': return 179; case L'┤': return 180; case L'╡': return 181; case L'╢': return 182; case L'╖': return 183; case L'╕': return 184; case L'╣': return 185; case L'║': return 186; case L'╗': return 187; case L'╝': return 188; case L'╜': return 189; case L'╛': return 190; case L'┐': return 191; case L'└': return 192; case L'┴': return 193; case L'┬': return 194; case L'├': return 195; case L'─': return 196; case L'┼': return 197; case L'╞': return 198; case L'╟': return 199; case L'╚': return 200; case L'╔': return 201; case L'╩': return 202; case L'╦': return 203; case L'╠': return 204; case L'═': return 205; case L'╬': return 206; case L'╧': return 207; case L'╨': return 208; case L'╤': return 209; case L'╥': return 210; case L'╙': return 211; case L'╘': return 212; case L'╒': return 213; case L'╓': return 214; case L'╫': return 215; case L'╪': return 216; case L'┘': return 217; case L'┌': return 218; case L'█': return 219; case L'▄': return 220; case L'▌': return 221; case L'▐': return 222; case L'▀': return 223; case L'α': return 224; case L'ß': return 225; case L'Γ': return 226; case L'π': return 227; case L'Σ': return 228; case L'σ': return 229; case L'µ': return 230; case L'τ': return 231; case L'Φ': return 232; case L'Θ': return 233; case L'Ω': return 234; case L'δ': return 235; case L'∞': return 236; case L'φ': return 237; case L'ε': return 238; case L'∩': return 239; case L'≡': return 240; case L'±': return 241; case L'≥': return 242; case L'≤': return 243; case L'⌠': return 244; case L'⌡': return 245; case L'÷': return 246; case L'≈': return 247; case L'°': return 248; case L'∙': return 249; case L'·': return 250; case L'√': return 251; case L'ⁿ': return 252; case L'²': return 253; case L'■': return 254; } return 4; } /* Write a character to the window. */ static void term_write_char(uint32_t val, uint16_t x, uint16_t y, uint32_t fg, uint32_t bg, uint8_t flags) { uint32_t _fg, _bg; /* Select foreground color from palette. */ if (fg < PALETTE_COLORS) { _fg = term_colors[fg]; _fg |= 0xFF << 24; } else { _fg = fg; } /* Select background color from aplette. */ if (bg < PALETTE_COLORS) { _bg = term_colors[bg]; if (flags & ANSI_SPECBG) { _bg |= 0xFF << 24; } else { _bg |= TERM_DEFAULT_OPAC << 24; } } else { _bg = bg; } /* Draw block characters */ if (val >= 0x2580 && val <= 0x2588) { for (uint8_t i = 0; i < char_height; ++i) { for (uint8_t j = 0; j < char_width; ++j) { term_set_point(x+j,y+i,premultiply(_bg)); } } draw_semi_block(val, x, y, _fg, _bg); goto _extra_stuff; } /* TODO: This is where a Unicode-capable rendering engine * such as freetype should be patched in. The older * version of this terminal included in mainline * ToaruOS included freetype support. */ /* Convert other unicode characters. */ if (val > 128) { val = ununicode(val); } /* Draw glyphs */ if (_use_sdf) { /* Draw using the Toaru SDF rendering library */ char tmp[2] = {val,0}; for (uint8_t i = 0; i < char_height; ++i) { for (uint8_t j = 0; j < char_width; ++j) { term_set_point(x+j,y+i,_bg); } } if (val != 0 && val != ' ' && _fg != _bg) { int _font = SDF_FONT_MONO; if (flags & ANSI_BOLD && flags & ANSI_ITALIC) { _font = SDF_FONT_MONO_BOLD_OBLIQUE; } else if (flags & ANSI_BOLD) { _font = SDF_FONT_MONO_BOLD; } else if (flags & ANSI_ITALIC) { _font = SDF_FONT_MONO_OBLIQUE; } if (_no_frame) { draw_sdf_string_gamma(ctx, x-1, y, tmp, font_size, _fg, _font, font_gamma); } else { draw_sdf_string_gamma(ctx, x+decor_left_width-1, y+decor_top_height+menu_bar_height, tmp, font_size, _fg, _font, font_gamma); } } } else { /* Draw using the bitmap font. */ uint16_t * c = large_font[val]; for (uint8_t i = 0; i < char_height; ++i) { for (uint8_t j = 0; j < char_width; ++j) { if (c[i] & (1 << (15-j))) { term_set_point(x+j,y+i,_fg); } else { term_set_point(x+j,y+i,_bg); } } } } /* Draw additional text elements, like underlines and cross-outs. */ _extra_stuff: if (flags & ANSI_UNDERLINE) { for (uint8_t i = 0; i < char_width; ++i) { term_set_point(x + i, y + char_height - 1, _fg); } } if (flags & ANSI_CROSS) { for (uint8_t i = 0; i < char_width; ++i) { term_set_point(x + i, y + char_height - 7, _fg); } } if (flags & ANSI_BORDER) { for (uint8_t i = 0; i < char_height; ++i) { term_set_point(x , y + i, _fg); term_set_point(x + (char_width - 1), y + i, _fg); } for (uint8_t j = 0; j < char_width; ++j) { term_set_point(x + j, y, _fg); term_set_point(x + j, y + (char_height - 1), _fg); } } /* Calculate the bounds of the updated region of the window */ if (!_no_frame) { l_x = min(l_x, decor_left_width + x); l_y = min(l_y, decor_top_height+menu_bar_height + y); if (flags & ANSI_WIDE) { r_x = max(r_x, decor_left_width + x + char_width * 2); r_y = max(r_y, decor_top_height+menu_bar_height + y + char_height * 2); } else { r_x = max(r_x, decor_left_width + x + char_width); r_y = max(r_y, decor_top_height+menu_bar_height + y + char_height); } } else { l_x = min(l_x, x); l_y = min(l_y, y); if (flags & ANSI_WIDE) { r_x = max(r_x, x + char_width * 2); r_y = max(r_y, y + char_height * 2); } else { r_x = max(r_x, x + char_width); r_y = max(r_y, y + char_height); } } } /* Set a terminal cell */ static void cell_set(uint16_t x, uint16_t y, uint32_t c, uint32_t fg, uint32_t bg, uint32_t flags) { /* Avoid setting cells out of range. */ if (x >= term_width || y >= term_height) return; /* Calculate the cell position in the terminal buffer */ term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); /* Set cell attributes */ cell->c = c; cell->fg = fg; cell->bg = bg; cell->flags = flags; } /* Redraw an embedded image cell */ static void redraw_cell_image(uint16_t x, uint16_t y, term_cell_t * cell) { /* Avoid setting cells out of range. */ if (x >= term_width || y >= term_height) return; /* Draw the image data */ uint32_t * data = (uint32_t *)cell->fg; for (uint32_t yy = 0; yy < char_height; ++yy) { for (uint32_t xx = 0; xx < char_width; ++xx) { term_set_point(x * char_width + xx, y * char_height + yy, *data); data++; } } /* Update bounds */ if (!_no_frame) { l_x = min(l_x, decor_left_width + x * char_width); l_y = min(l_y, decor_top_height+menu_bar_height + y * char_height); r_x = max(r_x, decor_left_width + x * char_width + char_width); r_y = max(r_y, decor_top_height+menu_bar_height + y * char_height + char_height); } else { l_x = min(l_x, x * char_width); l_y = min(l_y, y * char_height); r_x = max(r_x, x * char_width + char_width); r_y = max(r_y, y * char_height + char_height); } } /* Redraw a text cell normally. */ static void cell_redraw(uint16_t x, uint16_t y) { /* Avoid cells out of range. */ if (x >= term_width || y >= term_height) return; /* Calculate the cell position in the terminal buffer */ term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); /* If it's an image cell, redraw the image data. */ if (cell->flags & ANSI_EXT_IMG) { redraw_cell_image(x,y,cell); return; } /* Special case empty cells. */ if (((uint32_t *)cell)[0] == 0x00000000) { term_write_char(' ', x * char_width, y * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS); } else { term_write_char(cell->c, x * char_width, y * char_height, cell->fg, cell->bg, cell->flags); } } /* Redraw text cell inverted. */ static void cell_redraw_inverted(uint16_t x, uint16_t y) { /* Avoid cells out of range. */ if (x >= term_width || y >= term_height) return; /* Calculate the cell position in the terminal buffer */ term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); /* If it's an image cell, redraw the image data. */ if (cell->flags & ANSI_EXT_IMG) { redraw_cell_image(x,y,cell); return; } /* Special case empty cells. */ if (((uint32_t *)cell)[0] == 0x00000000) { term_write_char(' ', x * char_width, y * char_height, TERM_DEFAULT_BG, TERM_DEFAULT_FG, TERM_DEFAULT_FLAGS | ANSI_SPECBG); } else { term_write_char(cell->c, x * char_width, y * char_height, cell->bg, cell->fg, cell->flags | ANSI_SPECBG); } } /* Redraw text cell with a surrounding box (used by cursor) */ static void cell_redraw_box(uint16_t x, uint16_t y) { /* Avoid cells out of range. */ if (x >= term_width || y >= term_height) return; /* Calculate the cell position in the terminal buffer */ term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); /* If it's an image cell, redraw the image data. */ if (cell->flags & ANSI_EXT_IMG) { redraw_cell_image(x,y,cell); return; } /* Special case empty cells. */ if (((uint32_t *)cell)[0] == 0x00000000) { term_write_char(' ', x * char_width, y * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS | ANSI_BORDER); } else { term_write_char(cell->c, x * char_width, y * char_height, cell->fg, cell->bg, cell->flags | ANSI_BORDER); } } /* Draw the cursor cell */ static void render_cursor() { if (!window->focused) { /* An unfocused terminal should draw an unfilled box. */ cell_redraw_box(csr_x, csr_y); } else { /* A focused terminal draws a solid box. */ cell_redraw_inverted(csr_x, csr_y); } } static uint8_t cursor_flipped = 0; /* A soft request to draw the cursor. */ static void draw_cursor() { if (!cursor_on) return; mouse_ticks = get_ticks(); cursor_flipped = 0; render_cursor(); } /* Timer callback to flip (flash) the cursor */ static void maybe_flip_cursor(void) { uint64_t ticks = get_ticks(); if (ticks > mouse_ticks + 600000LL) { mouse_ticks = ticks; if (scrollback_offset != 0) { return; /* Don't flip cursor while drawing scrollback */ } if (window->focused && cursor_flipped) { cell_redraw(csr_x, csr_y); } else { render_cursor(); } display_flip(); cursor_flipped = 1 - cursor_flipped; } } /* Draw all cells. Duplicates code from cell_redraw to avoid unecessary bounds checks. */ static void term_redraw_all() { for (int i = 0; i < term_height; i++) { for (int x = 0; x < term_width; ++x) { /* Calculate the cell position in the terminal buffer */ term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (i * term_width + x) * sizeof(term_cell_t)); /* If it's an image cell, redraw the image data. */ if (cell->flags & ANSI_EXT_IMG) { redraw_cell_image(x,i,cell); continue; } /* Special case empty cells. */ if (((uint32_t *)cell)[0] == 0x00000000) { term_write_char(' ', x * char_width, i * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS); } else { term_write_char(cell->c, x * char_width, i * char_height, cell->fg, cell->bg, cell->flags); } } } } /* Remove no-longer-visible image cell data. */ static void flush_unused_images(void) { list_t * tmp = list_create(); for (int y = 0; y < term_height; ++y) { for (int x = 0; x < term_width; ++x) { term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); if (cell->flags & ANSI_EXT_IMG) { list_insert(tmp, (void *)cell->fg); } } } foreach(node, images_list) { if (!list_find(tmp, node->value)) { free(node->value); } } list_free(images_list); images_list = tmp; } /* Scroll the terminal up or down. */ static void term_scroll(int how_much) { /* A large scroll request should just clear the screen. */ if (how_much >= term_height || -how_much >= term_height) { term_clear(); return; } /* A request to scroll 0... is a request not to scroll. */ if (how_much == 0) { return; } /* Redraw the cursor before continuing. */ cell_redraw(csr_x, csr_y); if (how_much > 0) { /* Scroll up */ memmove(term_buffer, (void *)((uintptr_t)term_buffer + sizeof(term_cell_t) * term_width), sizeof(term_cell_t) * term_width * (term_height - how_much)); /* Reset the "new" row to clean cells */ memset((void *)((uintptr_t)term_buffer + sizeof(term_cell_t) * term_width * (term_height - how_much)), 0x0, sizeof(term_cell_t) * term_width * how_much); /* In graphical modes, we will shift the graphics buffer up as necessary */ uintptr_t dst, src; size_t siz = char_height * (term_height - how_much) * GFX_W(ctx) * GFX_B(ctx); if (!_no_frame) { /* Must include decorations */ dst = (uintptr_t)ctx->backbuffer + (GFX_W(ctx) * (decor_top_height+menu_bar_height)) * GFX_B(ctx); src = (uintptr_t)ctx->backbuffer + (GFX_W(ctx) * (decor_top_height+menu_bar_height + char_height * how_much)) * GFX_B(ctx); } else { /* Can skip decorations */ dst = (uintptr_t)ctx->backbuffer; src = (uintptr_t)ctx->backbuffer + (GFX_W(ctx) * char_height * how_much) * GFX_B(ctx); } /* Perform the shift */ memmove((void *)dst, (void *)src, siz); /* And redraw the new rows */ for (int i = 0; i < how_much; ++i) { for (uint16_t x = 0; x < term_width; ++x) { cell_set(x,term_height - how_much,' ', current_fg, current_bg, ansi_state->flags); cell_redraw(x, term_height - how_much); } } } else { how_much = -how_much; /* Scroll down */ memmove((void *)((uintptr_t)term_buffer + sizeof(term_cell_t) * term_width), term_buffer, sizeof(term_cell_t) * term_width * (term_height - how_much)); /* Reset the "new" row to clean cells */ memset(term_buffer, 0x0, sizeof(term_cell_t) * term_width * how_much); uintptr_t dst, src; size_t siz = char_height * (term_height - how_much) * GFX_W(ctx) * GFX_B(ctx); if (!_no_frame) { src = (uintptr_t)ctx->backbuffer + (GFX_W(ctx) * (decor_top_height+menu_bar_height)) * GFX_B(ctx); dst = (uintptr_t)ctx->backbuffer + (GFX_W(ctx) * (decor_top_height+menu_bar_height + char_height * how_much)) * GFX_B(ctx); } else { src = (uintptr_t)ctx->backbuffer; dst = (uintptr_t)ctx->backbuffer + (GFX_W(ctx) * char_height * how_much) * GFX_B(ctx); } /* Perform the shift */ memmove((void *)dst, (void *)src, siz); /* And redraw the new rows */ for (int i = 0; i < how_much; ++i) { for (uint16_t x = 0; x < term_width; ++x) { cell_redraw(x, i); } } } /* Remove image data for image cells that are no longer on screen. */ flush_unused_images(); /* Flip the entire window. */ yutani_flip(yctx, window); } /* Is this a wide character? (does wcwidth == 2) */ static int is_wide(uint32_t codepoint) { if (codepoint < 256) return 0; return wcwidth(codepoint) == 2; } /* Save the row that is about to be scrolled offscreen into the scrollback buffer. */ static void save_scrollback(void) { /* If the scrollback is already full, remove the oldest element. */ if (scrollback_list->length == MAX_SCROLLBACK) { free(list_dequeue(scrollback_list)); } struct scrollback_row * row = malloc(sizeof(struct scrollback_row) + sizeof(term_cell_t) * term_width + 20); row->width = term_width; for (int i = 0; i < term_width; ++i) { term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (i) * sizeof(term_cell_t)); memcpy(&row->cells[i], cell, sizeof(term_cell_t)); } list_insert(scrollback_list, row); } /* Draw the scrollback. */ static void redraw_scrollback(void) { if (!scrollback_offset) { term_redraw_all(); display_flip(); return; } if (scrollback_offset < term_height) { for (int i = scrollback_offset; i < term_height; i++) { int y = i - scrollback_offset; for (int x = 0; x < term_width; ++x) { term_cell_t * cell = (term_cell_t *)((uintptr_t)term_buffer + (y * term_width + x) * sizeof(term_cell_t)); if (cell->flags & ANSI_EXT_IMG) { redraw_cell_image(x,i,cell); continue; } if (((uint32_t *)cell)[0] == 0x00000000) { term_write_char(' ', x * char_width, i * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS); } else { term_write_char(cell->c, x * char_width, i * char_height, cell->fg, cell->bg, cell->flags); } } } node_t * node = scrollback_list->tail; for (int i = 0; i < scrollback_offset; ++i) { struct scrollback_row * row = (struct scrollback_row *)node->value; int y = scrollback_offset - 1 - i; int width = row->width; if (width > term_width) { width = term_width; } else { for (int x = row->width; x < term_width; ++x) { term_write_char(' ', x * char_width, y * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS); } } for (int x = 0; x < width; ++x) { term_cell_t * cell = &row->cells[x]; if (((uint32_t *)cell)[0] == 0x00000000) { term_write_char(' ', x * char_width, y * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS); } else { term_write_char(cell->c, x * char_width, y * char_height, cell->fg, cell->bg, cell->flags); } } node = node->prev; } } else { node_t * node = scrollback_list->tail; for (int i = 0; i < scrollback_offset - term_height; ++i) { node = node->prev; } for (int i = scrollback_offset - term_height; i < scrollback_offset; ++i) { struct scrollback_row * row = (struct scrollback_row *)node->value; int y = scrollback_offset - 1 - i; int width = row->width; if (width > term_width) { width = term_width; } else { for (int x = row->width; x < term_width; ++x) { term_write_char(' ', x * char_width, y * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS); } } for (int x = 0; x < width; ++x) { term_cell_t * cell = &row->cells[x]; if (((uint32_t *)cell)[0] == 0x00000000) { term_write_char(' ', x * char_width, y * char_height, TERM_DEFAULT_FG, TERM_DEFAULT_BG, TERM_DEFAULT_FLAGS); } else { term_write_char(cell->c, x * char_width, y * char_height, cell->fg, cell->bg, cell->flags); } } node = node->prev; } } display_flip(); } /* * ANSI callback for writing characters. * Parses some things (\n\r, etc.) itself that should probably * be moved into the ANSI library. */ static void term_write(char c) { static uint32_t unicode_state = 0; static uint32_t codepoint = 0; cell_redraw(csr_x, csr_y); if (!decode(&unicode_state, &codepoint, (uint8_t)c)) { uint32_t o = codepoint; codepoint = 0; if (c == '\r') { csr_x = 0; return; } if (csr_x < 0) csr_x = 0; if (csr_y < 0) csr_y = 0; if (csr_x == term_width) { csr_x = 0; ++csr_y; } if (csr_y == term_height) { save_scrollback(); term_scroll(1); csr_y = term_height - 1; } if (c == '\n') { if (csr_x == 0 && _hold_out) { _hold_out = 0; return; } ++csr_y; if (csr_y == term_height) { save_scrollback(); term_scroll(1); csr_y = term_height - 1; } draw_cursor(); } else if (c == '\007') { /* bell */ /* XXX play sound */ } else if (c == '\b') { if (csr_x > 0) { --csr_x; } cell_redraw(csr_x, csr_y); draw_cursor(); } else if (c == '\t') { csr_x += (8 - csr_x % 8); draw_cursor(); } else { int wide = is_wide(o); uint8_t flags = ansi_state->flags; if (wide && csr_x == term_width - 1) { csr_x = 0; ++csr_y; } if (wide) { flags = flags | ANSI_WIDE; } cell_set(csr_x,csr_y, o, current_fg, current_bg, flags); cell_redraw(csr_x,csr_y); csr_x++; if (wide && csr_x != term_width) { cell_set(csr_x, csr_y, 0xFFFF, current_fg, current_bg, ansi_state->flags); cell_redraw(csr_x,csr_y); cell_redraw(csr_x-1,csr_y); csr_x++; } } } else if (unicode_state == UTF8_REJECT) { unicode_state = 0; codepoint = 0; } draw_cursor(); } /* ANSI callback to set cursor position */ static void term_set_csr(int x, int y) { cell_redraw(csr_x,csr_y); csr_x = x; csr_y = y; draw_cursor(); } /* ANSI callback to get cursor x position */ static int term_get_csr_x(void) { return csr_x; } /* ANSI callback to get cursor y position */ static int term_get_csr_y(void) { return csr_y; } /* ANSI callback to set cell image data. */ static void term_set_cell_contents(int x, int y, char * data) { char * cell_data = malloc(char_width * char_height * sizeof(uint32_t)); memcpy(cell_data, data, char_width * char_height * sizeof(uint32_t)); list_insert(images_list, cell_data); cell_set(x, y, ' ', (uint32_t)cell_data, 0, ANSI_EXT_IMG); } /* ANSI callback to get character cell width */ static int term_get_cell_width(void) { return char_width; } /* ANSI callback to get character cell height */ static int term_get_cell_height(void) { return char_height; } /* ANSI callback to set cursor visibility */ static void term_set_csr_show(int on) { cursor_on = on; if (on) { draw_cursor(); } } /* ANSI callback to set the foreground/background colors. */ static void term_set_colors(uint32_t fg, uint32_t bg) { current_fg = fg; current_bg = bg; } /* ANSI callback to force the cursor to draw */ static void term_redraw_cursor() { if (term_buffer) { draw_cursor(); } } /* ANSI callback to set a cell to a codepoint (only ever used to set spaces) */ static void term_set_cell(int x, int y, uint32_t c) { cell_set(x, y, c, current_fg, current_bg, ansi_state->flags); cell_redraw(x, y); } /* ANSI callback to clear the terminal. */ static void term_clear(int i) { if (i == 2) { /* Clear all */ csr_x = 0; csr_y = 0; memset((void *)term_buffer, 0x00, term_width * term_height * sizeof(term_cell_t)); if (!_no_frame) { render_decors(); } term_redraw_all(); } else if (i == 0) { /* Clear after cursor */ for (int x = csr_x; x < term_width; ++x) { term_set_cell(x, csr_y, ' '); } for (int y = csr_y + 1; y < term_height; ++y) { for (int x = 0; x < term_width; ++x) { term_set_cell(x, y, ' '); } } } else if (i == 1) { /* Clear before cursor */ for (int y = 0; y < csr_y; ++y) { for (int x = 0; x < term_width; ++x) { term_set_cell(x, y, ' '); } } for (int x = 0; x < csr_x; ++x) { term_set_cell(x, csr_y, ' '); } } flush_unused_images(); } #define SWAP(T,a,b) do { T _a = a; a = b; b = _a; } while(0); static void term_switch_buffer(int buffer) { if (buffer != 0 && buffer != 1) return; if (buffer != active_buffer) { active_buffer = buffer; term_buffer = active_buffer == 0 ? term_buffer_a : term_buffer_b; SWAP(int, csr_x, _orig_x); SWAP(int, csr_y, _orig_y); SWAP(uint32_t, current_fg, _orig_fg); SWAP(uint32_t, current_bg, _orig_bg); term_redraw_all(); display_flip(); } } /* ANSI callbacks */ term_callbacks_t term_callbacks = { term_write, term_set_colors, term_set_csr, term_get_csr_x, term_get_csr_y, term_set_cell, term_clear, term_scroll, term_redraw_cursor, input_buffer_stuff, set_title, term_set_cell_contents, term_get_cell_width, term_get_cell_height, term_set_csr_show, term_switch_buffer, }; /* Write data into the PTY */ static void handle_input(char c) { write(fd_master, &c, 1); display_flip(); } /* Write a string into the PTY */ static void handle_input_s(char * c) { write(fd_master, c, strlen(c)); display_flip(); } /* Scroll the view up (scrollback) */ static void scroll_up(int amount) { int i = 0; while (i < amount && scrollback_list && scrollback_offset < (int)scrollback_list->length) { scrollback_offset ++; i++; } redraw_scrollback(); } /* Scroll the view down (scrollback) */ void scroll_down(int amount) { int i = 0; while (i < amount && scrollback_list && scrollback_offset != 0) { scrollback_offset -= 1; i++; } redraw_scrollback(); } /* Handle a key press from Yutani */ static void key_event(int ret, key_event_t * event) { if (ret) { /* Ctrl-Shift-C - Copy selection */ if ((event->modifiers & KEY_MOD_LEFT_SHIFT || event->modifiers & KEY_MOD_RIGHT_SHIFT) && (event->modifiers & KEY_MOD_LEFT_CTRL || event->modifiers & KEY_MOD_RIGHT_CTRL) && (event->keycode == 'c')) { if (selection) { /* Copy selection */ copy_selection(); } return; } /* Ctrl-Shift-V - Paste selection */ if ((event->modifiers & KEY_MOD_LEFT_SHIFT || event->modifiers & KEY_MOD_RIGHT_SHIFT) && (event->modifiers & KEY_MOD_LEFT_CTRL || event->modifiers & KEY_MOD_RIGHT_CTRL) && (event->keycode == 'v')) { /* Paste selection */ yutani_special_request(yctx, NULL, YUTANI_SPECIAL_REQUEST_CLIPBOARD); return; } /* Left alt */ if (event->modifiers & KEY_MOD_LEFT_ALT || event->modifiers & KEY_MOD_RIGHT_ALT) { handle_input('\033'); } /* Shift-Tab */ if ((event->modifiers & KEY_MOD_LEFT_SHIFT || event->modifiers & KEY_MOD_RIGHT_SHIFT) && event->key == '\t') { handle_input_s("\033[Z"); return; } /* Pass key value to PTY */ handle_input(event->key); } else { /* Special keys without ->key values */ /* Only trigger on key down */ if (event->action == KEY_ACTION_UP) return; switch (event->keycode) { case KEY_F1: handle_input_s("\033OP"); break; case KEY_F2: handle_input_s("\033OQ"); break; case KEY_F3: handle_input_s("\033OR"); break; case KEY_F4: handle_input_s("\033OS"); break; case KEY_F5: handle_input_s("\033[15~"); break; case KEY_F6: handle_input_s("\033[17~"); break; case KEY_F7: handle_input_s("\033[18~"); break; case KEY_F8: handle_input_s("\033[19~"); break; case KEY_F9: handle_input_s("\033[20~"); break; case KEY_F10: handle_input_s("\033[21~"); break; case KEY_F11: handle_input_s("\033[23~"); break; case KEY_F12: /* Toggle decorations */ if (!_fullscreen) { _no_frame = !_no_frame; window_width = window->width - decor_width * (!_no_frame); window_height = window->height - (decor_height + menu_bar_height) * (!_no_frame); reinit(1); } break; case KEY_ARROW_UP: if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[6A"); } else if (event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[5A"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[4A"); } else if (event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[3A"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT) { handle_input_s("\033[2A"); } else { handle_input_s("\033[A"); } break; case KEY_ARROW_DOWN: if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[6B"); } else if (event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[5B"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[4B"); } else if (event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[3B"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT) { handle_input_s("\033[2B"); } else { handle_input_s("\033[B"); } break; case KEY_ARROW_RIGHT: if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[6C"); } else if (event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[5C"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[4C"); } else if (event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[3C"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT) { handle_input_s("\033[2C"); } else { handle_input_s("\033[C"); } break; case KEY_ARROW_LEFT: if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[6D"); } else if (event->modifiers & KEY_MOD_LEFT_CTRL) { handle_input_s("\033[5D"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT && event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[4D"); } else if (event->modifiers & KEY_MOD_LEFT_ALT) { handle_input_s("\033[3D"); } else if (event->modifiers & KEY_MOD_LEFT_SHIFT) { handle_input_s("\033[2D"); } else { handle_input_s("\033[D"); } break; case KEY_PAGE_UP: if (event->modifiers & KEY_MOD_LEFT_SHIFT) { scroll_up(term_height/2); } else { handle_input_s("\033[5~"); } break; case KEY_PAGE_DOWN: if (event->modifiers & KEY_MOD_LEFT_SHIFT) { scroll_down(term_height/2); } else { handle_input_s("\033[6~"); } break; case KEY_HOME: if (event->modifiers & KEY_MOD_LEFT_SHIFT) { if (scrollback_list) { scrollback_offset = scrollback_list->length; redraw_scrollback(); } } else { handle_input_s("\033[H"); } break; case KEY_END: if (event->modifiers & KEY_MOD_LEFT_SHIFT) { scrollback_offset = 0; redraw_scrollback(); } else { handle_input_s("\033[F"); } break; case KEY_DEL: handle_input_s("\033[3~"); break; case KEY_INSERT: handle_input_s("\033[2~"); break; } } } /* Check if the Terminal should close. */ static void check_for_exit(void) { /* If something has set exit_application, we should exit. */ if (exit_application) return; pid_t pid = waitpid(-1, NULL, WNOHANG); /* If the child has exited, we should exit. */ if (pid != child_pid) return; /* Clean up */ exit_application = 1; /* Write [Process terminated] */ char exit_message[] = "[Process terminated]\n"; write(fd_slave, exit_message, sizeof(exit_message)); } static term_cell_t * copy_terminal(int old_width, int old_height, term_cell_t * term_buffer) { term_cell_t * new_term_buffer = malloc(sizeof(term_cell_t) * term_width * term_height); memset(new_term_buffer, 0x0, sizeof(term_cell_t) * term_width * term_height); int offset = 0; if (term_height < old_height) { while (csr_y >= term_height) { offset++; old_height--; csr_y--; } } for (int row = 0; row < min(old_height, term_height); ++row) { for (int col = 0; col < min(old_width, term_width); ++col) { term_cell_t * old_cell = (term_cell_t *)((uintptr_t)term_buffer + ((row + offset) * old_width + col) * sizeof(term_cell_t)); term_cell_t * new_cell = (term_cell_t *)((uintptr_t)new_term_buffer + (row * term_width + col) * sizeof(term_cell_t)); *new_cell = *old_cell; } } if (csr_x >= term_width) { csr_x = term_width-1; } return new_term_buffer; } /* Reinitialize the terminal after a resize. */ static void reinit(int send_sig) { /* Figure out character sizes if fonts have changed. */ if (_use_sdf) { char_width = 9; char_height = 17; font_size = 16; if (scale_fonts) { font_size *= font_scaling; char_height *= font_scaling; char_width *= font_scaling; } } else { char_width = 9; char_height = 20; } int old_width = term_width; int old_height = term_height; /* Resize the terminal buffer */ term_width = window_width / char_width; term_height = window_height / char_height; if (term_buffer) { term_cell_t * new_a = copy_terminal(old_width, old_height, term_buffer_a); term_cell_t * new_b = copy_terminal(old_width, old_height, term_buffer_b); free(term_buffer_a); term_buffer_a = new_a; free(term_buffer_b); term_buffer_b = new_b; if (active_buffer == 0) { term_buffer = new_a; } else { term_buffer = new_b; } } else { term_buffer_a = malloc(sizeof(term_cell_t) * term_width * term_height); memset(term_buffer_a, 0x0, sizeof(term_cell_t) * term_width * term_height); term_buffer_b = malloc(sizeof(term_cell_t) * term_width * term_height); memset(term_buffer_b, 0x0, sizeof(term_cell_t) * term_width * term_height); term_buffer = term_buffer_a; } /* Reset the ANSI library, ensuring we keep certain values */ int old_mouse_state = 0; if (ansi_state) old_mouse_state = ansi_state->mouse_on; ansi_state = ansi_init(ansi_state, term_width, term_height, &term_callbacks); ansi_state->mouse_on = old_mouse_state; /* Redraw the window */ draw_fill(ctx, rgba(0,0,0, TERM_DEFAULT_OPAC)); render_decors(); term_redraw_all(); display_flip(); /* Send window size change ioctl */ struct winsize w; w.ws_row = term_height; w.ws_col = term_width; w.ws_xpixel = term_width * char_width; w.ws_ypixel = term_height * char_height; ioctl(fd_master, TIOCSWINSZ, &w); /* If requested, send a signal to the application. */ if (send_sig) { kill(child_pid, SIGWINCH); } } static void update_bounds(void) { struct decor_bounds bounds; decor_get_bounds(window, &bounds); decor_left_width = bounds.left_width; decor_top_height = bounds.top_height; decor_right_width = bounds.right_width; decor_bottom_height = bounds.bottom_height; decor_width = bounds.width; decor_height = bounds.height; } /* Handle window resize event. */ static void resize_finish(int width, int height) { static int resize_attempts = 0; int extra_x = 0; int extra_y = 0; /* Calculate window size */ if (!_no_frame) { update_bounds(); extra_x = decor_width; extra_y = decor_height + menu_bar_height; } int t_window_width = width - extra_x; int t_window_height = height - extra_y; /* Prevent the terminal from becoming too small. */ if (t_window_width < char_width * 20 || t_window_height < char_height * 10) { resize_attempts++; int n_width = extra_x + max(char_width * 20, t_window_width); int n_height = extra_y + max(char_height * 10, t_window_height); yutani_window_resize_offer(yctx, window, n_width, n_height); return; } /* If requested, ensure the terminal resizes to a fixed size based on the cell size. */ if (!_free_size && ((t_window_width % char_width != 0 || t_window_height % char_height != 0) && resize_attempts < 3)) { resize_attempts++; int n_width = extra_x + t_window_width - (t_window_width % char_width); int n_height = extra_y + t_window_height - (t_window_height % char_height); yutani_window_resize_offer(yctx, window, n_width, n_height); return; } resize_attempts = 0; /* Accept new window size */ yutani_window_resize_accept(yctx, window, width, height); window_width = window->width - extra_x; window_height = window->height - extra_y; /* Reinitialize the graphics library */ reinit_graphics_yutani(ctx, window); /* Reinitialize the terminal buffer and ANSI library */ reinit(1); /* We are done resizing. */ yutani_window_resize_done(yctx, window); yutani_flip(yctx, window); } /* Insert a mouse event sequence into the PTY */ static void mouse_event(int button, int x, int y) { char buf[7]; sprintf(buf, "\033[M%c%c%c", button + 32, x + 33, y + 33); handle_input_s(buf); } /* Handle Yutani messages */ static void * handle_incoming(void) { yutani_msg_t * m = yutani_poll(yctx); while (m) { if (menu_process_event(yctx, m)) { render_decors(); } switch (m->type) { case YUTANI_MSG_KEY_EVENT: { struct yutani_msg_key_event * ke = (void*)m->data; int ret = (ke->event.action == KEY_ACTION_DOWN) && (ke->event.key); key_event(ret, &ke->event); } break; case YUTANI_MSG_WINDOW_FOCUS_CHANGE: { struct yutani_msg_window_focus_change * wf = (void*)m->data; yutani_window_t * win = hashmap_get(yctx->windows, (void*)wf->wid); if (win == window) { win->focused = wf->focused; render_decors(); } } break; case YUTANI_MSG_WINDOW_CLOSE: { struct yutani_msg_window_close * wc = (void*)m->data; if (wc->wid == window->wid) { kill(child_pid, SIGKILL); exit_application = 1; } } break; case YUTANI_MSG_SESSION_END: { kill(child_pid, SIGKILL); exit_application = 1; } break; case YUTANI_MSG_RESIZE_OFFER: { struct yutani_msg_window_resize * wr = (void*)m->data; resize_finish(wr->width, wr->height); } break; case YUTANI_MSG_CLIPBOARD: { struct yutani_msg_clipboard * cb = (void *)m->data; if (selection_text) { free(selection_text); } if (*cb->content == '\002') { int size = atoi(&cb->content[2]); FILE * clipboard = yutani_open_clipboard(yctx); selection_text = malloc(size + 1); fread(selection_text, 1, size, clipboard); selection_text[size] = '\0'; fclose(clipboard); } else { selection_text = malloc(cb->size+1); memcpy(selection_text, cb->content, cb->size); selection_text[cb->size] = '\0'; } handle_input_s(selection_text); } break; case YUTANI_MSG_WINDOW_MOUSE_EVENT: { struct yutani_msg_window_mouse_event * me = (void*)m->data; if (me->wid != window->wid) break; if (!_no_frame) { int decor_response = decor_handle_event(yctx, m); switch (decor_response) { case DECOR_CLOSE: kill(child_pid, SIGKILL); exit_application = 1; break; case DECOR_RIGHT: /* right click in decoration, show appropriate menu */ decor_show_default_menu(window, window->x + me->new_x, window->y + me->new_y); break; default: break; } menu_bar_mouse_event(yctx, window, &terminal_menu_bar, me, me->new_x, me->new_y); } if (me->new_x < 0 || me->new_y < 0) break; if (!_no_frame) { if (me->new_x >= (int)window_width + (int)decor_width) break; if (me->new_y >= (int)window_height + (int)decor_height) break; if (me->new_y < (int)decor_top_height+menu_bar_height) break; if (me->new_y >= (int)(window_height + decor_top_height+menu_bar_height)) break; if (me->new_x < (int)decor_left_width) break; if (me->new_y >= (int)(window_width + decor_left_width)) break; } else { if (me->new_x >= (int)window_width) break; if (me->new_y >= (int)window_height) break; } int new_x = me->new_x; int new_y = me->new_y; if (!_no_frame) { new_x -= decor_left_width; new_y -= decor_top_height+menu_bar_height; } /* Convert from coordinate to cell positon */ new_x /= char_width; new_y /= char_height; if (new_x < 0 || new_y < 0) break; if (new_x > term_width || new_y > term_height) break; /* Map Cursor Action */ if (ansi_state->mouse_on) { if (me->buttons & YUTANI_MOUSE_SCROLL_UP) { mouse_event(32+32, new_x, new_y); } else if (me->buttons & YUTANI_MOUSE_SCROLL_DOWN) { mouse_event(32+32+1, new_x, new_y); } if (me->buttons != button_state) { /* Figure out what changed */ if (me->buttons & YUTANI_MOUSE_BUTTON_LEFT && !(button_state & YUTANI_MOUSE_BUTTON_LEFT)) mouse_event(0, new_x, new_y); if (me->buttons & YUTANI_MOUSE_BUTTON_MIDDLE && !(button_state & YUTANI_MOUSE_BUTTON_MIDDLE)) mouse_event(1, new_x, new_y); if (me->buttons & YUTANI_MOUSE_BUTTON_RIGHT && !(button_state & YUTANI_MOUSE_BUTTON_RIGHT)) mouse_event(2, new_x, new_y); if (!(me->buttons & YUTANI_MOUSE_BUTTON_LEFT) && button_state & YUTANI_MOUSE_BUTTON_LEFT) mouse_event(3, new_x, new_y); if (!(me->buttons & YUTANI_MOUSE_BUTTON_MIDDLE) && button_state & YUTANI_MOUSE_BUTTON_MIDDLE) mouse_event(3, new_x, new_y); if (!(me->buttons & YUTANI_MOUSE_BUTTON_RIGHT) && button_state & YUTANI_MOUSE_BUTTON_RIGHT) mouse_event(3, new_x, new_y); last_mouse_x = new_x; last_mouse_y = new_y; button_state = me->buttons; } else if (ansi_state->mouse_on == 2) { /* Report motion for pressed buttons */ if (last_mouse_x == new_x && last_mouse_y == new_y) break; if (button_state & YUTANI_MOUSE_BUTTON_LEFT) mouse_event(32, new_x, new_y); if (button_state & YUTANI_MOUSE_BUTTON_MIDDLE) mouse_event(33, new_x, new_y); if (button_state & YUTANI_MOUSE_BUTTON_RIGHT) mouse_event(34, new_x, new_y); last_mouse_x = new_x; last_mouse_y = new_y; } } else { if (me->command == YUTANI_MOUSE_EVENT_DOWN && me->buttons & YUTANI_MOUSE_BUTTON_LEFT) { term_redraw_all(); selection_start_x = new_x; selection_start_y = new_y; selection_end_x = new_x; selection_end_y = new_y; selection = 1; redraw_selection(); display_flip(); } if (me->command == YUTANI_MOUSE_EVENT_DRAG && me->buttons & YUTANI_MOUSE_BUTTON_LEFT ){ unredraw_selection(); selection_end_x = new_x; selection_end_y = new_y; redraw_selection(); display_flip(); } if (me->command == YUTANI_MOUSE_EVENT_RAISE) { if (me->new_x == me->old_x && me->new_y == me->old_y) { selection = 0; term_redraw_all(); display_flip(); } /* else selection */ } if (me->buttons & YUTANI_MOUSE_SCROLL_UP) { scroll_up(5); } else if (me->buttons & YUTANI_MOUSE_SCROLL_DOWN) { scroll_down(5); } else if (me->buttons & YUTANI_MOUSE_BUTTON_RIGHT) { if (!menu_right_click->window) { menu_show(menu_right_click, yctx); if (window->x + me->new_x + menu_right_click->window->width > yctx->display_width) { yutani_window_move(yctx, menu_right_click->window, window->x + me->new_x - menu_right_click->window->width, window->y + me->new_y); } else { yutani_window_move(yctx, menu_right_click->window, window->x + me->new_x, window->y + me->new_y); } } } } } break; default: break; } free(m); m = yutani_poll_async(yctx); } return NULL; } /* * Menu Actions */ /* File > Exit */ static void _menu_action_exit(struct MenuEntry * self) { kill(child_pid, SIGKILL); exit_application = 1; } /* We need to track these so we can retitle both of them */ static struct MenuEntry * _menu_toggle_borders_context = NULL; static struct MenuEntry * _menu_toggle_borders_bar = NULL; static void _menu_action_hide_borders(struct MenuEntry * self) { _no_frame = !(_no_frame); window_width = window->width - decor_width * (!_no_frame); window_height = window->height - (decor_height + menu_bar_height) * (!_no_frame); menu_update_title(_menu_toggle_borders_context, _no_frame ? "Show borders" : "Hide borders"); menu_update_title(_menu_toggle_borders_bar, _no_frame ? "Show borders" : "Hide borders"); reinit(1); } static void _menu_action_toggle_sdf(struct MenuEntry * self) { _use_sdf = !(_use_sdf); menu_update_title(self, _use_sdf ? "Bitmap font" : "Anti-aliased font"); reinit(1); } static void _menu_action_show_about(struct MenuEntry * self) { char about_cmd[1024] = "\0"; strcat(about_cmd, "about \"About Terminal\" /usr/share/icons/48/utilities-terminal.bmp \"ToaruOS Terminal\" \"(C) 2013-2018 K. Lange\n-\nPart of ToaruOS, which is free software\nreleased under the NCSA/University of Illinois\nlicense.\n-\n%https://toaruos.org\n%https://gitlab.com/toaruos\" "); char coords[100]; sprintf(coords, "%d %d &", (int)window->x + (int)window->width / 2, (int)window->y + (int)window->height / 2); strcat(about_cmd, coords); system(about_cmd); render_decors(); } static void _menu_action_show_help(struct MenuEntry * self) { system("help-browser terminal.trt &"); render_decors(); } static void _menu_action_copy(struct MenuEntry * self) { copy_selection(); } static void _menu_action_paste(struct MenuEntry * self) { yutani_special_request(yctx, NULL, YUTANI_SPECIAL_REQUEST_CLIPBOARD); } static void _menu_action_set_scale(struct MenuEntry * self) { struct MenuEntry_Normal * _self = (struct MenuEntry_Normal *)self; if (!_self->action) { scale_fonts = 0; font_scaling = 1.0; } else { scale_fonts = 1; font_scaling = atof(_self->action); } reinit(1); } int main(int argc, char ** argv) { window_width = char_width * 80; window_height = char_height * 24; static struct option long_opts[] = { {"fullscreen", no_argument, 0, 'F'}, {"bitmap", no_argument, 0, 'b'}, {"scale", required_argument, 0, 's'}, {"help", no_argument, 0, 'h'}, {"grid", no_argument, 0, 'x'}, {"no-frame", no_argument, 0, 'n'}, {"geometry", required_argument, 0, 'g'}, {0,0,0,0} }; /* Read some arguments */ int index, c; while ((c = getopt_long(argc, argv, "bhxnFls:g:", long_opts, &index)) != -1) { if (!c) { if (long_opts[index].flag == 0) { c = long_opts[index].val; } } switch (c) { case 'x': _free_size = 0; break; case 'n': _no_frame = 1; break; case 'F': _fullscreen = 1; _no_frame = 1; break; case 'b': _use_sdf = 0; break; case 'h': usage(argv); return 0; break; case 's': scale_fonts = 1; font_scaling = atof(optarg); break; case 'g': { char * c = strstr(optarg, "x"); if (c) { *c = '\0'; c++; window_width = atoi(optarg); window_height = atoi(c); } } break; case '?': break; default: break; } } /* Initialize the windowing library */ yctx = yutani_init(); if (!yctx) { fprintf(stderr, "%s: failed to connect to compositor\n", argv[0]); return 1; } /* Full screen mode forces window size to be that the display server */ if (_fullscreen) { window_width = yctx->display_width; window_height = yctx->display_height; } if (_no_frame) { window = yutani_window_create(yctx, window_width, window_height); } else { init_decorations(); struct decor_bounds bounds; decor_get_bounds(NULL, &bounds); window = yutani_window_create(yctx, window_width + bounds.width, window_height + bounds.height + menu_bar_height); update_bounds(); } if (_fullscreen) { /* If fullscreen, assume we're always focused and put us on the bottom. */ yutani_set_stack(yctx, window, YUTANI_ZORDER_BOTTOM); window->focused = 1; } else { window->focused = 0; } /* Set up menus */ terminal_menu_bar.entries = terminal_menu_entries; terminal_menu_bar.redraw_callback = render_decors; struct MenuEntry * _menu_exit = menu_create_normal("exit","exit","Exit", _menu_action_exit); struct MenuEntry * _menu_copy = menu_create_normal(NULL, NULL, "Copy", _menu_action_copy); struct MenuEntry * _menu_paste = menu_create_normal(NULL, NULL, "Paste", _menu_action_paste); menu_right_click = menu_create(); menu_insert(menu_right_click, _menu_copy); menu_insert(menu_right_click, _menu_paste); menu_insert(menu_right_click, menu_create_separator()); _menu_toggle_borders_context = menu_create_normal(NULL, NULL, _no_frame ? "Show borders" : "Hide borders", _menu_action_hide_borders); menu_insert(menu_right_click, _menu_toggle_borders_context); menu_insert(menu_right_click, menu_create_separator()); menu_insert(menu_right_click, _menu_exit); /* Menu Bar menus */ terminal_menu_bar.set = menu_set_create(); struct MenuList * m; m = menu_create(); /* File */ menu_insert(m, _menu_exit); menu_set_insert(terminal_menu_bar.set, "file", m); m = menu_create(); menu_insert(m, _menu_copy); menu_insert(m, _menu_paste); menu_set_insert(terminal_menu_bar.set, "edit", m); m = menu_create(); menu_insert(m, menu_create_normal(NULL, "0.75", "75%", _menu_action_set_scale)); menu_insert(m, menu_create_normal(NULL, NULL, "100%", _menu_action_set_scale)); menu_insert(m, menu_create_normal(NULL, "1.5", "150%", _menu_action_set_scale)); menu_insert(m, menu_create_normal(NULL, "2.0", "200%", _menu_action_set_scale)); menu_set_insert(terminal_menu_bar.set, "zoom", m); m = menu_create(); _menu_toggle_borders_bar = menu_create_normal(NULL, NULL, _no_frame ? "Show borders" : "Hide borders", _menu_action_hide_borders); menu_insert(m, _menu_toggle_borders_bar); menu_insert(m, menu_create_submenu(NULL,"zoom","Set zoom...")); menu_insert(m, menu_create_normal(NULL, NULL, _use_sdf ? "Bitmap font" : "Anti-aliased font", _menu_action_toggle_sdf)); menu_set_insert(terminal_menu_bar.set, "view", m); m = menu_create(); menu_insert(m, menu_create_normal("help","help","Contents", _menu_action_show_help)); menu_insert(m, menu_create_separator()); menu_insert(m, menu_create_normal("star","star","About Terminal", _menu_action_show_about)); menu_set_insert(terminal_menu_bar.set, "help", m); scrollback_list = list_create(); images_list = list_create(); /* Initialize the graphics context */ ctx = init_graphics_yutani_double_buffer(window); /* Clear to black */ draw_fill(ctx, rgba(0,0,0,0)); /* Move window to screen center (XXX maybe remove this and do better window placement elsewhere */ yutani_window_move(yctx, window, yctx->display_width / 2 - window->width / 2, yctx->display_height / 2 - window->height / 2); /* Open a PTY */ openpty(&fd_master, &fd_slave, NULL, NULL, NULL); terminal = fdopen(fd_slave, "w"); /* Initialize the terminal buffer and ANSI library for the first time. */ reinit(0); /* Make sure we're not passing anything to stdin on the child */ fflush(stdin); /* Fork off child */ child_pid = fork(); if (!child_pid) { /* Prepare stdin/out/err */ dup2(fd_slave, 0); dup2(fd_slave, 1); dup2(fd_slave, 2); /* Set the TERM environment variable. */ putenv("TERM=toaru"); /* Execute requested initial process */ if (argv[optind] != NULL) { /* Run something specified by the terminal startup */ char * tokens[] = {argv[optind], NULL}; execvp(tokens[0], tokens); fprintf(stderr, "Failed to launch requested startup application.\n"); } else { /* Run the user's shell */ char * shell = getenv("SHELL"); if (!shell) shell = "/bin/sh"; /* fallback */ char * tokens[] = {shell,NULL}; execvp(tokens[0], tokens); exit(1); } /* Failed to start */ exit_application = 1; return 1; } else { /* Set up fswait to check Yutani and the PTY master */ int fds[2] = {fileno(yctx->sock), fd_master}; /* PTY read buffer */ unsigned char buf[1024]; while (!exit_application) { /* Wait for something to happen. */ int index = fswait2(2,fds,200); /* Check if the child application has closed. */ check_for_exit(); if (index == 1) { /* Read from PTY */ maybe_flip_cursor(); int r = read(fd_master, buf, 1024); for (int i = 0; i < r; ++i) { ansi_put(ansi_state, buf[i]); } display_flip(); } else if (index == 0) { /* Handle Yutani events. */ maybe_flip_cursor(); handle_incoming(); } else if (index == 2) { /* Timeout, flip the cursor. */ maybe_flip_cursor(); } } } /* Windows will close automatically on exit. */ return 0; }