aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-11 05:36:22 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-11 05:36:22 +0800
commit4ef9c771444943008bb990343127a7d659fd6df2 (patch)
tree560b1c93f1950ddc5540999eae9b8d446e384470
parent5efb7400b96fa112c38009b18e2a48620f328148 (diff)
downloadrwebbrowser-4ef9c771444943008bb990343127a7d659fd6df2.tar.gz
rwebbrowser-4ef9c771444943008bb990343127a7d659fd6df2.tar.xz
feat: implement SSLSocket for encrypted connections and add terminal size detection utility
-rw-r--r--include/ricochet/net/ssl_socket.hh93
-rw-r--r--include/ricochet/tui/terminal.hh31
-rw-r--r--src/core/browser.cc32
-rw-r--r--src/net/http_client.cc135
-rw-r--r--src/net/ssl_socket.cc118
-rw-r--r--src/render/renderer.cc38
-rw-r--r--src/tui/terminal.cc71
7 files changed, 439 insertions, 79 deletions
diff --git a/include/ricochet/net/ssl_socket.hh b/include/ricochet/net/ssl_socket.hh
new file mode 100644
index 0000000..36a92b6
--- /dev/null
+++ b/include/ricochet/net/ssl_socket.hh
@@ -0,0 +1,93 @@
+/*
+ * 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 "sys/address.hh"
+#include "sys/socket.hh"
+#include <memory>
+#include <string>
+#include <string_view>
+
+extern "C" {
+using SSL_CTX = struct ssl_ctx_st;
+using SSL = struct ssl_st;
+}
+
+namespace ricochet::net {
+
+struct SslCtxDeleter
+{
+ void operator()( SSL_CTX* ptr ) const;
+};
+struct SslDeleter
+{
+ void operator()( SSL* ptr ) const;
+};
+
+using UniqueSslCtx = std::unique_ptr<SSL_CTX, SslCtxDeleter>;
+using UniqueSsl = std::unique_ptr<SSL, SslDeleter>;
+
+/**
+ * @brief Socket wrapper that supports TLS/SSL encrypted handshake
+ */
+class SSLSocket
+{
+public:
+ SSLSocket();
+ ~SSLSocket() = default;
+
+ SSLSocket( const SSLSocket& ) = delete;
+ SSLSocket& operator=( const SSLSocket& ) = delete;
+ SSLSocket( SSLSocket&& ) = delete;
+ SSLSocket& operator=( SSLSocket&& ) = delete;
+
+ /**
+ * @brief Connect to a remote host using TLS/SSL
+ * @param host The hostname to connect to
+ * @param port The port to connect to
+ * @return True if the connection was successful, false otherwise
+ */
+ void connect( const Address& address, const std::string& hostname );
+
+ /**
+ * @brief Send data over the TLS/SSL connection
+ * @param data The data to send
+ * @return The number of bytes sent, or -1 if an error occurred
+ */
+ std::size_t write( std::string_view data );
+
+ /**
+ * @brief Receive data from the TLS/SSL connection
+ * @param buffer The buffer to store the received data
+ */
+ void read( std::string& buffer );
+
+ /**
+ * @brief Check if the TLS/SSL connection is closed
+ * @return True if the connection is closed, false otherwise
+ */
+ bool eof() const;
+
+private:
+ TCPSocket underlying_socket_ {}; // TCP Socket
+ UniqueSslCtx ctx_ { nullptr }; // OpenSSL global context
+ UniqueSsl ssl_ { nullptr }; // Current connection's SSL state
+ bool eof_ { false }; // EOF flag
+};
+} // namespace ricochet::net \ No newline at end of file
diff --git a/include/ricochet/tui/terminal.hh b/include/ricochet/tui/terminal.hh
index 5388845..59ca298 100644
--- a/include/ricochet/tui/terminal.hh
+++ b/include/ricochet/tui/terminal.hh
@@ -19,27 +19,32 @@
#pragma once
#include <termios.h>
+#include <utility>
namespace ricochet::tui {
-class Terminal {
+class Terminal
+{
public:
- Terminal();
- ~Terminal();
+ Terminal();
+ ~Terminal();
- Terminal(const Terminal&) = delete;
- Terminal& operator=(const Terminal&) = delete;
- Terminal(Terminal&&) = delete;
- Terminal& operator=(Terminal&&) = delete;
+ 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;
+ // Read a single key
+ char read_key() const;
+
+ // Clear screen and move cursor to top-left
+ void clear_screen() const;
+
+ // Get terminal size
+ std::pair<std::size_t, std::size_t> get_size() const;
private:
- struct termios original_termios_{}; // Save terminal original state
+ struct termios original_termios_ {}; // Save terminal original state
};
} // namespace ricochet::tui \ No newline at end of file
diff --git a/src/core/browser.cc b/src/core/browser.cc
index 0636de6..650ebcf 100644
--- a/src/core/browser.cc
+++ b/src/core/browser.cc
@@ -54,44 +54,48 @@ int Browser::run( std::string_view initial_url )
const std::string rendered_text = renderer.render( dom_root );
std::vector<std::string> lines;
- std::stringstream ss(rendered_text);
+ std::stringstream ss( rendered_text );
std::string line;
- while (std::getline(ss, line, '\n')) {
- lines.push_back(line);
+ while ( std::getline( ss, line, '\n' ) ) {
+ lines.push_back( line );
}
+ std::cout << "DEBUG: Status Code: " << response.status_code << "\r\n";
+ std::cout << "DEBUG: Raw Body Length: " << response.body.length() << "\r\n";
+
// TUI Event Loop
const tui::Terminal terminal;
std::size_t scroll_y = 0;
- const std::size_t screen_height = 24;
- while (true) {
+ while ( true ) {
terminal.clear_screen();
- for (std::size_t i = 0; i < screen_height - 1; ++i) {
+ const auto [term_width, term_height] = terminal.get_size();
+ const std::size_t usable_height = ( term_height > 1 ) ? ( term_height - 1 ) : 1;
+
+ for ( std::size_t i = 0; i < usable_height; ++i ) {
const std::size_t line_idx = scroll_y + i;
- if (line_idx < lines.size()) {
+ 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 << "\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') {
+ if ( c == 'q' ) {
break; // Break loop, terminal destructor will restore terminal
}
- if (c == 'j') {
- if (scroll_y + screen_height - 1 < lines.size()) {
+ if ( c == 'j' ) {
+ if ( scroll_y + usable_height < lines.size() ) {
scroll_y++; // Scroll down
}
- } else if (c == 'k') {
- if (scroll_y > 0) {
+ } else if ( c == 'k' ) {
+ if ( scroll_y > 0 ) {
scroll_y--; // Scroll up
}
}
diff --git a/src/net/http_client.cc b/src/net/http_client.cc
index b31073c..b31976f 100644
--- a/src/net/http_client.cc
+++ b/src/net/http_client.cc
@@ -17,6 +17,7 @@
*/
#include "ricochet/net/http_client.hh"
+#include "ricochet/net/ssl_socket.hh"
#include "ricochet/net/sys/address.hh"
#include "ricochet/net/sys/socket.hh"
@@ -37,13 +38,17 @@ struct ParsedUrl
std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
{
- const std::string_view http_prefix = "http://";
+ std::string_view prefix = "http://";
- if ( !url.starts_with( http_prefix ) ) {
- return std::unexpected( "Only 'http://' URLs are supported in MVP. (e.g., http://example.com)" );
+ if ( url.starts_with( "https://" ) ) {
+ prefix = "https://";
+ } else if ( url.starts_with( "http://" ) ) {
+ prefix = "http://";
+ } else {
+ return std::unexpected( "Only 'http://' or 'https://' URLs are supported." );
}
- url.remove_prefix( http_prefix.length() );
+ url.remove_prefix( prefix.length() );
auto slash_pos = url.find( '/' );
ParsedUrl result;
@@ -57,6 +62,77 @@ std::expected<ParsedUrl, std::string> parse_url( std::string_view url )
return result;
}
+
+std::string dechunk( std::string_view body ) // NOLINT(readability-convert-member-functions-to-static)
+{
+ std::string result;
+ std::size_t pos = 0;
+ while ( pos < body.size() ) {
+ // find hex length end with \r\n
+ auto line_end = body.find( "\r\n", pos );
+ if ( line_end == std::string_view::npos ) {
+ break;
+ }
+
+ // parse hex length
+ const std::string len_str( body.substr( pos, line_end - pos ) );
+ unsigned int chunk_size = 0;
+ try {
+ chunk_size = std::stoul( len_str, nullptr, 16 );
+ } catch ( ... ) {
+ break;
+ }
+
+ if ( chunk_size == 0 ) {
+ break; // end of stream
+ }
+
+ // extract data block (skip \r\n)
+ pos = line_end + 2;
+ if ( pos + chunk_size > body.size() ) {
+ break;
+ }
+ result.append( body.substr( pos, chunk_size ) );
+
+ // skip data block's \r\n
+ pos += chunk_size + 2;
+ }
+ return result;
+}
+
+std::string remove_html_comments( std::string html ) // NOLINT(readability-convert-member-functions-to-static)
+{
+ std::size_t start_pos = 0;
+ while ( ( start_pos = html.find( "<!--", start_pos ) ) != std::string::npos ) {
+ auto end_pos = html.find( "-->", start_pos );
+ if ( end_pos != std::string::npos ) {
+ html.erase( start_pos, end_pos - start_pos + 3 );
+ } else {
+ html.erase( start_pos );
+ break;
+ }
+ }
+ return html;
+}
+
+std::string remove_tag_blocks( std::string html, const std::string& tag_name ) // NOLINT
+{
+ const std::string open_tag = "<" + tag_name;
+ const std::string close_tag = "</" + tag_name + ">";
+ std::size_t start_pos = 0;
+
+ while ( ( start_pos = html.find( open_tag, start_pos ) ) != std::string::npos ) {
+ const std::size_t end_pos = html.find( close_tag, start_pos );
+ if ( end_pos != std::string::npos ) {
+ html.erase( start_pos, end_pos - start_pos + close_tag.length() );
+ } else {
+ html.erase( start_pos );
+ break;
+ }
+ }
+ return html;
+}
+
} // namespace
std::expected<HttpResponse, std::string>
@@ -69,30 +145,46 @@ HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static)
}
auto [host, path] = parsed_url.value();
+ const bool is_https = url.starts_with( "https://" );
+ const std::string service = is_https ? "https" : "http";
try {
- const Address server_addr( host, "http" );
-
- TCPSocket socket;
- socket.connect( server_addr );
+ const Address server_addr( host, service );
const std::string request = std::format( "GET {} HTTP/1.1\r\n"
"Host: {}\r\n"
- "User-Agent: Ricochet/0.1.0\r\n"
+ "User-Agent: Mozilla/5.0 (Ricochet/0.1.0)\r\n"
+ "Accept: text/html\r\n"
+ "Accept-Encoding: identity\r\n"
"Connection: close\r\n\r\n",
path,
host );
- size_t bytes_written = 0;
- while ( bytes_written < request.size() ) {
- bytes_written += socket.write( request.substr( bytes_written ) );
- }
-
std::string raw_response;
- while ( !socket.eof() ) {
- std::string buffer;
- socket.read( buffer );
- raw_response += buffer;
+ if ( is_https ) {
+ SSLSocket ssl_socket;
+ ssl_socket.connect( server_addr, host );
+ size_t bytes_written = 0;
+ while ( bytes_written < request.size() ) {
+ bytes_written += ssl_socket.write( request.substr( bytes_written ) );
+ }
+ while ( !ssl_socket.eof() ) {
+ std::string buffer;
+ ssl_socket.read( buffer );
+ raw_response += buffer;
+ }
+ } else {
+ TCPSocket tcp_socket;
+ tcp_socket.connect( server_addr );
+ size_t bytes_written = 0;
+ while ( bytes_written < request.size() ) {
+ bytes_written += tcp_socket.write( request.substr( bytes_written ) );
+ }
+ while ( !tcp_socket.eof() ) {
+ std::string buffer;
+ tcp_socket.read( buffer );
+ raw_response += buffer;
+ }
}
HttpResponse response;
@@ -111,6 +203,13 @@ HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static)
response.body = raw_response;
}
+ if ( raw_response.contains( "Transfer-Encoding: chunked" ) ) {
+ response.body = dechunk( response.body );
+ }
+
+ response.body = remove_html_comments( response.body );
+ response.body = remove_tag_blocks( response.body, "script" );
+ response.body = remove_tag_blocks( response.body, "style" );
return response;
} catch ( const std::exception& e ) {
return std::unexpected( std::string( "Network Error: " ) + e.what() );
diff --git a/src/net/ssl_socket.cc b/src/net/ssl_socket.cc
new file mode 100644
index 0000000..a31b564
--- /dev/null
+++ b/src/net/ssl_socket.cc
@@ -0,0 +1,118 @@
+/*
+ * 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/net/ssl_socket.hh"
+#include <iostream>
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+#include <stdexcept>
+
+namespace ricochet::net {
+
+// --- RAII Deleters ---
+
+void SslCtxDeleter::operator()( SSL_CTX* ptr ) const
+{
+ if ( ptr != nullptr ) {
+ SSL_CTX_free( ptr );
+ }
+}
+
+void SslDeleter::operator()( SSL* ptr ) const
+{
+ if ( ptr != nullptr ) {
+ SSL_free( ptr );
+ }
+}
+
+// SSLSocket Implementation
+
+SSLSocket::SSLSocket()
+{
+ const SSL_METHOD* method = TLS_client_method();
+ ctx_.reset( SSL_CTX_new( method ) );
+ if ( !ctx_ ) {
+ throw std::runtime_error( "Failed to create SSL_CTX" );
+ }
+
+ if ( SSL_CTX_set_default_verify_paths( ctx_.get() ) != 1 ) {
+ throw std::runtime_error( "Failed to load default CA certificates" );
+ }
+}
+
+void SSLSocket::connect( const Address& address, const std::string& hostname )
+{
+ // TCP Connect
+ underlying_socket_.connect( address );
+
+ ssl_.reset( SSL_new( ctx_.get() ) );
+ if ( !ssl_ ) {
+ throw std::runtime_error( "Failed to create SSL object" );
+ }
+
+ // Setup SNI (Server Name Indication)
+ if ( SSL_set_tlsext_host_name( ssl_.get(), hostname.c_str() ) != 1 ) {
+ // Some old servers do not support SNI, here we can ignore the error or throw an exception, depending on the
+ // requirements
+ std::cerr << "Warning: Failed to set SNI" << '\n';
+ }
+
+ SSL_set_fd( ssl_.get(), underlying_socket_.fd_num() );
+
+ if ( SSL_connect( ssl_.get() ) != 1 ) {
+ throw std::runtime_error( "TLS Handshake failed!" );
+ }
+}
+
+std::size_t SSLSocket::write( std::string_view data )
+{
+ if ( !ssl_ ) {
+ return 0;
+ }
+
+ const int bytes_written = SSL_write( ssl_.get(), data.data(), static_cast<int>( data.size() ) );
+ if ( bytes_written <= 0 ) {
+ throw std::runtime_error( "SSL write failed" );
+ }
+ return static_cast<std::size_t>( bytes_written );
+}
+
+void SSLSocket::read( std::string& buffer )
+{
+ if ( !ssl_ ) {
+ return;
+ }
+
+ std::array<char, 4096> buf {};
+ // Read decrypted data from the encrypted channel
+ const int bytes_read = SSL_read( ssl_.get(), buf.data(), static_cast<int>( buf.size() ) );
+
+ if ( bytes_read > 0 ) {
+ buffer.append( buf.data(), static_cast<std::size_t>( bytes_read ) );
+ } else {
+ // Read 0 or negative, representing the server closed the connection (EOF)
+ eof_ = true;
+ }
+}
+
+bool SSLSocket::eof() const
+{
+ return eof_;
+}
+
+} // namespace ricochet::net \ No newline at end of file
diff --git a/src/render/renderer.cc b/src/render/renderer.cc
index 0a91d49..2115ee1 100644
--- a/src/render/renderer.cc
+++ b/src/render/renderer.cc
@@ -23,6 +23,25 @@ namespace ricochet::render {
namespace {
+std::string decode_entities( std::string text ) // NOLINT(readability-convert-member-functions-to-static)
+{
+ constexpr std::array<std::pair<std::string_view, std::string_view>, 24> entities
+ = { { { "&nbsp;", " " }, { "&lt;", "<" }, { "&gt;", ">" }, { "&amp;", "&" }, { "&quot;", "\"" },
+ { "&copy;", "©" }, { "&reg;", "®" }, { "&trade;", "™" }, { "&euro;", "€" }, { "&pound;", "£" },
+ { "&yen;", "¥" }, { "&cent;", "¢" }, { "&sect;", "§" }, { "&para;", "¶" }, { "&hellip;", "…" },
+ { "&dagger;", "†" }, { "&Dagger;", "‡" }, { "&bull;", "•" }, { "&prime;", "′" }, { "&Prime;", "″" },
+ { "&lsaquo;", "‹" }, { "&rsaquo;", "›" }, { "&oline;", "‾" }, { "&frasl;", "⁄" } } };
+
+ for ( const auto& [entity, replacement] : entities ) {
+ std::size_t pos = 0;
+ while ( ( pos = text.find( entity, pos ) ) != std::string::npos ) {
+ text.replace( pos, entity.length(), replacement );
+ pos += replacement.length();
+ }
+ }
+ return text;
+}
+
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"
@@ -31,21 +50,27 @@ void render_node( const parser::DomNode& node, std::string& output ) // NOLINT(m
}
if ( node.tag_name.empty() ) {
- output += node.text_content;
+ output += decode_entities( node.text_content );
return;
}
- if ( node.tag_name == "h1" ) {
+
+ const bool is_header
+ = ( node.tag_name == "h1" || node.tag_name == "h2" || node.tag_name == "h3" || node.tag_name == "h4" );
+
+ if ( is_header ) {
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" ) {
- output += "\n";
+ } else if ( node.tag_name == "p" || node.tag_name == "div" || is_header ) {
+ if ( !output.empty() && output.back() != '\n' ) {
+ output += "\n";
+ }
}
for ( const auto& child : node.children ) {
render_node( child, output );
}
- if ( node.tag_name == "h1" || node.tag_name == "a" ) {
+ if ( is_header || node.tag_name == "a" ) {
output += "\033[0m";
}
@@ -56,7 +81,8 @@ void render_node( const parser::DomNode& node, std::string& output ) // NOLINT(m
} // namespace
-std::string Renderer::render( const parser::DomNode& node ) const // NOLINT(readability-convert-member-functions-to-static)
+std::string Renderer::render( // NOLINT(readability-convert-member-functions-to-static)
+ const parser::DomNode& node ) const
{
std::string output;
render_node( node, output );
diff --git a/src/tui/terminal.cc b/src/tui/terminal.cc
index 8fdb585..75c6c1f 100644
--- a/src/tui/terminal.cc
+++ b/src/tui/terminal.cc
@@ -19,44 +19,59 @@
#include "ricochet/tui/terminal.hh"
#include <cstdlib>
#include <iostream>
+#include <sys/ioctl.h>
#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()
+{
+ // 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_ );
}
-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;
}
-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";
}
-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";
+std::pair<std::size_t, std::size_t> Terminal::get_size() // NOLINT(readability-convert-member-functions-to-static)
+ const
+{
+ struct winsize w {};
+ if ( ioctl( STDOUT_FILENO, TIOCGWINSZ, &w ) == -1 ) {
+ return { 80, 24 };
+ }
+ return { static_cast<std::size_t>( w.ws_col ), static_cast<std::size_t>( w.ws_row ) };
}
} // namespace ricochet::tui \ No newline at end of file