diff --git a/Applications/Makefile.common b/Applications/Makefile.common index 8935cb38be..90302abafc 100755 --- a/Applications/Makefile.common +++ b/Applications/Makefile.common @@ -3,7 +3,7 @@ DEFINES += -DUSERLAND all: $(APP) $(APP): $(OBJS) - $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lgui -ldraw -laudio -lipc -lcore -lc + $(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lgui -ldraw -laudio -lipc -lvt -lcore -lc .cpp.o: @echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $< diff --git a/Applications/Terminal/Terminal.cpp b/Applications/Terminal/Terminal.cpp index ba83a1e7c7..5fd56b25a2 100644 --- a/Applications/Terminal/Terminal.cpp +++ b/Applications/Terminal/Terminal.cpp @@ -4,11 +4,11 @@ #include #include #include +#include #include #include #include #include -#include #include #include #include @@ -17,13 +17,12 @@ #include //#define TERMINAL_DEBUG -u8 Terminal::Attribute::default_foreground_color = 7; -u8 Terminal::Attribute::default_background_color = 0; Terminal::Terminal(int ptm_fd, RefPtr config) - : m_ptm_fd(ptm_fd) + : m_terminal(*this) + , m_ptm_fd(ptm_fd) , m_notifier(ptm_fd, CNotifier::Read) - , m_config(config) + , m_config(move(config)) { set_frame_shape(FrameShape::Container); set_frame_shadow(FrameShadow::Sunken); @@ -59,916 +58,24 @@ Terminal::Terminal(int ptm_fd, RefPtr config) return; } for (ssize_t i = 0; i < nread; ++i) - on_char(buffer[i]); + m_terminal.on_char(buffer[i]); flush_dirty_lines(); }; m_line_height = font().glyph_height() + m_line_spacing; - set_size(m_config->read_num_entry("Window", "Width", 80), - m_config->read_num_entry("Window", "Height", 25)); -} - -Terminal::Line::Line(u16 length) -{ - set_length(length); -} - -Terminal::Line::~Line() -{ - delete[] characters; - delete[] attributes; -} - -void Terminal::Line::set_length(u16 new_length) -{ - if (m_length == new_length) - return; - auto* new_characters = new u8[new_length]; - auto* new_attributes = new Attribute[new_length]; - memset(new_characters, ' ', new_length); - if (characters && attributes) { - memcpy(new_characters, characters, min(m_length, new_length)); - memcpy(new_attributes, attributes, min(m_length, new_length) * sizeof(Attribute)); - } - delete[] characters; - delete[] attributes; - characters = new_characters; - attributes = new_attributes; - m_length = new_length; -} - -void Terminal::Line::clear(Attribute attribute) -{ - if (dirty) { - memset(characters, ' ', m_length); - for (u16 i = 0; i < m_length; ++i) - attributes[i] = attribute; - return; - } - for (unsigned i = 0; i < m_length; ++i) { - if (characters[i] != ' ') - dirty = true; - characters[i] = ' '; - } - for (unsigned i = 0; i < m_length; ++i) { - if (attributes[i] != attribute) - dirty = true; - attributes[i] = attribute; - } + m_terminal.set_size(m_config->read_num_entry("Window", "Width", 80), m_config->read_num_entry("Window", "Height", 25)); } Terminal::~Terminal() { } -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(u8 ch) -{ - return ch >= 0x30 && ch <= 0x3f; -} - -inline bool is_valid_intermediate_character(u8 ch) -{ - return ch >= 0x20 && ch <= 0x2f; -} - -inline bool is_valid_final_character(u8 ch) -{ - return ch >= 0x40 && ch <= 0x7e; -} - static inline Color lookup_color(unsigned color) { return Color::from_rgb(xterm_colors[color]); } -void Terminal::escape$h_l(bool should_set, bool question_param, const ParamVector& params) -{ - int mode = 2; - if (params.size() > 0) { - mode = params[0]; - } - if (!question_param) { - switch (mode) { - // FIXME: implement *something* for this - default: - unimplemented_escape(); - break; - } - } else { - switch (mode) { - case 25: - // Hide cursor command, but doesn't need to be run (for now, because - // we don't do inverse control codes anyways) - if (should_set) - dbgprintf("Terminal: Hide Cursor escapecode recieved. Not needed: ignored.\n"); - else - dbgprintf("Terminal: Show Cursor escapecode recieved. Not needed: ignored.\n"); - break; - default: - break; - } - } -} - -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: - m_current_attribute.flags |= Attribute::Bold; - break; - case 3: - m_current_attribute.flags |= Attribute::Italic; - break; - case 4: - m_current_attribute.flags |= Attribute::Underline; - break; - case 5: - m_current_attribute.flags |= Attribute::Blink; - break; - case 7: - m_current_attribute.flags |= Attribute::Negative; - break; - case 22: - m_current_attribute.flags &= ~Attribute::Bold; - break; - case 23: - m_current_attribute.flags &= ~Attribute::Italic; - break; - case 24: - m_current_attribute.flags &= ~Attribute::Underline; - break; - case 25: - m_current_attribute.flags &= ~Attribute::Blink; - break; - case 27: - m_current_attribute.flags &= ~Attribute::Negative; - break; - case 30: - case 31: - case 32: - case 33: - case 34: - case 35: - case 36: - case 37: - // Foreground color - if (m_current_attribute.flags & Attribute::Bold) - param += 8; - m_current_attribute.foreground_color = param - 30; - break; - case 39: - // reset foreground - m_current_attribute.foreground_color = Attribute::default_foreground_color; - break; - case 40: - case 41: - case 42: - case 43: - case 44: - case 45: - case 46: - case 47: - // Background color - if (m_current_attribute.flags & Attribute::Bold) - param += 8; - m_current_attribute.background_color = param - 40; - break; - case 49: - // reset background - m_current_attribute.background_color = Attribute::default_background_color; - break; - default: - dbgprintf("FIXME: escape$m: p: %u\n", param); - } - } -} - -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 (param count: %d)\n", params[0], params.size()); -} - -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]; - if ((bottom - top) < 2 || bottom > m_rows) { - dbgprintf("Error: escape$r: scrolling region invalid: %u-%u\n", top, bottom); - return; - } - m_scroll_region_top = top - 1; - m_scroll_region_bottom = bottom - 1; - set_cursor(0, 0); -} - -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$b(const ParamVector& params) -{ - if (params.size() < 1) - return; - - for (unsigned i = 0; i < params[0]; ++i) - put_character_at(m_cursor_row, m_cursor_column++, m_last_char); -} - -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: - // Clear the complete line - for (int i = 0; i < m_columns; ++i) { - put_character_at(m_cursor_row, i, ' '); - } - 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: - /// Clear from cursor to beginning of screen - for (int i = m_cursor_column - 1; i >= 0; --i) - put_character_at(m_cursor_row, i, ' '); - for (int row = m_cursor_row - 1; row >= 0; --row) { - for (int column = 0; column < m_columns; ++column) { - put_character_at(row, column, ' '); - } - } - break; - case 2: - clear(); - break; - case 3: - // FIXME: [3J should also clear the scrollback buffer. - clear(); - break; - default: - unimplemented_escape(); - break; - } -} - -void Terminal::escape$S(const ParamVector& params) -{ - int count = 1; - if (params.size() >= 1) - count = params[0]; - - for (u16 i = 0; i < count; i++) - scroll_up(); -} - -void Terminal::escape$T(const ParamVector& params) -{ - int count = 1; - if (params.size() >= 1) - count = params[0]; - - for (u16 i = 0; i < count; i++) - scroll_down(); -} - -void Terminal::escape$L(const ParamVector& params) -{ - int count = 1; - if (params.size() >= 1) - count = params[0]; - invalidate_cursor(); - for (; count > 0; --count) { - m_lines.insert(m_cursor_row + m_scroll_region_top, make(m_columns)); - if (m_scroll_region_bottom + 1 < m_lines.size()) - m_lines.remove(m_scroll_region_bottom + 1); - else - m_lines.remove(m_lines.size() - 1); - } - m_need_full_flush = true; -} - -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_scroll_region_top + m_cursor_row); - count = min(count, max_count); - - for (int c = count; c > 0; --c) { - m_lines.remove(m_cursor_row + m_scroll_region_top); - if (m_scroll_region_bottom < m_lines.size()) - m_lines.insert(m_scroll_region_bottom, make(m_columns)); - else - m_lines.append(make(m_columns)); - } -} - -void Terminal::escape$P(const ParamVector& params) -{ - int num = 1; - if (params.size() >= 1) - num = params[0]; - - if (num == 0) - num = 1; - - auto& line = this->line(m_cursor_row); - - // Move n characters of line to the left - for (int i = m_cursor_column; i < line.m_length - num; i++) - line.characters[i] = line.characters[i + num]; - - // Fill remainder of line with blanks - for (int i = line.m_length - num; i < line.m_length; i++) - line.characters[i] = ' '; - - line.dirty = true; -} - -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(u8 final) -{ - bool question_param = false; - m_final = final; - ParamVector params; - - if (m_parameters.size() > 0 && m_parameters[0] == '?') { - question_param = true; - m_parameters.remove(0); - } - auto paramparts = String::copy(m_parameters).split(';'); - for (auto& parampart : paramparts) { - bool ok; - unsigned value = parampart.to_uint(ok); - if (!ok) { - // FIXME: Should we do something else? - m_parameters.clear_with_capacity(); - m_intermediates.clear_with_capacity(); - return; - } - params.append(value); - } - -#if defined(TERMINAL_DEBUG) - dbgprintf("Terminal::execute_escape_sequence: Handled final '%c'\n", final); - dbgprintf("Params: "); - for (auto& p : params) { - dbgprintf("%d ", p); - } - dbgprintf("\b\n"); -#endif - - 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 'P': - escape$P(params); - break; - case 'S': - escape$S(params); - break; - case 'T': - escape$T(params); - break; - case 'L': - escape$L(params); - break; - case 'G': - escape$G(params); - break; - case 'X': - escape$X(params); - break; - case 'b': - escape$b(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; - case 'l': - escape$h_l(true, question_param, params); - break; - case 'h': - escape$h_l(false, question_param, params); - break; - default: - dbgprintf("Terminal::execute_escape_sequence: Unhandled final '%c'\n", final); - break; - } - -#if defined(TERMINAL_DEBUG) - dbgprintf("\n"); - for (auto& line : m_lines) { - dbgprintf("Terminal: Line: "); - for (int i = 0; i < line->length; i++) { - dbgprintf("%c", line->characters[i]); - } - dbgprintf("\n"); - } -#endif - - m_parameters.clear_with_capacity(); - m_intermediates.clear_with_capacity(); -} - -void Terminal::newline() -{ - u16 new_row = m_cursor_row; - if (m_cursor_row == m_scroll_region_bottom) { - scroll_up(); - } else { - ++new_row; - } - set_cursor(new_row, 0); -} - -void Terminal::scroll_up() -{ - // NOTE: We have to invalidate the cursor first. - invalidate_cursor(); - m_lines.remove(m_scroll_region_top); - m_lines.insert(m_scroll_region_bottom, make(m_columns)); - m_need_full_flush = true; -} - -void Terminal::scroll_down() -{ - // NOTE: We have to invalidate the cursor first. - invalidate_cursor(); - m_lines.remove(m_scroll_region_bottom); - m_lines.insert(m_scroll_region_top, make(m_columns)); - 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() - 1u) - m_stomp = false; - invalidate_cursor(); -} - -void Terminal::put_character_at(unsigned row, unsigned column, u8 ch) -{ - ASSERT(row < rows()); - ASSERT(column < columns()); - auto& line = this->line(row); - line.characters[column] = ch; - line.attributes[column] = m_current_attribute; - line.attributes[column].flags |= Attribute::Touched; - line.dirty = true; - - m_last_char = ch; -} - -void Terminal::on_char(u8 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': - if (m_should_beep) - sysbeep(); - else { - m_visual_beep_timer.restart(200); - m_visual_beep_timer.set_single_shot(true); - m_visual_beep_timer.on_timeout = [this] { - force_repaint(); - }; - force_repaint(); - } - 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(u16 columns, u16 rows) -{ - if (columns == m_columns && rows == m_rows) - return; - -#if defined(TERMINAL_DEBUG) - dbgprintf("Terminal: RESIZE to: %d rows\n", rows); -#endif - - if (rows > m_rows) { - while (m_lines.size() < rows) - m_lines.append(make(columns)); - } else { - m_lines.shrink(rows); - } - - for (int i = 0; i < rows; ++i) - m_lines[i].set_length(columns); - - m_columns = columns; - m_rows = rows; - - m_scroll_region_top = 0; - m_scroll_region_bottom = rows - 1; - - m_cursor_row = min((int)m_cursor_row, m_rows - 1); - m_cursor_column = min((int)m_cursor_column, m_columns - 1); - m_saved_cursor_row = min((int)m_saved_cursor_row, m_rows - 1); - m_saved_cursor_column = min((int)m_saved_cursor_column, m_columns - 1); - - m_horizontal_tabs.resize(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_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_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(u16 row, u16 column) { int y = row * m_line_height; @@ -979,24 +86,11 @@ Rect Terminal::glyph_rect(u16 row, u16 column) Rect Terminal::row_rect(u16 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 rect = { frame_thickness() + m_inset, y + frame_thickness() + m_inset, font().glyph_width('x') * m_terminal.columns(), font().glyph_height() }; rect.inflate(0, m_line_spacing); return rect; } -bool Terminal::Line::has_only_one_background_color() const -{ - if (!m_length) - return true; - // FIXME: Cache this result? - auto color = attributes[0].background_color; - for (size_t i = 1; i < m_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) { @@ -1087,16 +181,16 @@ void Terminal::paint_event(GPaintEvent& event) painter.fill_rect(frame_inner_rect(), Color(Color::Black).with_alpha(m_opacity)); invalidate_cursor(); - for (u16 row = 0; row < m_rows; ++row) { - auto& line = this->line(row); + for (u16 row = 0; row < m_terminal.rows(); ++row) { + auto& line = m_terminal.line(row); bool has_only_one_background_color = line.has_only_one_background_color(); if (m_visual_beep_timer.is_active()) painter.fill_rect(row_rect(row), Color::Red); else if (has_only_one_background_color) painter.fill_rect(row_rect(row), lookup_color(line.attributes[0].background_color).with_alpha(m_opacity)); - for (u16 column = 0; column < m_columns; ++column) { + for (u16 column = 0; column < m_terminal.columns(); ++column) { char ch = line.characters[column]; - bool should_reverse_fill_for_cursor_or_selection = (m_cursor_blink_state && m_in_active_window && row == m_cursor_row && column == m_cursor_column) + bool should_reverse_fill_for_cursor_or_selection = (m_cursor_blink_state && m_in_active_window && row == m_terminal.cursor_row() && column == m_terminal.cursor_column()) || selection_contains({ row, column }); auto& attribute = line.attributes[column]; auto character_rect = glyph_rect(row, column); @@ -1109,18 +203,18 @@ void Terminal::paint_event(GPaintEvent& event) painter.draw_glyph( character_rect.location(), ch, - attribute.flags & Attribute::Bold ? Font::default_bold_fixed_width_font() : font(), + attribute.flags & VT::Attribute::Bold ? Font::default_bold_fixed_width_font() : font(), lookup_color(should_reverse_fill_for_cursor_or_selection ? 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)); + auto cell_rect = glyph_rect(m_terminal.cursor_row(), m_terminal.cursor_column()).inflated(0, m_line_spacing); + painter.draw_rect(cell_rect, lookup_color(m_terminal.line(m_terminal.cursor_row()).attributes[m_terminal.cursor_column()].foreground_color)); } } -void Terminal::set_window_title(const String& title) +void Terminal::set_window_title(const StringView& title) { auto* w = window(); if (!w) @@ -1130,19 +224,19 @@ void Terminal::set_window_title(const String& title) void Terminal::invalidate_cursor() { - line(m_cursor_row).dirty = true; + m_terminal.invalidate_cursor(); } void Terminal::flush_dirty_lines() { - if (m_need_full_flush) { + if (m_terminal.m_need_full_flush) { update(); - m_need_full_flush = false; + m_terminal.m_need_full_flush = false; return; } Rect rect; - for (int i = 0; i < m_rows; ++i) { - if (line(i).dirty) + for (int i = 0; i < m_terminal.rows(); ++i) { + if (m_terminal.line(i).dirty) rect = rect.united(row_rect(i)); } update(rect); @@ -1151,8 +245,8 @@ void Terminal::flush_dirty_lines() void Terminal::force_repaint() { m_needs_background_fill = true; - for (int i = 0; i < m_rows; ++i) - line(i).dirty = true; + for (int i = 0; i < m_terminal.rows(); ++i) + m_terminal.line(i).dirty = true; update(); } @@ -1160,7 +254,7 @@ 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); + m_terminal.set_size(new_columns, new_rows); } void Terminal::apply_size_increments_to_window(GWindow& window) @@ -1185,14 +279,14 @@ void Terminal::set_opacity(u8 new_opacity) force_repaint(); } -BufferPosition Terminal::normalized_selection_start() const +VT::BufferPosition Terminal::normalized_selection_start() const { if (m_selection_start < m_selection_end) return m_selection_start; return m_selection_end; } -BufferPosition Terminal::normalized_selection_end() const +VT::BufferPosition Terminal::normalized_selection_end() const { if (m_selection_start < m_selection_end) return m_selection_end; @@ -1201,10 +295,10 @@ BufferPosition Terminal::normalized_selection_end() const bool Terminal::has_selection() const { - return m_selection_start.is_valid() && m_selection_end.is_valid(); + return m_selection_start.is_valid() && m_selection_end.is_valid(); } -bool Terminal::selection_contains(const BufferPosition& position) const +bool Terminal::selection_contains(const VT::BufferPosition& position) const { if (!has_selection()) return false; @@ -1212,7 +306,7 @@ bool Terminal::selection_contains(const BufferPosition& position) const return position >= normalized_selection_start() && position <= normalized_selection_end(); } -BufferPosition Terminal::buffer_position_at(const Point& position) const +VT::BufferPosition Terminal::buffer_position_at(const Point& position) const { auto adjusted_position = position.translated(-(frame_thickness() + m_inset), -(frame_thickness() + m_inset)); int row = adjusted_position.y() / m_line_height; @@ -1221,10 +315,10 @@ BufferPosition Terminal::buffer_position_at(const Point& position) const row = 0; if (column < 0) column = 0; - if (row >= m_rows) - row = m_rows - 1; - if (column >= m_columns) - column = m_columns - 1; + if (row >= m_terminal.rows()) + row = m_terminal.rows() - 1; + if (column >= m_terminal.columns()) + column = m_terminal.columns() - 1; return { row, column }; } @@ -1274,9 +368,9 @@ String Terminal::selected_text() const for (int row = start.row(); row <= end.row(); ++row) { int first_column = row == start.row() ? start.column() : 0; - int last_column = row == end.row() ? end.column() : m_columns - 1; + int last_column = row == end.row() ? end.column() : m_terminal.columns() - 1; for (int column = first_column; column <= last_column; ++column) { - auto& line = this->line(row); + auto& line = m_terminal.line(row); if (line.attributes[column].is_untouched()) { builder.append('\n'); break; @@ -1290,3 +384,35 @@ String Terminal::selected_text() const return builder.to_string(); } + +void Terminal::terminal_did_resize(u16 columns, u16 rows) +{ + m_pixel_width = (frame_thickness() * 2) + (m_inset * 2) + (columns * font().glyph_width('x')); + m_pixel_height = (frame_thickness() * 2) + (m_inset * 2) + (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_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); +} + +void Terminal::beep() +{ + if (m_should_beep) { + sysbeep(); + return; + } + m_visual_beep_timer.restart(200); + m_visual_beep_timer.set_single_shot(true); + m_visual_beep_timer.on_timeout = [this] { + force_repaint(); + }; + force_repaint(); +} diff --git a/Applications/Terminal/Terminal.h b/Applications/Terminal/Terminal.h index 48a9defe43..3d985c56e4 100644 --- a/Applications/Terminal/Terminal.h +++ b/Applications/Terminal/Terminal.h @@ -1,68 +1,22 @@ #pragma once #include -#include -#include #include #include #include #include #include #include +#include -class Font; - -class BufferPosition { -public: - BufferPosition() {} - BufferPosition(int row, int column) - : m_row(row) - , m_column(column) - { - } - - bool is_valid() const { return m_row >= 0 && m_column >= 0; } - int row() const { return m_row; } - int column() const { return m_column; } - - bool operator<(const BufferPosition& other) const - { - return m_row < other.m_row || (m_row == other.m_row && m_column < other.m_column); - } - - bool operator<=(const BufferPosition& other) const - { - return *this < other || *this == other; - } - - bool operator>=(const BufferPosition& other) const - { - return !(*this < other); - } - - bool operator==(const BufferPosition& other) const - { - return m_row == other.m_row && m_column == other.m_column; - } - - bool operator!=(const BufferPosition& other) const - { - return !(*this == other); - } - -private: - int m_row { -1 }; - int m_column { -1 }; -}; - -class Terminal final : public GFrame { +class Terminal final : public GFrame + , public VT::TerminalClient { C_OBJECT(Terminal) public: explicit Terminal(int ptm_fd, RefPtr config); virtual ~Terminal() override; void create_window(); - void on_char(u8); void flush_dirty_lines(); void force_repaint(); @@ -77,15 +31,14 @@ public: RefPtr config() const { return m_config; } bool has_selection() const; - bool selection_contains(const BufferPosition&) const; + bool selection_contains(const VT::BufferPosition&) const; String selected_text() const; - BufferPosition buffer_position_at(const Point&) const; - BufferPosition normalized_selection_start() const; - BufferPosition normalized_selection_end() const; + VT::BufferPosition buffer_position_at(const Point&) const; + VT::BufferPosition normalized_selection_start() const; + VT::BufferPosition normalized_selection_end() const; private: - typedef Vector ParamVector; - + // ^GWidget virtual void event(CEvent&) override; virtual void paint_event(GPaintEvent&) override; virtual void resize_event(GResizeEvent&) override; @@ -94,155 +47,24 @@ private: virtual void mousemove_event(GMouseEvent&) override; virtual void mouseup_event(GMouseEvent&) override; - void scroll_up(); - void scroll_down(); - void newline(); - void set_cursor(unsigned row, unsigned column); - void put_character_at(unsigned row, unsigned column, u8 ch); - void invalidate_cursor(); - void set_window_title(const String&); + // ^TerminalClient + virtual void beep() override; + virtual void set_window_title(const StringView&) override; + virtual void terminal_did_resize(u16 columns, u16 rows) override; - void inject_string(const String&); - void unimplemented_escape(); - void unimplemented_xterm_escape(); - void escape$A(const ParamVector&); - void escape$B(const ParamVector&); - void escape$C(const ParamVector&); - void escape$D(const ParamVector&); - void escape$H(const ParamVector&); - void escape$J(const ParamVector&); - void escape$K(const ParamVector&); - void escape$M(const ParamVector&); - void escape$P(const ParamVector&); - void escape$G(const ParamVector&); - void escape$X(const ParamVector&); - void escape$b(const ParamVector&); - void escape$d(const ParamVector&); - void escape$m(const ParamVector&); - void escape$s(const ParamVector&); - void escape$u(const ParamVector&); - void escape$t(const ParamVector&); - void escape$r(const ParamVector&); - void escape$S(const ParamVector&); - void escape$T(const ParamVector&); - void escape$L(const ParamVector&); - void escape$h_l(bool, bool, const ParamVector&); - - void clear(); - - void set_size(u16 columns, u16 rows); - u16 columns() const { return m_columns; } - u16 rows() const { return m_rows; } Rect glyph_rect(u16 row, u16 column); Rect row_rect(u16 row); + void update_cursor(); + void invalidate_cursor(); - struct Attribute { - Attribute() { reset(); } + VT::Terminal m_terminal; - static u8 default_foreground_color; - static u8 default_background_color; - - void reset() - { - foreground_color = default_foreground_color; - background_color = default_background_color; - flags = Flags::NoAttributes; - } - u8 foreground_color; - u8 background_color; - - enum Flags { - NoAttributes = 0x00, - Bold = 0x01, - Italic = 0x02, - Underline = 0x04, - Negative = 0x08, - Blink = 0x10, - Touched = 0x20, - }; - - bool is_untouched() const { return !(flags & Touched); } - - // TODO: it would be really nice if we had a helper for enums that - // exposed bit ops for class enums... - int flags = Flags::NoAttributes; - - bool operator==(const Attribute& other) const - { - return foreground_color == other.foreground_color && background_color == other.background_color && flags == other.flags; - } - bool operator!=(const Attribute& other) const - { - return !(*this == other); - } - }; - - struct Line { - explicit Line(u16 columns); - ~Line(); - void clear(Attribute); - bool has_only_one_background_color() const; - void set_length(u16); - u8* characters { nullptr }; - Attribute* attributes { nullptr }; - bool dirty { false }; - u16 m_length { 0 }; - }; - Line& line(size_t index) - { - ASSERT(index < m_rows); - return m_lines[index]; - } - const Line& line(size_t index) const - { - ASSERT(index < m_rows); - return m_lines[index]; - } - - NonnullOwnPtrVector m_lines; - - BufferPosition m_selection_start; - BufferPosition m_selection_end; - - int m_scroll_region_top { 0 }; - int m_scroll_region_bottom { 0 }; - - u16 m_columns { 0 }; - u16 m_rows { 0 }; - - u16 m_cursor_row { 0 }; - u16 m_cursor_column { 0 }; - u16 m_saved_cursor_row { 0 }; - u16 m_saved_cursor_column { 0 }; - bool m_stomp { false }; + VT::BufferPosition m_selection_start; + VT::BufferPosition m_selection_end; bool m_should_beep { false }; - - Attribute m_current_attribute; - - void execute_escape_sequence(u8 final); - void execute_xterm_command(); - - enum EscapeState { - Normal, - ExpectBracket, - ExpectParameter, - ExpectIntermediate, - ExpectFinal, - - ExpectXtermParameter1, - ExpectXtermParameter2, - ExpectXtermFinal, - }; - EscapeState m_escape_state { Normal }; - Vector m_parameters; - Vector m_intermediates; - Vector m_xterm_param1; - Vector m_xterm_param2; - Vector m_horizontal_tabs; - u8 m_final { 0 }; bool m_belling { false }; int m_pixel_width { 0 }; @@ -254,10 +76,7 @@ private: int m_ptm_fd { -1 }; - bool m_swallow_current { false }; - bool m_in_active_window { false }; - bool m_need_full_flush { false }; CNotifier m_notifier; @@ -270,6 +89,4 @@ private: CTimer m_cursor_blink_timer; CTimer m_visual_beep_timer; RefPtr m_config; - - u8 m_last_char { 0 }; }; diff --git a/Kernel/makeall.sh b/Kernel/makeall.sh index f43468f612..3db26c42fe 100755 --- a/Kernel/makeall.sh +++ b/Kernel/makeall.sh @@ -28,6 +28,7 @@ build_targets="$build_targets ../Servers/AudioServer" build_targets="$build_targets ../Servers/TTYServer" build_targets="$build_targets ../Libraries/LibAudio" build_targets="$build_targets ../Libraries/LibGUI" +build_targets="$build_targets ../Libraries/LibVT" build_targets="$build_targets ../Libraries/LibHTML" build_targets="$build_targets ../Userland" build_targets="$build_targets ../Applications/Terminal" diff --git a/Libraries/LibVT/Makefile b/Libraries/LibVT/Makefile new file mode 100644 index 0000000000..2370a93b12 --- /dev/null +++ b/Libraries/LibVT/Makefile @@ -0,0 +1,20 @@ +include ../../Makefile.common + +OBJS = \ + Terminal.o + +LIBRARY = libvt.a +DEFINES += -DUSERLAND + +all: $(LIBRARY) + +$(LIBRARY): $(OBJS) + @echo "LIB $@"; $(AR) rcs $@ $(OBJS) $(LIBS) + +.cpp.o: + @echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $< + +-include $(OBJS:%.o=%.d) + +clean: + @echo "CLEAN"; rm -f $(LIBRARY) $(OBJS) *.d diff --git a/Libraries/LibVT/Terminal.cpp b/Libraries/LibVT/Terminal.cpp new file mode 100644 index 0000000000..d0656667d3 --- /dev/null +++ b/Libraries/LibVT/Terminal.cpp @@ -0,0 +1,906 @@ +#include +#include + +namespace VT { + +u8 Attribute::default_foreground_color = 7; +u8 Attribute::default_background_color = 0; + +Terminal::Terminal(TerminalClient& client) + : m_client(client) +{ +} + +Terminal::~Terminal() +{ +} + +Terminal::Line::Line(u16 length) +{ + set_length(length); +} + +Terminal::Line::~Line() +{ + delete[] characters; + delete[] attributes; +} + +void Terminal::Line::set_length(u16 new_length) +{ + if (m_length == new_length) + return; + auto* new_characters = new u8[new_length]; + auto* new_attributes = new Attribute[new_length]; + memset(new_characters, ' ', new_length); + if (characters && attributes) { + memcpy(new_characters, characters, min(m_length, new_length)); + memcpy(new_attributes, attributes, min(m_length, new_length) * sizeof(Attribute)); + } + delete[] characters; + delete[] attributes; + characters = new_characters; + attributes = new_attributes; + m_length = new_length; +} + +void Terminal::Line::clear(Attribute attribute) +{ + if (dirty) { + memset(characters, ' ', m_length); + for (u16 i = 0; i < m_length; ++i) + attributes[i] = attribute; + return; + } + for (unsigned i = 0; i < m_length; ++i) { + if (characters[i] != ' ') + dirty = true; + characters[i] = ' '; + } + for (unsigned i = 0; i < m_length; ++i) { + if (attributes[i] != attribute) + dirty = true; + attributes[i] = attribute; + } +} + +bool Terminal::Line::has_only_one_background_color() const +{ + if (!m_length) + return true; + // FIXME: Cache this result? + auto color = attributes[0].background_color; + for (size_t i = 1; i < m_length; ++i) { + if (attributes[i].background_color != color) + return false; + } + return true; +} + +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(u8 ch) +{ + return ch >= 0x30 && ch <= 0x3f; +} + +inline bool is_valid_intermediate_character(u8 ch) +{ + return ch >= 0x20 && ch <= 0x2f; +} + +inline bool is_valid_final_character(u8 ch) +{ + return ch >= 0x40 && ch <= 0x7e; +} + +void Terminal::escape$h_l(bool should_set, bool question_param, const ParamVector& params) +{ + int mode = 2; + if (params.size() > 0) { + mode = params[0]; + } + if (!question_param) { + switch (mode) { + // FIXME: implement *something* for this + default: + unimplemented_escape(); + break; + } + } else { + switch (mode) { + case 25: + // Hide cursor command, but doesn't need to be run (for now, because + // we don't do inverse control codes anyways) + if (should_set) + dbgprintf("Terminal: Hide Cursor escapecode recieved. Not needed: ignored.\n"); + else + dbgprintf("Terminal: Show Cursor escapecode recieved. Not needed: ignored.\n"); + break; + default: + break; + } + } +} + +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: + m_current_attribute.flags |= Attribute::Bold; + break; + case 3: + m_current_attribute.flags |= Attribute::Italic; + break; + case 4: + m_current_attribute.flags |= Attribute::Underline; + break; + case 5: + m_current_attribute.flags |= Attribute::Blink; + break; + case 7: + m_current_attribute.flags |= Attribute::Negative; + break; + case 22: + m_current_attribute.flags &= ~Attribute::Bold; + break; + case 23: + m_current_attribute.flags &= ~Attribute::Italic; + break; + case 24: + m_current_attribute.flags &= ~Attribute::Underline; + break; + case 25: + m_current_attribute.flags &= ~Attribute::Blink; + break; + case 27: + m_current_attribute.flags &= ~Attribute::Negative; + break; + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: + // Foreground color + if (m_current_attribute.flags & Attribute::Bold) + param += 8; + m_current_attribute.foreground_color = param - 30; + break; + case 39: + // reset foreground + m_current_attribute.foreground_color = Attribute::default_foreground_color; + break; + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: + // Background color + if (m_current_attribute.flags & Attribute::Bold) + param += 8; + m_current_attribute.background_color = param - 40; + break; + case 49: + // reset background + m_current_attribute.background_color = Attribute::default_background_color; + break; + default: + dbgprintf("FIXME: escape$m: p: %u\n", param); + } + } +} + +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 (param count: %d)\n", params[0], params.size()); +} + +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]; + if ((bottom - top) < 2 || bottom > m_rows) { + dbgprintf("Error: escape$r: scrolling region invalid: %u-%u\n", top, bottom); + return; + } + m_scroll_region_top = top - 1; + m_scroll_region_bottom = bottom - 1; + set_cursor(0, 0); +} + +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$b(const ParamVector& params) +{ + if (params.size() < 1) + return; + + for (unsigned i = 0; i < params[0]; ++i) + put_character_at(m_cursor_row, m_cursor_column++, m_last_char); +} + +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: + // Clear the complete line + for (int i = 0; i < m_columns; ++i) { + put_character_at(m_cursor_row, i, ' '); + } + 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: + /// Clear from cursor to beginning of screen + for (int i = m_cursor_column - 1; i >= 0; --i) + put_character_at(m_cursor_row, i, ' '); + for (int row = m_cursor_row - 1; row >= 0; --row) { + for (int column = 0; column < m_columns; ++column) { + put_character_at(row, column, ' '); + } + } + break; + case 2: + clear(); + break; + case 3: + // FIXME: [3J should also clear the scrollback buffer. + clear(); + break; + default: + unimplemented_escape(); + break; + } +} + +void Terminal::escape$S(const ParamVector& params) +{ + int count = 1; + if (params.size() >= 1) + count = params[0]; + + for (u16 i = 0; i < count; i++) + scroll_up(); +} + +void Terminal::escape$T(const ParamVector& params) +{ + int count = 1; + if (params.size() >= 1) + count = params[0]; + + for (u16 i = 0; i < count; i++) + scroll_down(); +} + +void Terminal::escape$L(const ParamVector& params) +{ + int count = 1; + if (params.size() >= 1) + count = params[0]; + invalidate_cursor(); + for (; count > 0; --count) { + m_lines.insert(m_cursor_row + m_scroll_region_top, make(m_columns)); + if (m_scroll_region_bottom + 1 < m_lines.size()) + m_lines.remove(m_scroll_region_bottom + 1); + else + m_lines.remove(m_lines.size() - 1); + } + + m_need_full_flush = true; +} + +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_scroll_region_top + m_cursor_row); + count = min(count, max_count); + + for (int c = count; c > 0; --c) { + m_lines.remove(m_cursor_row + m_scroll_region_top); + if (m_scroll_region_bottom < m_lines.size()) + m_lines.insert(m_scroll_region_bottom, make(m_columns)); + else + m_lines.append(make(m_columns)); + } +} + +void Terminal::escape$P(const ParamVector& params) +{ + int num = 1; + if (params.size() >= 1) + num = params[0]; + + if (num == 0) + num = 1; + + auto& line = this->line(m_cursor_row); + + // Move n characters of line to the left + for (int i = m_cursor_column; i < line.m_length - num; i++) + line.characters[i] = line.characters[i + num]; + + // Fill remainder of line with blanks + for (int i = line.m_length - num; i < line.m_length; i++) + line.characters[i] = ' '; + + line.dirty = true; +} + +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: + m_client.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(u8 final) +{ + bool question_param = false; + m_final = final; + ParamVector params; + + if (m_parameters.size() > 0 && m_parameters[0] == '?') { + question_param = true; + m_parameters.remove(0); + } + auto paramparts = String::copy(m_parameters).split(';'); + for (auto& parampart : paramparts) { + bool ok; + unsigned value = parampart.to_uint(ok); + if (!ok) { + // FIXME: Should we do something else? + m_parameters.clear_with_capacity(); + m_intermediates.clear_with_capacity(); + return; + } + params.append(value); + } + +#if defined(TERMINAL_DEBUG) + dbgprintf("Terminal::execute_escape_sequence: Handled final '%c'\n", final); + dbgprintf("Params: "); + for (auto& p : params) { + dbgprintf("%d ", p); + } + dbgprintf("\b\n"); +#endif + + 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 'P': + escape$P(params); + break; + case 'S': + escape$S(params); + break; + case 'T': + escape$T(params); + break; + case 'L': + escape$L(params); + break; + case 'G': + escape$G(params); + break; + case 'X': + escape$X(params); + break; + case 'b': + escape$b(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; + case 'l': + escape$h_l(true, question_param, params); + break; + case 'h': + escape$h_l(false, question_param, params); + break; + default: + dbgprintf("Terminal::execute_escape_sequence: Unhandled final '%c'\n", final); + break; + } + +#if defined(TERMINAL_DEBUG) + dbgprintf("\n"); + for (auto& line : m_lines) { + dbgprintf("Terminal: Line: "); + for (int i = 0; i < line->length; i++) { + dbgprintf("%c", line->characters[i]); + } + dbgprintf("\n"); + } +#endif + + m_parameters.clear_with_capacity(); + m_intermediates.clear_with_capacity(); +} + +void Terminal::newline() +{ + u16 new_row = m_cursor_row; + if (m_cursor_row == m_scroll_region_bottom) { + scroll_up(); + } else { + ++new_row; + } + set_cursor(new_row, 0); +} + +void Terminal::scroll_up() +{ + // NOTE: We have to invalidate the cursor first. + invalidate_cursor(); + m_lines.remove(m_scroll_region_top); + m_lines.insert(m_scroll_region_bottom, make(m_columns)); + m_need_full_flush = true; +} + +void Terminal::scroll_down() +{ + // NOTE: We have to invalidate the cursor first. + invalidate_cursor(); + m_lines.remove(m_scroll_region_bottom); + m_lines.insert(m_scroll_region_top, make(m_columns)); + 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() - 1u) + m_stomp = false; + invalidate_cursor(); +} + +void Terminal::put_character_at(unsigned row, unsigned column, u8 ch) +{ + ASSERT(row < rows()); + ASSERT(column < columns()); + auto& line = this->line(row); + line.characters[column] = ch; + line.attributes[column] = m_current_attribute; + line.attributes[column].flags |= Attribute::Touched; + line.dirty = true; + + m_last_char = ch; +} + +void Terminal::on_char(u8 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': + m_client.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(u16 columns, u16 rows) +{ + if (columns == m_columns && rows == m_rows) + return; + +#if defined(TERMINAL_DEBUG) + dbgprintf("Terminal: RESIZE to: %d rows\n", rows); +#endif + + if (rows > m_rows) { + while (m_lines.size() < rows) + m_lines.append(make(columns)); + } else { + m_lines.shrink(rows); + } + + for (int i = 0; i < rows; ++i) + m_lines[i].set_length(columns); + + m_columns = columns; + m_rows = rows; + + m_scroll_region_top = 0; + m_scroll_region_bottom = rows - 1; + + m_cursor_row = min((int)m_cursor_row, m_rows - 1); + m_cursor_column = min((int)m_cursor_column, m_columns - 1); + m_saved_cursor_row = min((int)m_saved_cursor_row, m_rows - 1); + m_saved_cursor_column = min((int)m_saved_cursor_column, m_columns - 1); + + m_horizontal_tabs.resize(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_client.terminal_did_resize(m_columns, m_rows); +} + +void Terminal::invalidate_cursor() +{ + line(m_cursor_row).dirty = true; +} + +} diff --git a/Libraries/LibVT/Terminal.h b/Libraries/LibVT/Terminal.h new file mode 100644 index 0000000000..ca523ebbc7 --- /dev/null +++ b/Libraries/LibVT/Terminal.h @@ -0,0 +1,224 @@ +#pragma once + +#include +#include +#include + +namespace VT { + +class TerminalClient { +public: + virtual ~TerminalClient() {} + + virtual void beep() = 0; + virtual void set_window_title(const StringView&) = 0; + virtual void terminal_did_resize(u16 columns, u16 rows) = 0; +}; + +struct Attribute { + Attribute() { reset(); } + + static u8 default_foreground_color; + static u8 default_background_color; + + void reset() + { + foreground_color = default_foreground_color; + background_color = default_background_color; + flags = Flags::NoAttributes; + } + u8 foreground_color; + u8 background_color; + + enum Flags { + NoAttributes = 0x00, + Bold = 0x01, + Italic = 0x02, + Underline = 0x04, + Negative = 0x08, + Blink = 0x10, + Touched = 0x20, + }; + + bool is_untouched() const { return !(flags & Touched); } + + // TODO: it would be really nice if we had a helper for enums that + // exposed bit ops for class enums... + int flags = Flags::NoAttributes; + + bool operator==(const Attribute& other) const + { + return foreground_color == other.foreground_color && background_color == other.background_color && flags == other.flags; + } + bool operator!=(const Attribute& other) const + { + return !(*this == other); + } +}; + +class BufferPosition { +public: + BufferPosition() {} + BufferPosition(int row, int column) + : m_row(row) + , m_column(column) + { + } + + bool is_valid() const { return m_row >= 0 && m_column >= 0; } + int row() const { return m_row; } + int column() const { return m_column; } + + bool operator<(const BufferPosition& other) const + { + return m_row < other.m_row || (m_row == other.m_row && m_column < other.m_column); + } + + bool operator<=(const BufferPosition& other) const + { + return *this < other || *this == other; + } + + bool operator>=(const BufferPosition& other) const + { + return !(*this < other); + } + + bool operator==(const BufferPosition& other) const + { + return m_row == other.m_row && m_column == other.m_column; + } + + bool operator!=(const BufferPosition& other) const + { + return !(*this == other); + } + +private: + int m_row { -1 }; + int m_column { -1 }; +}; + +class Terminal { +public: + explicit Terminal(TerminalClient&); + ~Terminal(); + + bool m_need_full_flush { false }; + + void invalidate_cursor(); + void on_char(u8); + + void clear(); + void set_size(u16 columns, u16 rows); + u16 columns() const { return m_columns; } + u16 rows() const { return m_rows; } + + u16 cursor_column() const { return m_cursor_column; } + u16 cursor_row() const { return m_cursor_row; } + + struct Line { + explicit Line(u16 columns); + ~Line(); + void clear(Attribute); + bool has_only_one_background_color() const; + void set_length(u16); + u8* characters { nullptr }; + Attribute* attributes { nullptr }; + bool dirty { false }; + u16 m_length { 0 }; + }; + + Line& line(size_t index) + { + ASSERT(index < m_rows); + return m_lines[index]; + } + const Line& line(size_t index) const + { + ASSERT(index < m_rows); + return m_lines[index]; + } + +private: + typedef Vector ParamVector; + + void scroll_up(); + void scroll_down(); + void newline(); + void set_cursor(unsigned row, unsigned column); + void put_character_at(unsigned row, unsigned column, u8 ch); + void set_window_title(const String&); + + void inject_string(const String&); + void unimplemented_escape(); + void unimplemented_xterm_escape(); + + void escape$A(const ParamVector&); + void escape$B(const ParamVector&); + void escape$C(const ParamVector&); + void escape$D(const ParamVector&); + void escape$H(const ParamVector&); + void escape$J(const ParamVector&); + void escape$K(const ParamVector&); + void escape$M(const ParamVector&); + void escape$P(const ParamVector&); + void escape$G(const ParamVector&); + void escape$X(const ParamVector&); + void escape$b(const ParamVector&); + void escape$d(const ParamVector&); + void escape$m(const ParamVector&); + void escape$s(const ParamVector&); + void escape$u(const ParamVector&); + void escape$t(const ParamVector&); + void escape$r(const ParamVector&); + void escape$S(const ParamVector&); + void escape$T(const ParamVector&); + void escape$L(const ParamVector&); + void escape$h_l(bool, bool, const ParamVector&); + + TerminalClient& m_client; + + NonnullOwnPtrVector m_lines; + + int m_scroll_region_top { 0 }; + int m_scroll_region_bottom { 0 }; + + u16 m_columns { 0 }; + u16 m_rows { 0 }; + + u16 m_cursor_row { 0 }; + u16 m_cursor_column { 0 }; + u16 m_saved_cursor_row { 0 }; + u16 m_saved_cursor_column { 0 }; + bool m_swallow_current { false }; + bool m_stomp { false }; + + Attribute m_current_attribute; + + void execute_escape_sequence(u8 final); + void execute_xterm_command(); + + enum EscapeState { + Normal, + ExpectBracket, + ExpectParameter, + ExpectIntermediate, + ExpectFinal, + + ExpectXtermParameter1, + ExpectXtermParameter2, + ExpectXtermFinal, + }; + EscapeState m_escape_state { Normal }; + Vector m_parameters; + Vector m_intermediates; + Vector m_xterm_param1; + Vector m_xterm_param2; + Vector m_horizontal_tabs; + u8 m_final { 0 }; + + u8 m_last_char { 0 }; +}; + +} diff --git a/Makefile.common b/Makefile.common index c4bbac01c1..e8e973920d 100644 --- a/Makefile.common +++ b/Makefile.common @@ -22,6 +22,7 @@ LDFLAGS = \ -L$(SERENITY_BASE_DIR)/Libraries/LibM \ -L$(SERENITY_BASE_DIR)/Libraries/LibDraw \ -L$(SERENITY_BASE_DIR)/Libraries/LibGUI \ + -L$(SERENITY_BASE_DIR)/Libraries/LibVT \ -L$(SERENITY_BASE_DIR)/Libraries/LibAudio CLANG_FLAGS = -Wconsumed -m32 -ffreestanding -march=i686