From 843a6851c4240bad7769f184dabcd94c68c004ec Mon Sep 17 00:00:00 2001 From: Bananymous Date: Mon, 3 Jun 2024 18:04:33 +0300 Subject: [PATCH] Userspace: Start work on a terminal emulator Terminal is still missing some ANSI codes, cursor and pseudo terminal support. Shell's builtin start-gui now launches a Terminal instead of test windows. --- userspace/CMakeLists.txt | 1 + userspace/Shell/main.cpp | 4 +- userspace/Terminal/CMakeLists.txt | 17 ++ userspace/Terminal/Terminal.cpp | 380 ++++++++++++++++++++++++ userspace/Terminal/Terminal.h | 64 ++++ userspace/Terminal/main.cpp | 7 + userspace/WindowServer/WindowServer.cpp | 2 +- 7 files changed, 471 insertions(+), 4 deletions(-) create mode 100644 userspace/Terminal/CMakeLists.txt create mode 100644 userspace/Terminal/Terminal.cpp create mode 100644 userspace/Terminal/Terminal.h create mode 100644 userspace/Terminal/main.cpp diff --git a/userspace/CMakeLists.txt b/userspace/CMakeLists.txt index a0f7e208..ddcdeffa 100644 --- a/userspace/CMakeLists.txt +++ b/userspace/CMakeLists.txt @@ -30,6 +30,7 @@ set(USERSPACE_PROJECTS sudo sync tee + Terminal test test-framebuffer test-globals diff --git a/userspace/Shell/main.cpp b/userspace/Shell/main.cpp index 4fc75031..30abe0e5 100644 --- a/userspace/Shell/main.cpp +++ b/userspace/Shell/main.cpp @@ -337,9 +337,7 @@ BAN::Optional execute_builtin(BAN::Vector& args, int fd_in, in if (pid == 0) execl("/bin/WindowServer", "WindowServer", NULL); if (fork() == 0) - execl("/bin/test-window", "test-window", NULL); - if (fork() == 0) - execl("/bin/test-window", "test-window", NULL); + execl("/bin/Terminal", "Terminal", NULL); waitpid(pid, nullptr, 0); } else if (args.front() == "page-fault-test"sv) diff --git a/userspace/Terminal/CMakeLists.txt b/userspace/Terminal/CMakeLists.txt new file mode 100644 index 00000000..e8a08493 --- /dev/null +++ b/userspace/Terminal/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.26) + +project(Terminal CXX) + +set(SOURCES + main.cpp + Terminal.cpp +) + +add_executable(Terminal ${SOURCES}) +target_compile_options(Terminal PUBLIC -O2 -g) +target_link_libraries(Terminal PUBLIC libc ban libfont libgui libinput) + +add_custom_target(Terminal-install + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/Terminal ${BANAN_BIN}/ + DEPENDS Terminal +) diff --git a/userspace/Terminal/Terminal.cpp b/userspace/Terminal/Terminal.cpp new file mode 100644 index 00000000..44816be0 --- /dev/null +++ b/userspace/Terminal/Terminal.cpp @@ -0,0 +1,380 @@ +#include "Terminal.h" + +#include +#include + +#include +#include +#include +#include + +void Terminal::start_shell() +{ + int shell_stdin[2]; + if (pipe(shell_stdin) == -1) + { + dwarnln("pipe: {}", strerror(errno)); + exit(1); + } + + int shell_stdout[2]; + if (pipe(shell_stdout) == -1) + { + dwarnln("pipe: {}", strerror(errno)); + exit(1); + } + + int shell_stderr[2]; + if (pipe(shell_stderr) == -1) + { + dwarnln("pipe: {}", strerror(errno)); + exit(1); + } + + pid_t shell_pid = fork(); + if (shell_pid == 0) + { + if (dup2(shell_stdin[0], STDIN_FILENO) == -1) + { + dwarnln("dup2: {}", strerror(errno)); + exit(1); + } + close(shell_stdin[0]); + close(shell_stdin[1]); + + if (dup2(shell_stdout[1], STDOUT_FILENO) == -1) + { + dwarnln("dup2: {}", strerror(errno)); + exit(1); + } + close(shell_stdout[0]); + close(shell_stdout[1]); + + if (dup2(shell_stderr[1], STDERR_FILENO) == -1) + { + dwarnln("dup2: {}", strerror(errno)); + exit(1); + } + close(shell_stderr[0]); + close(shell_stderr[1]); + + execl("/bin/Shell", "Shell", NULL); + exit(1); + } + + if (shell_pid == -1) + { + dwarnln("fork: {}", strerror(errno)); + exit(1); + } + + close(shell_stdin[0]); + close(shell_stdout[1]); + close(shell_stderr[1]); + + m_shell_info = { + .in = shell_stdin[1], + .out = shell_stdout[0], + .err = shell_stderr[0], + .pid = shell_pid + }; +} + +static volatile bool s_shell_exited = false; + +void Terminal::run() +{ + signal(SIGCHLD, [](int) { s_shell_exited = true; }); + start_shell(); + + m_window = MUST(LibGUI::Window::create(600, 400, "Terminal"sv)); + m_window->fill(m_bg_color); + m_window->invalidate(); + + m_font = MUST(LibFont::Font::load("/usr/share/fonts/lat0-16.psfu"sv)); + + m_window->set_key_event_callback([&](LibGUI::EventPacket::KeyEvent event) { on_key_event(event); }); + + const int max_fd = BAN::Math::max(BAN::Math::max(m_shell_info.out, m_shell_info.err), m_window->server_fd()); + while (!s_shell_exited) + { + fd_set fds; + FD_ZERO(&fds); + FD_SET(m_shell_info.out, &fds); + FD_SET(m_shell_info.err, &fds); + FD_SET(m_window->server_fd(), &fds); + + select(max_fd + 1, &fds, nullptr, nullptr, nullptr); + + if (FD_ISSET(m_shell_info.out, &fds)) + if (!read_shell(m_shell_info.out)) + break; + if (FD_ISSET(m_shell_info.err, &fds)) + if (!read_shell(m_shell_info.err)) + break; + if (FD_ISSET(m_window->server_fd(), &fds)) + m_window->poll_events(); + } +} + + +bool Terminal::read_shell(int fd) +{ + char buffer[128]; + ssize_t nread = read(fd, buffer, sizeof(buffer) - 1); + if (nread < 0) + dwarnln("read: {}", strerror(errno)); + if (nread <= 0) + return false; + for (ssize_t i = 0; i < nread; i++) + putchar(buffer[i]); + return true; +} + +void Terminal::handle_sgr() +{ + constexpr uint32_t colors_default[] { + 0x555555, + 0xFF5555, + 0x55FF55, + 0xFFFF55, + 0x5555FF, + 0xFF55FF, + 0x55FFFF, + 0xFFFFFF, + }; + + constexpr uint32_t colors_bright[] { + 0xAAAAAA, + 0xFFAAAA, + 0xAAFFAA, + 0xFFFFAA, + 0xAAAAFF, + 0xFFAAFF, + 0xAAFFFF, + 0xFFFFFF, + }; + + switch (m_csi_info.fields[0]) + { + case -1: case 0: + m_fg_color = 0xFFFFFF; + m_bg_color = 0x000000; + break; + case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: + m_fg_color = colors_default[m_csi_info.fields[0] - 30]; + break; + case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: + m_bg_color = colors_default[m_csi_info.fields[0] - 40]; + break; + case 90: case 91: case 92: case 93: case 94: case 95: case 96: case 97: + m_fg_color = colors_bright[m_csi_info.fields[0] - 90]; + break; + case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107: + m_bg_color = colors_bright[m_csi_info.fields[0] - 100]; + break; + default: + dprintln("TODO: SGR {}", m_csi_info.fields[0]); + break; + } +} + +void Terminal::handle_csi(char ch) +{ + if (ch == ';') + { + m_csi_info.index++; + return; + } + + if (ch == '?') + return; + + if (isdigit(ch)) + { + if (m_csi_info.index <= 1) + { + auto& field = m_csi_info.fields[m_csi_info.index]; + field = (BAN::Math::max(field, 0) * 10) + (ch - '0'); + } + return; + } + + switch (ch) + { + case 'C': + if (m_csi_info.fields[0] == -1) + m_csi_info.fields[0] = 1; + m_cursor.x = BAN::Math::clamp(m_cursor.x + m_csi_info.fields[0], 0, cols() - 1); + break; + case 'D': + if (m_csi_info.fields[0] == -1) + m_csi_info.fields[0] = 1; + m_cursor.x = BAN::Math::clamp((int32_t)m_cursor.x - m_csi_info.fields[0], 0, cols() - 1); + break; + case 'G': + m_cursor.x = BAN::Math::clamp(m_csi_info.fields[0], 1, cols()) - 1; + break; + case 'H': + m_cursor.y = BAN::Math::clamp(m_csi_info.fields[0], 1, rows()) - 1; + m_cursor.x = BAN::Math::clamp(m_csi_info.fields[1], 1, cols()) - 1; + break; + case 'J': + { + uint32_t rects[2][4] { { (uint32_t)-1 }, { (uint32_t)-1 } }; + + if (m_csi_info.fields[0] == -1 || m_csi_info.fields[0] == 0) + { + rects[0][0] = m_cursor.x * m_font.width(); + rects[0][1] = m_cursor.y * m_font.height(); + rects[0][2] = m_window->width() - rects[0][0]; + rects[0][3] = m_font.height(); + + rects[1][0] = 0; + rects[1][1] = (m_cursor.y + 1) * m_font.height(); + rects[1][2] = m_window->width(); + rects[1][3] = m_window->height() - rects[1][1]; + } + else if (m_csi_info.fields[0] == 1) + { + rects[0][0] = 0; + rects[0][1] = m_cursor.y * m_font.height(); + rects[0][2] = m_cursor.x * m_font.width(); + rects[0][3] = m_font.height(); + + rects[1][0] = 0; + rects[1][1] = 0; + rects[1][2] = m_window->width(); + rects[1][3] = m_cursor.y * m_font.height(); + } + else + { + rects[0][0] = 0; + rects[0][1] = 0; + rects[0][2] = m_window->width(); + rects[0][3] = m_window->height(); + } + + for (int i = 0; i < 2; i++) + { + if (rects[i][0] == (uint32_t)-1) + continue; + m_window->fill_rect(rects[i][0], rects[i][1], rects[i][2], rects[i][3], m_bg_color); + m_window->invalidate(rects[i][0], rects[i][1], rects[i][2], rects[i][3]); + } + break; + } + case 'K': + { + m_csi_info.fields[0] = BAN::Math::max(m_csi_info.fields[0], 0); + + uint32_t rect[4]; + rect[0] = (m_csi_info.fields[0] == 0) ? m_cursor.x * m_font.width() : 0; + rect[1] = m_cursor.y * m_font.height(); + rect[2] = (m_csi_info.fields[0] == 1) ? m_cursor.x * m_font.width() : m_window->width() - rect[0]; + rect[3] = m_font.height(); + + m_window->fill_rect(rect[0], rect[1], rect[2], rect[3], m_bg_color); + m_window->invalidate(rect[0], rect[1], rect[2], rect[3]); + + break; + } + case 'm': + handle_sgr(); + break; + case 's': + m_saved_cursor = m_cursor; + break; + case 'u': + m_cursor = m_saved_cursor; + break; + default: + dprintln("TODO: CSI {}", ch); + break; + } + m_state = State::Normal; +} + +void Terminal::putchar(uint32_t codepoint) +{ + if (m_state == State::ESC) + { + if (codepoint != '[') + { + dprintln("unknown escape character 0x{H}", codepoint); + m_state = State::Normal; + return; + } + m_state = State::CSI; + m_csi_info.index = 0; + m_csi_info.fields[0] = -1; + m_csi_info.fields[1] = -1; + return; + } + + if (m_state == State::CSI) + { + if (codepoint < 0x20 || codepoint > 0xFE) + { + dprintln("invalid CSI 0x{H}", codepoint); + m_state = State::Normal; + return; + } + handle_csi(codepoint); + return; + } + + switch (codepoint) + { + case '\e': + m_state = State::ESC; + break; + case '\n': + m_cursor.x = 0; + m_cursor.y++; + break; + case '\r': + m_cursor.x = 0; + break; + case '\b': + if (m_cursor.x > 0) + m_cursor.x--; + break; + default: + { + const uint32_t cell_w = m_font.width(); + const uint32_t cell_h = m_font.height(); + const uint32_t cell_x = m_cursor.x * cell_w; + const uint32_t cell_y = m_cursor.y * cell_h; + + m_window->fill_rect(cell_x, cell_y, cell_w, cell_h, m_bg_color); + m_window->draw_character(codepoint, m_font, cell_x, cell_y, m_fg_color); + m_window->invalidate(cell_x, cell_y, cell_w, cell_h); + m_cursor.x++; + break; + } + } + + if (m_cursor.x >= cols()) + { + m_cursor.x = 0; + m_cursor.y++; + } + + if (m_cursor.y >= rows()) + { + uint32_t scroll = m_cursor.y - rows() + 1; + m_cursor.y -= scroll; + m_window->shift_vertical(-scroll * (int32_t)m_font.height()); + m_window->fill_rect(0, m_window->height() - scroll * m_font.height(), m_window->width(), scroll * m_font.height(), m_bg_color); + m_window->invalidate(); + } +} + +void Terminal::on_key_event(LibGUI::EventPacket::KeyEvent event) +{ + if (event.released()) + return; + if (const char* text = LibInput::key_to_utf8_ansi(event.key, event.modifier)) + write(m_shell_info.in, text, strlen(text)); +} diff --git a/userspace/Terminal/Terminal.h b/userspace/Terminal/Terminal.h new file mode 100644 index 00000000..26d19c11 --- /dev/null +++ b/userspace/Terminal/Terminal.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include + +class Terminal +{ +public: + void run(); + + uint32_t cols() const { return m_window->width() / m_font.width(); } + uint32_t rows() const { return m_window->height() / m_font.height(); } + +private: + void handle_csi(char ch); + void handle_sgr(); + void putchar(uint32_t codepoint); + bool read_shell(int fd); + + void on_key_event(LibGUI::EventPacket::KeyEvent); + + void start_shell(); + +private: + struct Cursor + { + uint32_t x; + uint32_t y; + }; + + struct ShellInfo + { + int in; + int out; + int err; + pid_t pid; + }; + + enum class State + { + Normal, + ESC, + CSI, + }; + + struct CSIInfo + { + int32_t fields[2]; + size_t index; + }; + +private: + BAN::UniqPtr m_window; + LibFont::Font m_font; + Cursor m_cursor { 0, 0 }; + ShellInfo m_shell_info; + State m_state { State::Normal }; + CSIInfo m_csi_info; + + Cursor m_saved_cursor { 0, 0 }; + uint32_t m_fg_color { 0xFFFFFF }; + uint32_t m_bg_color { 0x000000 }; +}; diff --git a/userspace/Terminal/main.cpp b/userspace/Terminal/main.cpp new file mode 100644 index 00000000..d36ad635 --- /dev/null +++ b/userspace/Terminal/main.cpp @@ -0,0 +1,7 @@ +#include "Terminal.h" + +int main() +{ + Terminal terminal; + terminal.run(); +} diff --git a/userspace/WindowServer/WindowServer.cpp b/userspace/WindowServer/WindowServer.cpp index c3ef9adb..9f8c34cb 100644 --- a/userspace/WindowServer/WindowServer.cpp +++ b/userspace/WindowServer/WindowServer.cpp @@ -271,7 +271,7 @@ void WindowServer::invalidate(Rectangle area) area = fb_overlap.release_value(); for (int32_t y = area.y; y < area.y + area.height; y++) - memset(&m_framebuffer.mmap[y * m_framebuffer.width + area.x], 0, area.width * 4); + memset(&m_framebuffer.mmap[y * m_framebuffer.width + area.x], 0x10, area.width * 4); for (auto& pwindow : m_client_windows) {