Add default browser & external-open support

Introduce default-browser integration and external open handling across platforms. Added platform/default_browser.h with a Windows implementation (registry registration + settings UI invocation) and mac/linux stubs. Exposed new commands from the renderer (check-default-browser / set-default-browser) and implemented request/response plumbing in NebulaController (SendDefaultBrowserResult) and BrowserClient. Added UI controls and JS helpers in settings and setup pages to check and prompt the user to make Nebula the default browser.

Also added single-instance launch target handling: command-line URL normalization, passing/consolidating the launch target, forwarding it to an existing window on Windows via WM_COPYDATA, and exposing OnExternalOpenRequested on the window/controller. Implemented delayed navigation (CefTask) to safely load pending initial URLs after CEF initialization. Updated CMakeLists and platform startup signatures to include and accept the new files/parameters.
This commit is contained in:
Andrew Zambazos
2026-05-20 21:05:59 +12:00
parent 659d1530b0
commit ce92b3841f
18 changed files with 748 additions and 17 deletions
+3
View File
@@ -54,6 +54,7 @@ set(NEBULA_COMMON_SOURCES
if(OS_WINDOWS) if(OS_WINDOWS)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
src/platform/win/default_browser_win.cpp
src/platform/win/paths_win.cpp src/platform/win/paths_win.cpp
src/platform/win/startup_win.cpp src/platform/win/startup_win.cpp
src/platform/win/browser_host_win.cpp src/platform/win/browser_host_win.cpp
@@ -61,6 +62,7 @@ if(OS_WINDOWS)
) )
elseif(OS_MACOSX) elseif(OS_MACOSX)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
src/platform/mac/default_browser_mac.mm
src/platform/mac/paths_mac.cpp src/platform/mac/paths_mac.cpp
src/platform/mac/startup_mac.mm src/platform/mac/startup_mac.mm
src/platform/mac/browser_host_mac.mm src/platform/mac/browser_host_mac.mm
@@ -75,6 +77,7 @@ elseif(OS_MACOSX)
) )
elseif(OS_LINUX) elseif(OS_LINUX)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
src/platform/linux/default_browser_linux.cpp
src/platform/linux/paths_linux.cpp src/platform/linux/paths_linux.cpp
src/platform/linux/startup_linux.cpp src/platform/linux/startup_linux.cpp
src/platform/linux/browser_host_linux.cpp src/platform/linux/browser_host_linux.cpp
+107
View File
@@ -13,8 +13,10 @@
#include "include/cef_app.h" #include "include/cef_app.h"
#include "include/cef_browser.h" #include "include/cef_browser.h"
#include "include/cef_cookie.h" #include "include/cef_cookie.h"
#include "include/cef_task.h"
#include "include/wrapper/cef_helpers.h" #include "include/wrapper/cef_helpers.h"
#include "platform/browser_host.h" #include "platform/browser_host.h"
#include "platform/default_browser.h"
#include "ui/paths.h" #include "ui/paths.h"
namespace nebula::app { namespace nebula::app {
@@ -203,6 +205,7 @@ void NebulaController::OnWindowCreated() {
} else if (initial_url_.empty()) { } else if (initial_url_.empty()) {
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl()); tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
} else { } else {
// Create tab directly with the external URL - no delayed navigation needed
tabs_.CreateInitialTab(initial_url_); tabs_.CreateInitialTab(initial_url_);
} }
PersistSession(); PersistSession();
@@ -231,6 +234,25 @@ void NebulaController::OnWindowCloseRequested() {
BeginShutdown(); BeginShutdown();
} }
void NebulaController::OnExternalOpenRequested(const std::string& target) {
if (target.empty()) {
return;
}
if (!chrome_ready_ && !big_picture_ready_) {
pending_initial_navigation_ = target;
return;
}
auto* tab = tabs_.ActiveTab();
if (tab && tab->browser) {
tabs_.LoadURL(target);
return;
}
CreateNewTab(target);
}
void NebulaController::BeginShutdown() { void NebulaController::BeginShutdown() {
if (closing_) { if (closing_) {
if (window_ && window_->native_handle()) { if (window_ && window_->native_handle()) {
@@ -297,6 +319,13 @@ void NebulaController::OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr
if (const auto* tab = tabs_.ActiveTab()) { if (const auto* tab = tabs_.ActiveTab()) {
SendChromeState(*tab); SendChromeState(*tab);
} }
// If we have a pending initial navigation, we might need to load it now
// if the content browser is already ready, or we'll let the content browser
// handle it when it's created.
if (!pending_initial_navigation_.empty() && tabs_.ActiveTab() && tabs_.ActiveTab()->browser) {
LoadPendingNavigationDelayed();
}
} else if (role == nebula::cef::BrowserRole::BigPicture) { } else if (role == nebula::cef::BrowserRole::BigPicture) {
big_picture_browser_ = browser; big_picture_browser_ = browser;
big_picture_ready_ = true; big_picture_ready_ = true;
@@ -311,6 +340,12 @@ void NebulaController::OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr
SendMenuPopupZoom(); SendMenuPopupZoom();
} else { } else {
tabs_.SetActiveBrowser(browser); tabs_.SetActiveBrowser(browser);
// Only load the pending navigation if the UI (chrome or big picture) is ready.
// Otherwise, wait for the UI to be ready.
if (!pending_initial_navigation_.empty() && (chrome_ready_ || big_picture_ready_)) {
LoadPendingNavigationDelayed();
}
} }
ResizeBrowsers(); ResizeBrowsers();
@@ -399,6 +434,15 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
SendThemeToChromeSurfaces(payload); SendThemeToChromeSurfaces(payload);
} else if (command == "complete-first-run") { } else if (command == "complete-first-run") {
CompleteFirstRunSetup(); CompleteFirstRunSetup();
} else if (command == "check-default-browser") {
SendDefaultBrowserResult(payload, true, false);
} else if (command == "set-default-browser") {
const bool success = nebula::platform::RequestDefaultBrowser();
SendDefaultBrowserResult(
payload,
success,
success && !nebula::platform::IsDefaultBrowser(),
success ? std::string{} : "Unable to register Nebula as a browser.");
} else if (command == "clear-site-history") { } else if (command == "clear-site-history") {
site_history_.clear(); site_history_.clear();
SaveSiteHistory(site_history_); SaveSiteHistory(site_history_);
@@ -991,6 +1035,34 @@ void NebulaController::CompleteFirstRunSetup() {
ResizeBrowsers(); ResizeBrowsers();
} }
void NebulaController::SendDefaultBrowserResult(const std::string& request_id,
bool success,
bool needs_user_action,
const std::string& error) {
auto* tab = tabs_.ActiveTab();
if (!tab || !tab->browser) {
return;
}
std::string detail = "{";
detail += "\"requestId\":\"" + nebula::browser::JsonEscape(request_id) + "\"";
detail += ",\"success\":";
detail += success ? "true" : "false";
detail += ",\"isDefault\":";
detail += nebula::platform::IsDefaultBrowser() ? "true" : "false";
detail += ",\"needsUserAction\":";
detail += needs_user_action ? "true" : "false";
if (!error.empty()) {
detail += ",\"error\":\"" + nebula::browser::JsonEscape(error) + "\"";
}
detail += "}";
const std::string script =
"window.dispatchEvent(new CustomEvent('nebula-default-browser-result',{detail:" +
detail + "}));";
tab->browser->GetMainFrame()->ExecuteJavaScript(script, tab->url, 0);
}
void NebulaController::ResizeBrowsers() { void NebulaController::ResizeBrowsers() {
if (!window_) { if (!window_) {
return; return;
@@ -1230,4 +1302,39 @@ bool NebulaController::ForgetClosingTabBrowser(CefRefPtr<CefBrowser> browser) {
return true; return true;
} }
namespace {
class DelayedNavigationTask : public CefTask {
public:
DelayedNavigationTask(nebula::browser::TabManager* tabs, std::string url)
: tabs_(tabs), url_(std::move(url)) {}
void Execute() override {
if (tabs_) {
tabs_->LoadURL(url_);
}
}
private:
nebula::browser::TabManager* tabs_;
std::string url_;
IMPLEMENT_REFCOUNTING(DelayedNavigationTask);
};
} // namespace
void NebulaController::LoadPendingNavigationDelayed() {
if (pending_initial_navigation_.empty()) {
return;
}
const std::string target = std::move(pending_initial_navigation_);
pending_initial_navigation_.clear();
// Post a delayed task to load the URL after CEF has fully initialized.
// This gives CEF time to complete internal setup after browser creation.
CefPostDelayedTask(TID_UI, new DelayedNavigationTask(&tabs_, target), 250);
}
} // namespace nebula::app } // namespace nebula::app
+7
View File
@@ -27,6 +27,7 @@ public:
void OnWindowCreated() override; void OnWindowCreated() override;
void OnWindowResized(const nebula::window::BrowserLayout& layout) override; void OnWindowResized(const nebula::window::BrowserLayout& layout) override;
void OnWindowCloseRequested() override; void OnWindowCloseRequested() override;
void OnExternalOpenRequested(const std::string& target) override;
void OnActiveTabChanged(const nebula::browser::NebulaTab& tab) override; void OnActiveTabChanged(const nebula::browser::NebulaTab& tab) override;
@@ -69,6 +70,10 @@ private:
void SetBigPictureBrowseVisible(bool visible); void SetBigPictureBrowseVisible(bool visible);
void SetContentFullscreen(bool fullscreen); void SetContentFullscreen(bool fullscreen);
void CompleteFirstRunSetup(); void CompleteFirstRunSetup();
void SendDefaultBrowserResult(const std::string& request_id,
bool success,
bool needs_user_action,
const std::string& error = {});
void ResizeBrowsers(); void ResizeBrowsers();
void SendChromeState(const nebula::browser::NebulaTab& tab); void SendChromeState(const nebula::browser::NebulaTab& tab);
void SendBigPictureState(const nebula::browser::NebulaTab& tab); void SendBigPictureState(const nebula::browser::NebulaTab& tab);
@@ -80,9 +85,11 @@ private:
void BeginShutdown(); void BeginShutdown();
void MaybeFinishShutdown(); void MaybeFinishShutdown();
bool ForgetClosingTabBrowser(CefRefPtr<CefBrowser> browser); bool ForgetClosingTabBrowser(CefRefPtr<CefBrowser> browser);
void LoadPendingNavigationDelayed();
nebula::platform::AppStartup startup_; nebula::platform::AppStartup startup_;
std::string initial_url_; std::string initial_url_;
std::string pending_initial_navigation_;
LaunchOptions launch_options_; LaunchOptions launch_options_;
bool closing_ = false; bool closing_ = false;
bool chrome_ready_ = false; bool chrome_ready_ = false;
+108 -9
View File
@@ -1,13 +1,115 @@
#include "app/run.h" #include "app/run.h"
#include "app/nebula_controller.h" #include "app/nebula_controller.h"
#include "browser/url_utils.h"
#include "cef/nebula_app.h" #include "cef/nebula_app.h"
#include "include/cef_app.h" #include "include/cef_app.h"
#include "include/cef_command_line.h" #include "include/cef_command_line.h"
#include "platform/default_browser.h"
#include "platform/startup.h" #include "platform/startup.h"
#include "ui/paths.h" #include "ui/paths.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <system_error>
#include <vector>
#if defined(_WIN32)
#include <windows.h>
#endif
namespace nebula::app { namespace nebula::app {
namespace {
std::string Trim(std::string value) {
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front()))) {
value.erase(value.begin());
}
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back()))) {
value.pop_back();
}
return value;
}
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 StartsWithKnownScheme(const std::string& value) {
const std::string lower = ToLowerAscii(value);
return lower.starts_with("http://") ||
lower.starts_with("https://") ||
lower.starts_with("file:") ||
lower.starts_with("data:") ||
lower.starts_with("blob:") ||
lower.starts_with("chrome:") ||
lower.starts_with("nebula://");
}
std::filesystem::path PathFromUtf8(const std::string& value) {
#if defined(_WIN32)
const int size = MultiByteToWideChar(
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
if (size <= 0) {
return std::filesystem::path(value);
}
std::wstring wide(size, L'\0');
MultiByteToWideChar(
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), wide.data(), size);
return std::filesystem::path(wide);
#else
return std::filesystem::path(value);
#endif
}
std::string NormalizeLaunchTarget(std::string target) {
target = Trim(std::move(target));
if (target.empty() || nebula::ui::IsChromiumNewTabUrl(target)) {
return target.empty() ? std::string{} : nebula::ui::GetHomeUrl();
}
if (StartsWithKnownScheme(target)) {
return target;
}
const std::filesystem::path path = PathFromUtf8(target);
std::error_code ec;
if (!path.empty() && std::filesystem::exists(path, ec) && !ec) {
const std::filesystem::path absolute_path = std::filesystem::absolute(path, ec);
return nebula::ui::FilePathToUrl(ec ? path : absolute_path);
}
return nebula::browser::NormalizeNavigationInput(target);
}
std::string GetLaunchTarget(CefRefPtr<CefCommandLine> command_line) {
if (!command_line) {
return {};
}
std::string target = command_line->GetSwitchValue("url");
if (!target.empty()) {
return NormalizeLaunchTarget(std::move(target));
}
std::vector<CefString> arguments;
command_line->GetArguments(arguments);
for (const auto& argument : arguments) {
target = argument.ToString();
if (!Trim(target).empty()) {
return NormalizeLaunchTarget(std::move(target));
}
}
return {};
}
} // namespace
int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options) { int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options) {
nebula::platform::PrepareApp(); nebula::platform::PrepareApp();
@@ -20,9 +122,14 @@ int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options
return subprocess_exit_code; return subprocess_exit_code;
} }
if (!nebula::platform::TryAcquireSingleInstance()) { CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
nebula::platform::InitCommandLine(command_line, startup);
std::string initial_url = GetLaunchTarget(command_line);
if (!nebula::platform::TryAcquireSingleInstance(initial_url)) {
return 0; return 0;
} }
nebula::platform::EnsureDefaultBrowserRegistration();
CefSettings settings; CefSettings settings;
settings.no_sandbox = true; settings.no_sandbox = true;
@@ -33,14 +140,6 @@ int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options
return CefGetExitCode(); return CefGetExitCode();
} }
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
nebula::platform::InitCommandLine(command_line, startup);
std::string initial_url = command_line->GetSwitchValue("url");
if (!initial_url.empty() && nebula::ui::IsChromiumNewTabUrl(initial_url)) {
initial_url = nebula::ui::GetHomeUrl();
}
NebulaController controller(startup, std::move(initial_url), options); NebulaController controller(startup, std::move(initial_url), options);
const bool created = controller.Create(); const bool created = controller.Create();
if (created) { if (created) {
+5 -1
View File
@@ -111,10 +111,14 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
const bool allowed_settings_command = const bool allowed_settings_command =
IsSettingsFrame(frame) && (command == "navigate" || IsSettingsFrame(frame) && (command == "navigate" ||
command == "new-tab" || command == "new-tab" ||
command == "check-default-browser" ||
command == "set-default-browser" ||
command == "clear-site-history" || command == "clear-site-history" ||
command == "clear-search-history"); command == "clear-search-history");
const bool allowed_setup_command = const bool allowed_setup_command =
command == "complete-first-run" && IsSetupFrame(frame); IsSetupFrame(frame) && (command == "complete-first-run" ||
command == "check-default-browser" ||
command == "set-default-browser");
const bool allowed_big_picture_command = const bool allowed_big_picture_command =
IsBigPictureFrame(frame) && command == "exit-bigpicture"; IsBigPictureFrame(frame) && command == "exit-bigpicture";
if (!allowed_insecure_command && !allowed_settings_command && if (!allowed_insecure_command && !allowed_settings_command &&
+9
View File
@@ -0,0 +1,9 @@
#pragma once
namespace nebula::platform {
bool IsDefaultBrowser();
bool EnsureDefaultBrowserRegistration();
bool RequestDefaultBrowser();
} // namespace nebula::platform
@@ -0,0 +1,17 @@
#include "platform/default_browser.h"
namespace nebula::platform {
bool IsDefaultBrowser() {
return false;
}
bool EnsureDefaultBrowserRegistration() {
return false;
}
bool RequestDefaultBrowser() {
return false;
}
} // namespace nebula::platform
+3 -1
View File
@@ -18,7 +18,9 @@ int g_single_instance_lock = -1;
void PrepareApp() {} void PrepareApp() {}
bool TryAcquireSingleInstance() { bool TryAcquireSingleInstance(const std::string& launch_target) {
NEBULA_UNUSED(launch_target);
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock"; const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
std::error_code ec; std::error_code ec;
std::filesystem::create_directories(lock_path.parent_path(), ec); std::filesystem::create_directories(lock_path.parent_path(), ec);
+17
View File
@@ -0,0 +1,17 @@
#include "platform/default_browser.h"
namespace nebula::platform {
bool IsDefaultBrowser() {
return false;
}
bool EnsureDefaultBrowserRegistration() {
return false;
}
bool RequestDefaultBrowser() {
return false;
}
} // namespace nebula::platform
+3 -1
View File
@@ -26,7 +26,9 @@ void PrepareApp() {
} }
} }
bool TryAcquireSingleInstance() { bool TryAcquireSingleInstance(const std::string& launch_target) {
NEBULA_UNUSED(launch_target);
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock"; const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
std::error_code ec; std::error_code ec;
std::filesystem::create_directories(lock_path.parent_path(), ec); std::filesystem::create_directories(lock_path.parent_path(), ec);
+3 -1
View File
@@ -3,10 +3,12 @@
#include "include/cef_app.h" #include "include/cef_app.h"
#include "platform/types.h" #include "platform/types.h"
#include <string>
namespace nebula::platform { namespace nebula::platform {
void PrepareApp(); void PrepareApp();
bool TryAcquireSingleInstance(); bool TryAcquireSingleInstance(const std::string& launch_target = {});
CefMainArgs MakeMainArgs(const AppStartup& startup); CefMainArgs MakeMainArgs(const AppStartup& startup);
void InitCommandLine(CefRefPtr<CefCommandLine> command_line, const AppStartup& startup); void InitCommandLine(CefRefPtr<CefCommandLine> command_line, const AppStartup& startup);
void ConfigureCefSettings(CefSettings& settings); void ConfigureCefSettings(CefSettings& settings);
+176
View File
@@ -0,0 +1,176 @@
#include "platform/default_browser.h"
#include <windows.h>
#include <shellapi.h>
#include <filesystem>
#include <string>
#include <string_view>
namespace nebula::platform {
namespace {
constexpr wchar_t kAppName[] = L"Nebula Browser";
constexpr wchar_t kClientKeyName[] = L"NebulaBrowser";
constexpr wchar_t kRegisteredApplicationsKey[] = L"Software\\RegisteredApplications";
constexpr wchar_t kClientRootKey[] = L"Software\\Clients\\StartMenuInternet\\NebulaBrowser";
constexpr wchar_t kFileAssociationsKey[] =
L"Software\\Clients\\StartMenuInternet\\NebulaBrowser\\Capabilities\\FileAssociations";
constexpr wchar_t kUrlAssociationsKey[] =
L"Software\\Clients\\StartMenuInternet\\NebulaBrowser\\Capabilities\\URLAssociations";
constexpr wchar_t kHttpUserChoiceKey[] =
L"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice";
constexpr wchar_t kHttpsUserChoiceKey[] =
L"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice";
constexpr std::wstring_view kWebFileExtensions[] = {
L".htm",
L".html",
L".shtml",
L".xht",
L".xhtml",
L".svg",
L".webp",
};
std::wstring CurrentExecutablePath() {
std::wstring path(MAX_PATH, L'\0');
DWORD length = 0;
while (true) {
length = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
if (length == 0) {
return {};
}
if (length < path.size() - 1) {
path.resize(length);
return path;
}
path.resize(path.size() * 2);
}
}
std::wstring Quote(std::wstring_view value) {
std::wstring quoted = L"\"";
quoted += value;
quoted += L"\"";
return quoted;
}
bool SetStringValue(HKEY root,
const std::wstring& subkey,
const wchar_t* value_name,
const std::wstring& value) {
HKEY key = nullptr;
const LSTATUS status = RegCreateKeyExW(
root, subkey.c_str(), 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, nullptr);
if (status != ERROR_SUCCESS) {
return false;
}
const DWORD byte_size = static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t));
const LSTATUS set_status = RegSetValueExW(
key,
value_name,
0,
REG_SZ,
reinterpret_cast<const BYTE*>(value.c_str()),
byte_size);
RegCloseKey(key);
return set_status == ERROR_SUCCESS;
}
std::wstring ReadStringValue(HKEY root, const wchar_t* subkey, const wchar_t* value_name) {
HKEY key = nullptr;
if (RegOpenKeyExW(root, subkey, 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) {
return {};
}
DWORD type = 0;
DWORD byte_size = 0;
if (RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &byte_size) != ERROR_SUCCESS ||
type != REG_SZ || byte_size == 0) {
RegCloseKey(key);
return {};
}
std::wstring value(byte_size / sizeof(wchar_t), L'\0');
const LSTATUS status = RegQueryValueExW(
key, value_name, nullptr, nullptr, reinterpret_cast<BYTE*>(value.data()), &byte_size);
RegCloseKey(key);
if (status != ERROR_SUCCESS) {
return {};
}
while (!value.empty() && value.back() == L'\0') {
value.pop_back();
}
return value;
}
bool RegisterDefaultBrowserCapabilities() {
const std::wstring exe_path = CurrentExecutablePath();
if (exe_path.empty()) {
return false;
}
const std::wstring exe_name = std::filesystem::path(exe_path).filename().wstring();
const std::wstring command = Quote(exe_path) + L" --url=" + Quote(L"%1");
bool ok = true;
ok &= SetStringValue(HKEY_CURRENT_USER, kRegisteredApplicationsKey, kAppName,
std::wstring(kClientRootKey) + L"\\Capabilities");
ok &= SetStringValue(HKEY_CURRENT_USER, kClientRootKey, nullptr, kAppName);
ok &= SetStringValue(HKEY_CURRENT_USER, std::wstring(kClientRootKey) + L"\\DefaultIcon",
nullptr, exe_path + L",0");
ok &= SetStringValue(HKEY_CURRENT_USER, std::wstring(kClientRootKey) + L"\\shell\\open\\command",
nullptr, Quote(exe_path));
ok &= SetStringValue(HKEY_CURRENT_USER, std::wstring(kClientRootKey) + L"\\Capabilities",
L"ApplicationName", kAppName);
ok &= SetStringValue(HKEY_CURRENT_USER, std::wstring(kClientRootKey) + L"\\Capabilities",
L"ApplicationDescription", L"Nebula Browser");
for (std::wstring_view extension : kWebFileExtensions) {
const std::wstring extension_name(extension);
ok &= SetStringValue(HKEY_CURRENT_USER, kFileAssociationsKey, extension_name.c_str(),
kClientKeyName);
}
ok &= SetStringValue(HKEY_CURRENT_USER, kUrlAssociationsKey, L"http", kClientKeyName);
ok &= SetStringValue(HKEY_CURRENT_USER, kUrlAssociationsKey, L"https", kClientKeyName);
ok &= SetStringValue(HKEY_CURRENT_USER, L"Software\\Classes\\NebulaBrowser", nullptr,
L"Nebula Browser HTML Document");
ok &= SetStringValue(HKEY_CURRENT_USER, L"Software\\Classes\\NebulaBrowser", L"URL Protocol",
L"");
ok &= SetStringValue(HKEY_CURRENT_USER, L"Software\\Classes\\NebulaBrowser\\DefaultIcon",
nullptr, exe_path + L",0");
ok &= SetStringValue(HKEY_CURRENT_USER, L"Software\\Classes\\NebulaBrowser\\shell\\open\\command",
nullptr, command);
const std::wstring application_key = std::wstring(L"Software\\Classes\\Applications\\") + exe_name;
ok &= SetStringValue(HKEY_CURRENT_USER, application_key,
L"ApplicationName", kAppName);
ok &= SetStringValue(HKEY_CURRENT_USER,
application_key + L"\\shell\\open\\command",
nullptr, command);
return ok;
}
void OpenDefaultAppsSettings() {
ShellExecuteW(nullptr, L"open", L"ms-settings:defaultapps", nullptr, nullptr, SW_SHOWNORMAL);
}
} // namespace
bool IsDefaultBrowser() {
return ReadStringValue(HKEY_CURRENT_USER, kHttpUserChoiceKey, L"ProgId") == kClientKeyName &&
ReadStringValue(HKEY_CURRENT_USER, kHttpsUserChoiceKey, L"ProgId") == kClientKeyName;
}
bool EnsureDefaultBrowserRegistration() {
return RegisterDefaultBrowserCapabilities();
}
bool RequestDefaultBrowser() {
const bool registered = RegisterDefaultBrowserCapabilities();
OpenDefaultAppsSettings();
return registered;
}
} // namespace nebula::platform
+47
View File
@@ -5,6 +5,7 @@
#include <windowsx.h> #include <windowsx.h>
#include <algorithm> #include <algorithm>
#include <string_view>
namespace nebula::window { namespace nebula::window {
namespace { namespace {
@@ -13,6 +14,7 @@ constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
constexpr wchar_t kWindowTitle[] = L"Nebula Browser"; constexpr wchar_t kWindowTitle[] = L"Nebula Browser";
constexpr wchar_t kChildFrameHitTestOldProcProp[] = L"NebulaChildFrameHitTestOldProc"; constexpr wchar_t kChildFrameHitTestOldProcProp[] = L"NebulaChildFrameHitTestOldProc";
constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent"; constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent";
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
constexpr int kTitleRowHeightDip = 42; constexpr int kTitleRowHeightDip = 42;
constexpr int kWindowControlWidthDip = 46; constexpr int kWindowControlWidthDip = 46;
constexpr int kWindowControlCount = 3; constexpr int kWindowControlCount = 3;
@@ -122,6 +124,37 @@ RECT ToNativeRect(const platform::Rect& rect) {
}; };
} }
std::string WideToUtf8(std::wstring_view value) {
if (value.empty()) {
return {};
}
const int size = WideCharToMultiByte(
CP_UTF8,
0,
value.data(),
static_cast<int>(value.size()),
nullptr,
0,
nullptr,
nullptr);
if (size <= 0) {
return {};
}
std::string result(size, '\0');
WideCharToMultiByte(
CP_UTF8,
0,
value.data(),
static_cast<int>(value.size()),
result.data(),
size,
nullptr,
nullptr);
return result;
}
} // namespace } // namespace
struct nebula::window::NebulaWindowImpl { struct nebula::window::NebulaWindowImpl {
@@ -354,6 +387,20 @@ LRESULT nebula::window::NebulaWindowImpl::WndProc(UINT message, WPARAM wparam, L
} }
break; break;
case WM_COPYDATA: {
const auto* copy_data = reinterpret_cast<const COPYDATASTRUCT*>(lparam);
if (copy_data && copy_data->dwData == kOpenTargetCopyDataId &&
copy_data->lpData && copy_data->cbData >= sizeof(wchar_t)) {
const auto* text = static_cast<const wchar_t*>(copy_data->lpData);
const size_t char_count = (copy_data->cbData / sizeof(wchar_t)) - 1;
if (delegate) {
delegate->OnExternalOpenRequested(WideToUtf8(std::wstring_view(text, char_count)));
}
return TRUE;
}
break;
}
case WM_DESTROY: case WM_DESTROY:
hwnd = nullptr; hwnd = nullptr;
return 0; return 0;
+52 -2
View File
@@ -2,6 +2,8 @@
#include <windows.h> #include <windows.h>
#include <string>
#include "include/cef_command_line.h" #include "include/cef_command_line.h"
#include "ui/paths.h" #include "ui/paths.h"
@@ -9,6 +11,8 @@ namespace nebula::platform {
namespace { namespace {
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance"; constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
class ScopedHandle { class ScopedHandle {
public: public:
@@ -28,15 +32,61 @@ private:
HANDLE handle_ = nullptr; HANDLE handle_ = nullptr;
}; };
std::wstring Utf8ToWide(const std::string& value) {
if (value.empty()) {
return {};
}
const int size = MultiByteToWideChar(
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
if (size <= 0) {
return {};
}
std::wstring result(size, L'\0');
MultiByteToWideChar(
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), size);
return result;
}
void ForwardLaunchTargetToExistingWindow(const std::string& launch_target) {
if (launch_target.empty()) {
return;
}
HWND existing_window = FindWindowW(kWindowClassName, nullptr);
if (!existing_window) {
return;
}
const std::wstring target = Utf8ToWide(launch_target);
if (target.empty()) {
return;
}
COPYDATASTRUCT data = {};
data.dwData = kOpenTargetCopyDataId;
data.cbData = static_cast<DWORD>((target.size() + 1) * sizeof(wchar_t));
data.lpData = const_cast<wchar_t*>(target.c_str());
SendMessageW(existing_window, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&data));
ShowWindow(existing_window, SW_SHOWNORMAL);
SetForegroundWindow(existing_window);
}
} // namespace } // namespace
void PrepareApp() { void PrepareApp() {
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
} }
bool TryAcquireSingleInstance() { bool TryAcquireSingleInstance(const std::string& launch_target) {
static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName)); static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName));
return !(mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS); const bool already_running = mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS;
if (already_running) {
ForwardLaunchTargetToExistingWindow(launch_target);
}
return !already_running;
} }
CefMainArgs MakeMainArgs(const AppStartup& startup) { CefMainArgs MakeMainArgs(const AppStartup& startup) {
+1
View File
@@ -17,6 +17,7 @@ public:
virtual void OnWindowCreated() = 0; virtual void OnWindowCreated() = 0;
virtual void OnWindowResized(const BrowserLayout& layout) = 0; virtual void OnWindowResized(const BrowserLayout& layout) = 0;
virtual void OnWindowCloseRequested() = 0; virtual void OnWindowCloseRequested() = 0;
virtual void OnExternalOpenRequested(const std::string& target) = 0;
}; };
class NebulaWindow { class NebulaWindow {
+105
View File
@@ -12,6 +12,109 @@ const HOME_SEARCH_Y_KEY = 'nebula-home-search-y'; // number (vh)
const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; // number (vh) const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; // number (vh)
const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; // 'br'|'bl'|'tr'|'tl' const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; // 'br'|'bl'|'tr'|'tl'
const DISPLAY_SCALE_KEY = 'nebula-display-scale'; // number (50-300) const DISPLAY_SCALE_KEY = 'nebula-display-scale'; // number (50-300)
const defaultBrowserRequests = new Map();
let defaultBrowserRequestId = 0;
function hasNebulaNativeBridge() {
return !!(window.nebulaNative && typeof window.nebulaNative.postMessage === 'function');
}
window.addEventListener('nebula-default-browser-result', (event) => {
const detail = event.detail || {};
const pending = defaultBrowserRequests.get(detail.requestId);
if (!pending) return;
defaultBrowserRequests.delete(detail.requestId);
pending.resolve(detail);
});
function sendDefaultBrowserRequest(command) {
return new Promise((resolve, reject) => {
if (!hasNebulaNativeBridge()) {
reject(new Error('Native browser integration is unavailable.'));
return;
}
const requestId = `settings-default-browser-${Date.now()}-${++defaultBrowserRequestId}`;
const timeout = setTimeout(() => {
defaultBrowserRequests.delete(requestId);
reject(new Error('Timed out waiting for default browser status.'));
}, 10000);
defaultBrowserRequests.set(requestId, {
resolve: (value) => {
clearTimeout(timeout);
resolve(value);
}
});
try {
window.nebulaNative.postMessage(command, requestId);
} catch (error) {
defaultBrowserRequests.delete(requestId);
clearTimeout(timeout);
reject(error);
}
});
}
async function refreshDefaultBrowserStatus() {
const btn = document.getElementById('set-default-browser-btn');
const status = document.getElementById('default-browser-status');
if (!btn || !status) return;
if (!hasNebulaNativeBridge()) {
btn.disabled = true;
status.textContent = 'Default browser setup is only available in the native app.';
return;
}
try {
const result = await sendDefaultBrowserRequest('check-default-browser');
btn.disabled = !!result.isDefault;
btn.textContent = result.isDefault ? 'Already Default' : 'Make Default Browser';
status.textContent = result.isDefault
? 'Nebula is your default browser.'
: 'Nebula is not your default browser.';
} catch (error) {
console.error('Default browser status error:', error);
status.textContent = 'Unable to check default browser status.';
}
}
function attachDefaultBrowserHandler() {
const btn = document.getElementById('set-default-browser-btn');
const status = document.getElementById('default-browser-status');
if (!btn || !status) return;
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.textContent = 'Opening Settings...';
status.textContent = 'Opening Windows default apps settings...';
try {
const result = await sendDefaultBrowserRequest('set-default-browser');
if (result.isDefault) {
btn.textContent = 'Already Default';
status.textContent = 'Nebula is your default browser.';
return;
}
btn.disabled = false;
btn.textContent = 'Check Again';
status.textContent = result.success
? 'Choose Nebula in Windows default apps settings, then check again.'
: (result.error || 'Unable to open default browser settings.');
} catch (error) {
console.error('Default browser setup error:', error);
btn.disabled = false;
btn.textContent = 'Try Again';
status.textContent = 'Unable to open default browser settings.';
}
});
refreshDefaultBrowserStatus();
}
function showStatus(message) { function showStatus(message) {
if (statusText && statusDiv) { if (statusText && statusDiv) {
@@ -74,6 +177,8 @@ function attachClearHandler(btn) {
// Try attaching immediately, and again on DOMContentLoaded // Try attaching immediately, and again on DOMContentLoaded
attachClearHandler(clearBtn); attachClearHandler(clearBtn);
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
attachDefaultBrowserHandler();
if (!clearBtn) { if (!clearBtn) {
clearBtn = document.getElementById('clear-data-btn'); clearBtn = document.getElementById('clear-data-btn');
attachClearHandler(clearBtn); attachClearHandler(clearBtn);
+76 -2
View File
@@ -16,6 +16,78 @@ function hasNebulaNativeBridge() {
return !!(window.nebulaNative && typeof window.nebulaNative.postMessage === 'function'); return !!(window.nebulaNative && typeof window.nebulaNative.postMessage === 'function');
} }
const defaultBrowserRequests = new Map();
let defaultBrowserRequestId = 0;
window.addEventListener('nebula-default-browser-result', (event) => {
const detail = event.detail || {};
const pending = defaultBrowserRequests.get(detail.requestId);
if (!pending) return;
defaultBrowserRequests.delete(detail.requestId);
pending.resolve(detail);
});
function sendDefaultBrowserRequest(command) {
return new Promise((resolve, reject) => {
if (!hasNebulaNativeBridge()) {
reject(new Error('Native browser integration is unavailable.'));
return;
}
const requestId = `default-browser-${Date.now()}-${++defaultBrowserRequestId}`;
const timeout = setTimeout(() => {
defaultBrowserRequests.delete(requestId);
reject(new Error('Timed out waiting for default browser status.'));
}, 10000);
defaultBrowserRequests.set(requestId, {
resolve: (value) => {
clearTimeout(timeout);
resolve(value);
},
reject: (error) => {
clearTimeout(timeout);
reject(error);
}
});
try {
window.nebulaNative.postMessage(command, requestId);
} catch (error) {
defaultBrowserRequests.delete(requestId);
clearTimeout(timeout);
reject(error);
}
});
}
function createNebulaNativeApi() {
return {
async getAllThemes() {
return { default: getPresetThemes() };
},
async isDefaultBrowser() {
const result = await sendDefaultBrowserRequest('check-default-browser');
return !!result.isDefault;
},
async setAsDefaultBrowser() {
return sendDefaultBrowserRequest('set-default-browser');
},
async applyTheme(themeId) {
const theme = getThemeById(themeId);
if (theme) {
localStorage.setItem('currentTheme', JSON.stringify(normalizeTheme(theme)));
}
localStorage.setItem('activeThemeName', themeId);
},
async completeFirstRun(data) {
localStorage.setItem('nebula-first-run-complete', JSON.stringify(data));
window.nebulaNative.postMessage('complete-first-run', JSON.stringify(data));
}
};
}
function getPresetThemes() { function getPresetThemes() {
if (typeof BrowserCustomizer === 'function') { if (typeof BrowserCustomizer === 'function') {
const customizer = new BrowserCustomizer({ skipInit: true }); const customizer = new BrowserCustomizer({ skipInit: true });
@@ -61,7 +133,7 @@ function normalizeTheme(theme) {
}; };
} }
const nativeApi = window.api || { const fallbackApi = {
async getAllThemes() { async getAllThemes() {
return { default: getPresetThemes() }; return { default: getPresetThemes() };
}, },
@@ -86,6 +158,8 @@ const nativeApi = window.api || {
} }
}; };
const nativeApi = window.api || (hasNebulaNativeBridge() ? createNebulaNativeApi() : fallbackApi);
// Initialize setup when DOM is ready // Initialize setup when DOM is ready
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
console.log('[Setup] Initializing first-time setup...'); console.log('[Setup] Initializing first-time setup...');
@@ -402,7 +476,7 @@ async function setDefaultBrowser() {
const result = await nativeApi.setAsDefaultBrowser(); const result = await nativeApi.setAsDefaultBrowser();
if (result.success) { if (result.success) {
const isDefault = await window.api.isDefaultBrowser(); const isDefault = !!result.isDefault || await nativeApi.isDefaultBrowser();
if (isDefault) { if (isDefault) {
setupState.defaultBrowserSet = true; setupState.defaultBrowserSet = true;
+9
View File
@@ -45,6 +45,15 @@
</div> </div>
</div> </div>
<div class="setting-group">
<h3>Default Browser</h3>
<p class="note">Use Nebula Browser for web links opened from other apps.</p>
<div class="setting-row">
<button id="set-default-browser-btn" class="primary-btn">Make Default Browser</button>
<span id="default-browser-status" class="note">Checking status...</span>
</div>
</div>
<div class="setting-group"> <div class="setting-group">
<h3>Weather Display</h3> <h3>Weather Display</h3>
<p class="note">Choose how temperature is displayed on the Home page weather card.</p> <p class="note">Choose how temperature is displayed on the Home page weather card.</p>