Compare commits

...

17 Commits

Author SHA1 Message Date
a577a3c254 Remove get for operator 2025-01-24 14:20:22 +01:00
bd5fe465a7 Better output 2025-01-24 14:20:03 +01:00
ea56f6ee0a Format 2025-01-24 14:07:33 +01:00
e0eba95ff4 Use snake_case 2025-01-24 14:06:26 +01:00
51943a5123 Use hpp 2025-01-24 14:05:10 +01:00
0014abc9c7 Add caching 2025-01-24 14:04:42 +01:00
e91759c70d Add no-pretty 2025-01-24 13:43:10 +01:00
8b0447668b Use correct number type 2025-01-24 13:37:58 +01:00
95c409ad95 catch 2025-01-24 13:06:05 +01:00
ebfda949ea Catch no number exception 2025-01-24 13:00:30 +01:00
984d1f3aed headers 2025-01-24 12:59:22 +01:00
8710c31c1f Use char 2025-01-24 12:57:49 +01:00
f4eeb3d0e1 Don't crash on empty pages 2025-01-24 12:57:27 +01:00
40d98cdbb6 Command done 2025-01-24 12:35:54 +01:00
28613af006 Remove unused + cosnt and so on 2025-01-23 16:29:28 +01:00
c09929d2f7 Formatting 2025-01-22 14:04:12 +01:00
360a85b7bf Only one subpage 2025-01-22 13:36:57 +01:00
13 changed files with 491 additions and 60 deletions

View File

@ -7,6 +7,12 @@ add_executable(${PROJECT_NAME}
Page.cpp Page.cpp
stringutil.hpp stringutil.hpp
stringutil.cpp stringutil.cpp
Tui.cpp
Tui.hpp
Command.hpp
Command.cpp
Pager.cpp
Pager.hpp
) )
target_link_libraries(${PROJECT_NAME} PRIVATE LibXml2::LibXml2 cpr::cpr) target_link_libraries(${PROJECT_NAME} PRIVATE LibXml2::LibXml2 cpr::cpr)

59
src/Command.cpp Normal file
View File

@ -0,0 +1,59 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#include "Command.hpp"
#include <iostream>
namespace Command {
Command readCommand() {
if (!std::cin)
return {Exit{}};
std::string line;
if (!std::getline(std::cin, line))
return {Exit{}};
// Trim leading and trailing whitespaces
constexpr std::string_view TO_TRIM = " \t\n\r";
const size_t start = line.find_first_not_of(TO_TRIM);
const size_t end = line.find_last_not_of(TO_TRIM);
if (start == std::string::npos) return None{};
const std::string_view line_trimmed(line.data() + start, end - start + 1);
using namespace std::string_view_literals;
if (line_trimmed == "r"sv || line_trimmed == "refresh"sv)
return Refresh{};
if (line_trimmed == "n"sv || line_trimmed == ">"sv || line_trimmed == "next"sv)
return Next{};
if (line_trimmed == "p"sv || line_trimmed == "<"sv || line_trimmed == "previous"sv)
return Previous{};
if (line_trimmed == "x"sv || line_trimmed == "exit"sv || line_trimmed == "q"sv || line_trimmed == "quit"sv)
return Exit{};
if (line_trimmed == "help"sv || line_trimmed == "?"sv || line_trimmed == "?"sv)
return Help{};
if (line_trimmed.starts_with("seek") || line.starts_with("s ")) {
const size_t space = line_trimmed.find(' ');
if (space == std::string::npos)
throw NoNumberException();
int number = 0;
const size_t size = line.size();
for (size_t i = space + 1; i < size; ++i) {
const char c = line_trimmed[i];
if (!std::isdigit(c))
throw NoNumberException();
number = number * 10 + (c - '0');
}
return {Seek{number}};
}
return {None{}};
}
}

49
src/Command.hpp Normal file
View File

@ -0,0 +1,49 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#pragma once
#include <string>
#include <variant>
namespace Command {
class NoNumberException final : public std::exception {
public:
[[nodiscard]] const char *what() const noexcept override {
return "Seek option was used without a number";
}
};
struct Help {
};
struct None {
};
struct Next {
};
struct Previous {
};
struct Refresh {
};
struct Exit {
};
struct Seek {
int number;
};
using Command = std::variant<None, Help, Next, Previous, Refresh, Exit, Seek>;
Command readCommand();
}

