Persist session state and single-instance

Add session persistence and single-instance handling. Introduces browser/session_state.{h,cpp} to load/save a simple JSON session_state.json (limits restored tabs to 50, basic JSON parsing, atomic write via a .tmp rename). TabManager gains RestoreTabs and ActiveTabIndex to restore and track tabs. NebulaController now calls PersistSession on tab/title/activate/close events, flushes cookies on shutdown, and sets CEF runtime style to Alloy for embedded child browsers and devtools. run.cpp adds a named mutex to prevent multiple instances, enables persistent session cookies, and tweaks initial URL handling. Added GetSessionStatePath() to ui/paths and updated CMakeLists.txt to include the new source file.
This commit is contained in:
2026-05-14 20:48:48 +12:00
parent 406d73c10f
commit 8eb5c1a3b2
10 changed files with 365 additions and 3 deletions
+229
View File
@@ -0,0 +1,229 @@
#include "browser/session_state.h"
#include <algorithm>
#include <charconv>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>
#include <system_error>
#include "browser/url_utils.h"
#include "ui/paths.h"
namespace nebula::browser {
namespace {
constexpr size_t kMaxRestoredTabs = 50;
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input) {
return {};
}
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
std::optional<size_t> ReadUnsignedValue(const std::string& json, std::string_view key) {
const size_t key_pos = json.find(key);
if (key_pos == std::string::npos) {
return std::nullopt;
}
size_t colon = json.find(':', key_pos + key.size());
if (colon == std::string::npos) {
return std::nullopt;
}
++colon;
while (colon < json.size() && std::isspace(static_cast<unsigned char>(json[colon]))) {
++colon;
}
size_t end = colon;
while (end < json.size() && std::isdigit(static_cast<unsigned char>(json[end]))) {
++end;
}
size_t value = 0;
const auto result = std::from_chars(json.data() + colon, json.data() + end, value);
if (result.ec != std::errc{} || result.ptr != json.data() + end) {
return std::nullopt;
}
return value;
}
std::optional<std::string> ReadStringValue(const std::string& object, std::string_view key) {
const size_t key_pos = object.find(key);
if (key_pos == std::string::npos) {
return std::nullopt;
}
size_t colon = object.find(':', key_pos + key.size());
if (colon == std::string::npos) {
return std::nullopt;
}
size_t quote = object.find('"', colon + 1);
if (quote == std::string::npos) {
return std::nullopt;
}
std::string value;
for (size_t i = quote + 1; i < object.size(); ++i) {
const char ch = object[i];
if (ch == '"') {
return value;
}
if (ch != '\\') {
value += ch;
continue;
}
if (++i >= object.size()) {
return std::nullopt;
}
switch (object[i]) {
case '"':
case '\\':
case '/':
value += object[i];
break;
case 'b':
value += '\b';
break;
case 'f':
value += '\f';
break;
case 'n':
value += '\n';
break;
case 'r':
value += '\r';
break;
case 't':
value += '\t';
break;
default:
return std::nullopt;
}
}
return std::nullopt;
}
std::vector<PersistedTab> ReadTabs(const std::string& json) {
std::vector<PersistedTab> tabs;
const size_t tabs_pos = json.find("\"tabs\"");
if (tabs_pos == std::string::npos) {
return tabs;
}
const size_t array_start = json.find('[', tabs_pos);
const size_t array_end = json.find(']', array_start == std::string::npos ? tabs_pos : array_start);
if (array_start == std::string::npos || array_end == std::string::npos) {
return tabs;
}
size_t cursor = array_start + 1;
while (cursor < array_end && tabs.size() < kMaxRestoredTabs) {
const size_t object_start = json.find('{', cursor);
if (object_start == std::string::npos || object_start >= array_end) {
break;
}
const size_t object_end = json.find('}', object_start + 1);
if (object_end == std::string::npos || object_end > array_end) {
break;
}
const std::string object = json.substr(object_start, object_end - object_start + 1);
const auto url = ReadStringValue(object, "\"url\"");
if (url && !url->empty()) {
PersistedTab tab;
tab.url = *url;
if (const auto title = ReadStringValue(object, "\"title\""); title && !title->empty()) {
tab.title = *title;
}
tabs.push_back(std::move(tab));
}
cursor = object_end + 1;
}
return tabs;
}
} // namespace
SessionState LoadSessionState() {
SessionState state;
const std::string json = ReadFile(nebula::ui::GetSessionStatePath());
if (json.empty()) {
return state;
}
state.tabs = ReadTabs(json);
if (const auto active_index = ReadUnsignedValue(json, "\"activeTabIndex\"")) {
state.active_tab_index = *active_index;
}
if (!state.tabs.empty()) {
state.active_tab_index = std::min(state.active_tab_index, state.tabs.size() - 1);
} else {
state.active_tab_index = 0;
}
return state;
}
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index) {
const auto path = nebula::ui::GetSessionStatePath();
if (path.empty()) {
return;
}
std::ostringstream json;
json << "{\n \"activeTabIndex\": " << active_tab_index << ",\n \"tabs\": [\n";
bool wrote_tab = false;
for (const auto& tab : tabs) {
if (tab.url.empty()) {
continue;
}
if (wrote_tab) {
json << ",\n";
}
json << " {\"url\": \"" << JsonEscape(tab.url)
<< "\", \"title\": \"" << JsonEscape(tab.title) << "\"}";
wrote_tab = true;
}
json << "\n ]\n}\n";
std::filesystem::path temp_path = path;
temp_path += L".tmp";
{
std::ofstream output(temp_path, std::ios::binary | std::ios::trunc);
if (!output) {
return;
}
output << json.str();
}
std::error_code ec;
std::filesystem::remove(path, ec);
ec.clear();
std::filesystem::rename(temp_path, path, ec);
}
} // namespace nebula::browser
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include "browser/tab.h"
namespace nebula::browser {
struct PersistedTab {
std::string url;
std::string title = "New Tab";
};
struct SessionState {
std::vector<PersistedTab> tabs;
size_t active_tab_index = 0;
};
SessionState LoadSessionState();
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index);
} // namespace nebula::browser
+33
View File
@@ -1,5 +1,7 @@
#include "browser/tab_manager.h"
#include <algorithm>
#include "browser/url_utils.h"
#include "ui/paths.h"
@@ -28,6 +30,28 @@ NebulaTab& TabManager::CreateTab(std::string url) {
return tabs_.back();
}
void TabManager::RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index) {
tabs_.clear();
active_tab_id_ = 0;
for (const auto& restored_tab : tabs) {
NebulaTab tab;
tab.id = next_tab_id_++;
tab.url = restored_tab.url.empty() ? nebula::ui::GetHomeUrl() : restored_tab.url;
tab.title = restored_tab.title.empty() ? "New Tab" : restored_tab.title;
tabs_.push_back(std::move(tab));
}
if (tabs_.empty()) {
CreateInitialTab(nebula::ui::GetHomeUrl());
return;
}
active_tab_index = std::min(active_tab_index, tabs_.size() - 1);
active_tab_id_ = tabs_[active_tab_index].id;
Notify();
}
NebulaTab* TabManager::ActiveTab() {
for (auto& tab : tabs_) {
if (tab.id == active_tab_id_) {
@@ -50,6 +74,15 @@ const std::vector<NebulaTab>& TabManager::Tabs() const {
return tabs_;
}
size_t TabManager::ActiveTabIndex() const {
for (size_t i = 0; i < tabs_.size(); ++i) {
if (tabs_[i].id == active_tab_id_) {
return i;
}
}
return 0;
}
bool TabManager::ActivateTab(int tab_id) {
if (!FindTab(tab_id)) {
return false;
+4
View File
@@ -1,9 +1,11 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include "browser/tab.h"
#include "browser/session_state.h"
namespace nebula::browser {
@@ -19,9 +21,11 @@ public:
NebulaTab& CreateInitialTab(std::string initial_url);
NebulaTab& CreateTab(std::string url);
void RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index);
NebulaTab* ActiveTab();
const NebulaTab* ActiveTab() const;
const std::vector<NebulaTab>& Tabs() const;
size_t ActiveTabIndex() const;
bool ActivateTab(int tab_id);
CefRefPtr<CefBrowser> CloseTab(int tab_id);