#include "Terminal.h" #include "XtermColors.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#define TERMINAL_DEBUG Terminal::Terminal(int ptm_fd) : m_ptm_fd(ptm_fd) , m_notifier(ptm_fd, CNotifier::Read) { set_frame_shape(FrameShape::Container); set_frame_shadow(FrameShadow::Sunken); set_frame_thickness(2); m_cursor_blink_timer.set_interval(500); m_cursor_blink_timer.on_timeout = [this] { m_cursor_blink_state = !m_cursor_blink_state; update_cursor(); }; set_font(Font::default_fixed_width_font()); m_notifier.on_ready_to_read = [this]{ byte buffer[BUFSIZ]; ssize_t nread = read(m_ptm_fd, buffer, sizeof(buffer)); if (nread < 0) { dbgprintf("Terminal read error: %s\n", strerror(errno)); perror("read(ptm)"); GApplication::the().quit(1); return; } if (nread == 0) { dbgprintf("Terminal: EOF on master pty, closing.\n"); GApplication::the().quit(0); return; } for (ssize_t i = 0; i < nread; ++i) on_char(buffer[i]); flush_dirty_lines(); }; m_line_height = font().glyph_height() + m_line_spacing; set_size(80, 25); } Terminal::Line::Line(word columns) : length(columns) { characters = new byte[length]; attributes = new Attribute[length]; memset(characters, ' ', length); } Terminal::Line::~Line() { delete [] characters; delete [] attributes; } void Terminal::Line::clear(Attribute attribute) { if (dirty) { memset(characters, ' ', length); for (word i = 0 ; i < length; ++i) attributes[i] = attribute; return; } for (unsigned i = 0 ; i < length; ++i) { if (characters[i] != ' ') dirty = true; characters[i] = ' '; } for (unsigned i = 0 ; i < length; ++i) { if (attributes[i] != attribute) dirty = true; attributes[i] = attribute; } } Terminal::~Terminal() { for (int i = 0; i < m_rows; ++i) delete m_lines[i]; delete [] m_lines; free(m_horizontal_tabs); } void Terminal::clear() { for (size_t i = 0; i < rows(); ++i) line(i).clear(m_current_attribute); set_cursor(0, 0); } inline bool is_valid_parameter_character(byte ch) { return ch >= 0x30 && ch <= 0x3f; } inline bool is_valid_intermediate_character(byte ch) { return ch >= 0x20 && ch <= 0x2f; } inline bool is_valid_final_character(byte ch) { return ch >= 0x40 && ch <= 0x7e; } static inline Color lookup_color(unsigned color) { return Color::from_rgb(xterm_colors[color]); } void Terminal::escape$m(const ParamVector& params) { if (params.is_empty()) { m_current_attribute.reset(); return; } if (params.size() == 3 && params[1] == 5) { if (params[0] == 38) { m_current_attribute.foreground_color = params[2]; return; } else if (params[0] == 48) { m_current_attribute.background_color = params[2]; return; } } for (auto param : params) { switch (param) { case 0: // Reset m_current_attribute.reset(); break; case 1: // Bold //m_current_attribute.bold = true; break; case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: // Foreground color m_current_attribute.foreground_color = param - 30; break; case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: // Background color m_current_attribute.background_color = param - 40; break; } } } void Terminal::escape$s(const ParamVector&) { m_saved_cursor_row = m_cursor_row; m_saved_cursor_column = m_cursor_column; } void Terminal::escape$u(const ParamVector&) { set_cursor(m_saved_cursor_row, m_saved_cursor_column); } void Terminal::escape$t(const ParamVector& params) { if (params.size() < 1) return; dbgprintf("FIXME: escape$t: Ps: %u\n", params[0]); } void Terminal::escape$r(const ParamVector& params) { unsigned top = 1; unsigned bottom = m_rows; if (params.size() >= 1) top = params[0]; if (params.size() >= 2) bottom = params[1]; dbgprintf("FIXME: escape$r: Set scrolling region: %u-%u\n", top, bottom); } void Terminal::escape$H(const ParamVector& params) { unsigned row = 1; unsigned col = 1; if (params.size() >= 1) row = params[0]; if (params.size() >= 2) col = params[1]; set_cursor(row - 1, col - 1); } void Terminal::escape$A(const ParamVector& params) { int num = 1; if (params.size() >= 1) num = params[0]; if (num == 0) num = 1; int new_row = (int)m_cursor_row - num; if (new_row < 0) new_row = 0; set_cursor(new_row, m_cursor_column); } void Terminal::escape$B(const ParamVector& params) { int num = 1; if (params.size() >= 1) num = params[0]; if (num == 0) num = 1; int new_row = (int)m_cursor_row + num; if (new_row >= m_rows) new_row = m_rows - 1; set_cursor(new_row, m_cursor_column); } void Terminal::escape$C(const ParamVector& params) { int num = 1; if (params.size() >= 1) num = params[0]; if (num == 0) num = 1; int new_column = (int)m_cursor_column + num; if (new_column >= m_columns) new_column = m_columns - 1; set_cursor(m_cursor_row, new_column); } void Terminal::escape$D(const ParamVector& params) { int num = 1; if (params.size() >= 1) num = params[0]; if (num == 0) num = 1; int new_column = (int)m_cursor_column - num; if (new_column < 0) new_column = 0; set_cursor(m_cursor_row, new_column); } void Terminal::escape$G(const ParamVector& params) { int new_column = 1; if (params.size() >= 1) new_column = params[0] - 1; if (new_column < 0) new_column = 0; set_cursor(m_cursor_row, new_column); } void Terminal::escape$d(const ParamVector& params) { int new_row = 1; if (params.size() >= 1) new_row = params[0] - 1; if (new_row < 0) new_row = 0; set_cursor(new_row, m_cursor_column); } void Terminal::escape$X(const ParamVector& params) { // Erase characters (without moving cursor) int num = 1; if (params.size() >= 1) num = params[0]; if (num == 0) num = 1; // Clear from cursor to end of line. for (int i = m_cursor_column; i < num; ++i) { put_character_at(m_cursor_row, i, ' '); } } void Terminal::escape$K(const ParamVector& params) { int mode = 0; if (params.size() >= 1) mode = params[0]; switch (mode) { case 0: // Clear from cursor to end of line. for (int i = m_cursor_column; i < m_columns; ++i) { put_character_at(m_cursor_row, i, ' '); } break; case 1: // Clear from cursor to beginning of line. for (int i = 0; i < m_cursor_column; ++i) { put_character_at(m_cursor_row, i, ' '); } break; case 2: unimplemented_escape(); break; default: unimplemented_escape(); break; } } void Terminal::escape$J(const ParamVector& params) { int mode = 0; if (params.size() >= 1) mode = params[0]; switch (mode) { case 0: // Clear from cursor to end of screen. for (int i = m_cursor_column; i < m_columns; ++i) { put_character_at(m_cursor_row, i, ' '); } for (int row = m_cursor_row + 1; row < m_rows; ++row) { for (int column = 0; column < m_columns; ++column) { put_character_at(row, column, ' '); } } break; case 1: // FIXME: Clear from cursor to beginning of screen. unimplemented_escape(); break; case 2: clear(); break; case 3: // FIXME: [3J should also clear the scrollback buffer. clear(); break; default: unimplemented_escape(); break; } } void Terminal::escape$M(const ParamVector& params) { int count = 1; if (params.size() >= 1) count = params[0]; if (count == 1 && m_cursor_row == 0) { scroll_up(); return; } int max_count = m_rows - m_cursor_row; count = min(count, max_count); dbgprintf("Delete %d line(s) starting from %d\n", count, m_cursor_row); // FIXME: Implement. ASSERT_NOT_REACHED(); } void Terminal::execute_xterm_command() { m_final = '@'; bool ok; unsigned value = String::copy(m_xterm_param1).to_uint(ok); if (ok) { switch (value) { case 0: case 1: case 2: set_window_title(String::copy(m_xterm_param2)); break; default: unimplemented_xterm_escape(); break; } } m_xterm_param1.clear_with_capacity(); m_xterm_param2.clear_with_capacity(); } void Terminal::execute_escape_sequence(byte final) { m_final = final; auto paramparts = String::copy(m_parameters).split(';'); ParamVector params; for (auto& parampart : paramparts) { bool ok; unsigned value = parampart.to_uint(ok); if (!ok) { m_parameters.clear_with_capacity(); m_intermediates.clear_with_capacity(); // FIXME: Should we do something else? return; } params.append(value); } switch (final) { case 'A': escape$A(params); break; case 'B': escape$B(params); break; case 'C': escape$C(params); break; case 'D': escape$D(params); break; case 'H': escape$H(params); break; case 'J': escape$J(params); break; case 'K': escape$K(params); break; case 'M': escape$M(params); break; case 'G': escape$G(params); break; case 'X': escape$X(params); break; case 'd': escape$d(params); break; case 'm': escape$m(params); break; case 's': escape$s(params); break; case 'u': escape$u(params); break; case 't': escape$t(params); break; case 'r': escape$r(params); break; default: dbgprintf("Terminal::execute_escape_sequence: Unhandled final '%c'\n", final); break; } m_parameters.clear_with_capacity(); m_intermediates.clear_with_capacity(); } void Terminal::newline() { word new_row = m_cursor_row; if (m_cursor_row == (rows() - 1)) { scroll_up(); } else { ++new_row; } set_cursor(new_row, 0); } void Terminal::scroll_up() { // NOTE: We have to invalidate the cursor first. invalidate_cursor(); delete m_lines[0]; for (word row = 1; row < rows(); ++row) m_lines[row - 1] = m_lines[row]; m_lines[m_rows - 1] = new Line(m_columns); ++m_rows_to_scroll_backing_store; m_need_full_flush = true; } void Terminal::set_cursor(unsigned a_row, unsigned a_column) { unsigned row = min(a_row, m_rows - 1u); unsigned column = min(a_column, m_columns - 1u); if (row == m_cursor_row && column == m_cursor_column) return; ASSERT(row < rows()); ASSERT(column < columns()); invalidate_cursor(); m_cursor_row = row; m_cursor_column = column; if (column != columns() - 1) m_stomp = false; invalidate_cursor(); } void Terminal::put_character_at(unsigned row, unsigned column, byte ch) { ASSERT(row < rows()); ASSERT(column < columns()); auto& line = this->line(row); if ((line.characters[column] == ch) && (line.attributes[column] == m_current_attribute)) return; line.characters[column] = ch; line.attributes[column] = m_current_attribute; line.dirty = true; } void Terminal::on_char(byte ch) { #ifdef TERMINAL_DEBUG dbgprintf("Terminal::on_char: %b (%c), fg=%u, bg=%u\n", ch, ch, m_current_attribute.foreground_color, m_current_attribute.background_color); #endif switch (m_escape_state) { case ExpectBracket: if (ch == '[') m_escape_state = ExpectParameter; else if (ch == '(') { m_swallow_current = true; m_escape_state = ExpectParameter; } else if (ch == ']') m_escape_state = ExpectXtermParameter1; else m_escape_state = Normal; return; case ExpectXtermParameter1: if (ch != ';') { m_xterm_param1.append(ch); return; } m_escape_state = ExpectXtermParameter2; return; case ExpectXtermParameter2: if (ch != '\007') { m_xterm_param2.append(ch); return; } m_escape_state = ExpectXtermFinal; [[fallthrough]]; case ExpectXtermFinal: m_escape_state = Normal; if (ch == '\007') execute_xterm_command(); return; case ExpectParameter: if (is_valid_parameter_character(ch)) { m_parameters.append(ch); return; } m_escape_state = ExpectIntermediate; [[fallthrough]]; case ExpectIntermediate: if (is_valid_intermediate_character(ch)) { m_intermediates.append(ch); return; } m_escape_state = ExpectFinal; [[fallthrough]]; case ExpectFinal: if (is_valid_final_character(ch)) { m_escape_state = Normal; if (!m_swallow_current) execute_escape_sequence(ch); m_swallow_current = false; return; } m_escape_state = Normal; m_swallow_current = false; return; case Normal: break; } switch (ch) { case '\0': return; case '\033': m_escape_state = ExpectBracket; m_swallow_current = false; return; case 8: // Backspace if (m_cursor_column) { set_cursor(m_cursor_row, m_cursor_column - 1); put_character_at(m_cursor_row, m_cursor_column, ' '); return; } return; case '\a': beep(); return; case '\t': { for (unsigned i = m_cursor_column; i < columns(); ++i) { if (m_horizontal_tabs[i]) { set_cursor(m_cursor_row, i); return; } } return; } case '\r': set_cursor(m_cursor_row, 0); return; case '\n': newline(); return; } auto new_column = m_cursor_column + 1; if (new_column < columns()) { put_character_at(m_cursor_row, m_cursor_column, ch); set_cursor(m_cursor_row, new_column); } else { if (m_stomp) { m_stomp = false; newline(); put_character_at(m_cursor_row, m_cursor_column, ch); set_cursor(m_cursor_row, 1); } else { // Curious: We wait once on the right-hand side m_stomp = true; put_character_at(m_cursor_row, m_cursor_column, ch); } } } void Terminal::inject_string(const String& str) { for (int i = 0; i < str.length(); ++i) on_char(str[i]); } void Terminal::unimplemented_escape() { StringBuilder builder; builder.appendf("((Unimplemented escape: %c", m_final); if (!m_parameters.is_empty()) { builder.append(" parameters:"); for (int i = 0; i < m_parameters.size(); ++i) builder.append((char)m_parameters[i]); } if (!m_intermediates.is_empty()) { builder.append(" intermediates:"); for (int i = 0; i < m_intermediates.size(); ++i) builder.append((char)m_intermediates[i]); } builder.append("))"); inject_string(builder.to_string()); } void Terminal::unimplemented_xterm_escape() { auto message = String::format("((Unimplemented xterm escape: %c))\n", m_final); inject_string(message); } void Terminal::set_size(word columns, word rows) { if (columns == m_columns && rows == m_rows) return; if (m_lines) { for (size_t i = 0; i < m_rows; ++i) delete m_lines[i]; delete m_lines; } m_columns = columns; m_rows = rows; m_cursor_row = 0; m_cursor_column = 0; m_saved_cursor_row = 0; m_saved_cursor_column = 0; if (m_horizontal_tabs) free(m_horizontal_tabs); m_horizontal_tabs = static_cast(malloc(columns)); for (unsigned i = 0; i < columns; ++i) m_horizontal_tabs[i] = (i % 8) == 0; // Rightmost column is always last tab on line. m_horizontal_tabs[columns - 1] = 1; m_lines = new Line*[rows]; for (size_t i = 0; i < rows; ++i) m_lines[i] = new Line(columns); m_pixel_width = (frame_thickness() * 2) + (m_inset * 2) + (m_columns * font().glyph_width('x')); m_pixel_height = (frame_thickness() * 2) + (m_inset * 2) + (m_rows * (font().glyph_height() + m_line_spacing)) - m_line_spacing; set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed); set_preferred_size({ m_pixel_width, m_pixel_height }); m_rows_to_scroll_backing_store = 0; m_needs_background_fill = true; force_repaint(); winsize ws; ws.ws_row = rows; ws.ws_col = columns; int rc = ioctl(m_ptm_fd, TIOCSWINSZ, &ws); ASSERT(rc == 0); } Rect Terminal::glyph_rect(word row, word column) { int y = row * m_line_height; int x = column * font().glyph_width('x'); return { x + frame_thickness() + m_inset, y + frame_thickness() + m_inset, font().glyph_width('x'), font().glyph_height() }; } Rect Terminal::row_rect(word row) { int y = row * m_line_height; Rect rect = { frame_thickness() + m_inset, y + frame_thickness() + m_inset, font().glyph_width('x') * m_columns, font().glyph_height() }; rect.inflate(0, m_line_spacing); return rect; } bool Terminal::Line::has_only_one_background_color() const { if (!length) return true; // FIXME: Cache this result? auto color = attributes[0].background_color; for (size_t i = 1; i < length; ++i) { if (attributes[i].background_color != color) return false; } return true; } void Terminal::event(CEvent& event) { if (event.type() == GEvent::WindowBecameActive || event.type() == GEvent::WindowBecameInactive) { m_in_active_window = event.type() == GEvent::WindowBecameActive; if (!m_in_active_window) { m_cursor_blink_timer.stop(); } else { m_cursor_blink_state = true; m_cursor_blink_timer.start(); } invalidate_cursor(); update(); } return GWidget::event(event); } void Terminal::keydown_event(GKeyEvent& event) { char ch = !event.text().is_empty() ? event.text()[0] : 0; if (event.ctrl()) { if (ch >= 'a' && ch <= 'z') { ch = ch - 'a' + 1; } else if (ch == '\\') { ch = 0x1c; } } switch (event.key()) { case KeyCode::Key_Up: write(m_ptm_fd, "\033[A", 3); break; case KeyCode::Key_Down: write(m_ptm_fd, "\033[B", 3); break; case KeyCode::Key_Right: write(m_ptm_fd, "\033[C", 3); break; case KeyCode::Key_Left: write(m_ptm_fd, "\033[D", 3); break; case KeyCode::Key_Home: write(m_ptm_fd, "\033[H", 3); break; case KeyCode::Key_End: write(m_ptm_fd, "\033[F", 3); break; default: write(m_ptm_fd, &ch, 1); break; } } void Terminal::paint_event(GPaintEvent& event) { GFrame::paint_event(event); GPainter painter(*this); if (m_needs_background_fill) { m_needs_background_fill = false; painter.fill_rect(frame_inner_rect(), Color(Color::Black).with_alpha(255 * m_opacity)); } if (m_rows_to_scroll_backing_store && m_rows_to_scroll_backing_store < m_rows) { int first_scanline = m_inset; int second_scanline = m_inset + (m_rows_to_scroll_backing_store * m_line_height); int num_rows_to_memcpy = m_rows - m_rows_to_scroll_backing_store; int scanlines_to_copy = (num_rows_to_memcpy * m_line_height) - m_line_spacing; memcpy( painter.target()->scanline(first_scanline), painter.target()->scanline(second_scanline), scanlines_to_copy * painter.target()->pitch() ); line(max(0, m_cursor_row - m_rows_to_scroll_backing_store)).dirty = true; } m_rows_to_scroll_backing_store = 0; invalidate_cursor(); for (word row = 0; row < m_rows; ++row) { auto& line = this->line(row); if (!line.dirty) continue; line.dirty = false; bool has_only_one_background_color = line.has_only_one_background_color(); if (has_only_one_background_color) { painter.fill_rect(row_rect(row), lookup_color(line.attributes[0].background_color).with_alpha(255 * m_opacity)); } for (word column = 0; column < m_columns; ++column) { bool should_reverse_fill_for_cursor = m_cursor_blink_state && m_in_active_window && row == m_cursor_row && column == m_cursor_column; auto& attribute = line.attributes[column]; char ch = line.characters[column]; auto character_rect = glyph_rect(row, column); if (!has_only_one_background_color || should_reverse_fill_for_cursor) { auto cell_rect = character_rect.inflated(0, m_line_spacing); painter.fill_rect(cell_rect, lookup_color(should_reverse_fill_for_cursor ? attribute.foreground_color : attribute.background_color).with_alpha(255 * m_opacity)); } if (ch == ' ') continue; painter.draw_glyph(character_rect.location(), ch, lookup_color(should_reverse_fill_for_cursor ? attribute.background_color : attribute.foreground_color)); } } if (!m_in_active_window) { auto cell_rect = glyph_rect(m_cursor_row, m_cursor_column).inflated(0, m_line_spacing); painter.draw_rect(cell_rect, lookup_color(line(m_cursor_row).attributes[m_cursor_column].foreground_color)); } if (m_belling) painter.draw_rect(frame_inner_rect(), Color::Red); } void Terminal::set_window_title(const String& title) { auto* w = window(); if (!w) return; w->set_title(title); } void Terminal::invalidate_cursor() { line(m_cursor_row).dirty = true; } void Terminal::flush_dirty_lines() { if (m_need_full_flush) { update(); m_need_full_flush = false; return; } Rect rect; for (int i = 0; i < m_rows; ++i) { if (line(i).dirty) rect = rect.united(row_rect(i)); } update(rect); } void Terminal::force_repaint() { m_needs_background_fill = true; for (int i = 0; i < m_rows; ++i) line(i).dirty = true; update(); } void Terminal::resize_event(GResizeEvent& event) { int new_columns = (event.size().width() - frame_thickness() * 2 - m_inset * 2) / font().glyph_width('x'); int new_rows = (event.size().height() - frame_thickness() * 2 - m_inset * 2) / m_line_height; set_size(new_columns, new_rows); } void Terminal::apply_size_increments_to_window(GWindow& window) { window.set_size_increment({ font().glyph_width('x'), m_line_height }); window.set_base_size({ frame_thickness() * 2 + m_inset * 2, frame_thickness() * 2 + m_inset * 2 }); } void Terminal::update_cursor() { invalidate_cursor(); flush_dirty_lines(); } void Terminal::set_opacity(float opacity) { if (m_opacity == opacity) return; window()->set_has_alpha_channel(opacity < 1); m_opacity = opacity; force_repaint(); }