View File

@ -10,65 +10,108 @@
#include <iostream> #include <iostream>
#include <format> #include <format>
#include <algorithm>
#include "ansi.hpp"
#include "stringutil.hpp" #include "stringutil.hpp"
#include "cpr/cpr.h" #include "cpr/cpr.h"
#include "libxml/HTMLparser.h" #include "libxml/HTMLparser.h"
#include "libxml/xpath.h" #include "libxml/xpath.h"
Page::Page(const uint_fast8_t number): number(number), subpages(fetchSubpages()) { bool is_number(const std::string_view s) {
return std::ranges::all_of(s, [](const unsigned char c) {
return std::isdigit(c);
});
}
void italize_numbers(std::string &content, size_t leave_chars = 0) {
size_t end = content.size();
// Process backwards, word by word
for (;;) {
const size_t space = content.rfind(' ', end - 1);
const size_t begin = (space == std::string::npos) ? 0 : space + 1;
const size_t word_length = end - begin;
if (is_number(content.substr(begin, word_length))) {
content.insert(end, ansi::CLEAR);
content.insert(begin, ansi::ITALIC);
}
if (space == std::string::npos)
break;
if (leave_chars >= space)
break;
end = space;
}
}
Page::Page(const int number): m_number(number), m_subpage(fetchSubpage()) {
} }
Page Page::operator--(int) const { Page Page::operator--(int) const {
return Page(number - 1); return Page(m_number - 1);
} }
Page Page::operator++(int) const { Page Page::operator++(int) const {
return Page(number + 1); return Page(m_number + 1);
} }
Page &Page::operator-=(int) { Page &Page::operator-=(int) {
number--; m_number--;
refresh(); refresh();
return *this; return *this;
} }
std::string Page::str_pretty() const {
std::string content = str();
content.insert(0, ansi::BOLD);
size_t line_end = content.find('\n');
if (line_end == std::string::npos)
line_end = content.size();
content.insert(line_end, ansi::CLEAR);
italize_numbers(content, line_end + ansi::CLEAR.size());
return content;
}
std::string Page::str() const { std::string Page::str() const {
std::string ret; std::string ret;
for (const std::string &pageText: subpages) { std::istringstream stream(m_subpage);
std::istringstream stream(pageText);
std::string line; std::string line;
while (std::getline(stream, line)) while (std::getline(stream, line))
ret += line + "\n"; ret += line + "\n";
string_utils::limitConsecutiveWhitespace(ret, MAX_WHITESPACE); string_utils::limit_consecutive_whitespace(ret, MAX_WHITESPACE);
string_utils::removeTrailingWhitespace(ret); string_utils::remove_trailing_whitespace(ret);
}
return ret; return ret;
} }
Page &Page::operator+=(int) { Page &Page::operator+=(int) {
number++; m_number++;
refresh(); refresh();
return *this; return *this;
} }
bool Page::refresh() { bool Page::refresh() {
std::vector<std::string> newSubpages = fetchSubpages(); std::string newSubpage = fetchSubpage();
const bool replace = !contentEquals(newSubpages); const bool replace = newSubpage != m_subpage;
if (replace) if (replace)
subpages = newSubpages; m_subpage = newSubpage;
return replace; return replace;
} }
std::string Page::url() const { std::string Page::url() const {
return std::format("https://www.svt.se/svttext/web/pages/{}.html", number); return std::format("https://www.svt.se/svttext/web/pages/{}.html", m_number);
} }
std::vector<std::string> Page::fetchSubpages() const { std::string Page::fetchSubpage() const {
const cpr::Response response = cpr::Get(cpr::Url{url()}); const cpr::Response response = cpr::Get(cpr::Url{url()});
if (response.status_code / 100 != 2) if (response.status_code / 100 != 2)
throw std::runtime_error("Page not found"); throw std::runtime_error("Page not found");
@ -91,14 +134,16 @@ std::vector<std::string> Page::fetchSubpages() const {
throw std::runtime_error("Could not evaluate XPath expression."); throw std::runtime_error("Could not evaluate XPath expression.");
} }
std::vector<std::string> pages; // There's only one valid page
std::string page;
if (const xmlNodeSetPtr nodes = xpathObj->nodesetval) { if (const xmlNodeSetPtr nodes = xpathObj->nodesetval) {
for (int i = 0; i < nodes->nodeNr; ++i) { for (int i = 0; i < nodes->nodeNr; i++) {
xmlChar *content = xmlNodeGetContent(nodes->nodeTab[i]); xmlChar *content = xmlNodeGetContent(nodes->nodeTab[i]);
if (!content) if (!content)
continue; continue;
pages.emplace_back(reinterpret_cast<const char *>(content)); page = reinterpret_cast<const char *>(content);
xmlFree(content); xmlFree(content);
break;
} }
} }
@ -106,16 +151,5 @@ std::vector<std::string> Page::fetchSubpages() const {
xmlXPathFreeContext(xpathCtx); xmlXPathFreeContext(xpathCtx);
xmlFreeDoc(doc); xmlFreeDoc(doc);
return pages; return page;
}
bool Page::contentEquals(const std::vector<std::string> &subpagesOther) const {
if (subpagesOther.size() != subpages.size())
return false;
for (std::size_t i = 0; i < subpages.size(); i++)
if (subpages[i] != subpagesOther[i])
return false;
return true;
} }

