From eff21ee2ce1fdb4328d71d801352ec25f4e457ff Mon Sep 17 00:00:00 2001 From: littleblack111 Date: Wed, 2 Apr 2025 18:49:15 +0800 Subject: feat: ftxui (#17) * ready for merge rm sum random ocmments first and https://github.com/ArthurSonzogni/FTXUI/issues/1024 but still, ready for merge since we can't do anything on our end * style: clang-tidy * ci * ci * style: clang-tidy * fix * fix: scroll https://github.com/ArthurSonzogni/git-tui/blob/master/src/scroller.cpp * focus * update todo * style: clang-format * update todo * sticky scroll * update todxo * smol fix need event to update screen. so we just give event when IO updates * style: clang-format --------- Co-authored-by: github-actions[bot] --- src/IOManager.cpp | 22 +++++++++++++ src/IOManager.hpp | 30 ++++++++++++++++++ src/TODO.md | 11 +++++++ src/chatManager.cpp | 15 ++++++--- src/chatManager.hpp | 5 +-- src/chatServer.cpp | 14 +++++++-- src/inputManager.cpp | 64 ++++++++++++++++++++++++++++++-------- src/inputManager.hpp | 14 ++++++++- src/log.hpp | 13 +++++--- src/renderManager.cpp | 19 ++++++++++++ src/renderManager.hpp | 21 +++++++++++++ src/scroller.cpp | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/scroller.hpp | 15 +++++++++ 13 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 src/IOManager.cpp create mode 100644 src/IOManager.hpp create mode 100644 src/TODO.md create mode 100644 src/renderManager.cpp create mode 100644 src/renderManager.hpp create mode 100644 src/scroller.cpp create mode 100644 src/scroller.hpp (limited to 'src') diff --git a/src/IOManager.cpp b/src/IOManager.cpp new file mode 100644 index 0000000..c5363af --- /dev/null +++ b/src/IOManager.cpp @@ -0,0 +1,22 @@ +#include "IOManager.hpp" +#include "inputManager.hpp" +#include "log.hpp" + +CIOManager::CIOManager() { + log(LOG, "IOManager: initialized"); +} + +CIOManager::~CIOManager() { + m_vIO.clear(); + log(SYS, "IOManager: bye"); +} + +const std::vector &CIOManager::getIO() const { + return m_vIO; +} + +void CIOManager::addLog(const SLog &log) { + m_vIO.push_back({.msg = std::nullopt, .log = log}); + if (g_pInputManager) + g_pInputManager->updateIO(); +} diff --git a/src/IOManager.hpp b/src/IOManager.hpp new file mode 100644 index 0000000..ba741ff --- /dev/null +++ b/src/IOManager.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "chatManager.hpp" +#include "format.hpp" +#include + +class CIOManager { + public: + CIOManager(); + ~CIOManager(); + + struct SLog { + eFormatType type; + std::string log; + }; + struct SIO { + // cannot have both at once + std::optional msg; + std::optional log; + }; + + const std::vector &getIO() const; + void addLog(const SLog &log); + + private: + std::vector m_vIO; + + friend class CChatManager; +}; +inline std::unique_ptr g_pIOManager; diff --git a/src/TODO.md b/src/TODO.md new file mode 100644 index 0000000..381095c --- /dev/null +++ b/src/TODO.md @@ -0,0 +1,11 @@ +## General +- [ ] ci unit testing + +## FTXUI + +- [ ] tabs for each user for direct message +- ~[ ] fix: bottom(input) only 20% of screen instead of hardcoded px~ + - [x] fix: bottom(input) only take 1line of screen +- [-] auto focus input // FIXME +- [ ] tabs for logs(log/chat) +- [-] sticky scroll(scroll to bottom) diff --git a/src/chatManager.cpp b/src/chatManager.cpp index 3b8c82d..96c1b0c 100644 --- a/src/chatManager.cpp +++ b/src/chatManager.cpp @@ -1,4 +1,6 @@ #include "chatManager.hpp" +#include "IOManager.hpp" +#include "inputManager.hpp" #include "log.hpp" #include "sessionManager.hpp" @@ -7,7 +9,6 @@ CChatManager::CChatManager() { } CChatManager::~CChatManager() { - m_vMessages.clear(); log(SYS, "ChatManager: bye"); } @@ -19,13 +20,19 @@ void CChatManager::newMessage(const SMessage &msg) { return; } g_pSessionManager->broadcastChat(msg); - m_vMessages.push_back(msg); + g_pIOManager->m_vIO.push_back({.msg = msg, .log = std::nullopt}); + g_pInputManager->updateIO(); } std::string CChatManager::fmtBroadcastMessage(const SMessage &msg) { return NFormatter::fmt(NONE, "{}: {}", msg.username, msg.msg); } -const std::vector &CChatManager::getChat() const { - return m_vMessages; +std::vector CChatManager::getChat() const { + std::vector chat; + + for (const auto &[msg, log] : g_pIOManager->getIO()) + if (msg) + chat.push_back(*msg); + return chat; } diff --git a/src/chatManager.hpp b/src/chatManager.hpp index 084c002..0f423f7 100644 --- a/src/chatManager.hpp +++ b/src/chatManager.hpp @@ -16,9 +16,6 @@ class CChatManager { void newMessage(const SMessage &msg); std::string fmtBroadcastMessage(const SMessage &msg); - const std::vector &getChat() const; - - private: - std::vector m_vMessages; + std::vector getChat() const; }; inline std::unique_ptr g_pChatManager; diff --git a/src/chatServer.cpp b/src/chatServer.cpp index 56caf4b..7b64e79 100644 --- a/src/chatServer.cpp +++ b/src/chatServer.cpp @@ -1,9 +1,11 @@ #include "chatServer.hpp" +#include "IOManager.hpp" #include "chatManager.hpp" #include "commandHandler.hpp" #include "commands.hpp" #include "inputManager.hpp" #include "log.hpp" +#include "renderManager.hpp" #include "server.hpp" #include "sessionManager.hpp" @@ -28,18 +30,24 @@ void CChatServer::cleanup() { g_pServer.reset(); g_pCommandHandler.reset(); g_pInputManager.reset(); + g_pIOManager.reset(); + g_pRenderManager.reset(); } void CChatServer::initManagers() { log(LOG, "ChatServer: initializing managers"); + log(LOG, "Creating RenderManager"); + g_pRenderManager = std::make_unique(); + log(LOG, "Creating InputManager"); + g_pInputManager = std::make_unique(); log(LOG, "Creating Server"); g_pServer = std::make_unique(m_port); + log(LOG, "Creating IOManager"); + g_pIOManager = std::make_unique(); log(LOG, "Creating SessionManager"); g_pSessionManager = std::make_shared(); log(LOG, "Creating ChatManager"); g_pChatManager = std::make_unique(); - log(LOG, "Creating InputManager"); - g_pInputManager = std::make_unique(); } void CChatServer::start() { @@ -53,5 +61,5 @@ void CChatServer::start() { m_sessionManagerThread = std::jthread(&CSessionManager::run, g_pSessionManager.get()); m_sessionManagerThread.detach(); - g_pInputManager->run(); + g_pInputManager->inputLoop(); } diff --git a/src/inputManager.cpp b/src/inputManager.cpp index 8de26bd..cc3880b 100644 --- a/src/inputManager.cpp +++ b/src/inputManager.cpp @@ -1,10 +1,43 @@ #include "inputManager.hpp" +#include "IOManager.hpp" #include "chatManager.hpp" #include "commandHandler.hpp" #include "log.hpp" -#include +#include "renderManager.hpp" +#include "scroller.hpp" + +CInputManager::CInputManager() + : m_input(ftxui::Input(&m_szInput, "Type here...", {.transform = [](ftxui::InputState state) { + state.element |= ftxui::bgcolor(ftxui::Color::HSVA(0, 0, 0, 0)); + state.element |= ftxui::color(ftxui::Color::White); + return state.element; }, .multiline = false, .on_enter = [&] { + if (m_szInput.empty()) + return; + + g_pCommandHandler->isCommand(m_szInput) ? g_pCommandHandler->handleCommand(m_szInput) : g_pChatManager->newMessage({.msg = m_szInput, .username = "Server"}); + m_szInput.clear(); }}) // NOLINT + ) + , inputComponent(ftxui::Renderer(m_input, [&] { + return ftxui::hbox({ + ftxui::text("> "), + m_input->Render(), + }); + })) + , logComponent(ftxui::Renderer([] { + std::vector messages; + for (const auto &[chat, log] : g_pIOManager->getIO()) { + if (chat && !chat->msg.empty()) { + std::string username = chat->username; + if (username.back() == '\n') + username.pop_back(); + messages.push_back(ftxui::window(ftxui::text(username), ftxui::paragraph(chat->msg))); + } else if (log && !log->log.empty()) + messages.push_back(ftxui::window(ftxui::text(NFormatter::fmt(log->type, "")), ftxui::paragraph(log->log))); + } + + return ftxui::vbox(messages); + })) { -CInputManager::CInputManager() { log(LOG, "InputManager: initialized"); } @@ -12,19 +45,24 @@ CInputManager::~CInputManager() { log(SYS, "InputManager: bye"); } -void CInputManager::run() { - inputLoop(); +void CInputManager::updateIO() { + logComponent->OnEvent(ftxui::Event::Custom); + g_pRenderManager->screen.PostEvent(ftxui::Event::Custom); } void CInputManager::inputLoop() { - while (true) { - std::string input; - std::print("> "); - std::getline(std::cin, input); - - if (input.empty()) - continue; + container = logComponent |= ftxui::Scroller; + container = ftxui::ResizableSplitBottom(inputComponent, container, &bottomSize); - g_pCommandHandler->isCommand(input) ? g_pCommandHandler->handleCommand(input) : g_pChatManager->newMessage({.msg = input, .username = "Server"}); - } + // FIXME: this shit spams TakeFocus(not ideal for power etc reasons) + // consider: custom loop, or just only allow focus to be inputComponent somehow + container |= ftxui::CatchEvent([&](ftxui::Event) { + inputComponent->TakeFocus(); + return false; + }); + g_pRenderManager->setRenderer(ftxui::Renderer(container, [this] { + return container->Render() | ftxui::border; + })); + inputComponent->TakeFocus(); + g_pRenderManager->enterLoop(); } diff --git a/src/inputManager.hpp b/src/inputManager.hpp index ef75b52..a81e39d 100644 --- a/src/inputManager.hpp +++ b/src/inputManager.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include class CInputManager { @@ -7,7 +8,18 @@ class CInputManager { CInputManager(); ~CInputManager(); - void run(); void inputLoop(); + + void updateIO(); + + private: + std::string m_szInput; + + ftxui::Component m_input; + ftxui::Component inputComponent; + ftxui::Component logComponent; + ftxui::Component container; + + int bottomSize = 1; }; inline std::unique_ptr g_pInputManager; diff --git a/src/log.hpp b/src/log.hpp index 996b6bc..a4654c4 100644 --- a/src/log.hpp +++ b/src/log.hpp @@ -1,16 +1,18 @@ #pragma once +#include "IOManager.hpp" #include "format.hpp" -#include #include #include -#include inline void log(FILE *_stream, eFormatType type, std::string str) { #ifndef DEBUG if (type == TRACE) return; #endif - std::println(_stream, "{}", NFormatter::fmt(type, str)); + if (g_pIOManager) + g_pIOManager->addLog({.type = type, .log = str}); + else + std::println(_stream, "{}", NFormatter::fmt(type, str)); } template @@ -19,7 +21,10 @@ void log(FILE *_stream, eFormatType type, std::format_string fmt, Args if (type == TRACE) return; #endif - std::println(_stream, "{}", NFormatter::fmt(type, fmt, std::forward(args)...)); + if (g_pIOManager) + g_pIOManager->addLog({.type = type, .log = NFormatter::fmt(NONEWLINE, fmt, std::forward(args)...)}); + else + std::println(_stream, "{}", NFormatter::fmt(type, fmt, std::forward(args)...)); } inline void log(eFormatType type, std::string str) { diff --git a/src/renderManager.cpp b/src/renderManager.cpp new file mode 100644 index 0000000..d3720c7 --- /dev/null +++ b/src/renderManager.cpp @@ -0,0 +1,19 @@ +#include "renderManager.hpp" +#include "log.hpp" + +CRenderManager::CRenderManager() + : screen(ftxui::ScreenInteractive::Fullscreen()) { + log(SYS, "RenderManager: initialized"); +}; + +void CRenderManager::enterLoop() { + screen.Loop(renderer); +} + +void CRenderManager::setRenderer(ftxui::Component component) { + renderer = std::move(component); +} + +CRenderManager::~CRenderManager() { + screen.Exit(); +} diff --git a/src/renderManager.hpp b/src/renderManager.hpp new file mode 100644 index 0000000..4407f66 --- /dev/null +++ b/src/renderManager.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +class CRenderManager { + public: + CRenderManager(); + ~CRenderManager(); + + void enterLoop(); + + void setRenderer(ftxui::Component component); + + private: + ftxui::ScreenInteractive screen; + ftxui::Component renderer; + + friend class CInputManager; +}; +inline std::unique_ptr g_pRenderManager; diff --git a/src/scroller.cpp b/src/scroller.cpp new file mode 100644 index 0000000..bf80770 --- /dev/null +++ b/src/scroller.cpp @@ -0,0 +1,85 @@ +#include "scroller.hpp" + +#include "log.hpp" +#include // for max, min +#include // for Component, ComponentBase +#include // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp +#include // for shared_ptr, allocator, __shared_ptr_access +#include // for move + +#include "ftxui/component/component.hpp" // for Make +#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::WheelDown, Mouse::WheelUp +#include "ftxui/dom/deprecated.hpp" // for text +#include "ftxui/dom/elements.hpp" // for operator|, Element, size, vbox, EQUAL, HEIGHT, dbox, reflect, focus, inverted, nothing, select, vscroll_indicator, yflex, yframe +#include "ftxui/dom/node.hpp" // for Node +#include "ftxui/dom/requirement.hpp" // for Requirement +#include "ftxui/screen/box.hpp" // for Box + +namespace ftxui { + +class ScrollerBase : public ComponentBase { + public: + ScrollerBase(Component child) { Add(child); } + + private: + Element OnRender() final { + auto focused = Focused() ? focus : ftxui::select; + auto style = Focused() ? inverted : nothing; + + Element background = ComponentBase::Render(); + background->ComputeRequirement(); + size_ = background->requirement().min_y; + return dbox({ + std::move(background), + vbox({ + text(L"") | size(HEIGHT, EQUAL, selected_), + text(L"") | style | focused, + }), + }) | + vscroll_indicator | yframe | yflex | reflect(box_); + } + + bool OnEvent(Event event) final { + if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y)) + TakeFocus(); + + int selected_old = selected_; + if (event == Event::ArrowUp || event == Event::Character('k') || + (event.is_mouse() && event.mouse().button == Mouse::WheelUp)) + selected_--; + if ((event == Event::ArrowDown || event == Event::Character('j') || + (event.is_mouse() && event.mouse().button == Mouse::WheelDown))) + selected_++; + if (event == Event::PageDown) + selected_ += box_.y_max - box_.y_min; + if (event == Event::PageUp) + selected_ -= box_.y_max - box_.y_min; + if (event == Event::Home) + selected_ = 0; + if (event == Event::End) + selected_ = size_; + + if (event == Event::Custom && sticky_select) + selected_ = size_; + + selected_ = std::max(0, std::min(size_, selected_)); + sticky_select = selected_ == size_; + return selected_old != selected_; + } + + bool Focusable() const final { return true; } + + bool sticky_select = true; + int selected_ = 0; + int size_ = 0; + Box box_; +}; + +Component Scroller(Component child) { + return Make(std::move(child)); +} +} // namespace ftxui + +// Copyright 2021 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/scroller.hpp b/src/scroller.hpp new file mode 100644 index 0000000..81a2a6d --- /dev/null +++ b/src/scroller.hpp @@ -0,0 +1,15 @@ +#ifndef SCROLLER_H +#define SCROLLER_H + +#include + +#include "ftxui/component/component_base.hpp" // for Component + +namespace ftxui { +Component Scroller(Component child); +} +#endif /* end of include guard: SCROLLER_H */ + +// Copyright 2021 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. -- cgit v1.2.3