diff --git a/CMakeLists.txt b/CMakeLists.txt index 015c103..e899011 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ if(OS_WINDOWS) set(NEBULA_PLATFORM_SOURCES src/platform/win/default_browser_win.cpp src/platform/win/paths_win.cpp + src/platform/win/process_win.cpp src/platform/win/startup_win.cpp src/platform/win/browser_host_win.cpp src/platform/win/nebula_window_win.cpp @@ -64,6 +65,7 @@ elseif(OS_MACOSX) set(NEBULA_PLATFORM_SOURCES src/platform/mac/default_browser_mac.mm src/platform/mac/paths_mac.cpp + src/platform/mac/process_mac.cpp src/platform/mac/startup_mac.mm src/platform/mac/browser_host_mac.mm src/platform/mac/nebula_window_mac.mm @@ -79,6 +81,7 @@ elseif(OS_LINUX) set(NEBULA_PLATFORM_SOURCES src/platform/linux/default_browser_linux.cpp src/platform/linux/paths_linux.cpp + src/platform/linux/process_linux.cpp src/platform/linux/startup_linux.cpp src/platform/linux/browser_host_linux.cpp src/platform/linux/nebula_window_linux.cpp diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..4f6addc --- /dev/null +++ b/build.bat @@ -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 diff --git a/src/app/nebula_controller.cpp b/src/app/nebula_controller.cpp index 2733785..3f434d8 100644 --- a/src/app/nebula_controller.cpp +++ b/src/app/nebula_controller.cpp @@ -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 {}; diff --git a/src/app/nebula_controller.h b/src/app/nebula_controller.h index e902a79..114f78d 100644 --- a/src/app/nebula_controller.h +++ b/src/app/nebula_controller.h @@ -53,6 +53,7 @@ private: void CreateContentBrowser(); void EnterBigPictureMode(); void ExitBigPictureMode(); + bool SwitchToSiblingMode(AppMode mode); nebula::window::BrowserLayout CurrentBrowserLayout() const; void ToggleMenuPopup(); void CloseMenuPopup(); diff --git a/src/app/run.cpp b/src/app/run.cpp index 6b24a86..ba9813e 100644 --- a/src/app/run.cpp +++ b/src/app/run.cpp @@ -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 command_line = CefCommandLine::CreateCommandLine(); nebula::platform::InitCommandLine(command_line, startup); std::string initial_url = GetLaunchTarget(command_line); diff --git a/src/cef/browser_client.cpp b/src/cef/browser_client.cpp index 0494ed6..399a82a 100644 --- a/src/cef/browser_client.cpp +++ b/src/cef/browser_client.cpp @@ -111,6 +111,7 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr 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" || diff --git a/src/platform/linux/process_linux.cpp b/src/platform/linux/process_linux.cpp new file mode 100644 index 0000000..15172b2 --- /dev/null +++ b/src/platform/linux/process_linux.cpp @@ -0,0 +1,37 @@ +#include "platform/process.h" + +#include "platform/paths_platform.h" + +#include +#include + +#include +#include +#include + +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(nullptr)); + _exit(127); + } + return true; +} + +} // namespace nebula::platform diff --git a/src/platform/mac/process_mac.cpp b/src/platform/mac/process_mac.cpp new file mode 100644 index 0000000..a444c29 --- /dev/null +++ b/src/platform/mac/process_mac.cpp @@ -0,0 +1,57 @@ +#include "platform/process.h" + +#include "platform/paths_platform.h" + +#include +#include + +#include +#include +#include + +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(nullptr)); + _exit(127); + } + return true; +} + +} // namespace nebula::platform diff --git a/src/platform/process.h b/src/platform/process.h new file mode 100644 index 0000000..9a09407 --- /dev/null +++ b/src/platform/process.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace nebula::platform { + +// Launches an executable that lives next to the current process. +bool LaunchSiblingExecutable(const std::string& executable_name); + +} // namespace nebula::platform diff --git a/src/platform/win/nebula_window_win.cpp b/src/platform/win/nebula_window_win.cpp index c9c89c2..dbe4d09 100644 --- a/src/platform/win/nebula_window_win.cpp +++ b/src/platform/win/nebula_window_win.cpp @@ -7,11 +7,22 @@ #include #include +#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()); } diff --git a/src/platform/win/process_win.cpp b/src/platform/win/process_win.cpp new file mode 100644 index 0000000..ff4d796 --- /dev/null +++ b/src/platform/win/process_win.cpp @@ -0,0 +1,79 @@ +#include "platform/process.h" + +#include "platform/paths_platform.h" + +#include + +#include +#include +#include + +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(utf8.size()), nullptr, 0); + if (size <= 0) { + return {}; + } + + std::wstring result(static_cast(size), L'\0'); + MultiByteToWideChar( + CP_UTF8, 0, utf8.data(), static_cast(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 diff --git a/src/platform/win/startup_win.cpp b/src/platform/win/startup_win.cpp index d99422d..f57e23f 100644 --- a/src/platform/win/startup_win.cpp +++ b/src/platform/win/startup_win.cpp @@ -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); diff --git a/src/ui/paths.cpp b/src/ui/paths.cpp index ae4f3d6..5ff6a24 100644 --- a/src/ui/paths.cpp +++ b/src/ui/paths.cpp @@ -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; diff --git a/src/ui/paths.h b/src/ui/paths.h index 371cafb..cb79a51 100644 --- a/src/ui/paths.h +++ b/src/ui/paths.h @@ -2,9 +2,15 @@ #include #include +#include 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(); diff --git a/ui/js/settings.js b/ui/js/settings.js index afb7084..f577c2b 100644 --- a/ui/js/settings.js +++ b/ui/js/settings.js @@ -382,11 +382,11 @@ window.addEventListener('DOMContentLoaded', () => { } if (bigPictureBtn) { - bigPictureBtn.addEventListener('click', async () => { + bigPictureBtn.addEventListener('click', () => { try { - if (window.bigPictureAPI && typeof window.bigPictureAPI.launch === 'function') { + if (hasNebulaNativeBridge()) { showStatus('Launching Big Picture Mode...'); - await window.bigPictureAPI.launch(); + window.nebulaNative.postMessage('big-picture'); } else { showStatus('Big Picture Mode not available'); }