View File

@ -12,17 +12,16 @@
#include <string> #include <string>
#include <cstdint> #include <cstdint>
static constexpr uint_fast8_t DEFAULT_NUMBER = 100; static constexpr int DEFAULT_NUMBER = 100;
static constexpr uint_fast8_t MAX_WHITESPACE = 2; static constexpr int MAX_WHITESPACE = 2;
class Page class Page {
{
private: private:
uint_fast8_t number{}; int m_number;
std::vector<std::string> subpages; std::string m_subpage;
public: public:
explicit Page(uint_fast8_t number = DEFAULT_NUMBER); explicit Page(int number = DEFAULT_NUMBER);
Page operator--(int) const; Page operator--(int) const;
@ -32,14 +31,14 @@ public:
Page &operator-=(int); Page &operator-=(int);
std::string str() const; [[nodiscard]] std::string str_pretty() const;
[[nodiscard]] std::string str() const;
bool refresh(); bool refresh();
private: private:
[[nodiscard]] std::string url() const; [[nodiscard]] std::string url() const;
[[nodiscard]] std::vector<std::string> fetchSubpages() const; [[nodiscard]] std::string fetchSubpage() const;
[[nodiscard]] bool contentEquals(const std::vector<std::string> &subpagesOther) const;
}; };

50
src/Pager.cpp Normal file
View File

@ -0,0 +1,50 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#include "Pager.hpp"
Page &Pager::seek(const int number) {
Page &page = get_page_at(number);
m_page_number = number;
return page;
}
Pager &Pager::operator+=(const int number) {
seek(m_page_number + number);
return *this;
}
Pager &Pager::operator-=(const int number) {
seek(m_page_number - number);
return *this;
}
Page &Pager::operator*() {
return get_page_at(m_page_number);
}
Page *Pager::operator->() {
return &get_page_at(m_page_number);
}
void Pager::set(const int number) {
m_page_number = number;
}
void Pager::clear() {
m_pages.clear();
}
Page &Pager::get_page_at(const int number) {
if (!m_pages.contains(number)) {
const Page p(number);
m_pages[number] = p;
}
return m_pages.at(number);
}

40
src/Pager.hpp Normal file
View File

@ -0,0 +1,40 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#pragma once
#include <unordered_map>
#include "Page.hpp"
class Pager {
public:
Pager() = default;
Page &seek(int number);
Pager &operator+=(int number);
Pager &operator-=(int number);
Page &operator*();
Page *operator->();
void set(int number);
void clear();
private:
Page &get_page_at(int number);
private:
std::unordered_map<int, Page> m_pages{};
int m_page_number = 100;
};

130
src/Tui.cpp Normal file
View File

@ -0,0 +1,130 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#include "Tui.hpp"
#include <iostream>
#include <optional>
#include <ostream>
#include <utility>
#include "Command.hpp"
#include "Pager.hpp"
Tui::Tui(Pager pager) : m_pager(std::move(pager)) {
}
std::optional<int> get_page_number(const std::string_view line) {
const size_t space = line.find_first_of(' ');
if (space == std::string::npos)
return std::nullopt;
int number = 0;
const size_t size = line.size();
for (size_t i = space; i < size; i++) {
const char c = line[i];
if (!std::isdigit(c))
return std::nullopt;
number = number * 10 + (c - '0');
}
return number;
}
void printHelp() {
std::cout
<< "Available Commands:\n"
<< " r, refresh : Refresh the current view.\n"
<< " n, >, next : Move to the next item.\n"
<< " p, <, previous : Move to the previous item.\n"
<< " x, exit, q, quit : Exit the application.\n"
<< " seek <number>, s <number> : Seek to the specified number.\n"
<< " help, h, ? : Display this help message."
<< std::endl;
}
void Tui::run() {
clear_console();
std::cout << m_pager->str_pretty() << std::endl;
for (;;) {
std::cout << "Enter a command: ";
std::flush(std::cout);
Command::Command command;
try {
command = Command::readCommand();
} catch (const Command::NoNumberException &e) {
std::cout << e.what() << std::endl;
continue;
}
bool should_exit = false;
bool should_output = false;
auto visitor = [this, &should_exit, &should_output]<typename T0>(T0 &&arg) {
using T = std::decay_t<T0>;
if constexpr (std::is_same_v<T, Command::None>) {
std::cout << "Invalid command!" << std::endl;
} else if constexpr (std::is_same_v<T, Command::Next>) {
try {
std::cout << "Fetching..." << std::endl;
m_pager += 1;
} catch (const std::runtime_error &e) {
std::cout << e.what() << std::endl;
return;
}
should_output = true;
} else if constexpr (std::is_same_v<T, Command::Previous>) {
try {
std::cout << "Fetching..." << std::endl;
m_pager -= 1;
} catch (const std::runtime_error &e) {
std::cout << e.what() << std::endl;
return;
}
should_output = true;
} else if constexpr (std::is_same_v<T, Command::Refresh>) {
m_pager.clear();
should_output = true;
} else if constexpr (std::is_same_v<T, Command::Exit>) {
should_exit = true;
} else if constexpr (std::is_same_v<T, Command::Help>) {
printHelp();
} else if constexpr (std::is_same_v<T, Command::Seek>) {
const Command::Seek &seek = arg;
const int number = seek.number;
try {
std::cout << "Fetching..." << std::endl;
m_pager.seek(number);
} catch (const std::runtime_error &e) {
std::cout << e.what() << std::endl;
return;
}
should_output = true;
}
};
std::visit(visitor, command);
if (should_exit)
return;
clear_console();
if (should_output)
std::cout << m_pager->str_pretty() << std::endl;
}
}
void Tui::clear_console() {
const auto CMD =
#ifdef _WIN32 || _WIN64
"cls";
#else
"clear";
#endif
system(CMD);
}

25
src/Tui.hpp Normal file
View File

@ -0,0 +1,25 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#pragma once
#include "Pager.hpp"
class Tui {
public:
Tui(Pager pager);
void run();
private:;
static void clear_console();
private:
Pager m_pager;
};

17
src/ansi.hpp Normal file
View File

@ -0,0 +1,17 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#pragma once
#include <string>
namespace ansi {
constexpr std::string_view BOLD = "\033[1m";
constexpr std::string_view ITALIC = "\033[3m";
constexpr std::string_view CLEAR = "\033[0m";
} // namespace ansi

View File

@ -1,8 +1,14 @@
#include <iostream> // _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#include "Page.hpp" #include "Tui.hpp"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
const auto page = Page(); Tui tui(Pager{});
std::cout << page.str() << std::endl; tui.run();
} }

View File

@ -1,3 +1,11 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#include "stringutil.hpp" #include "stringutil.hpp"
#include <algorithm> #include <algorithm>
@ -6,28 +14,28 @@
namespace string_utils { namespace string_utils {
bool isAllWhitespace(const std::string &str) { bool is_all_whitespace(const std::string &str) {
return std::ranges::all_of(str, [](const unsigned char c) -> bool { return std::ranges::all_of(str, [](const unsigned char c) -> bool {
return std::isspace(c); return std::isspace(c);
}); });
} }
void removeTrailingWhitespace(std::string &str) { void remove_trailing_whitespace(std::string &str) {
auto shouldRemoveTrailingWhitespace = [&str]() -> bool { auto shouldRemoveTrailingWhitespace = [&str]() -> bool {
std::size_t last_newline = str.find_last_of('\n'); std::size_t last_newline = str.find_last_of('\n');
if (last_newline == std::string::npos) if (last_newline == std::string::npos)
return isAllWhitespace(str); return is_all_whitespace(str);
const std::string last_line = str.substr(last_newline + 1); const std::string last_line = str.substr(last_newline + 1);
return isAllWhitespace(last_line); return is_all_whitespace(last_line);
}; };
while (shouldRemoveTrailingWhitespace()) { while (shouldRemoveTrailingWhitespace()) {
const std::size_t last_newline = str.find_last_of('\n'); const std::size_t last_newline = str.find_last_of('\n');
if (last_newline == std::string::npos) { if (last_newline == std::string::npos) {
if (isAllWhitespace(str)) if (is_all_whitespace(str))
str.clear(); str.clear();
break; break;
} }
@ -36,11 +44,11 @@ namespace string_utils {
} }
} }
void removeTabs(std::string &str) { void remove_tabs(std::string &str) {
std::erase(str, '\t'); std::erase(str, '\t');
} }
void limitConsecutiveWhitespace(std::string &str, const uint_fast8_t maxWhitespace) { void limit_consecutive_whitespace(std::string &str, const uint_fast8_t maxWhitespace) {
std::istringstream stream(str); std::istringstream stream(str);
std::string line; std::string line;
std::ostringstream processedStream; std::ostringstream processedStream;
@ -49,7 +57,7 @@ namespace string_utils {
bool hasAddedRealTextJet = false; bool hasAddedRealTextJet = false;
while (std::getline(stream, line)) { while (std::getline(stream, line)) {
const bool onlySpace = isAllWhitespace(line); const bool onlySpace = is_all_whitespace(line);
if (!hasAddedRealTextJet) { if (!hasAddedRealTextJet) {
if (onlySpace) if (onlySpace)

View File

@ -1,3 +1,11 @@
// _____ _ _____
// |_ _|____ _| ||_ _|_ __
// | |/ _ \ \/ / __|| | \ \ / /
// | | __/> <| |_ | | \ V /
// |_|\___/_/\_\\__||_| \_/
// Author: Love Billenius <lovebillenius@disroot.org>
// License: GPL-3
#pragma once #pragma once
#include <string> #include <string>
@ -15,7 +23,7 @@ namespace string_utils {
* @return true If all characters in the string are whitespace. * @return true If all characters in the string are whitespace.
* @return false Otherwise. * @return false Otherwise.
*/ */
bool isAllWhitespace(const std::string& str); bool is_all_whitespace(const std::string& str);
/** /**
* @brief Removes trailing whitespace lines from the given string. * @brief Removes trailing whitespace lines from the given string.
@ -25,14 +33,14 @@ namespace string_utils {
* *
* @param str The string from which to remove trailing whitespace lines. * @param str The string from which to remove trailing whitespace lines.
*/ */
void removeTrailingWhitespace(std::string& str); void remove_trailing_whitespace(std::string& str);
/** /**
* @brief Removes all tab characters from the given string. * @brief Removes all tab characters from the given string.
* *
* @param str The string from which to remove tab characters. * @param str The string from which to remove tab characters.
*/ */
void removeTabs(std::string& str); void remove_tabs(std::string& str);
/** /**
* @brief Limits the number of consecutive whitespace lines in the given string. * @brief Limits the number of consecutive whitespace lines in the given string.
@ -43,6 +51,6 @@ namespace string_utils {
* @param str The string to process. * @param str The string to process.
* @param maxWhitespace The maximum allowed consecutive whitespace lines. * @param maxWhitespace The maximum allowed consecutive whitespace lines.
*/ */
void limitConsecutiveWhitespace(std::string& str, uint_fast8_t maxWhitespace); void limit_consecutive_whitespace(std::string& str, uint_fast8_t maxWhitespace);
} }