toaruos/apps/terminal.c

2255 lines
65 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <getopt.h>
#include <errno.h>
#include <pty.h>
#include <wchar.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/fswait.h>
#define TRACE_APP_NAME "terminal"
#include <toaru/trace.h>
#include <toaru/decodeutf8.h>
#include <toaru/yutani.h>
#include <toaru/decorations.h>
#include <toaru/graphics.h>
#include <toaru/kbd.h>
#include <toaru/termemu.h>
#include <toaru/spinlock.h>
#include <toaru/list.h>
#include <toaru/menu.h>
#include <toaru/sdf.h>
/* 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;
}