Compare commits
191 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e1a0d0e13 | |||
773cd576d1 | |||
498c371547 | |||
b0be170b41 | |||
5cb4b8b1fe | |||
d9713723c9 | |||
c0cf952113 | |||
22766a6724 | |||
abdaad5ea4 | |||
f116afd59d | |||
1b80111938 | |||
f91800f5e1 | |||
6dcdc43dc2 | |||
00382421b2 | |||
5d5c85a022 | |||
48ee803e58 | |||
984200ca9a | |||
ac260d0397 | |||
fb3333a086 | |||
ccef3e2069 | |||
ad3cea7e78 | |||
865a913502 | |||
499bf6dd19 | |||
94e7dde8af | |||
f38c9e68c1 | |||
3b8aabce0f | |||
5f56e4b63a | |||
24b886b0d1 | |||
d8e4489079 | |||
2868fd8122 | |||
56a2b607b5 | |||
ec6ceb4c8d | |||
d05d6fad0b | |||
42afef5ccb | |||
853a6d7b38 | |||
8e30e0e19d | |||
dc766e1da7 | |||
6fc49a0be5 | |||
7761a8a41f | |||
0ca6c5f814 | |||
3032415bc0 | |||
7b2977a036 | |||
9e65131452 | |||
d908ccea6b | |||
e3613d1653 | |||
53f8a583dc | |||
c21fc2a297 | |||
fd26f40938 | |||
fd2fe16538 | |||
38fcd8e3e1 | |||
05bf792dbd | |||
b95cfac3ec | |||
17a31e5ea9 | |||
1f0286c9c7 | |||
ffd1c73b0f | |||
12ab71ee40 | |||
4cf39c14a1 | |||
bbe1eca711 | |||
e7d361ca51 | |||
bb6759986e | |||
4cc8a44ec7 | |||
0a9578c1ec | |||
1fc2da4fb0 | |||
9c4f20790f | |||
0a143e8729 | |||
d4237d10a0 | |||
b24aa1821c | |||
7c0ff8c75a | |||
a11aa7a2d0 | |||
0abd9153ae | |||
abbfd5825f | |||
bfb45c7d4a | |||
d3fbddb191 | |||
0ab8efd405 | |||
2aefbdc4ee | |||
15dc71e8e1 | |||
140910763e | |||
829f455129 | |||
d10cb10404 | |||
c97876bba0 | |||
31c36b9b83 | |||
5fe0507ab1 | |||
e1c287a45b | |||
db2f91b1fb | |||
7345a952ca | |||
903dcfa52c | |||
2ce2d57eff | |||
907049c405 | |||
450ef2ce27 | |||
01dcb954e5 | |||
04649fce8a | |||
66983ce17c | |||
0a46cfc80c | |||
fb52c67f16 | |||
ab70a72434 | |||
1d0f18cab9 | |||
de6f5c38d8 | |||
e0ed4be0db | |||
6293aeea58 | |||
646a15d295 | |||
b59a787b9e | |||
e2ff0ad273 | |||
62cb53069c | |||
fe302f5967 | |||
69f3e28f2c | |||
2f56a52489 | |||
1176e64a7c | |||
e7780b04ee | |||
6ded7247e0 | |||
8b3755873b | |||
489d54c531 | |||
eb3af60497 | |||
aee100753d | |||
0be6a896bb | |||
7d738433ed | |||
701dc30221 | |||
6968961d5c | |||
d8914b3efa | |||
82985d691d | |||
ff10e5f3b2 | |||
71df91b4a0 | |||
7dc4b17d46 | |||
332976dde9 | |||
5b94217316 | |||
7205020bac | |||
898eb43360 | |||
a863b17746 | |||
5087b6db30 | |||
7d69ac56e2 | |||
f9b39c5ff3 | |||
d70effd1db | |||
59713279a0 | |||
6443ec77f8 | |||
86372a3893 | |||
3dc2c24ec5 | |||
06b8a41d2f | |||
eab44307f0 | |||
cdab3dea90 | |||
2780ee2ebc | |||
70c63572b2 | |||
fc37634a18 | |||
f05fea441c | |||
5975e58b4a | |||
d385e01796 | |||
4dc060e0b3 | |||
644614cdd8 | |||
1070c85922 | |||
8d5f598488 | |||
8efcf6d852 | |||
7165ff7683 | |||
0847a2cda0 | |||
42a7c7af5f | |||
49f84c9dda | |||
f5c0e724d5 | |||
ac3175cf26 | |||
a78620a7d2 | |||
fcd8c1d583 | |||
e1c2dfb9ba | |||
7fcc0659c8 | |||
3fc5f2b836 | |||
64965cd322 | |||
d0ad103e3d | |||
3728558b13 | |||
7cd6e9b12a | |||
e02dee1f41 | |||
ab2700ef5d | |||
bb18749d5b | |||
d4b368b078 | |||
7f2a65f6d6 | |||
02f8102d38 | |||
9bb66716a4 | |||
b9ccda132a | |||
b8470f753b | |||
909d0ed289 | |||
6bdf3169d2 | |||
75d0d12b71 | |||
a7ff298852 | |||
ca5b4de2d8 | |||
f8a39ffeec | |||
d440559d54 | |||
443d8957f3 | |||
ea14dab7d7 | |||
16223b2f53 | |||
9c36ef6e9e | |||
63f785563d | |||
1cd355a8e8 | |||
d4d748e153 | |||
cee677b1f7 | |||
e4c9211edc | |||
6bf8225f63 | |||
1223c6c20b |
25
.drone.yml
25
.drone.yml
@ -1,25 +0,0 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: test
|
||||
|
||||
platform:
|
||||
arch: arm64
|
||||
os: linux
|
||||
|
||||
steps:
|
||||
- name: build-and-test
|
||||
image: ubuntu
|
||||
commands:
|
||||
- apt update
|
||||
- apt install build-essential cmake ninja-build wget nasm genext2fs qemu-system git -y
|
||||
- wget https://pub.cloudapio.eu/luna/toolchains/ci-toolchain-arm64.tar.gz --quiet
|
||||
- tar xf ci-toolchain-arm64.tar.gz
|
||||
- rm ci-toolchain-arm64.tar.gz
|
||||
- tools/run-tests.sh
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
23
.gitea/workflows/build.yaml
Normal file
23
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,23 @@
|
||||
name: Build and test
|
||||
run-name: ${{ gitea.actor }} is testing and running the code
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v3
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
apt update
|
||||
apt install -y cmake ninja-build nasm genext2fs qemu-system build-essential wget git clang-format
|
||||
- name: Set up the toolchain
|
||||
run: |
|
||||
wget https://pub.cloudapio.eu/luna/toolchains/ci-toolchain-arm64.tar.gz --quiet
|
||||
tar xf ci-toolchain-arm64.tar.gz
|
||||
rm ci-toolchain-arm64.tar.gz
|
||||
- name: Check formatting
|
||||
run: tools/check-formatting.sh
|
||||
- name: Build and run tests
|
||||
run: tools/run-tests.sh
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ base/usr/*
|
||||
base/usr/share/*
|
||||
!base/usr/share/fonts
|
||||
!base/usr/share/icons
|
||||
!base/usr/share/applications
|
||||
base/etc/skel/LICENSE
|
||||
.fakeroot
|
||||
kernel/config.cmake
|
||||
|
@ -5,8 +5,8 @@ set(CMAKE_CXX_COMPILER_WORKS 1)
|
||||
|
||||
set(CMAKE_CROSSCOMPILING true)
|
||||
|
||||
project(Luna LANGUAGES C CXX ASM ASM_NASM VERSION 0.6.0)
|
||||
set(LUNA_RELEASE_NAME "Andromeda")
|
||||
project(Luna LANGUAGES C CXX ASM ASM_NASM VERSION 0.7.0)
|
||||
set(LUNA_RELEASE_NAME "Pulsar")
|
||||
|
||||
set(LUNA_ROOT ${CMAKE_CURRENT_LIST_DIR})
|
||||
set(LUNA_BASE ${CMAKE_CURRENT_LIST_DIR}/base)
|
||||
@ -45,11 +45,10 @@ endif()
|
||||
|
||||
add_subdirectory(libluna)
|
||||
add_subdirectory(libos)
|
||||
add_subdirectory(libui)
|
||||
add_subdirectory(gui)
|
||||
add_subdirectory(libc)
|
||||
add_subdirectory(kernel)
|
||||
add_subdirectory(apps)
|
||||
add_subdirectory(utils)
|
||||
add_subdirectory(tests)
|
||||
add_subdirectory(shell)
|
||||
add_subdirectory(wind)
|
||||
add_subdirectory(terminal)
|
||||
add_subdirectory(system)
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2022-2024, apio.
|
||||
Copyright (c) 2022-2025, apio.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
42
README.md
42
README.md
@ -1,18 +1,18 @@
|
||||
# Luna
|
||||
A simple POSIX-based operating system for personal computers, written in C++. [![Build Status](https://drone.cloudapio.eu/api/badges/apio/Luna/status.svg)](https://drone.cloudapio.eu/apio/Luna)
|
||||
A simple POSIX-based operating system for 64-bit computers, written in C++.
|
||||
|
||||
## Another UNIX clone?
|
||||
[Yes, another UNIX clone](https://wiki.osdev.org/User:Sortie/Yes_Another_Unix_Clone).
|
||||
|
||||
## Features
|
||||
- x86_64-compatible lightweight [kernel](kernel/).
|
||||
- Simple round-robin [scheduler](kernel/src/thread/).
|
||||
- Lightweight 64-bit [kernel](kernel/). Compatible with the x86_64 architecture.
|
||||
- Basic threads/processes, using a simple round-robin [scheduler](kernel/src/thread/).
|
||||
- Read-only [ext2](kernel/src/fs/ext2/) filesystem.
|
||||
- Can [load ELF programs](kernel/src/binfmt/ELF.cpp), [shebang scripts](kernel/src/binfmt/Script.cpp) or [arbitrary binary formats](kernel/src/binfmt/BinaryFormat.h) (registered through kernel modules, which are not supported yet =D).
|
||||
- Can [load ELF executables](kernel/src/binfmt/ELF.cpp), [shebang scripts](kernel/src/binfmt/Script.cpp) or [arbitrary binary formats](kernel/src/binfmt/BinaryFormat.h) (registered through kernel modules, which are not supported yet =D).
|
||||
- [C Library](libc/), aiming for POSIX compatibility, with many features such as local domain sockets, signals, and shared memory.
|
||||
- Support for [several third-party programs](ports/), including the [GNU binutils](ports/binutils/PACKAGE) suite of utilities.
|
||||
- Designed to be [portable](kernel/src/arch), no need to be restricted to x86_64.
|
||||
- Everything is designed around [UTF-8](libluna/include/luna/Utf8.h).
|
||||
- Support for [several third-party programs](ports/), including the [GNU binutils](ports/binutils/PACKAGE) suite of utilities and the [GCC](ports/gcc/PACKAGE) compiler.
|
||||
- Designed to be [portable](kernel/src/arch), so that additional architectures can be added in the future with relatively low effort.
|
||||
- Everything text-related is designed around [UTF-8](libluna/include/luna/Utf8.h).
|
||||
- Environment-agnostic [utility library](libluna/), which can be used in both kernel and userspace.
|
||||
- An extensive set of [standard Unix utilities](apps/), from [ls](apps/ls.cpp) to [uname](apps/uname.cpp) to [base64](apps/base64.cpp). Written in modern C++ and very small amounts of code, using Luna's practical [OS library](libos/).
|
||||
- A simple and efficient [windowing system](wind/), providing a lightweight GUI environment (still in development, not many GUI apps exist).
|
||||
@ -20,35 +20,33 @@ A simple POSIX-based operating system for personal computers, written in C++. [!
|
||||
## Screenshot
|
||||
![Screenshot as of 0.6.0](docs/screenshots/screenshot-0.6.0.png)
|
||||
|
||||
## System requirements and dependencies
|
||||
|
||||
Read [docs/dependencies.md](docs/dependencies.md) for the full information. In short, all modern Unixes should work, provided the dependencies are available.
|
||||
|
||||
## Setup
|
||||
|
||||
To build and run Luna, you will need to build a [cross-compiler](https://wiki.osdev.org/Why_do_I_need_a_Cross_Compiler) and cross-binutils for `x86_64-luna`.
|
||||
|
||||
For this, you should start by installing the [required dependencies](https://wiki.osdev.org/GCC_Cross_Compiler#Installing_Dependencies).
|
||||
|
||||
Then, run `tools/setup.sh` to build the toolchain.
|
||||
There is a script provided for this. Run `tools/setup.sh` to build the toolchain.
|
||||
|
||||
Please beware that building GCC and Binutils can take some time, depending on your machine.
|
||||
|
||||
## Running
|
||||
|
||||
To run Luna in a virtual machine, you should have [QEMU](https://www.qemu.org/) installed.
|
||||
|
||||
Additionally, the build process needs some extra dependencies to run: `cmake`, `ninja`, `nasm` and `genext2fs`.
|
||||
|
||||
`tools/run.sh` is the script you should use in most cases. It will build changed files, install, make an ISO image, and run Luna in QEMU.
|
||||
|
||||
If you have no toolchain set up, `run.sh` will build it automatically, which means that you don't necessarily have to run `setup.sh` since `run.sh` does it for you.
|
||||
If you have no toolchain set up, `run.sh` will build it automatically, which means that you don't necessarily have to run `setup.sh` manually since `run.sh` does it for you.
|
||||
|
||||
## Login UI
|
||||
|
||||
For development convenience, the system automatically starts a GUI session as the default user, without prompting for a password.
|
||||
|
||||
Despite this, Luna does have a login window built-in. If you'd like to try this feature out or start a GUI session as a different user, you'll need to edit [base/etc/loginui.conf](base/etc/loginui.conf) and change the line that says `Autologin=true` to `Autologin=false`.
|
||||
|
||||
## Prebuilt images
|
||||
|
||||
Prebuilt ISO images (numbered) for every version can be found at [pub.cloudapio.eu](https://pub.cloudapio.eu/luna/releases).
|
||||
|
||||
These images are built manually whenever a new release is created, and thus don't reflect the latest changes on the `main` branch.
|
||||
|
||||
Every hour, this server pulls the latest commits on `main` and builds an hourly ISO image. The ten most recent ones can be found in the [hourly](https://pub.cloudapio.eu/luna/hourly) directory, and [Luna-latest.iso](https://pub.cloudapio.eu/luna/Luna-latest.iso) should always be symlinked to the newest one.
|
||||
|
||||
These images do reflect the latest changes on the `main` branch, but are obviously less stable. Additionally, an hourly image will be skipped if building the latest commit of the project fails.
|
||||
Prebuilt ISO images for every release version can be found at [pub.cloudapio.eu](https://pub.cloudapio.eu/luna/releases).
|
||||
|
||||
## Is there third-party software I can use on Luna?
|
||||
|
||||
|
@ -1,51 +0,0 @@
|
||||
function(luna_app SOURCE_FILE APP_NAME)
|
||||
add_executable(${APP_NAME} ${SOURCE_FILE})
|
||||
target_compile_options(${APP_NAME} PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||
add_dependencies(${APP_NAME} libc)
|
||||
target_include_directories(${APP_NAME} PRIVATE ${LUNA_BASE}/usr/include)
|
||||
target_link_libraries(${APP_NAME} PRIVATE os)
|
||||
install(TARGETS ${APP_NAME} DESTINATION ${LUNA_BASE}/usr/bin)
|
||||
endfunction()
|
||||
|
||||
add_executable(preinit preinit.cpp)
|
||||
target_compile_options(preinit PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||
add_dependencies(preinit libc)
|
||||
target_include_directories(preinit PRIVATE ${LUNA_BASE}/usr/include)
|
||||
install(TARGETS preinit DESTINATION ${LUNA_ROOT}/initrd/bin)
|
||||
|
||||
luna_app(init.cpp init)
|
||||
luna_app(env.cpp env)
|
||||
luna_app(su.cpp su)
|
||||
luna_app(cat.cpp cat)
|
||||
luna_app(date.cpp date)
|
||||
luna_app(edit.cpp edit)
|
||||
luna_app(ls.cpp ls)
|
||||
luna_app(chown.cpp chown)
|
||||
luna_app(chmod.cpp chmod)
|
||||
luna_app(mkdir.cpp mkdir)
|
||||
luna_app(rm.cpp rm)
|
||||
luna_app(stat.cpp stat)
|
||||
luna_app(uname.cpp uname)
|
||||
luna_app(base64.cpp base64)
|
||||
luna_app(login.cpp login)
|
||||
luna_app(mount.cpp mount)
|
||||
luna_app(umount.cpp umount)
|
||||
luna_app(ps.cpp ps)
|
||||
luna_app(time.cpp time)
|
||||
luna_app(ln.cpp ln)
|
||||
luna_app(mktemp.cpp mktemp)
|
||||
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)
|
||||
luna_app(2048.cpp 2048)
|
||||
target_link_libraries(2048 PUBLIC ui)
|
||||
luna_app(clock.cpp clock)
|
||||
target_link_libraries(clock PUBLIC ui)
|
@ -1,70 +0,0 @@
|
||||
#include <os/File.h>
|
||||
#include <os/Process.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <ui/App.h>
|
||||
#include <ui/Button.h>
|
||||
#include <ui/Container.h>
|
||||
#include <ui/Image.h>
|
||||
#include <ui/Layout.h>
|
||||
|
||||
static constexpr ui::Color TASKBAR_COLOR = ui::Color::from_rgb(83, 83, 83);
|
||||
|
||||
void sigchld_handler(int)
|
||||
{
|
||||
wait(nullptr);
|
||||
}
|
||||
|
||||
Result<void> create_widget_group_for_app(ui::HorizontalLayout& layout, Slice<StringView> args, StringView icon)
|
||||
{
|
||||
auto* button = new (std::nothrow) ui::Button({ 0, 0, 50, 50 });
|
||||
if (!button) return err(ENOMEM);
|
||||
layout.add_widget(*button);
|
||||
|
||||
auto* container = new (std::nothrow)
|
||||
ui::Container({ 0, 0, 50, 50 }, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center);
|
||||
if (!container) return err(ENOMEM);
|
||||
button->set_widget(*container);
|
||||
button->set_action([=] { os::Process::spawn(args[0], args, false); });
|
||||
|
||||
auto image = TRY(ui::ImageWidget::load(icon));
|
||||
container->set_widget(*image);
|
||||
|
||||
image.leak();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
{
|
||||
ui::App app;
|
||||
TRY(app.init(argc, argv));
|
||||
|
||||
TRY(os::EventLoop::the().register_signal_handler(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, ui::WindowType::System));
|
||||
app.set_main_window(window);
|
||||
|
||||
window->set_background(TASKBAR_COLOR);
|
||||
|
||||
ui::HorizontalLayout layout(ui::Margins { 0, 0, 0, 0 }, ui::AdjustHeight::Yes, ui::AdjustWidth::No);
|
||||
window->set_main_widget(layout);
|
||||
|
||||
StringView terminal_command[] = { "/usr/bin/terminal" };
|
||||
TRY(create_widget_group_for_app(layout, { terminal_command, 1 }, "/usr/share/icons/32x32/app-terminal.tga"));
|
||||
|
||||
StringView about_command[] = { "/usr/bin/about" };
|
||||
TRY(create_widget_group_for_app(layout, { about_command, 1 }, "/usr/share/icons/32x32/app-about.tga"));
|
||||
|
||||
StringView gol_command[] = { "/usr/bin/gol" };
|
||||
TRY(create_widget_group_for_app(layout, { gol_command, 1 }, "/usr/share/icons/32x32/app-gol.tga"));
|
||||
|
||||
StringView clock_command[] = { "/usr/bin/clock" };
|
||||
TRY(create_widget_group_for_app(layout, { clock_command, 1 }, "/usr/share/icons/32x32/app-clock.tga"));
|
||||
|
||||
return app.run();
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
root:!:0:
|
||||
users:!:1:selene
|
||||
wind:!:2:selene
|
||||
wsys:!:3:
|
||||
selene:!:1000:
|
||||
|
@ -1,6 +1,6 @@
|
||||
Name=login
|
||||
Description=Start the display server.
|
||||
Command=/usr/bin/wind --user=selene
|
||||
Description=Start a graphical user session.
|
||||
Command=/usr/bin/loginui
|
||||
StandardOutput=/dev/uart0
|
||||
StandardError=/dev/uart0
|
||||
Restart=true
|
||||
|
5
base/etc/loginui.conf
Normal file
5
base/etc/loginui.conf
Normal file
@ -0,0 +1,5 @@
|
||||
# Configuration file for loginui.
|
||||
# If this parameter is set to "true", loginui automatically spawns a UI session as the below user instead of prompting for a username and password.
|
||||
Autologin=true
|
||||
# The user to create a session for if "Autologin" is set to true (see above). If the username is invalid, loginui will behave as if "Autologin" was set to false.
|
||||
AutologinUser=selene
|
@ -1,3 +1,3 @@
|
||||
root:toor:0:0:Administrator:/:/usr/bin/sh
|
||||
wind:!:2:2:Window Manager:/:/usr/bin/init
|
||||
selene:moon:1000:1000:User:/home/selene:/usr/bin/sh
|
||||
root:x:0:0:Administrator:/:/usr/bin/sh
|
||||
wind:x:2:2:Window Manager:/:/usr/bin/init
|
||||
selene:x:1000:1000:User:/home/selene:/usr/bin/sh
|
||||
|
3
base/etc/shadow
Normal file
3
base/etc/shadow
Normal file
@ -0,0 +1,3 @@
|
||||
root:ce5ca673d13b36118d54a7cf13aeb0ca012383bf771e713421b4d1fd841f539a:0:0:99999:7:::
|
||||
wind:!:0:0:99999:7:::
|
||||
selene:9e78b43ea00edcac8299e0cc8df7f6f913078171335f733a21d5d911b6999132:0:0:99999:7:::
|
@ -1,14 +1,26 @@
|
||||
Welcome to the Luna operating system!
|
||||
You are running on the default user account, selene.
|
||||
|
||||
If you are familiar with Unix-style operating systems (like Linux or *BSD), you should be able to use the Luna terminal without much problems.
|
||||
If you are familiar with Unix-style operating systems (like Linux or *BSD),
|
||||
you should be able to use the Luna terminal without much problems.
|
||||
|
||||
Following the traditional Unix filesystem structure, programs are installed in /usr/bin (/bin is a symlink to /usr/bin). The command `ls /bin` will show all commands available on your current Luna installation.
|
||||
Following the traditional Unix filesystem structure,
|
||||
programs are installed in /usr/bin (/bin is a symlink to /usr/bin).
|
||||
The command `ls /bin` will show all commands available on
|
||||
your current Luna installation.
|
||||
|
||||
Currently, because of driver limitations, the root file system is mounted read-only. Your home folder is writable, but volatile; it is created and populated on boot, and its contents will vanish after a reboot.
|
||||
Currently, because of driver limitations,
|
||||
the root file system is mounted read-only.
|
||||
Your home folder is writable, but volatile; it is
|
||||
created and populated on boot,
|
||||
and its contents will vanish after a reboot.
|
||||
|
||||
The system is booted using the 'init' program. You can read its configuration files in the /etc/init directory to learn more about the boot process.
|
||||
The system is booted using the 'init' program.
|
||||
You can read its configuration files in the /etc/init directory to
|
||||
learn more about the boot process.
|
||||
|
||||
Luna is free software, released under the BSD-2-Clause license. The license is included in the LICENSE file in your home directory.
|
||||
Luna is free software, released under the BSD-2-Clause license.
|
||||
The license is included in the LICENSE file in your home directory.
|
||||
|
||||
View the source code and read more about Luna at https://git.cloudapio.eu/apio/Luna.
|
||||
View the source code and read more about Luna at
|
||||
https://git.cloudapio.eu/apio/Luna.
|
||||
|
@ -2,5 +2,9 @@
|
||||
# Create and populate a volatile home directory.
|
||||
mount -t tmpfs tmpfs /home/selene
|
||||
chown selene:selene /home/selene
|
||||
|
||||
cp /etc/skel/welcome /home/selene/
|
||||
cp /etc/skel/LICENSE /home/selene/
|
||||
|
||||
chown selene:selene /home/selene/welcome
|
||||
chown selene:selene /home/selene/LICENSE
|
||||
|
@ -1,5 +0,0 @@
|
||||
Name=taskbar
|
||||
Description=Start the taskbar.
|
||||
Command=/usr/bin/taskbar
|
||||
WorkingDirectory=/home/selene
|
||||
Restart=true
|
3
base/etc/user/00-welcome
Normal file
3
base/etc/user/00-welcome
Normal file
@ -0,0 +1,3 @@
|
||||
Name=welcome
|
||||
Description=Show a welcome message for the user.
|
||||
Command=/usr/bin/editor welcome
|
@ -1,4 +0,0 @@
|
||||
Name=terminal
|
||||
Description=Start the terminal.
|
||||
WorkingDirectory=/home/selene
|
||||
Command=/usr/bin/terminal
|
3
base/usr/share/applications/00-terminal
Normal file
3
base/usr/share/applications/00-terminal
Normal file
@ -0,0 +1,3 @@
|
||||
Name=terminal
|
||||
Icon=/usr/share/icons/32x32/app-terminal.tga
|
||||
Command=/usr/bin/terminal
|
3
base/usr/share/applications/01-about
Normal file
3
base/usr/share/applications/01-about
Normal file
@ -0,0 +1,3 @@
|
||||
Name=about
|
||||
Icon=/usr/share/icons/32x32/app-about.tga
|
||||
Command=/usr/bin/about
|
3
base/usr/share/applications/02-gol
Normal file
3
base/usr/share/applications/02-gol
Normal file
@ -0,0 +1,3 @@
|
||||
Name=gol
|
||||
Icon=/usr/share/icons/32x32/app-gol.tga
|
||||
Command=/usr/bin/gol
|
3
base/usr/share/applications/03-clock
Normal file
3
base/usr/share/applications/03-clock
Normal file
@ -0,0 +1,3 @@
|
||||
Name=clock
|
||||
Icon=/usr/share/icons/32x32/app-clock.tga
|
||||
Command=/usr/bin/clock
|
3
base/usr/share/applications/04-2048
Normal file
3
base/usr/share/applications/04-2048
Normal file
@ -0,0 +1,3 @@
|
||||
Name=2048
|
||||
Icon=/usr/share/icons/32x32/app-2048.tga
|
||||
Command=/usr/bin/2048
|
3
base/usr/share/applications/05-editor
Normal file
3
base/usr/share/applications/05-editor
Normal file
@ -0,0 +1,3 @@
|
||||
Name=editor
|
||||
Icon=/usr/share/icons/32x32/app-editor.tga
|
||||
Command=/usr/bin/editor
|
BIN
base/usr/share/icons/32x32/app-2048.tga
Normal file
BIN
base/usr/share/icons/32x32/app-2048.tga
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
BIN
base/usr/share/icons/32x32/app-editor.tga
Normal file
BIN
base/usr/share/icons/32x32/app-editor.tga
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
256
docs/boot_process.md
Normal file
256
docs/boot_process.md
Normal file
@ -0,0 +1,256 @@
|
||||
# The Luna boot process
|
||||
|
||||
## Stage 0: The Bootloader
|
||||
Luna uses the [BOOTBOOT](https://gitlab.com/bztsrc/bootboot) bootloader. _(For more information, read the [bootloader specification](https://gitlab.com/bztsrc/bootboot/-/blob/master/bootboot_spec_1st_ed.pdf).)_
|
||||
|
||||
This bootloader reads the initial ramdisk, which contains the following files:
|
||||
```
|
||||
/sys/config - copy of the configuration file for the bootloader
|
||||
/boot/moon - the kernel itself
|
||||
/bin/preinit - the first user program run in the boot process, before the root filesystem is mounted
|
||||
```
|
||||
|
||||
The bootloader loads the kernel in 64-bit mode into the higher half at address `0xffffffffffe02000`, with an appropriate stack already set up.
|
||||
|
||||
The first 16Gb of memory are identity-mapped at page 0.
|
||||
|
||||
It places a few other things into known addresses:
|
||||
```
|
||||
0xfffffffffc000000 - initial framebuffer
|
||||
0xffffffffffe00000 - bootloader information passed to the kernel
|
||||
0xffffffffffe01000 - kernel command line
|
||||
```
|
||||
|
||||
From here, the kernel takes over.
|
||||
|
||||
## Stage 1: The Kernel
|
||||
_Relevant files: [kernel/src/main.cpp](../kernel/src/main.cpp), [kernel/src/arch/x86_64/CPU.cpp](../kernel/src/arch/x86_64/CPU.cpp#L285)_
|
||||
|
||||
The kernel begins execution in the `_start()` function. This function initializes basic kernel functionality, such as time-keeping, memory management, graphics, and finally threading.
|
||||
|
||||
Once threading is set up and the scheduler is started, the kernel starts up a new kernel thread titled `[kinit]` to finish starting up other subsystems that assume they're running in a thread.
|
||||
|
||||
Before switching to `[kinit]`, `_start` does one more thing, it calls the `CPU::platform_finish_init()` function which is platform-specific. On x86_64, this function does the following things:
|
||||
|
||||
- Creates a new kernel thread: `[x86_64-io]`, which handles keyboard and mouse interrupts asynchronously
|
||||
- Starts receiving external interrupts
|
||||
- Initializes the mouse
|
||||
|
||||
As soon as the scheduler switches to the `[kinit]` thread, it will never return to `_start` (since it has no thread associated to it).
|
||||
|
||||
**IMPORTANT**: Although the `[kinit]` thread is the first thread to be started in the system, it has PID 2, not 1. The reason for this is that PID 1 is reserved for the userspace init process.
|
||||
|
||||
`[kinit]` does the following things, in order:
|
||||
|
||||
- Loads kernel debug symbols from the initial ramdisk
|
||||
- Creates the virtual file system and mounts the initial ramdisk on /
|
||||
- Initializes virtual device files such as `/dev/null` (the internal kernel representation of them, `/dev` is not mounted yet)
|
||||
- Loads `/bin/preinit` from the initial ramdisk as PID 1
|
||||
- Creates two more kernel threads, `[reap]` and `[oom]`
|
||||
- Scans for ATA hard disks and reads their partition tables
|
||||
- Finally, it sets PID 1's state to "Running" so that the scheduler can switch to it, and exits
|
||||
|
||||
### Kernel threads
|
||||
|
||||
`[kinit]` spawns two more kernel threads, `[reap]` and `[oom]`. While `[kinit]` exits before PID 1 is started, `[reap]` and `[oom]` are present throughout the lifetime of a Luna system, and can be seen in the output of `ps`. Let's take a look at what they do.
|
||||
|
||||
- `[reap]`: To understand what this thread does, we must take a look at what happens when processes exit on Luna.
|
||||
|
||||
_(Relevant files: [kernel/src/main.cpp](../kernel/src/main.cpp#L23), [kernel/src/thread/Scheduler.cpp](../kernel/src/thread/Scheduler.cpp#L231), [kernel/src/thread/Thread.cpp](../kernel/src/thread/Thread.cpp#L126), [kernel/src/sys/waitpid.cpp](../kernel/src/sys/waitpid.cpp#L84))_
|
||||
|
||||
When a process calls the `_exit()` syscall, all its threads' states are set to "Dying". This tells the scheduler to avoid switching to them, and the process's parent is notified, by sending SIGCHLD and (optionally) unblocking a blocked `waitpid()` call. The process remains visible to the rest of the system, and if its parent does not wait for it, it will stay there as a "zombie process". Meanwhile, the `[reap]` thread runs and collects all the resources from each thread. The process object is still alive (in a "zombie" state), but its threads have been cleaned up.
|
||||
|
||||
When the process's parent waits for it, it is marked for reaping (by setting its thread count to -1 (PROCESS_SHOULD_REAP)), and the `[reap]` thread runs.
|
||||
|
||||
The `[reap]` thread then "reaps" all the dead processes' resources. It frees up their memory, file descriptors, and other resources. After reaping, the process is deleted, and no trace of it is left.
|
||||
|
||||
- `[oom]`: This thread handles Out-Of-Memory (OOM) situations. Whenever the kernel has 1/4 or 1/8 of the available physical memory left (thresholds may be tweaked in the future), or it has run out, it runs this thread.
|
||||
|
||||
The OOM thread then goes through all the disk caches and purges them all, hoping to reclaim as much memory as possible.
|
||||
|
||||
### File system and process layout
|
||||
|
||||
After the kernel stage of the boot process, the system looks like this:
|
||||
|
||||
#### File system
|
||||
```
|
||||
/ - initial ramdisk
|
||||
/sys/config - copy of the configuration file for the bootloader
|
||||
/boot/moon - the kernel itself
|
||||
/bin/preinit - the first user program run in the boot process
|
||||
```
|
||||
|
||||
#### Processes
|
||||
```
|
||||
/bin/preinit - PID 1
|
||||
[kinit] - PID 2 (Exited, soon to be reaped)
|
||||
[x86_64-io] - PID 3
|
||||
[reap] - PID 4
|
||||
[oom] - PID 5
|
||||
```
|
||||
|
||||
## Stage 2: preinit
|
||||
_Relevant files: [system/preinit.cpp](../system/preinit.cpp)_
|
||||
|
||||
Luna's userspace init process is split into two programs: `/bin/preinit`, which resides on the initial ramdisk, and `/usr/bin/init`, which resides on the root partition.
|
||||
|
||||
`/bin/preinit`'s job is to set up the file system in a "minimal known good" state for the actual `init` to run.
|
||||
|
||||
The "minimal known good" state includes:
|
||||
|
||||
- The ext2 root partition, which includes all the binaries in /usr
|
||||
- The /dev file system
|
||||
|
||||
`preinit` does the following things, in order:
|
||||
|
||||
- Mounts `/dev` to get access to disk device files
|
||||
- Mounts the root partition (`/dev/cd0p2`) on `/osroot`
|
||||
- Unmounts `/dev`
|
||||
- Uses the `pivot_root` system call to change the root file system to the one that was in `/osroot`, and mounts the old one on `/mnt` (previously `/osroot/mnt`)
|
||||
- Unmounts the initial ramdisk on `/mnt`
|
||||
- Mounts the `/dev` file system again on the new root partition
|
||||
- Executes `/usr/bin/init`
|
||||
|
||||
For now, much of `preinit`'s functionality is hard-coded, but as Luna supports more devices, it will become responsible for loading device drivers, discovering the root partition, and more...
|
||||
|
||||
### File system and process layout
|
||||
|
||||
After the preinit stage of the boot process, the system looks like this:
|
||||
|
||||
#### File system
|
||||
```
|
||||
/ - ext2 root partition
|
||||
/dev - device file system
|
||||
/usr, /etc, /home... - other directories contained in the root partition
|
||||
```
|
||||
|
||||
#### Processes
|
||||
```
|
||||
/usr/bin/init - PID 1
|
||||
[x86_64-io] - PID 3
|
||||
[reap] - PID 4
|
||||
[oom] - PID 5
|
||||
```
|
||||
|
||||
## Stage 3: init
|
||||
_Relevant files: [system/init.cpp](../system/init.cpp#L406)_
|
||||
|
||||
`/usr/bin/init` is the actual init system. It is in charge of starting user-defined services.
|
||||
|
||||
It does the following things:
|
||||
|
||||
- Mounts `/tmp`, `/dev/shm` and `/dev/pts`
|
||||
- Sets the system hostname by reading `/etc/hostname`
|
||||
- Reads configuration files from `/etc/init`
|
||||
- Starts services defined in `/etc/init`
|
||||
- Enters the init loop, waiting for child processes and restarting them if needed
|
||||
|
||||
Currently, there are two service files defined by default in `/etc/init`:
|
||||
|
||||
`00-home`: This service sets up a `tmpfs` on `/home/selene`, so that the home directory is writable.
|
||||
|
||||
`99-login`: This service starts a graphical session, by calling `/usr/bin/loginui`. This service will be restarted if necessary.
|
||||
|
||||
### File system and process layout
|
||||
|
||||
After the init stage of the boot process, the system looks like this:
|
||||
|
||||
#### File system
|
||||
```
|
||||
/ - ext2 root partition
|
||||
/dev - device file system
|
||||
/dev/shm - POSIX shared memory file system
|
||||
/dev/pts - POSIX pseudoterminal file system
|
||||
/tmp - system temporary file directory
|
||||
/usr, /etc, /home... - other directories contained in the root partition
|
||||
/home/selene - temporary home directory
|
||||
```
|
||||
|
||||
#### Processes
|
||||
```
|
||||
/usr/bin/init - PID 1
|
||||
[x86_64-io] - PID 3
|
||||
[reap] - PID 4
|
||||
[oom] - PID 5
|
||||
/usr/bin/loginui - PID 13
|
||||
```
|
||||
|
||||
_Note: loginui is PID 13 because the `00-home` service is a shell script, which starts a few subprocesses. Since Luna does not allow for PID reuse right now, loginui ends up with PID 13._
|
||||
|
||||
## Stage 4: loginui
|
||||
_Relevant files: [gui/loginui.cpp](../gui/loginui.cpp), [gui/wind/main.cpp](../gui/wind/main.cpp)_
|
||||
|
||||
`/usr/bin/loginui`'s job is quite simple: it prompts the user to log in with their password, after which a graphical session is started.
|
||||
|
||||
_Note: On development builds, Autologin=true is added to /etc/loginui.conf which disables password prompting and executes startui directly._
|
||||
|
||||
First, loginui starts the display server, `/usr/bin/wind`, so that it can use its capabilities to show a graphical login prompt. It is started with permissions `root:root`, and later drops privileges to `wind:wind`.
|
||||
|
||||
After that, loginui prompts for a username and password, checks it against the hashed password stored in `/etc/shadow`, and finally executes `/usr/bin/startui` which does the actual heavy work of starting all the services needed for a UI session.
|
||||
|
||||
### File system and process layout
|
||||
|
||||
After the loginui stage of the boot process, the system looks like this:
|
||||
|
||||
#### File system
|
||||
```
|
||||
/ - ext2 root partition
|
||||
/dev - device file system
|
||||
/dev/shm - POSIX shared memory file system
|
||||
/dev/pts - POSIX pseudoterminal file system
|
||||
/tmp - system temporary file directory
|
||||
/usr, /etc, /home... - other directories contained in the root partition
|
||||
/home/selene - temporary home directory
|
||||
```
|
||||
|
||||
#### Processes
|
||||
```
|
||||
/usr/bin/init - PID 1
|
||||
[x86_64-io] - PID 3
|
||||
[reap] - PID 4
|
||||
[oom] - PID 5
|
||||
/usr/bin/startui - PID 13
|
||||
/usr/bin/wind - PID 14
|
||||
```
|
||||
|
||||
## Stage 5: startui
|
||||
_Relevant files: [system/startui.cpp](../system/startui.cpp), [gui/wind/main.cpp](../gui/wind/main.cpp)_
|
||||
|
||||
`/usr/bin/startui` starts a graphical user session.
|
||||
|
||||
A Luna graphical user session includes the following components:
|
||||
|
||||
- The display server itself, `/usr/bin/wind`. If not already started by loginui, `startui` makes sure it's running.
|
||||
- The execution server (`/usr/bin/execd`), which starts processes and keeps them alive on behalf of other processes. It is started with the standard permissions `selene:selene`.
|
||||
- The taskbar, `/usr/bin/taskbar`. It is started with the standard permissions `selene:selene`, plus an extra group `wsys` to be able to connect to a special display server socket (`/tmp/wsys.sock`, as opposed to the standard `/tmp/wind.sock`). This grants it the ability to use advanced wind features, such as placing the taskbar window behind all other windows.
|
||||
- The init process corresponding to that session (`/usr/bin/init --user`). This process does the same thing as `init` above (manages services), but runs with user privileges and reads configuration files from `/etc/user` instead (in the future this will be changed to a user-specific directory).
|
||||
|
||||
Currently, `init --user` only does one thing: it opens up a text editor with a welcome message on startup. It can be configured to do whatever the user desires to do on startup, by placing the appropriate configuration files in `/etc/user`.
|
||||
|
||||
### File system and process layout
|
||||
|
||||
After the startui stage of the boot process, the system is fully started up and looks like this:
|
||||
|
||||
#### File system
|
||||
```
|
||||
/ - ext2 root partition
|
||||
/dev - device file system
|
||||
/dev/shm - POSIX shared memory file system
|
||||
/dev/pts - POSIX pseudoterminal file system
|
||||
/tmp - system temporary file directory
|
||||
/usr, /etc, /home... - other directories contained in the root partition
|
||||
/home/selene - temporary home directory
|
||||
```
|
||||
|
||||
#### Processes
|
||||
```
|
||||
/usr/bin/init - PID 1
|
||||
[x86_64-io] - PID 3
|
||||
[reap] - PID 4
|
||||
[oom] - PID 5
|
||||
/usr/bin/startui - PID 13
|
||||
/usr/bin/wind - PID 14
|
||||
/usr/bin/execd - PID 15
|
||||
/usr/bin/taskbar - PID 16
|
||||
/usr/bin/init --user - PID 17
|
||||
/usr/bin/editor welcome - PID 18
|
35
docs/dependencies.md
Normal file
35
docs/dependencies.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Dependencies required to build and run Luna
|
||||
|
||||
## System requirements
|
||||
|
||||
Any modern UNIX-like system that supports all the tools listed below should work (Hopefully, that will include Luna itself in the future!).
|
||||
|
||||
I personally build and run Luna on an amd64 Fedora Linux 40 machine. CI runs on arm64 Ubuntu 22.04. Any other configurations are untested. Windows is not supported, although you can try using WSL if you really want to.
|
||||
|
||||
## Building a cross-compiler toolchain
|
||||
For this, you should start by installing the [required dependencies](https://wiki.osdev.org/GCC_Cross_Compiler#Installing_Dependencies) for any OSdev cross-compiler build.
|
||||
|
||||
Also make sure you have the perl module `File::Compare` installed, it is required to build autoconf. On Fedora you can install it using the package manager by running `# dnf install perl-File-Compare`. If your distro doesn't have it, you might have to install it via `cpan`.
|
||||
|
||||
## Building the actual system
|
||||
The build process needs some extra dependencies to run: `cmake`, `ninja`, `nasm`, `fakeroot` and `genext2fs`. On some distributions the `ninja` package is called `ninja-build` instead.
|
||||
|
||||
If you want to use `make` instead of `ninja`, create a file called `env-local.sh` in the project root and add the line `USE_MAKE=1`. In this case, ninja does not need to be installed.
|
||||
|
||||
## Running the built image in a virtual machine
|
||||
The script provided by the project to run the system, `tools/run.sh`, assumes that QEMU is installed and uses that to run the image. Therefore, make sure your system has `qemu-system-x86_64` in the PATH. If it doesn't, install it using the method appropriate for your system, usually installing `qemu` or `qemu-system` from the package manager.
|
||||
|
||||
That being said, there's no requirement to use QEMU. If you want to use a different virtualization program, such as Oracle VirtualBox or VMWare, just use `tools/build-iso.sh` instead of `run.sh` and use the built `Luna.iso` in those programs.
|
||||
|
||||
## Formatting/linting
|
||||
|
||||
Please make sure you have `clang-format` installed. Additionally, if your editor does not support format-on-save or you do not have it configured, please run `tools/run-clang-format.sh` before committing, to make sure all code follows the same style conventions.
|
||||
|
||||
## Source dependencies
|
||||
TLDR: Luna does not depend on any third-party library.
|
||||
|
||||
Every part of Luna is written from scratch and depends only on its own libraries and programs, with two small exceptions (included here for crediting and licensing purposes, but there is no need to download and build them separately):
|
||||
|
||||
The bootloader, BOOTBOOT. It is available at [gitlab.com/bztsrc/bootboot](https://gitlab.com/bztsrc/bootboot), under the MIT license. It is automatically pulled and built from source by `tools/setup.sh`.
|
||||
|
||||
[libc/src/strtod.cpp](../libc/src/strtod.cpp). Written by Yasuhiro Matsumoto, adapted from https://gist.github.com/mattn/1890186 and available under a public domain license.
|
17
gui/CMakeLists.txt
Normal file
17
gui/CMakeLists.txt
Normal file
@ -0,0 +1,17 @@
|
||||
function(luna_service SOURCE_FILE APP_NAME)
|
||||
add_executable(${APP_NAME} ${SOURCE_FILE})
|
||||
target_compile_options(${APP_NAME} PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||
add_dependencies(${APP_NAME} libc)
|
||||
target_include_directories(${APP_NAME} PRIVATE ${LUNA_BASE}/usr/include)
|
||||
target_link_libraries(${APP_NAME} PRIVATE os)
|
||||
install(TARGETS ${APP_NAME} DESTINATION ${LUNA_BASE}/usr/bin)
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(libui)
|
||||
add_subdirectory(wind)
|
||||
add_subdirectory(apps)
|
||||
|
||||
luna_service(execd.cpp execd)
|
||||
luna_service(run.cpp run)
|
||||
luna_service(loginui.cpp loginui)
|
||||
target_link_libraries(loginui PRIVATE ui)
|
@ -1,4 +1,4 @@
|
||||
#include <luna/String.h>
|
||||
#include <luna/RefString.h>
|
||||
#include <luna/Utf8.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
@ -50,9 +50,9 @@ class GameWidget final : public ui::Widget
|
||||
|
||||
bool should_add_tile = false;
|
||||
|
||||
switch (request.key)
|
||||
switch (request.code)
|
||||
{
|
||||
case 'w': {
|
||||
case moon::K_UpArrow: {
|
||||
bool changed;
|
||||
changed = move_up();
|
||||
if (changed) should_add_tile = true;
|
||||
@ -61,7 +61,7 @@ class GameWidget final : public ui::Widget
|
||||
if (changed) should_add_tile = true;
|
||||
}
|
||||
break;
|
||||
case 'a': {
|
||||
case moon::K_LeftArrow: {
|
||||
bool changed;
|
||||
changed = move_left();
|
||||
if (changed) should_add_tile = true;
|
||||
@ -70,7 +70,7 @@ class GameWidget final : public ui::Widget
|
||||
if (changed) should_add_tile = true;
|
||||
}
|
||||
break;
|
||||
case 's': {
|
||||
case moon::K_DownArrow: {
|
||||
bool changed;
|
||||
changed = move_down();
|
||||
if (changed) should_add_tile = true;
|
||||
@ -79,7 +79,7 @@ class GameWidget final : public ui::Widget
|
||||
if (changed) should_add_tile = true;
|
||||
}
|
||||
break;
|
||||
case 'd': {
|
||||
case moon::K_RightArrow: {
|
||||
bool changed;
|
||||
changed = move_right();
|
||||
if (changed) should_add_tile = true;
|
||||
@ -88,7 +88,7 @@ class GameWidget final : public ui::Widget
|
||||
if (changed) should_add_tile = true;
|
||||
}
|
||||
break;
|
||||
case 'r': {
|
||||
case moon::K_Home: {
|
||||
reset();
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
@ -325,7 +325,7 @@ class GameWidget final : public ui::Widget
|
||||
|
||||
canvas.fill(colors[tile.color]);
|
||||
|
||||
auto fmt = TRY(String::format("%d"_sv, tile.number));
|
||||
auto fmt = TRY(RefString::format("%d"_sv, tile.number));
|
||||
|
||||
auto font = ui::Font::default_bold_font();
|
||||
auto rect = ui::align({ 0, 0, canvas.width, canvas.height },
|
||||
@ -343,12 +343,12 @@ class GameWidget final : public ui::Widget
|
||||
}
|
||||
};
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
Result<int> luna_main(int, char**)
|
||||
{
|
||||
srand((unsigned)time(NULL));
|
||||
|
||||
ui::App app;
|
||||
TRY(app.init(argc, argv));
|
||||
TRY(app.init());
|
||||
|
||||
auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 400 }));
|
||||
app.set_main_window(window);
|
17
gui/apps/CMakeLists.txt
Normal file
17
gui/apps/CMakeLists.txt
Normal file
@ -0,0 +1,17 @@
|
||||
function(luna_app SOURCE_FILE APP_NAME)
|
||||
add_executable(${APP_NAME} ${SOURCE_FILE})
|
||||
target_compile_options(${APP_NAME} PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||
add_dependencies(${APP_NAME} libc)
|
||||
target_include_directories(${APP_NAME} PRIVATE ${LUNA_BASE}/usr/include)
|
||||
target_link_libraries(${APP_NAME} PRIVATE os ui)
|
||||
install(TARGETS ${APP_NAME} DESTINATION ${LUNA_BASE}/usr/bin)
|
||||
endfunction()
|
||||
|
||||
luna_app(about.cpp about)
|
||||
luna_app(taskbar.cpp taskbar)
|
||||
luna_app(2048.cpp 2048)
|
||||
luna_app(clock.cpp clock)
|
||||
luna_app(gol.cpp gol)
|
||||
|
||||
add_subdirectory(editor)
|
||||
add_subdirectory(terminal)
|
@ -7,10 +7,10 @@
|
||||
|
||||
static constexpr ui::Color BACKGROUND_COLOR = ui::Color::from_rgb(89, 89, 89);
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
Result<int> luna_main(int, char**)
|
||||
{
|
||||
ui::App app;
|
||||
TRY(app.init(argc, argv));
|
||||
TRY(app.init());
|
||||
|
||||
auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 300 }));
|
||||
app.set_main_window(window);
|
@ -18,10 +18,10 @@ void update_time()
|
||||
ui::App::the().main_window()->draw();
|
||||
}
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
Result<int> luna_main(int, char**)
|
||||
{
|
||||
ui::App app;
|
||||
TRY(app.init(argc, argv));
|
||||
TRY(app.init());
|
||||
|
||||
auto* window = TRY(ui::Window::create(ui::Rect { 500, 400, 100, 50 }));
|
||||
app.set_main_window(window);
|
12
gui/apps/editor/CMakeLists.txt
Normal file
12
gui/apps/editor/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
EditorWidget.h
|
||||
EditorWidget.cpp
|
||||
)
|
||||
|
||||
add_executable(editor ${SOURCES})
|
||||
target_compile_options(editor PRIVATE -Os ${COMMON_FLAGS} -Wno-write-strings)
|
||||
add_dependencies(editor libc)
|
||||
target_include_directories(editor PRIVATE ${LUNA_BASE}/usr/include ${CMAKE_CURRENT_LIST_DIR})
|
||||
target_link_libraries(editor PRIVATE os ui)
|
||||
install(TARGETS editor DESTINATION ${LUNA_BASE}/usr/bin)
|
259
gui/apps/editor/EditorWidget.cpp
Normal file
259
gui/apps/editor/EditorWidget.cpp
Normal file
@ -0,0 +1,259 @@
|
||||
/**
|
||||
* @file EditorWidget.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Multiline text editing widget.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "EditorWidget.h"
|
||||
#include <ctype.h>
|
||||
#include <luna/PathParser.h>
|
||||
#include <luna/RefString.h>
|
||||
#include <luna/Utf8.h>
|
||||
#include <os/File.h>
|
||||
#include <os/FileSystem.h>
|
||||
#include <ui/App.h>
|
||||
#include <ui/Dialog.h>
|
||||
|
||||
EditorWidget::EditorWidget(SharedPtr<ui::Font> font) : ui::TextInput(), m_font(font)
|
||||
{
|
||||
recalculate_lines();
|
||||
}
|
||||
|
||||
Result<void> EditorWidget::load_file(const os::Path& path)
|
||||
{
|
||||
struct stat st;
|
||||
auto rc = os::FileSystem::stat(path, st, true);
|
||||
|
||||
if (!rc.has_error() && !S_ISREG(st.st_mode))
|
||||
{
|
||||
auto message = TRY(RefString::format("%s is not a regular file", path.name().chars()));
|
||||
ui::Dialog::show_message("Error", message.view());
|
||||
return {};
|
||||
}
|
||||
|
||||
os::eprintln("Loading file: %s", path.name().chars());
|
||||
|
||||
auto file = TRY(os::File::open_or_create(path, os::File::ReadOnly));
|
||||
|
||||
m_data = TRY(file->read_all());
|
||||
|
||||
os::eprintln("Read %zu bytes.", m_data.size());
|
||||
|
||||
m_cursor = m_data.size();
|
||||
|
||||
m_path = TRY(String::from_string_view(path.name()));
|
||||
|
||||
auto basename = TRY(PathParser::basename(m_path.view()));
|
||||
|
||||
String title = TRY(String::format("Text Editor - %s"_sv, basename.chars()));
|
||||
window()->set_title(title.view());
|
||||
|
||||
TRY(recalculate_lines());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<ui::EventResult> EditorWidget::handle_key_event(const ui::KeyEventRequest& request)
|
||||
{
|
||||
// Avoid handling "key released" events
|
||||
if (!request.pressed) return ui::EventResult::DidNotHandle;
|
||||
|
||||
if (request.code == moon::K_UpArrow)
|
||||
{
|
||||
if (m_cursor_position.y > 0) m_cursor_position.y--;
|
||||
else
|
||||
return ui::EventResult::DidNotHandle;
|
||||
recalculate_cursor_index();
|
||||
update_cursor();
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.code == moon::K_DownArrow)
|
||||
{
|
||||
if (m_cursor_position.y + 1 < (int)m_lines.size()) m_cursor_position.y++;
|
||||
else
|
||||
return ui::EventResult::DidNotHandle;
|
||||
recalculate_cursor_index();
|
||||
update_cursor();
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.code == moon::K_LeftArrow)
|
||||
{
|
||||
if (m_cursor > 0) m_cursor--;
|
||||
else
|
||||
return ui::EventResult::DidNotHandle;
|
||||
recalculate_cursor_position();
|
||||
update_cursor();
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.code == moon::K_RightArrow)
|
||||
{
|
||||
if (m_cursor < m_data.size()) m_cursor++;
|
||||
else
|
||||
return ui::EventResult::DidNotHandle;
|
||||
recalculate_cursor_position();
|
||||
update_cursor();
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.code == moon::K_Backspace)
|
||||
{
|
||||
if (m_cursor == 0) return ui::EventResult::DidNotHandle;
|
||||
m_cursor--;
|
||||
|
||||
delete_current_character();
|
||||
|
||||
TRY(recalculate_lines());
|
||||
|
||||
update_cursor();
|
||||
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.letter != '\n' && iscntrl(request.letter)) return ui::EventResult::DidNotHandle;
|
||||
|
||||
if (m_cursor == m_data.size()) TRY(m_data.append_data((const u8*)&request.letter, 1));
|
||||
else
|
||||
TRY(insert_character(request.letter));
|
||||
|
||||
m_cursor++;
|
||||
TRY(recalculate_lines());
|
||||
|
||||
update_cursor();
|
||||
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
Result<void> EditorWidget::save_file_as()
|
||||
{
|
||||
ui::Dialog::show_input_dialog(
|
||||
"Save file as...", "Please enter the path to save this file to:", [this](StringView path) {
|
||||
m_path = String::from_string_view(path).release_value();
|
||||
auto rc = save_file();
|
||||
if (rc.has_error())
|
||||
{
|
||||
os::eprintln("Failed to save file %s: %s", m_path.chars(), rc.error_string());
|
||||
ui::Dialog::show_message("Error", "Failed to save file");
|
||||
}
|
||||
else
|
||||
{
|
||||
auto basename = PathParser::basename(m_path.view()).release_value();
|
||||
|
||||
String title = String::format("Text Editor - %s"_sv, basename.chars()).release_value();
|
||||
window()->set_title(title.view());
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<void> EditorWidget::save_file()
|
||||
{
|
||||
if (m_path.is_empty())
|
||||
{
|
||||
TRY(save_file_as());
|
||||
return {};
|
||||
}
|
||||
|
||||
auto file = TRY(os::File::open_or_create(m_path.view(), os::File::WriteOnly));
|
||||
return file->write(m_data);
|
||||
}
|
||||
|
||||
Result<void> EditorWidget::draw(ui::Canvas& canvas)
|
||||
{
|
||||
int visible_lines = canvas.height / m_font->height();
|
||||
int visible_columns = canvas.width / m_font->width();
|
||||
|
||||
if ((usize)visible_lines > m_lines.size()) visible_lines = static_cast<int>(m_lines.size());
|
||||
|
||||
for (int i = 0; i < visible_lines; i++)
|
||||
{
|
||||
auto line = m_lines[i];
|
||||
if (line.begin == line.end) continue;
|
||||
|
||||
auto slice = TRY(m_data.slice(line.begin, line.end - line.begin));
|
||||
auto string = TRY(
|
||||
String::from_string_view(StringView::from_fixed_size_cstring((const char*)slice, line.end - line.begin)));
|
||||
|
||||
Utf8StringDecoder decoder(string.chars());
|
||||
wchar_t buf[4096];
|
||||
decoder.decode(buf, sizeof(buf)).release_value();
|
||||
|
||||
int characters_to_render = (int)wcslen(buf);
|
||||
|
||||
for (int j = 0; j < visible_columns && j < characters_to_render; j++)
|
||||
{
|
||||
auto subcanvas =
|
||||
canvas.subcanvas({ j * m_font->width(), i * m_font->height(), m_font->width(), m_font->height() });
|
||||
m_font->render(buf[j], ui::WHITE, subcanvas);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the cursor
|
||||
if (m_cursor_position.x < visible_columns && m_cursor_position.y < visible_lines && m_cursor_activated)
|
||||
{
|
||||
canvas
|
||||
.subcanvas(
|
||||
{ m_cursor_position.x * m_font->width(), m_cursor_position.y * m_font->height(), 1, m_font->height() })
|
||||
.fill(ui::WHITE);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<void> EditorWidget::recalculate_lines()
|
||||
{
|
||||
m_lines.clear();
|
||||
|
||||
Line l;
|
||||
l.begin = 0;
|
||||
for (usize i = 0; i < m_data.size(); i++)
|
||||
{
|
||||
if (m_data.data()[i] == '\n')
|
||||
{
|
||||
l.end = i;
|
||||
TRY(m_lines.try_append(l));
|
||||
l.begin = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
l.end = m_data.size();
|
||||
TRY(m_lines.try_append(l));
|
||||
|
||||
recalculate_cursor_position();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void EditorWidget::recalculate_cursor_position()
|
||||
{
|
||||
if (m_cursor == 0) m_cursor_position = { 0, 0 };
|
||||
|
||||
for (int i = 0; i < (int)m_lines.size(); i++)
|
||||
{
|
||||
auto line = m_lines[i];
|
||||
if (m_cursor >= line.begin && m_cursor <= line.end)
|
||||
{
|
||||
m_cursor_position.x = (int)(m_cursor - line.begin);
|
||||
m_cursor_position.y = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unreachable();
|
||||
}
|
||||
|
||||
void EditorWidget::recalculate_cursor_index()
|
||||
{
|
||||
m_cursor = m_lines[m_cursor_position.y].begin + m_cursor_position.x;
|
||||
if (m_cursor > m_lines[m_cursor_position.y].end)
|
||||
{
|
||||
m_cursor = m_lines[m_cursor_position.y].end;
|
||||
recalculate_cursor_position();
|
||||
}
|
||||
}
|
50
gui/apps/editor/EditorWidget.h
Normal file
50
gui/apps/editor/EditorWidget.h
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @file EditorWidget.h
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Multiline text editing widget.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <luna/String.h>
|
||||
#include <os/Timer.h>
|
||||
#include <ui/Font.h>
|
||||
#include <ui/TextInput.h>
|
||||
#include <ui/Widget.h>
|
||||
|
||||
class EditorWidget : public ui::TextInput
|
||||
{
|
||||
public:
|
||||
EditorWidget(SharedPtr<ui::Font> font);
|
||||
|
||||
Result<void> load_file(const os::Path& path);
|
||||
|
||||
Result<void> save_file();
|
||||
Result<void> save_file_as();
|
||||
|
||||
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||
|
||||
Result<void> draw(ui::Canvas& canvas) override;
|
||||
|
||||
os::Path path()
|
||||
{
|
||||
return m_path.view();
|
||||
}
|
||||
|
||||
private:
|
||||
SharedPtr<ui::Font> m_font;
|
||||
|
||||
struct Line
|
||||
{
|
||||
usize begin;
|
||||
usize end;
|
||||
};
|
||||
Vector<Line> m_lines;
|
||||
|
||||
String m_path;
|
||||
|
||||
Result<void> recalculate_lines();
|
||||
void recalculate_cursor_position();
|
||||
void recalculate_cursor_index();
|
||||
};
|
53
gui/apps/editor/main.cpp
Normal file
53
gui/apps/editor/main.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file main.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Graphical text editor.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "EditorWidget.h"
|
||||
#include <os/ArgumentParser.h>
|
||||
#include <os/File.h>
|
||||
#include <ui/App.h>
|
||||
#include <ui/Dialog.h>
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
{
|
||||
StringView path;
|
||||
|
||||
os::ArgumentParser parser;
|
||||
parser.add_description("A graphical text editor"_sv);
|
||||
parser.add_system_program_info("editor"_sv);
|
||||
parser.add_positional_argument(path, "path", false);
|
||||
parser.parse(argc, argv);
|
||||
|
||||
ui::App app;
|
||||
TRY(app.init());
|
||||
|
||||
auto* window = TRY(ui::Window::create(ui::Rect { 200, 300, 600, 600 }));
|
||||
window->set_background(ui::Color::from_rgb(40, 40, 40));
|
||||
window->set_title("Text Editor");
|
||||
app.set_main_window(window);
|
||||
|
||||
auto* editor = TRY(make<EditorWidget>(ui::Font::default_font()));
|
||||
window->set_main_widget(*editor);
|
||||
if (!path.is_empty()) editor->load_file(path);
|
||||
|
||||
TRY(window->add_keyboard_shortcut({ moon::K_CH26, ui::Mod_Ctrl }, true, [&](ui::Shortcut) {
|
||||
auto result = editor->save_file();
|
||||
if (result.has_error())
|
||||
{
|
||||
os::eprintln("Failed to save file %s: %s", editor->path().name().chars(), result.error_string());
|
||||
ui::Dialog::show_message("Error", "Failed to save file");
|
||||
}
|
||||
}));
|
||||
|
||||
TRY(window->add_keyboard_shortcut({ moon::K_CH26, ui::Mod_Ctrl | ui::Mod_Shift }, true,
|
||||
[&](ui::Shortcut) { editor->save_file_as(); }));
|
||||
|
||||
window->draw();
|
||||
|
||||
return app.run();
|
||||
}
|
@ -108,10 +108,10 @@ static void update()
|
||||
draw_cells();
|
||||
}
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
Result<int> luna_main(int, char**)
|
||||
{
|
||||
ui::App app;
|
||||
TRY(app.init(argc, argv));
|
||||
TRY(app.init());
|
||||
|
||||
g_window = TRY(ui::Window::create(ui::Rect { 200, 200, 600, 400 }));
|
||||
g_window->set_title("Game of Life");
|
152
gui/apps/taskbar.cpp
Normal file
152
gui/apps/taskbar.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include <luna/Sort.h>
|
||||
#include <luna/StringBuilder.h>
|
||||
#include <os/Config.h>
|
||||
#include <os/Directory.h>
|
||||
#include <os/File.h>
|
||||
#include <os/FileSystem.h>
|
||||
#include <os/IPC.h>
|
||||
#include <os/Process.h>
|
||||
#include <os/ipc/Launcher.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <ui/App.h>
|
||||
#include <ui/Button.h>
|
||||
#include <ui/Container.h>
|
||||
#include <ui/Image.h>
|
||||
#include <ui/Layout.h>
|
||||
|
||||
static constexpr ui::Color TASKBAR_COLOR = ui::Color::from_rgb(83, 83, 83);
|
||||
|
||||
static OwnedPtr<os::IPC::Client> launcher_client;
|
||||
|
||||
void sigquit_handler(int)
|
||||
{
|
||||
// Reload the taskbar by exec-ing the executable, resetting everything.
|
||||
StringView args[] = { "/usr/bin/taskbar" };
|
||||
os::Process::exec(args[0], { args, 1 });
|
||||
}
|
||||
|
||||
Result<void> create_widget_group_for_app(ui::HorizontalLayout& layout, StringView path, StringView icon)
|
||||
{
|
||||
auto* button = TRY(make<ui::Button>(ui::Rect { 0, 0, 50, 50 }));
|
||||
layout.add_widget(*button);
|
||||
|
||||
auto* container = TRY(
|
||||
make<ui::Container>(ui::Rect { 0, 0, 50, 50 }, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center));
|
||||
button->set_widget(*container);
|
||||
button->set_action([=] {
|
||||
os::Launcher::LaunchDetachedRequest request;
|
||||
SET_IPC_STRING(request.command, path.chars());
|
||||
launcher_client->send_async(request);
|
||||
});
|
||||
|
||||
auto image = TRY(ui::ImageWidget::load(icon));
|
||||
container->set_widget(*image);
|
||||
|
||||
image.leak();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
struct ApplicationFile
|
||||
{
|
||||
String name;
|
||||
String command;
|
||||
String icon;
|
||||
};
|
||||
|
||||
Vector<ApplicationFile> s_app_files;
|
||||
|
||||
// Pretty much copied from init.cpp.
|
||||
static Result<void> load_application_file(const os::Path& path)
|
||||
{
|
||||
os::println("[taskbar] reading app file: %s", path.name().chars());
|
||||
|
||||
auto file = TRY(os::ConfigFile::open(path));
|
||||
|
||||
ApplicationFile app_file;
|
||||
|
||||
app_file.name = TRY(String::from_string_view(file->read_string_or("Name", "")));
|
||||
if (app_file.name.is_empty())
|
||||
{
|
||||
os::println("[taskbar] app file is missing 'Name' entry, aborting!");
|
||||
return {};
|
||||
}
|
||||
|
||||
app_file.command = TRY(String::from_string_view(file->read_string_or("Command", "")));
|
||||
if (app_file.command.is_empty())
|
||||
{
|
||||
os::println("[taskbar] app file is missing 'Command' entry, aborting!");
|
||||
return {};
|
||||
}
|
||||
|
||||
app_file.icon = TRY(String::from_string_view(file->read_string_or("Icon", "")));
|
||||
if (app_file.icon.is_empty())
|
||||
{
|
||||
os::println("[taskbar] app file is missing 'Icon' entry, aborting!");
|
||||
return {};
|
||||
}
|
||||
|
||||
os::println("[taskbar] loaded app %s into memory", app_file.name.chars());
|
||||
|
||||
TRY(s_app_files.try_append(move(app_file)));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> load_app_files_from_path(StringView path)
|
||||
{
|
||||
os::println("[taskbar] loading app files from %s", path.chars());
|
||||
|
||||
auto dir = TRY(os::Directory::open(path));
|
||||
|
||||
auto services = TRY(dir->list_names(os::Directory::Filter::ParentAndBase));
|
||||
sort(services.begin(), services.end(), String::compare);
|
||||
|
||||
for (const auto& entry : services) TRY(load_application_file({ dir->fd(), entry.view() }));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<int> luna_main(int, char**)
|
||||
{
|
||||
ui::App app;
|
||||
TRY(app.init("/tmp/wsys.sock"));
|
||||
app.pledge(ui::Pledge::ExtendedLayers);
|
||||
|
||||
TRY(os::EventLoop::the().register_signal_handler(SIGQUIT, sigquit_handler));
|
||||
|
||||
launcher_client = TRY(os::IPC::Client::connect("/tmp/execd.sock", false));
|
||||
|
||||
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, ui::WindowType::System));
|
||||
app.set_main_window(window);
|
||||
|
||||
window->set_background(TASKBAR_COLOR);
|
||||
window->set_layer(ui::Layer::Background);
|
||||
app.pledge(0);
|
||||
|
||||
ui::HorizontalLayout layout(ui::Margins { 0, 0, 0, 0 }, ui::AdjustHeight::Yes, ui::AdjustWidth::No);
|
||||
window->set_main_widget(layout);
|
||||
|
||||
load_app_files_from_path("/usr/share/applications/");
|
||||
|
||||
auto home = TRY(os::FileSystem::home_directory());
|
||||
|
||||
StringBuilder sb;
|
||||
TRY(sb.add(home.view()));
|
||||
TRY(sb.add("/.applications/"_sv));
|
||||
auto local_app_file_dir = TRY(sb.string());
|
||||
|
||||
load_app_files_from_path(local_app_file_dir.view());
|
||||
|
||||
for (const auto& app_file : s_app_files)
|
||||
{
|
||||
create_widget_group_for_app(layout, app_file.command.view(), app_file.icon.view());
|
||||
}
|
||||
|
||||
return app.run();
|
||||
}
|
@ -29,14 +29,6 @@ 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);
|
||||
@ -48,9 +40,11 @@ Result<void> TerminalWidget::init(char* const* args)
|
||||
m_font = ui::Font::default_font();
|
||||
m_bold_font = ui::Font::default_bold_font();
|
||||
|
||||
m_terminal_canvas = ui::App::the().main_window()->canvas();
|
||||
m_terminal_canvas = window()->canvas();
|
||||
m_terminal_canvas.fill(ui::BLACK);
|
||||
|
||||
m_cursor_timer = TRY(os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }));
|
||||
|
||||
signal(SIGCHLD, sigchld_handler);
|
||||
|
||||
int master;
|
||||
@ -64,12 +58,10 @@ Result<void> TerminalWidget::init(char* const* args)
|
||||
|
||||
m_pty = master;
|
||||
|
||||
fcntl(master, F_SETFL, O_NONBLOCK);
|
||||
os::EventLoop::the().register_fd_listener(m_pty, [this](int, int) { this->process(); });
|
||||
|
||||
m_child_pid = child;
|
||||
|
||||
m_last_cursor_tick = get_time_in_milliseconds();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -91,7 +83,7 @@ Result<void> TerminalWidget::draw(ui::Canvas&)
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<bool> TerminalWidget::process()
|
||||
Result<void> TerminalWidget::process()
|
||||
{
|
||||
char buffer[BUFSIZ];
|
||||
ssize_t nread = read(m_pty, buffer, BUFSIZ);
|
||||
@ -102,8 +94,6 @@ Result<bool> TerminalWidget::process()
|
||||
return err(errno);
|
||||
}
|
||||
|
||||
bool should_update_cursor = tick_cursor();
|
||||
|
||||
ssize_t drawn = 0;
|
||||
|
||||
for (ssize_t i = 0; i < nread; i++)
|
||||
@ -112,32 +102,22 @@ Result<bool> TerminalWidget::process()
|
||||
if (did_draw) drawn++;
|
||||
}
|
||||
|
||||
if (should_update_cursor || drawn > 0) ui::App::the().main_window()->draw();
|
||||
if (drawn > 0) window()->draw();
|
||||
|
||||
return nread == 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool TerminalWidget::tick_cursor()
|
||||
void TerminalWidget::tick_cursor()
|
||||
{
|
||||
if (!m_cursor_enabled) return false;
|
||||
if (!m_cursor_enabled) return;
|
||||
|
||||
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;
|
||||
window()->draw();
|
||||
}
|
||||
|
||||
void TerminalWidget::draw_glyph(wchar_t c, int x, int y)
|
||||
@ -471,7 +451,7 @@ bool TerminalWidget::put_code_point(wchar_t c)
|
||||
|
||||
if (should_draw_cursor)
|
||||
{
|
||||
m_current_cursor_timeout = CURSOR_TIMEOUT;
|
||||
m_cursor_timer->restart();
|
||||
m_cursor_activated = true;
|
||||
draw_cursor();
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
#include <luna/EscapeSequence.h>
|
||||
#include <luna/Utf8.h>
|
||||
#include <luna/Vector.h>
|
||||
#include <os/Timer.h>
|
||||
#include <stdio.h>
|
||||
#include <termios.h>
|
||||
#include <ui/Font.h>
|
||||
@ -16,8 +17,6 @@ class TerminalWidget : public ui::Widget
|
||||
|
||||
Result<void> draw(ui::Canvas& canvas) override;
|
||||
|
||||
Result<bool> process();
|
||||
|
||||
private:
|
||||
ui::Canvas m_terminal_canvas;
|
||||
Vector<u8> m_line_buffer;
|
||||
@ -29,9 +28,7 @@ class TerminalWidget : public ui::Widget
|
||||
SharedPtr<ui::Font> m_font;
|
||||
SharedPtr<ui::Font> m_bold_font;
|
||||
|
||||
static constexpr int CURSOR_TIMEOUT = 500;
|
||||
|
||||
int m_current_cursor_timeout = CURSOR_TIMEOUT;
|
||||
OwnedPtr<os::Timer> m_cursor_timer;
|
||||
bool m_cursor_activated = false;
|
||||
bool m_cursor_enabled = true;
|
||||
|
||||
@ -45,7 +42,7 @@ class TerminalWidget : public ui::Widget
|
||||
ui::Color m_foreground_color { ui::WHITE };
|
||||
ui::Color m_background_color { ui::BLACK };
|
||||
|
||||
bool tick_cursor();
|
||||
void tick_cursor();
|
||||
|
||||
Utf8StateDecoder m_decoder;
|
||||
Option<EscapeSequenceParser> m_escape_parser;
|
||||
@ -63,4 +60,5 @@ class TerminalWidget : public ui::Widget
|
||||
bool handle_escape_sequence(wchar_t c);
|
||||
Result<bool> putchar(char c);
|
||||
bool put_code_point(wchar_t c);
|
||||
Result<void> process();
|
||||
};
|
@ -3,10 +3,10 @@
|
||||
#include <ui/App.h>
|
||||
#include <unistd.h>
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
Result<int> luna_main(int, char**)
|
||||
{
|
||||
ui::App app;
|
||||
TRY(app.init(argc, argv));
|
||||
TRY(app.init());
|
||||
|
||||
auto* window = TRY(ui::Window::create(ui::Rect { 150, 150, 640, 400 }));
|
||||
app.set_main_window(window);
|
||||
@ -20,11 +20,5 @@ Result<int> luna_main(int argc, char** argv)
|
||||
|
||||
window->draw();
|
||||
|
||||
while (app.process_events())
|
||||
{
|
||||
bool should_sleep = TRY(terminal.process());
|
||||
if (should_sleep) usleep(10000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return app.run();
|
||||
}
|
109
gui/execd.cpp
Normal file
109
gui/execd.cpp
Normal file
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file execd.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Background process that handles detached launching of apps.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <os/ArgumentParser.h>
|
||||
#include <os/File.h>
|
||||
#include <os/IPC.h>
|
||||
#include <os/LocalServer.h>
|
||||
#include <os/Process.h>
|
||||
#include <os/Security.h>
|
||||
#include <os/ipc/Launcher.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
Result<void> handle_launch_detached_message(os::IPC::ClientConnection& client)
|
||||
{
|
||||
os::Launcher::LaunchDetachedRequest request;
|
||||
if (!TRY(client.read_message(request))) return {};
|
||||
|
||||
auto path = COPY_IPC_STRING(request.command);
|
||||
|
||||
StringView args[] = { path.view() };
|
||||
|
||||
os::Process::spawn(args[0], { args, 1 }, request.search_in_path);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void handle_ipc_message(os::IPC::ClientConnection& client, u8 id, void*)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case os::Launcher::LAUNCH_DETACHED_ID: handle_launch_detached_message(client); break;
|
||||
default: os::eprintln("execd: Invalid IPC message from client!"); return;
|
||||
}
|
||||
}
|
||||
|
||||
void sigchld_handler(int)
|
||||
{
|
||||
os::Process::wait(os::Process::ANY_CHILD, nullptr, WNOHANG);
|
||||
}
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
{
|
||||
TRY(os::Security::pledge("stdio wpath cpath unix proc exec", NULL));
|
||||
|
||||
StringView socket_path = "/tmp/execd.sock";
|
||||
|
||||
os::ArgumentParser parser;
|
||||
parser.add_description("Background process that handles detached launching of apps."_sv);
|
||||
parser.add_system_program_info("execd"_sv);
|
||||
parser.parse(argc, argv);
|
||||
|
||||
signal(SIGCHLD, sigchld_handler);
|
||||
|
||||
auto server = TRY(os::LocalServer::create(socket_path, false));
|
||||
TRY(server->listen(20));
|
||||
|
||||
// We're ready now.
|
||||
os::IPC::notify_parent();
|
||||
|
||||
Vector<OwnedPtr<os::IPC::ClientConnection>> clients;
|
||||
Vector<struct pollfd> fds;
|
||||
TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 }));
|
||||
|
||||
TRY(os::Security::pledge("stdio unix proc exec", NULL));
|
||||
|
||||
while (1)
|
||||
{
|
||||
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)
|
||||
{
|
||||
auto client = TRY(server->accept());
|
||||
os::println("execd: New client connected!");
|
||||
TRY(fds.try_append({ .fd = client.fd(), .events = POLLIN, .revents = 0 }));
|
||||
|
||||
auto connection = TRY(os::IPC::ClientConnection::adopt_connection(move(client)));
|
||||
connection->set_message_handler(handle_ipc_message, nullptr);
|
||||
TRY(clients.try_append(move(connection)));
|
||||
}
|
||||
for (usize i = 0; i < clients.size(); i++)
|
||||
{
|
||||
if (fds[i + 1].revents & POLLIN) clients[i]->check_for_messages();
|
||||
if (fds[i + 1].revents & POLLHUP)
|
||||
{
|
||||
os::println("execd: Client %zu disconnected", i);
|
||||
fds.remove_at(i + 1);
|
||||
auto client = clients.remove_at(i);
|
||||
client->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,9 @@ set(SOURCES
|
||||
src/Container.cpp
|
||||
src/Button.cpp
|
||||
src/Label.cpp
|
||||
src/InputField.cpp
|
||||
src/TextInput.cpp
|
||||
src/Dialog.cpp
|
||||
)
|
||||
|
||||
add_library(ui ${SOURCES})
|
@ -9,8 +9,9 @@
|
||||
|
||||
#pragma once
|
||||
#include <luna/HashMap.h>
|
||||
#include <luna/StringView.h>
|
||||
#include <os/EventLoop.h>
|
||||
#include <os/LocalClient.h>
|
||||
#include <os/IPC.h>
|
||||
#include <ui/Window.h>
|
||||
|
||||
namespace ui
|
||||
@ -21,12 +22,12 @@ namespace ui
|
||||
App();
|
||||
~App();
|
||||
|
||||
Result<void> init(int, char**);
|
||||
Result<void> init(StringView socket_path = "/tmp/wind.sock");
|
||||
Result<int> run();
|
||||
|
||||
Rect screen_rect();
|
||||
|
||||
os::LocalClient& client()
|
||||
os::IPC::Client& client()
|
||||
{
|
||||
return *m_client;
|
||||
}
|
||||
@ -48,23 +49,28 @@ namespace ui
|
||||
return m_main_window;
|
||||
}
|
||||
|
||||
void pledge(i16 pledges);
|
||||
|
||||
Result<void> register_window(OwnedPtr<Window>&& window, Badge<Window>);
|
||||
void unregister_window(Window* window, Badge<Window>);
|
||||
|
||||
Result<void> handle_ipc_event(u8 id);
|
||||
|
||||
bool process_events();
|
||||
|
||||
static App& the();
|
||||
|
||||
private:
|
||||
static App* s_app;
|
||||
OwnedPtr<os::LocalClient> m_client;
|
||||
OwnedPtr<os::IPC::Client> m_client;
|
||||
Window* m_main_window { nullptr };
|
||||
HashMap<int, OwnedPtr<Window>> m_windows;
|
||||
bool m_should_close { false };
|
||||
os::EventLoop m_loop;
|
||||
Vector<int> m_window_clear_queue;
|
||||
|
||||
bool process_events();
|
||||
|
||||
Window* find_window(int id);
|
||||
|
||||
Result<void> handle_ipc_event(os::IPC::Client&, u8 id, void*);
|
||||
|
||||
friend void handle_socket_event(int, int);
|
||||
};
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <os/Action.h>
|
||||
#include <luna/Action.h>
|
||||
#include <ui/Widget.h>
|
||||
|
||||
namespace ui
|
||||
@ -19,7 +19,7 @@ namespace ui
|
||||
Button(Rect rect);
|
||||
|
||||
void set_widget(Widget& widget);
|
||||
void set_action(os::Action&& action);
|
||||
void set_action(Action&& action);
|
||||
|
||||
Result<EventResult> handle_mouse_move(Point position) override;
|
||||
Result<EventResult> handle_mouse_leave() override;
|
||||
@ -32,6 +32,6 @@ namespace ui
|
||||
bool m_hovered { false };
|
||||
bool m_clicked { false };
|
||||
Widget* m_child;
|
||||
os::Action m_action;
|
||||
Action m_action;
|
||||
};
|
||||
}
|
@ -69,5 +69,13 @@ namespace ui
|
||||
* @param stride The number of pixels to skip to go to the next line.
|
||||
*/
|
||||
void fill(u32* pixels, int stride);
|
||||
|
||||
/**
|
||||
* @brief Fill the canvas with pixels, without doing any extra processing.
|
||||
*
|
||||
* @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 copy(u32* pixels, int stride);
|
||||
};
|
||||
};
|
22
gui/libui/include/ui/Dialog.h
Normal file
22
gui/libui/include/ui/Dialog.h
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @file Window.h
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief UI window dialogs.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <luna/Action.h>
|
||||
#include <ui/Window.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
namespace Dialog
|
||||
{
|
||||
Result<void> show_message(StringView title, StringView message);
|
||||
|
||||
Result<void> show_input_dialog(StringView title, StringView message, Function<StringView> callback);
|
||||
}
|
||||
}
|
42
gui/libui/include/ui/InputField.h
Normal file
42
gui/libui/include/ui/InputField.h
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file InputField.h
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Single line text input widget.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <luna/Action.h>
|
||||
#include <ui/Font.h>
|
||||
#include <ui/TextInput.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class InputField final : public ui::TextInput
|
||||
{
|
||||
public:
|
||||
InputField(SharedPtr<ui::Font> font);
|
||||
|
||||
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) override;
|
||||
|
||||
Result<void> draw(ui::Canvas& canvas) override;
|
||||
|
||||
void clear();
|
||||
|
||||
StringView data();
|
||||
|
||||
void on_submit(Function<StringView>&& action)
|
||||
{
|
||||
m_on_submit_action = move(action);
|
||||
m_has_on_submit_action = true;
|
||||
}
|
||||
|
||||
private:
|
||||
SharedPtr<ui::Font> m_font;
|
||||
|
||||
Function<StringView> m_on_submit_action;
|
||||
bool m_has_on_submit_action { false };
|
||||
};
|
||||
}
|
41
gui/libui/include/ui/TextInput.h
Normal file
41
gui/libui/include/ui/TextInput.h
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @file TextInput.h
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Base class for text inputs.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <luna/Buffer.h>
|
||||
#include <os/Timer.h>
|
||||
#include <ui/Widget.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class TextInput : public Widget
|
||||
{
|
||||
public:
|
||||
TextInput();
|
||||
|
||||
virtual Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request) = 0;
|
||||
|
||||
virtual Result<void> draw(ui::Canvas& canvas) = 0;
|
||||
|
||||
protected:
|
||||
Buffer m_data;
|
||||
|
||||
usize m_cursor { 0 };
|
||||
ui::Point m_cursor_position { 0, 0 };
|
||||
|
||||
OwnedPtr<os::Timer> m_cursor_timer;
|
||||
bool m_cursor_activated = true;
|
||||
|
||||
void tick_cursor();
|
||||
void update_cursor();
|
||||
|
||||
Result<void> delete_current_character();
|
||||
Result<void> insert_character(char c);
|
||||
};
|
||||
}
|
@ -19,6 +19,24 @@
|
||||
|
||||
namespace ui
|
||||
{
|
||||
enum class WindowType : u8
|
||||
{
|
||||
Normal,
|
||||
NotDecorated,
|
||||
System,
|
||||
};
|
||||
|
||||
struct [[gnu::packed]] Shortcut
|
||||
{
|
||||
moon::KeyCode key;
|
||||
int modifiers;
|
||||
|
||||
bool operator==(const Shortcut& other) const
|
||||
{
|
||||
return key == other.key && modifiers == other.modifiers;
|
||||
}
|
||||
};
|
||||
|
||||
class Window
|
||||
{
|
||||
public:
|
||||
@ -47,17 +65,27 @@ namespace ui
|
||||
|
||||
void close();
|
||||
|
||||
void set_layer(Layer layer);
|
||||
|
||||
Result<void> draw();
|
||||
Result<ui::EventResult> handle_mouse_leave();
|
||||
Result<ui::EventResult> handle_mouse_move(ui::Point position);
|
||||
Result<ui::EventResult> handle_mouse_buttons(ui::Point position, int buttons);
|
||||
Result<ui::EventResult> handle_key_event(const ui::KeyEventRequest& request);
|
||||
|
||||
Result<void> add_keyboard_shortcut(ui::Shortcut shortcut, bool intercept, Function<ui::Shortcut>&& action);
|
||||
|
||||
int id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
void on_close(Action&& action)
|
||||
{
|
||||
m_on_close_action = move(action);
|
||||
m_has_on_close_action = true;
|
||||
}
|
||||
|
||||
~Window();
|
||||
|
||||
private:
|
||||
@ -71,6 +99,17 @@ namespace ui
|
||||
Option<int> m_old_mouse_buttons;
|
||||
bool m_decorated { false };
|
||||
|
||||
Action m_on_close_action;
|
||||
bool m_has_on_close_action { false };
|
||||
|
||||
struct ShortcutAction
|
||||
{
|
||||
bool intercept;
|
||||
Function<Shortcut> action;
|
||||
};
|
||||
|
||||
HashMap<Shortcut, ShortcutAction> m_shortcuts;
|
||||
|
||||
Result<void> draw_titlebar();
|
||||
};
|
||||
}
|
@ -19,7 +19,6 @@ namespace ui
|
||||
{
|
||||
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,
|
||||
@ -34,13 +33,6 @@ namespace ui
|
||||
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;
|
@ -24,14 +24,9 @@ namespace ui
|
||||
INVALIDATE_ID,
|
||||
CLOSE_WINDOW_ID,
|
||||
GET_SCREEN_RECT_ID,
|
||||
SET_TITLEBAR_RECT_ID,
|
||||
};
|
||||
|
||||
enum class WindowType : u8
|
||||
{
|
||||
Normal,
|
||||
NotDecorated,
|
||||
System,
|
||||
SET_TITLEBAR_HEIGHT_ID,
|
||||
SET_WINDOW_LAYER_ID,
|
||||
UPDATE_PLEDGE_REQUEST_ID,
|
||||
};
|
||||
|
||||
struct CreateWindowRequest
|
||||
@ -40,7 +35,6 @@ namespace ui
|
||||
static constexpr u8 ID = CREATE_WINDOW_ID;
|
||||
|
||||
ui::Rect rect;
|
||||
WindowType type;
|
||||
};
|
||||
|
||||
struct RemoveSharedMemoryRequest
|
||||
@ -80,11 +74,40 @@ namespace ui
|
||||
int _shadow; // Unused.
|
||||
};
|
||||
|
||||
struct SetTitlebarRectRequest
|
||||
struct SetTitlebarHeightRequest
|
||||
{
|
||||
static constexpr u8 ID = SET_TITLEBAR_RECT_ID;
|
||||
static constexpr u8 ID = SET_TITLEBAR_HEIGHT_ID;
|
||||
|
||||
int window;
|
||||
ui::Rect titlebar_rect;
|
||||
int height;
|
||||
};
|
||||
|
||||
enum Layer : u8
|
||||
{
|
||||
Background,
|
||||
Global,
|
||||
GlobalTop,
|
||||
System,
|
||||
Lock
|
||||
};
|
||||
|
||||
struct SetWindowLayer
|
||||
{
|
||||
static constexpr u8 ID = SET_WINDOW_LAYER_ID;
|
||||
|
||||
int window;
|
||||
Layer layer;
|
||||
};
|
||||
|
||||
enum Pledge : i16
|
||||
{
|
||||
ExtendedLayers = 1,
|
||||
};
|
||||
|
||||
struct UpdatePledgeRequest
|
||||
{
|
||||
static constexpr u8 ID = UPDATE_PLEDGE_REQUEST_ID;
|
||||
|
||||
i16 pledges;
|
||||
};
|
||||
}
|
145
gui/libui/src/App.cpp
Normal file
145
gui/libui/src/App.cpp
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @file App.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief UI application event loop.
|
||||
*
|
||||
* @copyright Copyright (c) 2023, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <os/ArgumentParser.h>
|
||||
#include <os/File.h>
|
||||
#include <os/IPC.h>
|
||||
#include <ui/App.h>
|
||||
#include <ui/ipc/Client.h>
|
||||
#include <ui/ipc/Server.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
void handle_socket_event(int, int status)
|
||||
{
|
||||
if (status & POLLHUP) ui::App::the().set_should_close(true);
|
||||
if (status & POLLIN) { ui::App::the().process_events(); }
|
||||
}
|
||||
|
||||
App* App::s_app { nullptr };
|
||||
|
||||
App::App()
|
||||
{
|
||||
s_app = this;
|
||||
}
|
||||
|
||||
App::~App()
|
||||
{
|
||||
s_app = nullptr;
|
||||
}
|
||||
|
||||
Result<void> App::init(StringView socket_path)
|
||||
{
|
||||
m_client = TRY(os::IPC::Client::connect(socket_path, false));
|
||||
m_client->set_message_handler(
|
||||
[this](os::IPC::Client& client, u8 id, void* arg) { this->handle_ipc_event(client, id, arg); }, nullptr);
|
||||
|
||||
TRY(m_loop.register_fd_listener(m_client->fd(), handle_socket_event));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<int> App::run()
|
||||
{
|
||||
TRY(m_main_window->draw());
|
||||
|
||||
return m_loop.run();
|
||||
}
|
||||
|
||||
App& App::the()
|
||||
{
|
||||
check(s_app);
|
||||
return *s_app;
|
||||
}
|
||||
|
||||
Rect App::screen_rect()
|
||||
{
|
||||
ui::GetScreenRectRequest request {};
|
||||
auto response = m_client->send_sync<ui::GetScreenRectResponse>(request).release_value();
|
||||
return response.rect;
|
||||
}
|
||||
|
||||
Result<void> App::register_window(OwnedPtr<Window>&& window, Badge<Window>)
|
||||
{
|
||||
int id = window->id();
|
||||
check(TRY(m_windows.try_set(id, move(window))));
|
||||
return {};
|
||||
}
|
||||
|
||||
void App::unregister_window(Window* window, Badge<Window>)
|
||||
{
|
||||
int id = window->id();
|
||||
m_window_clear_queue.try_append(id);
|
||||
}
|
||||
|
||||
Window* App::find_window(int id)
|
||||
{
|
||||
auto* window = m_windows.try_get_ref(id);
|
||||
check(window);
|
||||
return window->ptr();
|
||||
}
|
||||
|
||||
Result<void> App::handle_ipc_event(os::IPC::Client&, u8 id, void*)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case MOUSE_EVENT_REQUEST_ID: {
|
||||
MouseEventRequest request;
|
||||
if (!TRY(m_client->read_message(request))) return {};
|
||||
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;
|
||||
if (!TRY(m_client->read_message(request))) return {};
|
||||
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;
|
||||
if (!TRY(m_client->read_message(request))) return {};
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
||||
bool App::process_events()
|
||||
{
|
||||
check(m_main_window);
|
||||
m_client->check_for_messages().release_value();
|
||||
for (int id : m_window_clear_queue)
|
||||
{
|
||||
check(m_windows.try_remove(id));
|
||||
|
||||
ui::CloseWindowRequest request;
|
||||
request.window = id;
|
||||
client().send_async(request);
|
||||
}
|
||||
m_window_clear_queue.clear_data();
|
||||
return !m_should_close;
|
||||
}
|
||||
|
||||
void App::pledge(i16 pledges)
|
||||
{
|
||||
ui::UpdatePledgeRequest request;
|
||||
request.pledges = pledges;
|
||||
client().send_async(request);
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ namespace ui
|
||||
widget.set_parent(this);
|
||||
}
|
||||
|
||||
void Button::set_action(os::Action&& action)
|
||||
void Button::set_action(Action&& action)
|
||||
{
|
||||
m_action = move(action);
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <luna/CString.h>
|
||||
#include <ui/Canvas.h>
|
||||
|
||||
namespace ui
|
||||
@ -59,4 +60,16 @@ namespace ui
|
||||
p += stride * sizeof(Color);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::copy(u32* pixels, int _stride)
|
||||
{
|
||||
u8* p = ptr;
|
||||
for (int i = 0; i < height; i++)
|
||||
{
|
||||
u32* colorp = (u32*)p;
|
||||
memcpy(colorp, pixels, width * sizeof(u32));
|
||||
pixels += _stride;
|
||||
p += stride * sizeof(Color);
|
||||
}
|
||||
}
|
||||
}
|
82
gui/libui/src/Dialog.cpp
Normal file
82
gui/libui/src/Dialog.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @file Dialog.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief UI window dialogs.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <luna/Alloc.h>
|
||||
#include <ui/App.h>
|
||||
#include <ui/Dialog.h>
|
||||
#include <ui/InputField.h>
|
||||
#include <ui/Label.h>
|
||||
#include <ui/Layout.h>
|
||||
|
||||
namespace ui::Dialog
|
||||
{
|
||||
Result<void> show_message(StringView title, StringView message)
|
||||
{
|
||||
auto rect = ui::App::the().main_window()->canvas().rect();
|
||||
int text_length = (int)message.length() * ui::Font::default_font()->width();
|
||||
int text_height = ui::Font::default_font()->height();
|
||||
|
||||
ui::Rect dialog_rect = { 0, 0, text_length + 20, text_height + 20 };
|
||||
|
||||
auto* dialog = TRY(ui::Window::create(
|
||||
ui::align(rect, dialog_rect, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center)));
|
||||
|
||||
dialog->set_background(ui::GRAY);
|
||||
dialog->set_title(title);
|
||||
|
||||
ui::Label* text = TRY(make<ui::Label>(message));
|
||||
text->set_color(ui::BLACK);
|
||||
dialog->set_main_widget(*text);
|
||||
|
||||
dialog->on_close([text] { delete text; });
|
||||
|
||||
dialog->draw();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<void> show_input_dialog(StringView title, StringView message, Function<StringView> callback)
|
||||
{
|
||||
auto rect = ui::App::the().main_window()->canvas().rect();
|
||||
int text_length = (int)message.length() * ui::Font::default_font()->width();
|
||||
int text_height = ui::Font::default_font()->height();
|
||||
|
||||
ui::Rect dialog_rect = { 0, 0, max(text_length + 20, 300), text_height * 2 + 30 };
|
||||
|
||||
auto* dialog = TRY(ui::Window::create(
|
||||
ui::align(rect, dialog_rect, ui::VerticalAlignment::Center, ui::HorizontalAlignment::Center)));
|
||||
|
||||
dialog->set_background(ui::GRAY);
|
||||
dialog->set_title(title);
|
||||
|
||||
ui::VerticalLayout* layout = TRY(make<ui::VerticalLayout>());
|
||||
dialog->set_main_widget(*layout);
|
||||
|
||||
ui::Label* text = TRY(make<ui::Label>((message)));
|
||||
text->set_color(ui::BLACK);
|
||||
layout->add_widget(*text);
|
||||
|
||||
ui::InputField* input = TRY(make<ui::InputField>(ui::Font::default_font()));
|
||||
input->on_submit([dialog, callback](StringView s) {
|
||||
callback(s);
|
||||
dialog->close();
|
||||
});
|
||||
layout->add_widget(*input);
|
||||
|
||||
dialog->on_close([layout, text, input] {
|
||||
delete text;
|
||||
delete input;
|
||||
delete layout;
|
||||
});
|
||||
|
||||
dialog->draw();
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <luna/String.h>
|
||||
#include <luna/RefString.h>
|
||||
#include <os/File.h>
|
||||
#include <ui/Font.h>
|
||||
|
||||
@ -49,7 +49,7 @@ namespace ui
|
||||
|
||||
Result<SharedPtr<Font>> Font::load_builtin(StringView name, FontWeight weight)
|
||||
{
|
||||
auto path = TRY(String::format("/usr/share/fonts/%s-%s.psf"_sv, name.chars(),
|
||||
auto path = TRY(RefString::format("/usr/share/fonts/%s-%s.psf"_sv, name.chars(),
|
||||
weight == FontWeight::Bold ? "Bold" : "Regular"));
|
||||
|
||||
return load(path.view());
|
121
gui/libui/src/InputField.cpp
Normal file
121
gui/libui/src/InputField.cpp
Normal file
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @file InputField.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Single line text input widget.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ctype.h>
|
||||
#include <luna/String.h>
|
||||
#include <luna/StringView.h>
|
||||
#include <luna/Utf8.h>
|
||||
#include <ui/InputField.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
InputField::InputField(SharedPtr<ui::Font> font) : ui::TextInput(), m_font(font)
|
||||
{
|
||||
u8 zero = 0;
|
||||
m_data.append_data(&zero, 1);
|
||||
}
|
||||
|
||||
Result<ui::EventResult> InputField::handle_key_event(const ui::KeyEventRequest& request)
|
||||
{
|
||||
// Avoid handling "key released" events
|
||||
if (!request.pressed) return ui::EventResult::DidNotHandle;
|
||||
|
||||
if (request.code == moon::K_LeftArrow)
|
||||
{
|
||||
if (m_cursor > 0) m_cursor--;
|
||||
else
|
||||
return ui::EventResult::DidNotHandle;
|
||||
update_cursor();
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.code == moon::K_RightArrow)
|
||||
{
|
||||
if (m_cursor < (m_data.size() - 1)) m_cursor++;
|
||||
else
|
||||
return ui::EventResult::DidNotHandle;
|
||||
update_cursor();
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.code == moon::K_Backspace)
|
||||
{
|
||||
if (m_cursor == 0) return ui::EventResult::DidNotHandle;
|
||||
m_cursor--;
|
||||
|
||||
delete_current_character();
|
||||
|
||||
update_cursor();
|
||||
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
if (request.letter == '\n')
|
||||
{
|
||||
if (m_has_on_submit_action)
|
||||
{
|
||||
m_on_submit_action(data());
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
return ui::EventResult::DidNotHandle;
|
||||
}
|
||||
|
||||
if (iscntrl(request.letter)) return ui::EventResult::DidNotHandle;
|
||||
|
||||
TRY(insert_character(request.letter));
|
||||
|
||||
m_cursor++;
|
||||
|
||||
update_cursor();
|
||||
|
||||
return ui::EventResult::DidHandle;
|
||||
}
|
||||
|
||||
void InputField::clear()
|
||||
{
|
||||
m_data.try_resize(0);
|
||||
u8 zero = 0;
|
||||
m_data.append_data(&zero, 1);
|
||||
m_cursor = 0;
|
||||
}
|
||||
|
||||
Result<void> InputField::draw(ui::Canvas& canvas)
|
||||
{
|
||||
int visible_characters = canvas.width / m_font->width();
|
||||
|
||||
auto string = data();
|
||||
|
||||
Utf8StringDecoder decoder(string.chars());
|
||||
wchar_t buf[4096];
|
||||
decoder.decode(buf, sizeof(buf)).release_value();
|
||||
|
||||
int characters_to_render = (int)wcslen(buf);
|
||||
|
||||
for (int j = 0; j < visible_characters && j < characters_to_render; j++)
|
||||
{
|
||||
auto subcanvas = canvas.subcanvas({ j * m_font->width(), 0, m_font->width(), m_font->height() });
|
||||
m_font->render(buf[j], ui::WHITE, subcanvas);
|
||||
}
|
||||
|
||||
// Draw the cursor
|
||||
if ((int)m_cursor < visible_characters && m_cursor_activated)
|
||||
{
|
||||
canvas.subcanvas({ (int)m_cursor * m_font->width(), 0, 1, m_font->height() }).fill(ui::WHITE);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
StringView InputField::data()
|
||||
{
|
||||
if (m_data.size() < 2) return StringView {};
|
||||
return StringView { (const char*)m_data.data(), m_data.size() - 1 };
|
||||
}
|
||||
}
|
52
gui/libui/src/TextInput.cpp
Normal file
52
gui/libui/src/TextInput.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @file TextInput.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Base class for text inputs.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ui/App.h>
|
||||
#include <ui/TextInput.h>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
TextInput::TextInput() : Widget()
|
||||
{
|
||||
m_cursor_timer = os::Timer::create_repeating(500, [this]() { this->tick_cursor(); }).release_value();
|
||||
}
|
||||
|
||||
void TextInput::update_cursor()
|
||||
{
|
||||
m_cursor_timer->restart();
|
||||
m_cursor_activated = true;
|
||||
}
|
||||
|
||||
Result<void> TextInput::delete_current_character()
|
||||
{
|
||||
usize size = m_data.size() - m_cursor;
|
||||
u8* slice = TRY(m_data.slice(m_cursor, size));
|
||||
memmove(slice, slice + 1, size - 1);
|
||||
TRY(m_data.try_resize(m_data.size() - 1));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<void> TextInput::insert_character(char c)
|
||||
{
|
||||
usize size = m_data.size() - m_cursor;
|
||||
u8* slice = TRY(m_data.slice(m_cursor, size + 1));
|
||||
memmove(slice + 1, slice, size);
|
||||
*slice = (u8)c;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void TextInput::tick_cursor()
|
||||
{
|
||||
m_cursor_activated = !m_cursor_activated;
|
||||
|
||||
window()->draw();
|
||||
}
|
||||
}
|
@ -43,7 +43,7 @@ namespace ui
|
||||
|
||||
ui::CreateWindowRequest request;
|
||||
request.rect = rect;
|
||||
auto response = TRY(os::IPC::send_sync<ui::CreateWindowResponse>(App::the().client(), request));
|
||||
auto response = TRY(App::the().client().send_sync<ui::CreateWindowResponse>(request));
|
||||
|
||||
auto path = COPY_IPC_STRING(response.shm_path);
|
||||
|
||||
@ -59,10 +59,10 @@ namespace ui
|
||||
window->m_titlebar_canvas = canvas.subcanvas(ui::Rect { 0, 0, canvas.width, height });
|
||||
window->m_window_canvas = canvas.subcanvas(ui::Rect { 0, height, canvas.width, canvas.height - height });
|
||||
|
||||
ui::SetTitlebarRectRequest titlebar_request;
|
||||
titlebar_request.titlebar_rect = window->m_titlebar_canvas.rect();
|
||||
ui::SetTitlebarHeightRequest titlebar_request;
|
||||
titlebar_request.height = height;
|
||||
titlebar_request.window = response.window;
|
||||
os::IPC::send_async(App::the().client(), titlebar_request);
|
||||
App::the().client().send_async(titlebar_request);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -74,7 +74,7 @@ namespace ui
|
||||
|
||||
ui::RemoveSharedMemoryRequest shm_request;
|
||||
shm_request.window = response.window;
|
||||
os::IPC::send_async(App::the().client(), shm_request);
|
||||
App::the().client().send_async(shm_request);
|
||||
|
||||
App::the().register_window(move(window), {});
|
||||
|
||||
@ -84,6 +84,8 @@ namespace ui
|
||||
Window::~Window()
|
||||
{
|
||||
if (m_canvas.ptr) munmap(m_canvas.ptr, ((usize)m_canvas.width) * ((usize)m_canvas.height) * 4);
|
||||
|
||||
if (m_has_on_close_action) m_on_close_action();
|
||||
}
|
||||
|
||||
void Window::set_title(StringView title)
|
||||
@ -91,7 +93,7 @@ namespace ui
|
||||
ui::SetWindowTitleRequest request;
|
||||
request.window = m_id;
|
||||
SET_IPC_STRING(request.title, title.chars());
|
||||
os::IPC::send_async(App::the().client(), request);
|
||||
App::the().client().send_async(request);
|
||||
|
||||
m_name = String::from_string_view(title).release_value();
|
||||
draw();
|
||||
@ -101,22 +103,26 @@ namespace ui
|
||||
{
|
||||
ui::InvalidateRequest request;
|
||||
request.window = m_id;
|
||||
os::IPC::send_async(App::the().client(), request);
|
||||
App::the().client().send_async(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, {});
|
||||
}
|
||||
|
||||
void Window::set_layer(Layer layer)
|
||||
{
|
||||
ui::SetWindowLayer request;
|
||||
request.window = m_id;
|
||||
request.layer = layer;
|
||||
App::the().client().send_async(request);
|
||||
}
|
||||
|
||||
Result<void> Window::draw()
|
||||
{
|
||||
if (m_background.has_value()) m_window_canvas.fill(*m_background);
|
||||
@ -204,7 +210,24 @@ namespace ui
|
||||
|
||||
Result<ui::EventResult> Window::handle_key_event(const ui::KeyEventRequest& request)
|
||||
{
|
||||
if (request.pressed)
|
||||
{
|
||||
auto* shortcut = m_shortcuts.try_get_ref({ request.code, request.modifiers });
|
||||
if (shortcut)
|
||||
{
|
||||
shortcut->action({ request.code, request.modifiers });
|
||||
if (shortcut->intercept) return ui::EventResult::DidHandle;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_main_widget) return ui::EventResult::DidNotHandle;
|
||||
return m_main_widget->handle_key_event(request);
|
||||
}
|
||||
|
||||
Result<void> Window::add_keyboard_shortcut(ui::Shortcut shortcut, bool intercept, Function<ui::Shortcut>&& action)
|
||||
{
|
||||
TRY(m_shortcuts.try_set(shortcut, { intercept, move(action) }));
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
186
gui/loginui.cpp
Normal file
186
gui/loginui.cpp
Normal file
@ -0,0 +1,186 @@
|
||||
/**
|
||||
* @file loginui.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Graphical login prompt.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <luna/RefString.h>
|
||||
#include <luna/SHA.h>
|
||||
#include <os/ArgumentParser.h>
|
||||
#include <os/Config.h>
|
||||
#include <os/File.h>
|
||||
#include <os/FileSystem.h>
|
||||
#include <os/IPC.h>
|
||||
#include <os/Process.h>
|
||||
#include <os/Security.h>
|
||||
#include <pwd.h>
|
||||
#include <shadow.h>
|
||||
#include <sys/stat.h>
|
||||
#include <ui/App.h>
|
||||
#include <ui/Button.h>
|
||||
#include <ui/InputField.h>
|
||||
#include <ui/Label.h>
|
||||
#include <ui/Layout.h>
|
||||
#include <unistd.h>
|
||||
|
||||
enum Stage
|
||||
{
|
||||
UsernameInput,
|
||||
PasswordInput,
|
||||
};
|
||||
|
||||
static constexpr ui::Color BACKGROUND_COLOR = ui::Color::from_rgb(89, 89, 89);
|
||||
|
||||
Result<String> hash_password(StringView& view)
|
||||
{
|
||||
SHA256 sha;
|
||||
sha.append((const u8*)view.chars(), view.length());
|
||||
auto digest = TRY(sha.digest());
|
||||
return digest.to_string();
|
||||
}
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
{
|
||||
os::ArgumentParser parser;
|
||||
parser.add_description("Login prompt for a graphical UI session.");
|
||||
parser.add_system_program_info("loginui"_sv);
|
||||
parser.parse(argc, argv);
|
||||
|
||||
if (geteuid() != 0)
|
||||
{
|
||||
os::eprintln("error: %s can only be started as root.", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
TRY(os::Security::pledge("stdio rpath wpath unix proc exec id", nullptr));
|
||||
|
||||
setsid();
|
||||
|
||||
bool success = os::IPC::Notifier::run_and_wait(
|
||||
[&] {
|
||||
StringView wind_command[] = { "/usr/bin/wind" };
|
||||
os::Process::spawn(wind_command[0], Slice<StringView>(wind_command, 1));
|
||||
},
|
||||
1000);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
os::eprintln("loginui: failed to start wind, timed out");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto config = TRY(os::ConfigFile::open("/etc/loginui.conf"));
|
||||
|
||||
if (config->read_boolean_or("Autologin", false))
|
||||
{
|
||||
StringView username = config->read_string_or("AutologinUser", "");
|
||||
|
||||
if (!username.is_empty())
|
||||
{
|
||||
auto flag = RefString::format("--user=%s"_sv, username.chars()).release_value();
|
||||
|
||||
StringView startui_command[] = { "/usr/bin/startui", flag.view() };
|
||||
os::Process::exec(startui_command[0], Slice<StringView>(startui_command, 2));
|
||||
unreachable();
|
||||
}
|
||||
}
|
||||
|
||||
ui::App app;
|
||||
TRY(app.init());
|
||||
|
||||
auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 300 }));
|
||||
app.set_main_window(window);
|
||||
|
||||
window->set_title("Log in");
|
||||
window->set_background(BACKGROUND_COLOR);
|
||||
|
||||
ui::VerticalLayout main_layout;
|
||||
window->set_main_widget(main_layout);
|
||||
|
||||
ui::Label label("Username:");
|
||||
main_layout.add_widget(label);
|
||||
|
||||
ui::InputField input(ui::Font::default_font());
|
||||
main_layout.add_widget(input);
|
||||
|
||||
ui::Label error("");
|
||||
error.set_font(ui::Font::default_bold_font());
|
||||
error.set_color(ui::RED);
|
||||
main_layout.add_widget(error);
|
||||
|
||||
Stage stage = Stage::UsernameInput;
|
||||
struct passwd* pw;
|
||||
|
||||
input.on_submit([&](StringView data) {
|
||||
error.set_text("");
|
||||
if (stage == Stage::UsernameInput)
|
||||
{
|
||||
struct passwd* entry = getpwnam(data.chars());
|
||||
if (!entry)
|
||||
{
|
||||
error.set_text("User not found.");
|
||||
input.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
pw = entry;
|
||||
stage = Stage::PasswordInput;
|
||||
label.set_text("Password:");
|
||||
|
||||
RefString title = RefString::format("Log in: %s"_sv, data.chars()).release_value();
|
||||
window->set_title(title.view());
|
||||
|
||||
input.clear();
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* passwd = pw->pw_passwd;
|
||||
|
||||
// If the user's password entry is 'x', read their password from the shadow file instead.
|
||||
if (!strcmp(pw->pw_passwd, "x"))
|
||||
{
|
||||
struct spwd* sp = getspnam(pw->pw_name);
|
||||
|
||||
if (!sp)
|
||||
{
|
||||
error.set_text("User not found in shadow file.");
|
||||
input.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
endspent();
|
||||
|
||||
passwd = sp->sp_pwdp;
|
||||
}
|
||||
|
||||
if (!strcmp(passwd, "!"))
|
||||
{
|
||||
error.set_text("User's password is disabled.");
|
||||
input.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = hash_password(data).release_value();
|
||||
|
||||
if (strcmp(result.chars(), passwd))
|
||||
{
|
||||
error.set_text("Incorrect password.");
|
||||
input.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
auto flag = RefString::format("--user=%s"_sv, pw->pw_name).release_value();
|
||||
|
||||
StringView startui_command[] = { "/usr/bin/startui", flag.view() };
|
||||
os::Process::exec(startui_command[0], Slice<StringView>(startui_command, 2));
|
||||
unreachable();
|
||||
}
|
||||
});
|
||||
|
||||
return app.run();
|
||||
}
|
35
gui/run.cpp
Normal file
35
gui/run.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @file run.cpp
|
||||
* @author apio (cloudapio.eu)
|
||||
* @brief Tiny command-line utility to start a detached program in the current GUI session.
|
||||
*
|
||||
* @copyright Copyright (c) 2024, the Luna authors.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <os/ArgumentParser.h>
|
||||
#include <os/File.h>
|
||||
#include <os/LocalClient.h>
|
||||
#include <os/ipc/Launcher.h>
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
{
|
||||
StringView program;
|
||||
|
||||
os::ArgumentParser parser;
|
||||
parser.add_description("Start a detached program in the current GUI session."_sv);
|
||||
parser.add_system_program_info("run"_sv);
|
||||
parser.add_positional_argument(program, "program", true);
|
||||
parser.parse(argc, argv);
|
||||
|
||||
OwnedPtr<os::IPC::Client> launcher_client = TRY(os::IPC::Client::connect("/tmp/execd.sock", false));
|
||||
|
||||
os::println("Requesting to start program '%s'...", program.chars());
|
||||
|
||||
os::Launcher::LaunchDetachedRequest request;
|
||||
SET_IPC_STRING(request.command, program.chars());
|
||||
request.search_in_path = true;
|
||||
launcher_client->send_async(request);
|
||||
|
||||
return 0;
|
||||
}
|
@ -11,6 +11,9 @@ set(SOURCES
|
||||
Keyboard.cpp
|
||||
Keyboard.h
|
||||
Client.h
|
||||
Client.cpp
|
||||
Layer.cpp
|
||||
Layer.h
|
||||
)
|
||||
|
||||
add_executable(wind ${SOURCES})
|
52
gui/wind/Client.cpp
Normal file
52
gui/wind/Client.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include "Client.h"
|
||||
#include <os/File.h>
|
||||
|
||||
Client::Client(OwnedPtr<os::IPC::ClientConnection>&& client, i16 _pledges)
|
||||
: conn(move(client)), windows(), pledges(_pledges)
|
||||
{
|
||||
conn->set_message_handler(wind::handle_ipc_message, this);
|
||||
}
|
||||
|
||||
bool Client::update_pledges(i16 _pledges)
|
||||
{
|
||||
if (_pledges < 0)
|
||||
{
|
||||
os::eprintln("wind: Client trying to set an invalid pledge, disconnecting!");
|
||||
should_be_disconnected = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pledges < 0)
|
||||
{
|
||||
pledges = _pledges;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_pledges & ~pledges)
|
||||
{
|
||||
os::eprintln("wind: Client trying to add pledges, disconnecting!");
|
||||
should_be_disconnected = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
pledges = _pledges;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Client::check_pledge(i16 pledge)
|
||||
{
|
||||
check(pledge > 0);
|
||||
|
||||
if (pledges < 0)
|
||||
{
|
||||
os::eprintln("wind: Client trying to use pledge-protected functions before pledging anything, disconnecting!");
|
||||
should_be_disconnected = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((pledges & pledge) == pledge) return true;
|
||||
|
||||
os::eprintln("wind: Client trying to use a function they haven't pledged, disconnecting!");
|
||||
should_be_disconnected = true;
|
||||
return false;
|
||||
}
|
21
gui/wind/Client.h
Normal file
21
gui/wind/Client.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "IPC.h"
|
||||
#include "Window.h"
|
||||
#include <os/IPC.h>
|
||||
|
||||
constexpr i16 HAS_NOT_YET_PLEDGED = -1;
|
||||
constexpr i16 EMPTY_PLEDGE = 0;
|
||||
|
||||
struct Client
|
||||
{
|
||||
OwnedPtr<os::IPC::ClientConnection> conn;
|
||||
Vector<Window*> windows;
|
||||
const bool privileged { false };
|
||||
bool should_be_disconnected { false };
|
||||
i16 pledges = 0;
|
||||
|
||||
bool update_pledges(i16 pledges);
|
||||
bool check_pledge(i16 pledge);
|
||||
|
||||
Client(OwnedPtr<os::IPC::ClientConnection>&& client, i16 pledges);
|
||||
};
|
229
gui/wind/IPC.cpp
Normal file
229
gui/wind/IPC.cpp
Normal file
@ -0,0 +1,229 @@
|
||||
#include "IPC.h"
|
||||
#include "Layer.h"
|
||||
#include "Mouse.h"
|
||||
#include "Screen.h"
|
||||
#include <luna/Alignment.h>
|
||||
#include <luna/RefString.h>
|
||||
#include <os/File.h>
|
||||
#include <os/SharedMemory.h>
|
||||
#include <sys/mman.h>
|
||||
#include <time.h>
|
||||
|
||||
#define TRY_OR_IPC_ERROR(expr) \
|
||||
({ \
|
||||
auto _expr_rc = (expr); \
|
||||
if (!_expr_rc.has_value()) \
|
||||
{ \
|
||||
client.conn->send_error(_expr_rc.error()); \
|
||||
return {}; \
|
||||
} \
|
||||
_expr_rc.release_value(); \
|
||||
})
|
||||
|
||||
#define CHECK_WINDOW_ID(request, context) \
|
||||
do { \
|
||||
if ((usize)request.window >= client.windows.size() || !client.windows[request.window]) \
|
||||
{ \
|
||||
os::eprintln("wind: Window id is invalid! (%s)", context); \
|
||||
return {}; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static Result<void> handle_create_window_message(Client& client)
|
||||
{
|
||||
ui::CreateWindowRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
request.rect = request.rect.normalized();
|
||||
|
||||
auto name = TRY_OR_IPC_ERROR(RefString::from_cstring("Window"));
|
||||
|
||||
auto shm_path = TRY_OR_IPC_ERROR(RefString::format("/wind-shm-%d-%lu"_sv, client.conn->fd(), time(NULL)));
|
||||
|
||||
auto* window = new (std::nothrow) Window(request.rect, move(name));
|
||||
if (!window)
|
||||
{
|
||||
client.conn->send_error(ENOMEM);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto guard = make_scope_guard([window] {
|
||||
window->layer->windows.remove(window);
|
||||
delete window;
|
||||
});
|
||||
|
||||
window->pixels = (u32*)TRY_OR_IPC_ERROR(
|
||||
os::SharedMemory::create(shm_path.view(), window->surface.height * window->surface.width * 4));
|
||||
|
||||
TRY_OR_IPC_ERROR(client.windows.try_append(window));
|
||||
int id = static_cast<int>(client.windows.size() - 1);
|
||||
|
||||
// No more fallible operations, this operation is guaranteed to succeed now.
|
||||
guard.deactivate();
|
||||
|
||||
window->client = &client;
|
||||
window->id = id;
|
||||
|
||||
ui::CreateWindowResponse response;
|
||||
response.window = id;
|
||||
SET_IPC_STRING(response.shm_path, shm_path.chars());
|
||||
window->shm_path = move(shm_path);
|
||||
client.conn->send_async(response);
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_remove_shm_message(Client& client)
|
||||
{
|
||||
ui::RemoveSharedMemoryRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
CHECK_WINDOW_ID(request, "RemoveShm");
|
||||
|
||||
shm_unlink(client.windows[request.window]->shm_path.chars());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_set_window_title_message(Client& client)
|
||||
{
|
||||
ui::SetWindowTitleRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
auto name = TRY(RefString::from_string(COPY_IPC_STRING(request.title)));
|
||||
|
||||
os::println("wind: SetWindowTitle(\"%s\") for window %d", name.chars(), request.window);
|
||||
|
||||
CHECK_WINDOW_ID(request, "SetWindowTitle");
|
||||
|
||||
client.windows[request.window]->name = move(name);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_invalidate_message(Client& client)
|
||||
{
|
||||
ui::InvalidateRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
CHECK_WINDOW_ID(request, "Invalidate");
|
||||
|
||||
client.windows[request.window]->dirty = true;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_close_window_message(Client& client)
|
||||
{
|
||||
ui::CloseWindowRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
CHECK_WINDOW_ID(request, "CloseWindow");
|
||||
|
||||
auto* window = client.windows[request.window];
|
||||
client.windows[request.window] = nullptr;
|
||||
window->layer->windows.remove(window);
|
||||
Mouse::the().window_did_close(window);
|
||||
delete window;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_get_screen_rect_message(Client& client)
|
||||
{
|
||||
ui::GetScreenRectRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {}; // Kinda pointless, but required.
|
||||
|
||||
ui::GetScreenRectResponse response;
|
||||
response.rect = Screen::the().canvas().rect();
|
||||
client.conn->send_async(response);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_set_titlebar_height_message(Client& client)
|
||||
{
|
||||
ui::SetTitlebarHeightRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
if (request.height < 0) request.height = 0;
|
||||
|
||||
CHECK_WINDOW_ID(request, "SetTitlebarHeight");
|
||||
|
||||
auto* window = client.windows[request.window];
|
||||
|
||||
if (request.height > window->surface.height)
|
||||
{
|
||||
os::eprintln("wind: SetTitlebarHeight: titlebar height bigger than window!");
|
||||
return {};
|
||||
}
|
||||
|
||||
window->titlebar = ui::Rect { 0, 0, window->surface.width, request.height };
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_set_window_layer_message(Client& client)
|
||||
{
|
||||
ui::SetWindowLayer request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
if (request.layer != ui::Layer::Global && request.layer != ui::Layer::GlobalTop)
|
||||
{
|
||||
if (!client.check_pledge(ui::Pledge::ExtendedLayers)) return {};
|
||||
}
|
||||
|
||||
CHECK_WINDOW_ID(request, "SetWindowLayer");
|
||||
|
||||
auto* window = client.windows[request.window];
|
||||
|
||||
window->layer->windows.remove(window);
|
||||
|
||||
switch (request.layer)
|
||||
{
|
||||
case ui::Layer::Background: window->layer = &l_background; break;
|
||||
case ui::Layer::Global: window->layer = &l_global; break;
|
||||
case ui::Layer::GlobalTop: window->layer = &l_global_top; break;
|
||||
case ui::Layer::System: window->layer = &l_system; break;
|
||||
case ui::Layer::Lock: window->layer = &l_lock; break;
|
||||
default: {
|
||||
window->layer->windows.append(window);
|
||||
os::eprintln("wind: Client trying to set window layer to an invalid layer, disconnecting!");
|
||||
client.should_be_disconnected = true;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
window->layer->windows.append(window);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static Result<void> handle_update_pledge_request_message(Client& client)
|
||||
{
|
||||
ui::UpdatePledgeRequest request;
|
||||
if (!TRY(client.conn->read_message(request))) return {};
|
||||
|
||||
client.update_pledges(request.pledges); // update_pledges does all the checking.
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
namespace wind
|
||||
{
|
||||
void handle_ipc_message(os::IPC::ClientConnection&, u8 id, void* c)
|
||||
{
|
||||
Client& client = *(Client*)c;
|
||||
switch (id)
|
||||
{
|
||||
case ui::CREATE_WINDOW_ID: handle_create_window_message(client); break;
|
||||
case ui::REMOVE_SHM_ID: handle_remove_shm_message(client); break;
|
||||
case ui::SET_WINDOW_TITLE_ID: handle_set_window_title_message(client); break;
|
||||
case ui::INVALIDATE_ID: handle_invalidate_message(client); break;
|
||||
case ui::CLOSE_WINDOW_ID: handle_close_window_message(client); break;
|
||||
case ui::GET_SCREEN_RECT_ID: handle_get_screen_rect_message(client); break;
|
||||
case ui::SET_TITLEBAR_HEIGHT_ID: handle_set_titlebar_height_message(client); break;
|
||||
case ui::SET_WINDOW_LAYER_ID: handle_set_window_layer_message(client); break;
|
||||
case ui::UPDATE_PLEDGE_REQUEST_ID: handle_update_pledge_request_message(client); break;
|
||||
default: os::eprintln("wind: Invalid IPC message from client!"); return;
|
||||
}
|
||||
}
|
||||
}
|
8
gui/wind/IPC.h
Normal file
8
gui/wind/IPC.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include "Client.h"
|
||||
#include <ui/ipc/Server.h>
|
||||
|
||||
namespace wind
|
||||
{
|
||||
void handle_ipc_message(os::IPC::ClientConnection& connection, u8 id, void* c);
|
||||
}
|
79
gui/wind/Layer.cpp
Normal file
79
gui/wind/Layer.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
#include "Layer.h"
|
||||
#include "Client.h"
|
||||
#include "Window.h"
|
||||
|
||||
Layer l_background;
|
||||
Layer l_global;
|
||||
Layer l_global_top;
|
||||
Layer l_system;
|
||||
Layer l_lock;
|
||||
|
||||
constexpr int NUM_LAYERS = 5;
|
||||
|
||||
static Layer* const layers_front_to_back[NUM_LAYERS] = { &l_lock, &l_system, &l_global_top, &l_global, &l_background };
|
||||
static Layer* const layers_back_to_front[NUM_LAYERS] = { &l_background, &l_global, &l_global_top, &l_system, &l_lock };
|
||||
|
||||
Window* Layer::focused_window()
|
||||
{
|
||||
for (int i = 0; i < NUM_LAYERS; i++)
|
||||
{
|
||||
Layer* l = layers_front_to_back[i];
|
||||
if (l->windows.last().has_value()) return l->windows.last().value();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Layer::draw_all_windows(ui::Canvas& canvas)
|
||||
{
|
||||
for (int i = 0; i < NUM_LAYERS; i++)
|
||||
{
|
||||
Layer* l = layers_back_to_front[i];
|
||||
for (Window* w : l->windows) { w->draw(canvas); }
|
||||
}
|
||||
}
|
||||
|
||||
Window* Layer::propagate_mouse_event(ui::Point position, u8 buttons)
|
||||
{
|
||||
for (int i = 0; i < NUM_LAYERS; i++)
|
||||
{
|
||||
Layer* l = layers_front_to_back[i];
|
||||
for (Window* window = l->windows.last().value_or(nullptr); window;
|
||||
window = l->windows.previous(window).value_or(nullptr))
|
||||
{
|
||||
if (window->surface.contains(position))
|
||||
{
|
||||
ui::MouseEventRequest request;
|
||||
request.window = window->id;
|
||||
request.position = window->surface.relative(position);
|
||||
request.buttons = buttons;
|
||||
window->client->conn->send_async(request);
|
||||
return window;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Window* Layer::propagate_drag_event(ui::Point position)
|
||||
{
|
||||
for (int i = 0; i < NUM_LAYERS; i++)
|
||||
{
|
||||
Layer* l = layers_front_to_back[i];
|
||||
for (Window* window = l->windows.last().value_or(nullptr); window;
|
||||
window = l->windows.previous(window).value_or(nullptr))
|
||||
{
|
||||
if (window->surface.contains(position))
|
||||
{
|
||||
window->focus();
|
||||
|
||||
if (window->surface.absolute(window->titlebar).contains(position)) return window;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
20
gui/wind/Layer.h
Normal file
20
gui/wind/Layer.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include "Window.h"
|
||||
#include <luna/LinkedList.h>
|
||||
#include <ui/Canvas.h>
|
||||
|
||||
struct Layer
|
||||
{
|
||||
LinkedList<Window> windows;
|
||||
|
||||
static Window* focused_window();
|
||||
static void draw_all_windows(ui::Canvas& canvas);
|
||||
static Window* propagate_mouse_event(ui::Point position, u8 buttons);
|
||||
static Window* propagate_drag_event(ui::Point position);
|
||||
};
|
||||
|
||||
extern Layer l_background;
|
||||
extern Layer l_global;
|
||||
extern Layer l_global_top;
|
||||
extern Layer l_system;
|
||||
extern Layer l_lock;
|
@ -1,5 +1,6 @@
|
||||
#include "Mouse.h"
|
||||
#include "Client.h"
|
||||
#include "Layer.h"
|
||||
#include <os/File.h>
|
||||
#include <os/IPC.h>
|
||||
#include <ui/Image.h>
|
||||
@ -57,45 +58,17 @@ void Mouse::update(const moon::MousePacket& packet)
|
||||
|
||||
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.contains(m_position))
|
||||
{
|
||||
window->focus();
|
||||
|
||||
if (window->surface.absolute(window->titlebar).contains(m_position))
|
||||
if (auto* window = Layer::propagate_drag_event(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);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
window->surface.pos.y, window->surface.width, window->surface.height, m_initial_drag_position.x,
|
||||
m_initial_drag_position.y);
|
||||
}
|
||||
}
|
||||
|
||||
Window* new_active_window = nullptr;
|
||||
|
||||
for (Window* window = g_windows.last().value_or(nullptr); window;
|
||||
window = g_windows.previous(window).value_or(nullptr))
|
||||
{
|
||||
if (window->surface.contains(m_position))
|
||||
{
|
||||
ui::MouseEventRequest request;
|
||||
request.window = window->id;
|
||||
request.position = window->surface.relative(m_position);
|
||||
request.buttons = packet.buttons;
|
||||
os::IPC::send_async(window->client->conn, request);
|
||||
new_active_window = window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Window* new_active_window = Layer::propagate_mouse_event(m_position, packet.buttons);
|
||||
|
||||
if (m_active_window != new_active_window)
|
||||
{
|
||||
@ -103,7 +76,7 @@ void Mouse::update(const moon::MousePacket& packet)
|
||||
{
|
||||
ui::MouseLeaveRequest request;
|
||||
request.window = m_active_window->id;
|
||||
os::IPC::send_async(m_active_window->client->conn, request);
|
||||
m_active_window->client->conn->send_async(request);
|
||||
}
|
||||
m_active_window = new_active_window;
|
||||
}
|
@ -1,32 +1,32 @@
|
||||
#include "Window.h"
|
||||
#include "Layer.h"
|
||||
#include <luna/Utf8.h>
|
||||
#include <os/File.h>
|
||||
#include <sys/mman.h>
|
||||
#include <ui/Font.h>
|
||||
#include <ui/Image.h>
|
||||
|
||||
LinkedList<Window> g_windows;
|
||||
|
||||
void Window::draw(ui::Canvas& screen)
|
||||
{
|
||||
dirty = false;
|
||||
|
||||
auto window = screen.subcanvas(surface);
|
||||
window.fill(pixels, surface.width);
|
||||
window.copy(pixels, surface.width);
|
||||
}
|
||||
|
||||
void Window::focus()
|
||||
{
|
||||
// Bring the window to the front of the list.
|
||||
g_windows.remove(this);
|
||||
g_windows.append(this);
|
||||
layer->windows.remove(this);
|
||||
layer->windows.append(this);
|
||||
}
|
||||
|
||||
Window::Window(ui::Rect r, String&& n) : surface(r), name(move(n))
|
||||
Window::Window(ui::Rect r, RefString&& n) : surface(r), name(move(n))
|
||||
{
|
||||
auto font = ui::Font::default_font();
|
||||
titlebar = ui::Rect { 0, 0, 0, 0 };
|
||||
g_windows.append(this);
|
||||
l_global.windows.append(this);
|
||||
layer = &l_global;
|
||||
}
|
||||
|
||||
Window::~Window()
|
@ -1,30 +1,30 @@
|
||||
#pragma once
|
||||
#include <luna/LinkedList.h>
|
||||
#include <luna/String.h>
|
||||
#include <luna/RefString.h>
|
||||
#include <ui/Canvas.h>
|
||||
#include <ui/Color.h>
|
||||
#include <ui/Rect.h>
|
||||
#include <ui/ipc/Server.h>
|
||||
|
||||
struct Client;
|
||||
struct Layer;
|
||||
|
||||
struct Window : public LinkedListNode<Window>
|
||||
{
|
||||
ui::Rect surface;
|
||||
ui::Rect titlebar;
|
||||
u32* pixels;
|
||||
String name;
|
||||
String shm_path;
|
||||
RefString name;
|
||||
RefString shm_path;
|
||||
bool dirty { false };
|
||||
Client* client;
|
||||
Layer* layer;
|
||||
int id;
|
||||
|
||||
Window(ui::Rect, String&&);
|
||||
Window(ui::Rect, RefString&&);
|
||||
~Window();
|
||||
|
||||
void focus();
|
||||
|
||||
void draw(ui::Canvas& screen);
|
||||
};
|
||||
|
||||
extern LinkedList<Window> g_windows;
|
@ -1,103 +1,49 @@
|
||||
#define CLIENT_IMPLEMENTATION
|
||||
#include "Client.h"
|
||||
#include "IPC.h"
|
||||
#include "Keyboard.h"
|
||||
#include "Layer.h"
|
||||
#include "Mouse.h"
|
||||
#include "Screen.h"
|
||||
#include "Window.h"
|
||||
#include <errno.h>
|
||||
#include <grp.h>
|
||||
#include <moon/Keyboard.h>
|
||||
#include <os/ArgumentParser.h>
|
||||
#include <os/File.h>
|
||||
#include <os/IPC.h>
|
||||
#include <os/LocalServer.h>
|
||||
#include <os/Process.h>
|
||||
#include <os/Security.h>
|
||||
#include <pwd.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void debug(const Vector<OwnedPtr<Client>>& 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, %sdirty (\"%s\") (%d,%d,%d,%d)", window->client->conn.fd(),
|
||||
window->id, 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<void> set_supplementary_groups(const char* name)
|
||||
{
|
||||
Vector<gid_t> extra_groups;
|
||||
|
||||
setgrent();
|
||||
group* grp;
|
||||
while ((grp = getgrent()))
|
||||
{
|
||||
for (char** user = grp->gr_mem; *user; user++)
|
||||
{
|
||||
if (!strcmp(*user, name))
|
||||
{
|
||||
os::println("Adding supplementary group: %d", grp->gr_gid);
|
||||
TRY(extra_groups.try_append(grp->gr_gid));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
endgrent();
|
||||
|
||||
if (setgroups(static_cast<int>(extra_groups.size()), extra_groups.data()) < 0) return err(errno);
|
||||
|
||||
return {};
|
||||
}
|
||||
static constexpr uid_t WIND_USER_ID = 2;
|
||||
static constexpr gid_t WIND_GROUP_ID = 2;
|
||||
static constexpr gid_t WSYS_GROUP_ID = 3;
|
||||
|
||||
Result<int> luna_main(int argc, char** argv)
|
||||
{
|
||||
srand((unsigned)time(NULL));
|
||||
|
||||
TRY(os::Security::pledge("stdio rpath wpath cpath unix proc exec tty id", NULL));
|
||||
TRY(os::Security::pledge("stdio rpath wpath cpath unix tty id", NULL));
|
||||
|
||||
StringView socket_path = "/tmp/wind.sock";
|
||||
StringView user;
|
||||
StringView system_socket_path = "/tmp/wsys.sock";
|
||||
|
||||
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.add_value_argument(system_socket_path, ' ', "system-socket"_sv,
|
||||
"the path for the system IPC socket, for privileged clients"_sv);
|
||||
parser.parse(argc, argv);
|
||||
|
||||
if (geteuid() != 0)
|
||||
{
|
||||
os::eprintln("error: wind must be run as root to initialize resources, run with --user=<USERNAME> to drop "
|
||||
"privileges afterwards");
|
||||
os::eprintln("error: wind must be run as root to initialize resources, the server will drop "
|
||||
"privileges automatically afterwards");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -114,6 +60,24 @@ Result<int> luna_main(int argc, char** argv)
|
||||
|
||||
Mouse mouse_pointer { screen.canvas() };
|
||||
|
||||
umask(0002);
|
||||
|
||||
// Set permissions to wind:wsys temporarily, to create /tmp/wsys.sock with those privileges.
|
||||
setegid(WSYS_GROUP_ID);
|
||||
seteuid(WIND_USER_ID);
|
||||
|
||||
auto system_server = TRY(os::LocalServer::create(system_socket_path, false));
|
||||
TRY(system_server->listen(20));
|
||||
|
||||
seteuid(0);
|
||||
|
||||
// Opened all necessary files as root, drop privileges now.
|
||||
setgid(WIND_GROUP_ID);
|
||||
setuid(WIND_USER_ID);
|
||||
|
||||
auto server = TRY(os::LocalServer::create(socket_path, false));
|
||||
TRY(server->listen(20));
|
||||
|
||||
int fd = open("/dev/null", O_RDONLY);
|
||||
if (fd >= 0)
|
||||
{
|
||||
@ -121,44 +85,8 @@ Result<int> luna_main(int argc, char** argv)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
setegid(2);
|
||||
seteuid(2);
|
||||
|
||||
if (setsid() < 0) perror("setsid");
|
||||
|
||||
mode_t mask = umask(0002);
|
||||
|
||||
auto server = TRY(os::LocalServer::create(socket_path, false));
|
||||
TRY(server->listen(20));
|
||||
|
||||
umask(mask);
|
||||
|
||||
seteuid(0);
|
||||
|
||||
clearenv();
|
||||
|
||||
pid_t child = TRY(os::Process::fork());
|
||||
if (!child)
|
||||
{
|
||||
if (!user.is_empty())
|
||||
{
|
||||
auto* pwd = getpwnam(user.chars());
|
||||
if (pwd)
|
||||
{
|
||||
TRY(set_supplementary_groups(user.chars()));
|
||||
setgid(pwd->pw_gid);
|
||||
setuid(pwd->pw_uid);
|
||||
}
|
||||
}
|
||||
|
||||
StringView args[] = { "/usr/bin/init"_sv, "--user"_sv };
|
||||
TRY(os::Process::exec("/usr/bin/init"_sv, Slice<StringView> { args, 2 }, false));
|
||||
}
|
||||
|
||||
umask(0002);
|
||||
|
||||
setuid(2);
|
||||
setgid(2);
|
||||
// We're ready now.
|
||||
os::IPC::notify_parent();
|
||||
|
||||
ui::Color background = ui::Color::from_rgb(0x10, 0x10, 0x10);
|
||||
|
||||
@ -167,13 +95,14 @@ Result<int> luna_main(int argc, char** argv)
|
||||
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(fds.try_append({ .fd = system_server->fd(), .events = POLLIN, .revents = 0 }));
|
||||
|
||||
TRY(os::Security::pledge("stdio rpath wpath cpath unix proc exec", NULL));
|
||||
TRY(os::Security::pledge("stdio rpath wpath cpath unix", NULL));
|
||||
|
||||
while (1)
|
||||
{
|
||||
screen.canvas().fill(background);
|
||||
for (auto* window : g_windows) window->draw(screen.canvas());
|
||||
Layer::draw_all_windows(screen.canvas());
|
||||
mouse_pointer.draw(screen.canvas());
|
||||
screen.sync();
|
||||
|
||||
@ -193,33 +122,11 @@ Result<int> luna_main(int argc, char** argv)
|
||||
{
|
||||
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())
|
||||
if (auto* window = Layer::focused_window())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
window->client->conn->send_async(request);
|
||||
}
|
||||
}
|
||||
if (fds[2].revents & POLLIN)
|
||||
@ -227,8 +134,43 @@ Result<int> luna_main(int argc, char** argv)
|
||||
auto client = TRY(server->accept());
|
||||
os::println("wind: New client connected!");
|
||||
TRY(fds.try_append({ .fd = client.fd(), .events = POLLIN, .revents = 0 }));
|
||||
OwnedPtr<Client> c = TRY(adopt_owned_if_nonnull(new Client(move(client))));
|
||||
|
||||
auto connection = TRY(os::IPC::ClientConnection::adopt_connection(move(client)));
|
||||
|
||||
OwnedPtr<Client> c = TRY(adopt_owned_if_nonnull(new Client(move(connection), EMPTY_PLEDGE)));
|
||||
TRY(clients.try_append(move(c)));
|
||||
}
|
||||
if (fds[3].revents & POLLIN)
|
||||
{
|
||||
auto client = TRY(system_server->accept());
|
||||
os::println("wind: New privileged client connected!");
|
||||
TRY(fds.try_append({ .fd = client.fd(), .events = POLLIN, .revents = 0 }));
|
||||
|
||||
auto connection = TRY(os::IPC::ClientConnection::adopt_connection(move(client)));
|
||||
|
||||
OwnedPtr<Client> c = TRY(adopt_owned_if_nonnull(new Client(move(connection), HAS_NOT_YET_PLEDGED)));
|
||||
TRY(clients.try_append(move(c)));
|
||||
}
|
||||
for (usize i = 0; i < clients.size(); i++)
|
||||
{
|
||||
if (fds[i + 4].revents & POLLIN) clients[i]->conn->check_for_messages();
|
||||
if (fds[i + 4].revents & POLLHUP) clients[i]->should_be_disconnected = true;
|
||||
if (clients[i]->should_be_disconnected)
|
||||
{
|
||||
os::println("wind: Client %zu disconnected", i);
|
||||
fds.remove_at(i + 4);
|
||||
auto client = clients.remove_at(i);
|
||||
client->conn->disconnect();
|
||||
for (auto& window : client->windows)
|
||||
{
|
||||
if (window)
|
||||
{
|
||||
window->layer->windows.remove(window);
|
||||
mouse_pointer.window_did_close(window);
|
||||
delete window;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,3 +19,6 @@
|
||||
# Uncomment the line below to make the kernel also calculate stack traces for userspace addresses on program crashes.
|
||||
# This can aid in debugging, but makes the kernel more unstable as stack tracing will access arbitrary userspace memory.
|
||||
# target_compile_definitions(moon PRIVATE MOON_ENABLE_USERSPACE_STACK_TRACES)
|
||||
|
||||
# Uncomment the line below to enable all kernel debug messages, and console logging.
|
||||
# include(debug.cmake)
|
||||
|
@ -12,4 +12,4 @@ target_compile_definitions(moon PRIVATE DEVICE_REGISTRY_DEBUG)
|
||||
target_compile_definitions(moon PRIVATE FORK_DEBUG)
|
||||
target_compile_definitions(moon PRIVATE MOUNT_DEBUG)
|
||||
target_compile_definitions(moon PRIVATE CACHE_DEBUG)
|
||||
target_compile_options(moon PRIVATE -fsanitize=undefined)
|
||||
#target_compile_options(moon PRIVATE -fsanitize=undefined)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user