Skip to content

Instantly share code, notes, and snippets.

@shimarin
Last active December 13, 2024 23:43
Show Gist options
  • Save shimarin/71ace40e7443ed46387a477abf12ea70 to your computer and use it in GitHub Desktop.
Save shimarin/71ace40e7443ed46387a477abf12ea70 to your computer and use it in GitHub Desktop.
SDL2 terminal emulator using libvterm
/*
MIT License
Copyright (c) Tomoatsu Shimada 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
// g++ -o vtermtest vtermtest.cpp -lvterm -lutil -lSDL2 -lSDL2_ttf -licuuc
#include <termios.h>
#include <pty.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <iostream>
#include <vector>
#include <vterm.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <unicode/unistr.h>
#include <unicode/normlzr.h>
template <typename T> class Matrix {
T* buf;
int rows, cols;
public:
Matrix(int _rows, int _cols) : rows(_rows), cols(_cols) {
buf = new T[cols * rows];
}
~Matrix() {
delete buf;
}
void fill(const T& by) {
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
buf[cols * row + col] = by;
}
}
}
T& operator()(int row, int col) {
if (row < 0 || col < 0 || row >= rows || col >= cols) throw std::runtime_error("invalid position");
//else
return buf[cols * row + col];
}
int getRows() const { return rows; }
int getCols() const { return cols; }
};
class Terminal {
VTerm* vterm;
VTermScreen* screen;
SDL_Surface* surface = NULL;
SDL_Texture* texture = NULL;
Matrix<unsigned char> matrix;
TTF_Font* font;
int font_width;
int font_height;
int fd;
bool ringing = false;
const VTermScreenCallbacks screen_callbacks = {
damage,
moverect,
movecursor,
settermprop,
bell,
resize,
sb_pushline,
sb_popline
};
VTermPos cursor_pos;
public:
Terminal(int _fd, int _rows, int _cols, TTF_Font* _font) : fd(_fd), matrix(_rows, _cols), font(_font), font_height(TTF_FontHeight(font)) {
vterm = vterm_new(_rows,_cols);
vterm_set_utf8(vterm, 1);
vterm_output_set_callback(vterm, output_callback, (void*)&fd);
screen = vterm_obtain_screen(vterm);
vterm_screen_set_callbacks(screen, &screen_callbacks, this);
vterm_screen_reset(screen, 1);
matrix.fill(0);
TTF_SizeUTF8(font, "X", &font_width, NULL);
surface = SDL_CreateRGBSurfaceWithFormat(0, font_width * _cols, font_height * _rows, 32, SDL_PIXELFORMAT_RGBA32);
SDL_CreateRGBSurface(0, font_width, font_height, 32, 0, 0, 0, 0);
//SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_BLEND);
}
~Terminal() {
vterm_free(vterm);
invalidateTexture();
SDL_FreeSurface(surface);
}
void invalidateTexture() {
if (texture) {
SDL_DestroyTexture(texture);
texture = NULL;
}
}
void keyboard_unichar(char c, VTermModifier mod) {
vterm_keyboard_unichar(vterm, c, mod);
}
void keyboard_key(VTermKey key, VTermModifier mod) {
vterm_keyboard_key(vterm, key, mod);
}
void input_write(const char* bytes, size_t len) {
vterm_input_write(vterm, bytes, len);
}
int damage(int start_row, int start_col, int end_row, int end_col) {
invalidateTexture();
for (int row = start_row; row < end_row; row++) {
for (int col = start_col; col < end_col; col++) {
matrix(row, col) = 1;
}
}
return 0;
}
int moverect(VTermRect dest, VTermRect src) {
return 0;
}
int movecursor(VTermPos pos, VTermPos oldpos, int visible) {
cursor_pos = pos;
return 0;
}
int settermprop(VTermProp prop, VTermValue *val) {
return 0;
}
int bell() {
ringing = true;
return 0;
}
int resize(int rows, int cols) {
return 0;
}
int sb_pushline(int cols, const VTermScreenCell *cells) {
return 0;
}
int sb_popline(int cols, VTermScreenCell *cells) {
return 0;
}
void render(SDL_Renderer* renderer, const SDL_Rect& window_rect) {
if (!texture) {
for (int row = 0; row < matrix.getRows(); row++) {
for (int col = 0; col < matrix.getCols(); col++) {
if (matrix(row, col)) {
VTermPos pos = { row, col };
VTermScreenCell cell;
vterm_screen_get_cell(screen, pos, &cell);
if (cell.chars[0] == 0xffffffff) continue;
icu::UnicodeString ustr;
for (int i = 0; cell.chars[i] != 0 && i < VTERM_MAX_CHARS_PER_CELL; i++) {
ustr.append((UChar32)cell.chars[i]);
}
SDL_Color color = (SDL_Color){128,128,128};
SDL_Color bgcolor = (SDL_Color){0,0,0};
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};
}
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};
}
if (cell.attrs.reverse) std::swap(color, bgcolor);
int 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;
if (cell.attrs.blink) { /*TBD*/ }
SDL_Rect rect = { col * font_width, row * font_height, font_width * cell.width, font_height };
SDL_FillRect(surface, &rect, SDL_MapRGB(surface->format, bgcolor.r, bgcolor.g, bgcolor.b));
if (ustr.length() > 0) {
UErrorCode status = U_ZERO_ERROR;
auto normalizer = icu::Normalizer2::getNFKCInstance(status);
if (U_FAILURE(status)) throw std::runtime_error("unable to get NFKC normalizer");
auto ustr_normalized = normalizer->normalize(ustr, status);
std::string utf8;
if (U_SUCCESS(status)) {
ustr_normalized.toUTF8String(utf8);
} else {
ustr.toUTF8String(utf8);
}
TTF_SetFontStyle(font, style);
auto text_surface = TTF_RenderUTF8_Blended(font, utf8.c_str(), color);
SDL_SetSurfaceBlendMode(text_surface, SDL_BLENDMODE_BLEND);
SDL_BlitSurface(text_surface, NULL, surface, &rect);
SDL_FreeSurface(text_surface);
}
matrix(row, col) = 0;
}
}
}
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 };
// scale cursor
rect.x = window_rect.x + rect.x * window_rect.w / surface->w;
rect.y = window_rect.y + rect.y * window_rect.h / surface->h;
rect.w = rect.w * window_rect.w / surface->w;
rect.w *= cell.width;
rect.h = rect.h * window_rect.h / surface->h;
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 = 0;
}
}
void processEvent(const 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;
for (int i = 0; i < strlen(ev.text.text); i++) {
keyboard_unichar(ev.text.text[i], (VTermModifier)mod);
}
} else if (ev.type == SDL_KEYDOWN) {
switch (ev.key.keysym.sym) {
case SDLK_RETURN:
case SDLK_KP_ENTER:
keyboard_key(VTERM_KEY_ENTER, VTERM_MOD_NONE);
break;
case SDLK_BACKSPACE:
keyboard_key(VTERM_KEY_BACKSPACE, VTERM_MOD_NONE);
break;
case SDLK_ESCAPE:
keyboard_key(VTERM_KEY_ESCAPE, VTERM_MOD_NONE);
break;
case SDLK_TAB:
keyboard_key(VTERM_KEY_TAB, VTERM_MOD_NONE);
break;
case SDLK_UP:
keyboard_key(VTERM_KEY_UP, VTERM_MOD_NONE);
break;
case SDLK_DOWN:
keyboard_key(VTERM_KEY_DOWN, VTERM_MOD_NONE);
break;
case SDLK_LEFT:
keyboard_key(VTERM_KEY_LEFT, VTERM_MOD_NONE);
break;
case SDLK_RIGHT:
keyboard_key(VTERM_KEY_RIGHT, VTERM_MOD_NONE);
break;
case SDLK_PAGEUP:
keyboard_key(VTERM_KEY_PAGEUP, VTERM_MOD_NONE);
break;
case SDLK_PAGEDOWN:
keyboard_key(VTERM_KEY_PAGEDOWN, VTERM_MOD_NONE);
break;
case SDLK_HOME:
keyboard_key(VTERM_KEY_HOME, VTERM_MOD_NONE);
break;
case SDLK_END:
keyboard_key(VTERM_KEY_END, VTERM_MOD_NONE);
break;
default:
if (ev.key.keysym.mod & KMOD_CTRL && ev.key.keysym.sym < 127) {
//std::cout << ev.key.keysym.sym << std::endl;
keyboard_unichar(ev.key.keysym.sym, VTERM_MOD_CTRL);
}
break;
}
}
}
void processInput() {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeval timeout = { 0, 0 };
if (select(fd + 1, &readfds, NULL, NULL, &timeout) > 0) {
char buf[4096];
auto size = read(fd, buf, sizeof(buf));
if (size > 0) {
input_write(buf, size);
}
}
}
static void output_callback(const char* s, size_t len, void* user);
static int damage(VTermRect rect, void *user);
static int moverect(VTermRect dest, VTermRect src, void *user);
static int movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user);
static int settermprop(VTermProp prop, VTermValue *val, void *user);
static int bell(void *user);
static int resize(int rows, int cols, void *user);
static int sb_pushline(int cols, const VTermScreenCell *cells, void *user);
static int sb_popline(int cols, VTermScreenCell *cells, void *user);
};
void Terminal::output_callback(const char* s, size_t len, void* user)
{
write(*(int*)user, s, len);
}
int Terminal::damage(VTermRect rect, void *user)
{
return ((Terminal*)user)->damage(rect.start_row, rect.start_col, rect.end_row, rect.end_col);
}
int Terminal::moverect(VTermRect dest, VTermRect src, void *user)
{
return ((Terminal*)user)->moverect(dest, src);
}
int Terminal::movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
{
return ((Terminal*)user)->movecursor(pos, oldpos, visible);
}
int Terminal::settermprop(VTermProp prop, VTermValue *val, void *user)
{
return ((Terminal*)user)->settermprop(prop, val);
}
int Terminal::bell(void *user)
{
return ((Terminal*)user)->bell();
}
int Terminal::resize(int rows, int cols, void *user)
{
return ((Terminal*)user)->resize(rows, cols);
}
int Terminal::sb_pushline(int cols, const VTermScreenCell *cells, void *user)
{
return ((Terminal*)user)->sb_pushline(cols, cells);
}
int Terminal::sb_popline(int cols, VTermScreenCell *cells, void *user)
{
return ((Terminal*)user)->sb_popline(cols, cells);
}
std::pair<int, int> createSubprocessWithPty(int rows, int cols, const char* prog, const std::vector<std::string>& args = {}, const char* TERM = "xterm-256color")
{
int fd;
struct winsize win = { (unsigned short)rows, (unsigned short)cols, 0, 0 };
auto pid = forkpty(&fd, NULL, NULL, &win);
if (pid < 0) throw std::runtime_error("forkpty failed");
//else
if (!pid) {
setenv("TERM", TERM, 1);
char ** argv = new char *[args.size() + 2];
argv[0] = strdup(prog);
for (int i = 1; i <= args.size(); i++) {
argv[i] = strdup(args[i - 1].c_str());
}
argv[args.size() + 1] = NULL;
if (execvp(prog, argv) < 0) exit(-1);
}
//else
return { pid, fd };
}
std::pair<pid_t,int> waitpid(pid_t pid, int options)
{
int status;
auto done_pid = waitpid(pid, &status, options);
return {done_pid, status};
}
int main()
{
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << SDL_GetError() << std::endl;
return 1;
}
if (TTF_Init() < 0) {
std::cerr << "TTF_Init: " << TTF_GetError() << std::endl;
return 1;
}
TTF_Font* font = TTF_OpenFont("/usr/share/fonts/vlgothic/VL-Gothic-Regular.ttf", 48);
//TTF_Font* font = TTF_OpenFont("RictyDiminished-Regular.ttf", 48);
if (font == NULL) {
std::cerr << "TTF_OpenFont: " << TTF_GetError() << std::endl;
return 1;
}
SDL_ShowCursor(SDL_DISABLE);
SDL_Window* window = SDL_CreateWindow("term",SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,1024,768,SDL_WINDOW_SHOWN);
if (window == NULL) {
std::cerr << "SDL_CreateWindow: " << SDL_GetError() << std::endl;
return 1;
}
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC);
if (renderer == NULL) {
std::cerr << "SDL_CreateRenderer: " << SDL_GetError() << std::endl;
return 1;
}
const int rows = 32, cols = 100;
auto subprocess = createSubprocessWithPty(rows, cols, getenv("SHELL"), {"-"});
auto pid = subprocess.first;
Terminal terminal(subprocess.second/*fd*/, rows, cols, font);
std::pair<pid_t, int> rst;
while ((rst = waitpid(pid, WNOHANG)).first != 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 {
terminal.processEvent(ev);
}
}
terminal.processInput();
SDL_Rect rect = { 0, 0, 1024, 768 };
terminal.render(renderer, rect);
SDL_RenderPresent(renderer);
}
std::cout << "Process exit status: " << rst.second << std::endl;
TTF_Quit();
SDL_Quit();
return 0;
}
@ThePerfectComputer
Copy link

What is the license for this? I'd like to reference it for something I'm building.

@shimarin
Copy link
Author

@ThePerfectComputer License terms added.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment