diff --git a/CMakeLists.txt b/CMakeLists.txt index dd08c362..4d9d1e48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,3 +52,4 @@ add_subdirectory(apps) add_subdirectory(tests) add_subdirectory(shell) add_subdirectory(wind) +add_subdirectory(terminal) diff --git a/base/etc/user/01-gclient b/base/etc/user/01-gclient deleted file mode 100644 index fcd404ac..00000000 --- a/base/etc/user/01-gclient +++ /dev/null @@ -1,3 +0,0 @@ -Name=gclient -Description=Sample user application. -Command=/usr/bin/gclient diff --git a/base/etc/user/01-terminal b/base/etc/user/01-terminal new file mode 100644 index 00000000..2e93a0e6 --- /dev/null +++ b/base/etc/user/01-terminal @@ -0,0 +1,3 @@ +Name=terminal +Description=Start the terminal. +Command=/usr/bin/terminal diff --git a/terminal/CMakeLists.txt b/terminal/CMakeLists.txt new file mode 100644 index 00000000..d9ebbc1f --- /dev/null +++ b/terminal/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCES + main.cpp + TerminalWidget.h + TerminalWidget.cpp +) + +add_executable(terminal ${SOURCES}) +target_compile_options(terminal PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings) +add_dependencies(terminal libc) +target_include_directories(terminal PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR}) +target_link_libraries(terminal PRIVATE os ui) +install(TARGETS terminal DESTINATION ${LUNA_BASE}/usr/bin) diff --git a/terminal/TerminalWidget.cpp b/terminal/TerminalWidget.cpp new file mode 100644 index 00000000..6587fa0a --- /dev/null +++ b/terminal/TerminalWidget.cpp @@ -0,0 +1,441 @@ +#include "TerminalWidget.h" +#include +#include +#include +#include +#include +#include +#include + +static constexpr auto RED = ui::Color::from_u32(0xffcd0000); +static constexpr auto GREEN = ui::Color::from_u32(0xff00cd00); +static constexpr auto YELLOW = ui::Color::from_u32(0xffcdcd00); +static constexpr auto BLUE = ui::Color::from_u32(0xff0000ee); +static constexpr auto MAGENTA = ui::Color::from_u32(0xffcd00cd); +static constexpr auto CYAN = ui::Color::from_u32(0xff00cdcd); +static constexpr auto GRAY = ui::Color::from_u32(0xffe5e5e5); + +static constexpr auto BRIGHT_BLACK = ui::Color::from_u32(0xff7f7f7f); +static constexpr auto BRIGHT_RED = ui::Color::from_u32(0xffff0000); +static constexpr auto BRIGHT_GREEN = ui::Color::from_u32(0xff00ff00); +static constexpr auto BRIGHT_YELLOW = ui::Color::from_u32(0xffffff00); +static constexpr auto BRIGHT_BLUE = ui::Color::from_u32(0xff5c5cff); +static constexpr auto BRIGHT_MAGENTA = ui::Color::from_u32(0xffff00ff); +static constexpr auto BRIGHT_CYAN = ui::Color::from_u32(0xff00ffff); +static constexpr auto BRIGHT_GRAY = ui::Color::from_u32(0xffffffff); + +static void sigchld_handler(int) +{ + wait(NULL); + ui::App::the().set_should_close(true); +} + +Result TerminalWidget::init(char* const* args) +{ + m_font = ui::Font::default_font(); + m_bold_font = ui::Font::default_bold_font(); + + m_terminal_canvas = ui::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 infds[2]; + int outfds[2]; + + int result = pipe(infds); + if (result < 0) return err(errno); + + result = pipe(outfds); + if (result < 0) return err(errno); + + pid_t child = TRY(os::Process::fork()); + if (child == 0) + { + dup2(infds[0], STDIN_FILENO); + dup2(outfds[1], STDOUT_FILENO); + dup2(outfds[1], STDERR_FILENO); + + close(infds[0]); + close(infds[1]); + close(outfds[0]); + close(outfds[1]); + + execv(args[0], args); + _exit(127); + } + + close(infds[0]); + close(outfds[1]); + + m_write_fd = infds[1]; + m_read_fd = outfds[0]; + + fcntl(m_read_fd, F_SETFL, O_NONBLOCK); + + m_child_pid = child; + + return {}; +} + +Result TerminalWidget::draw(ui::Canvas& canvas) +{ + canvas.fill((u32*)m_terminal_canvas.ptr, m_terminal_canvas.stride); + return {}; +} + +Result TerminalWidget::process() +{ + char buffer[BUFSIZ]; + ssize_t nread = read(m_read_fd, buffer, BUFSIZ); + if (nread < 0) + { + if (errno == EAGAIN) return {}; + return err(errno); + } + + os::eprintln("terminal: read %zd characters, processing...", nread); + + for (ssize_t i = 0; i < nread; i++) TRY(putchar(buffer[i])); + + ui::App::the().main_window()->draw(); + + return {}; +} + +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() }); +} + +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_y_position -= m_font->width(); +} + +void TerminalWidget::erase_current_char() +{ + m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::BLACK); +} + +void TerminalWidget::draw_cursor() +{ + m_terminal_canvas.subcanvas({ m_x_position, m_y_position, m_font->width(), m_font->height() }).fill(ui::WHITE); +} + +bool TerminalWidget::at_end_of_screen() +{ + return (m_x_position + m_font->width()) > m_rect.width; +} + +bool TerminalWidget::handle_escape_sequence(wchar_t c) +{ + auto rc = m_escape_parser->advance(static_cast(c)); + if (rc.has_error()) + { + m_escape_parser = Option {}; + return false; + } + if (!rc.value()) return true; + if (!m_escape_parser->valid()) + { + m_escape_parser = Option {}; + return false; + } + + const auto& params = m_escape_parser->parameters(); + switch (m_escape_parser->code()) + { + case EscapeCode::CursorUp: { + int lines = params.size() ? params[0] : 1; + int pixels = lines * m_font->height(); + if (pixels > m_y_position) m_y_position = 0; + else + m_y_position -= pixels; + }; + break; + case EscapeCode::CursorDown: { + int lines = params.size() ? params[0] : 1; + int pixels = lines * m_font->height(); + if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height(); + else + m_y_position += pixels; + }; + break; + case EscapeCode::CursorBack: { + int chars = params.size() ? params[0] : 1; + int pixels = chars * m_font->width(); + if (pixels > m_x_position) m_x_position = 0; + else + m_x_position -= pixels; + }; + break; + case EscapeCode::CursorForward: { + int chars = params.size() ? params[0] : 1; + int pixels = chars * m_font->width(); + if (pixels + m_x_position >= m_rect.width) m_x_position = m_rect.width - m_font->width(); + else + m_x_position += pixels; + }; + break; + case EscapeCode::CursorNextLine: { + int lines = params.size() ? params[0] : 1; + int pixels = lines * m_font->height(); + if (pixels > m_y_position) m_y_position = 0; + else + m_y_position -= pixels; + m_x_position = 0; + }; + break; + case EscapeCode::CursorPreviousLine: { + int lines = params.size() ? params[0] : 1; + int pixels = lines * m_font->height(); + if (pixels + m_y_position >= m_rect.height) m_y_position = m_rect.height - m_font->height(); + else + m_y_position += pixels; + m_x_position = 0; + }; + break; + case EscapeCode::CursorHorizontalAbsolute: { + int line = (params.size() ? params[0] : 1) - 1; + if (line < 0) break; + int position = line * m_font->height(); + if (position >= m_rect.height) position = m_rect.height - m_font->height(); + m_y_position = position; + }; + break; + case EscapeCode::SetCursorPosition: { + int x = (params.size() ? params[0] : 1) - 1; + int y = (params.size() > 1 ? params[1] : 1) - 1; + if (x < 0 || y < 0) break; + int x_position = x * m_font->width(); + if (x_position >= m_rect.width) x_position = m_rect.width - m_font->height(); + m_x_position = x_position; + int y_position = y * m_font->height(); + if (y_position >= m_rect.height) y_position = m_rect.height - m_font->height(); + m_y_position = y_position; + }; + break; + case EscapeCode::SelectGraphicRendition: { + if (!params.size()) + { + m_foreground_color = ui::WHITE; + m_background_color = ui::BLACK; + m_bold = false; + break; + } + + for (usize i = 0; i < params.size(); i++) + { + int arg = params[i]; + switch (arg) + { + case 0: { + m_foreground_color = ui::BLACK; + m_background_color = ui::WHITE; + m_bold = false; + break; + } + case 1: { + m_bold = true; + break; + } + case 22: { + m_bold = false; + break; + } + case 30: { + m_foreground_color = m_bold ? BRIGHT_BLACK : ui::BLACK; + break; + } + case 31: { + m_foreground_color = m_bold ? BRIGHT_RED : RED; + break; + } + case 32: { + m_foreground_color = m_bold ? BRIGHT_GREEN : GREEN; + break; + } + case 33: { + m_foreground_color = m_bold ? BRIGHT_YELLOW : YELLOW; + break; + } + case 34: { + m_foreground_color = m_bold ? BRIGHT_BLUE : BLUE; + break; + } + case 35: { + m_foreground_color = m_bold ? BRIGHT_MAGENTA : MAGENTA; + break; + } + case 36: { + m_foreground_color = m_bold ? BRIGHT_CYAN : CYAN; + break; + } + case 37: { + m_foreground_color = m_bold ? BRIGHT_GRAY : GRAY; + break; + } + case 39: { + m_foreground_color = ui::WHITE; + break; + } + case 40: { + m_background_color = m_bold ? BRIGHT_BLACK : ui::BLACK; + break; + } + case 41: { + m_background_color = m_bold ? BRIGHT_RED : RED; + break; + } + case 42: { + m_background_color = m_bold ? BRIGHT_GREEN : GREEN; + break; + } + case 43: { + m_background_color = m_bold ? BRIGHT_YELLOW : YELLOW; + break; + } + case 44: { + m_background_color = m_bold ? BRIGHT_BLUE : BLUE; + break; + } + case 45: { + m_background_color = m_bold ? BRIGHT_MAGENTA : MAGENTA; + break; + } + case 46: { + m_background_color = m_bold ? BRIGHT_CYAN : CYAN; + break; + } + case 47: { + m_background_color = m_bold ? BRIGHT_GRAY : GRAY; + break; + } + case 49: { + m_background_color = ui::BLACK; + break; + } + default: break; + } + } + } + break; + default: break; + } + + m_escape_parser = Option {}; + return true; +} + +Result TerminalWidget::putchar(char c) +{ + auto guard = make_scope_guard([this] { m_decoder.reset(); }); + + bool is_ready = TRY(m_decoder.feed(c)); + + 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_enabled) 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); +} diff --git a/terminal/TerminalWidget.h b/terminal/TerminalWidget.h new file mode 100644 index 00000000..aaf26c5b --- /dev/null +++ b/terminal/TerminalWidget.h @@ -0,0 +1,60 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class TerminalWidget : public ui::Widget +{ + public: + Result init(char* const* args); + + Result draw(ui::Canvas& canvas) override; + + Result process(); + + void quit(); + + private: + ui::Canvas m_terminal_canvas; + Vector m_line_buffer; + int m_write_fd; + int m_read_fd; + pid_t m_child_pid; + + SharedPtr m_font; + SharedPtr m_bold_font; + + static constexpr int CURSOR_TIMEOUT = 500; + + int m_current_cursor_timeout = CURSOR_TIMEOUT; + bool m_cursor_activated = true; + bool m_cursor_enabled = false; + + int m_x_position { 0 }; + int m_y_position { 0 }; + + bool m_bold { false }; + + ui::Color m_foreground_color { ui::WHITE }; + ui::Color m_background_color { ui::BLACK }; + + Utf8StateDecoder m_decoder; + Option m_escape_parser; + + void draw_glyph(wchar_t c, int x, int y); + void erase_current_line(); + void scroll(); + bool should_scroll(); + void next_line(); + void next_char(); + void prev_char(); + void erase_current_char(); + void draw_cursor(); + bool at_end_of_screen(); + bool handle_escape_sequence(wchar_t c); + Result putchar(char c); + void put_code_point(wchar_t c); +}; diff --git a/terminal/main.cpp b/terminal/main.cpp new file mode 100644 index 00000000..20986d35 --- /dev/null +++ b/terminal/main.cpp @@ -0,0 +1,29 @@ +#include "TerminalWidget.h" +#include +#include + +Result luna_main(int argc, char** argv) +{ + ui::App app; + TRY(app.init(argc, argv)); + app.set_nonblocking(); + + auto* window = TRY(ui::Window::create(ui::Rect { 150, 150, 500, 300 })); + app.set_main_window(window); + window->set_background(ui::BLACK); + window->set_title("Terminal"); + + TerminalWidget terminal; + window->set_main_widget(terminal); + + char* args[] = { "/bin/sh", "-i", nullptr }; + TRY(terminal.init(args)); + + window->draw(); + + while (app.process_events()) TRY(terminal.process()); + + terminal.quit(); + + return 0; +} diff --git a/wind/main.cpp b/wind/main.cpp index 3986a90e..4354f7f5 100644 --- a/wind/main.cpp +++ b/wind/main.cpp @@ -122,7 +122,7 @@ Result luna_main(int argc, char** argv) StringView args[] = { "/usr/bin/init"_sv, "--user"_sv }; TRY(os::Process::spawn("/usr/bin/init"_sv, Slice { args, 2 }, false)); - ui::Color background = ui::BLACK; + ui::Color background = ui::Color::from_rgb(0x10, 0x10, 0x10); Vector> clients; Vector fds;