From 09745a7835be3ffbd41191e3e4202099cb6be962 Mon Sep 17 00:00:00 2001 From: Bananymous Date: Tue, 6 May 2025 00:41:22 +0300 Subject: [PATCH] userspace: Implement program launcher This is kinda useless as I only have Terminal and test-window implemented. Also the code is really messy as I don't have GUI widgets. --- userspace/programs/CMakeLists.txt | 1 + .../programs/ProgramLauncher/CMakeLists.txt | 12 + userspace/programs/ProgramLauncher/main.cpp | 344 ++++++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 userspace/programs/ProgramLauncher/CMakeLists.txt create mode 100644 userspace/programs/ProgramLauncher/main.cpp diff --git a/userspace/programs/CMakeLists.txt b/userspace/programs/CMakeLists.txt index 93c98441..554918e8 100644 --- a/userspace/programs/CMakeLists.txt +++ b/userspace/programs/CMakeLists.txt @@ -24,6 +24,7 @@ set(USERSPACE_PROGRAMS mkdir nslookup poweroff + ProgramLauncher resolver rm Shell diff --git a/userspace/programs/ProgramLauncher/CMakeLists.txt b/userspace/programs/ProgramLauncher/CMakeLists.txt new file mode 100644 index 00000000..63ad5412 --- /dev/null +++ b/userspace/programs/ProgramLauncher/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCES + main.cpp +) + +add_executable(ProgramLauncher ${SOURCES}) +banan_link_library(ProgramLauncher ban) +banan_link_library(ProgramLauncher libc) +banan_link_library(ProgramLauncher libfont) +banan_link_library(ProgramLauncher libgui) +banan_link_library(ProgramLauncher libinput) + +install(TARGETS ProgramLauncher OPTIONAL) diff --git a/userspace/programs/ProgramLauncher/main.cpp b/userspace/programs/ProgramLauncher/main.cpp new file mode 100644 index 00000000..c60388f1 --- /dev/null +++ b/userspace/programs/ProgramLauncher/main.cpp @@ -0,0 +1,344 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +static constexpr uint32_t s_line_chars = 64; +static constexpr uint32_t s_list_height = 15; + +static constexpr uint32_t s_padding = 3; +static constexpr uint32_t s_margin = 5; + +static constexpr uint32_t s_separator_h = 2; + +static constexpr uint32_t s_scroll_w = 10; +static constexpr uint32_t s_scroll_h_min = 5; + +static constexpr uint32_t s_color_bg1 = 0xCC'404040; +static constexpr uint32_t s_color_bg2 = 0xCC'606060; +static constexpr uint32_t s_color_selected = 0xCC'0000FF; +static constexpr uint32_t s_color_text = 0xCC'FFFFFF; +static constexpr uint32_t s_color_separator = 0xCC'FFFFFF; +static constexpr uint32_t s_color_scroll = 0xCC'808080; + +static uint32_t line_w(const LibFont::Font& font) +{ + return s_line_chars * font.width() + 2 * s_padding; +} + +static uint32_t line_h(const LibFont::Font& font) +{ + return font.height() + 2 * s_padding; +} + +static const BAN::Vector get_program_list() +{ + BAN::HashSet paths; + if (const char* path_env = getenv("PATH")) + { + auto path_env_copy = BAN::String(path_env); + + const char* token = strtok(path_env_copy.data(), ":"); + do + MUST(paths.insert(BAN::StringView(token))); + while ((token = strtok(nullptr, ":"))); + } + + BAN::HashSet program_set; + for (const auto& path : paths) + { + DIR* dirp = opendir(path.data()); + if (dirp == nullptr) + continue; + + dirent* dent; + while ((dent = readdir(dirp))) + { + if (dent->d_type != DT_REG && dent->d_type != DT_LNK) + continue; + + struct stat st; + if (fstatat(dirfd(dirp), dent->d_name, &st, 0) == -1) + continue; + if (!S_ISREG(st.st_mode)) + continue; + if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) + continue; + + MUST(program_set.insert(BAN::String(dent->d_name))); + } + + closedir(dirp); + } + + BAN::Vector programs; + MUST(programs.reserve(program_set.size())); + for (auto& program : program_set) + MUST(programs.emplace_back(BAN::move(program))); + + BAN::sort::sort(programs.begin(), programs.end(), + [](const auto& a, const auto& b) -> bool + { + const size_t min_size = BAN::Math::min(a.size(), b.size()); + for (size_t i = 0; i < min_size; i++) + if (a[i] != b[i]) + return a[i] < b[i]; + return a.size() < b.size(); + } + ); + + return programs; +} + +static BAN::Vector get_filtered_program_list(BAN::Span program_list, BAN::StringView prompt) +{ + BAN::Vector filtered_list; + for (const auto& program : program_list) + { + for (size_t i = 0; i + prompt.size() <= program.size(); i++) + { + bool match = true; + for (size_t j = 0; j < prompt.size() && match; j++) + if (tolower(prompt[j]) != tolower(program[i + j])) + match = false; + + if (!match) + continue; + + MUST(filtered_list.push_back(program.sv())); + break; + } + } + + return filtered_list; +} + +void render_search_box(LibGUI::Texture& texture, const LibFont::Font& font, BAN::StringView prompt) +{ + char buffer[s_line_chars + 1]; + snprintf(buffer, sizeof(buffer), "search: %.*s", (int)prompt.size(), prompt.data()); + + texture.fill(s_color_bg1); + texture.draw_text(buffer, font, s_padding, s_padding, s_color_text); +} + +void render_list(LibGUI::Texture& texture, const LibFont::Font& font, BAN::Span programs, size_t selected) +{ + texture.fill(s_color_bg1); + + const size_t start = selected / s_list_height * s_list_height; + const size_t count = BAN::Math::min(s_list_height, programs.size() - start); + + const uint32_t line_w = ::line_w(font); + const uint32_t line_h = ::line_h(font); + + for (size_t i = 0; i < count; i++) + { + uint32_t color = (i % 2) ? s_color_bg2 : s_color_bg1; + if (start + i == selected) + color = s_color_selected; + texture.fill_rect(0, line_h * i, line_w, line_h, color); + texture.draw_text(programs[start + i], font, s_padding, s_padding + line_h * i, s_color_text); + } +} + +void render_scroll(LibGUI::Texture& texture, BAN::Span programs, size_t selected) +{ + texture.fill(s_color_bg1); + + if (programs.empty()) + return; + + texture.fill_rect( + s_padding, + texture.height() * selected / programs.size(), + s_scroll_w, + BAN::Math::max(texture.height() / programs.size(), s_scroll_h_min), + s_color_scroll + ); +} + +void render_initial_window(LibGUI::Window& window, const LibFont::Font& font) +{ + auto& texture = window.texture(); + texture.fill(s_color_bg1); + texture.fill_rect(s_margin, s_margin + line_h(font) + s_padding, line_w(font), s_separator_h, s_color_separator); +} + +struct Rectangle { uint32_t x, y, w, h; }; + +int main() +{ + auto attributes = LibGUI::Window::default_attributes; + attributes.alpha_channel = true; + attributes.title_bar = false; + + auto font = MUST(LibFont::Font::load("/usr/share/fonts/lat0-16.psfu"_sv)); + + const auto full_program_list = get_program_list(); + + // FIXME: implement widgets + + const Rectangle search_area { + .x = s_margin, + .y = s_margin, + .w = line_w(font), + .h = line_h(font), + }; + + const Rectangle list_area { + .x = s_margin, + .y = search_area.x + search_area.h + s_padding + s_separator_h + s_padding, + .w = line_w(font), + .h = line_h(font) * s_list_height, + }; + + const Rectangle scroll_area { + .x = list_area.x + list_area.w - (s_padding + s_scroll_w + s_padding), + .y = list_area.y, + .w = s_padding + s_scroll_w + s_padding, + .h = list_area.h, + }; + + auto search_texture = MUST(LibGUI::Texture::create( + search_area.w, + search_area.h, + s_color_bg1 + )); + + auto list_texture = MUST(LibGUI::Texture::create( + list_area.w, + list_area.h, + s_color_bg1 + )); + + auto scroll_texture = MUST(LibGUI::Texture::create( + scroll_area.w, + scroll_area.h, + s_color_bg1 + )); + + auto window = MUST(LibGUI::Window::create( + scroll_area.x + scroll_area.w + s_margin, + scroll_area.y + scroll_area.h + s_margin, + ""_sv, + attributes + )); + + BAN::String prompt; + + size_t selected = 0; + auto filtered_list = get_filtered_program_list(full_program_list.span(), prompt); + + const auto refresh_selected = + [&]() + { + render_list(list_texture, font, filtered_list.span(), selected); + window->texture().copy_texture(list_texture, list_area.x, list_area.y); + window->invalidate(list_area.x, list_area.y, list_area.w, list_area.h); + + if (filtered_list.size() > s_list_height) + { + render_scroll(scroll_texture, filtered_list.span(), selected); + window->texture().copy_texture(scroll_texture, scroll_area.x, scroll_area.y); + window->invalidate(scroll_area.x, scroll_area.y, scroll_area.w, scroll_area.h); + } + }; + + const auto refresh_search = + [&]() + { + selected = 0; + filtered_list = get_filtered_program_list(full_program_list.span(), prompt); + + render_search_box(search_texture, font, prompt); + window->texture().copy_texture(search_texture, search_area.x, search_area.y); + window->invalidate(search_area.x, search_area.y, search_area.w, search_area.h); + + render_list(list_texture, font, filtered_list.span(), selected); + window->texture().copy_texture(list_texture, list_area.x, list_area.y); + window->invalidate(list_area.x, list_area.y, list_area.w, list_area.h); + + if (filtered_list.size() > s_list_height) + { + render_scroll(scroll_texture, filtered_list.span(), selected); + window->texture().copy_texture(scroll_texture, scroll_area.x, scroll_area.y); + window->invalidate(scroll_area.x, scroll_area.y, scroll_area.w, scroll_area.h); + } + }; + + window->set_key_event_callback( + [&](LibGUI::EventPacket::KeyEvent::event_t event) + { + if (!event.pressed()) + return; + + switch (event.key) + { + case LibInput::Key::ArrowUp: + selected = (selected + filtered_list.size() - 1) % filtered_list.size(); + refresh_selected(); + break; + case LibInput::Key::ArrowDown: + selected = (selected + 1) % filtered_list.size(); + refresh_selected(); + break; + case LibInput::Key::Escape: + exit(0); + case LibInput::Key::Enter: + { + const char* program = filtered_list.empty() ? prompt.data() : filtered_list[selected].data(); + + int null = open("/dev/null", O_RDWR | O_CLOEXEC); + if (null == -1) + dwarnln("open: {}", strerror(errno)); + else + { + dup2(null, STDIN_FILENO); + dup2(null, STDOUT_FILENO); + dup2(null, STDERR_FILENO); + } + + execlp(program, program, nullptr); + dwarnln("execlp: {}", strerror(errno)); + exit(1); + } + case LibInput::Key::Backspace: + if (prompt.empty()) + break; + while (!prompt.empty() && (prompt.back() & 0xC0) == 0x80) + prompt.pop_back(); + if (!prompt.empty()) + prompt.pop_back(); + refresh_search(); + break; + default: + const char* utf8 = LibInput::key_to_utf8(event.key, event.modifier); + if (utf8 == nullptr) + break; + MUST(prompt.append(utf8)); + refresh_search(); + break; + } + } + ); + + render_initial_window(*window, font); + refresh_search(); + window->invalidate(); + + for (;;) + { + window->wait_events(); + window->poll_events(); + } +}