#include "TerminalWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include static constexpr auto RED = ui::Color::from_u32(0xffcd0000); static constexpr auto GREEN = ui::Color::from_u32(0xff00cd00); static constexpr auto YELLOW = ui::Color::from_u32(0xffcdcd00); static constexpr auto BLUE = ui::Color::from_u32(0xff0000ee); static constexpr auto MAGENTA = ui::Color::from_u32(0xffcd00cd); static constexpr auto CYAN = ui::Color::from_u32(0xff00cdcd); static constexpr auto GRAY = ui::Color::from_u32(0xffe5e5e5); static constexpr auto BRIGHT_BLACK = ui::Color::from_u32(0xff7f7f7f); static constexpr auto BRIGHT_RED = ui::Color::from_u32(0xffff0000); static constexpr auto BRIGHT_GREEN = ui::Color::from_u32(0xff00ff00); static constexpr auto BRIGHT_YELLOW = ui::Color::from_u32(0xffffff00); static constexpr auto BRIGHT_BLUE = ui::Color::from_u32(0xff5c5cff); static constexpr auto BRIGHT_MAGENTA = ui::Color::from_u32(0xffff00ff); static constexpr auto BRIGHT_CYAN = ui::Color::from_u32(0xff00ffff); static constexpr auto BRIGHT_GRAY = ui::Color::from_u32(0xffffffff); static long get_time_in_milliseconds() { struct timespec ts; check(clock_gettime(CLOCK_REALTIME, &ts) == 0); return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } static void sigchld_handler(int) { wait(NULL); ui::App::the().set_should_close(true); } Result TerminalWidget::init(char* const* args) { m_font = ui::Font::default_font(); m_bold_font = ui::Font::default_bold_font(); m_terminal_canvas = ui::App::the().main_window()->canvas(); m_terminal_canvas.fill(ui::BLACK); signal(SIGCHLD, sigchld_handler); int master; int slave; int result = openpty(&master, &slave, nullptr, nullptr, nullptr); if (result < 0) return err(errno); pid_t child = TRY(os::Process::fork()); if (child == 0) { setsid(); close(master); ioctl(slave, TIOCSCTTY); dup2(slave, STDIN_FILENO); dup2(slave, STDOUT_FILENO); dup2(slave, STDERR_FILENO); tcsetpgrp(slave, getpid()); close(slave); execv(args[0], args); _exit(127); } close(slave); m_pty = master; fcntl(master, F_SETFL, O_NONBLOCK); m_child_pid = child; m_last_cursor_tick = get_time_in_milliseconds(); return {}; } Result TerminalWidget::handle_key_event(const ui::KeyEventRequest& request) { // Avoid handling "key released" events if (!request.pressed) return ui::EventResult::DidNotHandle; // Non-printable key or key that has no special character (unlike Tab or Enter). We exit early to avoid inserting an // invalid zero byte into the terminal input (this would also happen on Shift or Ctrl keypresses). if (request.letter == '\0') return ui::EventResult::DidNotHandle; write(m_pty, &request.letter, 1); return ui::EventResult::DidHandle; } Result TerminalWidget::draw(ui::Canvas&) { return {}; } Result TerminalWidget::process() { char buffer[BUFSIZ]; ssize_t nread = read(m_pty, buffer, BUFSIZ); if (nread < 0) { if (errno == EAGAIN) nread = 0; else return err(errno); } bool should_update_cursor = tick_cursor(); ssize_t drawn = 0; for (ssize_t i = 0; i < nread; i++) { bool did_draw = TRY(putchar(buffer[i])); if (did_draw) drawn++; } if (should_update_cursor || drawn > 0) ui::App::the().main_window()->draw(); return nread == 0; } bool TerminalWidget::tick_cursor() { if (!m_cursor_enabled) return false; long now = get_time_in_milliseconds(); long diff = now - m_last_cursor_tick; m_last_cursor_tick = now; m_current_cursor_timeout -= (int)diff; if (m_current_cursor_timeout <= 0) { m_current_cursor_timeout = CURSOR_TIMEOUT; m_cursor_activated = !m_cursor_activated; if (m_cursor_activated) draw_cursor(); else erase_current_char(); return true; } return false; } void TerminalWidget::draw_glyph(wchar_t c, int x, int y) { auto subcanvas = m_terminal_canvas.subcanvas({ x, y, m_font->width(), m_font->height() }); subcanvas.fill(m_background_color); (m_bold ? m_bold_font : m_font)->render(c, m_foreground_color, subcanvas); } void TerminalWidget::erase_current_line() { m_terminal_canvas.subcanvas({ 0, m_y_position, m_rect.width, m_font->height() }).fill(ui::BLACK); } void TerminalWidget::scroll() { memcpy(m_terminal_canvas.ptr, m_terminal_canvas.ptr + (m_rect.width * sizeof(u32) * m_font->height()), (m_rect.width * m_rect.height * sizeof(u32)) - (m_rect.width * sizeof(u32) * m_font->height())); m_y_position -= m_font->height(); erase_current_line(); } bool TerminalWidget::should_scroll() { return m_y_position >= m_rect.height; } void TerminalWidget::next_line() { m_x_position = 0; m_y_position += m_font->height(); } void TerminalWidget::next_char() { m_x_position += m_font->width(); } void TerminalWidget::prev_char() { m_x_position -= m_font->width(); } void TerminalWidget::erase_current_char() { m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::BLACK); } void TerminalWidget::draw_cursor() { m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::WHITE); } bool TerminalWidget::at_end_of_screen() { return (m_x_position + m_font->width()) > m_rect.width; } bool TerminalWidget::handle_escape_sequence(wchar_t c) { auto rc = m_escape_parser->advance(static_cast(c)); if (rc.has_error()) { m_escape_parser = Option {}; return false; } if (!rc.value()) return true; if (!m_escape_parser->valid()) { m_escape_parser = Option {}; return false; } const auto& params = m_escape_parser->parameters(); switch (m_escape_parser->code()) { case EscapeCode::CursorUp: { int lines = params.size() ? params[0] : 1; int pixels = lines * m_font->height(); if (pixels > m_y_position) m_y_position = 0; else m_y_position -= pixels; }; break; case EscapeCode::CursorDown: { int lines = params.size() ? params[0] : 1; int pixels = lines * m_font->height(); if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height(); else m_y_position += pixels; }; break; case EscapeCode::CursorBack: { int chars = params.size() ? params[0] : 1; int pixels = chars * m_font->width(); if (pixels > m_x_position) m_x_position = 0; else m_x_position -= pixels; }; break; case EscapeCode::CursorForward: { int chars = params.size() ? params[0] : 1; int pixels = chars * m_font->width(); if (pixels + m_x_position >= m_rect.width) m_x_position = m_rect.width - m_font->width(); else m_x_position += pixels; }; break; case EscapeCode::CursorNextLine: { int lines = params.size() ? params[0] : 1; int pixels = lines * m_font->height(); if (pixels > m_y_position) m_y_position = 0; else m_y_position -= pixels; m_x_position = 0; }; break; case EscapeCode::CursorPreviousLine: { int lines = params.size() ? params[0] : 1; int pixels = lines * m_font->height(); if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height(); else m_y_position += pixels; m_x_position = 0; }; break; case EscapeCode::CursorHorizontalAbsolute: { int line = (params.size() ? params[0] : 1) - 1; if (line < 0) break; int position = line * m_font->height(); if (position >= m_rect.height) position = m_rect.height - m_font->height(); m_y_position = position; }; break; case EscapeCode::SetCursorPosition: { int x = (params.size() ? params[0] : 1) - 1; int y = (params.size() > 1 ? params[1] : 1) - 1; if (x < 0 || y < 0) break; int x_position = x * m_font->width(); if (x_position >= m_rect.width) x_position = m_rect.width - m_font->height(); m_x_position = x_position; int y_position = y * m_font->height(); if (y_position >= m_rect.height) y_position = m_rect.height - m_font->height(); m_y_position = y_position; }; break; case EscapeCode::SelectGraphicRendition: { if (!params.size()) { m_foreground_color = ui::WHITE; m_background_color = ui::BLACK; m_bold = false; break; } for (usize i = 0; i < params.size(); i++) { int arg = params[i]; switch (arg) { case 0: { m_foreground_color = ui::BLACK; m_background_color = ui::WHITE; m_bold = false; break; } case 1: { m_bold = true; break; } case 22: { m_bold = false; break; } case 30: { m_foreground_color = m_bold ? BRIGHT_BLACK : ui::BLACK; break; } case 31: { m_foreground_color = m_bold ? BRIGHT_RED : RED; break; } case 32: { m_foreground_color = m_bold ? BRIGHT_GREEN : GREEN; break; } case 33: { m_foreground_color = m_bold ? BRIGHT_YELLOW : YELLOW; break; } case 34: { m_foreground_color = m_bold ? BRIGHT_BLUE : BLUE; break; } case 35: { m_foreground_color = m_bold ? BRIGHT_MAGENTA : MAGENTA; break; } case 36: { m_foreground_color = m_bold ? BRIGHT_CYAN : CYAN; break; } case 37: { m_foreground_color = m_bold ? BRIGHT_GRAY : GRAY; break; } case 39: { m_foreground_color = ui::WHITE; break; } case 40: { m_background_color = m_bold ? BRIGHT_BLACK : ui::BLACK; break; } case 41: { m_background_color = m_bold ? BRIGHT_RED : RED; break; } case 42: { m_background_color = m_bold ? BRIGHT_GREEN : GREEN; break; } case 43: { m_background_color = m_bold ? BRIGHT_YELLOW : YELLOW; break; } case 44: { m_background_color = m_bold ? BRIGHT_BLUE : BLUE; break; } case 45: { m_background_color = m_bold ? BRIGHT_MAGENTA : MAGENTA; break; } case 46: { m_background_color = m_bold ? BRIGHT_CYAN : CYAN; break; } case 47: { m_background_color = m_bold ? BRIGHT_GRAY : GRAY; break; } case 49: { m_background_color = ui::BLACK; break; } default: break; } } } break; default: break; } m_escape_parser = Option {}; return true; } Result TerminalWidget::putchar(char c) { auto guard = make_scope_guard([this] { m_decoder.reset(); }); bool is_ready = TRY(m_decoder.feed(c)); bool result = false; if (is_ready) result = put_code_point(TRY(m_decoder.extract())); guard.deactivate(); return result; } bool TerminalWidget::put_code_point(wchar_t c) { if (c > (wchar_t)255) c = (wchar_t)256; if (m_escape_parser.has_value()) { if (handle_escape_sequence(c)) return false; } // Erase the current cursor. if (m_cursor_activated) erase_current_char(); bool should_draw_cursor = m_cursor_enabled; bool did_draw = false; switch (c) { case L'\n': { next_line(); if (should_scroll()) scroll(); break; } case L'\t': { for (int i = 0; i < 4; i++) { put_code_point(L' '); } did_draw = true; break; } case L'\r': m_x_position = 0; break; case L'\b': if (m_x_position != 0) { prev_char(); erase_current_char(); did_draw = true; } break; case L'\x1b': case L'\x9b': case L'\x90': case L'\x9d': m_escape_parser = EscapeSequenceParser { (u8)c }; should_draw_cursor = false; did_draw = true; break; default: { if (iscntrl(c)) return false; draw_glyph(c, m_x_position, m_y_position); next_char(); if (at_end_of_screen()) { next_line(); if (should_scroll()) scroll(); } break; } } if (should_draw_cursor) { m_current_cursor_timeout = CURSOR_TIMEOUT; m_cursor_activated = true; draw_cursor(); } return did_draw; }