diff --git a/README.md b/README.md index bae650f..147de72 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ MultiApp/ // Core application files - 🎉 Local chat app - ❌ Maze generator/player/solver - ❌ Game of life +- ❌ Local AI chat with ollama - 🔬 Other goodies in the future ## 🏗️ Requirements diff --git a/makefile b/makefile index b590dab..b8525e5 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ PROGRAM=multiapp -OBJECTS=build/main_application.o build/browser_app.o build/fractal_app.o build/chat_app.o +OBJECTS=build/main_application.o build/browser_app.o build/fractal_app.o build/chat_app.o build/session.o MAIN=build/main.o TESTS=test_build/main.o test_build/main_application_tests.o test_build/browser_app_tests.o test_build/fractal_app_tests.o test_build/chat_app_tests.o diff --git a/res/screenshots/chat.png b/res/screenshots/chat.png index d6a6ca4..0ce192b 100644 Binary files a/res/screenshots/chat.png and b/res/screenshots/chat.png differ diff --git a/src/browser_app.cc b/src/browser_app.cc index 8304e67..ec470f9 100644 --- a/src/browser_app.cc +++ b/src/browser_app.cc @@ -7,39 +7,6 @@ #include -std::string Browser::get_uri_root(const std::string &uri) { - std::string result {std::regex_replace(uri, std::regex("(https?://|www\\.)"), "")}; - return result; -} - -void Browser::entry_uri_load(std::string uri) const { - if(uri == "") [[unlikely]] - // Get entry text - uri = uri_entry->get_text(); - - // Is current URI a web page. - if(uri.find(' ') > uri.size() && - std::regex_search(uri, std::regex("^(http(s)?://)?(www\\.)?[A-Za-z0-9.]+\\.[A-Za-z0-9/+-_?=#]+$"))) [[likely]] { - // Add missing parts of the URL - if(uri.find("http") > uri.size()) [[likely]] { - if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[likely]] - uri = "www." + uri; - uri = "https://" + uri; - }else if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[unlikely]] - std::regex_replace(uri, std::regex("https?://"), "https://www."); - } else [[unlikely]] { - // Add as a google search - std::replace(uri.begin(), uri.end(), ' ', '+'); - uri = "https://www.google.com/search?q=" + uri; - } - - // Reload if the requested URI is the same as current - if(get_uri_root(webkit_web_view_get_uri(web_view)) != get_uri_root(uri)) [[likely]] - webkit_web_view_load_uri(web_view, uri.c_str()); - else [[unlikely]] - webkit_web_view_reload(web_view); -} - Browser::Browser() : Gtk::Box(Gtk::Orientation::VERTICAL) { // Load the GtkBuilder file and instantiate its widgets, check for errors auto ref_builder {Gtk::Builder::create()}; @@ -123,6 +90,38 @@ void Browser::on_realize() { status_label->set_text("Welcome to the Browser!"); } +std::string Browser::get_uri_root(const std::string &uri) { + std::string result { std::regex_replace(uri, std::regex("(https?://|www\\.)"), "") }; + return result; +} + +void Browser::entry_uri_load(std::string uri) const { + if(uri == "") [[unlikely]] + // Get entry text + uri = uri_entry->get_text(); + + // Is current URI a web page. + if(uri.find(' ') > uri.size() && + std::regex_search(uri, std::regex("^(http(s)?://)?(www\\.)?[A-Za-z0-9.]+\\.[A-Za-z0-9/+-_?=#]+$"))) [[likely]] { + // Add missing parts of the URL + if(uri.find("http") > uri.size()) [[likely]] { + if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[likely]] + uri = "www." + uri; + uri = "https://" + uri; + }else if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[unlikely]] + std::regex_replace(uri, std::regex("https?://"), "https://www."); + } else [[unlikely]] { + // Add as a google search + std::replace(uri.begin(), uri.end(), ' ', '+'); + uri = "https://www.google.com/search?q=" + uri; + } + + // Reload if the requested URI is the same as current + if(get_uri_root(webkit_web_view_get_uri(web_view)) != get_uri_root(uri)) [[likely]] + webkit_web_view_load_uri(web_view, uri.c_str()); + else [[unlikely]] + webkit_web_view_reload(web_view); +} GtkWidget *Browser::on_create_cb(WebKitWebView *web_view, WebKitNavigationAction *action, diff --git a/src/chat_app.cc b/src/chat_app.cc index be940aa..121861a 100644 --- a/src/chat_app.cc +++ b/src/chat_app.cc @@ -1,239 +1,10 @@ #include "include/chat_app.hpp" - -#include -#include -#include +#include "include/session.hpp" #include -#include - - -Session::Session(const std::string &host, unsigned port) : - host (host), - port (port), - send_timer (std::make_unique(ioc)), - socket (ioc) { - if ( port > 65535 ) { - std::cerr << "Port out of bounds (0-65535)" << std::endl; - throw std::out_of_range("Port out of bounds (0-65535)"); - } - send_timer->expires_at(asio::steady_timer::time_point::min()); - tcp::resolver resolver(ioc); - - std::cout << "Connecting to " << host << ":" << port << "...\n"; - - asio::error_code ec; - auto endpoints = resolver.resolve(host, std::to_string(port), ec); - if (ec) { - std::cerr << "[error] resolve(): " << ec.message() - << std::endl << "Is the server running?" << std::endl; - throw std::runtime_error("Failed to resolve host"); - } - - asio::connect(socket, endpoints, ec); - if (ec) { - std::cerr << "[error] connect(): " << ec.message() - << std::endl << "Is the server running?" << std::endl; - throw std::runtime_error("Failed to connect to server"); - } - - // Spawn receiver and sender - asio::co_spawn(ioc, receiver(socket), asio::detached); - asio::co_spawn(ioc, sender(socket), asio::detached); - - // Run the event loop in separate thread until ioc.stop() is called - ioc_thread = std::thread([this](){ ioc.run(); }); -} - -Session::~Session() noexcept { - if ( !ioc.stopped() ) ioc.stop(); - if ( ioc_thread.joinable() ) ioc_thread.join(); - if ( socket.is_open() ) socket.close(); -} - -awaitable Session::sender(tcp::socket& socket) { - try { - while (true) { - // Wait until there's something to send - while (send_buf.empty()) { - asio::error_code ec; - co_await send_timer->async_wait( - asio::redirect_error(use_awaitable, ec)); - if (ec && ec != asio::error::operation_aborted) co_return; - } - - std::string data; - std::swap(data, send_buf); - - co_await asio::async_write(socket, asio::buffer(data), use_awaitable); - - // Reset timer so we wait again next iteration - send_timer->expires_at(asio::steady_timer::time_point::min()); - } - } catch (const std::exception&) { - std::cout << std::endl << "[disconnected sender from server due to exception]" << std::endl; - } - - if ( socket.is_open() ) socket.close(); - if ( !ioc.stopped() ) ioc.stop(); -} - -awaitable Session::receiver(tcp::socket& socket) { - try { - asio::streambuf buf; - - while (true) { - // Receive until a '\n' - std::size_t n = co_await asio::async_read_until( - socket, buf, '\n', use_awaitable); - - Glib::ustring line( - asio::buffers_begin(buf.data()), - asio::buffers_begin(buf.data()) + static_cast(n)); - buf.consume(n); - - // Strip \r\n and make Glib valid - while (!line.empty() && (line[line.size() - 1] == '\n' || line[line.size() - 1] == '\r')) - line.erase(line.size() - 1); - if ( !line.size() ) continue; - line.make_valid(); { - std::lock_guard lock(queue_mutex); - message_queue.push_back(std::string(line.c_str())); - } - poster(); - } - } catch (const std::exception&) { - std::cout << std::endl << "[disconnected receiver from server due to exception]" << std::endl; - } - - if ( socket.is_open() ) socket.close(); - if ( !ioc.stopped() ) ioc.stop(); - if ( disconnecter ) disconnecter(); -} - -void Session::add_to_buffer(std::string message) { - send_buf.append(message + "\n"); - send_timer->cancel(); - return; -} - -void Session::set_poster(std::function message_poster) - { poster = std::move(message_poster); } - -void Session::set_disconnecter(std::function new_disconnecter) - { disconnecter = std::move(new_disconnecter); } - -Server::Client::Client(asio::io_context& ioc, unsigned id) : - socket (ioc), - timer (ioc), - nickname ("User" + std::to_string(id)) - { timer.expires_at(asio::steady_timer::time_point::min()); } - -Server::Server(unsigned port) : - acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port))) { - asio::co_spawn(ioc, accept_loop(), asio::detached); - ioc_thread = std::thread([this](){ ioc.run(); }); - std::cout << "[server] listening on port " << port << "\n"; -} - -Server::~Server() noexcept { - asio::post(ioc, [this]() { - asio::error_code ec; - acceptor.close(ec); - for (auto& client : clients) { - if (client->socket.is_open()) client->socket.close(ec); - client->timer.cancel(ec); - } - }); - if (!ioc.stopped()) ioc.stop(); - if (ioc_thread.joinable()) ioc_thread.join(); - std::cout << "[server] stopped\n"; -} - -void Server::broadcast(const std::string& line, Client* origin) { - for (auto& client : clients) { - if (client.get() == origin) continue; - client->buf += line; - client->timer.cancel(); - } -} - -awaitable Server::client_sender(std::shared_ptr client) { - try { - while (true) { - while (true) { - if (!client->buf.empty()) break; - asio::error_code ec; - co_await client->timer.async_wait(asio::redirect_error(use_awaitable, ec)); - if (ec && ec != asio::error::operation_aborted) co_return; - } - std::string data; - std::swap(data, client->buf); - client->timer.expires_at(asio::steady_timer::time_point::min()); - co_await asio::async_write(client->socket, asio::buffer(data), use_awaitable); - } - } catch (const std::exception& e) { - std::cout << "[server] sender error: " << e.what() << "\n"; - } - asio::error_code ec; - if (client->socket.is_open()) client->socket.close(ec); -} - -awaitable Server::client_receiver(std::shared_ptr client) { - try { - asio::streambuf buf; - while (true) { - std::size_t n = co_await asio::async_read_until(client->socket, buf, '\n', use_awaitable); - - Glib::ustring line( - asio::buffers_begin(buf.data()), - asio::buffers_begin(buf.data()) + static_cast(n)); - buf.consume(n); - - while (!line.empty() && (line[line.size()-1] == '\n' || line[line.size()-1] == '\r')) - line.erase(line.size()-1); - if (line.empty()) continue; - line.make_valid(); - - std::string message { line.c_str() }; - std::cout << "[server] " << client->nickname << ": " << message << "\n"; - - client->buf += "(You): " + message + "\n"; - client->timer.cancel(); - broadcast("(" + client->nickname + "): " + message + "\n", client.get()); - } - } catch (const std::exception& e) { - std::cout << "[server] " << client->nickname << " disconnected: " << e.what() << "\n"; - } - asio::error_code ec; - if (client->socket.is_open()) client->socket.close(ec); - { clients.remove(client); } - broadcast(client->nickname + " disconnected!!!\n"); -} - -awaitable Server::accept_loop() { - while (true) { - auto client = std::make_shared(ioc, ++current_id); - asio::error_code ec; - co_await acceptor.async_accept(client->socket, asio::redirect_error(use_awaitable, ec)); - if (ec) { - std::cout << "[server] accept loop ending: " << ec.message() << "\n"; - co_return; - } - - try { - auto ep = client->socket.remote_endpoint(); - client->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); - } catch (...) { client->fingerprint = "unknown"; } - std::cout << "[server] new client: " << client->fingerprint << "\n"; - - clients.push_back(client); - broadcast(client->nickname + " connected!!!\n", client.get()); +#include +#include - asio::co_spawn(ioc, client_receiver(client), asio::detached); - asio::co_spawn(ioc, client_sender(client), asio::detached); - } -} Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { @@ -242,13 +13,13 @@ Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { try { ref_builder->add_from_file("res/gtk/chat_app.ui"); } catch(const Glib::FileError& ex) { - std::cerr << "FileError: " << ex.what() << std::endl; + std::cerr << "FileError: " << ex.what() << "\n"; throw ex; } catch(const Glib::MarkupError& ex) { - std::cerr << "MarkupError: " << ex.what() << std::endl; + std::cerr << "MarkupError: " << ex.what() << "\n"; throw ex; } catch(const Gtk::BuilderError& ex) { - std::cerr << "BuilderError: " << ex.what() << std::endl; + std::cerr << "BuilderError: " << ex.what() << "\n"; throw ex; } @@ -288,10 +59,10 @@ Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { append(*chat_scrolled); append(*footer_box); + // Add css to the default display auto css_provider = Gtk::CssProvider::create(); css_provider->load_from_path("./res/gtk/chat_app.css"); - // Add to the default display Gtk::StyleContext::add_provider_for_display( Gdk::Display::get_default(), css_provider, @@ -302,24 +73,24 @@ Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { void Chat::on_realize() { Gtk::Box::on_realize(); status_label = dynamic_cast(get_parent()->get_parent()->get_parent()->get_parent()->get_last_child()); - if (!status_label) std::cout << "Status label not found" << std::endl; if (status_label) status_label->set_text("Welcome to the LAN Chat!"); + else + std::cout << "Status label not found\n"; } inline void Chat::session_connection() { // Disconnect if connected - if (!connect_button->get_label().compare("Disconnect")) { + if ( session ) { session = nullptr; - server = nullptr; - std::cout << "\n[disconnected from server due to user request]\n"; connect_button->set_label("Connect"); footer_box->set_visible(false); ip_entry->set_sensitive(true); port_entry->set_sensitive(true); home_button->set_sensitive(true); - status_label->set_label("Disconnected from server!"); + if (status_label) + status_label->set_label("Disconnected!"); return; } @@ -329,26 +100,25 @@ inline void Chat::session_connection() { // Try to connect as client try { - session = std::make_unique(host, port); + session = std::make_unique(host, port); status_label->set_label("Connected to server " + std::string(host) + ":" + std::to_string(port) + "!"); } catch (const std::exception& e) { session = nullptr; // If not localhost, give up if ( !(host == std::string(LOCALHOST)) ) { - std::cerr << "[fatal] while creating session: " << e.what() << std::endl; - status_label->set_label("Something went wrong starting client, check cerr"); + std::cerr << "[fatal] while creating session: " << e.what() << "\n"; + if (status_label) + status_label->set_label("Something went wrong starting client, check cerr"); return; } - // Localhost and no server found — become the server, then connect to ourselves - std::cout << "[info] no server on localhost, starting one...\n"; try { - server = std::make_unique(port); - session = std::make_unique(LOCALHOST, port); - status_label->set_label("Hosting on port " + std::to_string(port) + " — waiting for peers!"); + session = std::make_unique(port); + if (status_label) + status_label->set_label("Hosting on port " + std::to_string(port) + " — waiting for peers!"); } catch (const std::exception& e2) { - std::cerr << "[fatal] could not start server: " << e2.what() << std::endl; - status_label->set_label("Could not start server, check cerr"); - server = nullptr; + std::cerr << "[fatal] could not start server: " << e2.what() << "\n"; + if (status_label) + status_label->set_label("Could not start server, check cerr"); session = nullptr; return; } @@ -361,18 +131,17 @@ inline void Chat::session_connection() { footer_box->set_visible(true); message_entry->grab_focus(); - session->set_disconnecter([this]() - { Glib::signal_idle().connect_once([this]() { session_connection(); }); }); - session->set_poster([this]() { dispatcher->emit(); }); - + session->set_disconnecter([this]() { + Glib::signal_idle().connect_once([this]() { session_connection(); }); + }); dispatcher = std::make_unique(); dispatcher->connect([this]() { - std::lock_guard lock(session->queue_mutex); - while (!session->message_queue.empty()) { - std::string message{std::move(session->message_queue.front())}; - session->message_queue.pop_front(); + std::lock_guard lock(session->receive_mutex); + while (!session->receive_queue.empty()) { + std::string message{std::move(session->receive_queue.front())}; + session->receive_queue.pop_back(); - std::cout << message << std::endl; + std::cout << message << "\n"; auto bubble {Gtk::manage(new Gtk::Label())}; bubble->set_css_classes({ "bubble" }); @@ -386,7 +155,9 @@ inline void Chat::session_connection() { bubble->add_css_class("others"); } } - bubble->set_text(std::move(message)); + Glib::ustring valid_message{std::move(message)}; + valid_message.make_valid(); + bubble->set_text(std::move(valid_message)); bubble->set_hexpand(true); bubble->set_vexpand(false); bubble->set_wrap(true); @@ -399,11 +170,12 @@ inline void Chat::session_connection() { }); } }); - return; + session->set_poster([this]() { dispatcher->emit(); }); } inline void Chat::message_buffer () { if ( !message_entry->get_text_length() ) return; - session->add_to_buffer(message_entry->get_text()); + std::string message{message_entry->get_text()}; + session->process_message(message); message_entry->delete_text(0, -1); }; diff --git a/src/fractal_app.cc b/src/fractal_app.cc index 8d76825..1b7ced2 100644 --- a/src/fractal_app.cc +++ b/src/fractal_app.cc @@ -2,10 +2,11 @@ #include #include +#include +#include #include #include -#include int mandelbrot(double cr, double ci, int max_iter, const std::vector) { @@ -20,6 +21,10 @@ int mandelbrot(double cr, double ci, int max_iter, const std::vector) } int julia(double cr, double ci, int max_iter, const std::vector consts) { + if (consts.size() != 2) { + std::cerr << "Julia fractal requires two constants, using defaults." << std::endl; + return julia(cr, ci, max_iter, {-0.5125, 0.5213}); + } double zr {cr}, zi {ci}; for (int i{}; i < max_iter; ++i) { double zr2 {zr*zr - zi*zi + std::any_cast(consts[0])}, @@ -68,14 +73,14 @@ void FractalArea::iter_to_rgb(int iter, int max_iter, uint8_t &r, uint8_t &g, ui FractalArea::FractalArea(const std::string selection) : Gtk::DrawingArea(), - selection (selection), - cx (-0.5), - cy (0.0), - range (3.5), - drag_start_cx (), - drag_start_cy (), - surface (), - fractal_map ({ { "Julia", julia }, { "Mandelbrot", mandelbrot } }) { + selection (selection), + cx (-0.5), + cy (0.0), + range (3.5), + drag_start_cx (), + drag_start_cy (), + surface (), + fractal_map ({ { "Julia", julia }, { "Mandelbrot", mandelbrot } }) { set_expand(true); set_draw_func(sigc::mem_fun(*this, &FractalArea::on_draw)); @@ -124,7 +129,7 @@ void FractalArea::on_draw(const Cairo::RefPtr& cr, int w, int h) } }}; - int band {h/NTHREADS}; + int band { h/NTHREADS }; for (int t{}; t < NTHREADS; ++t) { int y0 {t*band}, y1 {(t == NTHREADS-1) ? h : y0 + band}; @@ -171,10 +176,8 @@ FractalBox::FractalBox() : Gtk::Box(Gtk::Orientation::HORIZONTAL) { throw ex; } - // Get the GtkBuilder-instantiated nav and header: - menu = Gtk::manage(ref_builder->get_widget("menu_bar")); - // Get the GtkBuilder-instantiated buttons, and connect a signal handler + menu = Gtk::manage(ref_builder->get_widget("menu_bar")); reset_button = ref_builder->get_widget("reset_button"); save_button = ref_builder->get_widget("save_button"); fractal_dropdown = ref_builder->get_widget("fractal_dropdown"); @@ -211,6 +214,7 @@ FractalBox::FractalBox() : Gtk::Box(Gtk::Orientation::HORIZONTAL) { } ); + // Iterations scale and color scales iter_scale = ref_builder->get_widget("iter_scale"); if(iter_scale) [[likely]] { iter_scale->signal_value_changed().connect([this]{ diff --git a/src/include/browser_app.hpp b/src/include/browser_app.hpp index acdf327..6f9dfbd 100644 --- a/src/include/browser_app.hpp +++ b/src/include/browser_app.hpp @@ -16,6 +16,43 @@ class Browser : public Gtk::Box { friend class BrowserTest; friend class BrowserTest_BrowserFunctionalTest_Test; +public: + /** + * @brief Creates Browser object with all needed Widgets and signals. + * + * Creates Browser object using res/gtk/browser_app.ui file, + * Checks for file issues, gets Widgets and connects needed signals. + */ + Browser(); + + /** + * @brief Function for webView load events. + * + * Detect load event type and modify Browser widgets accordingly + * for stopping load and disabling input. + * + * @param[in] web_view: webView that fired event. + * @param[in] load_event: Load event detected. + * @param[in] user_data: User provided data, will use to pass browser object. + */ + static void web_view_load_changed(WebKitWebView *web_view, + const WebKitLoadEvent load_event, + gpointer user_data); + /** + * @brief Function for webView load in new tab events. + * + * Detect load _blank event type and modify Browser widgets accordingly + * for stopping load and disabling input. + * + * @param[in] web_view: webView that fired event. + * @param[in] action: Action detected. + * @param[in] user_data: User provided data, will use to pass browser object. + */ + static GtkWidget *on_create_cb(WebKitWebView *web_view, + WebKitNavigationAction *action, + gpointer user_data); + +private: Gtk::Label *status_label{}; WebKitWebView *web_view{}; @@ -56,40 +93,4 @@ class Browser : public Gtk::Box { * the box needing to be contained. */ void on_realize() override; - -public: - /** - * @brief Creates Browser object with all needed Widgets and signals. - * - * Creates Browser object using res/gtk/browser_app.ui file, - * Checks for file issues, gets Widgets and connects needed signals. - */ - Browser(); - - /** - * @brief Function for webView load events. - * - * Detect load event type and modify Browser widgets accordingly - * for stopping load and disabling input. - * - * @param[in] web_view: webView that fired event. - * @param[in] load_event: Load event detected. - * @param[in] user_data: User provided data, will use to pass browser object. - */ - static void web_view_load_changed(WebKitWebView *web_view, - const WebKitLoadEvent load_event, - gpointer user_data); - /** - * @brief Function for webView load in new tab events. - * - * Detect load _blank event type and modify Browser widgets accordingly - * for stopping load and disabling input. - * - * @param[in] web_view: webView that fired event. - * @param[in] action: Action detected. - * @param[in] user_data: User provided data, will use to pass browser object. - */ - static GtkWidget *on_create_cb(WebKitWebView *web_view, - WebKitNavigationAction *action, - gpointer user_data); }; diff --git a/src/include/chat_app.hpp b/src/include/chat_app.hpp index dee61a3..2e543c2 100644 --- a/src/include/chat_app.hpp +++ b/src/include/chat_app.hpp @@ -1,148 +1,9 @@ #pragma once -#include -#include - -#include - -#include -#include - -using asio::ip::tcp; -using asio::awaitable; -using asio::use_awaitable; - -constexpr const char *DEFAULT_PORT {"1234"}; -constexpr const char *LOCALHOST {"127.0.0.1"}; - - -/** - * @brief Session class containing everything needed to open an async client session. - * - * Uses a string buffer for sending, appends received messages - * to a deque and runs asio in a different thread for the app to run other things. - * - * It has a poster and disconnecter function so it can interact with GUI. - */ -class Session { - std::string send_buf; - std::string host; - unsigned port; - - asio::io_context ioc; - std::unique_ptr send_timer; - std::thread ioc_thread; - tcp::socket socket; - - std::function poster; - std::function disconnecter; - -public: - std::deque message_queue; - std::mutex queue_mutex; - - /** - * @brief Connects the session as client and starts the asio thread. - * - * @throws out_of_range if the port is out of range. - * @throws RuntimeError on connection failure. - */ - Session(const std::string&, unsigned); - - /* Stops asio io context, joins asio thread and deletes sockets. */ - ~Session() noexcept; - - - /** - * @brief Sender function to run so the session sends data. - * - * @param[in] socket: Socket connection to send data to. - */ - awaitable sender(tcp::socket& socket); - - /** - * @brief Receiver function to run so the session awaits for data. - * - * @param[in] socket: Socket connection to receive data from. - */ - awaitable receiver(tcp::socket& socket); - - /** - * @brief Adds a function for the session to run on received message. - * - * @param[in] function: Function to run on received message. - */ - void set_poster(std::function); - - /** - * @brief Adds a function for the session to run on error disconnection. - * - * @param[in] function: Function to run on error disconnection. - */ - void set_disconnecter(std::function); - - /** - * @brief Adds a string to the buffer. - * - * Adds a string to the buffer, appending the string with a '\n' at the end, - * canceling the timer for the message to be sent. - * - * @param[in] message: Message to be buffered. - */ - void add_to_buffer(std::string); -}; - - -/** - * @brief Server class that accepts clients and broadcasts messages between them. - * - * Listens on a given port, relays every incoming line to all other connected - * clients, and echoes it back to the sender tagged with "(you)". - * Runs its own ioc on a dedicated thread — same pattern as Session. - */ -class Server { - /* Client struct containing socket, timer, buffer and nickname for each client. */ - struct Client { - tcp::socket socket; - asio::steady_timer timer; - std::string nickname{}; - std::string fingerprint{}; - std::string buf{}; - - explicit Client(asio::io_context&, unsigned); - }; - - asio::io_context ioc; - tcp::acceptor acceptor; - std::thread ioc_thread{}; - unsigned current_id{}; - - std::list> clients{}; +#include "session.hpp" - /* Queue line for every client except origin, then wake their sender. */ - void broadcast(const std::string&, Client* = nullptr); - - /** - * @brief Sender function to run so the session sends data. - * - * @param[in] client: Client struct to send data to. - */ - awaitable client_sender(std::shared_ptr); - - /** - * @brief Receiver function to run so the session awaits for data. - * - * @param[in] client: Client struct to receive data from. - */ - awaitable client_receiver(std::shared_ptr); - - /* Accept loop function to run so the session accepts new clients. */ - awaitable accept_loop(); +#include -public: - explicit Server(unsigned port); - ~Server() noexcept; -}; /** @@ -155,6 +16,16 @@ class Chat : public Gtk::Box { friend class ChatTest; friend class ChatTest_ChatFunctionalTest_Test; +public: + /** + * @brief Creates Chat object with all needed Widgets and signals. + * + * Creates Chat object using res/gtk/chat_app.ui file, + * Checks for file issues, gets Widgets and connects needed signals. + */ + Chat(); + +private: Gtk::Button *home_button{}; Gtk::Button *connect_button{}; Gtk::Button *message_button{}; @@ -169,7 +40,6 @@ class Chat : public Gtk::Box { std::unique_ptr dispatcher{}; std::unique_ptr session{}; - std::unique_ptr server{}; // non-null when we are the host /** * @brief Get Main Application status label. @@ -195,13 +65,4 @@ class Chat : public Gtk::Box { * connects to it. */ inline void session_connection(); - -public: - /** - * @brief Creates Chat object with all needed Widgets and signals. - * - * Creates Chat object using res/gtk/chat_app.ui file, - * Checks for file issues, gets Widgets and connects needed signals. - */ - Chat(); }; diff --git a/src/include/fractal_app.hpp b/src/include/fractal_app.hpp index 924b2fb..2d75b75 100644 --- a/src/include/fractal_app.hpp +++ b/src/include/fractal_app.hpp @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include #include @@ -46,37 +47,17 @@ class FractalArea : public Gtk::DrawingArea { friend class FractalTest; friend class FractalTest_FractalFunctionalTest_Test; - std::string selection; - double cx{}; - double cy{}; - double range{}; - double drag_start_cx{}; - double drag_start_cy{}; - double const_r{}; - double const_i{}; - double r_lower_level{}; - double r_upper_level{}; - double g_lower_level{}; - double g_upper_level{}; - double b_lower_level{}; - double b_upper_level{}; - int max_iter{}; - - Cairo::RefPtr surface; - - std::unordered_map)>> fractal_map; - /** - * @brief Uses custom RGB intervals to draw spectrum + * @brief Draws the fractal needed into the Cairo Context. * - * @param[in] iter: Iteration to compute the RGB spectrum with. - * @param[in] max_iter: Maximum iterations available for fractal resolution. - * @param[in] r: reference to the R data to write on. - * @param[in] g: reference to the G data to write on. - * @param[in] b: reference to the B data to write on. + * Whenever the drawing area needs to redraw, this slot will be called. + * It will draw the fractal using the current settings into the provided Cairo Context with threading support. + * + * @param[in] cr: Cairo Context to draw to. + * @param[in] w: Width of the drawing area. + * @param[in] h: Height of the drawing area. */ - void iter_to_rgb(int iter, int max_iter, uint8_t &r, uint8_t &g, uint8_t &b); - + void on_draw(const Cairo::RefPtr&, int, int); public: /** @@ -88,18 +69,6 @@ class FractalArea : public Gtk::DrawingArea { */ FractalArea(const std::string selection = "Julia"); - /** - * @brief Draws the fractal needed into the Cairo Context. - * - * Whenever the drawing area needs to redraw, this slot will be called. - * It will draw the fractal using the current settings into the provided Cairo Context with threading support. - * - * @param[in] cr: Cairo Context to draw to. - * @param[in] w: Width of the drawing area. - * @param[in] h: Height of the drawing area. - */ - void on_draw(const Cairo::RefPtr&, int, int); - /** * @brief Sets the selection string. * @@ -156,6 +125,38 @@ class FractalArea : public Gtk::DrawingArea { * Resets Drawing area to it's original positions. */ void reset(); + +private: + std::string selection; + double cx{}; + double cy{}; + double range{}; + double drag_start_cx{}; + double drag_start_cy{}; + double const_r{}; + double const_i{}; + double r_lower_level{}; + double r_upper_level{}; + double g_lower_level{}; + double g_upper_level{}; + double b_lower_level{}; + double b_upper_level{}; + int max_iter{}; + + Cairo::RefPtr surface; + + std::unordered_map)>> fractal_map; + + /** + * @brief Uses custom RGB intervals to draw spectrum + * + * @param[in] iter: Iteration to compute the RGB spectrum with. + * @param[in] max_iter: Maximum iterations available for fractal resolution. + * @param[in] r: reference to the R data to write on. + * @param[in] g: reference to the G data to write on. + * @param[in] b: reference to the B data to write on. + */ + void iter_to_rgb(int iter, int max_iter, uint8_t &r, uint8_t &g, uint8_t &b); }; /** @@ -168,6 +169,16 @@ class FractalBox : public Gtk::Box { friend class FractalTest; friend class FractalTest_FractalFunctionalTest_Test; +public: + /** + * @brief Creates FractalBox object with all needed Widgets and signals. + * + * Creates FractalBox object using res/gtk/fractal_app.ui file, + * Checks for file issues, gets Widgets and connects needed signals. + */ + FractalBox(); + +private: std::string selection{}; Gtk::Box *menu{}; Gtk::Box *consts_box{}; @@ -193,13 +204,4 @@ class FractalBox : public Gtk::Box { * the box needing to be contained. */ void on_realize() override; - -public: - /** - * @brief Creates FractalBox object with all needed Widgets and signals. - * - * Creates FractalBox object using res/gtk/fractal_app.ui file, - * Checks for file issues, gets Widgets and connects needed signals. - */ - FractalBox(); }; diff --git a/src/include/main_application.hpp b/src/include/main_application.hpp index 534eee2..a88b35e 100644 --- a/src/include/main_application.hpp +++ b/src/include/main_application.hpp @@ -13,9 +13,19 @@ class MainApplication : public Gtk::Application { friend class MainApplicationTest; friend class MainApplicationTest_MainApplicationStructureTest_Test; - Gtk::ApplicationWindow *main_window{}; +public: + Gtk::Label *status_label{}; - Gtk::Box *selected_app{}; + /** + * @brief Creates RefPtr using class constructor. + * + * @return New MainApplication object RefPtr. + */ + static Glib::RefPtr create(); + +private: + Gtk::ApplicationWindow *main_window{}; + Gtk::Box *selected_app{}; /** * @brief Creates ApplicationWindow to show. @@ -27,7 +37,6 @@ class MainApplication : public Gtk::Application { */ Gtk::ApplicationWindow* create_window(); -protected: /** * @brief Creates MainApplication object with application id and handles. */ @@ -37,14 +46,4 @@ class MainApplication : public Gtk::Application { * @brief Presents main window. */ void on_activate() override; - -public: - Gtk::Label *status_label{}; - - /** - * @brief Creates RefPtr using class constructor. - * - * @return New MainApplication object RefPtr. - */ - static Glib::RefPtr create(); }; diff --git a/src/include/session.hpp b/src/include/session.hpp new file mode 100644 index 0000000..89cd8c4 --- /dev/null +++ b/src/include/session.hpp @@ -0,0 +1,224 @@ +#pragma once + +#include +#include + +#include +#include + +using asio::ip::tcp; +using asio::awaitable; +using asio::use_awaitable; + +constexpr const char *DEFAULT_PORT {"1234"}; +constexpr const char *LOCALHOST {"127.0.0.1"}; + + + +/** + * @brief Connection struct containing important information for a connection. + * + * Contains the socket, timer, nickname, fingerprint and send buffer for a connection. + * Also contains a function to add messages to the send buffer using a mutex in case needed. + */ +struct Connection { + inline static unsigned current_id{}; + + tcp::socket socket; + asio::steady_timer send_timer; + std::mutex send_mutex; + std::string nickname{}; + std::string fingerprint{}; + std::string send_buffer{}; + + /* Constructor for Connection, initializes the socket and timer, and sets the default nickname. */ + explicit Connection(asio::io_context&); + + /** + * @brief Adds a string to the buffer. + * + * Adds a string to the buffer, appending the string with a '\n' at the end, + * canceling the timer for the message to be sent. + * + * @param[in] message: Message to be buffered. + */ + void add_to_send_buffer(std::string&); +}; + + +/** + * @brief Session class containing basic structure for Client/Server. + * + * Uses a vector to store received messages and runs connection in its own thread. + * Contains a sender and receiver asio functions and has a poster and disconnecter function so it can interact with GUI. + */ +class Session { +public: + std::vector receive_queue; + std::mutex receive_mutex; + + /* Default constructor for Session. */ + Session() = default; + + /* Virtual destructor for Session. Stops ioc and joins thread. */ + virtual ~Session() noexcept; + + /* Non copyable, non movable */ + Session(const Session&) = delete; + Session& operator=(const Session&) = delete; + Session(Session&&) = delete; + Session& operator=(Session&&) = delete; + + /** + * @brief Adds a function for the session to run when a message was processed. + * + * @param[in] function: Function to run on received message. + */ + void set_poster(std::function); + + /** + * @brief Adds a function for the session to run on error disconnection. + * + * @param[in] function: Function to run on error disconnection. + */ + void set_disconnecter(std::function); + + /** + * @brief Processes a message depending on its type. + * + * @param[in] message: Message to be processed. + * @param[in] origin: Connection that sent the message. + */ + virtual void process_message(std::string&, std::shared_ptr = nullptr) = 0; + +protected: + std::string host; + unsigned port; + + asio::io_context ioc; + std::thread ioc_thread; + std::function poster; + std::function disconnecter; + + /** + * @brief Sender function to run so the session sends data. + * + * @param[in] connection: Connection struct to send data to. + */ + awaitable sender(std::shared_ptr); + + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] connection: Connection struct to receive data from. + */ + virtual awaitable receiver(std::shared_ptr); + + /** + * @brief Broadcasting method for Server and Client. + * + * @param[in] message: Message to be broadcasted. + * @param[in] origin: Connection that sent the message. + */ + virtual void broadcast(const std::string&, std::shared_ptr = nullptr) = 0; +}; + + +/** + * @brief Client class that connects to a server and sends messages. + * + * Connects to a server on a given host and port, sends messages to the server, + * and receives messages from the server. + */ +class Client : public Session { +public: + /* Constructor for Client, initializes the connection and connects to the server. */ + explicit Client(const std::string &host, unsigned port); + + /* Destructor for Client. Closes socket if needed. */ + ~Client() noexcept; + + /** + * @brief Processes a message for Client. + * + * Client posts the message and sends it to the server in case there's no origin. + * + * @param[in] message: Message to be processed. + * @param[in] origin: Connection that sent the message. + */ + void process_message(std::string&, std::shared_ptr = nullptr) override; + +private: + std::shared_ptr connection; + + /** + * @brief Receiver function to run so the connection awaits for data. + * + * It also removes closed connections. + * + * @param[in] connection: Connection struct to receive data from. + */ + awaitable receiver(std::shared_ptr) override; + + /** + * @brief Broadcasting method for Client. + * + * Broadcasts a message to the server, sending it to the server with the origin connection as nullptr. + * + * @param[in] message: Message to be broadcasted. + * @param[in] origin: nullptr. + */ + void broadcast(const std::string&, std::shared_ptr = nullptr) override; +}; + + +/** + * @brief Server class that accepts clients and broadcasts messages between them. + * + * Listens on a given port, relays every incoming line to all other connected + * clients, and echoes it back to the sender tagged with "(you)". + */ +class Server : public Session { +public: + /* Constructor for Server, initializes the acceptor and starts the accept loop. */ + explicit Server(unsigned port); + + /* Destructor for Server. Closes the acceptor and all connections. */ + ~Server() noexcept; + + /** + * @brief Processes a message for the Server. + * + * Always posts and broadcasts the message to all clients. + * + * @param[in] message: Message to be processed. + * @param[in] origin: Connection that sent the message. + */ + void process_message(std::string&, std::shared_ptr = nullptr) override; + +private: + tcp::acceptor acceptor; + + std::list> connections{}; + + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] socket: Socket connection to receive data from. + */ + awaitable receiver(std::shared_ptr) override; + + /* Accept loop function to run so the session accepts new clients. */ + awaitable accept_loop(); + + /** + * @brief Broadcasting method for Server. + * + * Broadcasts a message to all clients. + * + * @param[in] message: Message to be broadcasted. + * @param[in] origin: Connection that sent the message. + */ + void broadcast(const std::string&, std::shared_ptr = nullptr) override; +}; + diff --git a/src/session.cc b/src/session.cc new file mode 100644 index 0000000..39b4e7c --- /dev/null +++ b/src/session.cc @@ -0,0 +1,223 @@ +#include "include/session.hpp" + +#include +#include +#include + +#include +#include + + + +Connection::Connection(asio::io_context& ioc) : + socket (ioc), + send_timer (ioc), + nickname ("User" + std::to_string(++current_id)) + { send_timer.expires_at(asio::steady_timer::time_point::min()); } + +void Connection::add_to_send_buffer(std::string& message) { + send_buffer.append(message + "\n"); + send_timer.cancel(); +} + + +Session::~Session() noexcept { + poster = nullptr; + disconnecter = nullptr; + if ( !ioc.stopped() ) ioc.stop(); + if ( ioc_thread.joinable() ) ioc_thread.join(); + std::cout << "[Server] stopped\n"; +} + +awaitable Session::sender(std::shared_ptr connection) { + try { + while (true) { + while (true) { + if (!connection->send_buffer.empty()) break; + asio::error_code ec; + co_await connection->send_timer.async_wait(asio::redirect_error(use_awaitable, ec)); + if (ec && ec != asio::error::operation_aborted) co_return; + } + std::string data; + std::swap(data, connection->send_buffer); + connection->send_timer.expires_at(asio::steady_timer::time_point::min()); + co_await asio::async_write(connection->socket, asio::buffer(data), use_awaitable); + } + } catch (const std::exception& e) { + std::cout << "[Server] sender error: " << e.what() << "\n"; + } +} + +awaitable Session::receiver(std::shared_ptr connection) { + try { + asio::streambuf buf; + while (true) { + std::size_t n = co_await asio::async_read_until(connection->socket, buf, '\n', use_awaitable); + + std::string line( + asio::buffers_begin(buf.data()), + asio::buffers_begin(buf.data()) + static_cast(n) + ); + buf.consume(n); + + process_message(line, connection); + } + } catch (const std::exception& e) { + std::cout << "[Server] " << connection->nickname << " disconnected: " << e.what() << "\n"; + } +} + +void Session::set_poster(std::function message_poster) + { poster = std::move(message_poster); } + +void Session::set_disconnecter(std::function new_disconnecter) + { disconnecter = std::move(new_disconnecter); } + +Client::Client(const std::string &host, unsigned port) : + Session::Session(), + connection (std::make_shared(ioc)) { + connection->nickname = "Server"; + this->host = host; + this->port = port; + + if ( port > 65535 ) { + std::cerr << "Port out of bounds (0-65535)\n"; + throw std::out_of_range("Port out of bounds (0-65535)"); + } + connection->send_timer.expires_at(asio::steady_timer::time_point::min()); + tcp::resolver resolver(ioc); + + std::cout << "Connecting to " << host << ":" << port << "...\n"; + + asio::error_code ec; + auto endpoints = resolver.resolve(host, std::to_string(port), ec); + if (ec) { + std::cerr << "[error] resolve(): " << ec.message() + << "\nIs the server running?\n"; + throw std::runtime_error("Failed to resolve host"); + } + + asio::connect(connection->socket, endpoints, ec); + if (ec) { + std::cerr << "[error] connect(): " << ec.message() + << "\nIs the server running?\n"; + throw std::runtime_error("Failed to connect to server"); + } + + // Spawn receiver and sender + asio::co_spawn(ioc, receiver(connection), asio::detached); + asio::co_spawn(ioc, sender(connection), asio::detached); + + // Run the event loop in separate thread until ioc.stop() is called + ioc_thread = std::thread([this](){ ioc.run(); }); +} + + +Client::~Client() noexcept { + if ( connection->socket.is_open() ) + connection->socket.close(); +} + +awaitable Client::receiver(std::shared_ptr connection) { + co_await Session::receiver(connection); + if ( connection->socket.is_open() ) + connection->socket.close(); + if (disconnecter) disconnecter(); +} + +void Client::broadcast(const std::string& message, std::shared_ptr origin) { + (void) origin; + connection->send_buffer += message + "\n"; + connection->send_timer.cancel(); +} + +void Client::process_message(std::string &message, std::shared_ptr origin) { + while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) + message.erase(message.size()-1); + + if ( origin ) { + std::lock_guard lock(receive_mutex); + receive_queue.push_back(message); + if (poster) poster(); + } + else { + broadcast(message, origin); + } +} + + +Server::Server(unsigned port) : Session::Session(), + acceptor (tcp::acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port)))) { + host = "Server"; + asio::co_spawn(ioc, accept_loop(), asio::detached); + ioc_thread = std::thread([this](){ ioc.run(); }); + std::cout << "[Server] listening on port " << port << "\n"; +} + +Server::~Server() noexcept { + acceptor.close(); +} + +awaitable Server::receiver(std::shared_ptr connection) { + co_await Session::receiver(connection); + if ( connection->socket.is_open() ) + connection->socket.close(); + connections.remove(connection); + std::string farewell{connection->nickname + " has left the chat!!!"}; + process_message(farewell); + if (disconnecter) disconnecter(); +} + +awaitable Server::accept_loop() { + while (true) { + auto connection = std::make_shared(ioc); + asio::error_code ec; + co_await acceptor.async_accept(connection->socket, asio::redirect_error(use_awaitable, ec)); + if (ec) { + std::cout << "[server] accept loop ending: " << ec.message() << "\n"; + co_return; + } + + try { + auto ep = connection->socket.remote_endpoint(); + connection->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); + } catch (...) { connection->fingerprint = "unknown"; } + std::cout << "[server] new client: " << connection->nickname << " (" << connection->fingerprint << ")\n"; + + std::string message {connection->nickname + " says Hi!!!\n"}; + process_message(message); + + asio::co_spawn(ioc, receiver(connection), asio::detached); + asio::co_spawn(ioc, sender(connection), asio::detached); + + connections.push_back(connection); + } +} + +void Server::broadcast(const std::string& message, std::shared_ptr origin) { + for (auto& connection : connections) { + std::lock_guard send_lock(connection->send_mutex); + if ( !origin ) + connection->send_buffer += message + "\n"; + else if ( connection->fingerprint == origin->fingerprint ) + connection->send_buffer += "(You): " + message + "\n"; + else + connection->send_buffer += "(" + origin->nickname + "): " + message + "\n"; + connection->send_timer.cancel(); + } +} + +void Server::process_message(std::string &message, std::shared_ptr connection) { + (void) connection; + while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) + message.erase(message.size()-1); + + broadcast(message, connection); + + if ( connection ) + message = "(" + connection->nickname + "): " + message; + + std::lock_guard lock(receive_mutex); + receive_queue.push_back(message); + if (poster) poster(); +}