diff --git a/.gitignore b/.gitignore index 20ec66d8..43437f04 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,14 @@ build/ initrd/boot/moon env-local.sh initrd/bin/** -base/usr/** +base/usr/bin/** +base/usr/include/** +base/usr/lib/** +base/usr/share/pkgdb/** +!base/usr/share/fonts/* +!base/usr/share/icons/* +base/usr/share/** +base/usr/x86_64-luna/** base/etc/skel/LICENSE .fakeroot kernel/config.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index acacbcf6..4d9d1e48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,8 +45,11 @@ endif() add_subdirectory(libluna) add_subdirectory(libos) +add_subdirectory(libui) add_subdirectory(libc) add_subdirectory(kernel) add_subdirectory(apps) add_subdirectory(tests) add_subdirectory(shell) +add_subdirectory(wind) +add_subdirectory(terminal) diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index cfe1853d..18faa4f2 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -38,5 +38,10 @@ luna_app(sysfuzz.cpp sysfuzz) luna_app(cp.cpp cp) luna_app(kill.cpp kill) luna_app(gol.cpp gol) +target_link_libraries(gol PUBLIC ui) luna_app(touch.cpp touch) luna_app(free.cpp free) +luna_app(about.cpp about) +target_link_libraries(about PUBLIC ui) +luna_app(taskbar.cpp taskbar) +target_link_libraries(taskbar PUBLIC ui) diff --git a/apps/about.cpp b/apps/about.cpp new file mode 100644 index 00000000..4bd95ef1 --- /dev/null +++ b/apps/about.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include +#include + +static constexpr ui::Color BACKGROUND_COLOR = ui::Color::from_rgb(89, 89, 89); + +Result luna_main(int argc, char** argv) +{ + ui::App app; + TRY(app.init(argc, argv)); + + auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 300 })); + app.set_main_window(window); + + window->set_title("About"); + window->set_background(BACKGROUND_COLOR); + + utsname info; + uname(&info); + + ui::VerticalLayout main_layout; + window->set_main_widget(main_layout); + + ui::Label title("About Luna", ui::WHITE, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center, + ui::Font::default_bold_font()); + main_layout.add_widget(title); + + ui::VerticalLayout version_info; + main_layout.add_widget(version_info); + + ui::Label license("Licensed under the BSD-2-Clause license."); + main_layout.add_widget(license); + + String os_release_text = TRY(String::format("OS release: %s"_sv, info.release)); + ui::Label os_release(os_release_text.view()); + version_info.add_widget(os_release); + + String kernel_version_text = TRY(String::format("Kernel version: %s"_sv, info.version)); + ui::Label kernel_version(kernel_version_text.view()); + version_info.add_widget(kernel_version); + + window->draw(); + + return app.run(); +} diff --git a/apps/gol.cpp b/apps/gol.cpp index 02399a7c..464fd31a 100644 --- a/apps/gol.cpp +++ b/apps/gol.cpp @@ -1,13 +1,12 @@ -#include #include #include #include #include #include #include -#include -#include #include +#include +#include #include struct Cell @@ -16,16 +15,12 @@ struct Cell bool new_state; }; -static int g_num_rows = 76; -static int g_num_columns = 102; - -static int g_fb_width; -static int g_fb_height; - -static int g_fd; +static int g_num_rows = 40; +static int g_num_columns = 60; static Cell* g_cells; -static char* g_fb; + +static ui::Window* g_window; static Result fill_cells() { @@ -47,31 +42,30 @@ static Cell& find_cell(int row, int column) return g_cells[row * g_num_columns + column]; } -static constexpr int BYTES_PER_PIXEL = 4; +static constexpr int BYTES_PER_PIXEL = sizeof(u32); +static constexpr ui::Color activated_cell_color = ui::CYAN; +static constexpr ui::Color deactivated_cell_color = ui::Color::from_rgb(40, 40, 40); static void draw_cells() { - const int CELL_WIDTH = g_fb_width / g_num_columns; - const int CELL_HEIGHT = g_fb_height / g_num_rows; + const int CELL_WIDTH = g_window->canvas().width / g_num_columns; + const int CELL_HEIGHT = g_window->canvas().height / g_num_rows; + + auto canvas = g_window->canvas(); for (int i = 0; i < g_num_rows; i++) { - for (int j = 0; j < g_num_columns; j++) { - char* buf = g_fb + (i * g_fb_width * CELL_HEIGHT * BYTES_PER_PIXEL); + auto subcanvas = canvas.subcanvas(ui::Rect { j * CELL_WIDTH, i * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT }); auto& cell = find_cell(i, j); - u8 color = cell.state ? 0xff : 0x00; - for (int k = 0; k < CELL_HEIGHT; k++) - { - memset(buf + (j * CELL_WIDTH * BYTES_PER_PIXEL), color, CELL_WIDTH * BYTES_PER_PIXEL); - buf += g_fb_width * BYTES_PER_PIXEL; - } + ui::Color color = cell.state ? activated_cell_color : deactivated_cell_color; + subcanvas.fill(color); } } - msync(g_fb, g_fb_height * g_fb_width * BYTES_PER_PIXEL, MS_SYNC); + g_window->update(); } static int find_neighbors(int row, int column) @@ -109,71 +103,30 @@ static void next_generation() Result luna_main(int argc, char** argv) { - u64 delay_between_iterations = 250; - u64 delay_at_end = 3000; - u64 num_iterations = 100; + ui::App app; + TRY(app.init(argc, argv)); + app.set_nonblocking(); - StringView columns; - StringView rows; - StringView delay; - StringView end_delay; - StringView iterations; - StringView seed; - - os::ArgumentParser parser; - parser.add_description("A framebuffer-based implementation for Conway's Game of Life."); - parser.add_system_program_info("gol"_sv); - parser.add_positional_argument(rows, "rows"_sv, "76"_sv); - parser.add_positional_argument(columns, "columns"_sv, "102"_sv); - parser.add_value_argument(delay, 'd', "delay"_sv, "the delay between generations (in ms)"); - parser.add_value_argument(end_delay, 'e', "end-delay"_sv, - "after finishing, how much to wait before returning to the shell (in ms)"); - parser.add_value_argument(iterations, 'i', "iterations"_sv, "how many generations to show (default: 100)"); - parser.add_value_argument(seed, 's', "seed"_sv, "the seed for the random number generator"); - parser.parse(argc, argv); - - g_num_columns = (int)TRY(columns.to_uint()); - g_num_rows = (int)TRY(rows.to_uint()); - if (!delay.is_empty()) delay_between_iterations = TRY(delay.to_uint()); - if (!end_delay.is_empty()) delay_at_end = TRY(end_delay.to_uint()); - if (!iterations.is_empty()) num_iterations = TRY(iterations.to_uint()); - if (!seed.is_empty()) srand((unsigned)TRY(seed.to_uint())); - else - srand((unsigned)time(NULL)); - - g_fd = open("/dev/fb0", O_RDWR); - if (g_fd < 0) - { - perror("gol: cannot open framebuffer for writing"); - return 1; - } - - g_fb_height = ioctl(g_fd, FB_GET_HEIGHT); - g_fb_width = ioctl(g_fd, FB_GET_WIDTH); + g_window = TRY(ui::Window::create(ui::Rect { 200, 200, 600, 400 })); + g_window->set_title("Game of Life"); + app.set_main_window(g_window); TRY(fill_cells()); - g_fb = - (char*)mmap(nullptr, g_fb_height * g_fb_width * BYTES_PER_PIXEL, PROT_READ | PROT_WRITE, MAP_SHARED, g_fd, 0); - if (g_fb == MAP_FAILED) + int counter = 0; + + while (app.process_events()) { - perror("gol: cannot map framebuffer into memory"); - return 1; + if (counter >= 10) + { + next_generation(); + draw_cells(); + counter = 0; + } + else + counter++; + usleep(10000); } - draw_cells(); - - while (num_iterations--) - { - usleep(delay_between_iterations * 1000); - next_generation(); - draw_cells(); - } - - usleep(delay_at_end * 1000); - - munmap(g_fb, g_fb_height * g_fb_width * BYTES_PER_PIXEL); - free(g_cells); - return 0; } diff --git a/apps/init.cpp b/apps/init.cpp index c0ee8ec6..f7f4015b 100644 --- a/apps/init.cpp +++ b/apps/init.cpp @@ -339,6 +339,13 @@ static void mount_shmfs() if (chmod("/dev/shm", 01777) < 0) exit(255); } +static void mount_devpts() +{ + if (mkdir("/dev/pts", 0755) < 0) exit(255); + + if (mount("/dev/pts", "devpts", "devpts") < 0) exit(255); +} + Result sysinit(StringView path) { if (getpid() != 1) @@ -359,6 +366,7 @@ Result sysinit(StringView path) mount_tmpfs(); mount_shmfs(); + mount_devpts(); umask(022); diff --git a/apps/taskbar.cpp b/apps/taskbar.cpp new file mode 100644 index 00000000..e179f5fa --- /dev/null +++ b/apps/taskbar.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr ui::Color TASKBAR_COLOR = ui::Color::from_rgb(83, 83, 83); + +void sigchld_handler(int) +{ + wait(nullptr); +} + +Result luna_main(int argc, char** argv) +{ + ui::App app; + TRY(app.init(argc, argv)); + + signal(SIGCHLD, sigchld_handler); + + ui::Rect screen = app.screen_rect(); + + ui::Rect bar = ui::Rect { ui::Point { 0, screen.height - 50 }, screen.width, 50 }; + + auto window = TRY(ui::Window::create(bar, false)); + app.set_main_window(window); + window->set_background(TASKBAR_COLOR); + + ui::HorizontalLayout layout(ui::AdjustHeight::Yes, ui::AdjustWidth::No); + window->set_main_widget(layout); + + ui::Button term_button({ 0, 0, 50, 50 }); + layout.add_widget(term_button); + + ui::Container term_container({ 0, 0, 50, 50 }, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center); + term_button.set_widget(term_container); + term_button.set_action([] { + StringView args[] = { "/usr/bin/terminal" }; + os::Process::spawn("/usr/bin/terminal", Slice { args, 1 }, false); + }); + + auto term_image = TRY(ui::ImageWidget::load("/usr/share/icons/32x32/app-terminal.tga")); + term_container.set_widget(*term_image); + + ui::Button about_button({ 0, 0, 50, 50 }); + layout.add_widget(about_button); + + ui::Container about_container({ 0, 0, 50, 50 }, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center); + about_button.set_widget(about_container); + about_button.set_action([] { + StringView args[] = { "/usr/bin/about" }; + os::Process::spawn("/usr/bin/about", Slice { args, 1 }, false); + }); + + auto about_image = TRY(ui::ImageWidget::load("/usr/share/icons/32x32/app-about.tga")); + about_container.set_widget(*about_image); + + window->draw(); + + return app.run(); +} diff --git a/base/etc/init/01-motd b/base/etc/init/01-motd deleted file mode 100644 index 3c6d5aac..00000000 --- a/base/etc/init/01-motd +++ /dev/null @@ -1,4 +0,0 @@ -Name=motd -Description=Show the message of the day to the user. -Command=/usr/bin/cat /etc/motd -Wait=true diff --git a/base/etc/init/99-login b/base/etc/init/99-login index 2a3b5f95..bb7e7ac9 100644 --- a/base/etc/init/99-login +++ b/base/etc/init/99-login @@ -1,4 +1,6 @@ Name=login -Description=Start the command-line login program. -Command=/usr/bin/login +Description=Start the display server. +Command=/usr/bin/wind --user=selene +StandardOutput=/dev/uart0 +StandardError=/dev/uart0 Restart=true diff --git a/base/etc/user/00-taskbar b/base/etc/user/00-taskbar new file mode 100644 index 00000000..19614216 --- /dev/null +++ b/base/etc/user/00-taskbar @@ -0,0 +1,4 @@ +Name=taskbar +Description=Start the taskbar. +Command=/usr/bin/taskbar +Restart=true 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/base/usr/share/cursors/default.tga b/base/usr/share/cursors/default.tga new file mode 100644 index 00000000..ef3bb7a0 Binary files /dev/null and b/base/usr/share/cursors/default.tga differ diff --git a/base/usr/share/fonts/Tamsyn-Bold.psf b/base/usr/share/fonts/Tamsyn-Bold.psf new file mode 100644 index 00000000..4eb9107d Binary files /dev/null and b/base/usr/share/fonts/Tamsyn-Bold.psf differ diff --git a/base/usr/share/fonts/Tamsyn-Regular.psf b/base/usr/share/fonts/Tamsyn-Regular.psf new file mode 100644 index 00000000..6fd82c17 Binary files /dev/null and b/base/usr/share/fonts/Tamsyn-Regular.psf differ diff --git a/base/usr/share/icons/16x16/app-close.tga b/base/usr/share/icons/16x16/app-close.tga new file mode 100644 index 00000000..9ced1838 Binary files /dev/null and b/base/usr/share/icons/16x16/app-close.tga differ diff --git a/base/usr/share/icons/32x32/app-terminal.tga b/base/usr/share/icons/32x32/app-terminal.tga new file mode 100644 index 00000000..44f522d8 Binary files /dev/null and b/base/usr/share/icons/32x32/app-terminal.tga differ diff --git a/kernel/CMakeLists.txt b/kernel/CMakeLists.txt index 438a512e..35571e8d 100644 --- a/kernel/CMakeLists.txt +++ b/kernel/CMakeLists.txt @@ -58,6 +58,8 @@ set(SOURCES src/net/UnixSocket.cpp src/fs/tmpfs/FileSystem.cpp src/fs/tmpfs/Inode.cpp + src/fs/devpts/FileSystem.cpp + src/fs/devpts/Inode.cpp src/fs/ext2/FileSystem.cpp src/fs/ext2/Inode.cpp src/fs/devices/DeviceRegistry.cpp @@ -70,6 +72,9 @@ set(SOURCES src/fs/devices/UARTDevice.cpp src/fs/devices/MouseDevice.cpp src/fs/devices/KeyboardDevice.cpp + src/fs/devices/PTYMultiplexer.cpp + src/fs/devices/MasterPTY.cpp + src/fs/devices/SlavePTY.cpp src/fs/InitRD.cpp src/binfmt/ELF.cpp src/binfmt/BinaryFormat.cpp diff --git a/kernel/src/fs/VFS.h b/kernel/src/fs/VFS.h index 2a63f68e..dbe471ee 100644 --- a/kernel/src/fs/VFS.h +++ b/kernel/src/fs/VFS.h @@ -118,6 +118,11 @@ namespace VFS return err(EACCES); } + virtual Result> open() + { + return SharedPtr { this }; + } + // Directory-specific methods virtual Result> find(const char* name) const = 0; diff --git a/kernel/src/fs/devices/DeviceRegistry.cpp b/kernel/src/fs/devices/DeviceRegistry.cpp index 33d6a946..a46b1d10 100644 --- a/kernel/src/fs/devices/DeviceRegistry.cpp +++ b/kernel/src/fs/devices/DeviceRegistry.cpp @@ -7,6 +7,7 @@ #include "fs/devices/KeyboardDevice.h" #include "fs/devices/MouseDevice.h" #include "fs/devices/NullDevice.h" +#include "fs/devices/PTYMultiplexer.h" #include "fs/devices/UARTDevice.h" #include "fs/devices/ZeroDevice.h" #include "fs/tmpfs/FileSystem.h" @@ -92,6 +93,12 @@ namespace DeviceRegistry for (const auto& descriptor : g_available_devices) TRY(create_special_device_inode(fs, descriptor)); + auto multiplexer = TRY(make_shared()); + multiplexer->set_fs(*fs); + multiplexer->set_inode_number(TRY(fs->allocate_inode_number())); + + TRY(fs->root_inode()->add_entry(multiplexer, "ptmx")); + return fs; } } diff --git a/kernel/src/fs/devices/DeviceRegistry.h b/kernel/src/fs/devices/DeviceRegistry.h index e4a8504d..7ee4a89d 100644 --- a/kernel/src/fs/devices/DeviceRegistry.h +++ b/kernel/src/fs/devices/DeviceRegistry.h @@ -17,6 +17,7 @@ namespace DeviceRegistry DiskPartition = 5, Serial = 6, Input = 7, + Terminal = 8, }; Result> fetch_special_device(u32 major, u32 minor); diff --git a/kernel/src/fs/devices/MasterPTY.cpp b/kernel/src/fs/devices/MasterPTY.cpp new file mode 100644 index 00000000..6cf7dc20 --- /dev/null +++ b/kernel/src/fs/devices/MasterPTY.cpp @@ -0,0 +1,125 @@ +#include "fs/devices/MasterPTY.h" +#include "Pledge.h" +#include "fs/devices/DeviceRegistry.h" +#include "fs/devices/PTYMultiplexer.h" +#include "fs/devpts/FileSystem.h" +#include "memory/MemoryManager.h" +#include "thread/Scheduler.h" + +Result> MasterPTY::create_pair(int index) +{ + auto master = TRY(make_shared()); + auto slave = TRY(make_shared()); + + auto name = TRY(String::format("%d"_sv, index)); + for (auto& fs : g_devpts_instances) { fs->root_inode()->add_entry(slave, name.chars()); } + slave->m_name = move(name); + + master->m_metadata.mode = 0666; + master->m_index = index; + master->m_slave = slave; + master->m_metadata.devid = luna_dev_makedev(DeviceRegistry::Terminal, 0); + master->m_settings.c_lflag = ECHO | ECHOE | ECHOCTL | ISIG | ICANON; + master->m_settings.c_cc[VEOF] = '\4'; + master->m_settings.c_cc[VERASE] = '\b'; + master->m_settings.c_cc[VINTR] = '\3'; + master->m_settings.c_cc[VQUIT] = '\x1c'; + master->m_window.ws_col = 80; + master->m_window.ws_row = 25; + + slave->m_master = master.ptr(); + slave->m_metadata.devid = luna_dev_makedev(DeviceRegistry::Terminal, index + 1); + slave->m_metadata.uid = Scheduler::current()->auth.euid; + slave->m_metadata.gid = Scheduler::current()->auth.egid; + slave->m_metadata.mode = 0620; + + return (SharedPtr)master; +} + +Result MasterPTY::handle_background_process_group(bool can_succeed, int signo) const +{ + if (!m_foreground_process_group.has_value()) return {}; + + auto foreground_pgrp = m_foreground_process_group.value(); + + auto* current = Scheduler::current(); + if (current->pgid == foreground_pgrp) return {}; + + if ((current->signal_mask.get(signo - 1)) || (current->signal_handlers[signo - 1].sa_handler == SIG_IGN)) + { + if (can_succeed) return {}; + return err(EIO); + } + + current->send_signal(signo); + + if (can_succeed) return err(EINTR); + return err(EIO); +} + +Result MasterPTY::read(u8* buf, usize, usize length) const +{ + length = m_buffer.dequeue_data(buf, length); + + return length; +} + +Result MasterPTY::write(const u8* buf, usize, usize length) +{ + TRY(m_slave->m_buffer.append_data(buf, length)); + + return length; +} + +Result MasterPTY::ioctl(int request, void* arg) +{ + auto* current = Scheduler::current(); + TRY(check_pledge(current, Promise::p_tty)); + + switch (request) + { + case TCGETS: { + return MemoryManager::copy_to_user_typed((struct termios*)arg, &m_settings) ? 0 : err(EFAULT); + } + case TCSETS: { + if (!MemoryManager::copy_from_user_typed((const struct termios*)arg, &m_settings)) return err(EFAULT); + + return 0; + } + case TIOCSPGRP: { + pid_t pgid; + if (!MemoryManager::copy_from_user_typed((const pid_t*)arg, &pgid)) return err(EFAULT); + + bool pgid_exists = false; + Scheduler::for_each_in_process_group(pgid, [&pgid_exists](Thread*) { + pgid_exists = true; + return false; + }); + if (!pgid_exists) return err(EPERM); + + m_foreground_process_group = pgid; + return 0; + } + case TIOCGPGRP: { + pid_t pgid = m_foreground_process_group.value_or((pid_t)next_thread_id()); + if (!MemoryManager::copy_to_user_typed((pid_t*)arg, &pgid)) return err(EFAULT); + return 0; + } + case TIOCGWINSZ: { + if (!MemoryManager::copy_to_user_typed((struct winsize*)arg, &m_window)) return err(EFAULT); + return 0; + } + case TIOCGPTN: { + if (!MemoryManager::copy_to_user_typed((int*)arg, &m_index)) return err(EFAULT); + return 0; + } + default: return err(EINVAL); + } +} + +MasterPTY::~MasterPTY() +{ + m_slave->m_master = nullptr; + for (auto& fs : g_devpts_instances) { fs->root_inode()->remove_entry(m_slave->m_name.chars()); } + PTYMultiplexer::did_remove_pty(m_index); +} diff --git a/kernel/src/fs/devices/MasterPTY.h b/kernel/src/fs/devices/MasterPTY.h new file mode 100644 index 00000000..a4131a09 --- /dev/null +++ b/kernel/src/fs/devices/MasterPTY.h @@ -0,0 +1,75 @@ +#pragma once +#include "fs/VFS.h" +#include "fs/devices/SlavePTY.h" +#include +#include + +class MasterPTY : public VFS::DeviceInode +{ + public: + MasterPTY() = default; + + static Result> create_pair(int index); + + VFS::InodeType type() const override + { + return VFS::InodeType::CharacterDevice; + } + + Result query_shared_memory(off_t, usize) override + { + return err(ENOTSUP); + } + + VFS::FileSystem* fs() const override + { + return nullptr; + } + + Result read(u8* buf, usize offset, usize length) const override; + + Result write(const u8* buf, usize offset, usize length) override; + + Result ioctl(int request, void* arg) override; + + Result isatty() const override + { + return 1; + } + + Result truncate(usize) override + { + // POSIX says truncate is for regular files, but doesn't tell us what error to return for non-regular files. + return err(EINVAL); + } + + bool will_block_if_read() const override + { + return m_buffer.is_empty(); + } + + void did_link() override + { + m_metadata.nlinks++; + } + + void did_unlink() override + { + m_metadata.nlinks--; + } + + virtual ~MasterPTY(); + + private: + struct termios m_settings; + mutable Buffer m_buffer; + SharedPtr m_slave; + mutable Option m_foreground_process_group; + struct winsize m_window; + + Result handle_background_process_group(bool can_succeed, int signo) const; + + int m_index; + + friend class SlavePTY; +}; diff --git a/kernel/src/fs/devices/PTYMultiplexer.cpp b/kernel/src/fs/devices/PTYMultiplexer.cpp new file mode 100644 index 00000000..74216560 --- /dev/null +++ b/kernel/src/fs/devices/PTYMultiplexer.cpp @@ -0,0 +1,36 @@ +#include "fs/devices/PTYMultiplexer.h" + +Bitset PTYMultiplexer::m_available_indexes = 0; + +PTYMultiplexer::PTYMultiplexer() +{ + m_metadata.devid = luna_dev_makedev(DeviceRegistry::Terminal, 0); + m_metadata.mode = 0666; +} + +Result> PTYMultiplexer::open() +{ + int index = -1; + for (int i = 0; i < 64; i++) + { + if (!m_available_indexes.get(i)) + { + index = i; + m_available_indexes.set(i, true); + break; + } + } + if (index == -1) return err(ENOSPC); + + return MasterPTY::create_pair(index); +} + +void PTYMultiplexer::init() +{ + m_available_indexes.clear(); +} + +void PTYMultiplexer::did_remove_pty(int index) +{ + m_available_indexes.set(index, false); +} diff --git a/kernel/src/fs/devices/PTYMultiplexer.h b/kernel/src/fs/devices/PTYMultiplexer.h new file mode 100644 index 00000000..1ec53dad --- /dev/null +++ b/kernel/src/fs/devices/PTYMultiplexer.h @@ -0,0 +1,80 @@ +#pragma once +#include "fs/VFS.h" +#include "fs/devices/DeviceRegistry.h" +#include "fs/devices/MasterPTY.h" +#include + +class PTYMultiplexer : public VFS::DeviceInode +{ + public: + PTYMultiplexer(); + + VFS::InodeType type() const override + { + return VFS::InodeType::CharacterDevice; + } + + void set_fs(VFS::FileSystem& fs) + { + m_fs = &fs; + } + + void set_inode_number(usize inum) + { + m_metadata.inum = inum; + } + + Result query_shared_memory(off_t, usize) override + { + unreachable(); + } + + Result> open() override; + + VFS::FileSystem* fs() const override + { + return m_fs; + } + + Result read(u8*, usize, usize) const override + { + unreachable(); + } + + Result write(const u8*, usize, usize) override + { + unreachable(); + } + + Result truncate(usize) override + { + // POSIX says truncate is for regular files, but doesn't tell us what error to return for non-regular files. + return err(EINVAL); + } + + bool will_block_if_read() const override + { + unreachable(); + } + + void did_link() override + { + m_metadata.nlinks++; + } + + void did_unlink() override + { + m_metadata.nlinks--; + } + + static void init(); + + static void did_remove_pty(int index); + + virtual ~PTYMultiplexer() = default; + + private: + VFS::FileSystem* m_fs; + + static Bitset m_available_indexes; +}; diff --git a/kernel/src/fs/devices/SlavePTY.cpp b/kernel/src/fs/devices/SlavePTY.cpp new file mode 100644 index 00000000..c5f7d4a7 --- /dev/null +++ b/kernel/src/fs/devices/SlavePTY.cpp @@ -0,0 +1,71 @@ +#include "fs/devices/SlavePTY.h" +#include "Pledge.h" +#include "fs/devices/MasterPTY.h" +#include "memory/MemoryManager.h" +#include "thread/Scheduler.h" + +Result SlavePTY::read(u8* buf, usize, usize length) const +{ + if (!m_master) return err(EIO); + + TRY(m_master->handle_background_process_group(false, SIGTTIN)); + + length = m_buffer.dequeue_data(buf, length); + + return length; +} + +Result SlavePTY::write(const u8* buf, usize, usize length) +{ + if (!m_master) return err(EIO); + + if (m_master->m_settings.c_lflag & TOSTOP) TRY(m_master->handle_background_process_group(true, SIGTTOU)); + + TRY(m_master->m_buffer.append_data(buf, length)); + + return length; +} + +Result SlavePTY::ioctl(int request, void* arg) +{ + auto* current = Scheduler::current(); + TRY(check_pledge(current, Promise::p_tty)); + + if (!m_master) return err(EIO); + + switch (request) + { + case TCGETS: { + return MemoryManager::copy_to_user_typed((struct termios*)arg, &m_master->m_settings) ? 0 : err(EFAULT); + } + case TCSETS: { + if (!MemoryManager::copy_from_user_typed((const struct termios*)arg, &m_master->m_settings)) return err(EFAULT); + + return 0; + } + case TIOCSPGRP: { + pid_t pgid; + if (!MemoryManager::copy_from_user_typed((const pid_t*)arg, &pgid)) return err(EFAULT); + + bool pgid_exists = false; + Scheduler::for_each_in_process_group(pgid, [&pgid_exists](Thread*) { + pgid_exists = true; + return false; + }); + if (!pgid_exists) return err(EPERM); + + m_master->m_foreground_process_group = pgid; + return 0; + } + case TIOCGPGRP: { + pid_t pgid = m_master->m_foreground_process_group.value_or((pid_t)next_thread_id()); + if (!MemoryManager::copy_to_user_typed((pid_t*)arg, &pgid)) return err(EFAULT); + return 0; + } + case TIOCGWINSZ: { + if (!MemoryManager::copy_to_user_typed((struct winsize*)arg, &m_master->m_window)) return err(EFAULT); + return 0; + } + default: return err(EINVAL); + } +} diff --git a/kernel/src/fs/devices/SlavePTY.h b/kernel/src/fs/devices/SlavePTY.h new file mode 100644 index 00000000..dafba4f9 --- /dev/null +++ b/kernel/src/fs/devices/SlavePTY.h @@ -0,0 +1,69 @@ +#pragma once +#include "fs/VFS.h" +#include +#include + +class MasterPTY; + +class SlavePTY : public VFS::DeviceInode +{ + public: + SlavePTY() = default; + + VFS::InodeType type() const override + { + return VFS::InodeType::CharacterDevice; + } + + Result query_shared_memory(off_t, usize) override + { + return err(ENOTSUP); + } + + VFS::FileSystem* fs() const override + { + return nullptr; + } + + Result read(u8* buf, usize offset, usize length) const override; + + Result write(const u8* buf, usize offset, usize length) override; + + Result ioctl(int request, void* arg) override; + + Result isatty() const override + { + return 1; + } + + Result truncate(usize) override + { + // POSIX says truncate is for regular files, but doesn't tell us what error to return for non-regular files. + return err(EINVAL); + } + + bool will_block_if_read() const override + { + return m_buffer.is_empty(); + } + + void did_link() override + { + m_metadata.nlinks++; + } + + void did_unlink() override + { + m_metadata.nlinks--; + } + + virtual ~SlavePTY() = default; + + private: + mutable Buffer m_buffer; + + MasterPTY* m_master; + String m_name; + + friend class MasterPTY; +}; diff --git a/kernel/src/fs/devpts/FileSystem.cpp b/kernel/src/fs/devpts/FileSystem.cpp new file mode 100644 index 00000000..b7d29cac --- /dev/null +++ b/kernel/src/fs/devpts/FileSystem.cpp @@ -0,0 +1,62 @@ +#include "fs/devpts/FileSystem.h" +#include "arch/Timer.h" +#include "fs/devices/DeviceRegistry.h" +#include "fs/devpts/Inode.h" +#include +#include +#include + +Vector g_devpts_instances; + +namespace DevPTS +{ + Result> FileSystem::create() + { + SharedPtr fs = TRY(adopt_shared_if_nonnull(new (std::nothrow) FileSystem())); + SharedPtr root = TRY(make_shared()); + + TRY(root->add_entry(root, ".")); + TRY(root->add_entry(root, "..")); + + root->set_self(root, {}); + root->set_fs(*fs, {}); + root->set_inode_number(); + root->m_metadata.mode = 0755; + root->m_metadata.atime = root->m_metadata.ctime = root->m_metadata.mtime = *Timer::realtime_clock(); + fs->set_root(root); + + TRY(g_devpts_instances.try_append(fs.ptr())); + + return (SharedPtr)fs; + } + + Result FileSystem::allocate_inode_number() + { + return err(ENOTSUP); + } + + FileSystem::FileSystem() + { + m_host_device_id = DeviceRegistry::next_null_device_id(); + } + + Result FileSystem::set_mount_dir(SharedPtr parent) + { + return m_root_inode->replace_entry(parent, ".."); + } + + Result FileSystem::reset_mount_dir() + { + return m_root_inode->replace_entry(m_root_inode, ".."); + } + + void FileSystem::set_root(SharedPtr root) + { + m_root_inode = root; + } + + FileSystem::~FileSystem() + { + g_devpts_instances.remove_first_matching([this](VFS::FileSystem* ptr) { return ptr == this; }); + } +} diff --git a/kernel/src/fs/devpts/FileSystem.h b/kernel/src/fs/devpts/FileSystem.h new file mode 100644 index 00000000..f9101938 --- /dev/null +++ b/kernel/src/fs/devpts/FileSystem.h @@ -0,0 +1,61 @@ +#pragma once +#include "fs/VFS.h" +#include "fs/devices/DeviceRegistry.h" + +namespace DevPTS +{ + class FileSystem : public VFS::FileSystem + { + public: + SharedPtr root_inode() const override + { + return m_root_inode; + } + + Result> create_file_inode(mode_t) override + { + return err(ENOTSUP); + } + + Result> create_dir_inode(SharedPtr, mode_t) override + { + return err(ENOTSUP); + } + + Result> create_device_inode(u32, u32, mode_t) override + { + return err(ENOTSUP); + } + + Result> create_symlink_inode(StringView) override + { + return err(ENOTSUP); + } + + Result allocate_inode_number() override; + + Result set_mount_dir(SharedPtr parent) override; + + Result reset_mount_dir() override; + + static Result> create(); + + dev_t host_device_id() const override + { + return m_host_device_id; + } + + virtual ~FileSystem(); + + private: + FileSystem(); + + void set_root(SharedPtr root); + + SharedPtr m_root_inode; + + dev_t m_host_device_id; + }; +} + +extern Vector g_devpts_instances; diff --git a/kernel/src/fs/devpts/Inode.cpp b/kernel/src/fs/devpts/Inode.cpp new file mode 100644 index 00000000..6efb94fe --- /dev/null +++ b/kernel/src/fs/devpts/Inode.cpp @@ -0,0 +1,78 @@ +#include "fs/devpts/Inode.h" + +namespace DevPTS +{ + Result> RootInode::find(const char* name) const + { + for (const auto& entry : m_entries) + { + if (!strcmp(name, entry.name.chars())) return entry.inode; + } + + return err(ENOENT); + } + + Result RootInode::replace_entry(SharedPtr inode, const char* name) + { + for (auto& entry : m_entries) + { + if (!strcmp(name, entry.name.chars())) + { + entry.inode = inode; + return {}; + } + } + + return err(ENOENT); + } + + Option RootInode::get(usize index) const + { + if (index >= m_entries.size()) return {}; + + return m_entries[index]; + } + + Result RootInode::add_entry(SharedPtr inode, const char* name) + { + if (find(name).has_value()) return err(EEXIST); + + VFS::DirectoryEntry entry { inode, name }; + + TRY(m_entries.try_append(move(entry))); + + inode->did_link(); + + m_metadata.mtime = *Timer::realtime_clock(); + + return {}; + } + + Result RootInode::remove_entry(const char* name) + { + SharedPtr inode = TRY(find(name)); + + if (inode->type() == VFS::InodeType::Directory && inode->entries() != 2) return err(ENOTEMPTY); + + if (inode->is_mountpoint()) return err(EBUSY); + + m_entries.remove_first_matching( + [&](const VFS::DirectoryEntry& entry) { return !strcmp(entry.name.chars(), name); }); + + inode->did_unlink(); + + m_metadata.mtime = *Timer::realtime_clock(); + + return {}; + } + + Result> RootInode::create_file(const char*, mode_t) + { + return err(ENOTSUP); + } + + Result> RootInode::create_subdirectory(const char*, mode_t) + { + return err(ENOTSUP); + } +} diff --git a/kernel/src/fs/devpts/Inode.h b/kernel/src/fs/devpts/Inode.h new file mode 100644 index 00000000..0575236a --- /dev/null +++ b/kernel/src/fs/devpts/Inode.h @@ -0,0 +1,93 @@ +#pragma once +#include "fs/VFS.h" +#include "fs/devices/DeviceRegistry.h" +#include "fs/devpts/FileSystem.h" + +namespace DevPTS +{ + class RootInode : public VFS::Inode + { + public: + RootInode() = default; + + void set_fs(FileSystem& fs, Badge) + { + m_fs = &fs; + } + + void set_inode_number() + { + m_metadata.inum = 2; + } + + void set_self(SharedPtr self, Badge) + { + m_self = self; + } + + Result> find(const char* name) const override; + Option get(usize index) const override; + + Result read(u8*, usize, usize) const override + { + return err(EISDIR); + } + + Result write(const u8*, usize, usize) override + { + return err(EISDIR); + } + + Result truncate(usize) override + { + return err(EISDIR); + } + + bool will_block_if_read() const override + { + return false; + } + + VFS::FileSystem* fs() const override + { + return m_fs; + } + + VFS::InodeType type() const override + { + return VFS::InodeType::Directory; + } + + void did_link() override + { + } + + void did_unlink() override + { + m_self = {}; + m_entries.clear(); + } + + usize entries() const override + { + return m_entries.size(); + } + + Result remove_entry(const char* name) override; + + Result> create_file(const char* name, mode_t mode) override; + Result> create_subdirectory(const char* name, mode_t mode) override; + + Result add_entry(SharedPtr inode, const char* name); + Result replace_entry(SharedPtr inode, const char* name); + + virtual ~RootInode() = default; + + private: + VFS::FileSystem* m_fs; + SharedPtr m_self; + Vector m_entries; + + friend class FileSystem; + }; +} diff --git a/kernel/src/main.cpp b/kernel/src/main.cpp index 84703bfe..5e73a722 100644 --- a/kernel/src/main.cpp +++ b/kernel/src/main.cpp @@ -6,6 +6,7 @@ #include "config.h" #include "fs/InitRD.h" #include "fs/devices/DeviceRegistry.h" +#include "fs/devices/PTYMultiplexer.h" #include "fs/tmpfs/FileSystem.h" #include "memory/MemoryManager.h" #include "thread/Scheduler.h" @@ -94,6 +95,7 @@ extern "C" [[noreturn]] void _start() Thread::init(); Scheduler::init(); + PTYMultiplexer::init(); Scheduler::new_kernel_thread(init, "[kinit]"); diff --git a/kernel/src/memory/MemoryManager.cpp b/kernel/src/memory/MemoryManager.cpp index fc530b16..eb0a7ab1 100644 --- a/kernel/src/memory/MemoryManager.cpp +++ b/kernel/src/memory/MemoryManager.cpp @@ -157,6 +157,13 @@ namespace MemoryManager used_mem += ARCH_PAGE_SIZE; free_mem -= ARCH_PAGE_SIZE; + if (free_mem < 4 * 1024 * 1024) + { + // Less than 4 MiB of free memory! Let's start clearing caches... + kwarnln("Less than 4 MiB of free memory, clearing caches to try to gain extra memory"); + Scheduler::signal_oom_thread(); + } + return index * ARCH_PAGE_SIZE; } diff --git a/kernel/src/net/Socket.h b/kernel/src/net/Socket.h index 1ac16c92..47c66807 100644 --- a/kernel/src/net/Socket.h +++ b/kernel/src/net/Socket.h @@ -65,6 +65,12 @@ class Socket : public VFS::FileInode m_metadata.nlinks--; } + virtual bool can_accept_connections() const = 0; + + virtual bool can_read_data() const = 0; + + virtual bool peer_disconnected() const = 0; + virtual ~Socket() = default; protected: diff --git a/kernel/src/net/UnixSocket.h b/kernel/src/net/UnixSocket.h index 69b23cfa..953947d6 100644 --- a/kernel/src/net/UnixSocket.h +++ b/kernel/src/net/UnixSocket.h @@ -17,6 +17,21 @@ class UnixSocket : public Socket return (m_state == Connected || m_state == Reset) && !m_data.size(); } + bool can_read_data() const override + { + return (m_state == Connected || m_state == Reset) && m_data.size(); + } + + bool can_accept_connections() const override + { + return !m_listen_queue.is_empty(); + } + + bool peer_disconnected() const override + { + return m_state == Reset; + } + Result send(const u8*, usize, int) override; Result recv(u8*, usize, int) const override; diff --git a/kernel/src/sys/mount.cpp b/kernel/src/sys/mount.cpp index b01512e2..20b4a580 100644 --- a/kernel/src/sys/mount.cpp +++ b/kernel/src/sys/mount.cpp @@ -1,5 +1,6 @@ #include "Pledge.h" #include "fs/VFS.h" +#include "fs/devpts/FileSystem.h" #include "fs/ext2/FileSystem.h" #include "fs/tmpfs/FileSystem.h" #include "memory/MemoryManager.h" @@ -26,6 +27,8 @@ Result sys_mount(Registers*, SyscallArgs args) SharedPtr fs; if (fstype.view() == "tmpfs") fs = TRY(TmpFS::FileSystem::create()); + else if (fstype.view() == "devpts") + fs = TRY(DevPTS::FileSystem::create()); else if (fstype.view() == "devfs") fs = TRY(DeviceRegistry::create_devfs_instance()); else if (fstype.view() == "ext2") diff --git a/kernel/src/sys/open.cpp b/kernel/src/sys/open.cpp index 90d5d84a..0f0d03f2 100644 --- a/kernel/src/sys/open.cpp +++ b/kernel/src/sys/open.cpp @@ -62,6 +62,8 @@ Result sys_openat(Registers*, SyscallArgs args) if ((flags & O_WRONLY) && !VFS::can_write(inode, current->auth)) return err(EACCES); } + inode = TRY(inode->open()); + // This should only be possible if O_NOFOLLOW was in flags. if (inode->type() == VFS::InodeType::Symlink) return err(ELOOP); diff --git a/kernel/src/sys/poll.cpp b/kernel/src/sys/poll.cpp index f268060b..4f09241c 100644 --- a/kernel/src/sys/poll.cpp +++ b/kernel/src/sys/poll.cpp @@ -2,6 +2,7 @@ #include "Pledge.h" #include "fs/VFS.h" #include "memory/MemoryManager.h" +#include "net/Socket.h" #include "sys/Syscall.h" #include "thread/Scheduler.h" #include @@ -47,10 +48,30 @@ Result sys_poll(Registers*, SyscallArgs args) auto& inode = inodes[i]; if (!inode) continue; - if (kfds[i].events & POLLIN && !inode->will_block_if_read()) + if (kfds[i].events & POLLIN) { - fds_with_events++; - kfds[i].revents |= POLLIN; + if (inode->type() == VFS::InodeType::Socket) + { + auto socket = (Socket*)inode.ptr(); + if (socket->can_read_data() || socket->can_accept_connections()) + { + fds_with_events++; + kfds[i].revents |= POLLIN; + } + if (socket->peer_disconnected()) + { + fds_with_events++; + kfds[i].revents |= POLLHUP; + } + } + else + { + if (!inode->will_block_if_read()) + { + fds_with_events++; + kfds[i].revents |= POLLIN; + } + } } } diff --git a/kernel/src/sys/signal.cpp b/kernel/src/sys/signal.cpp index 779b151c..2d2d05c9 100644 --- a/kernel/src/sys/signal.cpp +++ b/kernel/src/sys/signal.cpp @@ -54,16 +54,65 @@ Result sys_kill(Registers*, SyscallArgs args) pid_t pid = (pid_t)args[0]; int signo = (int)args[1]; - // FIXME: Support this case. - if (pid <= 0) return err(ENOTSUP); + auto send_signal = [&](Thread* target) -> Result { + if (current->auth.euid != 0 && current->auth.euid != target->auth.euid && + current->auth.egid != target->auth.egid) + return err(EPERM); + if (target->is_kernel) return {}; + if (signo == 0) return {}; - auto* target = TRY(Result::from_option(Scheduler::find_by_pid(pid), ESRCH)); - if (current->auth.euid != 0 && current->auth.euid != target->auth.euid && current->auth.egid != target->auth.egid) - return err(EPERM); - if (target->is_kernel) return 0; - if (signo == 0) return 0; + target->send_signal(signo); - target->send_signal(signo); + return {}; + }; + + if (pid > 0) + { + auto* target = TRY(Result::from_option(Scheduler::find_by_pid(pid), ESRCH)); + TRY(send_signal(target)); + } + else if (pid == 0) + { + int errno = -1; + bool pgid_exists = false; + Scheduler::for_each_in_process_group(current->pgid, [&](Thread* target) { + pgid_exists = true; + auto rc = send_signal(target); + if (rc.has_error()) + { + errno = rc.error(); + return false; + } + return true; + }); + if (errno > 0) return err(errno); + if (!pgid_exists) return err(ESRCH); + } + else if (pid == -1) + { + for (auto* thread : g_threads) + { + // We ignore permission errors here. + if (thread != current && thread->id != 1) send_signal(thread); + } + } + else if (pid < -1) + { + int errno = -1; + bool pgid_exists = false; + Scheduler::for_each_in_process_group(-pid, [&](Thread* target) { + pgid_exists = true; + auto rc = send_signal(target); + if (rc.has_error()) + { + errno = rc.error(); + return false; + } + return true; + }); + if (errno > 0) return err(errno); + if (!pgid_exists) return err(ESRCH); + } return 0; } diff --git a/kernel/src/thread/Thread.cpp b/kernel/src/thread/Thread.cpp index 8907c917..ebac3c37 100644 --- a/kernel/src/thread/Thread.cpp +++ b/kernel/src/thread/Thread.cpp @@ -211,7 +211,7 @@ void Thread::send_signal(int signo) check(signo > 0 && signo <= NSIG); pending_signals.set(signo - 1, true); - if (state == ThreadState::Waiting || state == ThreadState::Sleeping) + if (state == ThreadState::Waiting || state == ThreadState::Sleeping || is_in_kernel(®s)) { interrupted = true; wake_up(); diff --git a/libc/include/bits/poll.h b/libc/include/bits/poll.h index 35600ed8..e0c6b84a 100644 --- a/libc/include/bits/poll.h +++ b/libc/include/bits/poll.h @@ -8,6 +8,7 @@ #define POLLIN (1 << 0) #define POLLERR (1 << 1) #define POLLNVAL (1 << 2) +#define POLLHUP (1 << 3) typedef __u64_t nfds_t; diff --git a/libc/include/bits/termios.h b/libc/include/bits/termios.h index d1506915..5273c236 100644 --- a/libc/include/bits/termios.h +++ b/libc/include/bits/termios.h @@ -45,5 +45,6 @@ struct winsize #define TIOCSPGRP 2 #define TIOCGPGRP 3 #define TIOCGWINSZ 4 +#define TIOCGPTN 5 #endif diff --git a/libc/include/stdlib.h b/libc/include/stdlib.h index b06e1861..35357d21 100644 --- a/libc/include/stdlib.h +++ b/libc/include/stdlib.h @@ -148,6 +148,18 @@ extern "C" /* Create a unique file from a template string whose last 6 bytes must be XXXXXX. */ int mkstemp(char* _template); + /* Create a new pseudoterminal pair. */ + int posix_openpt(int flags); + + /* Set the credentials of a pseudoterminal master. */ + int grantpt(int fd); + + /* Unlock a pseudoterminal master. */ + int unlockpt(int fd); + + /* Return the name of the slave associated with a pseudoterminal master. */ + char* ptsname(int fd); + #ifdef __cplusplus } #endif diff --git a/libc/src/stdlib.cpp b/libc/src/stdlib.cpp index b19b2a11..acbd7d9b 100644 --- a/libc/src/stdlib.cpp +++ b/libc/src/stdlib.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -7,8 +8,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -300,4 +303,31 @@ extern "C" { return strtod(str, nullptr); } + + int posix_openpt(int flags) + { + return open("/dev/ptmx", flags); + } + + int grantpt(int) + { + return 0; + } + + int unlockpt(int) + { + return 0; + } + + char* ptsname(int fd) + { + static char buffer[4096]; + + int index; + if (ioctl(fd, TIOCGPTN, &index) < 0) return nullptr; + + snprintf(buffer, sizeof(buffer), "/dev/pts/%d", index); + + return buffer; + } } diff --git a/libluna/include/luna/Buffer.h b/libluna/include/luna/Buffer.h index cd711c0a..3a561eaf 100644 --- a/libluna/include/luna/Buffer.h +++ b/libluna/include/luna/Buffer.h @@ -23,6 +23,9 @@ class Buffer Buffer(Buffer&& other); Buffer(const Buffer& other) = delete; // For now. + Buffer& operator=(Buffer&&); + Buffer& operator=(const Buffer&) = delete; + /** * @brief Create a Buffer object, allocating a specific amount of memory for it. * diff --git a/libluna/include/luna/CircularQueue.h b/libluna/include/luna/CircularQueue.h index a9e087f2..922fda0e 100644 --- a/libluna/include/luna/CircularQueue.h +++ b/libluna/include/luna/CircularQueue.h @@ -37,7 +37,7 @@ template class CircularQueue * @return true The queue is empty. * @return false The queue is not empty. */ - bool is_empty() + bool is_empty() const { return m_tail.load() == m_head.load(); } @@ -124,7 +124,7 @@ template class DynamicCircularQueue * @return true The queue is empty. * @return false The queue is not empty. */ - bool is_empty() + bool is_empty() const { return m_tail.load() == m_head.load(); } diff --git a/libluna/src/Buffer.cpp b/libluna/src/Buffer.cpp index b0a0b29b..188ec368 100644 --- a/libluna/src/Buffer.cpp +++ b/libluna/src/Buffer.cpp @@ -24,6 +24,16 @@ Buffer::Buffer(Buffer&& other) : m_data(other.data()), m_size(other.size()) other.m_data = nullptr; } +Buffer& Buffer::operator=(Buffer&& other) +{ + if (&other == this) return *this; + if (m_data) free_impl(m_data); + m_data = other.m_data; + m_size = other.m_size; + other.m_data = nullptr; + return *this; +} + Buffer::~Buffer() { if (m_data) free_impl(m_data); diff --git a/libos/CMakeLists.txt b/libos/CMakeLists.txt index df35f48a..321f4b4c 100644 --- a/libos/CMakeLists.txt +++ b/libos/CMakeLists.txt @@ -14,6 +14,10 @@ set(SOURCES src/Mode.cpp src/Prompt.cpp src/Security.cpp + src/LocalServer.cpp + src/LocalClient.cpp + src/IPC.cpp + src/SharedMemory.cpp ) add_library(os ${SOURCES}) diff --git a/libos/include/os/IPC.h b/libos/include/os/IPC.h new file mode 100644 index 00000000..fe185a15 --- /dev/null +++ b/libos/include/os/IPC.h @@ -0,0 +1,157 @@ +/** + * @file IPC.h + * @author apio (cloudapio.eu) + * @brief Inter-process communication primitives. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include + +#define IPC_ENUM_SERVER(name) __##name##_SERVER_ERROR = 0 +#define IPC_ENUM_CLIENT(name) __##name##_CLIENT_ERROR = 0 + +/** + * @brief Called to handle IPC events (client-side). + * + * @param conn The connection object being used. + * @param id The ID of the message. + * @return Result Whether the operation succeded. + */ +extern Result handle_ipc_client_event(os::LocalClient& conn, u8 id); + +/** + * @brief Called to handle IPC events (server-side). + * + * @param conn The connection object being used. + * @param id The ID of the message. + * @return Result Whether the operation succeded. + */ +extern Result handle_ipc_server_event(os::LocalServer::Client& conn, u8 id); + +namespace os +{ + namespace IPC + { + static constexpr usize IPC_STRING_LENGTH = 256; + +#define IPC_STRING(name) char name[os::IPC::IPC_STRING_LENGTH]; +#define COPY_IPC_STRING(name) \ + TRY(String::from_string_view(StringView::from_fixed_size_cstring(name, os::IPC::IPC_STRING_LENGTH))) +#define SET_IPC_STRING(name, value) strlcpy(name, value, os::IPC::IPC_STRING_LENGTH) + + /** + * @brief Sends an IPC message without waiting for a reply. + * + * @tparam Client The type of the client interface being used to communicate. + * @tparam T The type of the message. + * @param client The connection object being used to communicate. + * @param message The IPC message. + * @return Result Whether the operation succeded. + */ + template Result send_async(Client& client, const T& message) + { + u8 id = T::ID; + TRY(client.send_typed(id)); + TRY(client.send_typed(message)); + return {}; + } + + /** + * @brief Sends an error result to the IPC connection, indicating that an operation could not be performed. + * + * @tparam Client The type of the client interface being used to communicate. + * @param client The connection object being used to communicate. + * @param error The error code. + * @return Result Whether the operation succeded. + */ + template Result send_error(Client& client, int error) + { + u8 id = 0; + TRY(client.send_typed(id)); + TRY(client.send_typed(error)); + return {}; + } + + /** + * @brief Sends an IPC message and waits for a reply (client-only). + * + * @tparam ResponseType The type of the response. + * @tparam T The type of the message. + * @param client The connection object being used to communicate. + * @param message The IPC message. + * @param handler The function used to handle messages that do not match the reply. + * @return Result An error, or the response. + */ + template + Result send_sync(os::LocalClient& client, const T& message, + decltype(handle_ipc_client_event) handler = handle_ipc_client_event) + { + u8 id = T::ID; + TRY(client.send_typed(id)); + TRY(client.send_typed(message)); + + // We allow receiving 5 messages of different types, but if those have passed and we still don't have a + // reply, fail with ENOMSG. + int max_other_messages = 5; + + while (max_other_messages) + { + u8 response_id; + auto rc = client.recv_typed(response_id); + if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue; + + if (response_id == 0) // Error result + { + while (1) + { + int code; + rc = client.recv_typed(code); + if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue; + return err(code); + } + } + + if (response_id != ResponseType::ID) + { + TRY(handler(client, response_id)); + max_other_messages--; + continue; + } + + while (1) + { + ResponseType response; + rc = client.recv_typed(response); + if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue; + return response; + } + } + + return err(ENOMSG); + } + + /** + * @brief Check for new IPC messages on a connection and handle them appropriately. + * + * @param client The client connection. + * @param handler The function used to handle messages. + * @return Result Whether the operation succeded. + */ + Result check_for_messages(os::LocalClient& client, + decltype(handle_ipc_client_event) handler = handle_ipc_client_event); + + /** + * @brief Check for new IPC messages on a connection and handle them appropriately. + * + * @param server The server connection. + * @param handler The function used to handle messages. + * @return Result Whether the operation succeded. + */ + Result check_for_messages(os::LocalServer::Client& server, + decltype(handle_ipc_server_event) handler = handle_ipc_server_event); + } +} diff --git a/libos/include/os/LocalClient.h b/libos/include/os/LocalClient.h new file mode 100644 index 00000000..7ba1791d --- /dev/null +++ b/libos/include/os/LocalClient.h @@ -0,0 +1,99 @@ +/** + * @file LocalClient.h + * @author apio (cloudapio.eu) + * @brief UNIX local domain client class. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include + +namespace os +{ + /** + * @brief A client used to connect to a local server socket. + */ + class LocalClient + { + public: + /** + * @brief Create a new client object and connect it to a local server. + * + * @param path The path of the server socket to connect to. + * @param blocking Whether the client should block if no data is available and recv() is called. + * @return Result> An error, or a new client object. + */ + static Result> connect(StringView path, bool blocking); + + /** + * @brief Return the underlying socket file descriptor used by this object. + * + * @return int The file descriptor. + */ + int fd() const + { + return m_fd; + } + + /** + * @brief Read arbitrary data from the server. The call will block if there is no data and this object has not + * been created as non-blocking. + * + * @param buf The buffer to read data into. + * @param length The maximum amount of bytes to read. + * @return Result An error, or the number of bytes read. + */ + Result recv(u8* buf, usize length); + + /** + * @brief Read an object from the server. The call will block if there is no data and this object has not been + * created as non-blocking. + * + * @tparam T The type of the object. + * @param out A reference to the object to read data into. + * @return Result Whether the operation succeded. + */ + template Result recv_typed(T& out) + { + TRY(recv((u8*)&out, sizeof(T))); + return {}; + } + + /** + * @brief Send arbitrary data to the server. + * + * @param buf The buffer to send data from. + * @param length The amount of bytes to send. + * @return Result An error, or the number of bytes actually sent. + */ + Result send(const u8* buf, usize length); + + /** + * @brief Send an object to the server. + * + * @tparam T The type of the object. + * @param out A reference to the object to send data from. + * @return Result Whether the operation succeded. + */ + template Result send_typed(const T& out) + { + TRY(send((const u8*)&out, sizeof(T))); + return {}; + } + + /** + * @brief Disconnect from the attached server. + * + * This will make any further reads on this connection return ECONNRESET, and will make this object invalid. + */ + void disconnect(); + + ~LocalClient(); + + private: + int m_fd; + }; +} diff --git a/libos/include/os/LocalServer.h b/libos/include/os/LocalServer.h new file mode 100644 index 00000000..1adfa92a --- /dev/null +++ b/libos/include/os/LocalServer.h @@ -0,0 +1,142 @@ +/** + * @file LocalServer.h + * @author apio (cloudapio.eu) + * @brief UNIX local domain server class. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include + +namespace os +{ + /** + * @brief A local domain server, used to communicate between processes on the same machine. + */ + class LocalServer + { + public: + /** + * @brief Create a new server object and bind it to a local address. + * + * @param path The path to use for the server socket. + * @param blocking Whether the server should block if no connections are available when calling accept(). + * @return Result> An error, or a new server object. + */ + static Result> create(StringView path, bool blocking); + + /** + * @brief Activate the server and start listening for connections. + * + * @param backlog The number of unaccepted connections to keep. + * @return Result Whether the operation succeded. + */ + Result listen(int backlog); + + /** + * @brief Return the underlying socket file descriptor used by this object. + * + * @return int The file descriptor. + */ + int fd() const + { + return m_fd; + } + + /** + * @brief An interface to communicate with clients connected to a local server. + */ + class Client + { + public: + /** + * @brief Read arbitrary data from the client. The call will block if there is no data and the parent server + * object has not been created as non-blocking. + * + * @param buf The buffer to read data into. + * @param length The maximum amount of bytes to read. + * @return Result An error, or the number of bytes read. + */ + Result recv(u8* buf, usize length); + + /** + * @brief Read an object from the client. The call will block if there is no data and the parent server + * object has not been created as non-blocking. + * + * @tparam T The type of the object. + * @param out A reference to the object to read data into. + * @return Result Whether the operation succeded. + */ + template Result recv_typed(T& out) + { + TRY(recv((u8*)&out, sizeof(T))); + return {}; + } + + /** + * @brief Send arbitrary data to the client. + * + * @param buf The buffer to send data from. + * @param length The amount of bytes to send. + * @return Result An error, or the number of bytes actually sent. + */ + Result send(const u8* buf, usize length); + + /** + * @brief Send an object to the client. + * + * @tparam T The type of the object. + * @param out A reference to the object to send data from. + * @return Result Whether the operation succeded. + */ + template Result send_typed(const T& out) + { + TRY(send((const u8*)&out, sizeof(T))); + return {}; + } + + /** + * @brief Disconnect from the attached client. + * + * This will make any further reads on the client return ECONNRESET, and will make this object invalid. + */ + void disconnect(); + + /** + * @brief Return the underlying socket file descriptor used by this object. + * + * @return int The file descriptor. + */ + int fd() const + { + return m_fd; + } + + Client(Client&& other); + Client(int fd); + ~Client(); + + private: + int m_fd; + }; + + /** + * @brief Accept a new incoming connection and return a handle to it. If there are no incoming connections, + * accept() either blocks until there is one (if the object was created with blocking=true), or returns EAGAIN + * (if the object was created with blocking=false). + * + * @return Result An error, or a handle to the new connection. + */ + Result accept(); + + ~LocalServer(); + + private: + int m_fd; + bool m_blocking; + }; +} diff --git a/libos/include/os/SharedMemory.h b/libos/include/os/SharedMemory.h new file mode 100644 index 00000000..c1a9985c --- /dev/null +++ b/libos/include/os/SharedMemory.h @@ -0,0 +1,36 @@ +/** + * @file SharedMemory.h + * @author apio (cloudapio.eu) + * @brief Create and map areas of memory shared between processes. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include + +namespace os +{ + namespace SharedMemory + { + /** + * @brief Create a new shared memory region and map it. + * + * @param path The shared memory path to use. It must be of the same format as shm_open(). + * @param size The amount of bytes to use for the shared memory region. + * @return Result An error, or a pointer to the shared memory region. + */ + Result create(StringView path, usize size); + + /** + * @brief Map an existing shared memory region, possibly created by another process. + * + * @param path The shared memory path to use. It must be of the same format as shm_open(). + * @param size The amount of bytes to map from the shared memory region. + * @param delete_fs Whether to delete the region from the file system so no other processes can open it. + * @return Result An error, or a pointer to the shared memory region. + */ + Result adopt(StringView path, usize size, bool delete_fs = true); + }; +} diff --git a/libos/src/IPC.cpp b/libos/src/IPC.cpp new file mode 100644 index 00000000..b0be457d --- /dev/null +++ b/libos/src/IPC.cpp @@ -0,0 +1,43 @@ +/** + * @file IPC.cpp + * @author apio (cloudapio.eu) + * @brief Inter-process communication primitives. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include + +namespace os::IPC +{ + Result check_for_messages(os::LocalClient& client, decltype(handle_ipc_client_event) handler) + { + u8 id; + auto rc = client.recv_typed(id); + if (rc.has_error()) + { + if (rc.error() == EAGAIN) return {}; // No messages, and the caller does not want us to block. + if (rc.error() == EINTR) + return {}; // Let the caller check for anything having happened because a signal handler ran. + return rc.release_error(); + } + + return handler(client, id); + } + + Result check_for_messages(os::LocalServer::Client& client, decltype(handle_ipc_server_event) handler) + { + u8 id; + auto rc = client.recv_typed(id); + if (rc.has_error()) + { + if (rc.error() == EAGAIN) return {}; // No messages, and the caller does not want us to block. + if (rc.error() == EINTR) + return {}; // Let the caller check for anything having happened because a signal handler ran. + return rc.release_error(); + } + + return handler(client, id); + } +} diff --git a/libos/src/LocalClient.cpp b/libos/src/LocalClient.cpp new file mode 100644 index 00000000..f2263b09 --- /dev/null +++ b/libos/src/LocalClient.cpp @@ -0,0 +1,68 @@ +/** + * @file LocalClient.cpp + * @author apio (cloudapio.eu) + * @brief UNIX local domain client class. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include +#include +#include +#include +#include + +namespace os +{ + Result> LocalClient::connect(StringView path, bool blocking) + { + auto client = TRY(make_owned()); + + int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sockfd < 0) return err(errno); + + struct sockaddr_un un; + un.sun_family = AF_UNIX; + strncpy(un.sun_path, path.chars(), sizeof(un.sun_path)); + + if (::connect(sockfd, (struct sockaddr*)&un, sizeof(un)) < 0) + { + close(sockfd); + return err(errno); + } + + if (!blocking) { fcntl(sockfd, F_SETFL, O_NONBLOCK); } + + fcntl(sockfd, F_SETFD, FD_CLOEXEC); + + client->m_fd = sockfd; + return client; + } + + LocalClient::~LocalClient() + { + close(m_fd); + } + + Result LocalClient::recv(u8* buf, usize length) + { + ssize_t nread = read(m_fd, buf, length); + if (nread < 0) return err(errno); + return nread; + } + + Result LocalClient::send(const u8* buf, usize length) + { + ssize_t nwrite = write(m_fd, buf, length); + if (nwrite < 0) return err(errno); + return nwrite; + } + + void LocalClient::disconnect() + { + close(m_fd); + m_fd = -1; + } +} diff --git a/libos/src/LocalServer.cpp b/libos/src/LocalServer.cpp new file mode 100644 index 00000000..94e5d555 --- /dev/null +++ b/libos/src/LocalServer.cpp @@ -0,0 +1,101 @@ +/** + * @file LocalServer.cpp + * @author apio (cloudapio.eu) + * @brief UNIX local domain server class. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace os +{ + Result> LocalServer::create(StringView path, bool blocking) + { + auto server = TRY(make_owned()); + + (void)os::FileSystem::remove(path); // We explicitly ignore any error here, either it doesn't exist (which is + // fine), or it cannot be removed, which will make bind() fail later. + + int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sockfd < 0) return err(errno); + + struct sockaddr_un un; + un.sun_family = AF_UNIX; + strncpy(un.sun_path, path.chars(), sizeof(un.sun_path)); + + if (bind(sockfd, (struct sockaddr*)&un, sizeof(un)) < 0) + { + close(sockfd); + return err(errno); + } + + if (!blocking) { fcntl(sockfd, F_SETFL, O_NONBLOCK); } + server->m_blocking = blocking; + + fcntl(sockfd, F_SETFD, FD_CLOEXEC); + + server->m_fd = sockfd; + return server; + } + + Result LocalServer::listen(int backlog) + { + if (::listen(m_fd, backlog) < 0) return err(errno); + return {}; + } + + Result LocalServer::accept() + { + int fd = ::accept(m_fd, nullptr, nullptr); + if (fd < 0) return err(errno); + if (!m_blocking) fcntl(fd, F_SETFL, O_NONBLOCK); + return Client { fd }; + } + + LocalServer::~LocalServer() + { + close(m_fd); + } + + LocalServer::Client::Client(Client&& other) : m_fd(other.m_fd) + { + other.m_fd = -1; + } + + LocalServer::Client::Client(int fd) : m_fd(fd) + { + } + + LocalServer::Client::~Client() + { + if (m_fd >= 0) close(m_fd); + } + + Result LocalServer::Client::recv(u8* buf, usize length) + { + ssize_t nread = read(m_fd, buf, length); + if (nread < 0) return err(errno); + return nread; + } + + Result LocalServer::Client::send(const u8* buf, usize length) + { + ssize_t nwrite = write(m_fd, buf, length); + if (nwrite < 0) return err(errno); + return nwrite; + } + + void LocalServer::Client::disconnect() + { + close(m_fd); + m_fd = -1; + } +} diff --git a/libos/src/SharedMemory.cpp b/libos/src/SharedMemory.cpp new file mode 100644 index 00000000..743567f7 --- /dev/null +++ b/libos/src/SharedMemory.cpp @@ -0,0 +1,77 @@ +/** + * @file SharedMemory.cpp + * @author apio (cloudapio.eu) + * @brief Create and map areas of memory shared between processes. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace os::SharedMemory +{ + Result create(StringView path, usize size) + { + int fd = shm_open(path.chars(), O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd < 0) + { + int olderr = errno; + os::eprintln("os: could not create shared memory region: shm_open failed (%s) - %s", path, + strerror(olderr)); + return err(olderr); + } + + size = align_up(size); + + if (ftruncate(fd, size) < 0) + { + int olderr = errno; + os::eprintln("os: could not create shared memory region: ftruncate failed (%d, %zu) - %s", fd, size, + strerror(olderr)); + shm_unlink(path.chars()); + close(fd); + return err(olderr); + } + + void* p = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (p == MAP_FAILED) + { + int olderr = errno; + os::eprintln("os: could not create shared memory region: mmap failed (%zu, %d) - %s", size, fd, + strerror(olderr)); + shm_unlink(path.chars()); + close(fd); + return err(olderr); + } + + close(fd); + return (u8*)p; + } + + Result adopt(StringView path, usize size, bool delete_fs) + { + int fd = shm_open(path.chars(), O_RDWR, 0600); + if (delete_fs) shm_unlink(path.chars()); + if (fd < 0) return err(errno); + + void* p = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (p == MAP_FAILED) + { + int olderr = errno; + os::eprintln("os: could not adopt shared memory region: mmap failed (%zu, %d) - %s", size, fd, + strerror(olderr)); + close(fd); + return 0; + } + + close(fd); + return (u8*)p; + } +} diff --git a/libui/CMakeLists.txt b/libui/CMakeLists.txt new file mode 100644 index 00000000..04dcfcdc --- /dev/null +++ b/libui/CMakeLists.txt @@ -0,0 +1,31 @@ +# The UI and graphics library for Luna. + +file(GLOB HEADERS include/ui/*.h) + +set(SOURCES + ${HEADERS} + include/ui/ipc/Server.h + include/ui/ipc/Client.h + src/Canvas.cpp + src/Rect.cpp + src/Font.cpp + src/Image.cpp + src/App.cpp + src/Window.cpp + src/Layout.cpp + src/Alignment.cpp + src/Container.cpp + src/Button.cpp + src/Label.cpp +) + +add_library(ui ${SOURCES}) +target_compile_options(ui PRIVATE ${COMMON_FLAGS} -fno-threadsafe-statics) +target_include_directories(ui PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include/) +target_include_directories(ui PUBLIC ${LUNA_BASE}/usr/include) +target_link_libraries(ui PUBLIC os) + +add_custom_command( + TARGET ui + COMMAND "${CMAKE_COMMAND}" -E copy ${CMAKE_CURRENT_BINARY_DIR}/libui.a ${LUNA_BASE}/usr/lib/libui.a +) diff --git a/libui/include/ui/Alignment.h b/libui/include/ui/Alignment.h new file mode 100644 index 00000000..660eb605 --- /dev/null +++ b/libui/include/ui/Alignment.h @@ -0,0 +1,30 @@ +/** + * @file Alignment.h + * @author apio (cloudapio.eu) + * @brief UI component alignment. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include + +namespace ui +{ + enum class VerticalAlignment + { + Top, + Center, + Bottom + }; + + enum class HorizontalAlignment + { + Left, + Center, + Right + }; + + Rect align(Rect container, Rect contained, VerticalAlignment valign, HorizontalAlignment halign); +} diff --git a/libui/include/ui/App.h b/libui/include/ui/App.h new file mode 100644 index 00000000..b297c14b --- /dev/null +++ b/libui/include/ui/App.h @@ -0,0 +1,69 @@ +/** + * @file App.h + * @author apio (cloudapio.eu) + * @brief UI application event loop. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include + +namespace ui +{ + class App + { + public: + App(); + ~App(); + + Result init(int, char**); + Result run(); + + Rect screen_rect(); + + os::LocalClient& client() + { + return *m_client; + } + + void set_should_close(bool b) + { + m_should_close = b; + } + + void set_nonblocking(); + + void set_main_window(Window* window) + { + check(!m_main_window); + m_main_window = window; + } + + Window* main_window() + { + return m_main_window; + } + + Result register_window(OwnedPtr&& window, Badge); + void unregister_window(Window* window, Badge); + + Result handle_ipc_event(u8 id); + + bool process_events(); + + static App& the(); + + private: + static App* s_app; + OwnedPtr m_client; + Window* m_main_window { nullptr }; + HashMap> m_windows; + bool m_should_close { false }; + + Window* find_window(int id); + }; +} diff --git a/libui/include/ui/Button.h b/libui/include/ui/Button.h new file mode 100644 index 00000000..0fbcd2df --- /dev/null +++ b/libui/include/ui/Button.h @@ -0,0 +1,36 @@ +/** + * @file Button.h + * @author apio (cloudapio.eu) + * @brief A clickable component that triggers an action when pressed. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include + +namespace ui +{ + class Button : public Widget + { + public: + Button(Rect rect); + + void set_widget(Widget& widget); + void set_action(void (*action)(void)); + + Result handle_mouse_move(Point position) override; + Result handle_mouse_leave() override; + Result handle_mouse_down(Point position, int buttons) override; + Result handle_mouse_up(Point position, int buttons) override; + Result handle_key_event(const ui::KeyEventRequest& request) override; + Result draw(Canvas& canvas) override; + + private: + bool m_hovered { false }; + bool m_clicked { false }; + Widget* m_child; + void (*m_action)(void); + }; +} diff --git a/libui/include/ui/Canvas.h b/libui/include/ui/Canvas.h new file mode 100644 index 00000000..fb410638 --- /dev/null +++ b/libui/include/ui/Canvas.h @@ -0,0 +1,73 @@ +/** + * @file Canvas.h + * @author apio (cloudapio.eu) + * @brief Drawable surfaces. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include +#include + +namespace ui +{ + /** + * @brief A drawable surface. + */ + struct Canvas + { + int width; + int height; + int stride; + u8* ptr; + + /** + * @brief Create a new Canvas object. + * + * @param ptr The memory to use for the canvas. It must be of at least width * height * 4 bytes of length. + * @param width The width of the canvas. + * @param height The height of the canvas. + * @return Canvas The new Canvas object. + */ + static Canvas create(u8* ptr, int width, int height); + + /** + * @brief Return a new Canvas that represents a subsection of the current one. + * + * @param rect The dimensions of the new canvas. If these exceed the bounds of the current canvas, they will be + * clamped. + * @return Canvas The new Canvas object. + */ + Canvas subcanvas(Rect rect); + + /** + * @brief Return the dimensions of the current canvas. + * + * @return Rect This canvas's dimensions, as a Rect object. + */ + Rect rect() + { + return Rect { .pos = { 0, 0 }, .width = width, .height = height }; + } + + /** + * @brief Fill the entire canvas with one color. + * + * @param color The color to use. + */ + void fill(Color color); + + /** + * @brief Fill the canvas with pixels. + * + * @param pixels The array of pixels (must be at least width*height). + * @param stride The number of pixels to skip to go to the next line. + */ + void fill(u32* pixels, int stride); + }; +}; diff --git a/libui/include/ui/Color.h b/libui/include/ui/Color.h new file mode 100644 index 00000000..47a6d715 --- /dev/null +++ b/libui/include/ui/Color.h @@ -0,0 +1,113 @@ +/** + * @file Color.h + * @author apio (cloudapio.eu) + * @brief RGBA colors. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include + +namespace ui +{ + /** + * @brief A 32-bit ARGB color. + */ + struct Color + { + union { + u32 raw; + u8 colors[4]; + }; + + /** + * @brief Return the blue value of this color. + * + * @return constexpr u8 The blue value. + */ + constexpr u8 red() const + { + return colors[2]; + } + + /** + * @brief Return the green value of this color. + * + * @return constexpr u8 The green value. + */ + constexpr u8 green() const + { + return colors[1]; + } + + /** + * @brief Return the blue value of this color. + * + * @return constexpr u8 The blue value. + */ + constexpr u8 blue() const + { + return colors[0]; + } + + /** + * @brief Return the alpha value of this color. + * + * @return constexpr u8 The alpha value. + */ + constexpr u8 alpha() const + { + return colors[3]; + } + + /** + * @brief Construct a new color from a 32-bit ARGB integer. + * + * @param raw The integer representing the color. + * @return constexpr Color The new color. + */ + static constexpr Color from_u32(u32 raw) + { + return Color { .raw = raw }; + } + + /** + * @brief Construct a new color from its separate RGBA values (from 0 to 255). + * + * @param red The red value. + * @param green The green value. + * @param blue The blue value. + * @param alpha The alpha value. + * @return constexpr Color The new color. + */ + static constexpr Color from_rgba(u8 red, u8 green, u8 blue, u8 alpha) + { + return Color { .colors = { blue, green, red, alpha } }; + } + + /** + * @brief Construct a new color from its separate RGB values (from 0 to 255). + * + * @param red The red value. + * @param green The green value. + * @param blue The blue value. + * @return constexpr Color The new color. + */ + static constexpr Color from_rgb(u8 red, u8 green, u8 blue) + { + return from_rgba(red, green, blue, 0xff); + } + }; + + static constexpr Color WHITE = Color::from_rgb(0xff, 0xff, 0xff); + static constexpr Color BLACK = Color::from_rgb(0x00, 0x00, 0x00); + static constexpr Color GRAY = Color::from_rgb(0x80, 0x80, 0x80); + + static constexpr Color BLUE = Color::from_rgb(0x00, 0x00, 0xff); + static constexpr Color GREEN = Color::from_rgb(0x00, 0xff, 0x00); + static constexpr Color RED = Color::from_rgb(0xff, 0x00, 0x00); + + static constexpr Color CYAN = Color::from_rgb(0x00, 0xff, 0xff); +}; diff --git a/libui/include/ui/Container.h b/libui/include/ui/Container.h new file mode 100644 index 00000000..36337d67 --- /dev/null +++ b/libui/include/ui/Container.h @@ -0,0 +1,35 @@ +/** + * @file Container.h + * @author apio (cloudapio.eu) + * @brief A container widget to pad and align objects inside it. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include + +namespace ui +{ + class Container : public Widget + { + public: + Container(Rect rect, VerticalAlignment valign, HorizontalAlignment halign); + + void set_widget(Widget& widget); + + Result handle_mouse_move(Point position) override; + Result handle_mouse_leave() override; + Result handle_mouse_down(Point position, int buttons) override; + Result handle_mouse_up(Point position, int buttons) override; + Result handle_key_event(const ui::KeyEventRequest& request) override; + Result draw(Canvas& canvas) override; + + private: + Widget* m_widget; + VerticalAlignment m_valign; + HorizontalAlignment m_halign; + }; +} diff --git a/libui/include/ui/Font.h b/libui/include/ui/Font.h new file mode 100644 index 00000000..0d692972 --- /dev/null +++ b/libui/include/ui/Font.h @@ -0,0 +1,123 @@ +/** + * @file Font.h + * @author apio (cloudapio.eu) + * @brief PSF font loading and rendering. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include + +#define PSF_FONT_MAGIC 0x864ab572 + +namespace ui +{ + /** + * @brief A class holding PSF font data, used for low-level direct rendering of glyphs into a canvas. + * + * This class does not handle special characters such as tabs or newlines. For those, you should be using a more + * high-level component such as ui::Label instead. + */ + class Font : public Shareable + { + public: + /** + * @brief An enum used to select a font weight when loading a font. + */ + enum FontWeight + { + Regular, + Bold, + }; + + /** + * @brief Load a Font object from a font file. + * + * @param path The full path to the font file. + * @return Result> An error, or the loaded Font object. + */ + static Result> load(const os::Path& path); + + /** + * @brief Load a system font by name. + * + * @param name The name of the font to load (the default system font is "Tamsyn"). + * @param weight The weight of the font (regular or bold). + * @return Result> An error, or the loaded Font object. + */ + static Result> load_builtin(StringView name, FontWeight weight); + + /** + * @brief Return a pointer to the system's default font. + * + * @return SharedPtr The default font. + */ + static SharedPtr default_font(); + + /** + * @brief Return a pointer to the system's default bold font. + * + * @return SharedPtr The default bold font. + */ + static SharedPtr default_bold_font(); + + /** + * @brief Render a single Unicode code point onto a canvas, using this font's glyphs. + * + * @param codepoint The code point to render. + * @param color The color to draw the code point in. + * @param canvas The canvas to use. + */ + void render(wchar_t codepoint, ui::Color color, ui::Canvas& canvas); + + /** + * @brief Render a Unicode text string onto a canvas, using this font's glyphs. + * + * @param text The string to render (must be null-terminated). + * @param color The color to draw the code point in. + * @param canvas The canvas to use. + */ + void render(const wchar_t* text, ui::Color color, ui::Canvas& canvas); + + /** + * @brief Return the width of this font's glyphs. + * + * @return int The width. + */ + int width() const + { + return m_psf_header.width; + } + + /** + * @brief Return the height of this font's glyphs. + * + * @return int The height. + */ + int height() const + { + return m_psf_header.height; + } + + private: + struct PSFHeader + { + u32 magic; + u32 version; // zero + u32 headersize; + u32 flags; // 0 if there's no unicode table + u32 numglyph; + u32 bytesperglyph; + int height; + int width; + }; + + PSFHeader m_psf_header; + Buffer m_font_data; + }; +}; diff --git a/libui/include/ui/Image.h b/libui/include/ui/Image.h new file mode 100644 index 00000000..cbcc5485 --- /dev/null +++ b/libui/include/ui/Image.h @@ -0,0 +1,92 @@ +/** + * @file Image.h + * @author apio (cloudapio.eu) + * @brief TGA image loading and rendering. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include + +namespace ui +{ + /** + * @brief An image in the TGA file format. + */ + class Image : public Shareable + { + public: + /** + * @brief Load a new TGA image from a file. + * + * @param path The path to open. + * @return Result> An error, or a new Image object. + */ + static Result> load(const os::Path& path); + + /** + * @brief Return the array of pixels contained in the image. + * + * @return u32* The array of pixels. + */ + u32* pixels() + { + return (u32*)m_image_data.data(); + } + + /** + * @brief Return the width of the image. + * + * @return u16 The width. + */ + u16 width() + { + return m_tga_header.w; + } + + /** + * @brief Return the height of the image. + * + * @return u16 The height. + */ + u16 height() + { + return m_tga_header.h; + } + + private: + struct [[gnu::packed]] TGAHeader + { + u8 idlen; + u8 colormap; + u8 encoding; + u16 cmaporig, cmaplen; + u8 cmapent; + u16 x; + u16 y; + u16 w; + u16 h; + u8 bpp; + u8 pixeltype; + }; + + TGAHeader m_tga_header; + Buffer m_image_data; + }; + + class ImageWidget final : public Widget + { + public: + static Result> load(const os::Path& path); + + Result draw(Canvas& canvas) override; + + private: + SharedPtr m_image; + }; +} diff --git a/libui/include/ui/Key.h b/libui/include/ui/Key.h new file mode 100644 index 00000000..52aca98d --- /dev/null +++ b/libui/include/ui/Key.h @@ -0,0 +1,14 @@ +#pragma once +#include + +namespace ui +{ + enum Modifier + { + Mod_Shift = (1 << 0), + Mod_Alt = (1 << 1), + Mod_Super = (1 << 2), + Mod_AltGr = (1 << 3), + Mod_Ctrl = (1 << 4) + }; +} diff --git a/libui/include/ui/Label.h b/libui/include/ui/Label.h new file mode 100644 index 00000000..ca449335 --- /dev/null +++ b/libui/include/ui/Label.h @@ -0,0 +1,42 @@ +/** + * @file Label.h + * @author apio (cloudapio.eu) + * @brief A simple one-line text widget. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include + +namespace ui +{ + /** + * @brief Displays one line of text. + * + * This component does not handle newlines. + */ + class Label final : public Widget + { + public: + Label(StringView text, ui::Color color = ui::WHITE, VerticalAlignment valign = VerticalAlignment::Center, + HorizontalAlignment halign = HorizontalAlignment::Center, SharedPtr font = Font::default_font()); + + void set_text(StringView text) + { + m_text = text; + } + + Result draw(Canvas& canvas) override; + + private: + StringView m_text; + VerticalAlignment m_valign; + HorizontalAlignment m_halign; + ui::Color m_color; + SharedPtr m_font; + }; +} diff --git a/libui/include/ui/Layout.h b/libui/include/ui/Layout.h new file mode 100644 index 00000000..3b0e8ad7 --- /dev/null +++ b/libui/include/ui/Layout.h @@ -0,0 +1,71 @@ +/** + * @file Layout.h + * @author apio (cloudapio.eu) + * @brief Layout widgets to organize content. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include + +namespace ui +{ + enum class AdjustHeight + { + No, + Yes + }; + + enum class AdjustWidth + { + No, + Yes + }; + + class HorizontalLayout final : public Widget + { + public: + HorizontalLayout(AdjustHeight adjust_height = AdjustHeight::Yes, AdjustWidth adjust_width = AdjustWidth::Yes); + + Result handle_mouse_move(Point position) override; + Result handle_mouse_leave() override; + Result handle_mouse_down(Point position, int buttons) override; + Result handle_mouse_up(Point position, int buttons) override; + Result handle_key_event(const ui::KeyEventRequest& request) override; + + Result draw(Canvas& canvas) override; + + Result add_widget(Widget& widget); + + private: + Vector m_widgets; + AdjustHeight m_adjust_height; + AdjustWidth m_adjust_width; + int m_used_width { 0 }; + }; + + class VerticalLayout final : public Widget + { + public: + VerticalLayout(AdjustHeight adjust_height = AdjustHeight::Yes, AdjustWidth adjust_width = AdjustWidth::Yes); + + Result handle_mouse_move(Point position) override; + Result handle_mouse_leave() override; + Result handle_mouse_down(Point position, int buttons) override; + Result handle_mouse_up(Point position, int buttons) override; + Result handle_key_event(const ui::KeyEventRequest& request) override; + + Result draw(Canvas& canvas) override; + + Result add_widget(Widget& widget); + + private: + Vector m_widgets; + AdjustHeight m_adjust_height; + AdjustWidth m_adjust_width; + int m_used_height { 0 }; + }; +} diff --git a/libui/include/ui/Mouse.h b/libui/include/ui/Mouse.h new file mode 100644 index 00000000..db28dda7 --- /dev/null +++ b/libui/include/ui/Mouse.h @@ -0,0 +1,21 @@ +/** + * @file Mouse.h + * @author apio (cloudapio.eu) + * @brief Mouse buttons. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include + +namespace ui +{ + enum MouseButtons + { + LEFT = moon::Left, + MIDDLE = moon::Middle, + RIGHT = moon::Right, + }; +} diff --git a/libui/include/ui/Point.h b/libui/include/ui/Point.h new file mode 100644 index 00000000..f5837793 --- /dev/null +++ b/libui/include/ui/Point.h @@ -0,0 +1,22 @@ +/** + * @file Point.h + * @author apio (cloudapio.eu) + * @brief 2D space points. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once + +namespace ui +{ + /** + * @brief A point in 2D space. + */ + struct Point + { + int x { 0 }; + int y { 0 }; + }; +} diff --git a/libui/include/ui/Rect.h b/libui/include/ui/Rect.h new file mode 100644 index 00000000..8ac8d805 --- /dev/null +++ b/libui/include/ui/Rect.h @@ -0,0 +1,81 @@ +/** + * @file Rect.h + * @author apio (cloudapio.eu) + * @brief A simple 2D rectangle representation. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include + +namespace ui +{ + /** + * @brief A simple rectangle. + */ + struct Rect + { + Point pos; + int width; + int height; + + /** + * @brief Check if a point is contained in this rectangle. + * + * @param point The point to check. + * @return true The point is contained inside the rectangle. + * @return false The point is not contained inside the rectangle. + */ + bool contains(Point point); + + /** + * @brief Check if another rectangle is contained in this one. + * + * @param point The rectangle to check. + * @return true The other rectangle is contained inside this one. + * @return false The other rectangle is not contained inside this one. + */ + bool contains(Rect rect); + + /** + * @brief Normalize a point to fit inside this rectangle. + * + * @param point The original point. + * @return Point The normalized point. + */ + Point normalize(Point point); + + /** + * @brief Transform an absolute position to a position relative to this rectangle. + * + * @param pos The original absolute position. + * @return Point The position relative to this rectangle. + */ + Point relative(Point pos); + + /** + * @brief Transform a position relative to this rectangle to an absolute position. + * + * @param pos The original relative position. + * @return Point The absolute position. + */ + Point absolute(Point pos); + + /** + * @brief Transform another rectangle relative to this one to an absolute rectangle. + * + * @param rect The original relative rectangle. + * @return Point The absolute rectangle. + */ + Rect absolute(Rect rect); + + /** + * @brief Return a copy of this rectangle with no negative values (normalized to 0). + * + * @return Rect The new rectangle. + */ + Rect normalized(); + }; +} diff --git a/libui/include/ui/Widget.h b/libui/include/ui/Widget.h new file mode 100644 index 00000000..ea690f6e --- /dev/null +++ b/libui/include/ui/Widget.h @@ -0,0 +1,94 @@ +/** + * @file Widget.h + * @author apio (cloudapio.eu) + * @brief Abstract widget class. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace ui +{ + class Window; + + enum class EventResult + { + DidHandle, + DidNotHandle, + }; + + class Widget + { + public: + virtual Result handle_mouse_move(Point position) + { + ignore(position); + return EventResult::DidNotHandle; + } + + virtual Result handle_mouse_down(Point position, int buttons) + { + ignore(position, buttons); + return EventResult::DidNotHandle; + } + + virtual Result handle_mouse_up(Point position, int buttons) + { + ignore(position, buttons); + return EventResult::DidNotHandle; + } + + virtual Result handle_mouse_leave() + { + return EventResult::DidNotHandle; + } + + virtual Result handle_key_event(const ui::KeyEventRequest& request) + { + ignore(request); + return EventResult::DidNotHandle; + } + + virtual Result draw(Canvas& canvas); + + void set_window(Window* window, Rect rect, Badge) + { + m_window = window; + m_rect = rect; + } + + void set_parent(Widget* parent) + { + m_parent = parent; + m_window = parent->m_window; + } + + Widget* parent() + { + return m_parent; + } + + Window* window() + { + return m_window; + } + + Rect& rect() + { + return m_rect; + } + + protected: + Widget* m_parent { nullptr }; + Window* m_window; + Rect m_rect { 0, 0, 50, 50 }; + }; +} diff --git a/libui/include/ui/Window.h b/libui/include/ui/Window.h new file mode 100644 index 00000000..3e121a8f --- /dev/null +++ b/libui/include/ui/Window.h @@ -0,0 +1,68 @@ +/** + * @file Window.h + * @author apio (cloudapio.eu) + * @brief UI windows. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace ui +{ + class Window + { + public: + static Result create(Rect rect, bool decorated = true); + + void set_title(StringView title); + + void set_background(Color color) + { + m_background = color; + } + + void set_main_widget(Widget& widget) + { + check(!m_main_widget); + widget.set_window(this, m_canvas.rect(), {}); + m_main_widget = &widget; + } + + Canvas& canvas() + { + return m_canvas; + } + + void update(); + + void close(); + + Result draw(); + Result handle_mouse_leave(); + Result handle_mouse_move(ui::Point position); + Result handle_mouse_buttons(ui::Point position, int buttons); + Result handle_key_event(const ui::KeyEventRequest& request); + + int id() const + { + return m_id; + } + + ~Window(); + + private: + int m_id; + Canvas m_canvas; + Widget* m_main_widget { nullptr }; + Option m_background {}; + Option m_old_mouse_buttons; + }; +} diff --git a/libui/include/ui/ipc/Client.h b/libui/include/ui/ipc/Client.h new file mode 100644 index 00000000..f042d3f1 --- /dev/null +++ b/libui/include/ui/ipc/Client.h @@ -0,0 +1,80 @@ +/** + * @file ipc/Client.h + * @author apio (cloudapio.eu) + * @brief IPC message definitions for UI messages sent to the client. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include + +namespace ui +{ + enum ClientMessages : u8 + { + IPC_ENUM_CLIENT(ui), + CREATE_WINDOW_RESPONSE_ID, + WINDOW_CLOSE_REQUEST_ID, + MOUSE_EVENT_REQUEST_ID, + MOUSE_LEAVE_REQUEST_ID, + GET_SCREEN_RECT_RESPONSE_ID, + KEY_EVENT_REQUEST_ID, + }; + + struct CreateWindowResponse + { + static constexpr u8 ID = CREATE_WINDOW_RESPONSE_ID; + + int window; + IPC_STRING(shm_path); + }; + + struct WindowCloseRequest + { + static constexpr u8 ID = WINDOW_CLOSE_REQUEST_ID; + + int window; + }; + + struct MouseEventRequest + { + static constexpr u8 ID = MOUSE_EVENT_REQUEST_ID; + + int window; + Point position; + int buttons; + }; + + struct MouseLeaveRequest + { + static constexpr u8 ID = MOUSE_LEAVE_REQUEST_ID; + + int window; + }; + + struct GetScreenRectResponse + { + static constexpr u8 ID = GET_SCREEN_RECT_RESPONSE_ID; + + Rect rect; + }; + + struct KeyEventRequest + { + static constexpr u8 ID = KEY_EVENT_REQUEST_ID; + + int window; + + bool pressed; + + char letter; + char key; + moon::KeyCode code; + int modifiers; + }; +} diff --git a/libui/include/ui/ipc/Server.h b/libui/include/ui/ipc/Server.h new file mode 100644 index 00000000..7e675c53 --- /dev/null +++ b/libui/include/ui/ipc/Server.h @@ -0,0 +1,66 @@ +/** + * @file ipc/Server.h + * @author apio (cloudapio.eu) + * @brief IPC message definitions for UI messages sent to the server. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include + +namespace ui +{ + enum ServerMessages : u8 + { + IPC_ENUM_SERVER(ui), + CREATE_WINDOW_ID, + SET_WINDOW_TITLE_ID, + INVALIDATE_ID, + CLOSE_WINDOW_ID, + GET_SCREEN_RECT_ID, + }; + + struct CreateWindowRequest + { + using ResponseType = CreateWindowResponse; + static constexpr u8 ID = CREATE_WINDOW_ID; + + ui::Rect rect; + bool decorated; + }; + + struct SetWindowTitleRequest + { + static constexpr u8 ID = SET_WINDOW_TITLE_ID; + + int window; + IPC_STRING(title); + }; + + struct InvalidateRequest + { + static constexpr u8 ID = INVALIDATE_ID; + + int window; + }; + + struct CloseWindowRequest + { + static constexpr u8 ID = CLOSE_WINDOW_ID; + + int window; + }; + + struct GetScreenRectRequest + { + using ResponseType = GetScreenRectResponse; + static constexpr u8 ID = GET_SCREEN_RECT_ID; + + int _shadow; // Unused. + }; +} diff --git a/libui/src/Alignment.cpp b/libui/src/Alignment.cpp new file mode 100644 index 00000000..87292fdc --- /dev/null +++ b/libui/src/Alignment.cpp @@ -0,0 +1,40 @@ +/** + * @file Alignment.cpp + * @author apio (cloudapio.eu) + * @brief UI component alignment. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include + +namespace ui +{ + Rect align(Rect container, Rect contained, VerticalAlignment valign, HorizontalAlignment halign) + { + Rect result; + result.width = contained.width; + result.height = contained.height; + result.pos.y = container.pos.y; + result.pos.x = container.pos.x; + + switch (valign) + { + case VerticalAlignment::Top: break; + case VerticalAlignment::Center: result.pos.y += (container.height - contained.height) / 2; break; + case VerticalAlignment::Bottom: result.pos.y += container.height - contained.height; break; + default: break; + } + + switch (halign) + { + case HorizontalAlignment::Left: break; + case HorizontalAlignment::Center: result.pos.x += (container.width - contained.width) / 2; break; + case HorizontalAlignment::Right: result.pos.x += container.width - contained.width; break; + default: break; + } + + return result; + } +} diff --git a/libui/src/App.cpp b/libui/src/App.cpp new file mode 100644 index 00000000..7f4c9c98 --- /dev/null +++ b/libui/src/App.cpp @@ -0,0 +1,158 @@ +/** + * @file App.cpp + * @author apio (cloudapio.eu) + * @brief UI application event loop. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include +#include +#include +#include +#include + +Result handle_ipc_client_event(os::LocalClient&, u8 id) +{ + return ui::App::the().handle_ipc_event(id); +} + +namespace ui +{ + App* App::s_app { nullptr }; + + App::App() + { + s_app = this; + } + + App::~App() + { + s_app = nullptr; + } + + Result App::init(int argc, char** argv) + { + StringView socket_path = "/tmp/wind.sock"; + + os::ArgumentParser parser; + parser.add_description("A UI application."_sv); + parser.add_system_program_info(argv[0]); + parser.add_value_argument(socket_path, 's', "socket"_sv, "the path for the local IPC socket"_sv); + parser.parse(argc, argv); + + m_client = TRY(os::LocalClient::connect(socket_path, true)); + + return {}; + } + + Result App::run() + { + while (process_events()) + ; + return 0; + } + + App& App::the() + { + check(s_app); + return *s_app; + } + + Rect App::screen_rect() + { + ui::GetScreenRectRequest request {}; + auto response = os::IPC::send_sync(*m_client, request).release_value(); + return response.rect; + } + + Result App::register_window(OwnedPtr&& window, Badge) + { + int id = window->id(); + check(TRY(m_windows.try_set(id, move(window)))); + return {}; + } + + void App::unregister_window(Window* window, Badge) + { + int id = window->id(); + check(m_windows.try_remove(id)); + } + + Window* App::find_window(int id) + { + auto* window = m_windows.try_get_ref(id); + check(window); + return window->ptr(); + } + +#define READ_MESSAGE(request) \ + do { \ + auto rc = m_client->recv_typed(request); \ + if (rc.has_error()) \ + { \ + if (rc.error() == EAGAIN) { continue; } \ + if (rc.error() == EINTR) { continue; } \ + else \ + return rc.release_error(); \ + } \ + break; \ + } while (true) + + Result App::handle_ipc_event(u8 id) + { + switch (id) + { + case WINDOW_CLOSE_REQUEST_ID: { + WindowCloseRequest request; + READ_MESSAGE(request); + os::eprintln("ui: Window close request from server! Shall comply."); + auto* window = find_window(request.window); + window->close(); + return {}; + } + case MOUSE_EVENT_REQUEST_ID: { + MouseEventRequest request; + READ_MESSAGE(request); + auto* window = find_window(request.window); + auto move_result = window->handle_mouse_move(request.position).value_or(ui::EventResult::DidNotHandle); + auto button_result = + window->handle_mouse_buttons(request.position, request.buttons).value_or(ui::EventResult::DidNotHandle); + if (move_result == ui::EventResult::DidHandle || button_result == ui::EventResult::DidHandle) + window->draw(); + return {}; + } + case MOUSE_LEAVE_REQUEST_ID: { + MouseLeaveRequest request; + READ_MESSAGE(request); + auto* window = find_window(request.window); + if (window->handle_mouse_leave().value_or(ui::EventResult::DidNotHandle) == ui::EventResult::DidHandle) + window->draw(); + return {}; + } + case KEY_EVENT_REQUEST_ID: { + KeyEventRequest request; + READ_MESSAGE(request); + auto* window = find_window(request.window); + if (window->handle_key_event(request).value_or(ui::EventResult::DidNotHandle) == ui::EventResult::DidHandle) + window->draw(); + return {}; + } + default: fail("Unexpected IPC request from server!"); + } + } + + void App::set_nonblocking() + { + fcntl(m_client->fd(), F_SETFL, O_NONBLOCK); + } + + bool App::process_events() + { + check(m_main_window); + os::IPC::check_for_messages(*m_client).release_value(); + return !m_should_close; + } +} diff --git a/libui/src/Button.cpp b/libui/src/Button.cpp new file mode 100644 index 00000000..dd99d65b --- /dev/null +++ b/libui/src/Button.cpp @@ -0,0 +1,73 @@ +/** + * @file Button.cpp + * @author apio (cloudapio.eu) + * @brief A clickable component that triggers an action when pressed. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include + +namespace ui +{ + Button::Button(Rect rect) + { + m_rect = rect; + } + + void Button::set_widget(Widget& widget) + { + widget.rect() = m_rect; + m_child = &widget; + widget.set_parent(this); + } + + void Button::set_action(void (*action)(void)) + { + m_action = action; + } + + Result Button::handle_mouse_move(Point position) + { + m_hovered = true; + return m_child->handle_mouse_move(position); + } + + Result Button::handle_mouse_leave() + { + m_hovered = m_clicked = false; + return m_child->handle_mouse_leave(); + } + + Result Button::handle_mouse_down(Point position, int buttons) + { + auto result = TRY(m_child->handle_mouse_down(position, buttons)); + if (result == EventResult::DidNotHandle) + { + if (!m_clicked && (buttons == ui::MouseButtons::LEFT)) + { + m_clicked = true; + m_action(); + } + } + return EventResult::DidHandle; + } + + Result Button::handle_mouse_up(Point position, int buttons) + { + if (buttons & ui::MouseButtons::LEFT) m_clicked = false; + return m_child->handle_mouse_up(position, buttons); + } + + Result Button::handle_key_event(const ui::KeyEventRequest& request) + { + return m_child->handle_key_event(request); + } + + Result Button::draw(Canvas& canvas) + { + return m_child->draw(canvas); + } +} diff --git a/libui/src/Canvas.cpp b/libui/src/Canvas.cpp new file mode 100644 index 00000000..002f1592 --- /dev/null +++ b/libui/src/Canvas.cpp @@ -0,0 +1,62 @@ +/** + * @file Canvas.cpp + * @author apio (cloudapio.eu) + * @brief Drawable surfaces. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include + +namespace ui +{ + Canvas Canvas::create(u8* ptr, int width, int height) + { + return Canvas { .width = width, .height = height, .stride = width, .ptr = ptr }; + } + + Canvas Canvas::subcanvas(Rect rect) + { + if (rect.pos.x < 0) rect.pos.x = 0; + if (rect.pos.y < 0) rect.pos.y = 0; + if (rect.pos.x + rect.width > width) rect.width = width - rect.pos.x; + if (rect.pos.y + rect.height > height) rect.height = height - rect.pos.y; + + u8* p = ptr + rect.pos.x * sizeof(Color) + (rect.pos.y * sizeof(Color) * stride); + + return Canvas { .width = rect.width, .height = rect.height, .stride = stride, .ptr = p }; + } + + void Canvas::fill(Color color) + { + u8* p = ptr; + for (int i = 0; i < height; i++) + { + u32* colorp = (u32*)p; + for (int j = 0; j < width; j++) + { + *colorp = color.raw; + colorp++; + } + p += stride * sizeof(Color); + } + } + + void Canvas::fill(u32* pixels, int _stride) + { + u8* p = ptr; + for (int i = 0; i < height; i++) + { + u32* colorp = (u32*)p; + for (int j = 0; j < width; j++) + { + u32 pix = pixels[j]; + if (Color::from_u32(pix).alpha() == 0xff) *colorp = pix; + colorp++; + } + pixels += _stride; + p += stride * sizeof(Color); + } + } +} diff --git a/libui/src/Container.cpp b/libui/src/Container.cpp new file mode 100644 index 00000000..9b4fb07f --- /dev/null +++ b/libui/src/Container.cpp @@ -0,0 +1,63 @@ +/** + * @file Container.cpp + * @author apio (cloudapio.eu) + * @brief A container widget to pad and align objects inside it. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include + +namespace ui +{ + Container::Container(Rect rect, VerticalAlignment valign, HorizontalAlignment halign) + : m_valign(valign), m_halign(halign) + { + m_rect = rect; + } + + void Container::set_widget(Widget& widget) + { + m_widget = &widget; + widget.rect() = ui::align(m_rect, widget.rect(), m_valign, m_halign); + widget.set_parent(this); + } + + Result Container::handle_mouse_move(Point position) + { + if (m_widget->rect().contains(position)) return m_widget->handle_mouse_move(position); + return ui::EventResult::DidNotHandle; + } + + Result Container::handle_mouse_leave() + { + return m_widget->handle_mouse_leave(); + } + + Result Container::handle_mouse_down(Point position, int buttons) + { + if (m_widget->rect().contains(position)) return m_widget->handle_mouse_down(position, buttons); + return ui::EventResult::DidNotHandle; + } + + Result Container::handle_mouse_up(Point position, int buttons) + { + if (m_widget->rect().contains(position)) return m_widget->handle_mouse_up(position, buttons); + return ui::EventResult::DidNotHandle; + } + + Result Container::handle_key_event(const ui::KeyEventRequest& request) + { + return m_widget->handle_key_event(request); + } + + Result Container::draw(Canvas& canvas) + { + auto rect = ui::Rect { m_widget->rect().pos.x - m_rect.pos.x, m_widget->rect().pos.y - m_rect.pos.y, + m_widget->rect().width, m_widget->rect().height }; + auto subcanvas = canvas.subcanvas(rect); + return m_widget->draw(subcanvas); + } + +} diff --git a/libui/src/Font.cpp b/libui/src/Font.cpp new file mode 100644 index 00000000..0534680c --- /dev/null +++ b/libui/src/Font.cpp @@ -0,0 +1,121 @@ +/** + * @file Font.cpp + * @author apio (cloudapio.eu) + * @brief PSF font loading and rendering. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include +#include + +constexpr static int BYTES_PER_PIXEL = (int)sizeof(ui::Color); + +namespace ui +{ + Result> Font::load(const os::Path& path) + { + auto font = TRY(make_shared()); + + auto file = TRY(os::File::open(path, os::File::ReadOnly)); + + TRY(file->read_typed(font->m_psf_header)); + + if (font->m_psf_header.magic != PSF_FONT_MAGIC) + { + os::eprintln("ui::Font::load(%s) failed: font magic does not match PSF2 magic", path.name().chars()); + return err(ENOTSUP); + } + + if (font->m_psf_header.version != 0) + { + os::eprintln("ui::Font::load(%s) failed: font version is unsupported", path.name().chars()); + return err(ENOTSUP); + } + + if (font->m_psf_header.flags) + { + os::eprintln("ui::Font::load(%s) warning: font has a unicode table, which we're ignoring", + path.name().chars()); + // todo(); // Font has a unicode table, oh no! + } + + font->m_font_data = TRY(file->read_all()); // Read the rest of the file into the font data buffer. + + return font; + } + + Result> Font::load_builtin(StringView name, FontWeight weight) + { + auto path = TRY(String::format("/usr/share/fonts/%s-%s.psf"_sv, name.chars(), + weight == FontWeight::Bold ? "Bold" : "Regular")); + + return load(path.view()); + } + + SharedPtr Font::default_font() + { + static SharedPtr s_default_font = {}; + if (!s_default_font) s_default_font = load("/usr/share/fonts/Tamsyn-Regular.psf").release_value(); + return s_default_font; + } + + SharedPtr Font::default_bold_font() + { + static SharedPtr s_default_bold_font = {}; + if (!s_default_bold_font) s_default_bold_font = load("/usr/share/fonts/Tamsyn-Bold.psf").release_value(); + return s_default_bold_font; + } + + void Font::render(wchar_t codepoint, ui::Color color, ui::Canvas& canvas) + { + const wchar_t str[] = { codepoint, 0 }; + render(str, color, canvas); + } + + void Font::render(const wchar_t* text, ui::Color color, ui::Canvas& canvas) + { + usize len = wcslen(text); + + int height = m_psf_header.height; + int width = m_psf_header.width; + int last_char_width = width; + + if (canvas.width < (m_psf_header.width * static_cast(len))) + { + len = (canvas.width / width) + 1; + last_char_width = canvas.width % width; + } + + if (canvas.height < height) height = canvas.height; + + const int bytes_per_line = (m_psf_header.width + 7) / 8; + + for (usize i = 0; i < len; i++) + { + if (i + 1 == len) width = last_char_width; + wchar_t codepoint = text[i]; + + u8* glyph = + m_font_data.data() + (codepoint > 0 && codepoint < (wchar_t)m_psf_header.numglyph ? codepoint : 0) * + m_psf_header.bytesperglyph; + + u32 offset = (u32)i * m_psf_header.width * BYTES_PER_PIXEL; + for (int y = 0; y < height; y++) + { + u32 line = offset; + int mask = 1 << (m_psf_header.width - 1); + for (int x = 0; x < width; x++) + { + if (*((u32*)glyph) & mask) *(u32*)(canvas.ptr + line) = color.raw; + mask >>= 1; + line += BYTES_PER_PIXEL; + } + glyph += bytes_per_line; + offset += canvas.stride * BYTES_PER_PIXEL; + } + } + } +} diff --git a/libui/src/Image.cpp b/libui/src/Image.cpp new file mode 100644 index 00000000..f8b9d7f3 --- /dev/null +++ b/libui/src/Image.cpp @@ -0,0 +1,48 @@ +/** + * @file Image.cpp + * @author apio (cloudapio.eu) + * @brief TGA image loading and rendering. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include +#include + +namespace ui +{ + Result> Image::load(const os::Path& path) + { + auto image = TRY(make_shared()); + auto file = TRY(os::File::open(path, os::File::ReadOnly)); + + TRY(file->read_typed(image->m_tga_header)); + + if (image->m_tga_header.encoding != 2) todo(); + if (image->m_tga_header.bpp != 32) todo(); + + Buffer image_id; + TRY(file->read(image_id, image->m_tga_header.idlen)); + + TRY(file->read(image->m_image_data, + image->m_tga_header.w * image->m_tga_header.h * (image->m_tga_header.bpp / 8))); + + return image; + } + + Result> ImageWidget::load(const os::Path& path) + { + auto widget = TRY(make_owned()); + widget->m_image = TRY(Image::load(path)); + widget->m_rect = { 0, 0, widget->m_image->width(), widget->m_image->height() }; + return widget; + } + + Result ImageWidget::draw(Canvas& canvas) + { + canvas.subcanvas({ 0, 0, m_image->width(), m_image->height() }).fill(m_image->pixels(), m_image->width()); + return {}; + } +} diff --git a/libui/src/Label.cpp b/libui/src/Label.cpp new file mode 100644 index 00000000..71015c6e --- /dev/null +++ b/libui/src/Label.cpp @@ -0,0 +1,37 @@ +/** + * @file Label.cpp + * @author apio (cloudapio.eu) + * @brief A simple one-line text widget. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include + +namespace ui +{ + Label::Label(StringView text, ui::Color color, VerticalAlignment valign, HorizontalAlignment halign, + SharedPtr font) + : m_text(text), m_valign(valign), m_halign(halign), m_color(color), m_font(font) + { + } + + Result Label::draw(Canvas& canvas) + { + ui::Rect contained; + contained.pos = { 0, 0 }; + contained.width = static_cast(m_text.length() * m_font->width()); + contained.height = m_font->height(); + auto subcanvas = + canvas.subcanvas(ui::align({ 0, 0, m_rect.width, m_rect.height }, contained, m_valign, m_halign)); + + Utf8StringDecoder decoder(m_text.chars()); + wchar_t buf[4096]; + TRY(decoder.decode(buf, sizeof(buf))); + + m_font->render(buf, m_color, subcanvas); + return {}; + } +} diff --git a/libui/src/Layout.cpp b/libui/src/Layout.cpp new file mode 100644 index 00000000..300739ca --- /dev/null +++ b/libui/src/Layout.cpp @@ -0,0 +1,230 @@ +/** + * @file Layout.cpp + * @author apio (cloudapio.eu) + * @brief Layout widgets to organize content. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include + +namespace ui +{ + HorizontalLayout::HorizontalLayout(AdjustHeight adjust_height, AdjustWidth adjust_width) + : m_adjust_height(adjust_height), m_adjust_width(adjust_width) + { + } + + Result HorizontalLayout::handle_mouse_move(Point position) + { + EventResult result = ui::EventResult::DidNotHandle; + + for (auto widget : m_widgets) + { + if (widget->rect().contains(position)) result = TRY(widget->handle_mouse_move(position)); + else + TRY(widget->handle_mouse_leave()); + } + + return result; + } + + Result HorizontalLayout::handle_mouse_leave() + { + EventResult result = ui::EventResult::DidNotHandle; + + for (auto widget : m_widgets) + { + auto rc = TRY(widget->handle_mouse_leave()); + if (rc == ui::EventResult::DidHandle) result = rc; + } + + return result; + } + + Result HorizontalLayout::handle_mouse_up(Point position, int buttons) + { + for (auto widget : m_widgets) + { + if (widget->rect().contains(position)) return widget->handle_mouse_up(position, buttons); + } + + return ui::EventResult::DidNotHandle; + } + + Result HorizontalLayout::handle_mouse_down(Point position, int buttons) + { + for (auto widget : m_widgets) + { + if (widget->rect().contains(position)) return widget->handle_mouse_down(position, buttons); + } + + return ui::EventResult::DidNotHandle; + } + + Result HorizontalLayout::handle_key_event(const ui::KeyEventRequest& request) + { + EventResult result = ui::EventResult::DidNotHandle; + + for (auto widget : m_widgets) + { + auto rc = TRY(widget->handle_key_event(request)); + if (rc == ui::EventResult::DidHandle) result = rc; + } + + return result; + } + + Result HorizontalLayout::draw(Canvas& canvas) + { + for (auto widget : m_widgets) + { + ui::Rect rect = { m_rect.relative(widget->rect().pos), widget->rect().width, widget->rect().height }; + auto subcanvas = canvas.subcanvas(rect); + TRY(widget->draw(subcanvas)); + } + + return {}; + } + + Result HorizontalLayout::add_widget(Widget& widget) + { + TRY(m_widgets.try_append(&widget)); + + if (m_adjust_width == AdjustWidth::No) + { + widget.rect().pos.x = m_rect.pos.x + m_used_width; + m_used_width += widget.rect().width; + } + else + { + int used_width = 0; + div_t result = div(m_rect.width, (int)m_widgets.size()); + for (auto w : m_widgets) + { + w->rect().pos.x = m_rect.pos.x + used_width; + w->rect().width = result.quot; + used_width += result.quot; + } + m_widgets[m_widgets.size() - 1]->rect().width += result.rem; + } + + widget.rect().pos.y = m_rect.pos.y; + + if (m_adjust_height == AdjustHeight::Yes) { widget.rect().height = m_rect.height; } + + widget.set_parent(this); + + return {}; + } + + VerticalLayout::VerticalLayout(AdjustHeight adjust_height, AdjustWidth adjust_width) + : m_adjust_height(adjust_height), m_adjust_width(adjust_width) + { + } + + Result VerticalLayout::handle_mouse_move(Point position) + { + EventResult result = ui::EventResult::DidNotHandle; + + for (auto widget : m_widgets) + { + if (widget->rect().contains(position)) result = TRY(widget->handle_mouse_move(position)); + else + TRY(widget->handle_mouse_leave()); + } + + return result; + } + + Result VerticalLayout::handle_mouse_leave() + { + EventResult result = ui::EventResult::DidNotHandle; + + for (auto widget : m_widgets) + { + auto rc = TRY(widget->handle_mouse_leave()); + if (rc == ui::EventResult::DidHandle) result = rc; + } + + return result; + } + + Result VerticalLayout::handle_mouse_up(Point position, int buttons) + { + for (auto widget : m_widgets) + { + if (widget->rect().contains(position)) return widget->handle_mouse_up(position, buttons); + } + + return ui::EventResult::DidNotHandle; + } + + Result VerticalLayout::handle_mouse_down(Point position, int buttons) + { + for (auto widget : m_widgets) + { + if (widget->rect().contains(position)) return widget->handle_mouse_down(position, buttons); + } + + return ui::EventResult::DidNotHandle; + } + + Result VerticalLayout::handle_key_event(const ui::KeyEventRequest& request) + { + EventResult result = ui::EventResult::DidNotHandle; + + for (auto widget : m_widgets) + { + auto rc = TRY(widget->handle_key_event(request)); + if (rc == ui::EventResult::DidHandle) result = rc; + } + + return result; + } + + Result VerticalLayout::draw(Canvas& canvas) + { + for (auto widget : m_widgets) + { + ui::Rect rect = { m_rect.relative(widget->rect().pos), widget->rect().width, widget->rect().height }; + auto subcanvas = canvas.subcanvas(rect); + TRY(widget->draw(subcanvas)); + } + + return {}; + } + + Result VerticalLayout::add_widget(Widget& widget) + { + TRY(m_widgets.try_append(&widget)); + + if (m_adjust_height == AdjustHeight::No) + { + widget.rect().pos.y = m_rect.pos.y + m_used_height; + m_used_height += widget.rect().height; + } + else + { + int used_height = 0; + div_t result = div(m_rect.height, (int)m_widgets.size()); + for (auto w : m_widgets) + { + w->rect().pos.y = m_rect.pos.y + used_height; + w->rect().height = result.quot; + used_height += result.quot; + } + m_widgets[m_widgets.size() - 1]->rect().height += result.rem; + } + + widget.rect().pos.x = m_rect.pos.x; + + if (m_adjust_width == AdjustWidth::Yes) { widget.rect().width = m_rect.width; } + + widget.set_parent(this); + + return {}; + } +} diff --git a/libui/src/Rect.cpp b/libui/src/Rect.cpp new file mode 100644 index 00000000..14cf47c5 --- /dev/null +++ b/libui/src/Rect.cpp @@ -0,0 +1,62 @@ +/** + * @file Rect.cpp + * @author apio (cloudapio.eu) + * @brief A simple 2D rectangle representation. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include + +namespace ui +{ + bool Rect::contains(Point point) + { + return (point.x >= pos.x) && (point.y >= pos.y) && (point.x <= (pos.x + width)) && + (point.y <= (pos.y + height)); + } + + bool Rect::contains(Rect rect) + { + if (!contains(rect.pos)) return false; + Point rel = relative(rect.pos); + if ((rel.x + rect.width) > width) return false; + if ((rel.y + rect.height) > height) return false; + return true; + } + + Point Rect::normalize(Point point) + { + if (point.x < pos.x) point.x = pos.x; + if (point.y < pos.y) point.y = pos.y; + if (point.x > pos.x + width) point.x = pos.x + width; + if (point.y > pos.y + height) point.y = pos.y + height; + return point; + } + + Point Rect::relative(Point point) + { + point = normalize(point); + point.x -= pos.x; + point.y -= pos.y; + return point; + } + + Point Rect::absolute(Point point) + { + point.x += pos.x; + point.y += pos.y; + return point; + } + + Rect Rect::absolute(Rect rect) + { + return Rect { absolute(rect.pos), rect.width, rect.height }; + } + + Rect Rect::normalized() + { + return Rect { ui::Point { pos.x < 0 ? 0 : pos.x, pos.y < 0 ? 0 : pos.y }, width, height }; + } +}; diff --git a/libui/src/Window.cpp b/libui/src/Window.cpp new file mode 100644 index 00000000..298a4499 --- /dev/null +++ b/libui/src/Window.cpp @@ -0,0 +1,123 @@ +/** + * @file Window.cpp + * @author apio (cloudapio.eu) + * @brief UI windows. + * + * @copyright Copyright (c) 2023, the Luna authors. + * + */ + +#include +#include +#include +#include +#include +#include + +namespace ui +{ + Result Window::create(Rect rect, bool decorated) + { + auto window = TRY(make_owned()); + + ui::CreateWindowRequest request; + request.rect = rect; + request.decorated = decorated; + auto response = TRY(os::IPC::send_sync(App::the().client(), request)); + + auto path = COPY_IPC_STRING(response.shm_path); + + u32* pixels = (u32*)TRY(os::SharedMemory::adopt(path.view(), rect.height * rect.width * 4)); + + window->m_canvas = ui::Canvas { rect.width, rect.height, rect.width, (u8*)pixels }; + window->m_id = response.window; + + Window* p = window.ptr(); + + App::the().register_window(move(window), {}); + + return p; + } + + Window::~Window() + { + if (m_canvas.ptr) munmap(m_canvas.ptr, ((usize)m_canvas.width) * ((usize)m_canvas.height) * 4); + } + + void Window::set_title(StringView title) + { + ui::SetWindowTitleRequest request; + request.window = m_id; + SET_IPC_STRING(request.title, title.chars()); + os::IPC::send_async(App::the().client(), request); + } + + void Window::update() + { + ui::InvalidateRequest request; + request.window = m_id; + os::IPC::send_async(App::the().client(), request); + } + + void Window::close() + { + App& app = App::the(); + + ui::CloseWindowRequest request; + request.window = m_id; + os::IPC::send_async(app.client(), request); + + if (this == app.main_window()) app.set_should_close(true); + + app.unregister_window(this, {}); + } + + Result Window::draw() + { + if (m_background.has_value()) m_canvas.fill(*m_background); + if (m_main_widget) TRY(m_main_widget->draw(m_canvas)); + update(); + return {}; + } + + Result Window::handle_mouse_leave() + { + if (!m_main_widget) return ui::EventResult::DidNotHandle; + return m_main_widget->handle_mouse_leave(); + } + + Result Window::handle_mouse_move(ui::Point position) + { + if (!m_main_widget) return ui::EventResult::DidNotHandle; + return m_main_widget->handle_mouse_move(position); + } + + Result Window::handle_mouse_buttons(ui::Point position, int buttons) + { + if (!m_main_widget) return ui::EventResult::DidNotHandle; + auto result = ui::EventResult::DidNotHandle; + if (buttons) + { + auto rc = TRY(m_main_widget->handle_mouse_down(position, buttons)); + if (rc == ui::EventResult::DidHandle) result = rc; + } + if (m_old_mouse_buttons.has_value()) + { + int old_buttons = m_old_mouse_buttons.value(); + int diff = old_buttons & ~buttons; + if (diff) + { + auto rc = TRY(m_main_widget->handle_mouse_up(position, diff)); + if (rc == ui::EventResult::DidHandle) result = rc; + } + } + m_old_mouse_buttons = buttons; + return result; + } + + Result Window::handle_key_event(const ui::KeyEventRequest& request) + { + if (!m_main_widget) return ui::EventResult::DidNotHandle; + return m_main_widget->handle_key_event(request); + } +} diff --git a/shell/main.cpp b/shell/main.cpp index 70c68a2b..026475af 100644 --- a/shell/main.cpp +++ b/shell/main.cpp @@ -85,6 +85,7 @@ Result luna_main(int argc, char** argv) { StringView path; StringView command; + bool interactive { false }; SharedPtr input_file; @@ -93,6 +94,7 @@ Result luna_main(int argc, char** argv) parser.add_system_program_info("sh"_sv); parser.add_positional_argument(path, "path"_sv, "-"_sv); parser.add_value_argument(command, 'c', "command"_sv, "execute a single command and then exit"_sv); + parser.add_switch_argument(interactive, 'i', "interactive"_sv, "run an interactive shell"_sv); parser.parse(argc, argv); // TODO: This does not properly handle builtins. @@ -105,7 +107,7 @@ Result luna_main(int argc, char** argv) input_file->set_close_on_exec(); } - bool interactive = isatty(input_file->fd()); + if (isatty(input_file->fd())) interactive = true; if (interactive) { 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..a55274f9 --- /dev/null +++ b/terminal/TerminalWidget.cpp @@ -0,0 +1,596 @@ +#include "TerminalWidget.h" +#include +#include +#include +#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 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 TerminalWidget::init(char* const* args) +{ + m_font = ui::Font::default_font(); + m_bold_font = ui::Font::default_bold_font(); + + m_terminal_canvas = ui::App::the().main_window()->canvas(); + m_terminal_canvas.fill(ui::BLACK); + + 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 TerminalWidget::handle_key_event(const ui::KeyEventRequest& request) +{ + // Avoid handling "key released" events + if (!request.pressed) return ui::EventResult::DidNotHandle; + // Non-printable key or key that has no special character (unlike Tab or Enter). We exit early to avoid inserting an + // invalid zero byte into the terminal input (this would also happen on Shift or Ctrl keypresses). + if (request.letter == '\0') 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(); + + pid_t group = tcgetpgrp(m_pty); + TRY(os::Process::kill(-group, SIGINT)); + + is_special_character = true; + } + + if (request.letter == m_settings.c_cc[VQUIT]) + { + if (!(m_settings.c_lflag & NOFLSH)) m_line_buffer.clear(); + + pid_t group = tcgetpgrp(m_pty); + TRY(os::Process::kill(-group, SIGQUIT)); + + 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 TerminalWidget::draw(ui::Canvas&) +{ + return {}; +} + +Result 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 nread == 0; +} + +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(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_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(-tcgetpgrp(m_pty), SIGHUP); +} diff --git a/terminal/TerminalWidget.h b/terminal/TerminalWidget.h new file mode 100644 index 00000000..694761d8 --- /dev/null +++ b/terminal/TerminalWidget.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +class TerminalWidget : public ui::Widget +{ + public: + Result init(char* const* args); + + Result handle_key_event(const ui::KeyEventRequest& request) override; + + Result draw(ui::Canvas& canvas) override; + + Result process(); + + void quit(); + + private: + ui::Canvas m_terminal_canvas; + Vector m_line_buffer; + int m_pty; + pid_t m_child_pid; + + struct termios m_settings; + + SharedPtr m_font; + SharedPtr m_bold_font; + + static constexpr int CURSOR_TIMEOUT = 500; + + int m_current_cursor_timeout = CURSOR_TIMEOUT; + bool m_cursor_activated = false; + bool m_cursor_enabled = true; + + long m_last_cursor_tick; + + 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 }; + + void query_termios(); + + bool tick_cursor(); + + 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..48e438b9 --- /dev/null +++ b/terminal/main.cpp @@ -0,0 +1,33 @@ +#include "TerminalWidget.h" +#include +#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, 640, 400 })); + app.set_main_window(window); + window->set_title("Terminal"); + + TerminalWidget terminal; + window->set_main_widget(terminal); + + char* args[] = { "/bin/sh", nullptr }; + TRY(terminal.init(args)); + + window->draw(); + + while (app.process_events()) + { + bool should_sleep = TRY(terminal.process()); + if (should_sleep) usleep(10000); + } + + terminal.quit(); + + return 0; +} diff --git a/tools/install-headers.sh b/tools/install-headers.sh index e7f919dd..999936d2 100755 --- a/tools/install-headers.sh +++ b/tools/install-headers.sh @@ -8,10 +8,12 @@ cd $LUNA_ROOT mkdir -p $LUNA_BASE mkdir -p $LUNA_BASE/usr/include mkdir -p $LUNA_BASE/usr/include/luna +mkdir -p $LUNA_BASE/usr/include/ui mkdir -p $LUNA_BASE/usr/include/os mkdir -p $LUNA_BASE/usr/include/moon cp --preserve=timestamps -RT libc/include/ $LUNA_BASE/usr/include cp --preserve=timestamps -RT libluna/include/luna/ $LUNA_BASE/usr/include/luna +cp --preserve=timestamps -RT libui/include/ui/ $LUNA_BASE/usr/include/ui cp --preserve=timestamps -RT libos/include/os/ $LUNA_BASE/usr/include/os cp --preserve=timestamps -RT kernel/src/api/ $LUNA_BASE/usr/include/moon diff --git a/tools/sources.sh b/tools/sources.sh index fcbd49e1..752d1883 100755 --- a/tools/sources.sh +++ b/tools/sources.sh @@ -4,7 +4,7 @@ source $(dirname $0)/env.sh cd $LUNA_ROOT -FOLDERS=(kernel libc libos libluna apps shell tests) +FOLDERS=(kernel libc libos libui libluna apps shell tests) SOURCES=($(find ${FOLDERS[@]} -type f -name "*.cpp")) SOURCES+=($(find ${FOLDERS[@]} -type f -name "*.h")) diff --git a/wind/CMakeLists.txt b/wind/CMakeLists.txt new file mode 100644 index 00000000..2999afb8 --- /dev/null +++ b/wind/CMakeLists.txt @@ -0,0 +1,21 @@ +set(SOURCES + main.cpp + Screen.h + Screen.cpp + Mouse.h + Mouse.cpp + Window.h + Window.cpp + IPC.cpp + IPC.h + Keyboard.cpp + Keyboard.h + Client.h +) + +add_executable(wind ${SOURCES}) +target_compile_options(wind PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings -fno-threadsafe-statics) +add_dependencies(wind libc) +target_include_directories(wind PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR}) +target_link_libraries(wind PRIVATE os ui) +install(TARGETS wind DESTINATION ${LUNA_BASE}/usr/bin) diff --git a/wind/Client.h b/wind/Client.h new file mode 100644 index 00000000..435590cc --- /dev/null +++ b/wind/Client.h @@ -0,0 +1,18 @@ +#pragma once +#include "Window.h" +#include + +struct Client +{ + os::LocalServer::Client conn; + Vector windows; + bool rpc_in_progress { false }; + u8 rpc_id { 0 }; + + Client(os::LocalServer::Client&& client) +#ifdef CLIENT_IMPLEMENTATION + : conn(move(client)), windows() {} +#else + ; +#endif +}; diff --git a/wind/IPC.cpp b/wind/IPC.cpp new file mode 100644 index 00000000..e04d0300 --- /dev/null +++ b/wind/IPC.cpp @@ -0,0 +1,183 @@ +#include "IPC.h" +#include "Mouse.h" +#include "Screen.h" +#include +#include +#include +#include +#include + +#define TRY_OR_IPC_ERROR(expr) \ + ({ \ + auto _expr_rc = (expr); \ + if (!_expr_rc.has_value()) \ + { \ + g_windows.remove(window); \ + delete window; \ + os::IPC::send_error(client.conn, _expr_rc.error()); \ + return {}; \ + } \ + _expr_rc.release_value(); \ + }) + +#define READ_MESSAGE(request) \ + do { \ + auto rc = client.conn.recv_typed(request); \ + if (rc.has_error()) \ + { \ + if (rc.error() == EAGAIN) \ + { \ + client.rpc_in_progress = true; \ + client.rpc_id = decltype(request)::ID; \ + return {}; \ + } \ + if (rc.error() == EINTR) \ + { \ + client.rpc_in_progress = true; \ + client.rpc_id = decltype(request)::ID; \ + return {}; \ + } \ + else \ + return rc.release_error(); \ + } \ + } while (0) + +#define CHECK_WINDOW_ID(request) \ + do { \ + if ((usize)request.window >= client.windows.size() || !client.windows[request.window]) \ + { \ + os::eprintln("wind: Window id is invalid!"); \ + return {}; \ + } \ + } while (0) + +static Result handle_create_window_message(Client& client) +{ + ui::CreateWindowRequest request; + READ_MESSAGE(request); + + if (request.decorated) + { + request.rect.height += + Window::titlebar_height(); // Make sure we provide the full contents rect that was asked for. + request.rect.pos.y -= Window::titlebar_height(); // Adjust it so the contents begin at the expected coordinates. + } + + request.rect = request.rect.normalized(); + + auto name = TRY(String::from_cstring("Window")); + + auto shm_path = TRY(String::format("/wind-shm-%d-%lu"_sv, client.conn.fd(), time(NULL))); + + auto* window = new (std::nothrow) Window(request.rect, move(name), request.decorated); + if (!window) + { + os::IPC::send_error(client.conn, ENOMEM); + return {}; + } + + window->pixels = (u32*)TRY_OR_IPC_ERROR( + os::SharedMemory::create(shm_path.view(), window->contents.height * window->contents.width * 4)); + + TRY_OR_IPC_ERROR(client.windows.try_append(window)); + int id = static_cast(client.windows.size() - 1); + + window->client = &client; + window->id = id; + + ui::CreateWindowResponse response; + response.window = id; + SET_IPC_STRING(response.shm_path, shm_path.chars()); + os::IPC::send_async(client.conn, response); + return {}; +} + +static Result handle_set_window_title_message(Client& client) +{ + ui::SetWindowTitleRequest request; + READ_MESSAGE(request); + + auto name = COPY_IPC_STRING(request.title); + + os::println("wind: SetWindowTitle(\"%s\") for window %d", name.chars(), request.window); + + CHECK_WINDOW_ID(request); + + client.windows[request.window]->name = move(name); + + return {}; +} + +static Result handle_invalidate_message(Client& client) +{ + ui::InvalidateRequest request; + READ_MESSAGE(request); + + CHECK_WINDOW_ID(request); + + client.windows[request.window]->dirty = true; + + return {}; +} + +static Result handle_close_window_message(Client& client) +{ + ui::CloseWindowRequest request; + READ_MESSAGE(request); + + CHECK_WINDOW_ID(request); + + auto* window = client.windows[request.window]; + client.windows[request.window] = nullptr; + g_windows.remove(window); + Mouse::the().window_did_close(window); + delete window; + + return {}; +} + +static Result handle_get_screen_rect_message(Client& client) +{ + ui::GetScreenRectRequest request; + READ_MESSAGE(request); // Kinda pointless, but required. + + ui::GetScreenRectResponse response; + response.rect = Screen::the().canvas().rect(); + os::IPC::send_async(client.conn, response); + + return {}; +} + +namespace wind +{ + Result handle_ipc_message(Client& client, u8 id) + { + client.rpc_in_progress = false; + switch (id) + { + case ui::CREATE_WINDOW_ID: return handle_create_window_message(client); + case ui::SET_WINDOW_TITLE_ID: return handle_set_window_title_message(client); + case ui::INVALIDATE_ID: return handle_invalidate_message(client); + case ui::CLOSE_WINDOW_ID: return handle_close_window_message(client); + case ui::GET_SCREEN_RECT_ID: return handle_get_screen_rect_message(client); + default: os::eprintln("wind: Invalid IPC message from client!"); return err(EINVAL); + } + } + + Result handle_ipc(Client& client) + { + if (client.rpc_in_progress) return handle_ipc_message(client, client.rpc_id); + + u8 id; + auto rc = client.conn.recv_typed(id); + if (rc.has_error()) + { + if (rc.error() == EAGAIN) { return {}; } + if (rc.error() == EINTR) { return {}; } + else + return rc.release_error(); + } + + return handle_ipc_message(client, id); + } +} diff --git a/wind/IPC.h b/wind/IPC.h new file mode 100644 index 00000000..4c085302 --- /dev/null +++ b/wind/IPC.h @@ -0,0 +1,10 @@ +#pragma once +#include "Client.h" +#include + +namespace wind +{ + Result handle_ipc_message(Client& client, u8 id); + + Result handle_ipc(Client& client); +} diff --git a/wind/Keyboard.cpp b/wind/Keyboard.cpp new file mode 100644 index 00000000..ba95ce9e --- /dev/null +++ b/wind/Keyboard.cpp @@ -0,0 +1,305 @@ +#include "Keyboard.h" +#include + +static const char table[] = { + // Function keys + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + // System keys + '\x1b', + '\0', + '\0', + '\0', + '\0', + // Modifier keys + '\0', + '\0', + '\0', + '\0', // or AltGr on some keyboards + '\0', + '\0', + // Navigation keys + '\t', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + // Editing keys + '\b', + '\n', + '\0', + '\x7f', + '\n', + // Lock keys + '\0', + '\0', + '\0', + // Keypad keys + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '.', + '+', + '-', + '*', + '/', + // Character keys (depending on keyboard layout), examples in US QWERTY + '`', // ` + '1', // 1 + '2', // 2 + '3', // 3 + '4', // 4 + '5', // 5 + '6', // 6 + '7', // 7 + '8', // 8 + '9', // 9 + '0', // 0 + '-', // - + '=', // = + 'q', // Q + 'w', // W + 'e', // E + 'r', // R + 't', // T + 'y', // Y + 'u', // U + 'i', // I + 'o', // O + 'p', // P + '[', // [ + ']', // ] + 'a', // A + 's', // S + 'd', // D + 'f', // F + 'g', // G + 'h', // H + 'j', // J + 'k', // K + 'l', // L + ';', // ; + '\'', // ' + '#', // # + '\\', // Backslash + 'z', // Z + 'x', // X + 'c', // C + 'v', // V + 'b', // B + 'n', // N + 'm', // M + ',', // , + '.', // . + '/', // / + ' ', // Space + // Unknown key + '\0', +}; + +static const char shift_table[] = { + // Function keys + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + // System keys + '\x1b', + '\0', + '\0', + '\0', + '\0', + // Modifier keys + '\0', + '\0', + '\0', + '\0', // or AltGr on some keyboards + '\0', + '\0', + // Navigation keys + '\t', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + '\0', + // Editing keys + '\b', + '\n', + '\0', + '\x7f', + '\n', + // Lock keys + '\0', + '\0', + '\0', + // Keypad keys + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '.', + '+', + '-', + '*', + '/', + // Character keys (depending on keyboard layout), examples in US QWERTY + '~', // ` + '!', + '@', + '#', + '$', + '%', + '^', + '&', + '*', + '(', + ')', + '_', + '+', + 'Q', + 'W', + 'E', + 'R', + 'T', + 'Y', + 'U', + 'I', + 'O', + 'P', + '{', + '}', + 'A', + 'S', + 'D', + 'F', + 'G', + 'H', + 'J', + 'K', + 'L', + ':', + '"', + ' ', // # + '|', // Backslash + 'Z', + 'X', + 'C', + 'V', + 'B', + 'N', + 'M', + '<', + '>', + '?', + ' ', // Space + // Unknown key + '\0', +}; + +namespace wind::Keyboard +{ + static bool g_caps_lock = false; + static bool g_right_shift = false; + static bool g_left_shift = false; + static bool g_right_control = false; + static bool g_left_control = false; + static bool g_altgr = false; + static bool g_alt = false; + static bool g_super = false; + + ui::KeyEventRequest decode_keyboard_event(moon::KeyCode code, bool released) + { + ui::KeyEventRequest request; + request.code = code; + request.pressed = !released; + request.modifiers = 0; + + switch (code) + { + case moon::K_CapsLock: + if (!released) { g_caps_lock = !g_caps_lock; } + break; + case moon::K_RightShift: g_right_shift = !released; break; + case moon::K_LeftShift: g_left_shift = !released; break; + case moon::K_RightControl: g_right_control = !released; break; + case moon::K_LeftControl: g_left_control = !released; break; + case moon::K_RightAlt: g_altgr = !released; break; + case moon::K_LeftAlt: g_alt = !released; break; + case moon::K_Super: g_super = !released; break; + default: break; + } + + if ((g_caps_lock && !(g_left_shift || g_right_shift)) || (g_left_shift || g_right_shift)) + { + request.modifiers |= ui::Mod_Shift; + } + + if (g_right_control || g_left_control) request.modifiers |= ui::Mod_Ctrl; + + if (g_alt) request.modifiers |= ui::Mod_Alt; + if (g_altgr) request.modifiers |= ui::Mod_AltGr; + if (g_super) request.modifiers |= ui::Mod_Super; + + request.key = table[code]; + + if (request.modifiers & ui::Mod_Ctrl) + { + char letter; + if (request.modifiers & ui::Mod_Shift) letter = shift_table[code]; + else + letter = table[code]; + if (_islower(letter)) letter = (char)_toupper(letter); + if (_isupper(letter)) letter = letter - 0x40; + if (letter == '@') letter = letter - 0x40; + if (letter > 'Z' && letter < '`') letter = letter - 0x40; + if (letter == '?') letter = 0x7f; + request.letter = letter; + return request; + } + + if (request.modifiers & ui::Mod_Shift) request.letter = shift_table[code]; + else + request.letter = table[code]; + + return request; + } +} diff --git a/wind/Keyboard.h b/wind/Keyboard.h new file mode 100644 index 00000000..3259b669 --- /dev/null +++ b/wind/Keyboard.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace wind +{ + namespace Keyboard + { + ui::KeyEventRequest decode_keyboard_event(moon::KeyCode code, bool released); + } +} diff --git a/wind/Mouse.cpp b/wind/Mouse.cpp new file mode 100644 index 00000000..125a37a7 --- /dev/null +++ b/wind/Mouse.cpp @@ -0,0 +1,129 @@ +#include "Mouse.h" +#include "Client.h" +#include +#include +#include +#include + +static SharedPtr g_mouse_cursor; + +static Mouse* s_mouse; + +Mouse::Mouse(ui::Canvas& screen) +{ + m_position.x = screen.width / 2; + m_position.y = screen.height / 2; + m_screen_rect = screen.rect(); + + g_mouse_cursor = ui::Image::load("/usr/share/cursors/default.tga").value_or({}); + + s_mouse = this; +} + +Mouse& Mouse::the() +{ + check(s_mouse); + return *s_mouse; +} + +void Mouse::draw(ui::Canvas& screen) +{ + if (!g_mouse_cursor) return; + auto canvas = screen.subcanvas(ui::Rect { m_position, g_mouse_cursor->width(), g_mouse_cursor->height() }); + canvas.fill(g_mouse_cursor->pixels(), g_mouse_cursor->width()); +} + +void Mouse::update(const moon::MousePacket& packet) +{ + m_position.x += packet.xdelta; + m_position.y -= packet.ydelta; + m_position = m_screen_rect.normalize(m_position); + + if (m_dragging_window && !(packet.buttons & moon::MouseButton::Left)) + { + os::println("Stopped drag: window at (%d,%d,%d,%d) with offset (%d,%d)", m_dragging_window->surface.pos.x, + m_dragging_window->surface.pos.y, m_dragging_window->surface.width, + m_dragging_window->surface.height, this->m_initial_drag_position.x, + this->m_initial_drag_position.y); + m_dragging_window = nullptr; + } + + if (m_dragging_window) + { + m_dragging_window->surface.pos = + ui::Point { m_position.x - m_initial_drag_position.x, m_position.y - m_initial_drag_position.y }; + m_dragging_window->surface = m_dragging_window->surface.normalized(); + } + + else if ((packet.buttons & moon::MouseButton::Left) && !m_dragging_window) + { + // Iterate from the end of the list, since windows at the beginning are stacked at the bottom and windows at the + // top are at the end. + for (Window* window = g_windows.last().value_or(nullptr); window; + window = g_windows.previous(window).value_or(nullptr)) + { + if (window->surface.absolute(window->close_button).contains(m_position)) + { + ui::WindowCloseRequest request; + request.window = window->id; + auto& client = *window->client; + os::IPC::send_async(client.conn, request); + break; + } + else if (window->surface.absolute(window->titlebar).contains(m_position)) + { + m_dragging_window = window; + m_initial_drag_position = window->surface.relative(m_position); + os::println("Started drag: window at (%d,%d,%d,%d) with offset (%d,%d)", window->surface.pos.x, + window->surface.pos.y, window->surface.width, window->surface.height, + m_initial_drag_position.x, m_initial_drag_position.y); + window->focus(); + break; + } + else if (window->surface.absolute(window->contents).contains(m_position)) + { + window->focus(); + break; // We don't want to continue iterating, otherwise this would take into account windows whose + // titlebar is underneath another window's contents! + } + } + } + + Window* new_active_window = nullptr; + + for (Window* window = g_windows.last().value_or(nullptr); window; + window = g_windows.previous(window).value_or(nullptr)) + { + auto titlebar = window->surface.absolute(window->titlebar); + auto contents = window->surface.absolute(window->contents); + if (titlebar.contains(m_position)) break; + if (contents.contains(m_position)) + { + ui::MouseEventRequest request; + request.window = window->id; + request.position = contents.relative(m_position); + request.buttons = packet.buttons; + os::IPC::send_async(window->client->conn, request); + new_active_window = window; + break; + } + } + + if (m_active_window != new_active_window) + { + if (m_active_window) + { + ui::MouseLeaveRequest request; + request.window = m_active_window->id; + os::IPC::send_async(m_active_window->client->conn, request); + } + m_active_window = new_active_window; + } +} + +void Mouse::window_did_close(Window* window) +{ + if (m_dragging_window == window) { m_dragging_window = nullptr; } + + if (m_active_window == window) { m_active_window = nullptr; } +} diff --git a/wind/Mouse.h b/wind/Mouse.h new file mode 100644 index 00000000..3b068133 --- /dev/null +++ b/wind/Mouse.h @@ -0,0 +1,28 @@ +#pragma once +#include "Screen.h" +#include "Window.h" +#include +#include + +class Mouse +{ + public: + Mouse(ui::Canvas& screen); + + void update(const moon::MousePacket& packet); + + void draw(ui::Canvas& screen); + + void window_did_close(Window* window); + + static Mouse& the(); + + private: + ui::Point m_position; + ui::Rect m_screen_rect; + + Window* m_dragging_window = nullptr; + ui::Point m_initial_drag_position; + + Window* m_active_window = nullptr; +}; diff --git a/wind/Screen.cpp b/wind/Screen.cpp new file mode 100644 index 00000000..48ce774a --- /dev/null +++ b/wind/Screen.cpp @@ -0,0 +1,36 @@ +#include "Screen.h" +#include +#include +#include +#include +#include + +Screen Screen::s_the; + +Result Screen::open() +{ + int fd = ::open("/dev/fb0", O_RDWR); + if (fd < 0) return err(errno); + + int width = ioctl(fd, FB_GET_WIDTH); + int height = ioctl(fd, FB_GET_HEIGHT); + + void* p = mmap(nullptr, width * height * BYTES_PER_PIXEL, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + + if (p == MAP_FAILED) { return err(errno); } + + Screen screen; + + screen.m_canvas = ui::Canvas::create((u8*)p, width, height); + screen.m_size = width * height * BYTES_PER_PIXEL; + + s_the = screen; + + return {}; +} + +void Screen::sync() +{ + msync(m_canvas.ptr, size(), MS_SYNC); +} diff --git a/wind/Screen.h b/wind/Screen.h new file mode 100644 index 00000000..543b24f9 --- /dev/null +++ b/wind/Screen.h @@ -0,0 +1,34 @@ +#pragma once +#include +#include + +constexpr int BYTES_PER_PIXEL = 4; + +class Screen +{ + public: + static Result open(); + + ui::Canvas& canvas() + { + return m_canvas; + } + + int size() const + { + return m_size; + } + + static Screen& the() + { + return s_the; + } + + void sync(); + + private: + ui::Canvas m_canvas; + int m_size; + + static Screen s_the; +}; diff --git a/wind/Window.cpp b/wind/Window.cpp new file mode 100644 index 00000000..4bee2242 --- /dev/null +++ b/wind/Window.cpp @@ -0,0 +1,69 @@ +#include "Window.h" +#include +#include +#include +#include +#include + +LinkedList g_windows; + +static constexpr ui::Color TITLEBAR_COLOR = ui::Color::from_rgb(53, 53, 53); + +void Window::draw(ui::Canvas& screen) +{ + dirty = false; + + auto window = screen.subcanvas(surface); + window.subcanvas(contents).fill(pixels, contents.width); + + wchar_t buffer[4096]; + Utf8StringDecoder decoder(name.chars()); + decoder.decode(buffer, sizeof(buffer)).release_value(); + + auto font = ui::Font::default_font(); + + auto titlebar_canvas = window.subcanvas(titlebar); + titlebar_canvas.fill(TITLEBAR_COLOR); + + auto textarea = titlebar_canvas.subcanvas(ui::Rect { 10, 10, titlebar_canvas.width - 10, titlebar_canvas.height }); + font->render(buffer, ui::WHITE, textarea); + + static SharedPtr g_close_icon; + + if (!g_close_icon) g_close_icon = ui::Image::load("/usr/share/icons/16x16/app-close.tga").release_value(); + + auto close_area = window.subcanvas(close_button); + close_area.fill(g_close_icon->pixels(), g_close_icon->width()); +} + +void Window::focus() +{ + // Bring the window to the front of the list. + g_windows.remove(this); + g_windows.append(this); +} + +Window::Window(ui::Rect r, String&& n, bool d) : surface(r), name(move(n)), decorated(d) +{ + auto font = ui::Font::default_font(); + if (decorated && surface.width < 36) surface.width = 36; + if (decorated && surface.height < (font->height() + 20)) surface.height = font->height() + 20; + titlebar = decorated ? ui::Rect { 0, 0, surface.width, font->height() + 20 } : ui::Rect { 0, 0, 0, 0 }; + close_button = decorated ? ui::Rect { surface.width - 26, 10, 16, 16 } : ui::Rect { 0, 0, 0, 0 }; + contents = decorated ? ui::Rect { 0, font->height() + 20, surface.width, surface.height - (font->height() + 20) } + : ui::Rect { 0, 0, surface.width, surface.height }; + g_windows.append(this); +} + +int Window::titlebar_height() +{ + auto font = ui::Font::default_font(); + return font->height() + 20; +} + +Window::~Window() +{ + usize size = contents.width * contents.height * 4; + + munmap(pixels, size); +} diff --git a/wind/Window.h b/wind/Window.h new file mode 100644 index 00000000..9fcf33b2 --- /dev/null +++ b/wind/Window.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include +#include + +struct Client; + +struct Window : public LinkedListNode +{ + ui::Rect surface; + ui::Rect titlebar; + ui::Rect close_button; + ui::Rect contents; + u32* pixels; + String name; + bool dirty { false }; + Client* client; + int id; + bool decorated; + + static int titlebar_height(); + + Window(ui::Rect, String&&, bool); + ~Window(); + + void focus(); + + void draw(ui::Canvas& screen); +}; + +extern LinkedList g_windows; diff --git a/wind/main.cpp b/wind/main.cpp new file mode 100644 index 00000000..b0b4930c --- /dev/null +++ b/wind/main.cpp @@ -0,0 +1,197 @@ +#define CLIENT_IMPLEMENTATION +#include "Client.h" +#include "IPC.h" +#include "Keyboard.h" +#include "Mouse.h" +#include "Screen.h" +#include "Window.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void debug(const Vector>& clients) +{ + os::println("--- wind: DEBUG OUTPUT ---"); + + os::println("-- wind: Listing clients --"); + + for (const auto& client : clients) + { + os::println("Client with fd %d, owns %zu windows", client->conn.fd(), client->windows.size()); + } + + os::println("-- wind: Listing windows --"); + + for (const auto& window : g_windows) + { + os::println("Window of client (fd %d), id %d, %sdecorated, %sdirty (\"%s\") (%d,%d,%d,%d)", + window->client->conn.fd(), window->id, window->decorated ? "" : "not ", window->dirty ? "" : "not ", + window->name.chars(), window->surface.pos.x, window->surface.pos.y, window->surface.width, + window->surface.height); + } + + os::println("-- wind: Listing processes --"); + + system("ps"); + + os::println("-- wind: Listing memory usage --"); + + system("free -h"); + + os::println("--- wind: END DEBUG OUTPUT ---"); +} + +Result luna_main(int argc, char** argv) +{ + srand((unsigned)time(NULL)); + + TRY(os::Security::pledge("stdio rpath wpath cpath unix proc exec tty id", NULL)); + + StringView socket_path = "/tmp/wind.sock"; + StringView user; + + os::ArgumentParser parser; + parser.add_description("The display server for Luna's graphical user interface."_sv); + parser.add_system_program_info("wind"_sv); + parser.add_value_argument(socket_path, 's', "socket"_sv, "the path for the local IPC socket"_sv); + parser.add_value_argument(user, 'u', "user"_sv, "the user to run as"_sv); + parser.parse(argc, argv); + + if (geteuid() != 0) + { + os::eprintln("error: wind must be run as root to initialize resources, run with --user= to drop " + "privileges afterwards"); + return 1; + } + + if (!isatty(STDIN_FILENO)) + { + os::eprintln("error: wind must be run from a TTY!"); + return 1; + } + + auto mouse = TRY(os::File::open("/dev/mouse", os::File::ReadOnly)); + mouse->set_buffer(os::File::NotBuffered); + mouse->set_close_on_exec(); + + auto keyboard = TRY(os::File::open("/dev/kbd", os::File::ReadOnly)); + keyboard->set_buffer(os::File::NotBuffered); + keyboard->set_close_on_exec(); + + TRY(Screen::open()); + auto& screen = Screen::the(); + + Mouse mouse_pointer { screen.canvas() }; + + ioctl(STDIN_FILENO, TTYSETGFX, 1); + + setpgid(0, 0); + + int fd = open("/dev/null", O_RDONLY); + if (fd >= 0) + { + dup2(fd, STDIN_FILENO); + close(fd); + } + + clearenv(); + + if (!user.is_empty()) + { + auto* pwd = getpwnam(user.chars()); + if (pwd) + { + setgid(pwd->pw_gid); + setuid(pwd->pw_uid); + } + } + + auto server = TRY(os::LocalServer::create(socket_path, false)); + TRY(server->listen(20)); + + StringView args[] = { "/usr/bin/init"_sv, "--user"_sv }; + TRY(os::Process::spawn("/usr/bin/init"_sv, Slice { args, 2 }, false)); + + ui::Color background = ui::Color::from_rgb(0x10, 0x10, 0x10); + + Vector> clients; + Vector fds; + TRY(fds.try_append({ .fd = mouse->fd(), .events = POLLIN, .revents = 0 })); + TRY(fds.try_append({ .fd = keyboard->fd(), .events = POLLIN, .revents = 0 })); + TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 })); + + TRY(os::Security::pledge("stdio rpath wpath cpath unix proc exec", NULL)); + + while (1) + { + screen.canvas().fill(background); + for (auto* window : g_windows) window->draw(screen.canvas()); + mouse_pointer.draw(screen.canvas()); + screen.sync(); + + for (auto& pfd : fds) { pfd.revents = 0; } + + int rc = poll(fds.data(), fds.size(), 1000); + if (!rc) continue; + if (rc < 0 && errno != EINTR) { os::println("poll: error: %s", strerror(errno)); } + + if (fds[0].revents & POLLIN) + { + moon::MousePacket packet; + TRY(mouse->read_typed(packet)); + mouse_pointer.update(packet); + } + if (fds[1].revents & POLLIN) + { + moon::KeyboardPacket packet; + TRY(keyboard->read_typed(packet)); + if (!packet.released && packet.key == moon::K_Tab) debug(clients); + auto request = wind::Keyboard::decode_keyboard_event((moon::KeyCode)packet.key, packet.released); + if (g_windows.last().has_value()) + { + auto* window = g_windows.last().value(); + request.window = window->id; + os::IPC::send_async(window->client->conn, request); + } + } + for (usize i = 0; i < clients.size(); i++) + { + if (fds[i + 3].revents & POLLIN) wind::handle_ipc(*clients[i]); + if (fds[i + 3].revents & POLLHUP) + { + os::println("wind: Client %d disconnected", i); + fds.remove_at(i + 3); + auto client = clients.remove_at(i); + client->conn.disconnect(); + for (auto& window : client->windows) + { + if (window) + { + g_windows.remove(window); + mouse_pointer.window_did_close(window); + delete window; + } + } + } + } + if (fds[2].revents & POLLIN) + { + auto client = TRY(server->accept()); + os::println("wind: New client connected!"); + TRY(fds.try_append({ .fd = client.fd(), .events = POLLIN, .revents = 0 })); + OwnedPtr c = TRY(adopt_owned_if_nonnull(new Client(move(client)))); + TRY(clients.try_append(move(c))); + } + } +}