diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-04-11 04:12:56 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-04-11 04:12:56 +0800 |
| commit | 5efb7400b96fa112c38009b18e2a48620f328148 (patch) | |
| tree | 85f8065b8bb2aa03e940a71cb1fbf511d571a812 | |
| parent | 617b10ded3722a8c2d615fbee5dd256f5f3c6a7f (diff) | |
| download | rwebbrowser-5efb7400b96fa112c38009b18e2a48620f328148.tar.gz rwebbrowser-5efb7400b96fa112c38009b18e2a48620f328148.tar.xz | |
feat: implement interactive TUI with scrolling and add OpenSSL dependency support
| -rw-r--r-- | include/ricochet/render/renderer.hh | 2 | ||||
| -rw-r--r-- | include/ricochet/tui/terminal.hh | 45 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | src/core/browser.cc | 65 | ||||
| -rw-r--r-- | src/render/renderer.cc | 21 | ||||
| -rw-r--r-- | src/tui/terminal.cc | 62 |
6 files changed, 177 insertions, 27 deletions
diff --git a/include/ricochet/render/renderer.hh b/include/ricochet/render/renderer.hh index 8a5c463..f26a788 100644 --- a/include/ricochet/render/renderer.hh +++ b/include/ricochet/render/renderer.hh @@ -37,7 +37,7 @@ public: * @brief Traverse the DOM tree and output the formatted result to the terminal using ANSI escape codes * @param node The root node of the DOM tree */ - void render( const parser::DomNode& node ) const; + std::string render( const parser::DomNode& node ) const; }; } // namespace ricochet::render
\ No newline at end of file diff --git a/include/ricochet/tui/terminal.hh b/include/ricochet/tui/terminal.hh new file mode 100644 index 0000000..5388845 --- /dev/null +++ b/include/ricochet/tui/terminal.hh @@ -0,0 +1,45 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <termios.h> + +namespace ricochet::tui { + +class Terminal { +public: + Terminal(); + ~Terminal(); + + Terminal(const Terminal&) = delete; + Terminal& operator=(const Terminal&) = delete; + Terminal(Terminal&&) = delete; + Terminal& operator=(Terminal&&) = delete; + + // Read a single key + char read_key() const; + + // Clear screen and move cursor to top-left + void clear_screen() const; + +private: + struct termios original_termios_{}; // Save terminal original state +}; + +} // namespace ricochet::tui
\ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9ddc717..dcc8f49 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,8 +3,17 @@ file(GLOB_RECURSE ALL_SOURCES CONFIGURE_DEPENDS "*.cc") set(MAIN_SRC "${CMAKE_CURRENT_SOURCE_DIR}/main.cc") list(REMOVE_ITEM ALL_SOURCES ${MAIN_SRC}) +find_package(OpenSSL REQUIRED) +message(STATUS "Found OpenSSL Version: ${OPENSSL_VERSION}") +message(STATUS "OpenSSL Include dir: ${OPENSSL_INCLUDE_DIR}") + add_library(ricochet_core STATIC ${ALL_SOURCES}) target_include_directories(ricochet_core PUBLIC ${PROJECT_SOURCE_DIR}/include) +target_link_libraries(ricochet_core + PUBLIC + OpenSSL::SSL + OpenSSL::Crypto +) add_executable(ricochet ${MAIN_SRC}) target_link_libraries(ricochet PRIVATE ricochet_core) diff --git a/src/core/browser.cc b/src/core/browser.cc index 31f4df0..0636de6 100644 --- a/src/core/browser.cc +++ b/src/core/browser.cc @@ -21,23 +21,11 @@ #include "ricochet/parser/html_lexer.hh" #include "ricochet/parser/tree_builder.hh" #include "ricochet/render/renderer.hh" +#include "ricochet/tui/terminal.hh" +#include <cstddef> #include <iostream> +#include <sstream> -// namespace { -// void print_dom_tree( const ricochet::parser::DomNode& node, std::size_t depth = 0 ) // NOLINT(misc-no-recursion) -// { -// const std::string indent( depth * 2, ' ' ); - -// if ( node.tag_name.empty() ) { -// std::cout << indent << "\"" << node.text_content << "\"\n"; -// } else { -// std::cout << indent << "<" << node.tag_name << ">\n"; -// for ( const auto& child : node.children ) { -// print_dom_tree( child, depth + 1 ); -// } -// } -// } -// } // namespace namespace ricochet::core { int Browser::run( std::string_view initial_url ) @@ -63,8 +51,53 @@ int Browser::run( std::string_view initial_url ) // std::cout << "\033[2J\033[1;1H"; const render::Renderer renderer; - renderer.render( dom_root ); + const std::string rendered_text = renderer.render( dom_root ); + std::vector<std::string> lines; + std::stringstream ss(rendered_text); + std::string line; + while (std::getline(ss, line, '\n')) { + lines.push_back(line); + } + + // TUI Event Loop + const tui::Terminal terminal; + + std::size_t scroll_y = 0; + const std::size_t screen_height = 24; + + while (true) { + terminal.clear_screen(); + + for (std::size_t i = 0; i < screen_height - 1; ++i) { + const std::size_t line_idx = scroll_y + i; + if (line_idx < lines.size()) { + std::cout << lines[line_idx] << "\r\n"; + } else { + std::cout << "~\r\n"; + } + } + + std::cout << "\033[7m" << " URL: " << initial_url + << " | [j] Down [k] Up [q] Quit " << "\033[0m"; + std::cout.flush(); + + const char c = terminal.read_key(); + if (c == 'q') { + break; // Break loop, terminal destructor will restore terminal + } + if (c == 'j') { + if (scroll_y + screen_height - 1 < lines.size()) { + scroll_y++; // Scroll down + } + } else if (c == 'k') { + if (scroll_y > 0) { + scroll_y--; // Scroll up + } + } + } + + terminal.clear_screen(); return 0; } diff --git a/src/render/renderer.cc b/src/render/renderer.cc index 81c2be2..0a91d49 100644 --- a/src/render/renderer.cc +++ b/src/render/renderer.cc @@ -23,7 +23,7 @@ namespace ricochet::render { namespace { -void render_node( const parser::DomNode& node ) // NOLINT(misc-no-recursion) +void render_node( const parser::DomNode& node, std::string& output ) // NOLINT(misc-no-recursion) { if ( node.tag_name == "head" || node.tag_name == "style" || node.tag_name == "script" || node.tag_name == "meta" || node.tag_name == "title" || node.tag_name == "link" ) { @@ -31,35 +31,36 @@ void render_node( const parser::DomNode& node ) // NOLINT(misc-no-recursion) } if ( node.tag_name.empty() ) { - std::cout << node.text_content; + output += node.text_content; return; } if ( node.tag_name == "h1" ) { - std::cout << "\n\n\033[1;31m"; + output += "\n\n\033[1;31m"; } else if ( node.tag_name == "a" ) { std::cout << "\033[4;34m"; } else if ( node.tag_name == "p" || node.tag_name == "div" ) { - std::cout << "\n"; + output += "\n"; } for ( const auto& child : node.children ) { - render_node( child ); + render_node( child, output ); } if ( node.tag_name == "h1" || node.tag_name == "a" ) { - std::cout << "\033[0m"; + output += "\033[0m"; } if ( node.tag_name == "h1" || node.tag_name == "p" || node.tag_name == "div" ) { - std::cout << "\n"; + output += "\n"; } } } // namespace -void Renderer::render( const parser::DomNode& node ) const // NOLINT(readability-convert-member-functions-to-static) +std::string Renderer::render( const parser::DomNode& node ) const // NOLINT(readability-convert-member-functions-to-static) { - render_node( node ); - std::cout << "\n"; + std::string output; + render_node( node, output ); + return output; } } // namespace ricochet::render
\ No newline at end of file diff --git a/src/tui/terminal.cc b/src/tui/terminal.cc new file mode 100644 index 0000000..8fdb585 --- /dev/null +++ b/src/tui/terminal.cc @@ -0,0 +1,62 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/tui/terminal.hh" +#include <cstdlib> +#include <iostream> +#include <unistd.h> + +namespace ricochet::tui { + +Terminal::Terminal() { + // get terminal attributes + tcgetattr(STDIN_FILENO, &original_termios_); + + struct termios raw = original_termios_; + + // close echo, close canonical mode + // close terminal signals (ISIG disable Ctrl+C and Ctrl+Z, let the program handle it) + raw.c_lflag &= ~(ECHO | ICANON | ISIG); + + // close software flow control (Ctrl+S, Ctrl+Q) + raw.c_iflag &= ~(IXON | ICRNL); + + // apply new terminal attributes + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); +} + +Terminal::~Terminal() { + // RAII: restore terminal attributes when program exits + tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios_); +} + +char Terminal::read_key() const { // NOLINT(readability-convert-member-functions-to-static) + char c = '\0'; + // read 1 byte + if (read(STDIN_FILENO, &c, 1) == -1) { + return '\0'; + } + return c; +} + +void Terminal::clear_screen() const { // NOLINT(readability-convert-member-functions-to-static) + // ANSI escape codes: \x1b[2J clear screen, \x1b[H move cursor to (1,1) + std::cout << "\x1b[2J\x1b[H"; +} + +} // namespace ricochet::tui
\ No newline at end of file |
