-
-
Save min4builder/0a044cf8b1d5d60d8fcce10ee24993fe to your computer and use it in GitHub Desktop.
SDL2 terminal emulator using libvterm (C99 version)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* gcc vtermtest.c -std=c99 `pkg-config --cflags --libs vterm sdl2 SDL2_ttf` | |
warnings: -Wall -Wextra -Wno-parentheses -Wno-unused-parameter -Wpedantic | |
originally based on: https://gist.github.com/shimarin/71ace40e7443ed46387a477abf12ea70 | |
TODO: | |
- fix resize rendering | |
- fix vis rendering | |
- command line parsing (-e) | |
- color scheme | |
*/ | |
#define _POSIX_C_SOURCE 200112L | |
#include <pty.h> | |
#include <signal.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <sys/select.h> | |
#include <sys/wait.h> | |
#include <termios.h> | |
#include <unistd.h> | |
#include "SDL.h" | |
#include "SDL_ttf.h" | |
#include "vterm.h" | |
static int fd = -1; | |
static VTerm *vterm; | |
static VTermScreen *screen; | |
static SDL_Surface *surface; | |
static SDL_Texture *texture; | |
static bool *changed; | |
static char *row_string; | |
static TTF_Font *font; | |
static int font_width, font_height; | |
static int rows, cols; | |
static bool ringing; | |
static VTermPos cursor_pos; | |
static void | |
output_callback(char const *s, size_t len, void *user) | |
{ | |
if (write(fd, s, len) < 0) | |
perror("write failed"); | |
} | |
static int | |
damage(VTermRect rect, void *user) | |
{ | |
if (texture) { | |
SDL_DestroyTexture(texture); | |
texture = NULL; | |
} | |
for (int row = rect.start_row; row < rect.end_row; row++) | |
changed[row] = true; | |
return 0; | |
} | |
static int | |
movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) | |
{ | |
cursor_pos = pos; | |
return 0; | |
} | |
static int | |
bell(void *user) | |
{ | |
ringing = true; | |
return 0; | |
} | |
static int | |
resize(int newrows, int newcols, void *user) | |
{ | |
rows = newrows; | |
cols = newcols; | |
struct winsize ws = { (short)rows, (short)cols, (short)(cols * font_width), | |
(short)(rows * font_height) }; | |
ioctl(fd, TIOCSWINSZ, &ws); | |
SDL_FreeSurface(surface); | |
surface = SDL_CreateRGBSurfaceWithFormat(0, font_width * cols, font_height * rows, | |
32, SDL_PIXELFORMAT_RGBA32); | |
changed = calloc(rows, sizeof(*changed)); | |
for (int row = 0; row < rows; row++) | |
changed[row] = true; | |
row_string = calloc(cols * 4 * VTERM_MAX_CHARS_PER_CELL, sizeof(*row_string)); | |
return 0; | |
} | |
static VTermScreenCallbacks const screen_callbacks = { | |
.damage = damage, | |
.movecursor = movecursor, | |
.bell = bell, | |
.resize = resize, | |
}; | |
/* Encode a code point using UTF-8 | |
author: Ondřej Hruška <ondra@ondrovo.com> | |
license: MIT */ | |
static int | |
utf8_encode(char *out, uint32_t utf) | |
{ | |
if (utf <= 0x7F) { | |
out[0] = utf & 0x7F; | |
out[1] = 0; | |
return 1; | |
} else if (utf <= 0x07FF) { | |
out[0] = utf >> 6 & 0x1F | 0xC0; | |
out[1] = utf & 0x3F | 0x80; | |
out[2] = 0; | |
return 2; | |
} else if (utf <= 0xFFFF) { | |
out[0] = utf >> 12 & 0x0F | 0xE0; | |
out[1] = utf >> 6 & 0x3F | 0x80; | |
out[2] = utf & 0x3F | 0x80; | |
out[3] = 0; | |
return 3; | |
} else if (utf <= 0x10FFFF) { | |
out[0] = utf >> 18 & 0x07 | 0xF0; | |
out[1] = utf >> 12 & 0x3F | 0x80; | |
out[2] = utf >> 6 & 0x3F | 0x80; | |
out[3] = utf & 0x3F | 0x80; | |
out[4] = 0; | |
return 4; | |
} else { | |
out[0] = 0; | |
return 0; | |
} | |
} | |
static void | |
get_style(VTermScreenCell cell, SDL_Color *color, SDL_Color *bgcolor, int *style) | |
{ | |
*color = (SDL_Color){ 200, 200, 200, 255 }; | |
*bgcolor = (SDL_Color){ 40, 40, 40, 255 }; | |
if (VTERM_COLOR_IS_INDEXED(&cell.fg)) { | |
vterm_screen_convert_color_to_rgb(screen, &cell.fg); | |
} | |
if (VTERM_COLOR_IS_RGB(&cell.fg)) { | |
*color = (SDL_Color){ cell.fg.rgb.red, cell.fg.rgb.green, | |
cell.fg.rgb.blue, 255 }; | |
} | |
if (VTERM_COLOR_IS_INDEXED(&cell.bg)) { | |
vterm_screen_convert_color_to_rgb(screen, &cell.bg); | |
} | |
if (VTERM_COLOR_IS_RGB(&cell.bg)) { | |
*bgcolor = (SDL_Color){ cell.bg.rgb.red, cell.bg.rgb.green, | |
cell.bg.rgb.blue, 255 }; | |
} | |
if (cell.attrs.reverse) { | |
SDL_Color temp = *color; | |
*color = *bgcolor; | |
*bgcolor = temp; | |
} | |
*style = TTF_STYLE_NORMAL; | |
if (cell.attrs.bold) | |
*style |= TTF_STYLE_BOLD; | |
if (cell.attrs.underline) | |
*style |= TTF_STYLE_UNDERLINE; | |
if (cell.attrs.italic) | |
*style |= TTF_STYLE_ITALIC; | |
if (cell.attrs.strike) | |
*style |= TTF_STYLE_STRIKETHROUGH; | |
} | |
static void | |
render_row(SDL_Renderer *renderer, int row) | |
{ | |
for (int start = 0; start < cols;) { | |
int width = 0; | |
VTermPos pos = { row, start }; | |
VTermScreenCell cell; | |
vterm_screen_get_cell(screen, pos, &cell); | |
SDL_Color color, bgcolor; | |
int style; | |
get_style(cell, &color, &bgcolor, &style); | |
char *str = row_string; | |
for (int col = start; col < cols; col++) { | |
VTermPos pos = { row, col }; | |
VTermScreenCell cell; | |
vterm_screen_get_cell(screen, pos, &cell); | |
SDL_Color cur_color, cur_bgcolor; | |
int cur_style; | |
get_style(cell, &cur_color, &cur_bgcolor, &cur_style); | |
if (cur_color.r != color.r || cur_color.g != color.g | |
|| cur_color.b != color.b || cur_bgcolor.r != bgcolor.r | |
|| cur_bgcolor.g != bgcolor.g || cur_bgcolor.b != bgcolor.b | |
|| cur_style != style) | |
break; | |
// if (cell.chars[0] == 0xffffffff) continue; | |
for (int i = 0; | |
cell.chars[i] != 0 && i < VTERM_MAX_CHARS_PER_CELL; i++) { | |
str += utf8_encode(str, cell.chars[i]); | |
} | |
width += cell.width; | |
} | |
SDL_Rect rect = { start * font_width, row * font_height, | |
width * font_width, font_height }; | |
if (str - row_string > 0) { | |
TTF_SetFontStyle(font, style); | |
SDL_Surface *text_surface = | |
TTF_RenderUTF8_Shaded(font, row_string, color, bgcolor); | |
SDL_BlitSurface(text_surface, NULL, surface, &rect); | |
SDL_FreeSurface(text_surface); | |
if (rect.w < width * font_width) { | |
rect.x += rect.w; | |
rect.w = width * font_width - rect.w; | |
SDL_FillRect(surface, &rect, | |
SDL_MapRGB(surface->format, bgcolor.r, | |
bgcolor.g, bgcolor.b)); | |
} | |
} else { | |
SDL_FillRect(surface, &rect, | |
SDL_MapRGB(surface->format, bgcolor.r, bgcolor.g, | |
bgcolor.b)); | |
} | |
start += width; | |
} | |
} | |
static void | |
render(SDL_Renderer *renderer, SDL_Rect window_rect) | |
{ | |
if (!texture) { | |
for (int row = 0; row < rows; row++) { | |
if (!changed[row]) | |
continue; | |
render_row(renderer, row); | |
changed[row] = false; | |
} | |
texture = SDL_CreateTextureFromSurface(renderer, surface); | |
// SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); | |
} | |
SDL_RenderCopy(renderer, texture, NULL, &window_rect); | |
// draw cursor | |
VTermScreenCell cell; | |
vterm_screen_get_cell(screen, cursor_pos, &cell); | |
SDL_Rect rect = { cursor_pos.col * font_width, cursor_pos.row * font_height, | |
font_width, font_height }; | |
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); | |
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 96); | |
SDL_RenderFillRect(renderer, &rect); | |
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); | |
SDL_RenderDrawRect(renderer, &rect); | |
if (ringing) { | |
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 192); | |
SDL_RenderFillRect(renderer, &window_rect); | |
ringing = false; | |
} | |
} | |
static void | |
process_event(SDL_Event ev) | |
{ | |
if (ev.type == SDL_TEXTINPUT) { | |
const Uint8 *state = SDL_GetKeyboardState(NULL); | |
int mod = VTERM_MOD_NONE; | |
if (state[SDL_SCANCODE_LCTRL] || state[SDL_SCANCODE_RCTRL]) | |
mod |= VTERM_MOD_CTRL; | |
if (state[SDL_SCANCODE_LALT] || state[SDL_SCANCODE_RALT]) | |
mod |= VTERM_MOD_ALT; | |
if (state[SDL_SCANCODE_LSHIFT] || state[SDL_SCANCODE_RSHIFT]) | |
mod |= VTERM_MOD_SHIFT; | |
size_t len = strlen(ev.text.text); | |
for (size_t i = 0; i < len; i++) { | |
vterm_keyboard_unichar(vterm, ev.text.text[i], | |
(VTermModifier)mod); | |
} | |
} else if (ev.type == SDL_KEYDOWN) { | |
switch (ev.key.keysym.sym) { | |
case SDLK_RETURN: | |
case SDLK_KP_ENTER: | |
vterm_keyboard_key(vterm, VTERM_KEY_ENTER, VTERM_MOD_NONE); | |
break; | |
case SDLK_BACKSPACE: | |
vterm_keyboard_key(vterm, VTERM_KEY_BACKSPACE, VTERM_MOD_NONE); | |
break; | |
case SDLK_ESCAPE: | |
vterm_keyboard_key(vterm, VTERM_KEY_ESCAPE, VTERM_MOD_NONE); | |
break; | |
case SDLK_TAB: | |
vterm_keyboard_key(vterm, VTERM_KEY_TAB, VTERM_MOD_NONE); | |
break; | |
case SDLK_UP: | |
vterm_keyboard_key(vterm, VTERM_KEY_UP, VTERM_MOD_NONE); | |
break; | |
case SDLK_DOWN: | |
vterm_keyboard_key(vterm, VTERM_KEY_DOWN, VTERM_MOD_NONE); | |
break; | |
case SDLK_LEFT: | |
vterm_keyboard_key(vterm, VTERM_KEY_LEFT, VTERM_MOD_NONE); | |
break; | |
case SDLK_RIGHT: | |
vterm_keyboard_key(vterm, VTERM_KEY_RIGHT, VTERM_MOD_NONE); | |
break; | |
case SDLK_PAGEUP: | |
vterm_keyboard_key(vterm, VTERM_KEY_PAGEUP, VTERM_MOD_NONE); | |
break; | |
case SDLK_PAGEDOWN: | |
vterm_keyboard_key(vterm, VTERM_KEY_PAGEDOWN, VTERM_MOD_NONE); | |
break; | |
case SDLK_HOME: | |
vterm_keyboard_key(vterm, VTERM_KEY_HOME, VTERM_MOD_NONE); | |
break; | |
case SDLK_END: | |
vterm_keyboard_key(vterm, VTERM_KEY_END, VTERM_MOD_NONE); | |
break; | |
default: | |
if (ev.key.keysym.mod & KMOD_CTRL && ev.key.keysym.sym < 127) | |
vterm_keyboard_unichar(vterm, ev.key.keysym.sym, | |
VTERM_MOD_CTRL); | |
break; | |
} | |
} | |
} | |
static void | |
process_input(void) | |
{ | |
fd_set readfds; | |
FD_ZERO(&readfds); | |
FD_SET(fd, &readfds); | |
struct timeval timeout = { 0, 0 }; | |
if (select(fd + 1, &readfds, NULL, NULL, &timeout) > 0) { | |
char buf[4096]; | |
ssize_t size = read(fd, buf, sizeof(buf)); | |
if (size > 0) | |
vterm_input_write(vterm, buf, size); | |
} | |
} | |
int | |
main() | |
{ | |
if (SDL_Init(SDL_INIT_VIDEO) < 0) { | |
fprintf(stderr, "%s\n", SDL_GetError()); | |
return 1; | |
} | |
if (TTF_Init() < 0) { | |
fprintf(stderr, "TTF_Init: %s\n", TTF_GetError()); | |
return 1; | |
} | |
font = TTF_OpenFont("/usr/share/fonts/truetype/firacode/FiraCode-Regular.ttf", | |
18); | |
if (font == NULL) { | |
fprintf(stderr, "TTF_OpenFont: %s\n", TTF_GetError()); | |
return 1; | |
} | |
SDL_ShowCursor(SDL_DISABLE); | |
SDL_Window *window = | |
SDL_CreateWindow("term", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, | |
1024, 768, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); | |
if (window == NULL) { | |
fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); | |
return 1; | |
} | |
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0); | |
if (renderer == NULL) { | |
fprintf(stderr, "SDL_CreateRenderer: %s\n", SDL_GetError()); | |
return 1; | |
} | |
TTF_SizeUTF8(font, "X", &font_width, NULL); | |
font_height = TTF_FontHeight(font); | |
int width, height; | |
SDL_GetRendererOutputSize(renderer, &width, &height); | |
resize(height / font_height, width / font_width, NULL); | |
vterm = vterm_new(rows, cols); | |
vterm_set_utf8(vterm, 1); | |
vterm_output_set_callback(vterm, output_callback, NULL); | |
screen = vterm_obtain_screen(vterm); | |
vterm_screen_set_callbacks(screen, &screen_callbacks, NULL); | |
vterm_screen_reset(screen, 1); | |
struct winsize ws = { (short)rows, (short)cols, (short)width, (short)height }; | |
pid_t pid = forkpty(&fd, NULL, NULL, &ws); | |
if (pid < 0) { | |
perror("forkpty"); | |
return 1; | |
} | |
if (!pid) { | |
setenv("TERM", "xterm-256color", 1); | |
char *prog = getenv("SHELL"); | |
char *argv[] = { prog, "-", NULL }; | |
execvp(prog, argv); | |
return 1; | |
} | |
int status; | |
while (waitpid(pid, &status, WNOHANG) != pid) { | |
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); | |
SDL_RenderClear(renderer); | |
SDL_Event ev; | |
while (SDL_PollEvent(&ev)) { | |
if (ev.type == SDL_QUIT | |
|| (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_ESCAPE | |
&& (ev.key.keysym.mod & KMOD_CTRL))) { | |
kill(pid, SIGTERM); | |
} else if (ev.type == SDL_WINDOWEVENT) { | |
switch (ev.window.event) { | |
case SDL_WINDOWEVENT_SIZE_CHANGED: | |
vterm_set_size(vterm, | |
ev.window.data2 / font_height, | |
ev.window.data1 / font_width); | |
break; | |
} | |
} else { | |
process_event(ev); | |
} | |
} | |
process_input(); | |
SDL_Rect rect = { 0, 0, font_width * cols, font_height * rows }; | |
render(renderer, rect); | |
SDL_RenderPresent(renderer); | |
} | |
vterm_free(vterm); | |
if (texture) | |
SDL_DestroyTexture(texture); | |
SDL_FreeSurface(surface); | |
TTF_Quit(); | |
SDL_Quit(); | |
return status; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment