TUT/src/history.cpp
m1ngsama 8d56a7b67b feat: Add persistent browsing history
- Implement HistoryManager for JSON persistence (~/.config/tut/history.json)
- Auto-record page visits with URL, title, and timestamp
- Update visit time when revisiting URLs (move to front)
- Limit to 1000 entries maximum
- Add :history command to view browsing history
- History entries are clickable links
- Add test_history test suite
2025-12-27 18:13:40 +08:00

217 lines
5.9 KiB
C++

#include "history.h"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <sys/stat.h>
#include <cstdlib>
namespace tut {
HistoryManager::HistoryManager() {
load();
}
HistoryManager::~HistoryManager() {
save();
}
std::string HistoryManager::get_history_path() {
const char* home = std::getenv("HOME");
if (!home) {
home = "/tmp";
}
return std::string(home) + "/.config/tut/history.json";
}
bool HistoryManager::ensure_config_dir() {
const char* home = std::getenv("HOME");
if (!home) home = "/tmp";
std::string config_dir = std::string(home) + "/.config";
std::string tut_dir = config_dir + "/tut";
struct stat st;
if (stat(tut_dir.c_str(), &st) == 0) {
return S_ISDIR(st.st_mode);
}
mkdir(config_dir.c_str(), 0755);
return mkdir(tut_dir.c_str(), 0755) == 0 || errno == EEXIST;
}
// JSON escape/unescape
static std::string json_escape(const std::string& s) {
std::string result;
result.reserve(s.size() + 10);
for (char c : s) {
switch (c) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default: result += c; break;
}
}
return result;
}
static std::string json_unescape(const std::string& s) {
std::string result;
result.reserve(s.size());
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] == '\\' && i + 1 < s.size()) {
switch (s[i + 1]) {
case '"': result += '"'; ++i; break;
case '\\': result += '\\'; ++i; break;
case 'n': result += '\n'; ++i; break;
case 'r': result += '\r'; ++i; break;
case 't': result += '\t'; ++i; break;
default: result += s[i]; break;
}
} else {
result += s[i];
}
}
return result;
}
bool HistoryManager::load() {
entries_.clear();
std::ifstream file(get_history_path());
if (!file) {
return false;
}
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
size_t pos = content.find('[');
if (pos == std::string::npos) return false;
pos++;
while (pos < content.size()) {
pos = content.find('{', pos);
if (pos == std::string::npos) break;
pos++;
HistoryEntry entry;
while (pos < content.size() && content[pos] != '}') {
while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' ||
content[pos] == '\r' || content[pos] == '\t' || content[pos] == ',')) {
pos++;
}
if (content[pos] == '}') break;
if (content[pos] != '"') { pos++; continue; }
pos++;
size_t key_end = content.find('"', pos);
if (key_end == std::string::npos) break;
std::string key = content.substr(pos, key_end - pos);
pos = key_end + 1;
pos = content.find(':', pos);
if (pos == std::string::npos) break;
pos++;
while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\n' ||
content[pos] == '\r' || content[pos] == '\t')) {
pos++;
}
if (content[pos] == '"') {
pos++;
size_t val_end = pos;
while (val_end < content.size()) {
if (content[val_end] == '"' && content[val_end - 1] != '\\') break;
val_end++;
}
std::string value = json_unescape(content.substr(pos, val_end - pos));
pos = val_end + 1;
if (key == "url") entry.url = value;
else if (key == "title") entry.title = value;
} else {
size_t val_end = pos;
while (val_end < content.size() && content[val_end] >= '0' && content[val_end] <= '9') {
val_end++;
}
std::string value = content.substr(pos, val_end - pos);
pos = val_end;
if (key == "time") {
entry.visit_time = std::stoll(value);
}
}
}
if (!entry.url.empty()) {
entries_.push_back(entry);
}
pos = content.find('}', pos);
if (pos == std::string::npos) break;
pos++;
}
return true;
}
bool HistoryManager::save() const {
if (!ensure_config_dir()) {
return false;
}
std::ofstream file(get_history_path());
if (!file) {
return false;
}
file << "[\n";
for (size_t i = 0; i < entries_.size(); ++i) {
const auto& entry = entries_[i];
file << " {\n";
file << " \"url\": \"" << json_escape(entry.url) << "\",\n";
file << " \"title\": \"" << json_escape(entry.title) << "\",\n";
file << " \"time\": " << entry.visit_time << "\n";
file << " }";
if (i + 1 < entries_.size()) {
file << ",";
}
file << "\n";
}
file << "]\n";
return true;
}
void HistoryManager::add(const std::string& url, const std::string& title) {
// Remove existing entry with same URL
auto it = std::find_if(entries_.begin(), entries_.end(),
[&url](const HistoryEntry& e) { return e.url == url; });
if (it != entries_.end()) {
entries_.erase(it);
}
// Add new entry at the front
entries_.insert(entries_.begin(), HistoryEntry(url, title));
// Enforce max entries limit
if (entries_.size() > MAX_ENTRIES) {
entries_.resize(MAX_ENTRIES);
}
save();
}
void HistoryManager::clear() {
entries_.clear();
save();
}
} // namespace tut