apps: Add basic terminal app
All checks were successful
continuous-integration/drone/pr Build is passing
All checks were successful
continuous-integration/drone/pr Build is passing
This commit is contained in:
parent
8227e7c0ea
commit
05a0d912fa
@ -52,3 +52,4 @@ add_subdirectory(apps)
|
||||
add_subdirectory(tests)
|
||||
add_subdirectory(shell)
|
||||
add_subdirectory(wind)
|
||||
add_subdirectory(terminal)
|
||||
|
@ -1,3 +0,0 @@
|
||||
Name=gclient
|
||||
Description=Sample user application.
|
||||
Command=/usr/bin/gclient
|
3
base/etc/user/01-terminal
Normal file
3
base/etc/user/01-terminal
Normal file
@ -0,0 +1,3 @@
|
||||
Name=terminal
|
||||
Description=Start the terminal.
|
||||
Command=/usr/bin/terminal
|
12
terminal/CMakeLists.txt
Normal file
12
terminal/CMakeLists.txt
Normal file
@ -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)
|
441
terminal/TerminalWidget.cpp
Normal file
441
terminal/TerminalWidget.cpp
Normal file
@ -0,0 +1,441 @@
|
||||
#include "TerminalWidget.h"
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <os/File.h>
|
||||
#include <os/Process.h>
|
||||
#include <signal.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 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 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<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_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<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_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);
|
||||
}
|
60
terminal/TerminalWidget.h
Normal file
60
terminal/TerminalWidget.h
Normal file
@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
#include <luna/EscapeSequence.h>
|
||||
#include <luna/Utf8.h>
|
||||
#include <luna/Vector.h>
|
||||
#include <stdio.h>
|
||||
#include <ui/Font.h>
|
||||
#include <ui/Widget.h>
|
||||
|
||||
class TerminalWidget : public ui::Widget
|
||||
{
|
||||
public:
|
||||
Result<void> init(char* const* args);
|
||||
|
||||
Result<void> draw(ui::Canvas& canvas) override;
|
||||
|
||||
Result<void> process();
|
||||
|
||||
void quit();
|
||||
|
||||
private:
|
||||
ui::Canvas m_terminal_canvas;
|
||||
Vector<u8> m_line_buffer;
|
||||
int m_write_fd;
|
||||
int m_read_fd;
|
||||
pid_t m_child_pid;
|
||||
|
||||
SharedPtr<ui::Font> m_font;
|
||||
SharedPtr<ui::Font> 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<EscapeSequenceParser> 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<void> putchar(char c);
|
||||
void put_code_point(wchar_t c);
|
||||
};
|
29
terminal/main.cpp
Normal file
29
terminal/main.cpp
Normal file
@ -0,0 +1,29 @@
|
||||
#include "TerminalWidget.h"
|
||||
#include <os/ArgumentParser.h>
|
||||
#include <ui/App.h>
|
||||
|
||||
Result<int> 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;
|
||||
}
|
@ -122,7 +122,7 @@ Result<int> luna_main(int argc, char** argv)
|
||||
StringView args[] = { "/usr/bin/init"_sv, "--user"_sv };
|
||||
TRY(os::Process::spawn("/usr/bin/init"_sv, Slice<StringView> { args, 2 }, false));
|
||||
|
||||
ui::Color background = ui::BLACK;
|
||||
ui::Color background = ui::Color::from_rgb(0x10, 0x10, 0x10);
|
||||
|
||||
Vector<OwnedPtr<Client>> clients;
|
||||
Vector<struct pollfd> fds;
|
||||
|
Loading…
Reference in New Issue
Block a user