ladybird/Applications/Terminal/Terminal.cpp
Andreas Kling 6a5d92f0ad WindowServer+LibGUI: Allow changing whether windows have alpha channels.
Use this in Terminal to tell the window server to not bother with the alpha
channel in the backing store if we're running without transparency.
Semi-transparent terminals look neat but they slow everything down, so this
keeps things fast while making it easy to switch to the flashy mode. :^)
2019-05-03 21:07:16 +02:00

897 lines
23 KiB
C++

#include "Terminal.h"
#include "XtermColors.h"
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <AK/AKString.h>
#include <AK/StringBuilder.h>
#include <SharedGraphics/Font.h>
#include <LibGUI/GPainter.h>
#include <AK/StdLibExtras.h>
#include <LibGUI/GApplication.h>
#include <LibGUI/GWindow.h>
#include <Kernel/KeyCode.h>
#include <sys/ioctl.h>
//#define TERMINAL_DEBUG
Terminal::Terminal(int ptm_fd)
: m_ptm_fd(ptm_fd)
, m_notifier(ptm_fd, CNotifier::Read)
{
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;
}
unsigned parse_uint(const String& str, bool& ok)
{
unsigned value = 0;
for (int i = 0; i < str.length(); ++i) {
if (str[i] < '0' || str[i] > '9') {
ok = false;
return 0;
}
value = value * 10;
value += str[i] - '0';
}
ok = true;
return value;
}
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: <esc>[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 = parse_uint(String::copy(m_xterm_param1), 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 = parse_uint(parampart, 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':
// FIXME: Bell!
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<byte*>(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 = m_columns * font().glyph_width('x') + m_inset * 2;
m_pixel_height = (m_rows * (font().glyph_height() + m_line_spacing)) + (m_inset * 2) - 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 + m_inset, y + m_inset, font().glyph_width('x'), font().glyph_height() };
}
Rect Terminal::row_rect(word row)
{
int y = row * m_line_height;
Rect rect = { m_inset, y + 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;
default:
write(m_ptm_fd, &ch, 1);
break;
}
}
void Terminal::paint_event(GPaintEvent&)
{
GPainter painter(*this);
if (m_needs_background_fill) {
m_needs_background_fill = false;
painter.fill_rect(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;
fast_dword_copy(
painter.target()->scanline(first_scanline),
painter.target()->scanline(second_scanline),
scanlines_to_copy * m_pixel_width
);
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(rect(), Color::Red);
}
void Terminal::set_window_title(String&& title)
{
auto* w = window();
if (!w)
return;
w->set_title(move(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()
{
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() / font().glyph_width('x');
int new_rows = event.size().height() / 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({ m_inset * 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();
}