Support running Big Picture as separate process

Enable Big Picture mode to run concurrently with Desktop mode by:

- Implementing cross-platform process launching (LaunchSiblingExecutable) for Windows, macOS, and Linux
- Adding app profile system to isolate CEF user data between modes
- Making window class/title and mutex names profile-aware
- Changing mode switching to launch sibling executable instead of in-process switching
- Updating settings.js to use native bridge for Big Picture launch
- Adding build.bat helper for Windows builds
This commit is contained in:
Andrew Zambazos
2026-07-12 19:36:51 +12:00
parent b7596674ab
commit 76b942542a
15 changed files with 328 additions and 15 deletions
+3
View File
@@ -56,6 +56,7 @@ if(OS_WINDOWS)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
src/platform/win/default_browser_win.cpp src/platform/win/default_browser_win.cpp
src/platform/win/paths_win.cpp src/platform/win/paths_win.cpp
src/platform/win/process_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
src/platform/win/nebula_window_win.cpp src/platform/win/nebula_window_win.cpp
@@ -64,6 +65,7 @@ elseif(OS_MACOSX)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
src/platform/mac/default_browser_mac.mm src/platform/mac/default_browser_mac.mm
src/platform/mac/paths_mac.cpp src/platform/mac/paths_mac.cpp
src/platform/mac/process_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
src/platform/mac/nebula_window_mac.mm src/platform/mac/nebula_window_mac.mm
@@ -79,6 +81,7 @@ elseif(OS_LINUX)
set(NEBULA_PLATFORM_SOURCES set(NEBULA_PLATFORM_SOURCES
src/platform/linux/default_browser_linux.cpp src/platform/linux/default_browser_linux.cpp
src/platform/linux/paths_linux.cpp src/platform/linux/paths_linux.cpp
src/platform/linux/process_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
src/platform/linux/nebula_window_linux.cpp src/platform/linux/nebula_window_linux.cpp
+53
View File
@@ -0,0 +1,53 @@
@echo off
setlocal
rem Build Nebula Browser (Windows).
rem Usage:
rem build.bat - configure + Release build
rem build.bat Debug - configure + Debug build
rem build.bat Release - configure + Release build
rem build.bat configure - configure only
rem build.bat clean - remove the build folder
cd /d "%~dp0"
set "CONFIG=Release"
if /I "%~1"=="Debug" set "CONFIG=Debug"
if /I "%~1"=="Release" set "CONFIG=Release"
if /I "%~1"=="clean" (
echo Removing build\ ...
if exist build rmdir /s /q build
echo Done.
exit /b 0
)
if not exist "thirdparty\cef\cmake\FindCEF.cmake" (
echo ERROR: CEF not found at thirdparty\cef\
echo Download a Windows 64-bit CEF standard binary distribution and extract it
echo so that thirdparty\cef\cmake\FindCEF.cmake exists.
exit /b 1
)
echo Configuring CMake...
cmake -B build
if errorlevel 1 (
echo Configure failed.
exit /b 1
)
if /I "%~1"=="configure" (
echo Configure complete.
exit /b 0
)
echo Building %CONFIG%...
cmake --build build --config %CONFIG%
if errorlevel 1 (
echo Build failed.
exit /b 1
)
echo.
echo Build succeeded: build\%CONFIG%\NebulaBrowser.exe
exit /b 0
+29 -1
View File
@@ -17,6 +17,7 @@
#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 "platform/default_browser.h"
#include "platform/process.h"
#include "ui/paths.h" #include "ui/paths.h"
namespace nebula::app { namespace nebula::app {
@@ -243,6 +244,13 @@ void NebulaController::OnWindowCloseRequested() {
return; return;
} }
if (launch_options_.mode == AppMode::BigPicture && !closing_) {
if (!SwitchToSiblingMode(AppMode::Desktop)) {
BeginShutdown();
}
return;
}
BeginShutdown(); BeginShutdown();
} }
@@ -420,7 +428,8 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
CloseMenuPopup(); CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetSettingsUrl()); tabs_.LoadURL(nebula::ui::GetSettingsUrl());
} else if (command == "big-picture") { } else if (command == "big-picture") {
EnterBigPictureMode(); CloseMenuPopup();
SwitchToSiblingMode(AppMode::BigPicture);
} else if (command == "gpu-diagnostics") { } else if (command == "gpu-diagnostics") {
CloseMenuPopup(); CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl()); tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
@@ -488,7 +497,9 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
BeginShutdown(); BeginShutdown();
} else if (command == "exit-bigpicture" && window_) { } else if (command == "exit-bigpicture" && window_) {
if (launch_options_.mode == AppMode::BigPicture) { if (launch_options_.mode == AppMode::BigPicture) {
if (!SwitchToSiblingMode(AppMode::Desktop)) {
BeginShutdown(); BeginShutdown();
}
return; return;
} }
ExitBigPictureMode(); ExitBigPictureMode();
@@ -728,6 +739,23 @@ void NebulaController::ExitBigPictureMode() {
ResizeBrowsers(); ResizeBrowsers();
} }
bool NebulaController::SwitchToSiblingMode(AppMode mode) {
#if defined(_WIN32)
const char* executable =
mode == AppMode::BigPicture ? "NebulaBigPicture.exe" : "NebulaBrowser.exe";
#else
const char* executable =
mode == AppMode::BigPicture ? "NebulaBigPicture" : "NebulaBrowser";
#endif
if (!nebula::platform::LaunchSiblingExecutable(executable)) {
return false;
}
BeginShutdown();
return true;
}
nebula::window::BrowserLayout NebulaController::CurrentBrowserLayout() const { nebula::window::BrowserLayout NebulaController::CurrentBrowserLayout() const {
if (!window_) { if (!window_) {
return {}; return {};
+1
View File
@@ -53,6 +53,7 @@ private:
void CreateContentBrowser(); void CreateContentBrowser();
void EnterBigPictureMode(); void EnterBigPictureMode();
void ExitBigPictureMode(); void ExitBigPictureMode();
bool SwitchToSiblingMode(AppMode mode);
nebula::window::BrowserLayout CurrentBrowserLayout() const; nebula::window::BrowserLayout CurrentBrowserLayout() const;
void ToggleMenuPopup(); void ToggleMenuPopup();
void CloseMenuPopup(); void CloseMenuPopup();
+4
View File
@@ -122,6 +122,10 @@ int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options
return subprocess_exit_code; return subprocess_exit_code;
} }
// Isolate Big Picture from the desktop browser so both can run together.
nebula::ui::SetAppProfile(
options.mode == AppMode::BigPicture ? "bigpicture" : "desktop");
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine(); CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
nebula::platform::InitCommandLine(command_line, startup); nebula::platform::InitCommandLine(command_line, startup);
std::string initial_url = GetLaunchTarget(command_line); std::string initial_url = GetLaunchTarget(command_line);
+1
View File
@@ -111,6 +111,7 @@ 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 == "big-picture" ||
command == "check-default-browser" || command == "check-default-browser" ||
command == "set-default-browser" || command == "set-default-browser" ||
command == "clear-site-history" || command == "clear-site-history" ||
+37
View File
@@ -0,0 +1,37 @@
#include "platform/process.h"
#include "platform/paths_platform.h"
#include <sys/types.h>
#include <unistd.h>
#include <cstdlib>
#include <filesystem>
#include <system_error>
namespace nebula::platform {
bool LaunchSiblingExecutable(const std::string& executable_name) {
const auto directory = ExecutableDirectory();
if (directory.empty() || executable_name.empty()) {
return false;
}
const std::filesystem::path path = directory / executable_name;
std::error_code error;
if (!std::filesystem::exists(path, error)) {
return false;
}
const pid_t pid = fork();
if (pid < 0) {
return false;
}
if (pid == 0) {
execl(path.c_str(), path.c_str(), static_cast<char*>(nullptr));
_exit(127);
}
return true;
}
} // namespace nebula::platform
+57
View File
@@ -0,0 +1,57 @@
#include "platform/process.h"
#include "platform/paths_platform.h"
#include <sys/types.h>
#include <unistd.h>
#include <cstdlib>
#include <filesystem>
#include <system_error>
namespace nebula::platform {
namespace {
std::filesystem::path ResolveSiblingPath(const std::string& executable_name) {
const auto directory = ExecutableDirectory();
if (directory.empty()) {
return {};
}
const std::filesystem::path same_dir = directory / executable_name;
std::error_code error;
if (std::filesystem::exists(same_dir, error)) {
return same_dir;
}
// NebulaBrowser.app/Contents/MacOS -> sibling NebulaBigPicture.app/Contents/MacOS
const std::filesystem::path sibling_app =
directory / ".." / ".." / ".." / (executable_name + ".app") / "Contents" / "MacOS" /
executable_name;
if (std::filesystem::exists(sibling_app, error)) {
return sibling_app.lexically_normal();
}
return {};
}
} // namespace
bool LaunchSiblingExecutable(const std::string& executable_name) {
const std::filesystem::path path = ResolveSiblingPath(executable_name);
if (path.empty()) {
return false;
}
const pid_t pid = fork();
if (pid < 0) {
return false;
}
if (pid == 0) {
execl(path.c_str(), path.c_str(), static_cast<char*>(nullptr));
_exit(127);
}
return true;
}
} // namespace nebula::platform
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <string>
namespace nebula::platform {
// Launches an executable that lives next to the current process.
bool LaunchSiblingExecutable(const std::string& executable_name);
} // namespace nebula::platform
+17 -6
View File
@@ -7,11 +7,22 @@
#include <algorithm> #include <algorithm>
#include <string_view> #include <string_view>
#include "ui/paths.h"
namespace nebula::window { namespace nebula::window {
namespace { namespace {
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow"; const wchar_t* WindowClassName() {
constexpr wchar_t kWindowTitle[] = L"Nebula Browser"; return nebula::ui::GetAppProfile() == "bigpicture"
? L"NebulaBigPictureWindow"
: L"NebulaBrowserWindow";
}
const wchar_t* WindowTitle() {
return nebula::ui::GetAppProfile() == "bigpicture"
? L"Nebula Big Picture"
: 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 ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
@@ -415,7 +426,7 @@ void nebula::window::NebulaWindowImpl::RegisterClass(HINSTANCE instance_handle)
window_class.lpfnWndProc = StaticWndProc; window_class.lpfnWndProc = StaticWndProc;
window_class.hInstance = instance_handle; window_class.hInstance = instance_handle;
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName; window_class.lpszClassName = WindowClassName();
RegisterClassExW(&window_class); RegisterClassExW(&window_class);
} }
@@ -522,8 +533,8 @@ bool NebulaWindow::Create(const platform::AppStartup& startup) {
impl_->hwnd = CreateWindowExW( impl_->hwnd = CreateWindowExW(
0, 0,
kWindowClassName, WindowClassName(),
kWindowTitle, WindowTitle(),
WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN, WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN,
x, x,
y, y,
@@ -654,7 +665,7 @@ void NebulaWindow::SetTitle(const std::string& title) {
return; return;
} }
const std::wstring wide = title.empty() ? kWindowTitle : Utf8ToWide(title); const std::wstring wide = title.empty() ? WindowTitle() : Utf8ToWide(title);
SetWindowTextW(impl_->hwnd, wide.c_str()); SetWindowTextW(impl_->hwnd, wide.c_str());
} }
+79
View File
@@ -0,0 +1,79 @@
#include "platform/process.h"
#include "platform/paths_platform.h"
#include <windows.h>
#include <filesystem>
#include <string>
#include <system_error>
namespace nebula::platform {
namespace {
std::wstring Utf8ToWide(const std::string& utf8) {
if (utf8.empty()) {
return {};
}
const int size = MultiByteToWideChar(
CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
if (size <= 0) {
return {};
}
std::wstring result(static_cast<size_t>(size), L'\0');
MultiByteToWideChar(
CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), result.data(), size);
return result;
}
} // namespace
bool LaunchSiblingExecutable(const std::string& executable_name) {
const auto directory = ExecutableDirectory();
if (directory.empty() || executable_name.empty()) {
return false;
}
const std::wstring exe_name = Utf8ToWide(executable_name);
if (exe_name.empty()) {
return false;
}
const std::filesystem::path path = directory / exe_name;
std::error_code error;
if (!std::filesystem::exists(path, error)) {
return false;
}
const std::wstring path_wide = path.wstring();
const std::wstring dir_wide = directory.wstring();
STARTUPINFOW startup_info = {};
startup_info.cb = sizeof(startup_info);
PROCESS_INFORMATION process_info = {};
// CreateProcess requires a writable command line buffer.
std::wstring command_line = L"\"" + path_wide + L"\"";
const BOOL created = CreateProcessW(
path_wide.c_str(),
command_line.data(),
nullptr,
nullptr,
FALSE,
0,
nullptr,
dir_wide.c_str(),
&startup_info,
&process_info);
if (!created) {
return false;
}
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
return true;
}
} // namespace nebula::platform
+14 -4
View File
@@ -10,10 +10,20 @@
namespace nebula::platform { namespace nebula::platform {
namespace { namespace {
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL; constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
const wchar_t* MainInstanceMutexName() {
return nebula::ui::GetAppProfile() == "bigpicture"
? L"Local\\NebulaBigPictureMainInstance"
: L"Local\\NebulaBrowserMainInstance";
}
const wchar_t* WindowClassName() {
return nebula::ui::GetAppProfile() == "bigpicture"
? L"NebulaBigPictureWindow"
: L"NebulaBrowserWindow";
}
class ScopedHandle { class ScopedHandle {
public: public:
explicit ScopedHandle(HANDLE handle) : handle_(handle) {} explicit ScopedHandle(HANDLE handle) : handle_(handle) {}
@@ -54,7 +64,7 @@ void ForwardLaunchTargetToExistingWindow(const std::string& launch_target) {
return; return;
} }
HWND existing_window = FindWindowW(kWindowClassName, nullptr); HWND existing_window = FindWindowW(WindowClassName(), nullptr);
if (!existing_window) { if (!existing_window) {
return; return;
} }
@@ -81,7 +91,7 @@ void PrepareApp() {
} }
bool TryAcquireSingleInstance(const std::string& launch_target) { bool TryAcquireSingleInstance(const std::string& launch_target) {
static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName)); static ScopedHandle mutex(CreateMutexW(nullptr, TRUE, MainInstanceMutexName()));
const bool already_running = mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS; const bool already_running = mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS;
if (already_running) { if (already_running) {
ForwardLaunchTargetToExistingWindow(launch_target); ForwardLaunchTargetToExistingWindow(launch_target);
+13
View File
@@ -109,8 +109,18 @@ const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
return nullptr; return nullptr;
} }
std::string g_app_profile;
} // namespace } // namespace
void SetAppProfile(std::string_view profile) {
g_app_profile = std::string(profile);
}
const std::string& GetAppProfile() {
return g_app_profile;
}
std::filesystem::path GetExecutableDirectory() { std::filesystem::path GetExecutableDirectory() {
return nebula::platform::ExecutableDirectory(); return nebula::platform::ExecutableDirectory();
} }
@@ -125,6 +135,9 @@ std::filesystem::path GetUserDataDirectory() {
} }
std::filesystem::path user_data = root / "Nebula" / "User Data"; std::filesystem::path user_data = root / "Nebula" / "User Data";
if (!g_app_profile.empty() && g_app_profile != "desktop") {
user_data = root / "Nebula" / ("User Data " + g_app_profile);
}
std::error_code ec; std::error_code ec;
std::filesystem::create_directories(user_data, ec); std::filesystem::create_directories(user_data, ec);
return user_data; return user_data;
+6
View File
@@ -2,9 +2,15 @@
#include <filesystem> #include <filesystem>
#include <string> #include <string>
#include <string_view>
namespace nebula::ui { namespace nebula::ui {
// Process-wide profile used to isolate desktop vs Big Picture CEF data.
// Call before GetUserDataDirectory() / CEF initialize. Empty/"desktop" = default.
void SetAppProfile(std::string_view profile);
const std::string& GetAppProfile();
std::filesystem::path GetExecutableDirectory(); std::filesystem::path GetExecutableDirectory();
std::filesystem::path GetUserDataDirectory(); std::filesystem::path GetUserDataDirectory();
std::filesystem::path GetCacheDirectory(); std::filesystem::path GetCacheDirectory();
+3 -3
View File
@@ -382,11 +382,11 @@ window.addEventListener('DOMContentLoaded', () => {
} }
if (bigPictureBtn) { if (bigPictureBtn) {
bigPictureBtn.addEventListener('click', async () => { bigPictureBtn.addEventListener('click', () => {
try { try {
if (window.bigPictureAPI && typeof window.bigPictureAPI.launch === 'function') { if (hasNebulaNativeBridge()) {
showStatus('Launching Big Picture Mode...'); showStatus('Launching Big Picture Mode...');
await window.bigPictureAPI.launch(); window.nebulaNative.postMessage('big-picture');
} else { } else {
showStatus('Big Picture Mode not available'); showStatus('Big Picture Mode not available');
} }