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
+30 -2
View File
@@ -17,6 +17,7 @@
#include "include/wrapper/cef_helpers.h"
#include "platform/browser_host.h"
#include "platform/default_browser.h"
#include "platform/process.h"
#include "ui/paths.h"
namespace nebula::app {
@@ -243,6 +244,13 @@ void NebulaController::OnWindowCloseRequested() {
return;
}
if (launch_options_.mode == AppMode::BigPicture && !closing_) {
if (!SwitchToSiblingMode(AppMode::Desktop)) {
BeginShutdown();
}
return;
}
BeginShutdown();
}
@@ -420,7 +428,8 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
} else if (command == "big-picture") {
EnterBigPictureMode();
CloseMenuPopup();
SwitchToSiblingMode(AppMode::BigPicture);
} else if (command == "gpu-diagnostics") {
CloseMenuPopup();
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
@@ -488,7 +497,9 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
BeginShutdown();
} else if (command == "exit-bigpicture" && window_) {
if (launch_options_.mode == AppMode::BigPicture) {
BeginShutdown();
if (!SwitchToSiblingMode(AppMode::Desktop)) {
BeginShutdown();
}
return;
}
ExitBigPictureMode();
@@ -728,6 +739,23 @@ void NebulaController::ExitBigPictureMode() {
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 {
if (!window_) {
return {};
+1
View File
@@ -53,6 +53,7 @@ private:
void CreateContentBrowser();
void EnterBigPictureMode();
void ExitBigPictureMode();
bool SwitchToSiblingMode(AppMode mode);
nebula::window::BrowserLayout CurrentBrowserLayout() const;
void ToggleMenuPopup();
void CloseMenuPopup();
+4
View File
@@ -122,6 +122,10 @@ int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options
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();
nebula::platform::InitCommandLine(command_line, startup);
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 =
IsSettingsFrame(frame) && (command == "navigate" ||
command == "new-tab" ||
command == "big-picture" ||
command == "check-default-browser" ||
command == "set-default-browser" ||
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 <string_view>
#include "ui/paths.h"
namespace nebula::window {
namespace {
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
constexpr wchar_t kWindowTitle[] = L"Nebula Browser";
const wchar_t* WindowClassName() {
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 kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent";
constexpr ULONG_PTR kOpenTargetCopyDataId = 0x4E42554CUL;
@@ -415,7 +426,7 @@ void nebula::window::NebulaWindowImpl::RegisterClass(HINSTANCE instance_handle)
window_class.lpfnWndProc = StaticWndProc;
window_class.hInstance = instance_handle;
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.lpszClassName = WindowClassName();
RegisterClassExW(&window_class);
}
@@ -522,8 +533,8 @@ bool NebulaWindow::Create(const platform::AppStartup& startup) {
impl_->hwnd = CreateWindowExW(
0,
kWindowClassName,
kWindowTitle,
WindowClassName(),
WindowTitle(),
WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN,
x,
y,
@@ -654,7 +665,7 @@ void NebulaWindow::SetTitle(const std::string& title) {
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());
}
+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 {
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
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 {
public:
explicit ScopedHandle(HANDLE handle) : handle_(handle) {}
@@ -54,7 +64,7 @@ void ForwardLaunchTargetToExistingWindow(const std::string& launch_target) {
return;
}
HWND existing_window = FindWindowW(kWindowClassName, nullptr);
HWND existing_window = FindWindowW(WindowClassName(), nullptr);
if (!existing_window) {
return;
}
@@ -81,7 +91,7 @@ void PrepareApp() {
}
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;
if (already_running) {
ForwardLaunchTargetToExistingWindow(launch_target);
+13
View File
@@ -109,8 +109,18 @@ const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
return nullptr;
}
std::string g_app_profile;
} // 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() {
return nebula::platform::ExecutableDirectory();
}
@@ -125,6 +135,9 @@ std::filesystem::path GetUserDataDirectory() {
}
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::filesystem::create_directories(user_data, ec);
return user_data;
+6
View File
@@ -2,9 +2,15 @@
#include <filesystem>
#include <string>
#include <string_view>
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 GetUserDataDirectory();
std::filesystem::path GetCacheDirectory();