/** * @author Christian Burger (christian@krikkel.de) */ #include "kNCurses/PtyWindow.hpp" #include "Debug.hpp" #include #include #include #include #include #include #include #include #include namespace krikkel::NCurses { using gsl::narrow; using std::scoped_lock; using std::recursive_mutex; using std::system_error; using std::system_category; using std::string; PtyWindow::PtyWindow(recursive_mutex *ncursesMutex, int lines, int columns, int y, int x) : Window(lines, columns, y, x), ncursesMutex(ncursesMutex) { // to get the original terminal we need to shutdown ncurses for a moment /// @todo maybe try `reset_prog_mode()` and `reset_shell_mode()` instead ::endwin(); tcgetattr(STDIN_FILENO, &terminalParameters); ::refresh(); winsize windowSize = { /*.ws_row =*/ narrow(this->height()) , /*.ws_col =*/ narrow(this->width()) , /*.y =*/ 0 , /*.x =*/ 0 }; openpty(&fdPtyHost, &fdPtyClient, NULL, &terminalParameters, &windowSize); pseudoTerminal = vterm_new(windowSize.ws_row, windowSize.ws_col); vterm_set_utf8(pseudoTerminal, true); pseudoTerminalScreen = vterm_obtain_screen(pseudoTerminal); vterm_screen_reset(pseudoTerminalScreen, true); vterm_screen_set_callbacks(pseudoTerminalScreen, &screenCallbacks, this); vterm_output_set_callback(pseudoTerminal, &staticHandlerOutput, this); vterm_screen_enable_altscreen(pseudoTerminalScreen, true); //raw(); // — cbreak might suffice //noecho(); // — already set by NCursesWindow keypad(true); /// — already set by NCursesWindow //meta(true); // — already set by NCursesWindow //nodelay(true); // — @todo would need some programming, not sure if useful nonl(); /// @todo block all signals, this thread does not handle any readPtyClientThread = std::thread(&PtyWindow::readFromPtyClientThreadMethod, this); } PtyWindow::~PtyWindow() { close(fdPtyHost); close(fdPtyClient); vterm_free(pseudoTerminal); } int PtyWindow::getFdPtyClient() const { return fdPtyClient; } void PtyWindow::writeToPtyClient(const char *output, size_t length) { size_t bytesWritten = write(fdPtyHost, output, length); /// @todo this is a bit too simple; partial write is possible if(bytesWritten != length) { string basicError("Could not write to PTY client (i. e. the shell)."); throw system_error(errno, system_category(), basicError); } //__debug_log("written to PTY client: '" + __debug_make_bytes_printable(std::string(output, length)) + '\''); } void PtyWindow::writeUnicodeCharToClient(wint_t character) { scoped_lock lock(ptyMutex); vterm_keyboard_unichar(pseudoTerminal, character, VTERM_MOD_NONE); } void PtyWindow::writeFunctionKeyToClient(VTermKey key) { scoped_lock lock(ptyMutex); //__debug_log("writing key: " + std::to_string(key)); vterm_keyboard_key(pseudoTerminal, key, VTERM_MOD_NONE); } void PtyWindow::readFromPtyClientThreadMethod() { /// @todo in theory, there is no need for a timeout or select … /// file descriptor is blocking while(1) { readFromPtyClient(); struct timeval timeout = { /*.tv_sec =*/ 0, /*.tv_usec =*/ 200000 }; fd_set readFds; FD_ZERO(&readFds); FD_SET(fdPtyHost, &readFds); if(select(fdPtyHost + 1, &readFds, NULL, NULL, &timeout) < 0) break; } } void PtyWindow::readFromPtyClient() { size_t bytesRead = read(fdPtyHost, ptyClientOutputBuffer, PTY_CLIENT_OUTPUT_BUFFER_SIZE); if(bytesRead != (size_t) -1 && bytesRead != 0) { scoped_lock lock(ptyMutex, *ncursesMutex); vterm_input_write(pseudoTerminal, ptyClientOutputBuffer, bytesRead); } //__debug_log("read from PTY client: '" + __debug_make_bytes_printable(std::string(ptyClientOutputBuffer, bytesRead)) + '\''); } VTermScreenCallbacks PtyWindow::screenCallbacks = { /*.damage =*/ staticHandlerDamage , /*.moverect =*/ staticHandlerMoveRect , /*.movecursor =*/ staticHandlerMoveCursor , /*.settermprop =*/ staticHandlerSetTermProp , /*.bell =*/ staticHandlerBell , /*.resize =*/ staticHandlerResize , /*.sb_pushline =*/ staticHandlerPushLine , /*.sb_popline =*/ staticHandlerPopLine }; int PtyWindow::handlerDamage(VTermRect rect) { scoped_lock lock(ptyMutex, *ncursesMutex); for(int x = rect.start_col; x < rect.end_col; ++x) for(int y = rect.start_row; y < rect.end_row; ++y) copyPtyCellToNCursesWindow(x, y); refresh(); return 0; } int PtyWindow::handlerMoveRect(VTermRect dest, VTermRect src) { (void) dest; (void) src; __debug_log("(unimplemented) move content in rectangle from (" + std::to_string(src.start_col) + ", " + std::to_string(src.start_row) + ", " + std::to_string(src.end_col) + ", " + std::to_string(src.end_row) + ") " + "size: " + std::to_string((src.start_col - src.end_col) * (src.start_row - src.end_row)) + " to (" + std::to_string(dest.start_col) + ", " + std::to_string(dest.start_row) + ", " + std::to_string(dest.end_col) + ", " + std::to_string(dest.end_row) + ") " + "size: " + std::to_string((dest.start_col - dest.end_col) * (dest.start_row - dest.end_row))); return 0; } int PtyWindow::handlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible) { (void) oldpos; (void) visible; scoped_lock lock(*ncursesMutex); cursorX = pos.col; cursorY = pos.row; refresh(); return 0; } int PtyWindow::handlerSetTermProp(VTermProp prop, VTermValue *val) { (void) prop; (void) val; /// @todo maybe use "vterm_get_prop_type() —> bool, number, string"? __debug_stringify_switch_begin(handlerSetTermProp, prop, val); __debug_stringify_switch_case(VTERM_PROP_CURSORVISIBLE); __debug_stringify_switch_case(VTERM_PROP_CURSORBLINK); __debug_stringify_switch_case(VTERM_PROP_ALTSCREEN); __debug_stringify_switch_last_case_bool(VTERM_PROP_REVERSE, val->boolean); __debug_stringify_switch_case(VTERM_PROP_TITLE); __debug_stringify_switch_last_case_string(VTERM_PROP_ICONNAME, val->string); __debug_stringify_switch_case(VTERM_PROP_CURSORSHAPE); __debug_stringify_switch_last_case_number(VTERM_PROP_MOUSE, val->number); __debug_stringify_switch_end(prop); __debug_log(std::string("unimplemented handler called: ") + __debug_stringify_get_string(handlerSetTermProp) ); return 0; } int PtyWindow::handlerBell() { beep(); return 0; } int PtyWindow::handlerResize(int rows, int cols) { (void) rows; (void) cols; __debug_log("unimplemented handler called"); return 0; } int PtyWindow::handlerPushLine(int cols, const VTermScreenCell *cells) { (void) cols; (void) cells; __debug_log("(unimplemented) push line with " + std::to_string(cols) + " columns"); return 0; } int PtyWindow::handlerPopLine(int cols, VTermScreenCell *cells) { (void) cols; (void) cells; __debug_log("unimplemented handler called"); return 0; } attr_t PtyWindow::extractAttributesFromVTermCell(VTermScreenCell vTermCell) { (void) vTermCell; attr_t result = A_NORMAL; //__debug_log("unimplemented method called"); return result; } short PtyWindow::extractColorFromVTermCell(VTermScreenCell vTermCell) { (void) vTermCell; //__debug_log("unimplemented method called"); return 0; } void PtyWindow::copyPtyCellToNCursesWindow(int x, int y) { VTermPos cellPosition = {y, x}; VTermScreenCell vTermCell; cchar_t converted; attr_t formatting; short colorPair; wchar_t character; vterm_screen_get_cell(pseudoTerminalScreen, cellPosition, &vTermCell); formatting = extractAttributesFromVTermCell(vTermCell); colorPair = extractColorFromVTermCell(vTermCell); if(vTermCell.chars[0] == 0) character = ' '; else character = *vTermCell.chars; //__debug_log(std::string("written '") + (char) character + std::string("' ") + std::to_string(x) + ", " + std::to_string(y)); setcchar(&converted, &character, formatting, colorPair, NULL); { move(cellPosition.row, cellPosition.col); chgat(1, formatting, colorPair, NULL); add_wch(&converted); } } int PtyWindow::refresh() { //__debug_log("refreshing"); scoped_lock lock(*ncursesMutex); move(cursorY, cursorX); return Window::refresh(); } /// @todo potential racing condition where drawing into terminal while /// resizing? int PtyWindow::resize(int rows, int cols) { scoped_lock lock(ptyMutex, *ncursesMutex); winsize windowSize = { /*.ws_row =*/ narrow(rows) , /*.ws_col =*/ narrow(cols) , 0 , 0 }; ioctl(fdPtyHost, TIOCSWINSZ, &windowSize); vterm_set_size(pseudoTerminal, rows, cols); return Window::resize(rows, cols); } int PtyWindow::staticHandlerDamage(VTermRect rect, void *user) { PtyWindow *instance = static_cast(user); return instance->handlerDamage(rect); } int PtyWindow::staticHandlerMoveRect(VTermRect dest, VTermRect src, void *user) { PtyWindow *instance = static_cast(user); return instance->handlerMoveRect(dest, src); } int PtyWindow::staticHandlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible, void *user) { PtyWindow *instance = static_cast(user); return instance->handlerMoveCursor(pos, oldpos, visible); } int PtyWindow::staticHandlerSetTermProp(VTermProp prop, VTermValue *val, void *user) { PtyWindow *instance = static_cast(user); return instance->handlerSetTermProp(prop, val); } int PtyWindow::staticHandlerBell(void *user) { PtyWindow *instance = static_cast(user); return instance->handlerBell(); } int PtyWindow::staticHandlerResize(int rows, int cols, void *user) { PtyWindow *instance = static_cast(user); return instance->handlerResize(rows, cols); } int PtyWindow::staticHandlerPushLine(int cols, const VTermScreenCell *cells, void *user) { PtyWindow *instance = static_cast(user); return instance->handlerPushLine(cols, cells); } int PtyWindow::staticHandlerPopLine(int cols, VTermScreenCell *cells, void *user) { PtyWindow *instance = static_cast(user); return instance->handlerPopLine(cols, cells); } void PtyWindow::staticHandlerOutput(const char *s, size_t len, void *user) { PtyWindow *instance = static_cast(user); instance->writeToPtyClient(s, len); } }