Luna/terminal/TerminalWidget.cpp
apio ea3907d012
All checks were successful
continuous-integration/drone/pr Build is passing
terminal: Add cursor support
2023-09-16 13:12:39 +02:00

606 lines
17 KiB
C++

#include "TerminalWidget.h"
#include <ctype.h>
#include <errno.h>
#include <luna/CType.h>
#include <os/File.h>
#include <os/Process.h>
#include <signal.h>
#include <stdlib.h>
#include <time.h>
#include <ui/App.h>
#include <unistd.h>
static constexpr auto RED = ui::Color::from_u32(0xffcd0000);
static constexpr auto GREEN = ui::Color::from_u32(0xff00cd00);
static constexpr auto YELLOW = ui::Color::from_u32(0xffcdcd00);
static constexpr auto BLUE = ui::Color::from_u32(0xff0000ee);
static constexpr auto MAGENTA = ui::Color::from_u32(0xffcd00cd);
static constexpr auto CYAN = ui::Color::from_u32(0xff00cdcd);
static constexpr auto GRAY = ui::Color::from_u32(0xffe5e5e5);
static constexpr auto BRIGHT_BLACK = ui::Color::from_u32(0xff7f7f7f);
static constexpr auto BRIGHT_RED = ui::Color::from_u32(0xffff0000);
static constexpr auto BRIGHT_GREEN = ui::Color::from_u32(0xff00ff00);
static constexpr auto BRIGHT_YELLOW = ui::Color::from_u32(0xffffff00);
static constexpr auto BRIGHT_BLUE = ui::Color::from_u32(0xff5c5cff);
static constexpr auto BRIGHT_MAGENTA = ui::Color::from_u32(0xffff00ff);
static constexpr auto BRIGHT_CYAN = ui::Color::from_u32(0xff00ffff);
static constexpr auto BRIGHT_GRAY = ui::Color::from_u32(0xffffffff);
static long get_time_in_milliseconds()
{
struct timespec ts;
check(clock_gettime(CLOCK_REALTIME, &ts) == 0);
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static void sigchld_handler(int)
{
wait(NULL);
ui::App::the().set_should_close(true);
}
Result<void> TerminalWidget::init(char* const* args)
{
m_font = ui::Font::default_font();
m_bold_font = ui::Font::default_bold_font();
m_terminal_canvas = ui::Canvas { .width = m_rect.width,
.height = m_rect.height,
.stride = m_rect.width,
.ptr = (u8*)TRY(calloc_impl(m_rect.width, m_rect.height * sizeof(u32), false)) };
signal(SIGCHLD, sigchld_handler);
int fd = posix_openpt(O_RDWR | O_CLOEXEC);
if (fd < 0) return err(errno);
grantpt(fd);
unlockpt(fd);
pid_t child = TRY(os::Process::fork());
if (child == 0)
{
int ptsfd = open(ptsname(fd), O_RDWR);
dup2(ptsfd, STDIN_FILENO);
dup2(ptsfd, STDOUT_FILENO);
dup2(ptsfd, STDERR_FILENO);
setpgid(0, 0);
tcsetpgrp(ptsfd, getpid());
close(ptsfd);
execv(args[0], args);
_exit(127);
}
m_pty = fd;
fcntl(fd, F_SETFL, O_NONBLOCK);
m_child_pid = child;
m_last_cursor_tick = get_time_in_milliseconds();
return {};
}
Result<ui::EventResult> TerminalWidget::handle_key_event(const ui::KeyEventRequest& request)
{
if (!request.pressed) return ui::EventResult::DidNotHandle;
query_termios();
bool is_special_character { false };
if (/*is_canonical()*/ true)
{
if (request.letter == m_settings.c_cc[VERASE])
{
auto maybe_char = m_line_buffer.try_pop();
if (maybe_char.has_value() && maybe_char.value())
{
if ((m_settings.c_lflag & ECHO) && (m_settings.c_lflag & ECHOE))
{
put_code_point(L'\b');
if (_iscntrl(maybe_char.value())) put_code_point(L'\b');
if (maybe_char.value() == '\t')
{
put_code_point(L'\b');
put_code_point(L'\b');
}
}
ui::App::the().main_window()->draw();
return ui::EventResult::DidHandle;
}
if ((m_settings.c_lflag & ECHOE)) return ui::EventResult::DidHandle;
else
is_special_character = true;
}
if (request.letter == m_settings.c_cc[VEOF])
{
write(m_pty, m_line_buffer.data(), m_line_buffer.size());
m_line_buffer.clear();
// FIXME: tell the kernel that process may read without blocking.
is_special_character = true;
}
if (m_settings.c_lflag & ISIG)
{
if (request.letter == m_settings.c_cc[VINTR])
{
if (!(m_settings.c_lflag & NOFLSH)) m_line_buffer.clear();
// FIXME: Send SIGINT.
/*if (m_foreground_process_group.has_value())
{
Scheduler::for_each_in_process_group(m_foreground_process_group.value(), [](Thread* thread) {
thread->send_signal(SIGINT);
return true;
});
}*/
is_special_character = true;
}
if (request.letter == m_settings.c_cc[VQUIT])
{
if (!(m_settings.c_lflag & NOFLSH)) m_line_buffer.clear();
// FIXME: Send SIGINT.
/*if (m_foreground_process_group.has_value())
{
Scheduler::for_each_in_process_group(m_foreground_process_group.value(), [](Thread* thread) {
thread->send_signal(SIGQUIT);
return true;
});
}*/
is_special_character = true;
}
}
}
if (!is_special_character)
{
if (/*is_canonical()*/ true)
{
m_line_buffer.try_append((u8)request.letter);
if (request.letter == '\n')
{
write(m_pty, m_line_buffer.data(), m_line_buffer.size());
m_line_buffer.clear();
}
}
else
write(m_pty, &request.letter, 1);
}
if (!(m_settings.c_lflag & ECHO)) return ui::EventResult::DidHandle;
if (_iscntrl(request.letter))
{
if (m_settings.c_lflag & ECHOCTL)
{
bool should_echo = true;
if (request.letter == '\n' || request.letter == '\t' || request.letter == '\0' ||
request.letter == m_settings.c_cc[VEOF])
should_echo = false;
if (should_echo)
{
char caret_notation[3] = { '^', '\0', '\0' };
if (request.letter == 0x7f) caret_notation[1] = '?';
else
caret_notation[1] = request.letter + 0x40;
for (int i = 0; i < 2; i++) { putchar(caret_notation[i]); }
}
else
putchar(request.letter);
}
}
else
putchar(request.letter);
ui::App::the().main_window()->draw();
return ui::EventResult::DidHandle;
}
Result<void> TerminalWidget::draw(ui::Canvas& canvas)
{
canvas.fill((u32*)m_terminal_canvas.ptr, m_terminal_canvas.stride);
return {};
}
Result<void> TerminalWidget::process()
{
char buffer[BUFSIZ];
ssize_t nread = read(m_pty, buffer, BUFSIZ);
if (nread < 0)
{
if (errno == EAGAIN) nread = 0;
else
return err(errno);
}
query_termios();
bool should_update_cursor = tick_cursor();
for (ssize_t i = 0; i < nread; i++) TRY(putchar(buffer[i]));
if (should_update_cursor || nread > 0) ui::App::the().main_window()->draw();
return {};
}
bool TerminalWidget::tick_cursor()
{
if (!m_cursor_enabled) return false;
long now = get_time_in_milliseconds();
long diff = now - m_last_cursor_tick;
m_last_cursor_tick = now;
m_current_cursor_timeout -= (int)diff;
if (m_current_cursor_timeout <= 0)
{
m_current_cursor_timeout = CURSOR_TIMEOUT;
m_cursor_activated = !m_cursor_activated;
if (m_cursor_activated) draw_cursor();
else
erase_current_char();
return true;
}
return false;
}
void TerminalWidget::query_termios()
{
tcgetattr(m_pty, &m_settings);
}
void TerminalWidget::draw_glyph(wchar_t c, int x, int y)
{
auto subcanvas = m_terminal_canvas.subcanvas({ x, y, m_font->width(), m_font->height() });
subcanvas.fill(m_background_color);
(m_bold ? m_bold_font : m_font)->render(c, m_foreground_color, subcanvas);
}
void TerminalWidget::erase_current_line()
{
m_terminal_canvas.subcanvas({ 0, m_y_position, m_rect.width, m_font->height() }).fill(ui::BLACK);
}
void TerminalWidget::scroll()
{
memcpy(m_terminal_canvas.ptr, m_terminal_canvas.ptr + (m_rect.width * sizeof(u32) * m_font->height()),
(m_rect.width * m_rect.height * sizeof(u32)) - (m_rect.width * sizeof(u32) * m_font->height()));
m_y_position -= m_font->height();
erase_current_line();
}
bool TerminalWidget::should_scroll()
{
return m_y_position >= m_rect.height;
}
void TerminalWidget::next_line()
{
m_x_position = 0;
m_y_position += m_font->height();
}
void TerminalWidget::next_char()
{
m_x_position += m_font->width();
}
void TerminalWidget::prev_char()
{
m_x_position -= m_font->width();
}
void TerminalWidget::erase_current_char()
{
m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::BLACK);
}
void TerminalWidget::draw_cursor()
{
m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::WHITE);
}
bool TerminalWidget::at_end_of_screen()
{
return (m_x_position + m_font->width()) > m_rect.width;
}
bool TerminalWidget::handle_escape_sequence(wchar_t c)
{
auto rc = m_escape_parser->advance(static_cast<u8>(c));
if (rc.has_error())
{
m_escape_parser = Option<EscapeSequenceParser> {};
return false;
}
if (!rc.value()) return true;
if (!m_escape_parser->valid())
{
m_escape_parser = Option<EscapeSequenceParser> {};
return false;
}
const auto& params = m_escape_parser->parameters();
switch (m_escape_parser->code())
{
case EscapeCode::CursorUp: {
int lines = params.size() ? params[0] : 1;
int pixels = lines * m_font->height();
if (pixels > m_y_position) m_y_position = 0;
else
m_y_position -= pixels;
};
break;
case EscapeCode::CursorDown: {
int lines = params.size() ? params[0] : 1;
int pixels = lines * m_font->height();
if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height();
else
m_y_position += pixels;
};
break;
case EscapeCode::CursorBack: {
int chars = params.size() ? params[0] : 1;
int pixels = chars * m_font->width();
if (pixels > m_x_position) m_x_position = 0;
else
m_x_position -= pixels;
};
break;
case EscapeCode::CursorForward: {
int chars = params.size() ? params[0] : 1;
int pixels = chars * m_font->width();
if (pixels + m_x_position >= m_rect.width) m_x_position = m_rect.width - m_font->width();
else
m_x_position += pixels;
};
break;
case EscapeCode::CursorNextLine: {
int lines = params.size() ? params[0] : 1;
int pixels = lines * m_font->height();
if (pixels > m_y_position) m_y_position = 0;
else
m_y_position -= pixels;
m_x_position = 0;
};
break;
case EscapeCode::CursorPreviousLine: {
int lines = params.size() ? params[0] : 1;
int pixels = lines * m_font->height();
if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height();
else
m_y_position += pixels;
m_x_position = 0;
};
break;
case EscapeCode::CursorHorizontalAbsolute: {
int line = (params.size() ? params[0] : 1) - 1;
if (line < 0) break;
int position = line * m_font->height();
if (position >= m_rect.height) position = m_rect.height - m_font->height();
m_y_position = position;
};
break;
case EscapeCode::SetCursorPosition: {
int x = (params.size() ? params[0] : 1) - 1;
int y = (params.size() > 1 ? params[1] : 1) - 1;
if (x < 0 || y < 0) break;
int x_position = x * m_font->width();
if (x_position >= m_rect.width) x_position = m_rect.width - m_font->height();
m_x_position = x_position;
int y_position = y * m_font->height();
if (y_position >= m_rect.height) y_position = m_rect.height - m_font->height();
m_y_position = y_position;
};
break;
case EscapeCode::SelectGraphicRendition: {
if (!params.size())
{
m_foreground_color = ui::WHITE;
m_background_color = ui::BLACK;
m_bold = false;
break;
}
for (usize i = 0; i < params.size(); i++)
{
int arg = params[i];
switch (arg)
{
case 0: {
m_foreground_color = ui::BLACK;
m_background_color = ui::WHITE;
m_bold = false;
break;
}
case 1: {
m_bold = true;
break;
}
case 22: {
m_bold = false;
break;
}
case 30: {
m_foreground_color = m_bold ? BRIGHT_BLACK : ui::BLACK;
break;
}
case 31: {
m_foreground_color = m_bold ? BRIGHT_RED : RED;
break;
}
case 32: {
m_foreground_color = m_bold ? BRIGHT_GREEN : GREEN;
break;
}
case 33: {
m_foreground_color = m_bold ? BRIGHT_YELLOW : YELLOW;
break;
}
case 34: {
m_foreground_color = m_bold ? BRIGHT_BLUE : BLUE;
break;
}
case 35: {
m_foreground_color = m_bold ? BRIGHT_MAGENTA : MAGENTA;
break;
}
case 36: {
m_foreground_color = m_bold ? BRIGHT_CYAN : CYAN;
break;
}
case 37: {
m_foreground_color = m_bold ? BRIGHT_GRAY : GRAY;
break;
}
case 39: {
m_foreground_color = ui::WHITE;
break;
}
case 40: {
m_background_color = m_bold ? BRIGHT_BLACK : ui::BLACK;
break;
}
case 41: {
m_background_color = m_bold ? BRIGHT_RED : RED;
break;
}
case 42: {
m_background_color = m_bold ? BRIGHT_GREEN : GREEN;
break;
}
case 43: {
m_background_color = m_bold ? BRIGHT_YELLOW : YELLOW;
break;
}
case 44: {
m_background_color = m_bold ? BRIGHT_BLUE : BLUE;
break;
}
case 45: {
m_background_color = m_bold ? BRIGHT_MAGENTA : MAGENTA;
break;
}
case 46: {
m_background_color = m_bold ? BRIGHT_CYAN : CYAN;
break;
}
case 47: {
m_background_color = m_bold ? BRIGHT_GRAY : GRAY;
break;
}
case 49: {
m_background_color = ui::BLACK;
break;
}
default: break;
}
}
}
break;
default: break;
}
m_escape_parser = Option<EscapeSequenceParser> {};
return true;
}
Result<void> TerminalWidget::putchar(char c)
{
auto guard = make_scope_guard([this] { m_decoder.reset(); });
bool is_ready = TRY(m_decoder.feed(c));
if (is_ready) put_code_point(TRY(m_decoder.extract()));
guard.deactivate();
return {};
}
void TerminalWidget::put_code_point(wchar_t c)
{
if (c > (wchar_t)255) c = (wchar_t)256;
if (m_escape_parser.has_value())
{
if (handle_escape_sequence(c)) return;
}
// Erase the current cursor.
if (m_cursor_activated) erase_current_char();
bool should_draw_cursor = m_cursor_enabled;
switch (c)
{
case L'\n': {
next_line();
if (should_scroll()) scroll();
break;
}
case L'\t': {
for (int i = 0; i < 4; i++) { put_code_point(L' '); }
break;
}
case L'\r': m_x_position = 0; break;
case L'\b':
if (m_x_position != 0)
{
prev_char();
erase_current_char();
}
break;
case L'\x1b':
case L'\x9b':
case L'\x90':
case L'\x9d':
m_escape_parser = EscapeSequenceParser { (u8)c };
should_draw_cursor = false;
break;
default: {
if (iscntrl(c)) return;
draw_glyph(c, m_x_position, m_y_position);
next_char();
if (at_end_of_screen())
{
next_line();
if (should_scroll()) scroll();
}
break;
}
}
if (should_draw_cursor)
{
m_current_cursor_timeout = CURSOR_TIMEOUT;
m_cursor_activated = true;
draw_cursor();
}
}
void TerminalWidget::quit()
{
kill(m_child_pid, SIGHUP);
}