7 Commits

Author SHA1 Message Date
Andrew Zambazos 18bc607d93 Changed GitHub button in settings to Gitpub
Changed GitHub button in settings to Gitpub, yet to do icon
2026-05-16 13:27:25 +12:00
andrew 54216aa133 Persist site history to disk and integrate with settings
Add persistent site history storage and plumbing between the renderer settings UI and the native app. The app now loads/saves site_history.txt in the user data directory (max 200 entries, http/https-only, stored one URL per line) and records visited sites on navigation. Settings pages receive the history via injected JavaScript when the settings page finishes loading, and a "clear-site-history" message from the settings UI clears the on-disk history and updates the renderer.

Other changes: allow settings-related process messages from content frames in the CEF client, introduce OnContentLoadFinished to trigger history injection, expose electronAPI.send/sendToHost (and reuse the native postMessage handler) in the V8 context, and remove the BigPicture in-app history UI/refresh/clear handlers (history is now managed by the native app). Also cleaned up includes and added helper utilities for JSON escaping, lowercasing, and file path handling. The initial tab restore logic was simplified to always create an initial tab (home or initial_url) and persist the session.
2026-05-14 20:57:17 +12:00
andrew 8eb5c1a3b2 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.
2026-05-14 20:48:48 +12:00
andrew 406d73c10f Fullscreen and Fullscreen YouTube video fixes 2026-05-14 19:52:38 +12:00
andrew 6fac7e320b Made GPU diagnostics page more functional 2026-05-14 19:42:08 +12:00
andrew a32940a3f3 Enable GPU/WebGL and add persistent cache dirs
Enable hardware-accelerated rendering and persist GPU/cache data. Added a BrowserSettings() helper that enables WebGL and use it when creating Chrome/Content/MenuPopup browsers (src/app/nebula_controller.cpp). Configure CefSettings to use a persistent user data and cache directory (src/app/run.cpp) by calling nebula::ui::GetUserDataDirectory() and GetCacheDirectory(). Add command-line switches to initialize the GPU process and avoid sandbox/blocklist fallbacks (disable GPU sandbox, in-process-gpu, ignore-gpu-blocklist, enable-accelerated-video-decode, use ANGLE D3D11) to prevent GPU crashes and Chromium falling back to software rendering (src/cef/nebula_app.cpp). Implement GetUserDataDirectory() and GetCacheDirectory() (preferring %LOCALAPPDATA% with an executable-directory fallback) and expose them in the header (src/ui/paths.cpp, src/ui/paths.h). These changes ensure GPU shader caching, WebGL support, and smoother video/graphics behavior.
2026-05-14 19:37:49 +12:00
andrew 10180b7109 Support nebula:// internal pages & GPU tools
Introduce support for an internal nebula:// URL scheme and internal page routing (ResolveInternalUrl / ToInternalUrl), including dedicated slugs for home, settings, downloads, big-picture, gpu-diagnostics, insecure and a 404 fallback. Wire internal resolution into browser creation and tab navigation so internal pages load from local UI files. Add an insecure-warning interstitial flow with a navigate-insecure command and a one-shot bypass set (ShouldBypassInsecureWarning) so content can request navigating to an HTTP target after user confirmation. Harden BrowserClient handling to resolve Chromium new-tab and nebula internal URLs, redirect HTTP to the insecure warning when appropriate, and handle 404 responses by loading the internal 404 page. Update chrome UI behavior to hide internal home URLs, accept nebula:// in navigation input checks, and add a GPU Diagnostics page (revamped UI + diagnostic scripts) plus menu entry. Misc: improve URL utilities (scheme checks, percent-encoding, decorations), fix 404 display text, adjust menu popup size, tweak window frame styling (DWM attributes) and remove branding block from chrome UI CSS.
2026-05-14 19:11:06 +12:00
27 changed files with 1958 additions and 328 deletions
+1
View File
@@ -41,6 +41,7 @@ set(NEBULA_SOURCES
app/main.cpp
src/app/nebula_controller.cpp
src/app/run.cpp
src/browser/session_state.cpp
src/browser/tab.cpp
src/browser/tab_manager.cpp
src/browser/url_utils.cpp
+227 -13
View File
@@ -4,17 +4,24 @@
#include <algorithm>
#include <charconv>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <system_error>
#include "browser/session_state.h"
#include "browser/url_utils.h"
#include "include/cef_app.h"
#include "include/cef_browser.h"
#include "include/cef_cookie.h"
#include "include/wrapper/cef_helpers.h"
#include "ui/paths.h"
namespace nebula::app {
namespace {
constexpr size_t kMaxSiteHistoryEntries = 200;
std::wstring Utf8ToWide(const std::string& value) {
if (value.empty()) {
return {};
@@ -28,6 +35,67 @@ std::wstring Utf8ToWide(const std::string& value) {
return result;
}
std::filesystem::path GetSiteHistoryPath() {
const auto user_data = nebula::ui::GetUserDataDirectory();
return user_data.empty() ? std::filesystem::path{} : user_data / L"site_history.txt";
}
std::string ToLowerAscii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
bool IsSiteHistoryUrl(const std::string& url) {
const std::string lower = ToLowerAscii(url);
return lower.starts_with("http://") || lower.starts_with("https://");
}
std::vector<std::string> LoadSiteHistory() {
std::vector<std::string> history;
std::ifstream input(GetSiteHistoryPath(), std::ios::binary);
if (!input) {
return history;
}
std::string url;
while (std::getline(input, url) && history.size() < kMaxSiteHistoryEntries) {
if (IsSiteHistoryUrl(url)) {
history.push_back(url);
}
}
return history;
}
void SaveSiteHistory(const std::vector<std::string>& history) {
const auto path = GetSiteHistoryPath();
if (path.empty()) {
return;
}
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output) {
return;
}
for (const auto& url : history) {
output << url << '\n';
}
}
std::string SiteHistoryJson(const std::vector<std::string>& history) {
std::string json = "[";
for (size_t i = 0; i < history.size(); ++i) {
if (i > 0) {
json += ",";
}
json += "\"" + nebula::browser::JsonEscape(history[i]) + "\"";
}
json += "]";
return json;
}
CefWindowInfo ChildWindowInfo(HWND parent, const RECT& rect) {
CefWindowInfo info;
info.SetAsChild(
@@ -37,9 +105,19 @@ CefWindowInfo ChildWindowInfo(HWND parent, const RECT& rect) {
rect.top,
rect.right - rect.left,
rect.bottom - rect.top));
// CEF defaults to the Chrome runtime style, which ignores the
// SetAsChild hint and creates a top-level window per browser. Force the
// Alloy runtime style so each browser embeds inside the Nebula HWND.
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
CefBrowserSettings BrowserSettings() {
CefBrowserSettings settings;
settings.webgl = STATE_ENABLED;
return settings;
}
int ParseTabId(const std::string& value) {
int tab_id = 0;
const auto result = std::from_chars(value.data(), value.data() + value.size(), tab_id);
@@ -55,7 +133,7 @@ RECT MenuPopupRect(HWND hwnd, const nebula::window::BrowserLayout& layout) {
GetClientRect(hwnd, &client);
const int width = ScaleForWindow(hwnd, 260);
const int height = ScaleForWindow(hwnd, 218);
const int height = ScaleForWindow(hwnd, 258);
const int margin = ScaleForWindow(hwnd, 12);
const int overlap = ScaleForWindow(hwnd, 2);
@@ -103,6 +181,10 @@ std::string WithCacheBuster(std::string url) {
return url + separator + "nebula_cache_bust=" + std::to_string(GetTickCount64()) + fragment;
}
std::string GetChromeDisplayUrl(const std::string& url) {
return nebula::ui::IsInternalHomeUrl(url) ? std::string{} : url;
}
void SetBrowserVisible(CefRefPtr<CefBrowser> browser, bool visible) {
if (!browser) {
return;
@@ -125,7 +207,8 @@ NebulaController::NebulaController(HINSTANCE instance, std::string initial_url,
: instance_(instance),
initial_url_(std::move(initial_url)),
show_command_(show_command),
tabs_(this) {}
tabs_(this),
site_history_(LoadSiteHistory()) {}
NebulaController::~NebulaController() = default;
@@ -135,7 +218,13 @@ bool NebulaController::Create() {
}
void NebulaController::OnWindowCreated() {
tabs_.CreateInitialTab(initial_url_.empty() ? nebula::ui::GetHomeUrl() : initial_url_);
if (initial_url_.empty()) {
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
} else {
tabs_.CreateInitialTab(initial_url_);
}
PersistSession();
CreateChromeBrowser();
CreateContentBrowser();
}
@@ -147,11 +236,23 @@ void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layo
void NebulaController::OnWindowCloseRequested() {
if (closing_) {
// CEF re-sends WM_CLOSE to the top-level window after each Alloy
// child browser finishes its JS unload + DoClose phase. Destroy the
// Nebula window now so CEF can tear down the child browser HWNDs and
// fire OnBeforeClose; MaybeFinishShutdown will then quit the loop.
if (window_ && window_->hwnd()) {
DestroyWindow(window_->hwnd());
}
MaybeFinishShutdown();
return;
}
closing_ = true;
PersistSession();
if (auto cookie_manager = CefCookieManager::GetGlobalManager(nullptr)) {
cookie_manager->FlushStore(nullptr);
}
if (chrome_browser_) {
chrome_browser_->GetHost()->CloseBrowser(false);
}
@@ -163,7 +264,6 @@ void NebulaController::OnWindowCloseRequested() {
tab.browser->GetHost()->CloseBrowser(false);
}
}
MaybeFinishShutdown();
}
void NebulaController::OnActiveTabChanged(const nebula::browser::NebulaTab& tab) {
@@ -201,6 +301,12 @@ void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr
menu_popup_browser_ = nullptr;
menu_popup_client_ = nullptr;
} else {
if (content_fullscreen_) {
const auto* active_tab = tabs_.ActiveTab();
if (active_tab && active_tab->browser && active_tab->browser->IsSame(browser)) {
SetContentFullscreen(false);
}
}
tabs_.ClearBrowser(browser);
}
MaybeFinishShutdown();
@@ -209,6 +315,12 @@ void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr
void NebulaController::OnChromeCommand(const std::string& command, const std::string& payload) {
if (command == "navigate") {
tabs_.LoadURL(payload);
} else if (command == "navigate-insecure") {
const std::string target = nebula::browser::NormalizeNavigationInput(payload);
if (nebula::ui::IsHttpUrl(target)) {
insecure_warning_bypasses_.insert(target);
tabs_.LoadURL(target);
}
} else if (command == "new-tab") {
CreateNewTab();
} else if (command == "activate-tab") {
@@ -233,6 +345,9 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
} else if (command == "big-picture") {
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetBigPictureUrl());
} else if (command == "gpu-diagnostics") {
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
} else if (command == "toggle-devtools") {
ToggleDevTools();
} else if (command == "zoom-out") {
@@ -251,6 +366,12 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
CloseMenuPopup();
} else if (command == "home") {
tabs_.LoadURL(nebula::ui::GetHomeUrl());
} else if (command == "clear-site-history") {
site_history_.clear();
SaveSiteHistory(site_history_);
if (auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
InjectSettingsHistory(tab->browser);
}
} else if (command == "minimize" && window_) {
window_->Minimize();
} else if (command == "maximize" && window_) {
@@ -263,11 +384,18 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
}
void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) {
tabs_.UpdateURL(browser, nebula::ui::IsChromiumNewTabUrl(url) ? nebula::ui::GetHomeUrl() : url);
const std::string internal_url = nebula::ui::ToInternalUrl(url);
tabs_.UpdateURL(browser,
nebula::ui::IsChromiumNewTabUrl(url)
? nebula::ui::GetHomeUrl()
: internal_url);
RecordSiteHistory(internal_url);
PersistSession();
}
void NebulaController::OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) {
tabs_.UpdateTitle(browser, title);
PersistSession();
const auto* active_tab = tabs_.ActiveTab();
if (window_ && active_tab && active_tab->browser && active_tab->browser->IsSame(browser)) {
window_->SetTitle(Utf8ToWide(title.empty() ? "Nebula Browser" : title + " - Nebula"));
@@ -282,10 +410,25 @@ void NebulaController::OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browse
tabs_.UpdateLoadProgress(browser, progress);
}
void NebulaController::OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) {
if (nebula::ui::ToInternalUrl(url).starts_with(nebula::ui::GetSettingsUrl())) {
InjectSettingsHistory(browser);
}
}
void NebulaController::OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
tabs_.UpdateFavicon(browser, urls);
}
void NebulaController::OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) {
const auto* active_tab = tabs_.ActiveTab();
if (!active_tab || !active_tab->browser || !active_tab->browser->IsSame(browser)) {
return;
}
SetContentFullscreen(fullscreen);
}
void NebulaController::OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) {
if (!tabs_.OwnsBrowser(browser)) {
return;
@@ -296,12 +439,27 @@ void NebulaController::OnPopupRequested(CefRefPtr<CefBrowser> browser, const std
: target_url);
}
bool NebulaController::ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) {
if (!tabs_.OwnsBrowser(browser)) {
return false;
}
const auto bypass = insecure_warning_bypasses_.find(target_url);
if (bypass == insecure_warning_bypasses_.end()) {
return false;
}
insecure_warning_bypasses_.erase(bypass);
return true;
}
void NebulaController::CreateNewTab() {
if (auto* tab = tabs_.ActiveTab()) {
SetBrowserVisible(tab->browser, false);
}
tabs_.CreateTab(nebula::ui::GetHomeUrl());
PersistSession();
CreateContentBrowser();
}
@@ -315,6 +473,7 @@ void NebulaController::ActivateTab(int tab_id) {
if (!tabs_.ActivateTab(tab_id)) {
return;
}
PersistSession();
SetBrowserVisible(previous_browser, false);
if (auto* active_tab = tabs_.ActiveTab()) {
@@ -334,12 +493,14 @@ void NebulaController::CloseTab(int tab_id) {
}();
CefRefPtr<CefBrowser> closing_browser = tabs_.CloseTab(tab_id);
PersistSession();
if (closing_browser) {
closing_browser->GetHost()->CloseBrowser(false);
}
if (!tabs_.ActiveTab()) {
tabs_.CreateTab(nebula::ui::GetHomeUrl());
PersistSession();
CreateContentBrowser();
return;
}
@@ -358,7 +519,7 @@ void NebulaController::CreateChromeBrowser() {
}
const auto layout = window_->CurrentLayout();
CefBrowserSettings browser_settings;
CefBrowserSettings browser_settings = BrowserSettings();
chrome_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Chrome, this);
CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.chrome);
CefBrowserHost::CreateBrowser(
@@ -373,10 +534,11 @@ void NebulaController::CreateContentBrowser() {
const auto* tab = tabs_.ActiveTab();
const std::string url = tab && !tab->url.empty() ? tab->url : nebula::ui::GetHomeUrl();
const auto layout = window_->CurrentLayout();
CefBrowserSettings browser_settings;
CefBrowserSettings browser_settings = BrowserSettings();
content_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Content, this);
CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.content);
CefBrowserHost::CreateBrowser(window_info, content_client_, url, browser_settings, nullptr, nullptr);
CefBrowserHost::CreateBrowser(
window_info, content_client_, nebula::ui::ResolveInternalUrl(url), browser_settings, nullptr, nullptr);
}
void NebulaController::ToggleMenuPopup() {
@@ -400,7 +562,7 @@ void NebulaController::CreateMenuPopupBrowser() {
}
const auto layout = window_->CurrentLayout();
CefBrowserSettings browser_settings;
CefBrowserSettings browser_settings = BrowserSettings();
menu_popup_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::MenuPopup, this);
CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), MenuPopupRect(window_->hwnd(), layout));
CefBrowserHost::CreateBrowser(
@@ -408,7 +570,7 @@ void NebulaController::CreateMenuPopupBrowser() {
}
void NebulaController::PositionMenuPopup() {
if (!window_ || !window_->hwnd() || !menu_popup_browser_) {
if (content_fullscreen_ || !window_ || !window_->hwnd() || !menu_popup_browser_) {
return;
}
@@ -433,6 +595,7 @@ void NebulaController::ToggleDevTools() {
CefWindowInfo window_info;
window_info.SetAsPopup(window_->hwnd(), "Nebula Developer Tools");
window_info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
CefBrowserSettings browser_settings;
host->ShowDevTools(window_info, content_client_, browser_settings, CefPoint());
}
@@ -456,19 +619,38 @@ void NebulaController::FreshReload() {
tabs_.LoadURL(WithCacheBuster(tab->url));
}
void NebulaController::SetContentFullscreen(bool fullscreen) {
if (content_fullscreen_ == fullscreen) {
return;
}
content_fullscreen_ = fullscreen;
if (fullscreen) {
CloseMenuPopup();
}
SetBrowserVisible(chrome_browser_, !fullscreen);
if (window_) {
window_->SetFullscreen(fullscreen);
}
ResizeBrowsers();
}
void NebulaController::ResizeBrowsers() {
if (!window_) {
return;
}
const auto layout = window_->CurrentLayout();
const auto layout = window_->CurrentLayout(!content_fullscreen_);
if (chrome_browser_) {
window_->ResizeChild(chrome_browser_->GetHost()->GetWindowHandle(), layout.chrome);
}
if (const auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
window_->ResizeChild(tab->browser->GetHost()->GetWindowHandle(), layout.content);
}
PositionMenuPopup();
if (!content_fullscreen_) {
PositionMenuPopup();
}
}
void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
@@ -476,6 +658,7 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
return;
}
const std::string display_url = GetChromeDisplayUrl(tab.url);
std::string tabs_json = "[";
const auto& tabs = tabs_.Tabs();
for (size_t i = 0; i < tabs.size(); ++i) {
@@ -495,7 +678,7 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
const std::string script =
"window.NebulaChrome && window.NebulaChrome.applyState({"
"\"id\":" + std::to_string(tab.id) +
",\"url\":\"" + nebula::browser::JsonEscape(tab.url) + "\""
",\"url\":\"" + nebula::browser::JsonEscape(display_url) + "\""
",\"title\":\"" + nebula::browser::JsonEscape(tab.title) + "\""
",\"isLoading\":" + std::string(tab.is_loading ? "true" : "false") +
",\"progress\":" + std::to_string(tab.load_progress) +
@@ -508,6 +691,37 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
chrome_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetChromeUrl(), 0);
}
void NebulaController::RecordSiteHistory(const std::string& url) {
if (!IsSiteHistoryUrl(url)) {
return;
}
site_history_.erase(
std::remove(site_history_.begin(), site_history_.end(), url),
site_history_.end());
site_history_.insert(site_history_.begin(), url);
if (site_history_.size() > kMaxSiteHistoryEntries) {
site_history_.resize(kMaxSiteHistoryEntries);
}
SaveSiteHistory(site_history_);
}
void NebulaController::InjectSettingsHistory(CefRefPtr<CefBrowser> browser) {
if (!browser) {
return;
}
const std::string history_json = SiteHistoryJson(site_history_);
const std::string script =
"localStorage.setItem('siteHistory', \"" + nebula::browser::JsonEscape(history_json) + "\");"
"if (typeof loadHistories === 'function') { loadHistories(); }";
browser->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetSettingsUrl(), 0);
}
void NebulaController::PersistSession() const {
nebula::browser::SaveSessionState(tabs_.Tabs(), tabs_.ActiveTabIndex());
}
void NebulaController::MaybeFinishShutdown() {
if (!closing_) {
return;
+11
View File
@@ -2,6 +2,7 @@
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
#include "browser/tab_manager.h"
@@ -32,8 +33,11 @@ public:
void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) override;
void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) override;
void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) override;
void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) override;
void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override;
void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
private:
void CreateNewTab();
@@ -48,8 +52,12 @@ private:
void ToggleDevTools();
void AdjustZoom(double delta);
void FreshReload();
void SetContentFullscreen(bool fullscreen);
void ResizeBrowsers();
void SendChromeState(const nebula::browser::NebulaTab& tab);
void RecordSiteHistory(const std::string& url);
void InjectSettingsHistory(CefRefPtr<CefBrowser> browser);
void PersistSession() const;
void MaybeFinishShutdown();
HINSTANCE instance_ = nullptr;
@@ -57,6 +65,7 @@ private:
int show_command_ = SW_SHOWDEFAULT;
bool closing_ = false;
bool chrome_ready_ = false;
bool content_fullscreen_ = false;
std::unique_ptr<nebula::window::NebulaWindow> window_;
nebula::browser::TabManager tabs_;
@@ -65,6 +74,8 @@ private:
CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
std::unordered_set<std::string> insecure_warning_bypasses_;
std::vector<std::string> site_history_;
};
} // namespace nebula::app
+40 -1
View File
@@ -9,10 +9,30 @@
namespace nebula::app {
namespace {
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
void EnableDpiAwareness() {
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
class ScopedHandle {
public:
explicit ScopedHandle(HANDLE handle) : handle_(handle) {}
~ScopedHandle() {
if (handle_) {
CloseHandle(handle_);
}
}
ScopedHandle(const ScopedHandle&) = delete;
ScopedHandle& operator=(const ScopedHandle&) = delete;
bool valid() const { return handle_ != nullptr; }
private:
HANDLE handle_ = nullptr;
};
} // namespace
int RunNebula(HINSTANCE instance, int show_command) {
@@ -26,8 +46,27 @@ int RunNebula(HINSTANCE instance, int show_command) {
return subprocess_exit_code;
}
ScopedHandle main_instance_mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName));
if (main_instance_mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS) {
return 0;
}
CefSettings settings;
settings.no_sandbox = true;
settings.persist_session_cookies = true;
// A persistent profile is required for the GPU shader cache and several
// hardware acceleration features. Without these Chromium silently falls
// back to software rendering, which causes choppy video and disables
// WebGL/WebGL2 in the GPU diagnostics page.
const std::wstring user_data_dir = nebula::ui::GetUserDataDirectory().wstring();
const std::wstring cache_dir = nebula::ui::GetCacheDirectory().wstring();
if (!user_data_dir.empty()) {
CefString(&settings.root_cache_path).FromWString(user_data_dir);
}
if (!cache_dir.empty()) {
CefString(&settings.cache_path).FromWString(cache_dir);
}
if (!CefInitialize(main_args, settings, app, nullptr)) {
return CefGetExitCode();
@@ -37,7 +76,7 @@ int RunNebula(HINSTANCE instance, int show_command) {
command_line->InitFromString(GetCommandLineW());
std::string initial_url = command_line->GetSwitchValue("url");
if (nebula::ui::IsEmptyOrChromiumNewTabUrl(initial_url)) {
if (!initial_url.empty() && nebula::ui::IsChromiumNewTabUrl(initial_url)) {
initial_url = nebula::ui::GetHomeUrl();
}
+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
+35 -1
View File
@@ -1,6 +1,9 @@
#include "browser/tab_manager.h"
#include <algorithm>
#include "browser/url_utils.h"
#include "ui/paths.h"
namespace nebula::browser {
@@ -27,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_) {
@@ -49,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;
@@ -134,7 +168,7 @@ void TabManager::LoadURL(const std::string& input) {
tab->url = target;
tab->favicon_url.clear();
tab->browser->GetMainFrame()->LoadURL(target);
tab->browser->GetMainFrame()->LoadURL(nebula::ui::ResolveInternalUrl(target));
Notify();
}
+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);
+7 -2
View File
@@ -1,5 +1,6 @@
#include "browser/url_utils.h"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <sstream>
@@ -19,13 +20,17 @@ std::string Trim(std::string value) {
return value;
}
bool StartsWithScheme(const std::string& value) {
bool StartsWithScheme(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value.starts_with("http://") ||
value.starts_with("https://") ||
value.starts_with("file:") ||
value.starts_with("data:") ||
value.starts_with("blob:") ||
value.starts_with("chrome:");
value.starts_with("chrome:") ||
value.starts_with("nebula://");
}
bool LooksLikeHostName(const std::string& value) {
+66 -8
View File
@@ -9,6 +9,22 @@ namespace {
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
bool IsInsecureInterstitialFrame(CefRefPtr<CefFrame> frame) {
if (!frame) {
return false;
}
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://insecure");
}
bool IsSettingsFrame(CefRefPtr<CefFrame> frame) {
if (!frame) {
return false;
}
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings");
}
std::vector<std::string> ToStringVector(const std::vector<CefString>& values) {
std::vector<std::string> result;
result.reserve(values.size());
@@ -29,17 +45,32 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
CefRefPtr<CefProcessMessage> message) {
CEF_REQUIRE_UI_THREAD();
UNREFERENCED_PARAMETER(browser);
UNREFERENCED_PARAMETER(frame);
UNREFERENCED_PARAMETER(source_process);
if ((role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup) || !message ||
message->GetName().ToString() != kChromeCommandMessage) {
if (!message || message->GetName().ToString() != kChromeCommandMessage) {
return false;
}
if (role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup &&
role_ != BrowserRole::Content) {
return false;
}
CefRefPtr<CefListValue> args = message->GetArgumentList();
const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : "";
const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : "";
if (role_ == BrowserRole::Content) {
const bool allowed_insecure_command =
command == "navigate-insecure" && IsInsecureInterstitialFrame(frame);
const bool allowed_settings_command =
IsSettingsFrame(frame) && (command == "navigate" ||
command == "clear-site-history" ||
command == "clear-search-history");
if (!allowed_insecure_command && !allowed_settings_command) {
return false;
}
}
if (delegate_ && !command.empty()) {
delegate_->OnChromeCommand(command, payload);
return true;
@@ -73,6 +104,13 @@ void NebulaBrowserClient::OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
}
}
void NebulaBrowserClient::OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) {
CEF_REQUIRE_UI_THREAD();
if (role_ == BrowserRole::Content && delegate_) {
delegate_->OnContentFullscreenChanged(browser, fullscreen);
}
}
bool NebulaBrowserClient::OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
const CefKeyEvent& event,
CefEventHandle os_event,
@@ -172,10 +210,16 @@ void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) {
CEF_REQUIRE_UI_THREAD();
UNREFERENCED_PARAMETER(httpStatusCode);
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
if (httpStatusCode == 404) {
frame->LoadURL(nebula::ui::ResolveInternalUrl(
nebula::ui::GetNotFoundUrl(frame->GetURL().ToString())));
return;
}
delegate_->OnContentLoadProgressChanged(browser, 1.0);
delegate_->OnContentLoadFinished(browser, frame->GetURL().ToString());
}
}
@@ -189,10 +233,24 @@ bool NebulaBrowserClient::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
UNREFERENCED_PARAMETER(user_gesture);
UNREFERENCED_PARAMETER(is_redirect);
if (role_ == BrowserRole::Content && frame && frame->IsMain() && request &&
nebula::ui::IsChromiumNewTabUrl(request->GetURL().ToString())) {
frame->LoadURL(nebula::ui::GetHomeUrl());
return true;
if (role_ == BrowserRole::Content && frame && frame->IsMain() && request) {
const std::string url = request->GetURL().ToString();
if (nebula::ui::IsChromiumNewTabUrl(url)) {
frame->LoadURL(nebula::ui::ResolveInternalUrl(nebula::ui::GetHomeUrl()));
return true;
}
if (nebula::ui::IsNebulaInternalUrl(url)) {
frame->LoadURL(nebula::ui::ResolveInternalUrl(url));
return true;
}
if (nebula::ui::IsHttpUrl(url) &&
(!delegate_ || !delegate_->ShouldBypassInsecureWarning(browser, url))) {
frame->LoadURL(nebula::ui::ResolveInternalUrl(
nebula::ui::GetInsecureWarningUrl(url)));
return true;
}
}
return false;
+4
View File
@@ -29,8 +29,11 @@ public:
virtual void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) = 0;
virtual void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) = 0;
virtual void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) = 0;
virtual void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
virtual void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0;
virtual void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) = 0;
virtual void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
virtual bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
};
class NebulaBrowserClient final : public CefClient,
@@ -62,6 +65,7 @@ public:
const CefString& title) override;
void OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
const std::vector<CefString>& icon_urls) override;
void OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
const CefKeyEvent& event,
+34 -2
View File
@@ -18,7 +18,7 @@ public:
UNREFERENCED_PARAMETER(object);
UNREFERENCED_PARAMETER(retval);
if (name != "postMessage") {
if (name != "postMessage" && name != "sendToHost" && name != "send") {
return false;
}
@@ -58,6 +58,26 @@ void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type,
// The bundled UI is loaded from file:// and uses ES modules.
command_line->AppendSwitch("allow-file-access-from-files");
// CefSettings.no_sandbox disables the browser-level sandbox, but Chromium
// still attempts to bring up a separate GPU sandbox inside the GPU process.
// Without the host-side sandbox plumbing this fails with STATUS_BREAKPOINT
// (-2147483645) immediately on startup, which is exactly what the GPU
// diagnostics page was showing - the GPU process crashed three times and
// Chromium then fell back to software rendering. Disabling the GPU sandbox
// matches the rest of our no_sandbox configuration and lets the GPU
// process initialize.
command_line->AppendSwitch("no-sandbox");
command_line->AppendSwitch("disable-gpu-sandbox");
command_line->AppendSwitch("in-process-gpu");
// Avoid Chromium's conservative GPU blocklist, but let Chromium choose the
// safest graphics backend for this machine. Forcing raster/zero-copy paths
// can prevent WebGL shared contexts from initializing on some drivers.
command_line->AppendSwitch("ignore-gpu-blocklist");
command_line->AppendSwitch("enable-accelerated-video-decode");
command_line->AppendSwitchWithValue("use-gl", "angle");
command_line->AppendSwitchWithValue("use-angle", "d3d11");
}
void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
@@ -68,12 +88,24 @@ void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
UNREFERENCED_PARAMETER(frame);
CefRefPtr<CefV8Value> global = context->GetGlobal();
CefRefPtr<NativeBridgeHandler> handler = new NativeBridgeHandler();
CefRefPtr<CefV8Value> native = CefV8Value::CreateObject(nullptr, nullptr);
native->SetValue(
"postMessage",
CefV8Value::CreateFunction("postMessage", new NativeBridgeHandler()),
CefV8Value::CreateFunction("postMessage", handler),
V8_PROPERTY_ATTRIBUTE_NONE);
global->SetValue("nebulaNative", native, V8_PROPERTY_ATTRIBUTE_READONLY);
CefRefPtr<CefV8Value> electron_api = CefV8Value::CreateObject(nullptr, nullptr);
electron_api->SetValue(
"sendToHost",
CefV8Value::CreateFunction("sendToHost", handler),
V8_PROPERTY_ATTRIBUTE_NONE);
electron_api->SetValue(
"send",
CefV8Value::CreateFunction("send", handler),
V8_PROPERTY_ATTRIBUTE_NONE);
global->SetValue("electronAPI", electron_api, V8_PROPERTY_ATTRIBUTE_READONLY);
}
} // namespace nebula::cef
+192 -9
View File
@@ -4,10 +4,31 @@
#include <algorithm>
#include <cctype>
#include <string_view>
namespace nebula::ui {
namespace {
constexpr std::string_view kNebulaScheme = "nebula://";
constexpr std::wstring_view kInternalFallbackPage = L"404.html";
struct InternalPage {
std::string_view slug;
std::wstring_view file_name;
};
constexpr InternalPage kInternalPages[] = {
{"home", L"home.html"},
{"settings", L"settings.html"},
{"downloads", L"downloads.html"},
{"bigpicture", L"bigpicture.html"},
{"big-picture", L"bigpicture.html"},
{"gpu-diagnostics", L"gpu-diagnostics.html"},
{"setup", L"setup.html"},
{"404", L"404.html"},
{"insecure", L"insecure.html"},
};
std::string WideToUtf8(const std::wstring& value) {
if (value.empty()) {
return {};
@@ -31,6 +52,11 @@ std::string GetUrlWithoutDecoration(std::string url) {
return url;
}
std::string GetUrlDecoration(const std::string& url) {
const size_t split = url.find_first_of("?#");
return split == std::string::npos ? std::string{} : url.substr(split);
}
std::string ToLowerAscii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
@@ -38,6 +64,66 @@ std::string ToLowerAscii(std::string value) {
return value;
}
std::string PageFileUrl(std::wstring_view page_name) {
const auto path = GetUiPagePath(std::wstring(page_name));
return path.empty() ? std::string{} : FilePathToUrl(path);
}
std::string PercentEncode(const std::string& value) {
constexpr char kHex[] = "0123456789ABCDEF";
std::string encoded;
encoded.reserve(value.size());
for (unsigned char ch : value) {
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
encoded += static_cast<char>(ch);
} else {
encoded += '%';
encoded += kHex[ch >> 4];
encoded += kHex[ch & 0x0F];
}
}
return encoded;
}
std::string InternalPageName(const std::string& url) {
std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
if (!target.starts_with(kNebulaScheme)) {
return {};
}
target.erase(0, kNebulaScheme.size());
while (!target.empty() && target.front() == '/') {
target.erase(target.begin());
}
while (!target.empty() && target.back() == '/') {
target.pop_back();
}
return target.empty() ? "home" : target;
}
std::string InternalUrlForSlug(std::string_view slug) {
return std::string(kNebulaScheme) + std::string(slug);
}
const InternalPage* FindInternalPageBySlug(std::string_view slug) {
for (const auto& page : kInternalPages) {
if (page.slug == slug) {
return &page;
}
}
return nullptr;
}
const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
const std::string base_url = GetUrlWithoutDecoration(url);
for (const auto& page : kInternalPages) {
if (PageFileUrl(page.file_name) == base_url) {
return &page;
}
}
return nullptr;
}
} // namespace
std::filesystem::path GetExecutableDirectory() {
@@ -50,6 +136,49 @@ std::filesystem::path GetExecutableDirectory() {
return std::filesystem::path(exe_path).parent_path();
}
std::filesystem::path GetUserDataDirectory() {
std::filesystem::path root;
wchar_t buffer[MAX_PATH] = {};
// Prefer %LOCALAPPDATA% so the profile follows Chromium conventions and
// survives executable relocation.
const DWORD length =
GetEnvironmentVariableW(L"LOCALAPPDATA", buffer, MAX_PATH);
if (length > 0 && length < MAX_PATH) {
root = std::filesystem::path(buffer);
} else {
// Fall back to a directory next to the executable so a portable
// install still gets a writable profile.
root = GetExecutableDirectory();
}
if (root.empty()) {
return {};
}
std::filesystem::path user_data = root / L"Nebula" / L"User Data";
std::error_code ec;
std::filesystem::create_directories(user_data, ec);
return user_data;
}
std::filesystem::path GetCacheDirectory() {
auto user_data = GetUserDataDirectory();
if (user_data.empty()) {
return {};
}
std::filesystem::path cache = user_data / L"Cache";
std::error_code ec;
std::filesystem::create_directories(cache, ec);
return cache;
}
std::filesystem::path GetSessionStatePath() {
auto user_data = GetUserDataDirectory();
return user_data.empty() ? std::filesystem::path{} : user_data / L"session_state.json";
}
std::filesystem::path GetUiPagePath(const std::wstring& page_name) {
const auto exe_dir = GetExecutableDirectory();
if (exe_dir.empty()) {
@@ -77,31 +206,85 @@ std::string FilePathToUrl(std::filesystem::path path) {
std::string GetChromeUrl() {
const auto path = GetUiPagePath(L"chrome.html");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
const std::string fallback = PageFileUrl(L"home.html");
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
}
std::string GetHomeUrl() {
const auto path = GetUiPagePath(L"home.html");
return path.empty() ? "https://www.google.com" : FilePathToUrl(path);
return InternalUrlForSlug("home");
}
std::string GetSettingsUrl() {
const auto path = GetUiPagePath(L"settings.html");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
return InternalUrlForSlug("settings");
}
std::string GetDownloadsUrl() {
return InternalUrlForSlug("downloads");
}
std::string GetBigPictureUrl() {
const auto path = GetUiPagePath(L"bigpicture.html");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
return InternalUrlForSlug("bigpicture");
}
std::string GetGpuDiagnosticsUrl() {
return InternalUrlForSlug("gpu-diagnostics");
}
std::string GetMenuPopupUrl() {
const auto path = GetUiPagePath(L"menu-popup.html");
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
const std::string fallback = PageFileUrl(L"home.html");
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
}
std::string GetInsecureWarningUrl(const std::string& target_url) {
return InternalUrlForSlug("insecure") + "?target=" + PercentEncode(target_url);
}
std::string GetNotFoundUrl(const std::string& target_url) {
return InternalUrlForSlug("404") + "?url=" + PercentEncode(target_url);
}
std::string ResolveInternalUrl(const std::string& url) {
const std::string page_name = InternalPageName(url);
if (page_name.empty()) {
return url;
}
if (const auto* page = FindInternalPageBySlug(page_name)) {
const std::string file_url = PageFileUrl(page->file_name);
return file_url.empty() ? url : file_url + GetUrlDecoration(url);
}
const std::string fallback_url = PageFileUrl(kInternalFallbackPage);
return fallback_url.empty() ? url : fallback_url + "?url=" + PercentEncode(url);
}
std::string ToInternalUrl(const std::string& url) {
const std::string page_name = InternalPageName(url);
if (!page_name.empty()) {
if (const auto* page = FindInternalPageBySlug(page_name)) {
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
}
return url;
}
if (const auto* page = FindInternalPageByFileUrl(url)) {
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
}
return url;
}
bool IsInternalHomeUrl(const std::string& url) {
return GetUrlWithoutDecoration(url) == GetHomeUrl();
return GetUrlWithoutDecoration(ToInternalUrl(url)) == GetHomeUrl();
}
bool IsNebulaInternalUrl(const std::string& url) {
return ToLowerAscii(url).starts_with(kNebulaScheme);
}
bool IsHttpUrl(const std::string& url) {
return ToLowerAscii(url).starts_with("http://");
}
bool IsChromiumNewTabUrl(const std::string& url) {
+11
View File
@@ -6,15 +6,26 @@
namespace nebula::ui {
std::filesystem::path GetExecutableDirectory();
std::filesystem::path GetUserDataDirectory();
std::filesystem::path GetCacheDirectory();
std::filesystem::path GetSessionStatePath();
std::filesystem::path GetUiPagePath(const std::wstring& page_name);
std::string FilePathToUrl(std::filesystem::path path);
std::string GetChromeUrl();
std::string GetHomeUrl();
std::string GetSettingsUrl();
std::string GetDownloadsUrl();
std::string GetBigPictureUrl();
std::string GetGpuDiagnosticsUrl();
std::string GetMenuPopupUrl();
std::string GetInsecureWarningUrl(const std::string& target_url);
std::string GetNotFoundUrl(const std::string& target_url);
std::string ResolveInternalUrl(const std::string& url);
std::string ToInternalUrl(const std::string& url);
bool IsInternalHomeUrl(const std::string& url);
bool IsNebulaInternalUrl(const std::string& url);
bool IsHttpUrl(const std::string& url);
bool IsChromiumNewTabUrl(const std::string& url);
bool IsEmptyOrChromiumNewTabUrl(const std::string& url);
+116 -4
View File
@@ -15,6 +15,7 @@ constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestPare
constexpr int kTitleRowHeightDip = 42;
constexpr int kWindowControlWidthDip = 46;
constexpr int kWindowControlCount = 3;
constexpr COLORREF kNoWindowBorderColor = 0xFFFFFFFE;
RECT GetWorkArea() {
RECT work_area = {};
@@ -22,6 +23,30 @@ RECT GetWorkArea() {
return work_area;
}
RECT GetMonitorWorkArea(HWND hwnd) {
MONITORINFO monitor_info = {};
monitor_info.cbSize = sizeof(monitor_info);
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
return monitor_info.rcWork;
}
return GetWorkArea();
}
RECT GetMonitorArea(HWND hwnd) {
MONITORINFO monitor_info = {};
monitor_info.cbSize = sizeof(monitor_info);
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
return monitor_info.rcMonitor;
}
return GetWorkArea();
}
bool IsResizeHit(LRESULT hit) {
return hit == HTLEFT || hit == HTRIGHT || hit == HTTOP || hit == HTBOTTOM ||
hit == HTTOPLEFT || hit == HTTOPRIGHT || hit == HTBOTTOMLEFT || hit == HTBOTTOMRIGHT;
@@ -56,6 +81,28 @@ bool SetResizeCursor(LRESULT hit) {
return true;
}
void ApplyWindowFrameStyle(HWND hwnd) {
const BOOL dark_mode = TRUE;
const DWM_WINDOW_CORNER_PREFERENCE corner_preference = DWMWCP_ROUND;
DwmSetWindowAttribute(
hwnd,
DWMWA_USE_IMMERSIVE_DARK_MODE,
&dark_mode,
sizeof(dark_mode));
DwmSetWindowAttribute(
hwnd,
DWMWA_WINDOW_CORNER_PREFERENCE,
&corner_preference,
sizeof(corner_preference));
DwmSetWindowAttribute(
hwnd,
DWMWA_BORDER_COLOR,
&kNoWindowBorderColor,
sizeof(kNoWindowBorderColor));
}
} // namespace
NebulaWindow::NebulaWindow(WindowDelegate* delegate) : delegate_(delegate) {}
@@ -92,8 +139,9 @@ bool NebulaWindow::Create(HINSTANCE instance, int show_command) {
}
UpdateDpi();
ApplyWindowFrameStyle(hwnd_);
const MARGINS margins = {1, 1, 1, 1};
const MARGINS margins = {0, 0, 0, 0};
DwmExtendFrameIntoClientArea(hwnd_, &margins);
ShowWindow(hwnd_, show_command);
@@ -101,14 +149,16 @@ bool NebulaWindow::Create(HINSTANCE instance, int show_command) {
return true;
}
BrowserLayout NebulaWindow::CurrentLayout() const {
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
RECT client = {};
if (hwnd_) {
GetClientRect(hwnd_, &client);
}
BrowserLayout layout;
layout.chrome = {0, 0, client.right, std::min<LONG>(ScaleForDpi(chrome_height_dip_), client.bottom)};
layout.chrome = show_chrome
? RECT{0, 0, client.right, std::min<LONG>(ScaleForDpi(chrome_height_dip_), client.bottom)}
: RECT{0, 0, 0, 0};
layout.content = {0, layout.chrome.bottom, client.right, client.bottom};
return layout;
}
@@ -136,13 +186,55 @@ void NebulaWindow::Minimize() {
}
void NebulaWindow::ToggleMaximize() {
if (!hwnd_) {
if (!hwnd_ || fullscreen_) {
return;
}
ShowWindow(hwnd_, IsZoomed(hwnd_) ? SW_RESTORE : SW_MAXIMIZE);
}
void NebulaWindow::SetFullscreen(bool fullscreen) {
if (!hwnd_ || fullscreen_ == fullscreen) {
return;
}
if (fullscreen) {
restore_style_ = GetWindowLongPtrW(hwnd_, GWL_STYLE);
restore_ex_style_ = GetWindowLongPtrW(hwnd_, GWL_EXSTYLE);
restore_placement_.length = sizeof(restore_placement_);
GetWindowPlacement(hwnd_, &restore_placement_);
fullscreen_ = true;
const RECT monitor = GetMonitorArea(hwnd_);
SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_ & ~(WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX));
SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_);
SetWindowPos(
hwnd_,
HWND_TOPMOST,
monitor.left,
monitor.top,
monitor.right - monitor.left,
monitor.bottom - monitor.top,
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
} else {
fullscreen_ = false;
SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_);
SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_);
SetWindowPlacement(hwnd_, &restore_placement_);
SetWindowPos(
hwnd_,
HWND_NOTOPMOST,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
ApplyWindowFrameStyle(hwnd_);
}
NotifyResize();
}
void NebulaWindow::Close() {
if (hwnd_) {
SendMessageW(hwnd_, WM_CLOSE, 0, 0);
@@ -265,6 +357,10 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) {
}
break;
case WM_NCACTIVATE:
ApplyWindowFrameStyle(hwnd_);
return TRUE;
case WM_ERASEBKGND:
return 1;
@@ -307,6 +403,18 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) {
return 0;
}
case WM_GETMINMAXINFO: {
const RECT work_area = GetMonitorWorkArea(hwnd_);
const RECT monitor_area = GetMonitorArea(hwnd_);
auto* minmax = reinterpret_cast<MINMAXINFO*>(lparam);
minmax->ptMaxPosition.x = work_area.left - monitor_area.left;
minmax->ptMaxPosition.y = work_area.top - monitor_area.top;
minmax->ptMaxSize.x = work_area.right - work_area.left;
minmax->ptMaxSize.y = work_area.bottom - work_area.top;
return 0;
}
case WM_CLOSE:
if (delegate_) {
delegate_->OnWindowCloseRequested();
@@ -381,6 +489,10 @@ LRESULT NebulaWindow::HitTestPoint(POINT point) const {
RECT window = {};
GetWindowRect(hwnd_, &window);
if (fullscreen_ || IsZoomed(hwnd_)) {
return HTCLIENT;
}
const int resize_border = ScaleForDpi(resize_border_dip_);
const bool left = point.x >= window.left && point.x < window.left + resize_border;
const bool right = point.x < window.right && point.x >= window.right - resize_border;
+6 -1
View File
@@ -26,11 +26,12 @@ public:
bool Create(HINSTANCE instance, int show_command);
HWND hwnd() const { return hwnd_; }
BrowserLayout CurrentLayout() const;
BrowserLayout CurrentLayout(bool show_chrome = true) const;
void ResizeChild(HWND child, const RECT& rect) const;
void Minimize();
void ToggleMaximize();
void SetFullscreen(bool fullscreen);
void Close();
void BeginDrag();
void SetTitle(const std::wstring& title);
@@ -53,6 +54,10 @@ private:
WindowDelegate* delegate_ = nullptr;
HINSTANCE instance_ = nullptr;
HWND hwnd_ = nullptr;
bool fullscreen_ = false;
LONG_PTR restore_style_ = 0;
LONG_PTR restore_ex_style_ = 0;
WINDOWPLACEMENT restore_placement_ = {sizeof(WINDOWPLACEMENT)};
UINT dpi_ = 96;
int resize_border_dip_ = 8;
int chrome_height_dip_ = 104;
-20
View File
@@ -78,26 +78,6 @@ button:disabled {
background: var(--bg);
}
/* ── Brand ──────────────────────────────────────────────────── */
.brand {
display: flex;
align-items: center;
gap: 7px;
min-width: 104px;
color: var(--accent-2);
font-size: 0.73rem;
font-weight: 800;
letter-spacing: 0.15em;
text-transform: uppercase;
opacity: 0.85;
}
.brand-icon {
width: 17px;
height: 17px;
}
/* ── Tabs ───────────────────────────────────────────────────── */
.tabs {
-24
View File
@@ -338,20 +338,6 @@ function initNavigation() {
launchNebot.addEventListener('click', () => navigateTo('nebula://nebot'));
}
// History section buttons
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener('click', clearHistory);
}
const refreshHistoryBtn = document.getElementById('refreshHistoryBtn');
if (refreshHistoryBtn) {
refreshHistoryBtn.addEventListener('click', async () => {
await loadHistory();
showToast('History refreshed');
});
}
// Bookmarks actions
const addBookmarkBtn = document.getElementById('addBookmarkBtn');
if (addBookmarkBtn) {
@@ -1481,8 +1467,6 @@ async function loadHistory() {
const stored = localStorage.getItem('siteHistory');
state.history = stored ? JSON.parse(stored) : [];
}
renderHistory();
renderRecentSites();
} catch (err) {
console.error('[BigPicture] Failed to load history:', err);
state.history = [];
@@ -1505,8 +1489,6 @@ async function saveToHistory(url) {
if (history.length > 100) history = history.slice(0, 100);
localStorage.setItem('siteHistory', JSON.stringify(history));
state.history = history;
renderHistory();
renderRecentSites();
}
} catch (err) {
console.error('[BigPicture] Failed to save history:', err);
@@ -1522,8 +1504,6 @@ async function clearHistory() {
localStorage.removeItem('siteHistory');
}
state.history = [];
renderHistory();
renderRecentSites();
showToast('History cleared');
} catch (err) {
console.error('[BigPicture] Failed to clear history:', err);
@@ -2484,8 +2464,6 @@ async function clearAllBrowsingData() {
// Also clear localStorage
localStorage.removeItem('siteHistory');
state.history = [];
renderHistory();
renderRecentSites();
showToast('All browsing data cleared');
playSelectSound();
@@ -2503,8 +2481,6 @@ async function clearBrowsingHistory() {
localStorage.removeItem('siteHistory');
state.history = [];
renderHistory();
renderRecentSites();
showToast('Browsing history cleared');
playSelectSound();
+1 -1
View File
@@ -15,7 +15,7 @@ const state = {
function toNavigationUrl(input) {
const value = (input || '').trim();
if (!value) return null;
if (/^(https?:|file:|data:|blob:|chrome:)/i.test(value)) return value;
if (/^(https?:|file:|data:|blob:|chrome:|nebula:\/\/)/i.test(value)) return value;
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
return `${SEARCH_URL}${encodeURIComponent(value)}`;
}
+1 -1
View File
@@ -88,7 +88,7 @@ function applySelectedSearchEngine(engine) {
function normalizeNavigationUrl(input) {
const value = (input || '').trim();
if (!value) return null;
if (/^(https?:|file:|data:|blob:)/i.test(value)) return value;
if (/^(https?:|file:|data:|blob:|nebula:\/\/)/i.test(value)) return value;
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
return `${searchEngines[selectedSearchEngine]}${encodeURIComponent(value)}`;
}
+1 -1
View File
@@ -61,7 +61,7 @@
const attemptedUrl = params.get('url');
const box = document.getElementById('targetBox');
if (attemptedUrl) {
box.textContent = decodeURIComponent(attemptedUrl);
box.textContent = attemptedUrl;
} else {
box.textContent = 'Unknown URL';
}
-32
View File
@@ -72,10 +72,6 @@
<span class="material-symbols-outlined">bookmarks</span>
<span class="nav-label">Bookmarks</span>
</button>
<button class="nav-item" data-section="history" data-focusable tabindex="0">
<span class="material-symbols-outlined">history</span>
<span class="nav-label">History</span>
</button>
<button class="nav-item" data-section="downloads" data-focusable tabindex="0">
<span class="material-symbols-outlined">download</span>
<span class="nav-label">Downloads</span>
@@ -123,13 +119,6 @@
</div>
</div>
<!-- Recent sites -->
<div class="recent-sites">
<h2 class="subsection-title">Continue Browsing</h2>
<div class="horizontal-scroll" id="recentSitesScroll">
<!-- Recent sites will be populated dynamically -->
</div>
</div>
</section>
<!-- Browse section (for webview) -->
@@ -158,27 +147,6 @@
</div>
</section>
<!-- History section -->
<section id="section-history" class="bp-section">
<div class="section-header">
<h1 class="section-title">History</h1>
<p class="section-subtitle">Recently visited sites</p>
</div>
<div class="section-actions">
<button class="action-btn" id="clearHistoryBtn" data-focusable tabindex="0">
<span class="material-symbols-outlined">delete_sweep</span>
<span>Clear History</span>
</button>
<button class="action-btn" id="refreshHistoryBtn" data-focusable tabindex="0">
<span class="material-symbols-outlined">refresh</span>
<span>Refresh</span>
</button>
</div>
<div class="list-container" id="historyList">
<!-- History will be populated dynamically -->
</div>
</section>
<!-- Downloads section -->
<section id="section-downloads" class="bp-section">
<div class="section-header">
-5
View File
@@ -9,11 +9,6 @@
<body>
<div class="nebula-chrome" data-drag-region>
<div class="title-row" data-drag-region>
<div class="brand" data-drag-region>
<img src="../assets/images/branding/Nebula-Icon.svg" alt="" class="brand-icon">
<span>Nebula</span>
</div>
<div class="tabs" role="tablist" aria-label="Nebula tabs">
<button id="active-tab" class="tab active" type="button" role="tab" aria-selected="true">
<span id="tab-favicon" class="tab-favicon"></span>
File diff suppressed because it is too large Load Diff
+4
View File
@@ -62,6 +62,10 @@
const box = document.getElementById('targetBox');
if (target) box.textContent = target;
function sendNavigate(url, opts){
if (opts && opts.insecureBypass && window.nebulaNative && window.nebulaNative.postMessage){
window.nebulaNative.postMessage('navigate-insecure', url);
return;
}
if (window.electronAPI && window.electronAPI.sendToHost){
window.electronAPI.sendToHost('navigate', url, opts||{});
} else if (window.parent && window.parent !== window) {
+1
View File
@@ -9,6 +9,7 @@
<div id="menu-popup" role="menu">
<button data-cmd="open-settings" role="menuitem">Settings</button>
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</button>
<button data-cmd="gpu-diagnostics" role="menuitem">GPU Diagnostics</button>
<button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
<div class="zoom-controls" role="group" aria-label="Zoom controls">
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
+8 -2
View File
@@ -293,12 +293,12 @@
<div class="customization-group about-actions">
<button id="copy-about-btn">Copy diagnostics</button>
<a id="github-link" href="https://github.com/Bobbybear007/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
<a id="github-link" href="https://gitpub.zambazosmedia.group/#repo/nebula-project/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
<!-- GitHub mark (Octicons) MIT License -->
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false" role="img">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.01.08-2.11 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.91.08 2.11.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span>GitHub</span>
<span>Gitpub</span>
</a>
<a id="help-link" href="https://nebula.zambazosmedia.group" class="help-btn" rel="noopener noreferrer">
<!-- Help icon -->
@@ -534,6 +534,12 @@
async function clearSiteHistory() {
try {
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('clear-site-history');
} else if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') {
window.nebulaNative.postMessage('clear-site-history');
}
// Clear from localStorage
localStorage.removeItem('siteHistory');
console.log('[SETTINGS DEBUG] Cleared site history from localStorage');