31 Commits

Author SHA1 Message Date
andrew b7596674ab Add host platform detection and macOS styles
Expose host platform to the UI and apply platform-specific styling. Backend: add CurrentPlatformName() and include "platform" in the JSON sent from NebulaController so the frontend can know the host OS. Frontend: detectHostPlatform() initializes state.platform, applyPlatform() sets a platform-* body class, and applyState() applies it. CSS: add .platform-macos rules to adjust title padding and hide window controls on macOS. Also fix FilePathToUrl to avoid producing an extra slash when the encoded path already starts with '/'.
2026-05-22 10:55:16 +12:00
Andrew Zambazos ce92b3841f 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.
2026-05-20 21:05:59 +12:00
Andrew Zambazos 659d1530b0 Extract BeginShutdown and unify shutdown flow
Introduce BeginShutdown() (declared in header) and move window-close shutdown logic into it. OnWindowCloseRequested and chrome close/exit handlers now call BeginShutdown; shutdown now collects closing_tab_browsers_ plus any active tab browsers into a single list before closing them. MaybeFinishShutdown was updated to consider closing_tab_browsers_ so shutdown won't finish prematurely. This centralizes shutdown behavior and ensures all relevant browser instances are closed cleanly.
2026-05-20 20:19:38 +12:00
Andrew Zambazos 302753cd3d Add first-run setup and theme synchronization
Introduce first-run setup flow and live chrome theme syncing.

- Add first_run_state.cpp/.h to read/write a first_run_state.json under user data and decide whether to show the setup UI.
- Wire first-run logic into NebulaController: track first_run_setup_active_, create initial setup tab, defer/bring up chrome browser accordingly, and add CompleteFirstRunSetup() to persist state and finish setup.
- Add SendThemeToChromeSurfaces() and handle "theme-update" and "complete-first-run" chrome commands; restrict setup completion to setup frame.
- Expose GetFirstRunStatePath() and GetSetupUrl() in UI path helpers and include the state file in the build list (CMakeLists.txt).
- Update chrome UI: new CSS variables and styles for tabs/url-bar; chrome.js can apply themes (applyTheme), persist/load theme, and listen for storage updates to apply theme changes live.
- Update customization.js, settings.js, and setup.js to normalize/persist themes, send theme updates to the native host (or fallback), and communicate completion via the native bridge when available; include customization.js in setup.html.

These changes allow the app to run an interactive first-run setup and keep the separate chrome UI in sync with user-selected themes.
2026-05-20 20:14:43 +12:00
Andrew Zambazos bbba5b2927 Replace UNREFERENCED_PARAMETER with NEBULA_UNUSED
Introduce NEBULA_UNUSED macro in platform types and replace usages of UNREFERENCED_PARAMETER across multiple source files to standardize unused-parameter handling. Update various platform stubs (Linux window/host), CEF handlers, and Windows startup code to use NEBULA_UNUSED. Also adjust NebulaController::OnWindowCloseRequested to stop force-destroying the top-level window and rely on MaybeFinishShutdown once browsers report OnBeforeClose.
2026-05-19 15:08:39 +12:00
andrew 29908646ea Add macOS Cocoa port and CEF helper support
Introduce a macOS Cocoa-based UI and CEF helper subprocess support. CMake: enable OBJCXX on Apple, treat mac sources as .mm, set -fobjc-arc, link Cocoa frameworks, and generate helper app targets using a mac Info.plist template; keep libcef logical target off macOS. Implementation: add Objective-C++ implementations for browser_host and nebula_window, convert startup to ObjC++ (prepare NSApplication), add process_helper_mac (CEF helper entry) and load CEF library from main/main_bigpicture on mac. Tooling/docs: add .clangd fallback flags, compile_commands symlink helper, update cross-platform docs for mac status. Misc: add menu-popup UI page, define UNREFERENCED_PARAMETER in platform/types.h, small code cleanups (use (void)layout, include platform/types.h) and update .gitignore.
2026-05-19 12:57:26 +12:00
andrew 8cf9b50690 Add MPL-2.0 & asset licenses; expand README
Add a full Mozilla Public License v2.0 (LICENSE) and a new ASSETS-LICENSE.md that clarifies Nebula branding, artwork, and other non-code assets are not covered by MPL and are All Rights Reserved unless stated otherwise. Update README.md with a detailed project overview: CEF migration, project status, goals, distribution direction, planned features, architecture notes, build instructions, and explicit licensing guidance separating source code (MPL-2.0) from branding assets and documentation.
2026-05-19 10:25:37 +12:00
Andrew Zambazos d6f15c5dce Add Big Picture mode and multi-target build
Introduce a Big Picture mode and support building two app targets. CMakeLists was refactored to add a helper (add_nebula_app_target) and now registers NebulaBrowser and NebulaBigPicture executables, moving UI/assets post-build copying into the helper. A new app/main_bigpicture.cpp entry was added and RunNebula now accepts LaunchOptions (AppMode) with a new LaunchOptions struct. NebulaController was extended heavily to manage a BigPicture browser role: creation, enter/exit mode, layout logic, cursor injection/removal, remote input handlers (mouse move/click/wheel/text), and state syncing. Cef/browser_client updated for the new BigPicture role and message filtering. Platform API gained MoveCursorToBrowserPoint with platform stubs/Win implementation. Big-picture UI files (CSS/JS/HTML) were also updated to support the new mode.
2026-05-18 22:07:41 +12:00
Andrew Zambazos b4d93f24cd Force close browsers and handle bigpicture exit
Call CloseBrowser(true) for chrome, popup and tab browsers to force shutdown. Immediately destroy the top-level window and call MaybeFinishShutdown() to avoid hangs when WM_CLOSE is not resent by Alloy child-window paths. Route "close" and new "exit-bigpicture" chrome commands through OnWindowCloseRequested so they trigger the same shutdown flow. Add IsBigPictureFrame and permit the "exit-bigpicture" process message from bigpicture internal frames.
2026-05-18 18:35:18 +12:00
Andrew Zambazos c514e4faec Menu popup: visibility, zoom sync, and tab fixes
Track menu popup visibility and propagate zoom level to the popup. Add menu_popup_visible_ flag, SendMenuPopupZoom(), and call it when creating/showing the popup and when adjusting zoom. Make CreateNewTab accept an optional URL and route popup/new-tab flows to it. Prevent per-tab child closes from triggering app shutdown by tracking closing_tab_browsers_ and adding ForgetClosingTabBrowser(). Treat MenuPopup role specially when enabling frame hit-testing.

Platform: remove usage of ApplyRoundedBrowserRegion and switch menu popup sizing to use resized client_size helpers (consolidate calculations across Win/Mac/Linux). UI: update menu-popup CSS/JS (new styling, font, zoom formatting API via NebulaMenuPopup.setZoomLevel), wire settings to use nebulaNative.postMessage for new-tab, and remove the static menu-popup.html. Misc: small chrome CSS/JS tweaks and ensure chrome state includes zoomLevel.
2026-05-18 18:28:20 +12:00
Andrew Zambazos e51594a010 Add platform abstraction & cross-platform ports
Introduce a cross-platform platform layer and port scaffolding for macOS and Linux. CMakeLists.txt refactored to select platform sources, set executable type per OS, and use CEF helper macros for runtime deployment. Add platform/types.h, startup/paths/browser_host APIs and implementations for Windows, macOS, and Linux (many are stubs for mac/linux). Refactor app entry and lifetime to use nebula::platform::AppStartup (app/main, run.{h,cpp}), move window/browser host logic into platform/browser_host.*, and update NebulaController to use platform APIs (native handles, sizing, visibility, cache-busting token, etc.). Add README and detailed docs/cross-platform.md describing build layout and porting status.
2026-05-18 17:25:04 +12:00
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
Andrew Zambazos dd6b3fa70d Menu popup, icons, remove nebot 2026-05-14 10:19:11 +12:00
Andrew Zambazos a8786b4c1c Add NebulaController, tab manager, and CEF clients
Introduce core application structure and browser management: add NebulaController and run entry (src/app/*) to centralize window, tab and CEF lifecycle logic; implement TabManager and NebulaTab (src/browser/*) for tab creation, navigation and state tracking; add URL utilities (NormalizeNavigationInput, JsonEscape) and CEF browser client glue (src/cef/browser_client.cpp/.h) to forward chrome commands and content events. Update app/main.cpp to delegate startup to nebula::app::RunNebula. Add UI assets (chrome.html, chrome.css, chrome.js, lucide, menu-popup updates) and remove obsolete nebot.html. Update CMakeLists to include new sources, add ${CMAKE_SOURCE_DIR}/src to includes and link dwmapi on Windows. Overall this refactors startup and splits responsibilities for cleaner tab and browser lifecycle handling.
2026-05-14 10:18:51 +12:00
Andrew Zambazos 207a849f06 Add Nebula Browser app, UI and assets
Add initial Nebula Browser project skeleton: CMakeLists to configure and link CEF (including post-build steps to copy runtime and UI files), a Windows CEF-based entry (app/main.cpp) that initializes CEF and loads the bundled UI, and a full ui/ and assets/ tree (HTML, CSS, JS, fonts, icons, and branding images). Update .gitignore to ignore build/out, thirdparty/cef, IDE and common OS artifacts.
2026-05-13 22:17:58 +12:00
Andrew Zambazos 79565f2ef3 Removed Electron Project 2026-05-13 17:18:10 +12:00
andrew adefa1706e Added OS context menu for linux 2026-02-25 11:57:31 +13:00
andrew 8b87a07d1b Merge remote-tracking branch 'origin/main' 2026-02-25 11:21:04 +13:00
andrew a101899d9c Add RGB CSS variables for theme colors
Introduce --primary-rgb, --accent-rgb, --success-rgb and --warning-rgb and replace hardcoded rgba(...) usages in CSS with rgba(var(--*-rgb), alpha) to allow dynamic alpha blending from theme colors. Add a hexToRgb helper in setup.js and populate these RGB variables in applyThemeToSetupPage (handles 3- and 6-digit hex and validates input), so runtime theme changes can drive translucent shadows, backgrounds and highlights.
2026-02-24 21:00:38 +13:00
andrew 0b51d133a4 Archive Steam docs; add itch.io upload guide
Move legacy Steam-related docs into documentation/archived and add a new UPLOAD-ITCH.md describing how to publish builds to itch.io with Butler. Update top-level README to state official releases will be on itch.io. Remove Steam-specific UI/features: drop steamCloudOptIn from first-run preferences, remove the Steam Cloud teaser and summary from the setup flow, and adjust settings/setup copy to reference handheld devices and non‑Steam distribution. Also make a small wording tweak in the plugins doc about rendererPreload.
2026-02-24 20:50:00 +13:00
andrew 6b2a7c8404 Hide default linux task buttons
The electron app now uses the built in menu bar on linux
2026-02-24 15:19:41 +13:00
andrew 618ea7d12d Prefer usr/user-data for portable data
Rename and consolidate portable user-data to usr/user-data and update tooling and runtime to match. Updated appdir-example/run-nebula.sh to point at usr/user-data; make-appdir.sh and update-appdir.sh now patch/copy the launcher, create the usr/user-data directory and set secure permissions (mkdir -p, chmod 700), and remove sed backups. portable-data.js now defaults to app-local user-data and prefers <AppDir>/usr/user-data on Linux AppDir builds (with safe fs checks). Also minor UI change in renderer/setup.css to make the footer background transparent and disable the backdrop blur.
2026-02-19 17:33:26 +13:00
andrew 0137df60dd Updated create and update appdir files to include Nebula-Desktop and Nebula-Controller files 2026-02-01 21:29:13 +13:00
Andrew Zambazos b725d5a672 Revise project description and status in README
Updated project status and focus, changing from Steam-centric to Linux-first. Clarified maintenance mode and community engagement.
2026-02-01 21:12:20 +13:00
Andrew Zambazos 86f3b10e80 Update README to reflect dormant project status
Updated project status and description in README.md to reflect that development is paused and the project is in a dormant state. Added a section clarifying the implications of this status and maintained the licensing information.
2026-01-31 19:28:27 +13:00
170 changed files with 10728 additions and 17602 deletions
+17
View File
@@ -0,0 +1,17 @@
# clangd configuration for Nebula Browser
#
# Full diagnostics require:
# 1. CEF binary distribution unpacked to thirdparty/cef/
# 2. Running: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# 3. Symlink or copy: ln -s build/compile_commands.json .
#
# Without compile_commands.json, clangd uses fallback flags below.
CompileFlags:
Add:
- -std=c++20
- -xobjective-c++
- -Isrc
- -Ithirdparty/cef
- -Ithirdparty/cef/include
- -Wno-c++17-extensions
+13 -108
View File
@@ -1,116 +1,21 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Build output
/build/
/out/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# CEF binaries
/thirdparty/cef/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Visual Studio
.vs/
*.vcxproj.user
# Coverage directory used by tools like istanbul
coverage
*.lcov
# CMake
CMakeCache.txt
CMakeFiles/
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-temporary-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Mac files
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Electron build output
/dist
/out
/release
/build
*.nupkg
*.AppImage
*.dmg
*.exe
*.pkg
# IDE config files
.vscode/
.idea/
site-history.json
bookmarks.json
bookmarks.backup.json
search-history.json
# Portable user data folder
user-data/
# AppImage / SteamOS
squashfs-root/
*.AppImage
# Electron build output
dist/
build/
out/
release/
# Native binaries
nebula
nebula.exe
# Node/Electron
node_modules/
# Logs
*.log
# Build artifacts
nebula-appdir/
*.asar
/.cache
+11
View File
@@ -0,0 +1,11 @@
# Nebula Browser Asset License
The source code of Nebula Browser is licensed under the Mozilla Public License 2.0, as described in the LICENSE file.
However, the Nebula Browser name, logo, icons, artwork, screenshots, visual identity, branding materials, and other non-code assets are not automatically covered by the MPL-2.0 license.
Unless otherwise stated, these assets are All Rights Reserved.
You may not use the Nebula Browser name, logo, or branding in a way that suggests endorsement, official status, or affiliation with the Nebula Project without permission.
Community forks are welcome, but they should use their own name and branding unless permission is granted.
+258
View File
@@ -0,0 +1,258 @@
cmake_minimum_required(VERSION 3.21)
# Enable OBJCXX early on macOS for Cocoa integration
if(APPLE)
project(NebulaBrowser LANGUAGES CXX OBJCXX)
else()
project(NebulaBrowser LANGUAGES CXX)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ------------------------------------------------------------
# CEF location
# ------------------------------------------------------------
set(CEF_ROOT "${CMAKE_SOURCE_DIR}/thirdparty/cef" CACHE PATH "Path to the CEF binary distribution")
if(NOT EXISTS "${CEF_ROOT}/cmake/FindCEF.cmake")
message(FATAL_ERROR
"CEF was not found.\n"
"Expected CEF here:\n"
" ${CEF_ROOT}\n\n"
"Unpack the CEF binary distribution for your OS into thirdparty/cef."
)
endif()
list(APPEND CMAKE_MODULE_PATH "${CEF_ROOT}/cmake")
find_package(CEF REQUIRED)
add_subdirectory(
"${CEF_LIBCEF_DLL_WRAPPER_PATH}"
"${CMAKE_BINARY_DIR}/libcef_dll_wrapper"
)
SET_CEF_TARGET_OUT_DIR()
# ------------------------------------------------------------
# Sources
# ------------------------------------------------------------
set(NEBULA_COMMON_SOURCES
src/app/first_run_state.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
src/cef/browser_client.cpp
src/cef/nebula_app.cpp
src/ui/paths.cpp
)
if(OS_WINDOWS)
set(NEBULA_PLATFORM_SOURCES
src/platform/win/default_browser_win.cpp
src/platform/win/paths_win.cpp
src/platform/win/startup_win.cpp
src/platform/win/browser_host_win.cpp
src/platform/win/nebula_window_win.cpp
)
elseif(OS_MACOSX)
set(NEBULA_PLATFORM_SOURCES
src/platform/mac/default_browser_mac.mm
src/platform/mac/paths_mac.cpp
src/platform/mac/startup_mac.mm
src/platform/mac/browser_host_mac.mm
src/platform/mac/nebula_window_mac.mm
)
set_source_files_properties(
src/platform/mac/startup_mac.mm
src/platform/mac/browser_host_mac.mm
src/platform/mac/nebula_window_mac.mm
PROPERTIES
COMPILE_FLAGS "-fobjc-arc"
)
elseif(OS_LINUX)
set(NEBULA_PLATFORM_SOURCES
src/platform/linux/default_browser_linux.cpp
src/platform/linux/paths_linux.cpp
src/platform/linux/startup_linux.cpp
src/platform/linux/browser_host_linux.cpp
src/platform/linux/nebula_window_linux.cpp
)
else()
message(FATAL_ERROR "Unsupported platform.")
endif()
# On macOS, CEF is a framework linked via CEF_STANDARD_LIBS.
# On Windows/Linux, we create a logical target for libcef.
if(NOT OS_MACOSX)
ADD_LOGICAL_TARGET("libcef_lib" "${CEF_LIB_RELEASE}" "${CEF_LIB_DEBUG}")
endif()
if(OS_LINUX)
FIND_LINUX_LIBRARIES("X11")
endif()
function(add_nebula_app_target nebula_target entry_source)
if(OS_WINDOWS)
add_executable(${nebula_target} WIN32
${entry_source}
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
elseif(OS_MACOSX)
add_executable(${nebula_target} MACOSX_BUNDLE
${entry_source}
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
elseif(OS_LINUX)
add_executable(${nebula_target}
${entry_source}
${NEBULA_COMMON_SOURCES}
${NEBULA_PLATFORM_SOURCES}
)
endif()
SET_EXECUTABLE_TARGET_PROPERTIES(${nebula_target})
add_dependencies(${nebula_target} libcef_dll_wrapper)
target_include_directories(${nebula_target} PRIVATE
"${CMAKE_SOURCE_DIR}/src"
"${CEF_ROOT}"
"${CEF_ROOT}/include"
)
if(OS_MACOSX)
# On macOS, CEF is a framework; don't link libcef_lib
target_link_libraries(${nebula_target} PRIVATE
libcef_dll_wrapper
${CEF_STANDARD_LIBS}
)
else()
target_link_libraries(${nebula_target} PRIVATE
libcef_lib
libcef_dll_wrapper
${CEF_STANDARD_LIBS}
)
endif()
if(MSVC)
set_property(TARGET ${nebula_target} PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
)
endif()
# ------------------------------------------------------------
# Platform-specific CEF runtime deployment
# ------------------------------------------------------------
if(OS_WINDOWS)
target_link_libraries(${nebula_target} PRIVATE dwmapi)
target_compile_definitions(${nebula_target} PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
)
COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
elseif(OS_LINUX)
COPY_FILES("${nebula_target}" "${CEF_BINARY_FILES}" "${CEF_BINARY_DIR_RELEASE}" "${CEF_TARGET_OUT_DIR}")
COPY_FILES("${nebula_target}" "${CEF_RESOURCE_FILES}" "${CEF_RESOURCE_DIR}" "${CEF_TARGET_OUT_DIR}")
elseif(OS_MACOSX)
target_link_libraries(${nebula_target} PRIVATE
"-framework Cocoa"
"-framework ApplicationServices"
)
set(NEBULA_APP "${CEF_TARGET_OUT_DIR}/${nebula_target}.app")
set(NEBULA_HELPER_TARGET "${nebula_target}_Helper")
set(NEBULA_HELPER_OUTPUT_NAME "${nebula_target} Helper")
string(TOLOWER "${nebula_target}" NEBULA_HELPER_BUNDLE_NAME)
COPY_MAC_FRAMEWORK(
"${nebula_target}"
"${CEF_BINARY_DIR_RELEASE}"
"${NEBULA_APP}"
)
COPY_FILES(
"${nebula_target}"
"${CEF_BINARY_FILES}"
"${CEF_BINARY_DIR_RELEASE}"
"${NEBULA_APP}/Contents/Frameworks"
)
COPY_FILES(
"${nebula_target}"
"${CEF_RESOURCE_FILES}"
"${CEF_RESOURCE_DIR}"
"${NEBULA_APP}/Contents/Resources"
)
foreach(_suffix_list ${CEF_HELPER_APP_SUFFIXES})
string(REPLACE ":" ";" _suffix_list ${_suffix_list})
list(GET _suffix_list 0 _name_suffix)
list(GET _suffix_list 1 _target_suffix)
list(GET _suffix_list 2 _plist_suffix)
set(_helper_target "${NEBULA_HELPER_TARGET}${_target_suffix}")
set(_helper_output_name "${NEBULA_HELPER_OUTPUT_NAME}${_name_suffix}")
set(_helper_info_plist "${CMAKE_CURRENT_BINARY_DIR}/${_helper_target}-Info.plist")
file(READ "${CMAKE_SOURCE_DIR}/cmake/mac-helper-Info.plist.in" _plist_contents)
string(REPLACE "\${EXECUTABLE_NAME}" "${_helper_output_name}" _plist_contents "${_plist_contents}")
string(REPLACE "\${PRODUCT_NAME}" "${_helper_output_name}" _plist_contents "${_plist_contents}")
string(REPLACE "\${HELPER_BUNDLE_NAME}" "${NEBULA_HELPER_BUNDLE_NAME}" _plist_contents "${_plist_contents}")
string(REPLACE "\${BUNDLE_ID_SUFFIX}" "${_plist_suffix}" _plist_contents "${_plist_contents}")
file(WRITE "${_helper_info_plist}" "${_plist_contents}")
add_executable(${_helper_target} MACOSX_BUNDLE
app/process_helper_mac.cc
src/cef/nebula_app.cpp
)
SET_EXECUTABLE_TARGET_PROPERTIES(${_helper_target})
add_dependencies(${_helper_target} libcef_dll_wrapper)
target_include_directories(${_helper_target} PRIVATE
"${CMAKE_SOURCE_DIR}/src"
"${CEF_ROOT}"
"${CEF_ROOT}/include"
)
target_link_libraries(${_helper_target} PRIVATE
libcef_dll_wrapper
${CEF_STANDARD_LIBS}
)
set_target_properties(${_helper_target} PROPERTIES
MACOSX_BUNDLE_INFO_PLIST "${_helper_info_plist}"
OUTPUT_NAME "${_helper_output_name}"
)
add_dependencies(${nebula_target} "${_helper_target}")
add_custom_command(TARGET ${nebula_target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CEF_TARGET_OUT_DIR}/${_helper_output_name}.app"
"${NEBULA_APP}/Contents/Frameworks/${_helper_output_name}.app"
VERBATIM
)
endforeach()
endif()
# ------------------------------------------------------------
# Copy Nebula UI files after build
# ------------------------------------------------------------
add_custom_command(TARGET ${nebula_target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/ui"
"$<TARGET_FILE_DIR:${nebula_target}>/ui"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/assets"
"$<TARGET_FILE_DIR:${nebula_target}>/ui/assets"
COMMENT "Copying Nebula UI files and assets for ${nebula_target}..."
)
endfunction()
add_nebula_app_target(NebulaBrowser app/main.cpp)
add_nebula_app_target(NebulaBigPicture app/main_bigpicture.cpp)
-94
View File
@@ -1,94 +0,0 @@
# Electron Upgrade Feature - Bug Fixes
## Problem Identified
The upgrade feature was downloading and installing new Electron versions successfully, but the app always showed the old version (1.0.0) after restart because:
1. **Version Source Issue**: The app was reading `app.getVersion()` which gets the version from `package.json` at startup time
2. **Package.json Not Re-read**: Even after npm installed a new Electron version, the app didn't re-read the updated `package.json`
3. **Runtime Display**: The About tab showed the bundled Electron version (37.x) which is baked into the binary at build time
## Solutions Implemented
### 1. **New Helper Function: `getInstalledElectronVersion()`**
- Reads `package.json` directly every time it's called (not cached)
- Extracts the actual installed Electron version from `devDependencies`
- Handles both stable (`electron`) and nightly (`electron-nightly`) packages
- Strips version specifiers (^, ~, etc.) to get the clean version number
- Falls back to `app.getVersion()` if reading fails
```javascript
function getInstalledElectronVersion() {
try {
const packageJsonPath = path.join(__dirname, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const electronDep = packageJson.devDependencies?.electron;
const electronNightlyDep = packageJson.devDependencies?.['electron-nightly'];
if (electronDep) {
return electronDep.replace(/^\D+/, '');
}
if (electronNightlyDep) {
return electronNightlyDep.replace(/^\D+/, '');
}
return app.getVersion();
} catch (err) {
return app.getVersion();
}
}
```
### 2. **Updated `get-electron-versions` Handler**
- Now uses `getInstalledElectronVersion()` instead of `app.getVersion()`
- Returns the actual installed version that was modified by npm
- Performs fresh version checks each time (no caching)
### 3. **Improved `upgrade-electron` Handler**
- Increased `maxBuffer` to handle large npm output
- Added cleanup logic to remove the old Electron variant when switching types
- Removes `electron` when upgrading to `nightly`
- Removes `electron-nightly` when upgrading to `stable`
- Better error logging to debug npm failures
- Returns clearer messages about installation status
### 4. **Enhanced UI/UX in settings.js**
- Added more descriptive status text ("Downloading and installing..." instead of just "Upgrading...")
- Disables all controls during upgrade to prevent multiple clicks
- Reduced restart delay from 2000ms to 1500ms for faster feedback
- Better error handling with proper cleanup of disabled states
## How It Works Now
1. **User clicks "Check for Updates"**
- Queries npm registry for latest version
- Uses `getInstalledElectronVersion()` to read current version from `package.json`
- Compares versions and shows if update is available
2. **User clicks "Upgrade Electron"**
- Confirms action
- Runs `npm install --save-dev electron@latest` (or `electron-nightly@latest`)
- npm downloads and installs new version
- Handler removes the other Electron variant from `package.json` if needed
- Shows success message
3. **App Restarts**
- Uses `app.relaunch()` and `app.quit()`
- When app relaunches, it:
- Loads new Electron binary from `node_modules`
- Runs new Electron version
- Settings page shows correct new version on next check
## Testing Recommendations
1. Test upgrading from stable to nightly version
2. Test upgrading from nightly back to stable
3. Verify version display updates after restart
4. Check that old variant is removed from `package.json`
5. Verify app runs stably with new Electron version
## Notes for Future Development
- The About tab displays `process.versions.electron` which is the bundled Chromium version, not the Electron framework version
- The Electron version we display in the upgrade section comes from `package.json` which is the actual framework version
- When building with electron-builder, the bundled version becomes fixed until next rebuild
- For development/testing, the upgrade feature reads live from `package.json`
View File
+373
View File
@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
-98
View File
@@ -1,98 +0,0 @@
# Nebula Plugins (Early Preview)
This document explains how to build simple plugins for Nebula. The initial API is intentionally small and will grow with feedback.
## Overview
- Plugins live under either of these folders:
- App folder: `<app>/plugins/<plugin-id>/`
- User folder: `%APPDATA%/Nebula/plugins/<plugin-id>/` (Windows) preferred for user-installed plugins.
- Each plugin has a `plugin.json` manifest. Optional `main.js` runs in the main process. Optional `renderer-preload.js` runs in the renderer preload context and can expose safe APIs via `contextBridge`.
- Plugins are loaded on app start. Toggle a plugin by setting `"enabled": false` in its manifest.
## Manifest (plugin.json)
Example:
```json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"description": "What it does",
"main": "main.js",
"rendererPreload": "renderer-preload.js",
"categories": ["Search", "Productivity"],
"authors": ["Jane Doe", { "name": "Acme Labs", "email": "oss@acme.example" }],
"enabled": true
}
```
Fields:
- id: Unique id. Defaults to folder name if omitted.
- main: Optional entry for main process integration.
- rendererPreload: Optional file injected into the preload. Use it to expose limited APIs.
- categories: Optional string or array of strings used for organizing/filtering plugins in UI and APIs. Example: ["AI", "Utilities"].
- authors: Optional string or array of strings/objects describing authors. Objects support { name, email, url }. In APIs/UI, names are displayed.
- enabled: Defaults to true.
## Main process API (activate)
If `main` is present, export an `activate(ctx)` function. The `ctx` contains:
- Electron: `app`, `BrowserWindow`, `ipcMain`, `session`, `Menu`, `dialog`, `shell`
- paths: `{ appPath, userData, pluginDir }`
- log/warn/error: prefix logs with your plugin id
- on(event, cb): subscribe to lifecycle events (experimental)
- registerIPC(channel, handler): quickly expose an `ipcMain.handle`
- registerWebRequest(filter, listener): attach `session.webRequest.onBeforeRequest`
Example:
module.exports.activate = (ctx) => {
ctx.log('hello');
ctx.registerIPC('my-plugin:do', async (_evt, payload) => ({ ok: true }));
ctx.registerWebRequest({ urls: ['*://*/*'] }, (details) => ({ cancel: false }));
};
## Renderer preload API
If `rendererPreload` is present, it will be `require()`-d from the app preload. You can use `contextBridge` to expose a safe surface to the page:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('myPlugin', {
hello: () => ipcRenderer.invoke('my-plugin:do'),
});
Your exposed API will be available on `window.myPlugin` in `renderer/` code (e.g., `script.js`).
## Sample plugin
A working sample is included at `plugins/sample-hello/`:
- Adds menu item "Say Hello (Sample Plugin)" under Help.
- Exposes `window.sampleHello.ping()` and `window.sampleHello.onHello(cb)`.
Try it from the DevTools console:
await window.sampleHello.ping();
window.sampleHello.onHello((m) => console.log('got hello', m));
Click Help -> Say Hello (Sample Plugin) to see the message delivered to the page.
## Loading order and safety
- Plugins load after the app is ready. Renderer preloads run after Nebula's own preload has exposed its APIs.
- Context isolation stays enabled. Only data explicitly exposed via `contextBridge` is available to pages.
- Avoid long blocking work in plugin activation.
## Debugging
- See logs with a `[Plugin:<id>]` prefix in the app console.
- Temporarily disable a plugin by setting `enabled: false` in `plugin.json`.
## Roadmap
This is a first pass. Planned next:
- Enable plugin settings UI
- Hot reload/reload button
- More lifecycle hooks (tab events, context menu contributions)
- Theming hooks
-234
View File
@@ -1,234 +0,0 @@
Converting extracted AppImage (`squashfs-root`) into a distributable AppDir for Steam
If your environment lacks `rsync`, use `cp -a` to copy the extracted AppImage into a clean AppDir and prepare it for upload to Steam.
1) Copy the extracted AppImage to an AppDir folder
```bash
cp -a squashfs-root/ nebula-appdir
```
2) Unpack `app.asar` to edit or include app sources (optional; requires `npx asar`)
```bash
cd nebula-appdir/resources
npx asar extract app.asar app
# keep a backup if you want
mv app app.orig && rm app.asar
cd ../../
```
3) Add/verify launcher (we added `nebula-appdir/Nebula`):
```bash
chmod +x nebula-appdir/Nebula
```
Run locally:
```bash
cd nebula-appdir
./Nebula
```
4) Ensure binary & permissions are correct
```bash
chmod +x nebula-appdir/nebula
```
5) Package or upload to Steam
- Create a tarball to upload as game files, or upload the AppDir contents as the depot.
```bash
tar -czf nebula-appdir.tar.gz -C nebula-appdir .
```
- In Steamworks, set the launch command to `./Nebula` (or `./nebula`).
Notes
- `--no-sandbox` reduces Chromium sandboxing; prefer fixing `chrome-sandbox` and enabling sandboxing when possible.
- Using the AppDir avoids AppImage/FUSE dependency on target systems.
- Test on a clean SteamOS/Deck image before publishing.
Big Picture auto-start (SteamOS Gaming Mode)
- If Nebula is launched from SteamOS Gaming Mode, it will auto-start in Big Picture Mode.
- To force/disable via Steam Launch Options: `--big-picture` or `--no-big-picture`.
---
## Built-in Controller Support (Steam Deck / Game Mode)
Nebula has **native gamepad support** that signals to Steam that the application is consuming controller input. This prevents Steam from applying Desktop mouse/keyboard emulation when running in Game Mode.
### How It Works
Steam Deck only stops applying Desktop mouse emulation when:
1. The application actively reads controller/gamepad input, OR
2. Steam Input is enabled (which requires explicit configuration)
If an app does not read controller input at all, Steam assumes the user needs mouse emulation.
Nebula solves this by:
1. **Preload Gamepad Handler**: The preload script (`preload.js`) continuously polls `navigator.getGamepads()` from the moment any window loads. This signals to Steam that the app is consuming gamepad events and should not apply mouse emulation.
2. **Big Picture Mode**: Full controller-friendly UI with:
- D-pad / Left stick: Navigate menus
- A button: Select/activate
- B button: Back
- X button: Backspace (in keyboard)
- Y button: Space / Open search
- LB/RB: Navigate webview history
- Right stick: Virtual cursor (in browse mode)
- Triggers: Left/right click (in browse mode)
- Start: Toggle settings/sidebar
- Select: Toggle fullscreen webview
### Gamepad API (for Developers)
The gamepad handler exposes an API via `window.gamepadAPI`:
```javascript
// Check if gamepad handler is initialized
if (gamepadAPI.isAvailable()) {
console.log('Gamepad handler is running');
}
// Check if a gamepad is connected
if (gamepadAPI.isConnected()) {
console.log('Gamepad connected!');
}
// Get list of connected gamepads
const gamepads = gamepadAPI.getConnected();
// Returns: [{ id, index, mapping, buttons, axes }, ...]
console.log(gamepads);
// Get active gamepad's current state (buttons and axes)
const active = gamepadAPI.getActive();
if (active) {
console.log('Active gamepad:', active.id);
console.log('Buttons:', active.buttons);
console.log('Axes:', active.axes);
}
// Get handler state for debugging
const state = gamepadAPI.getState();
console.log('Handler state:', state);
// Returns: { initialized, connectedCount, activeGamepadIndex, isPolling }
// Listen for gamepad events (via CustomEvent on window)
window.addEventListener('nebula-gamepad-button', (e) => {
const { button, pressed, value } = e.detail;
console.log(`Button ${button}: ${pressed ? 'pressed' : 'released'}`);
});
window.addEventListener('nebula-gamepad-connect', (e) => {
console.log('Gamepad connected:', e.detail.id);
});
window.addEventListener('nebula-gamepad-disconnect', (e) => {
console.log('Gamepad disconnected:', e.detail.id);
});
window.addEventListener('nebula-gamepad-axis', (e) => {
const { axis, value } = e.detail;
console.log(`Axis ${axis}: ${value}`);
});
// Enable debug logging
gamepadAPI.setDebug(true);
```
### Troubleshooting
If Steam is still applying mouse emulation:
1. **Configure Steam Input per-game** (most reliable fix):
- **Windows / Desktop Steam UI**:
- Library → right-click Nebula → Properties → **Controller**
- Set **"Override for Nebula"** to **"Disable Steam Input"**
- **Steam Deck / SteamOS Gaming Mode**:
- Open Nebula → press the Steam button → **Controller Settings** (or the controller icon)
- Set the layout to a **Gamepad** template (not “Keyboard/Mouse”), or disable Steam Input if the toggle is available
- This stops Steam from translating controller input into keyboard/mouse events (“Desktop Layout” behavior).
If you **dont see a Controller tab** (common when the Steam entry is treated as an “application/tool”):
- Use **Big Picture / Gaming Mode** and edit the **Controller Layout** for that specific entry.
- Or change Steams global Desktop Layout: Steam → Settings → Controller → **Desktop Layout** → pick a gamepad-focused template or remove mouse/keyboard bindings.
2. **Verify gamepad polling is active**: Open DevTools (F12) and run `gamepadAPI.getState()` - check that `isPolling` is `true`
3. **Check gamepad connection**: Run `gamepadAPI.getConnected()` to see detected gamepads
4. **Press a button first**: On Linux, the `gamepadconnected` event may not fire until the first button press
5. **Enable debug mode**: Run `gamepadAPI.setDebug(true)` to see detailed logs
6. **Restart the app**: Close Nebula completely and relaunch from Steam
### Steam Launch Options
#### Windows
The `VAR=value %command%` syntax does **not** work on Windows. Use the Steam UI instead:
1. **Library** → right-click Nebula → **Properties****Controller** → set to **"Disable Steam Input"**
2. If no Controller tab exists, open Steam in **Big Picture Mode** → Nebula → **Manage Game** (gear) → **Controller Options****Disable Steam Input**
If you must use launch options on Windows, use this wrapper syntax:
```bat
cmd /c "set SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 && %command%"
```
#### Linux / SteamOS / Steam Deck
Add these to your Steam launch options (Right-click game → Properties → Launch Options):
```bash
# Disable Steam Input completely (recommended for native controller support)
SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 %command%
# Force native gamepad without Steam's emulation layer
STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command%
# Combined - full native controller mode with Big Picture UI
SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command% --big-picture
# If you need to debug controller issues
SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 %command% --big-picture 2>&1 | tee ~/nebula-debug.log
```
### Steam Deck Recommended Setup
For the best experience on Steam Deck:
1. **Add Nebula as a Non-Steam Game** (if not using Steamworks version)
2. **Controller Settings**:
- Right-click Nebula → Properties → Controller
- Set to **"Disable Steam Input"**
3. **Launch Options**:
```
SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command% --big-picture
```
4. **Shortcuts** (optional):
- Configure gamepad shortcuts in Steam for Steam button actions (screenshots, etc.)
### Why This Is Needed
Steam Deck / SteamOS Game Mode applies "Desktop Configuration" mouse/keyboard emulation to apps that don't appear to handle controller input. Even though Nebula polls `navigator.getGamepads()` continuously, Steam's input layer initializes before the app can signal its intent.
The solution is two-fold:
1. **Environment variables** (`SDL_GAMECONTROLLER_*`) signal to Steam's SDL-based input layer early
2. **Steam Input settings** ("Disable Steam Input") bypasses the emulation entirely
### Shipping Defaults (Steamworks “Software/App” limitation)
If your Steamworks package is categorized as **Software/Application**, Steamworks may not expose per-title Steam Input configuration the way it does for games.
In that case:
- You generally **cannot force a global Steam Input toggle** for all users from Steamworks.
- The practical, shippable approach is to (a) **consume controller input natively** (Nebula does this via early Gamepad API polling) so Steam Deck/Game Mode backs off Desktop emulation, and (b) provide user-facing guidance for disabling Steam Input / choosing a Gamepad layout.
If you need Steam Input defaults controlled centrally, the usual path is to ask Valve Partner Support to enable the relevant Steam Input configuration for your App ID, or to re-categorize the title where appropriate.
### Force Big Picture Mode
```bash
# Via command line
./Nebula --big-picture
# Via environment
NEBULA_BIG_PICTURE=1 ./Nebula
# Disable Big Picture Mode
./Nebula --no-big-picture
NEBULA_NO_BIG_PICTURE=1 ./Nebula
```
-208
View File
@@ -1,208 +0,0 @@
# Linux / SteamOS Build Upload Guide (SteamCMD)
This guide explains how to upload the **Linux / SteamOS** build of Nebula Browser to Steam using **SteamCMD**. It is tailored to the current project layout on Steam Deck / Linux.
---
## App & Depot IDs
* **App ID:** 4290110
* **Windows Depot:** 4290111
* **Linux / SteamOS Depot:** 4290112
This guide covers **Linux only (4290112)**.
---
## Current Folder Layout
Home directory layout expected by this guide:
```
/home/deck/
├── steamcmd/
│ └── steamcmd.sh
└── steam_build/
├── Nebula_SteamOS/
│ ├── nebula-browser
│ └── ...other runtime files...
├── output/
└── app_4290110.vdf
```
---
## Step 1: Verify Executable Permissions
The main Linux binary **must** be executable.
```bash
cd ~/steam_build/Nebula_SteamOS
ls -la
```
Identify the main binary (for example `nebula-browser`) and run:
```bash
chmod +x nebula-browser
```
Optional quick test:
```bash
./nebula-browser
```
---
## Step 2: App Build VDF Configuration
Ensure `app_4290110.vdf` exists at:
```
/home/deck/steam_build/app_4290110.vdf
```
Its contents should be **exactly**:
```
"AppBuild"
{
"AppID" "4290110"
"Desc" "Nebula SteamOS build"
"BuildOutput" "output/"
"ContentRoot" "."
"Preview" "0"
"Depots"
{
"4290112"
{
"FileMapping"
{
"LocalPath" "Nebula_SteamOS/*"
"DepotPath" "."
"recursive" "1"
}
}
}
}
```
Notes:
* `ContentRoot "."` maps to `/home/deck/steam_build`
* `Nebula_SteamOS/*` uploads everything inside that folder
* Depot **4290112** is the Linux / SteamOS depot
---
## Step 3: Launch SteamCMD
```bash
cd ~/steamcmd
./steamcmd.sh
```
You should now see:
```
Steam>
```
---
## Step 4: Log In
Inside SteamCMD:
```
login YOUR_STEAM_USERNAME
```
If Steam Guard is enabled, enter the code when prompted.
---
## Step 5: Upload the Linux Build
Inside SteamCMD, run **with full path** (no `~`):
```
run_app_build /home/deck/steam_build/app_4290110.vdf
```
Expected behavior:
* Content scan starts
* Files upload to depot 4290112
* A **BuildID** is printed on success
---
## Step 6: Exit SteamCMD
```
quit
```
---
## Step 7: Assign Build to a Branch (Required)
After upload completes:
1. Open **Steamworks**
2. Go to **Nebula Browser → Builds**
3. Find the new Build ID
4. Assign it to a branch (`internal`, `beta`, or `default`)
If this step is skipped, Steam Deck installs **0 bytes** even though upload succeeded.
---
## Common Issues
### App build file does not exist
* SteamCMD does **not** expand `~`
* Always use `/home/deck/...` absolute paths
### 0 bytes uploaded
* `LocalPath` is wrong
* Build not assigned to a branch
* Executable missing or filtered out
### Build installs but does not launch
* Binary not executable
* Missing runtime libraries
* Incorrect launch configuration in Steamworks
---
## Logs
If something fails, check:
```bash
tail -n 200 /home/deck/.local/share/Steam/logs/stderr.txt
tail -n 200 /home/deck/.local/share/Steam/logs/bootstrap_log.txt
```
---
## Notes for Steam Deck
* Test in **Desktop Mode** first
* Then test in **Gaming Mode**
* Steam Input and sandboxing differ between modes
* Avoid absolute paths inside the app
---
## Status
This process uploads **Linux / SteamOS depot 4290112 only**.
Windows builds should be uploaded separately using depot **4290111**.
+293 -91
View File
@@ -1,149 +1,351 @@
# NEBULA BROWSER
# Nebula Browser
*A controller-first browser originally designed for SteamOS*
A Chromium Embedded Framework (CEF) browser with a custom HTML chrome UI.
---
**Nebula Browser** is a controller-first web browser designed for Steam Deck, SteamOS-style systems, handheld PCs, living room setups, and gamepad-driven desktop environments.
### ⚠️ Final Release • Project Archived ⚠️
Originally developed as an Electron-based browser for SteamOS users, Nebula Browser is now being rebuilt with **CEF / Chromium Embedded Framework** to provide deeper control over the browser experience, improved native integration, and a cleaner long-term foundation for custom UI, controller navigation, and platform-specific builds.
Nebula Browser has reached end of support and is no longer under active development.
This repository represents the final archived state of the project.
Development on Nebula Browser has somewhat resumed, with the project now moving toward a more native custom browser architecture.
---
Nebula Browser is intended to remain open source. The browser source code is licensed under MPL-2.0, while Nebula branding and visual assets are protected separately.
# Nebula
## Documentation
![Nebula Logo](assets/images/Logos/Nebula-Logo.svg)
Nebula is a customizable and privacy-focused web browser built with Electron. It was designed to be a lightweight, secure, and user-friendly browser with a strong emphasis on controller-first interaction, performance, and privacy.
- **[Cross-platform build & architecture](docs/cross-platform.md)** — how to use one repo for Windows, macOS, and Linux; CEF setup; source layout; porting status.
---
## Project Status
**Status:** Archived
**Maintenance:** Ended
**Future Updates:** None planned
Nebula Browser is currently in early active redevelopment.
Nebula is no longer actively maintained. The source code remains available for reference, learning, and archival purposes.
The original Electron version proved the core idea: a browser built around controller use, large-screen navigation, and a UI that does not feel like a standard desktop browser awkwardly placed on a handheld gaming device.
The new CEF version is the next major step.
Current focus areas include:
* Rebuilding the browser around CEF
* Creating a fully custom browser interface
* Moving away from default Chromium UI elements
* Reusing and adapting existing Nebula UI concepts where possible
* Supporting controller-first navigation
* Preparing for cross-platform builds across Windows, Linux, and macOS
* Creating a stronger foundation for future NebulaOS integration
---
## Why Nebula Was Archived
## Why CEF?
Nebula was created with a very specific goal in mind: to be a **controller-first browser that lived inside the Steam ecosystem**, particularly for Steam Deck and SteamOS users who wanted a web experience without relying on desktop mode, keyboards, or external workarounds.
Nebula Browser originally used Electron because it was fast to prototype, easy to package, and allowed the project to move quickly. However, Electron also comes with limits when building a browser that is meant to feel fully custom and deeply integrated with the operating system.
During the Steam review process, Valve determined that Nebula does not fit within Steams allowed categories for non-game software. As a result, the application was permanently retired from Steam and cannot be distributed on the platform.
The move to **CEF** allows Nebula Browser to become more than a web app wrapped in a browser shell.
While Nebula can function as a desktop browser, distributing it outside of Steam fundamentally changes the experience it was designed to provide. Requiring third-party installation methods or desktop mode defeats the core problem Nebula was built to solve.
CEF gives the project:
Rather than ship a compromised version that no longer aligns with its original purpose, the decision was made to formally conclude development and archive the project.
* More control over the native window and browser lifecycle
* Better separation between the browser engine and the custom UI
* Greater flexibility for building a non-Chrome-like interface
* A more suitable foundation for SteamOS, Linux, and future NebulaOS use
* The ability to build a browser that feels purpose-built rather than wrapped
This repository preserves Nebula in its final state as a complete exploration of controller-first browser design.
The goal is not to create another Chromium clone. The goal is to create a browser interface designed around how people actually use handhelds, controllers, TVs, and gaming-first systems.
---
## Features
## Distribution Direction
* **Privacy Control:** Easily clear your browsing data (history, cookies, cache, local storage, and more).
* **Tab Management:** Open new tabs, pop a tab out into a new window, and manage them efficiently.
* **Bookmarks:** Save your favorite sites with automatic backup on save.
* **History:** Keeps track of your browsing and search history with one-click clear.
* **Downloads Manager:** Track downloads, pause/resume/cancel, and open or reveal completed files.
* **Context Menu:** Native rightclick menu with Back/Forward/Reload, open/download links, image actions, and Inspect Element.
* **Auth Compatibility:** Improved OAuth/SSO & WebAuthn support (popup windows enabled where needed).
* **Performance Monitoring:** Built-in tools to monitor app performance and force GC when needed.
* **GPU Acceleration Control:** Diagnostics and safe fallbacks to troubleshoot rendering issues.
* **Themes & Customization:** Built-in themes and live editor to craft your own.
* **Plugins:** Extend Nebula with custom or community plugins via a simple plugin API.
* **Cross-Platform:** Runs on Windows, macOS, and Linux.
Nebula Browser was originally aimed at Steam. However, after exploring Steam as a release platform, it became clear that Steam may not be the best fit for this kind of application.
[**Learn more about Nebula's features.**](documentation/FEATURES.md)
The current distribution direction is:
## Getting Started
### Primary Target
### Prerequisites
* **Itch.io**
* [Node.js](https://nodejs.org/) installed.
Itch.io is currently the most practical and flexible platform for distributing Nebula Browser. It allows the project to be released as a utility-style app without needing to fit into a traditional game store category.
### Installation
### Potential Future Targets
1. Clone the repository:
```sh
git clone https://github.com/Bobbybear007/NebulaBrowser.git
```
2. Navigate to the project directory:
```sh
cd NebulaBrowser
```
3. Install dependencies:
```sh
npm install
* **Epic Games Store**
* **GOG**
* **Flatpak / Linux package distribution**
* **Direct downloads from the Nebula website**
Epic Games and GOG are being considered as possible future distribution platforms, especially if Nebula Browser grows into a more polished desktop or gaming utility. These are not confirmed release platforms yet, but they remain possible options.
---
## Open Source Direction
Nebula Browser is intended to remain open source because browsers are privacy-sensitive software. Users should be able to inspect how the browser works, verify that it is not doing anything suspicious, and contribute fixes or improvements when they find problems.
Open source is especially important for:
* User trust
* Privacy transparency
* Linux and Steam Deck community adoption
* Cross-platform testing
* Community contributions
* Long-term sustainability
The core Nebula Browser project should remain open source, including:
* Browser shell
* CEF integration code
* Custom browser UI
* Controller navigation systems
* Settings and theme systems
* Build scripts and documentation
Not every Nebula-related component has to be open source. Some future or external components may remain private or be licensed separately, such as:
* NebulaOS-specific services
* Account systems
* Cloud sync
* Hosted backend services
* Internal analytics or crash reporting infrastructure
* Private distribution tooling
* Experimental unreleased features
* Protected branding assets
Suggested openness model:
```text
Nebula Browser: open source
Nebula Core: open source or source-available
NebulaOS: potentially partially open source
Nebula online services/backend: private
Branding assets: protected
```
### Running the Application
---
To start the browser, run the following command:
## Core Vision
```sh
npm start
Nebula Browser is built around one simple idea:
> The web should be comfortable to use from a couch, handheld, controller, or gaming-focused operating system.
Most browsers are designed around keyboards, mice, and desktop workflows. Nebula Browser is designed around a different experience.
Nebula Browser aims to support:
* Controller-first navigation
* Large, readable interface elements
* Gamepad-friendly menus and shortcuts
* Steam Deck and handheld PC usability
* Custom home pages and launcher-style navigation
* Better integration with gaming-focused environments
* A UI that feels closer to a console app than a traditional browser
---
## Planned Features
The CEF rebuild is still early, but the intended feature set includes:
* Fully custom browser chrome
* Custom tab bar
* Custom address bar
* Controller-friendly navigation system
* On-screen keyboard support
* Bookmarks and quick access tiles
* Custom home page
* Desktop mode and controller mode
* Fullscreen and couch-friendly browsing
* Custom context menus
* Download management
* Settings system
* Theme support
* Steam Deck / handheld-focused layout options
* Optional integration with NebulaOS
Some features may be ported or adapted from the earlier Electron version where it makes sense.
---
## Nebula Core
Nebula Browser may also make use of **Nebula Core**, a shared package system for Nebula-related projects.
Nebula Core is intended to provide reusable systems such as:
* Controller input handling
* Navigation helpers
* UI components
* Glyphs and button prompts
* Theming utilities
* On-screen keyboard systems
* Shared Nebula project logic
Because the new browser is CEF-based rather than Electron-based, some Nebula Core modules may need to be adapted for native integration or used through the browser UI layer rather than directly inside the C++ application layer.
---
## Relationship to NebulaOS
Nebula Browser is part of the wider **Nebula Project** ecosystem.
NebulaOS is a controller-focused operating system / shell concept designed around gaming, handheld PCs, and living room devices. Nebula Browser is intended to become one of the key built-in applications for that ecosystem.
While Nebula Browser can exist as a standalone app, its long-term role is to provide a web browsing experience that feels native inside NebulaOS.
---
## Technology Stack
The current redevelopment direction uses:
* **C++** for the native browser shell
* **CEF / Chromium Embedded Framework** for web rendering
* **HTML, CSS, and JavaScript** for the custom browser UI
* **CMake** for project configuration
* Platform-specific CEF builds for Windows, Linux, and macOS
The previous version used Electron, but the current direction is focused on CEF.
---
## Repository Structure
The repository structure may change as the CEF rebuild develops, but the project is expected to follow a layout similar to:
```text
NebulaBrowser/
├── app/
│ ├── browser/
│ ├── ui/
│ └── platform/
├── cef/
│ └── README.md
├── resources/
│ ├── icons/
│ ├── themes/
│ └── pages/
├── scripts/
├── third_party/
├── CMakeLists.txt
└── README.md
```
## Building the Application
CEF itself should generally not be committed directly into the repository. Instead, platform-specific CEF builds should be downloaded separately and placed into the expected local folder during development.
To build the application for your platform, run:
---
```sh
npm run dist
## Development Notes
Nebula Browser is still experimental and in transition from the original Electron version to the new CEF version.
Expect changes in:
* Build setup
* File structure
* UI architecture
* Controller input systems
* Platform support
* Packaging and distribution
The project is not yet considered production-ready.
---
## Building
Build instructions will be expanded as the CEF version becomes more stable.
At a high level, development requires:
1. A supported C++ compiler
2. CMake
3. A downloaded CEF binary distribution for your operating system
4. The Nebula Browser source code
5. A local build folder
Example build flow:
```bash
cmake -S . -B build
cmake --build build
```
This will create a distributable file in the `dist` directory.
More detailed platform-specific instructions will be added later. For macOS/Linux prerequisites, directory structure, and current port status, see [docs/cross-platform.md](docs/cross-platform.md).
Tip (Windows): If you encounter GPU issues, try starting with `start-gpu-safe.bat` to launch in a safer rendering mode.
### Quick start (Windows)
## Project Structure
1. Download the [CEF standard binary distribution](https://cef-builds.spotifycdn.com/index.html) for Windows 64-bit.
2. Extract into `thirdparty/cef/` so `thirdparty/cef/cmake/FindCEF.cmake` exists.
3. Build:
An overview of the project's structure. For a more detailed explanation, please see the [Project Structure documentation](documentation/PROJECT_STRUCTURE.md).
```powershell
cmake -B build
cmake --build build --config Release
```
- `main.js`: The main entry point for the Electron application.
- `renderer/`: Contains all the front-end files.
- `preload.js`: Bridges the main and renderer processes.
- `performance-monitor.js`: Module for monitoring performance.
- `gpu-config.js` & `gpu-fallback.js`: Modules for managing GPU settings.
- `assets/`: Contains static assets.
- `documentation/`: Contains additional documentation.
- `plugins/`: Sample plugins and scaffolding for developing your own.
4. Run `build\Release\NebulaBrowser.exe`.
## Core Concepts
---
Nebula is built on several core concepts that are essential to understanding how it works. For a deeper dive, read the [Core Concepts documentation](documentation/CORE_CONCEPTS.md).
## Platform Goals
- **Main and Renderer Processes**
- **Inter-Process Communication (IPC)**
- **Performance and GPU Management**
Nebula Browser is being designed with cross-platform support in mind.
Target platforms include:
* Windows
* Linux
* SteamOS / Steam Deck
* macOS
Linux and SteamOS are especially important because Nebula Browser was originally created to solve a gap in the handheld and couch-browsing experience.
---
## Release Plans
Nebula Browser does not currently have a fixed release date.
The current release plan is:
1. Rebuild the core browser shell in CEF
2. Replace default Chromium UI with a custom Nebula interface
3. Restore core browser functionality
4. Add controller-first navigation
5. Prepare an early public build
6. Release through Itch.io first
7. Explore additional distribution options later
---
## Contributing
Contributions are welcome! Please read our [contributing guidelines](documentation/CONTRIBUTING.md) to get started.
Nebula Browser is currently a personal / experimental project and is still finding its new technical direction.
## Technologies Used
Contributions, ideas, testing, and feedback may become more open once the CEF rebuild has a more stable foundation.
* [Electron](https://www.electronjs.org/)
* HTML, CSS, JavaScript
---
## License
## Licensing
This project is licensed under the MIT License. [Read More](documentation/MIT.md)
Nebula Browser separates source code licensing from branding and visual assets.
The preferred licensing direction is:
## Documentation
* **Source code:** Mozilla Public License 2.0
* **Documentation:** CC BY 4.0, unless otherwise specified
* **Branding, logos, icons, names, and visual identity:** All Rights Reserved, unless explicitly stated otherwise
* [MIT License](documentation/MIT.md)
* [GPU Fix](documentation/GPU-FIX-README.md)
* [Features](documentation/FEATURES.md)
* [Customization](documentation/Customization.md)
* [Project Structure](documentation/PROJECT_STRUCTURE.md)
* [Core Concepts](documentation/CORE_CONCEPTS.md)
* [Contributing Guide](documentation/CONTRIBUTING.md)
* [OAuth Debug](documentation/oauth-debug.md)
* [Plugins Guide](README-PLUGINS.md)
MPL-2.0 is being used because it is a practical open-source license for a browser-style project. It allows people to use, modify, and contribute to the code while requiring changes to MPL-licensed files to remain open under the same license.
The Nebula name, logo, and brand identity are not automatically granted for reuse just because the code is open source. See [ASSETS-LICENSE.md](ASSETS-LICENSE.md) for the separate asset and branding terms.
See the CEF distribution and Chromium license terms for third-party runtime components.
---
## About
Nebula Browser is created as part of the wider Nebula Project, a collection of software experiments focused on controller-first computing, gaming-focused interfaces, and custom desktop experiences.
The project began as a browser for SteamOS and Steam Deck users, but is now evolving into a more flexible, native, and custom browser experience built around CEF.
-133
View File
@@ -1,133 +0,0 @@
# Nebot Typing Animation Feature
## Overview
Added a realistic typing animation to the Nebot chat interface that makes AI responses appear character by character, similar to ChatGPT and other modern AI chat interfaces.
## Features Added
### 1. **Typing Animation**
- Characters appear one by one instead of instantly
- Smooth, natural typing rhythm
- Configurable typing speed
- Blinking cursor indicator during typing
### 2. **Settings Integration**
- **Enable/Disable Toggle**: Users can turn typing animation on/off
- **Speed Control**: Adjustable from 10-200 characters per second
- **Live Preview**: Speed indicator updates in real-time
- **Persistent Settings**: Preferences are saved and restored
### 3. **Smart Behavior**
- **Queue Management**: Handles fast token streams efficiently
- **Graceful Fallback**: Falls back to instant display if disabled
- **Markdown Rendering**: Waits for typing to complete before rendering markdown
- **Auto-scroll**: Maintains scroll position during animation
## Technical Implementation
### Code Changes Made:
#### 1. **page.js** - Main Logic
```javascript
// Typing animation state
let typingQueue = [];
let isTyping = false;
let typingSpeed = 25; // milliseconds per character
let typingEnabled = true; // can be toggled in settings
function startTypingAnimation(element) {
if (isTyping || typingQueue.length === 0) return;
isTyping = true;
element.classList.add('typing');
function typeNext() {
if (typingQueue.length === 0) {
isTyping = false;
element.classList.remove('typing');
return;
}
const char = typingQueue.shift();
element.textContent += char;
els.messages.scrollTop = els.messages.scrollHeight;
setTimeout(typeNext, typingSpeed);
}
typeNext();
}
```
#### 2. **page.css** - Visual Effects
```css
/* Typing animation cursor */
.markdown.typing:after {
content: "▋";
color: var(--accent);
animation: blink 1s infinite;
margin-left: 1px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
```
#### 3. **Settings UI** - User Controls
- Checkbox to enable/disable typing animation
- Range slider for speed control (10-200 chars/sec)
- Real-time speed display
- Proper styling for form elements
## User Experience
### Before:
- Text appeared instantly when AI responded
- No visual feedback during response generation
- Less engaging interaction
### After:
- Smooth character-by-character reveal
- Blinking cursor shows active typing
- Configurable speed for user preference
- More engaging, human-like interaction
## Usage Instructions
1. **Open Nebot**: Navigate to the Nebot page in Nebula Browser
2. **Start a Chat**: Send a message to begin conversation
3. **Watch the Animation**: AI responses will type out naturally
4. **Customize Settings**:
- Click the ⚙ Settings button
- Toggle "Enable typing animation"
- Adjust typing speed with the slider
- Save changes
## Performance Considerations
- **Efficient Queuing**: Uses character queue to handle fast token streams
- **Memory Friendly**: Minimal memory overhead
- **Responsive**: Maintains smooth UI during animation
- **Interruptible**: Can be disabled without restart
## Future Enhancements
Potential improvements could include:
- Variable speed based on punctuation (pause at periods)
- Sound effects for typing
- Different animation styles
- Per-conversation speed settings
- Typing speed based on message length
## Testing
To test the feature:
1. Start Nebula Browser (`npm start`)
2. Navigate to Nebot page
3. Send a message and observe the typing animation
4. Try different speed settings in the settings panel
5. Toggle the feature on/off to compare experiences
The typing animation enhances the user experience by making AI interactions feel more natural and engaging, similar to popular chat interfaces like ChatGPT.
+33
View File
@@ -0,0 +1,33 @@
#include "app/run.h"
#include "platform/types.h"
#if defined(_WIN32)
#include <windows.h>
#elif defined(__APPLE__)
#include "include/wrapper/cef_library_loader.h"
#endif
#if defined(_WIN32)
int APIENTRY wWinMain(HINSTANCE instance,
HINSTANCE previous_instance,
LPWSTR command_line,
int show_command) {
NEBULA_UNUSED(previous_instance);
NEBULA_UNUSED(command_line);
const nebula::platform::AppStartup startup{instance, show_command};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop});
}
#else
int main(int argc, char* argv[]) {
#if defined(__APPLE__)
CefScopedLibraryLoader library_loader;
if (!library_loader.LoadInMain()) {
return 1;
}
#endif
const nebula::platform::AppStartup startup{argc, argv};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::Desktop});
}
#endif
+33
View File
@@ -0,0 +1,33 @@
#include "app/run.h"
#include "platform/types.h"
#if defined(_WIN32)
#include <windows.h>
#elif defined(__APPLE__)
#include "include/wrapper/cef_library_loader.h"
#endif
#if defined(_WIN32)
int APIENTRY wWinMain(HINSTANCE instance,
HINSTANCE previous_instance,
LPWSTR command_line,
int show_command) {
NEBULA_UNUSED(previous_instance);
NEBULA_UNUSED(command_line);
const nebula::platform::AppStartup startup{instance, show_command};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture});
}
#else
int main(int argc, char* argv[]) {
#if defined(__APPLE__)
CefScopedLibraryLoader library_loader;
if (!library_loader.LoadInMain()) {
return 1;
}
#endif
const nebula::platform::AppStartup startup{argc, argv};
return nebula::app::RunNebula(startup, {nebula::app::AppMode::BigPicture});
}
#endif
+25
View File
@@ -0,0 +1,25 @@
#include "cef/nebula_app.h"
#include "include/cef_app.h"
#include "include/wrapper/cef_library_loader.h"
#if defined(CEF_USE_SANDBOX)
#include "include/cef_sandbox_mac.h"
#endif
int main(int argc, char* argv[]) {
#if defined(CEF_USE_SANDBOX)
CefScopedSandboxContext sandbox_context;
if (!sandbox_context.Initialize(argc, argv)) {
return 1;
}
#endif
CefScopedLibraryLoader library_loader;
if (!library_loader.LoadInHelper()) {
return 1;
}
const CefMainArgs main_args(argc, argv);
CefRefPtr<nebula::cef::NebulaApp> app(new nebula::cef::NebulaApp);
return CefExecuteProcess(main_args, app, nullptr);
}
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env bash
# Run Nebula from an extracted AppImage folder with safe LD/XDG paths and Linux flags.
# This repo can end up with either:
# - nebula-appdir/squashfs-root/nebula (common after copying squashfs-root into nebula-appdir)
# - nebula-appdir/nebula (if you copied the extracted contents directly into nebula-appdir)
#
# PORTABLE MODE: User data (cookies, history, bookmarks) is stored in usr/data/ alongside the app.
# This keeps all data self-contained and portable on Linux.
set -euo pipefail
SELF_DIR="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"
if [ -x "$SELF_DIR/nebula-appdir/nebula" ]; then
APPROOT="$SELF_DIR/nebula-appdir"
BIN="$APPROOT/nebula"
elif [ -x "$SELF_DIR/squashfs-root/nebula" ]; then
APPROOT="$SELF_DIR/squashfs-root"
BIN="$APPROOT/nebula"
elif [ -x "$SELF_DIR/nebula" ]; then
APPROOT="$SELF_DIR"
BIN="$APPROOT/nebula"
else
echo "Nebula binary not found. Expected either:"
echo "- $SELF_DIR/nebula-appdir/nebula"
echo "- $SELF_DIR/squashfs-root/nebula"
echo "- $SELF_DIR/nebula"
exit 1
fi
# --- PORTABLE DATA CONFIGURATION ---
# Store user data (cookies, history, bookmarks, etc.) in a local folder
# Data is stored with secure permissions (700 for dirs, 600 for files)
PORTABLE_DATA_DIR="$SELF_DIR/usr/data"
export NEBULA_PORTABLE=1
export NEBULA_PORTABLE_PATH="$PORTABLE_DATA_DIR"
# Create portable data directory with secure permissions if it doesn't exist
if [ ! -d "$PORTABLE_DATA_DIR" ]; then
mkdir -p "$PORTABLE_DATA_DIR"
chmod 700 "$PORTABLE_DATA_DIR"
fi
export LD_LIBRARY_PATH="$APPROOT/usr/lib:${LD_LIBRARY_PATH:-}"
export XDG_DATA_DIRS="$APPROOT/usr/share:${XDG_DATA_DIRS:-/usr/share}"
# =============================================================================
# STEAM INPUT CONFIGURATION - Always set these to prevent controller emulation
# =============================================================================
# These variables tell Steam's input layer that this app handles controller
# input natively and should NOT have mouse/keyboard emulation applied.
# Set unconditionally since Software apps may not receive Steam env vars.
# Disable Steam's virtual gamepad layer - CRITICAL for native controller support
export SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0
export STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0
# Allow raw gamepad access
export SDL_GAMECONTROLLER_IGNORE_DEVICES=""
# Allow background gamepad events (useful when app doesn't have focus)
export SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1
# Hint to SDL that we're using gamepads natively
export SDL_GAMECONTROLLERCONFIG="${SDL_GAMECONTROLLERCONFIG:-}"
# Steam can inject its overlay via LD_PRELOAD (gameoverlayrenderer.so).
# In some setups this breaks Chromium/Electron gamepad input. When we detect
# a Steam-launched session, strip the overlay preload but keep other entries.
# Also configure additional Steam-specific settings.
if [ -n "${SteamAppId:-}" ] || [ -n "${STEAM_APP_ID:-}" ] || [ -n "${STEAM_COMPAT_CLIENT_INSTALL_PATH:-}" ] || [ -n "${STEAM_COMPAT_DATA_PATH:-}" ]; then
# Enable Big Picture Mode for controller-friendly UI when launched from Steam
export NEBULA_BIG_PICTURE="${NEBULA_BIG_PICTURE:-1}"
# Enable GPU acceleration on Linux
export NEBULA_GPU_ALLOW_LINUX=1
if [ -n "${LD_PRELOAD:-}" ]; then
CLEANED_LD_PRELOAD=""
IFS=':' read -r -a _preload_parts <<< "$LD_PRELOAD"
for _p in "${_preload_parts[@]}"; do
[ -z "$_p" ] && continue
case "$_p" in
*gameoverlayrenderer.so*)
;;
*)
if [ -z "$CLEANED_LD_PRELOAD" ]; then
CLEANED_LD_PRELOAD="$_p"
else
CLEANED_LD_PRELOAD="$CLEANED_LD_PRELOAD:$_p"
fi
;;
esac
done
if [ -z "$CLEANED_LD_PRELOAD" ]; then
unset LD_PRELOAD
else
export LD_PRELOAD="$CLEANED_LD_PRELOAD"
fi
fi
fi
GPU_ARGS=()
# Optional GPU tuning profiles strike a balance between enabling hardware decode
# and keeping the launcher safe on systems with fragile Mesa/VA-API stacks.
# Default to enabling GPU tweaks on Linux unless explicitly disabled.
export NEBULA_GPU_PROFILE="${NEBULA_GPU_PROFILE:-}"
GPU_GL_MODE="${NEBULA_GPU_GL:-}"
export NEBULA_GPU_TWEAKS="${NEBULA_GPU_TWEAKS:-1}"
if [ "${NEBULA_GPU_TWEAKS}" = "1" ] && [ -z "$NEBULA_GPU_PROFILE" ]; then
export NEBULA_GPU_PROFILE=vaapi
fi
case "$NEBULA_GPU_PROFILE" in
vaapi)
GPU_ARGS+=(--ignore-gpu-blocklist)
GPU_ARGS+=(--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,CanvasOopRasterization,UseSkiaRenderer,VaapiVideoDecoderLinuxGL,VaapiVideoDecoderLinuxZeroCopyGL)
GPU_ARGS+=(--enable-zero-copy)
GPU_ARGS+=(--enable-gpu-rasterization)
GPU_ARGS+=(--enable-accelerated-video-decode)
GPU_ARGS+=(--enable-native-gpu-memory-buffers)
if [ -z "${LIBVA_DRIVER_NAME:-}" ]; then
export LIBVA_DRIVER_NAME=radeonsi
fi
;;
angle)
GPU_ARGS+=(--ignore-gpu-blocklist)
GPU_ARGS+=(--enable-features=VaapiVideoDecoder,VaapiVideoEncoder,CanvasOopRasterization,UseSkiaRenderer,VaapiVideoDecoderLinuxGL)
GPU_ARGS+=(--enable-zero-copy)
GPU_ARGS+=(--enable-gpu-rasterization)
GPU_ARGS+=(--enable-native-gpu-memory-buffers)
GPU_ARGS+=(--enable-accelerated-video-decode)
if [ -z "${LIBVA_DRIVER_NAME:-}" ]; then
export LIBVA_DRIVER_NAME=radeonsi
fi
if [ -z "${AMD_VULKAN_ICD:-}" ]; then
export AMD_VULKAN_ICD=RADV
fi
;;
amd-handheld)
GPU_ARGS+=(--ignore-gpu-blocklist)
GPU_ARGS+=(--use-angle=vulkan)
AMD_HANDHELD_FEATURES="VaapiVideoDecoder,VaapiVideoEncoder,CanvasOopRasterization,UseSkiaRenderer,VaapiVideoDecoderLinuxGL,VaapiVideoDecoderLinuxZeroCopyGL"
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
AMD_HANDHELD_FEATURES="UseOzonePlatform,WaylandWindowDecorations,${AMD_HANDHELD_FEATURES}"
GPU_ARGS+=(--ozone-platform-hint=auto)
GPU_ARGS+=(--ozone-platform=wayland)
fi
GPU_ARGS+=(--enable-features="$AMD_HANDHELD_FEATURES")
GPU_ARGS+=(--enable-zero-copy)
GPU_ARGS+=(--enable-gpu-rasterization)
if [ -z "${LIBVA_DRIVER_NAME:-}" ]; then
export LIBVA_DRIVER_NAME=radeonsi
fi
if [ -z "${MESA_VK_WSI_PRESENT_MODE:-}" ]; then
export MESA_VK_WSI_PRESENT_MODE=mailbox
fi
;;
software)
GPU_ARGS+=(--disable-gpu)
;;
"")
;;
*)
echo "Warning: Unknown NEBULA_GPU_PROFILE '$NEBULA_GPU_PROFILE'" >&2
;;
esac
if [ -n "$GPU_GL_MODE" ]; then
# Map common GL mode names to Electron-compatible values
case "$GPU_GL_MODE" in
egl|egl-angle) GPU_GL_MODE="angle" ;;
desktop) GPU_GL_MODE="desktop" ;;
esac
GPU_ARGS+=(--use-gl="$GPU_GL_MODE")
fi
if [ -n "${NEBULA_GPU_EXTRA_ARGS:-}" ]; then
# shellcheck disable=SC2086 # word splitting is intentional for custom flag tokens
read -r -a _nebula_gpu_extra <<< "${NEBULA_GPU_EXTRA_ARGS}"
GPU_ARGS+=("${_nebula_gpu_extra[@]}")
fi
exec "$BIN" --no-sandbox "${GPU_ARGS[@]}" "$@"
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env xdg-open
[Desktop Entry]
Name=Nebula
Comment=Nebula Browser (Portable Mode - data stored locally)
Exec=env NEBULA_GPU_TWEAKS=1 /home/deck/Documents/Repos/NebulaBrowser/nebula-appdir/Nebula %U
Terminal=false
Type=Application
Icon=nebula
Categories=Network;WebBrowser;
StartupWMClass=Nebula
-24
View File
@@ -1,24 +0,0 @@
#!/bin/bash
# Run Nebula with portable data storage
# User data (cookies, history, bookmarks) is stored in usr/data/ alongside the app.
set -e
HERE="$(cd "$(dirname "$0")" && pwd)"
export APPDIR="$HERE"
export PATH="$HERE/usr/bin:$PATH"
export LD_LIBRARY_PATH="$HERE/usr/lib:$HERE/usr/lib64:$LD_LIBRARY_PATH"
# --- PORTABLE DATA CONFIGURATION ---
# Store user data in a local folder for portable operation
PORTABLE_DATA_DIR="$HERE/usr/data"
export NEBULA_PORTABLE=1
export NEBULA_PORTABLE_PATH="$PORTABLE_DATA_DIR"
# Create portable data directory with secure permissions if it doesn't exist
if [ ! -d "$PORTABLE_DATA_DIR" ]; then
mkdir -p "$PORTABLE_DATA_DIR"
chmod 700 "$PORTABLE_DATA_DIR"
fi
exec "$HERE/nebula-appdir/AppRun"
-1
View File
@@ -1 +0,0 @@
4290110
@@ -1,9 +0,0 @@
[Desktop Entry]
Name=Nebula
Comment=Nebula Browser
Exec=./Nebula %U
Terminal=false
Type=Application
Icon=nebula
Categories=Network;WebBrowser;
StartupWMClass=Nebula

Before

Width:  |  Height:  |  Size: 976 B

After

Width:  |  Height:  |  Size: 976 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 352 KiB

Before

Width:  |  Height:  |  Size: 890 KiB

After

Width:  |  Height:  |  Size: 890 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

-3
View File
@@ -1,3 +0,0 @@
[
]
-1
View File
@@ -1 +0,0 @@
[]
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.nebula.browser.${HELPER_BUNDLE_NAME}.helper${BUNDLE_ID_SUFFIX}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>LSEnvironment</key>
<dict>
<key>MallocNanoZone</key>
<string>0</string>
</dict>
<key>LSFileQuarantineEnabled</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSUIElement</key>
<string>1</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
build/compile_commands.json
+215
View File
@@ -0,0 +1,215 @@
# Cross-platform build guide
Nebula Browser uses a **single codebase** and **one git branch** for Windows, macOS, and Linux. You do not maintain separate platform branches. Instead, each machine unpacks the correct [CEF](https://bitbucket.org/chromiumembedded/cef) binary distribution into `thirdparty/cef/` and builds with CMake.
## Overview
| Layer | Location | Platform-specific? |
|-------|----------|-------------------|
| CEF binaries | `thirdparty/cef/` (gitignored) | Yes — one distribution per OS |
| CMake / deploy | `CMakeLists.txt` | Yes — uses CEFs `COPY_FILES`, `COPY_MAC_FRAMEWORK`, etc. |
| App entry & CEF init | `src/app/run.cpp`, `src/platform/*/startup_*.cpp` | Partly |
| Native window shell | `src/platform/win\|mac\|linux/` | Yes |
| Browser embedding | `src/platform/*/browser_host_*.cpp` | Partly |
| Tabs, URLs, UI routing | `src/browser/`, `src/ui/`, `src/cef/` | No (shared) |
**Windows** has a full native shell (custom frame, DWM, embedded CEF views).
**macOS** has a Cocoa native shell (`NSWindow` / `NSView`) with CEF child-view embedding.
**Linux** compiles and links today, but `NebulaWindow::Create()` is still a stub that returns `false` until a real X11 (or GTK) host is implemented.
## Directory layout
```
thirdparty/cef/ # CEF standard distribution for your OS (not in git)
src/platform/
types.h # Rect, BrowserLayout, NativeWindow, AppStartup
startup.h # PrepareApp, single-instance, CefMainArgs, CefSettings paths
browser_host.h # CefWindowInfo helpers, resize/show/destroy browser HWNDs
paths_platform.h # ExecutableDirectory, DefaultUserDataRoot, PathToUtf8
win/ # Windows implementation
mac/ # macOS Cocoa implementation
linux/ # Linux stubs + partial CEF glue
src/window/
nebula_window.h # Platform-agnostic window API (PIMPL)
src/app/ # Shared controller and run loop
src/browser/ # Shared tab/session logic
src/cef/ # Shared CEF app and browser client
src/ui/ # Shared nebula:// URLs and path helpers
ui/ # HTML/JS UI (copied next to the binary at build time)
```
## Prerequisites
- **CMake** 3.21+
- **C++20** compiler
- Windows: Visual Studio 2022
- macOS: Xcode 16+ (Clang)
- Linux: GCC 10+ or Clang, plus development packages for X11 (CEFs CMake runs `pkg-config` for X11 on Linux)
- **CEF standard binary distribution** for your OS and CPU architecture from the [CEF builds](https://cef-builds.spotifycdn.com/index.html) page (or your usual CEF source)
## CEF setup
1. Download the **standard distribution** for your platform (e.g. `cef_binary_*_windows64`, `*_macos*`, `*_linux64`).
2. Extract so that this path exists:
```
thirdparty/cef/cmake/FindCEF.cmake
```
3. The rest of the distribution (`Release/`, `Resources/`, `libcef_dll/`, etc.) should sit directly under `thirdparty/cef/` as shipped by CEF.
`thirdparty/cef/` is listed in `.gitignore` so binaries are never committed. Each developer and CI machine supplies its own copy.
## IDE / clangd setup
Without CEF headers, clangd will show many errors (unknown types like `CefWindowInfo`, `CefMainArgs`, etc.). To get full IDE support:
1. Unpack CEF into `thirdparty/cef/` as described above
2. Generate `compile_commands.json`:
```bash
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
```
3. Symlink it to the project root:
```bash
ln -s build/compile_commands.json .
```
The included `.clangd` file provides fallback flags, but full diagnostics require the compile database.
Optional: override the location at configure time:
```bash
cmake -B build -DCEF_ROOT=/path/to/cef_binary
```
## Building
### Windows
```powershell
cmake -B build
cmake --build build --config Release
```
Output: `build/Release/NebulaBrowser.exe` (plus CEF DLLs and `ui/` copied beside it).
### macOS
```bash
cmake -B build -DPROJECT_ARCH=x86_64 # or arm64 on Apple Silicon
cmake --build build --config Release
```
CMake builds a `NebulaBrowser.app` bundle and copies the Chromium Embedded Framework into `Contents/Frameworks`. The macOS port includes a Cocoa `NSWindow` / `NSView` host and child-view embedding for CEF browsers.
CEF **helper app** subprocess targets may still need to be added if the selected CEF distribution does not provide a compatible default subprocess layout. Use CEFs `cefsimple` sample as the reference for the complete Mac bundle layout.
### Linux
```bash
cmake -B build
cmake --build build
```
Output: `build/NebulaBrowser` (or `build/<Config>/NebulaBrowser` depending on generator). CEF `.so` files and resources are copied next to the executable.
You may need to set **SUID** on `chrome-sandbox` as printed by CMake post-build (CEF documents this in its Linux README).
Until a Linux host window exists, the binary will not show a UI for the same reason as macOS.
## How CMake picks a platform
`find_package(CEF)` loads CEFs `cef_variables.cmake`, which sets:
- `OS_WINDOWS`
- `OS_MACOSX`
- `OS_LINUX`
`CMakeLists.txt` then:
1. Adds the matching `src/platform/{win,mac,linux}/*.cpp` sources
2. Sets the executable type (`WIN32`, `MACOSX_BUNDLE`, or plain executable)
3. Links `libcef_dll_wrapper`, `libcef_lib`, and `${CEF_STANDARD_LIBS}`
4. Runs CEFs copy macros so runtime files land beside the app
Shared sources are always compiled; only the `src/platform/...` tree changes per OS.
## Application flow
```
main (app/main.cpp)
└─ RunNebula(AppStartup) src/app/run.cpp
├─ platform::PrepareApp()
├─ CefExecuteProcess() (subprocesses)
├─ platform::TryAcquireSingleInstance()
├─ CefInitialize()
├─ NebulaController::Create()
│ └─ NebulaWindow::Create() platform-specific
└─ CefRunMessageLoop()
```
`AppStartup` carries platform entry data:
- **Windows:** `HINSTANCE` + show command (`wWinMain`)
- **macOS / Linux:** `argc` / `argv` (`main`)
## User data and profile paths
Shared logic in `src/ui/paths.cpp` writes under:
| OS | Default root |
|----|----------------|
| Windows | `%LOCALAPPDATA%\Nebula\User Data` (falls back to exe directory) |
| macOS | `~/Library/Application Support/Nebula/User Data` |
| Linux | `$XDG_DATA_HOME/Nebula/User Data` or `~/.local/share/Nebula/User Data` |
Platform-specific discovery lives in `src/platform/*/paths_*.cpp`.
## GPU / Chromium flags
`src/cef/nebula_app.cpp` applies shared switches (`no-sandbox`, `ignore-gpu-blocklist`, etc.) and platform graphics backends:
| OS | Backend |
|----|---------|
| Windows | ANGLE + D3D11 |
| macOS | ANGLE + Metal |
| Linux | EGL |
## Adding or changing platform code
1. **Shared behavior** (tabs, `nebula://` URLs, CEF clients) → edit under `src/browser/`, `src/ui/`, `src/cef/`, `src/app/` only if it is truly cross-platform.
2. **Native window or OS API** → edit `src/platform/<os>/` and keep `src/window/nebula_window.h` free of `#include <windows.h>` (or Cocoa/X11 headers).
3. **CEF child window embedding** → `src/platform/<os>/browser_host_*.cpp` (`MakeChildWindowInfo`, resize, visibility).
4. **Startup / paths** → `startup_*.cpp` and `paths_*.cpp` in the same folder.
5. **New OS** → add `src/platform/<os>/`, extend the `if(OS_...)` blocks in `CMakeLists.txt`, and document CEF distribution layout here.
## Porting checklist (macOS / Linux)
- [x] macOS: implement `NebulaWindow` in `nebula_window_mac.mm`
- [x] macOS: wire `browser_host_mac.mm` resize/show/raise to Cocoa views
- [ ] Linux: implement `NebulaWindow` in `nebula_window_linux.cpp`
- [ ] Linux: wire `browser_host_linux.cpp` resize/show/raise to the real toolkit
- [ ] macOS: add CEF helper app targets and bundle layout (copy from `cefsimple`)
- [ ] Linux: confirm X11 (or chosen toolkit) parent handle for `CefWindowInfo::SetAsChild`
- [ ] Test GPU diagnostics page and hardware acceleration on target hardware
- [ ] Package or script runtime layout (`ui/`, CEF frameworks/libs, ICU locales, etc.)
## FAQ
**Do I need a separate git branch per OS?**
No. Use one branch; swap `thirdparty/cef` and rebuild on each machine.
**Can I commit CEF into the repo?**
Not recommended (size, licensing, per-arch binaries). Keeping `thirdparty/cef/` gitignored is intentional.
**Why does Linux build but not run?**
The Linux window stub intentionally returns `false` from `Create()` until a native shell exists. Shared CEF and browser code are ready; the missing piece is the host window.
**Where is the Windows UI code?**
`src/platform/win/nebula_window_win.cpp` — custom chrome, hit-testing, fullscreen, and child browser placement.
-168
View File
@@ -1,168 +0,0 @@
# Big Picture Mode - Steam Deck & Controller UI
Nebula Browser includes a **Big Picture Mode** - a controller-friendly, console-style interface designed for Steam Deck, handheld devices, and living room setups.
## 🚀 Single Window Architecture
Big Picture Mode now opens **in the main window** instead of a separate window. This design:
- **Keeps resources low** - No extra Electron window process
- **Prevents SteamOS conflicts** - When auto-launching in Gaming Mode, Steam won't create a desktop mode alongside, preventing Steam from overriding controls to emulate keyboard/mouse
- **Seamless switching** - Navigate between Desktop and Big Picture modes smoothly
## ⚠️ Steam Deck: Disabling Mouse Emulation
If Steam is emulating mouse/keyboard input with the joysticks (overriding native controller support), you need to configure Steam Input:
### Quick Fix - Steam Launch Options
Add this to your Steam launch options (Right-click → Properties → Launch Options):
```bash
SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD=0 STEAM_INPUT_ENABLE_VIRTUAL_GAMEPAD=0 %command% --big-picture
```
### Recommended Fix - Disable Steam Input Per-Game
1. In Steam, right-click **Nebula****Properties****Controller**
2. Set **"Override for Nebula"** to **"Disable Steam Input"**
3. This completely disables Steam's input emulation for this app
### Using the Steam Deck Launcher Script
For the easiest setup, use the included launcher script:
1. Set Steam launch options to: `./start-steamdeck.sh`
The script automatically sets all necessary environment variables.
See [README-STEAM.md](../README-STEAM.md) for detailed troubleshooting.
---
## Features
### 🎮 Controller Support
- **Full gamepad navigation** - Use D-pad or left stick to navigate
- **Button mapping**:
- **A / Cross** - Select/Activate
- **B / Circle** - Go Back
- **Y / Triangle** - Quick Search
- **Start** - Toggle Settings
- **Audio feedback** for navigation
### 📱 Optimized for Steam Deck
- **1280x800 native resolution** support
- Automatic detection of Steam Deck screens
- Large touch-friendly UI elements
- Fullscreen immersive experience
### 🎨 Modern Console-Style UI
- Inspired by Steam OS Big Picture and Xbox Dashboard
- Smooth animations and transitions
- Glowing focus indicators
- Dark theme optimized for OLED displays
### ⌨️ On-Screen Keyboard
- Built-in virtual keyboard for controller input
- URL and search input support
- Special keys for common domains (.com, .org, etc.)
## How to Access
### From Desktop Mode
1. **Menu Button (☰)** → Click **"🎮 Big Picture Mode"**
2. **Settings****General** → Click **"Launch Big Picture Mode"**
### Keyboard Shortcut
- Press `F11` while in Big Picture Mode to toggle fullscreen
### Automatic Detection
If Nebula detects a Steam Deck-sized display (1280x800), it will suggest Big Picture Mode in settings.
### Auto-start in SteamOS Gaming Mode
When Nebula is launched from SteamOS **Gaming Mode** (gamescope / Steam gamepad UI), it will automatically start in **Big Picture Mode**.
You can override this behavior:
- Force Big Picture at launch: launch options `--big-picture` (or `--bigpicture`)
- Disable Big Picture auto-start: launch options `--no-big-picture` (or `--no-bigpicture`)
- Environment overrides: `NEBULA_BIG_PICTURE=1` / `NEBULA_NO_BIG_PICTURE=1`
## Navigation Sections
| Section | Description |
|---------|-------------|
| **Home** | Quick access sites, search, and recent browsing |
| **Bookmarks** | Your saved websites in a tile grid |
| **History** | Recently visited sites |
| **Downloads** | Downloaded files |
| **NeBot AI** | Launch the AI assistant |
| **Settings** | Theme, privacy, and display options |
## Controller Button Reference
| Button | Action |
|--------|--------|
| D-Pad / Left Stick | Navigate between elements |
| A / Cross | Select focused element |
| B / Circle | Go back / Close menu |
| Y / Triangle | Open search (on-screen keyboard) |
| Start | Open/Close settings |
| LB/RB | Scroll horizontally |
## Exiting Big Picture Mode
- Press the **Exit** button in the top-right corner
- Go to **Settings****Desktop Mode**
- Press `Escape` key multiple times
## Technical Details
### Files
- `renderer/bigpicture.html` - Main HTML structure
- `renderer/bigpicture.css` - Console-optimized styles
- `renderer/bigpicture.js` - Controller handling and navigation
### Screen Detection
Big Picture Mode is suggested for displays matching:
- Steam Deck resolution: 1280×800
- Screens smaller than 1366px width
- 16:10 or 16:9 aspect ratios
### API
```javascript
// Check if Big Picture Mode is recommended
const suggested = await window.bigPictureAPI.isSuggested();
// Get screen information
const info = await window.bigPictureAPI.getScreenInfo();
// Check if currently in Big Picture Mode
const isActive = await window.bigPictureAPI.isActive();
// Launch Big Picture Mode (navigates main window)
await window.bigPictureAPI.launch();
// Exit Big Picture Mode (navigates back to desktop UI)
await window.bigPictureAPI.exit();
```
## Customization
The Big Picture Mode respects your theme settings. Colors are applied from your selected theme:
- Background colors
- Accent and primary colors
- Text colors
## Known Limitations
- Some complex web forms may be difficult to navigate with controller only
- Video players use native controls
- Right-click context menus require mouse/touch
## Future Improvements
- [ ] Rumble/haptic feedback for compatible controllers
- [ ] Voice search integration with NeBot
- [ ] Picture-in-picture mode for videos
- [ ] Game overlay mode
- [ ] Custom controller mappings
-119
View File
@@ -1,119 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that are compassionate, direct, and respectful.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards and
will take appropriate and fair corrective action in response to any behavior that
they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interaction in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
-65
View File
@@ -1,65 +0,0 @@
# Contributing to Nebula
First off, thank you for considering contributing to Nebula! It's people like you that make open source such a great community.
## How Can I Contribute?
### Reporting Bugs
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Bobbybear007/NebulaBrowser/issues).
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Bobbybear007/NebulaBrowser/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
### Suggesting Enhancements
- Open a new issue to discuss your enhancement. Please provide a clear description of the enhancement and its potential benefits.
### Pull Requests
1. **Fork the repository** to your own GitHub account.
2. **Clone the project** to your machine.
3. **Create a new branch** for your changes:
```sh
git checkout -b feature/your-feature-name
```
4. **Make your changes** and commit them with a clear, descriptive commit message:
```sh
git commit -m "Add some feature"
```
5. **Push your branch** to your fork:
```sh
git push origin feature/your-feature-name
```
6. **Open a pull request** to the `main` branch of the original repository. Provide a clear title and description for your pull request, explaining the changes you've made.
## Styleguides
### Git Commit Messages
- Use the present tense ("Add feature" not "Added feature").
- Use the imperative mood ("Move cursor to..." not "Moves cursor to...").
- Limit the first line to 72 characters or less.
- Reference issues and pull requests liberally after the first line.
### Code of Conduct
We have a [Code of Conduct](CODE_OF_CONDUCT.md) that all contributors are expected to follow. Please make sure you are familiar with its contents.
### JavaScript Styleguide
- All JavaScript must adhere to [StandardJS](https://standardjs.com/). This helps us maintain a consistent coding style.
- Use soft-tabs with a two-space indent.
- Prefer single quotes `'` over double quotes `"`.
- No semicolons.
- For more details, please refer to the [StandardJS rules](https://standardjs.com/rules.html).
### CSS Styleguide
- Follow a BEM-like naming convention for classes (`block__element--modifier`).
- Use soft-tabs with a two-space indent.
- Write selectors and their properties on separate lines.
- Organize properties logically (e.g., positioning, box model, typography, visual).
- Use `rem` for font sizes and `px` for borders.
- Use `===` and `!==` instead of `==` and `!=` for comparisons.
- Always declare variables with `const` or `let` instead of `var`.
- Use arrow functions instead of `function` where appropriate.
- Prefer template literals over string concatenation.
-32
View File
@@ -1,32 +0,0 @@
# Core Concepts
This document explains the core architectural concepts of the Nebula browser.
### Main and Renderer Processes
Electron applications have two types of processes: the **main process** and one or more **renderer processes**.
- **Main Process**: The main process, which runs the `main.js` script, is the entry point of the application. It runs in a Node.js environment, meaning it has access to all Node.js APIs like `fs` for file system access and `ipcMain` for communication. It is responsible for creating and managing `BrowserWindow` instances, which are the application's windows.
- **Renderer Process**: Each `BrowserWindow` runs its own renderer process. The renderer process is responsible for rendering web content—in Nebula's case, the browser's user interface (UI) built with HTML, CSS, and JavaScript. The renderer process does not have direct access to Node.js APIs for security reasons.
### Inter-Process Communication (IPC)
Since the main and renderer processes are separate, they need a way to communicate. This is done through Inter-Process Communication (IPC).
- **`ipcMain` and `ipcRenderer`**: Electron provides the `ipcMain` and `ipcRenderer` modules for this purpose. The main process listens for messages using `ipcMain.handle`, and the renderer sends messages using `ipcRenderer.invoke`.
- Nebula uses IPC for features like downloads control, bookmarks/history management, performance reports, GPU diagnostics, and window controls.
- **Context Bridge and Preload Script**: To securely expose APIs from the main process to the renderer process, Electron uses a **preload script** and the **context bridge**. The `preload.js` script runs in a special environment that has access to both the `window` object of the renderer process and Node.js APIs. The `contextBridge` is used to expose specific functions from the preload script to the renderer process, ensuring that the renderer process cannot access powerful Node.js APIs directly.
### Performance and GPU Management
- **Performance Monitoring**: The `performance-monitor.js` module helps track the application's performance by monitoring metrics like memory usage and page load times. This is essential for identifying and addressing performance bottlenecks.
- **GPU Configuration**: The `gpu-config.js` and `gpu-fallback.js` modules manage GPU acceleration. Electron uses the system's GPU to render content, which can significantly improve performance. However, GPU drivers can sometimes be a source of instability. These modules allow Nebula to check the GPU status and apply fallbacks (like disabling hardware acceleration) if issues are detected, ensuring a more stable experience.
### Authentication & User Agent Strategy
- **Auth Flow Compatibility**: Nebula allows popup windows for http/https to preserve OAuth/SSO flows without stripping POST bodies.
- **WebAuthn**: Platform authenticator features are enabled where supported, with diagnostics logged to help troubleshoot availability.
- **User Agent**: The default Electron token is removed from the UA string to improve site compatibility while appending a `Nebula/<version>` marker.
-68
View File
@@ -1,68 +0,0 @@
# Nebula Browser Themes
This directory contains theme files for the Nebula Browser customization system.
## Theme Structure
Each theme is a JSON file with the following structure:
```json
{
"name": "Theme Name",
"colors": {
"bg": "#121418",
"darkBlue": "#0B1C2B",
"darkPurple": "#1B1035",
"primary": "#7B2EFF",
"accent": "#00C6FF",
"text": "#E0E0E0"
},
"layout": "centered",
"showLogo": true,
"customTitle": "Nebula Browser",
"gradient": "linear-gradient(145deg, #121418 0%, #1B1035 100%)",
"version": "1.0",
"description": "Theme description"
}
```
## Color Properties
- `bg`: Main background color
- `darkBlue`: Secondary dark blue accent
- `darkPurple`: Secondary dark purple accent
- `primary`: Primary accent color (used for buttons, logos)
- `accent`: Secondary accent color (used for highlights)
- `text`: Main text color
## Layout Options
- `centered`: Default centered layout
- `sidebar`: Sidebar navigation layout
- `compact`: Compact view layout
## Directories
- `/downloaded/`: Themes downloaded from the community
- `/user/`: User-created custom themes
## Usage
1. **Import Theme**: Go to Settings > Customization > Import Theme
2. **Export Theme**: Create your custom theme and export it
3. **Share Themes**: Share your exported .json files with other users
## Creating Custom Themes
1. Go to Settings > Browser Customization
2. Adjust colors and settings using the controls
3. Use the live preview to see changes
4. Save as custom theme or export to share
## Community Themes
Place downloaded community themes in the `/downloaded/` folder. The browser will automatically detect and make them available in the theme selector.
## Non-Destructive Design
All theme changes are stored separately and can be reset to default at any time. Your customizations never modify the original browser files.
-96
View File
@@ -1,96 +0,0 @@
# Features
This document provides a detailed overview of the features available in Nebula.
### Privacy Control
Nebula is designed with your privacy in mind. You have granular control over your browsing data.
- **Clear Browsing Data:** You can easily clear your browsing history, cookies, cache, and local storage. This can be done from the settings page.
- **No Tracking by Default:** Nebula does not collect any personal data for tracking or advertising purposes.
### Tab Management
Efficiently manage your browsing session with Nebula's tab management features.
- **New Tabs:** Open new tabs to browse multiple websites at once.
- **Tab Controls:** Each tab has standard controls for closing.
- **Open in New Window:** You can pop a tab out into its own separate window.
### Bookmarks
Save and access your favorite websites with ease.
- **Add Bookmarks:** Save the current page to your bookmarks.
- **View Bookmarks:** Access your saved bookmarks from the bookmarks bar or a dedicated page.
### History
Nebula keeps a record of your browsing and search history to help you find your way back to previously visited sites.
- **Site History:** A list of all the websites you have visited.
- **Search History:** A list of all the searches you have made.
- **Clear History:** You can clear your history at any time from the settings page.
### Downloads Manager
Reliably download files with progress and controls.
- **Progress & State:** Each download shows received/total bytes and status.
- **Pause/Resume/Cancel:** Control active downloads where the server supports resuming.
- **Open or Reveal:** Open downloaded files directly or show them in your file manager.
- **Safe Filenames:** Files are saved to your OS Downloads folder with automatic de-duplication.
### Performance Monitoring
Nebula includes built-in tools to help you monitor the browser's performance.
- **Performance Report:** View a detailed report of performance metrics, including page load times and memory usage.
- **Force Garbage Collection:** Manually trigger garbage collection to free up memory.
### GPU Acceleration Control
For advanced users, Nebula provides tools to manage GPU acceleration.
- **GPU Diagnostics:** View detailed information about your system's GPU and its status.
- **GPU Fallback:** If you experience rendering issues, you can apply a GPU fallback to use a more stable rendering path. This can help resolve visual glitches or crashes.
### Native Context Menu
Nebula provides a native right-click menu across pages and webviews.
- **Navigation:** Back, Forward, Reload.
- **Links:** Open in new tab, Download link, Open externally, Copy address.
- **Images:** Open in new tab, Save image as, Copy image address.
- **Editing:** Undo/Redo, Cut/Copy/Paste, Select All when applicable.
- **Developer:** Inspect Element (opens DevTools docked by default).
### Authentication & Web Compatibility
- **OAuth/SSO Friendly:** Popup windows are allowed for http/https to support common login flows.
- **WebAuthn Diagnostics:** Platform authenticator features are enabled where supported and logged for troubleshooting.
- **Sturdy Navigation:** Login POST navigations are not intercepted by the main process.
### Custom Themes & Customization
Nebula offers extensive customization options to personalize your browsing experience.
- **Theme System:** Choose from built-in themes (default, forest, ocean, sunset, cyberpunk, midnight-rose, arctic-ice, cherry-blossom, cosmic-purple, emerald-dream, mocha-coffee, lavender-fields) or create your own custom themes.
- **Live Theme Editor:** Modify colors, gradients, and layout options with real-time preview in the settings.
- **Import/Export Themes:** Share custom themes with the community or use themes created by other users.
- **Non-Destructive Design:** All customizations are stored separately and can be reset to default at any time.
- **Layout Options:** Switch between centered, sidebar, and compact view layouts.
- **Custom Branding:** Personalize the browser title and logo visibility.
For detailed information about creating and managing themes, see the [Customization Guide](Customization.md).
### Cross-Platform
Nebula is built with Electron, allowing it to run on multiple operating systems.
- **Windows, macOS, and Linux:** Enjoy a consistent browsing experience across different platforms.
### Miscellaneous
- **User Agent Strategy:** Electron token is removed from the default user agent to improve site compatibility while appending a `Nebula/x.y.z` marker. You can opt-in to include the Electron token by setting `NEBULA_DEBUG_ELECTRON_UA=1`.
- **Open Local Files:** Use the file picker to open `file://` URLs directly.
-158
View File
@@ -1,158 +0,0 @@
# Nebula Browser - GPU Error 18 Fix & Performance Optimizations
## Problem Solved ✅
**Error 18** - GPU process launch failure has been resolved. The browser now starts successfully and uses the best available rendering method.
## What Was Fixed
### 1. GPU Configuration System
- **New GPU Config Manager**: Created `gpu-config.js` that intelligently handles GPU setup
- **Automatic Fallback**: If GPU fails, automatically switches to software rendering
- **Progressive Enhancement**: Tries GPU acceleration first, falls back gracefully
- **No More Crashes**: Error 18 eliminated through proper GPU process handling
### 2. Command Line Optimizations
**Essential Fixes:**
```javascript
app.commandLine.appendSwitch('no-sandbox');
app.commandLine.appendSwitch('disable-dev-shm-usage');
app.commandLine.appendSwitch('disable-gpu-sandbox');
```
**Performance Improvements:**
```javascript
app.commandLine.appendSwitch('disable-background-timer-throttling');
app.commandLine.appendSwitch('disable-renderer-backgrounding');
app.commandLine.appendSwitch('max_old_space_size', '4096');
```
### 3. Smart GPU Detection
The browser now:
- ✅ Detects GPU capabilities at startup
- ✅ Provides clear status information
- ✅ Offers recommendations for improvements
- ✅ Gracefully handles GPU failures
## Performance Improvements Applied
### 1. Memory Management
- **Debounced History Recording**: Reduces file I/O operations
- **Async File Operations**: Prevents UI blocking
- **Garbage Collection**: Manual GC triggering available
- **Memory Monitoring**: Built-in performance tracking
### 2. Rendering Optimizations
- **Hardware Acceleration**: When available, uses GPU for better performance
- **Software Fallback**: Stable rendering when GPU isn't available
- **CSS Optimizations**: Hardware-accelerated animations and scrolling
- **Efficient Paint Management**: Reduced repaints and reflows
### 3. Caching & Network
- **Request Caching**: HTTP cache headers for faster loading
- **Resource Preloading**: Critical resources loaded early
- **QUIC Protocol**: Faster network connections
- **localStorage Optimization**: Efficient bookmark and history management
## Current Status
### GPU Status:
- **Hardware Acceleration**: ❌ Not available on this system
- **Software Rendering**: ✅ Working perfectly
- **Stability**: ✅ No crashes, no Error 18
- **Performance**: ✅ Optimized for software rendering
### Browser Performance:
- **Startup Time**: ⚡ Significantly improved
- **Memory Usage**: 📉 Reduced and monitored
- **Responsiveness**: ✅ Smooth UI interactions
- **Stability**: ✅ Robust error handling
## Diagnostic Tools Added
### 1. GPU Diagnostics Page
Location: `renderer/gpu-diagnostics.html`
- Real-time GPU status monitoring
- WebGL and Canvas 2D testing
- Performance metrics
- Manual fallback controls
### 2. Performance Monitor
- Memory usage tracking
- CPU monitoring
- Load time analysis
- Automatic reporting every 5 minutes
### 3. Startup Script
Location: `start-gpu-safe.bat`
- Multiple GPU configuration options
- Debug mode with verbose logging
- Administrator privilege checking
## Usage Instructions
### Normal Startup:
```bash
npm start
```
### Diagnostic Startup:
```bash
start-gpu-safe.bat
```
### Check GPU Status:
1. Open browser
2. Navigate to GPU diagnostics page
3. View real-time status and recommendations
## Why GPU Might Be Disabled
Common reasons for GPU acceleration being unavailable:
1. **Outdated Drivers**: Graphics drivers need updating
2. **Hardware Limitations**: Older or integrated graphics
3. **Windows Settings**: Hardware acceleration disabled in system
4. **Virtual Environment**: Running in VM or remote desktop
5. **Security Software**: Antivirus blocking GPU access
## Recommendations
### For Better Performance:
1. **Update Graphics Drivers**: Check manufacturer website
2. **Windows Update**: Ensure system is up to date
3. **Hardware Acceleration**: Enable in Windows display settings
4. **Run as Administrator**: May help with GPU access
5. **Check Antivirus**: Temporarily disable to test
### Current Configuration Works:
Even without GPU acceleration, the browser is now:
-**Fast**: Software rendering optimized
- 🛡️ **Stable**: No crashes or errors
- 🔧 **Configurable**: Easy to adjust settings
- 📊 **Monitored**: Performance tracking included
## Files Modified/Added
### Core Files:
- `main.js` - Enhanced GPU handling, performance optimizations
- `preload.js` - Improved API exposure with caching
- `performance-monitor.js` - System performance tracking
### GPU Management:
- `gpu-config.js` - Intelligent GPU configuration
- `gpu-fallback.js` - Crash handling and fallbacks
- `start-gpu-safe.bat` - Diagnostic startup script
### UI/CSS:
- `performance.css` - Hardware acceleration optimizations
- `gpu-diagnostics.html` - GPU status and testing page
## Result: ✅ Problem Solved
Your Nebula browser now:
1. **Starts without Error 18**
2. **Runs smoothly on your system**
3. **Uses optimal rendering method**
4. **Provides performance monitoring**
5. **Offers diagnostic tools**
The browser is optimized to work great with or without GPU acceleration!
-31
View File
@@ -1,31 +0,0 @@
# MIT License
The NebulaBrowser project is licensed under the MIT License. This license is a permissive open-source license that allows users to freely use, modify, distribute, and sublicense the software, provided that the original copyright notice and permission notice are included in all copies or substantial portions of the software.
## Key Points of the MIT License
1. **Permission to Use**: You are free to use this software for personal, educational, or commercial purposes without any restrictions.
2. **Modification and Distribution**: You can modify the source code and distribute your modified versions, as long as the original copyright notice is retained.
3. **No Warranty**: The software is provided "as is," without warranty of any kind. The authors are not liable for any damages or issues arising from the use of the software.
## How It Applies to NebulaBrowser
- **Open Contribution**: Developers are encouraged to contribute to the NebulaBrowser project. Contributions will also fall under the MIT License, ensuring that the project remains open and accessible to everyone.
- **Commercial Use**: Businesses can integrate NebulaBrowser into their products or services without needing to pay royalties or seek additional permissions.
- **Attribution**: Any distribution of NebulaBrowser or its derivatives must include the original copyright notice to acknowledge the work of the contributors.
## Copyright Notice
```
Copyright (c) 2025 Zambazos Media Group
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
By using or contributing to NebulaBrowser, you agree to the terms of the MIT License.
More on the MIT License [Here](https://tlo.mit.edu/understand-ip/exploring-mit-open-source-license-comprehensive-guide)
-40
View File
@@ -1,40 +0,0 @@
# Project Structure
This document provides an in-depth look at the project's file and directory structure.
- **`main.js`**: This is the heart of the Electron application. It's the main process script that controls the application's lifecycle, creates browser windows, and handles all interactions with the operating system.
- Manages native context menu, downloads, and OAuth/WebAuthn-friendly window behavior.
- **`renderer/`**: This directory contains all the client-side code and assets for the browser's user interface (the renderer process).
- **`index.html`**: The main HTML file that serves as the container for the browser's UI, including the tab bar, address bar, and the webview for displaying web content.
- **`style.css`**: The primary stylesheet for the entire browser interface.
- **`script.js`**: The main JavaScript file for the renderer process. It handles all the user interactions within the browser window, such as creating new tabs, handling navigation, and communicating with the main process.
- **`home.html`**, **`home.css`**, **`home.js`**: These files define the content and behavior of the default home page (new tab page).
- **`settings.html`**, **`settings.css`**, **`settings.js`**: These files create the settings page, allowing users to configure the browser and manage their data.
- **`404.html`**, **`404.css`**: Files for the "page not found" error page.
- **`gpu-diagnostics.html`**: The page for displaying GPU information.
- **`performance.css`**: Styles for the performance monitoring page.
- **`icons.js`**, **`icons.json`**: Files related to managing icons within the UI.
- Other pages: downloads UI and settings integrate with main-process IPC.
- **`preload.js`**: This script is a crucial part of Electron's security model. It runs in a privileged context before the renderer process's web page is loaded. It's used to selectively expose APIs from the main process to the renderer process via the `contextBridge`.
- **`performance-monitor.js`**: A Node.js module that runs in the main process to track application performance metrics like memory usage and page load times.
- **`gpu-config.js`** & **`gpu-fallback.js`**: These modules are responsible for managing GPU-related settings. `gpu-config.js` checks the system's GPU capabilities, and `gpu-fallback.js` provides mechanisms to disable or reduce GPU acceleration if problems are detected.
- `start-gpu-safe.bat` starts the app with safer GPU settings on Windows.
- **`assets/`**: This directory holds all static assets.
- **`images/`**: Contains logos, icons, and other images used in the application.
- **`fonts/`**: Contains font files.
- **`documentation/`**: Contains all supplementary documentation for the project.
- **`*.json`**: Configuration and data files.
- **`package.json`**: Defines the project's metadata, dependencies, and scripts.
- **`bookmarks.json`**: Stores the user's bookmarks.
- **`site-history.json`**: Stores the user's browsing history.
- **`search-history.json`**: Stores the user's search history.
- **`bookmarks.backup.json`**: Auto-created backup of bookmarks on save.
- **`start-gpu-safe.bat`**: A batch script for Windows users to start the application in a GPU-safe mode.
-53
View File
@@ -1,53 +0,0 @@
# Google OAuth Sign-in Debug Guide
## Changes Made to Fix Google Sign-in Issues
### 1. User Agent Strategy
- Nebula removes the default Electron token from the UA and appends `Nebula/<version>` for better compatibility while still identifying the app.
- The UA is applied at the session level (main/default sessions) so all tabs/webviews inherit it.
- To debug with Electron visible in UA, set environment variable `NEBULA_DEBUG_ELECTRON_UA=1` before launch.
### 2. Webview and Window Behavior
- Webviews inherit secure defaults from `webPreferences`.
- Popup windows opened by sites (e.g., OAuth) are allowed for `http`/`https` URLs to preserve login flows.
### 3. Session Configuration for OAuth
- Configured session permissions for OAuth compatibility.
- Added cookie change monitoring for Google domains.
- Enhanced request headers (Accept-Language, Accept) and `Referrer-Policy` for OAuth endpoints.
### 4. Unified Session Partitioning
- The main window uses partition `persist:main`, and sessions are configured consistently so auth/session state is shared across tabs.
## Testing Google Sign-in
1. **Open the browser** (already running)
2. **Navigate to** any Google service (Gmail, YouTube, Drive, etc.)
3. **Click Sign In** - you should now see the Google account picker
4. **Select your account** - should take you to password/2FA screen
5. **Complete sign-in** - should successfully sign you in
Note: POST-based navigations are not blocked or intercepted by the main process to avoid stripping request bodies.
## Debug Information
If issues persist, check the Console (F12) for:
- Cookie changes for Google domains
- OAuth redirect flows
- JavaScript errors
## Common OAuth Issues Fixed
- ✅ Missing User Agent (Google blocks unidentified browsers)
- ✅ Third-party cookie restrictions
- ✅ Session isolation between tabs
- ✅ Missing referrer policies
- ✅ Popup blocking for OAuth flows
## What Should Work Now
- Google account picker should appear
- Password entry screens should load
- Two-factor authentication should work
- OAuth redirects should complete properly
- Session should persist across tabs
-453
View File
@@ -1,453 +0,0 @@
/**
* Nebula Browser - Global Gamepad Input Handler (Standalone Reference)
*
* NOTE: This is a standalone reference implementation. The actual gamepad handler
* used by Nebula is integrated directly into preload.js for proper context isolation
* compatibility. This file is kept for reference and potential future use.
*
* This module actively polls and consumes gamepad input from the Gamepad API.
* This is CRITICAL for Steam Deck/SteamOS Game Mode:
*
* Steam only stops applying Desktop mouse emulation when:
* - The application actively reads controller/gamepad input, OR
* - Steam Input is enabled (which requires explicit configuration)
*
* If the app does not read controller input at all, Steam assumes the user
* needs mouse emulation. By continuously polling navigator.getGamepads(),
* Steam recognizes that the app is consuming gamepad events and backs off
* the Desktop mouse emulation layer.
*
* This module should be loaded as early as possible in the renderer process.
*/
(function() {
'use strict';
// Prevent double initialization
if (window.__nebulaGamepadHandler) {
return;
}
const CONFIG = {
// Polling rate in ms (60fps = ~16ms, we use requestAnimationFrame)
POLL_INTERVAL: 16,
// Deadzone for analog sticks
STICK_DEADZONE: 0.15,
TRIGGER_DEADZONE: 0.1,
// Enable debug logging
DEBUG: false,
};
// Global state
const state = {
initialized: false,
gamepads: {},
connectedCount: 0,
activeGamepadIndex: null,
lastPollTime: 0,
rafId: null,
// Button states for edge detection
buttonStates: {},
// Callbacks for interested listeners
listeners: {
connect: [],
disconnect: [],
button: [],
axis: [],
input: [], // Any input (for keeping the polling "active")
},
};
// Debug logger
const log = (...args) => {
if (CONFIG.DEBUG) {
console.log('[NebulaGamepad]', ...args);
}
};
/**
* Initialize the gamepad handler.
* This should be called as early as possible.
*/
function init() {
if (state.initialized) {
log('Already initialized');
return;
}
if (typeof navigator === 'undefined' || !navigator.getGamepads) {
console.warn('[NebulaGamepad] Gamepad API not available');
return;
}
log('Initializing gamepad handler');
// Listen for connect/disconnect events
window.addEventListener('gamepadconnected', handleGamepadConnected);
window.addEventListener('gamepaddisconnected', handleGamepadDisconnected);
// Do an initial scan for already-connected gamepads
// (important for Steam Deck where the controller is always connected)
scanGamepads();
// Start the polling loop immediately
// This is the KEY part: continuously polling getGamepads() signals to Steam
// that we're actively consuming gamepad input
startPolling();
state.initialized = true;
log('Gamepad handler initialized');
// Expose debug info
if (CONFIG.DEBUG) {
window.__nebulaGamepadDebug = {
state,
getActiveGamepad,
getConnectedGamepads,
};
}
}
/**
* Handle gamepad connection event
*/
function handleGamepadConnected(event) {
const gamepad = event.gamepad;
log('Gamepad connected:', gamepad.index, gamepad.id);
state.gamepads[gamepad.index] = {
id: gamepad.id,
index: gamepad.index,
connected: true,
mapping: gamepad.mapping,
timestamp: Date.now(),
};
state.connectedCount++;
// Set as active if we don't have one
if (state.activeGamepadIndex === null) {
state.activeGamepadIndex = gamepad.index;
log('Set active gamepad:', gamepad.index);
}
// Initialize button states for this gamepad
state.buttonStates[gamepad.index] = {};
// Notify listeners
emitEvent('connect', { gamepad, index: gamepad.index, id: gamepad.id });
}
/**
* Handle gamepad disconnection event
*/
function handleGamepadDisconnected(event) {
const gamepad = event.gamepad;
log('Gamepad disconnected:', gamepad.index, gamepad.id);
if (state.gamepads[gamepad.index]) {
state.gamepads[gamepad.index].connected = false;
delete state.gamepads[gamepad.index];
state.connectedCount--;
}
// Clear button states
delete state.buttonStates[gamepad.index];
// If this was the active gamepad, find another
if (state.activeGamepadIndex === gamepad.index) {
state.activeGamepadIndex = null;
// Try to find another connected gamepad
const gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length; i++) {
if (gamepads[i]) {
state.activeGamepadIndex = i;
log('Switched active gamepad to:', i);
break;
}
}
}
// Notify listeners
emitEvent('disconnect', { index: gamepad.index, id: gamepad.id });
}
/**
* Scan for already-connected gamepads
* This is important because on Linux/Steam Deck, the gamepadconnected event
* may not fire until the first button press
*/
function scanGamepads() {
const gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
if (gamepad && !state.gamepads[gamepad.index]) {
log('Found pre-connected gamepad:', gamepad.index, gamepad.id);
state.gamepads[gamepad.index] = {
id: gamepad.id,
index: gamepad.index,
connected: true,
mapping: gamepad.mapping,
timestamp: Date.now(),
};
state.connectedCount++;
if (state.activeGamepadIndex === null) {
state.activeGamepadIndex = gamepad.index;
}
state.buttonStates[gamepad.index] = {};
}
}
}
/**
* Start the gamepad polling loop
* Uses requestAnimationFrame for efficient, consistent polling
*/
function startPolling() {
if (state.rafId !== null) {
return; // Already polling
}
function pollLoop(timestamp) {
state.lastPollTime = timestamp;
// CRITICAL: This call to getGamepads() is what tells Steam we're
// actively consuming gamepad input
const gamepads = navigator.getGamepads();
// Process input from all connected gamepads
for (let i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
if (gamepad) {
processGamepadInput(gamepad);
}
}
// Also do periodic scans for newly connected gamepads
// (handles edge case where event doesn't fire)
if (timestamp % 1000 < 20) {
scanGamepads();
}
// Continue polling
state.rafId = requestAnimationFrame(pollLoop);
}
state.rafId = requestAnimationFrame(pollLoop);
log('Started gamepad polling');
}
/**
* Stop the polling loop (called on page unload)
*/
function stopPolling() {
if (state.rafId !== null) {
cancelAnimationFrame(state.rafId);
state.rafId = null;
log('Stopped gamepad polling');
}
}
/**
* Process input from a gamepad
*/
function processGamepadInput(gamepad) {
const index = gamepad.index;
const buttonState = state.buttonStates[index] || {};
let hasInput = false;
// Process buttons
for (let i = 0; i < gamepad.buttons.length; i++) {
const button = gamepad.buttons[i];
const wasPressed = buttonState[`b${i}`] || false;
const isPressed = button.pressed || button.value > 0.5;
if (isPressed !== wasPressed) {
buttonState[`b${i}`] = isPressed;
hasInput = true;
emitEvent('button', {
gamepad,
index,
button: i,
pressed: isPressed,
value: button.value,
});
log(`Button ${i}: ${isPressed ? 'pressed' : 'released'}`);
}
}
// Process axes (analog sticks, triggers)
for (let i = 0; i < gamepad.axes.length; i++) {
const value = gamepad.axes[i];
const prevValue = buttonState[`a${i}`] || 0;
// Only emit if there's significant change
if (Math.abs(value - prevValue) > 0.01) {
buttonState[`a${i}`] = value;
// Check if beyond deadzone
if (Math.abs(value) > CONFIG.STICK_DEADZONE) {
hasInput = true;
emitEvent('axis', {
gamepad,
index,
axis: i,
value,
});
}
}
}
state.buttonStates[index] = buttonState;
// Emit generic input event if any input detected
if (hasInput) {
emitEvent('input', { gamepad, index });
}
}
/**
* Emit an event to registered listeners
*/
function emitEvent(type, data) {
const listeners = state.listeners[type] || [];
for (const listener of listeners) {
try {
listener(data);
} catch (err) {
console.error('[NebulaGamepad] Listener error:', err);
}
}
}
/**
* Register a listener for gamepad events
* @param {string} type - Event type: 'connect', 'disconnect', 'button', 'axis', 'input'
* @param {function} callback - Callback function
* @returns {function} Unsubscribe function
*/
function on(type, callback) {
if (!state.listeners[type]) {
state.listeners[type] = [];
}
state.listeners[type].push(callback);
return () => {
const idx = state.listeners[type].indexOf(callback);
if (idx !== -1) {
state.listeners[type].splice(idx, 1);
}
};
}
/**
* Get the currently active gamepad
* @returns {Gamepad|null}
*/
function getActiveGamepad() {
if (state.activeGamepadIndex === null) {
return null;
}
const gamepads = navigator.getGamepads();
return gamepads[state.activeGamepadIndex] || null;
}
/**
* Get all connected gamepads
* @returns {Gamepad[]}
*/
function getConnectedGamepads() {
const gamepads = navigator.getGamepads();
return Array.from(gamepads).filter(gp => gp !== null);
}
/**
* Check if any gamepad is connected
* @returns {boolean}
*/
function isGamepadConnected() {
return state.connectedCount > 0;
}
/**
* Set the active gamepad by index
* @param {number} index
*/
function setActiveGamepad(index) {
const gamepads = navigator.getGamepads();
if (gamepads[index]) {
state.activeGamepadIndex = index;
log('Active gamepad set to:', index);
return true;
}
return false;
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
stopPolling();
window.removeEventListener('gamepadconnected', handleGamepadConnected);
window.removeEventListener('gamepaddisconnected', handleGamepadDisconnected);
});
// Pause polling when page is hidden to save resources
// but not for too long - we still want Steam to see we're active
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Continue polling but at a slower rate when hidden
// We don't stop entirely because Steam needs to see we're consuming input
log('Page hidden, continuing polling');
} else {
log('Page visible');
}
});
// Export the API
const gamepadHandler = {
init,
on,
getActiveGamepad,
getConnectedGamepads,
isGamepadConnected,
setActiveGamepad,
// Expose state for debugging
get state() {
return { ...state, buttonStates: { ...state.buttonStates } };
},
// Config
get config() {
return { ...CONFIG };
},
setDebug(enabled) {
CONFIG.DEBUG = !!enabled;
},
};
// Mark as initialized and expose globally
window.__nebulaGamepadHandler = gamepadHandler;
// Auto-initialize when DOM is ready (or immediately if already loaded)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// DOM already loaded, initialize immediately
init();
}
log('Gamepad handler module loaded');
})();
-152
View File
@@ -1,152 +0,0 @@
// gpu-config.js - Comprehensive GPU configuration manager
const { app } = require('electron');
function envTruthy(value) {
if (value === undefined || value === null) return false;
const normalized = String(value).trim().toLowerCase();
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
}
class GPUConfig {
constructor() {
this.isGPUSupported = false;
this.fallbackApplied = false;
}
// Apply GPU configuration based on system capabilities
configure() {
console.log('Configuring GPU settings...');
// Try to detect if we're on a system that supports GPU acceleration
const platform = process.platform;
const arch = process.arch;
console.log(`Platform: ${platform}, Architecture: ${arch}`);
// Start with conservative settings that usually work
this.applyConservativeSettings();
if (platform === 'linux') {
const env = process.env;
const profile = String(env.NEBULA_GPU_PROFILE || '').toLowerCase();
const forcedSoftware = envTruthy(env.NEBULA_GPU_FORCE_SOFTWARE) || profile === 'software';
const optInRequested = envTruthy(env.NEBULA_GPU_TWEAKS) || envTruthy(env.NEBULA_GPU_ALLOW_LINUX) || envTruthy(env.NEBULA_GPU_FORCE_GPU) || (profile && profile !== 'software') || Boolean(env.NEBULA_GPU_GL) || Boolean(env.NEBULA_GPU_EXTRA_ARGS);
if (forcedSoftware || !optInRequested) {
console.log('Linux detected: Disabling GPU (no opt-in overrides present) and enforcing no-sandbox');
app.commandLine.appendSwitch('disable-gpu');
app.commandLine.appendSwitch('no-sandbox');
this.fallbackApplied = true;
return;
}
console.log('Linux GPU opt-in detected: leaving GPU acceleration enabled for this session');
}
// Try to enable GPU features progressively
this.tryEnableGPU();
}
applyConservativeSettings() {
// Essential switches that usually don't cause issues
app.commandLine.appendSwitch('no-sandbox');
app.commandLine.appendSwitch('disable-dev-shm-usage');
app.commandLine.appendSwitch('disable-gpu-sandbox');
// Performance improvements that don't rely on GPU
app.commandLine.appendSwitch('disable-background-timer-throttling');
app.commandLine.appendSwitch('disable-renderer-backgrounding');
app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');
app.commandLine.appendSwitch('enable-quic');
app.commandLine.appendSwitch('max_old_space_size', '4096');
}
tryEnableGPU() {
try {
// GPU acceleration switches
app.commandLine.appendSwitch('ignore-gpu-blacklist');
app.commandLine.appendSwitch('ignore-gpu-blocklist');
// On Linux/SteamOS, these aggressive flags can cause webview rendering issues (black screen)
// We disable them for Linux to ensure stability
if (process.platform !== 'linux') {
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-zero-copy');
}
// Video acceleration (usually safer than full GPU)
app.commandLine.appendSwitch('enable-accelerated-video-decode');
app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode');
// Conservative feature enabling
app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecoder');
console.log('GPU acceleration switches applied');
} catch (err) {
console.error('Error applying GPU switches:', err);
this.applyFallback();
}
}
applyFallback() {
console.log('Applying GPU fallback configuration...');
// Force software rendering if GPU fails
app.commandLine.appendSwitch('disable-gpu');
app.commandLine.appendSwitch('disable-gpu-compositing');
app.commandLine.appendSwitch('disable-software-rasterizer');
this.fallbackApplied = true;
this.isGPUSupported = false;
}
// Check if GPU is working after app starts
async checkGPUStatus() {
try {
const gpuInfo = app.getGPUFeatureStatus();
// Check if any critical GPU features are enabled
const enabledFeatures = Object.entries(gpuInfo)
.filter(([key, value]) => !value.includes('disabled'))
.map(([key]) => key);
this.isGPUSupported = enabledFeatures.length > 2; // At least some features working
console.log('GPU Status Check:');
console.log('- Enabled features:', enabledFeatures);
console.log('- GPU supported:', this.isGPUSupported);
return {
isSupported: this.isGPUSupported,
enabledFeatures,
fullStatus: gpuInfo
};
} catch (err) {
console.error('GPU status check failed:', err);
return { isSupported: false, error: err.message };
}
}
getRecommendations() {
const recommendations = [];
if (!this.isGPUSupported) {
recommendations.push('GPU acceleration is not available on this system');
recommendations.push('The browser will use software rendering (slower but stable)');
recommendations.push('Consider updating your graphics drivers');
recommendations.push('Check if your system supports hardware acceleration');
} else {
recommendations.push('GPU acceleration is working');
recommendations.push('Browser should have good performance');
}
if (this.fallbackApplied) {
recommendations.push('Fallback mode is active due to GPU issues');
recommendations.push('Performance may be reduced but stability improved');
}
return recommendations;
}
}
module.exports = GPUConfig;
-105
View File
@@ -1,105 +0,0 @@
// gpu-fallback.js - GPU error handling and fallback system
const { app } = require('electron');
class GPUFallback {
constructor() {
this.gpuEnabled = true;
this.fallbackLevel = 0;
this.maxFallbacks = 3;
}
// Apply progressive GPU fallbacks
applyFallback(level = 0) {
console.log(`Applying GPU fallback level ${level}`);
switch (level) {
case 0:
// Level 0: Conservative GPU settings (already applied in main.js)
break;
case 1:
// Level 1: Disable hardware acceleration for some features
app.commandLine.appendSwitch('disable-accelerated-2d-canvas');
app.commandLine.appendSwitch('disable-accelerated-jpeg-decoding');
break;
case 2:
// Level 2: Software rendering only
app.commandLine.appendSwitch('disable-gpu');
app.commandLine.appendSwitch('disable-gpu-compositing');
this.gpuEnabled = false;
break;
case 3:
// Level 3: Most conservative settings
app.commandLine.appendSwitch('disable-gpu');
app.commandLine.appendSwitch('disable-gpu-compositing');
app.commandLine.appendSwitch('disable-software-rasterizer');
app.commandLine.appendSwitch('disable-2d-canvas-image-chromium');
this.gpuEnabled = false;
break;
default:
console.warn('Maximum fallback level reached');
}
this.fallbackLevel = level;
}
// Check if GPU is working properly
async checkGPUStatus() {
try {
const gpuInfo = app.getGPUFeatureStatus();
// Check for critical GPU failures
const criticalFeatures = ['gpu_compositing', 'webgl', 'webgl2'];
const failures = criticalFeatures.filter(feature =>
gpuInfo[feature] && gpuInfo[feature].includes('disabled')
);
if (failures.length > 0) {
console.warn('GPU features disabled:', failures);
return false;
}
return true;
} catch (err) {
console.error('GPU status check failed:', err);
return false;
}
}
// Handle GPU process crashes
setupCrashHandling() {
let crashCount = 0;
app.on('gpu-process-crashed', (event, killed) => {
crashCount++;
console.error(`GPU process crashed (count: ${crashCount}), killed: ${killed}`);
if (crashCount >= 3 && this.fallbackLevel < this.maxFallbacks) {
console.log('Too many GPU crashes, applying fallback...');
this.applyFallback(this.fallbackLevel + 1);
// Restart the app if needed
if (!killed) {
setTimeout(() => {
app.relaunch();
app.exit();
}, 1000);
}
}
});
}
// Get current GPU status
getStatus() {
return {
gpuEnabled: this.gpuEnabled,
fallbackLevel: this.fallbackLevel,
isHardwareAccelerated: this.fallbackLevel < 2
};
}
}
module.exports = GPUFallback;
-2972
View File
File diff suppressed because it is too large Load Diff
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env bash
# Assemble nebula-appdir from extracted squashfs-root
set -euo pipefail
SRC="${1:-squashfs-root}"
DEST="${2:-nebula-appdir}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ ! -d "$SRC" ]; then
echo "Source $SRC not found. Extract the AppImage first (./dist/Nebula-*.AppImage --appimage-extract)"
exit 1
fi
# Copy extracted contents into DEST
mkdir -p "$DEST"
cp -a "$SRC/." "$DEST/"
# Ensure launcher/binary exist
if [ -f "$DEST/run-nebula.sh" ]; then
mv "$DEST/run-nebula.sh" "$DEST/Nebula" 2>/dev/null || true
fi
chmod +x "$DEST/Nebula" || true
# Ensure directories for icons and desktop entries
mkdir -p "$DEST/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$DEST/usr/share/applications"
# Copy icon if present at top level of extracted AppImage
if [ -f "$SRC/nebula.png" ]; then
cp "$SRC/nebula.png" "$DEST/usr/share/icons/hicolor/256x256/apps/nebula.png"
fi
# Also embed project icon if present in repo assets
PROJECT_ICON="$(cd "$(dirname "$0")" && pwd)/assets/images/Logos/Nebula-Favicon.png"
if [ -f "$PROJECT_ICON" ]; then
echo "Embedding project icon into AppDir: $PROJECT_ICON"
cp "$PROJECT_ICON" "$DEST/usr/share/icons/hicolor/256x256/apps/nebula.png"
fi
# Install desktop file into AppDir
if [ -f "$DEST/nebula.desktop" ]; then
cp "$DEST/nebula.desktop" "$DEST/usr/share/applications/nebula.desktop"
else
cat > "$DEST/usr/share/applications/nebula.desktop" <<'EOF'
[Desktop Entry]
Name=Nebula
Comment=Nebula Browser
Exec=./Nebula %U
Terminal=false
Type=Application
Icon=nebula
Categories=Network;WebBrowser;
StartupWMClass=Nebula
EOF
fi
# Match appdir-example layout: extract app.asar to resources/app and keep app.asar.orig
if [ -f "$DEST/resources/app.asar" ]; then
if command -v npx &> /dev/null; then
echo "Extracting app.asar to resources/app (keeping app.asar.orig)"
(cd "$DEST/resources" && npx asar extract "app.asar" "app")
mv "$DEST/resources/app.asar" "$DEST/resources/app.asar.orig" 2>/dev/null || true
else
echo "Warning: npx not found; leaving app.asar in place."
fi
fi
# Copy Linux launch wrappers if present in appdir-example
if [ -f "$SCRIPT_DIR/appdir-example/run-nebula.sh" ]; then
cp "$SCRIPT_DIR/appdir-example/run-nebula.sh" "$DEST/run-nebula.sh"
chmod +x "$DEST/run-nebula.sh" || true
fi
if [ -f "$SCRIPT_DIR/appdir-example/steam_appid.txt" ]; then
cp "$SCRIPT_DIR/appdir-example/steam_appid.txt" "$DEST/steam_appid.txt"
fi
if [ -f "$SCRIPT_DIR/appdir-example/nebula.desktop" ]; then
cp "$SCRIPT_DIR/appdir-example/nebula.desktop" "$DEST/nebula.desktop"
cp "$SCRIPT_DIR/appdir-example/nebula.desktop" "$DEST/usr/share/applications/nebula.desktop"
fi
# Ensure root launcher exists (from example if needed)
if [ -f "$SCRIPT_DIR/appdir-example/Nebula" ]; then
cp "$SCRIPT_DIR/appdir-example/Nebula" "$DEST/Nebula"
chmod +x "$DEST/Nebula" || true
fi
# Fix permissions
chmod -R a+r "$DEST/usr/share/icons/hicolor/256x256/apps" || true
chmod +x "$DEST/Nebula" || true
echo "AppDir assembled at $DEST. Run with: $DEST/run-nebula.sh"
-3449
View File
File diff suppressed because it is too large Load Diff
-64
View File
@@ -1,64 +0,0 @@
{
"name": "nebula",
"productName": "Nebula",
"version": "1.3.2",
"main": "main.js",
"scripts": {
"start": "electron .",
"start:dev": "electron . --no-sandbox --disable-gpu",
"start:linux": "electron . --no-sandbox",
"dist": "electron-builder",
"run": "electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"dompurify": "^3.1.6",
"electron-updater": "^6.6.2",
"highlight.js": "^11.9.0",
"marked": "^12.0.2",
"steamworks.js": "^0.3.2"
},
"devDependencies": {
"electron": "^40.0.0",
"electron-builder": "^23.0.0",
"electron-nightly": "^39.0.0-nightly.20250811"
},
"build": {
"appId": "com.andrewzambazos.nebula",
"protocols": [
{
"name": "Nebula",
"schemes": [
"http",
"https"
]
}
],
"publish": [
{
"provider": "github",
"owner": "Bobbybear007",
"repo": "NebulaBrowser"
}
],
"files": [
"**/*",
"!user-data/**",
"!*.backup.json"
],
"extraResources": [],
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/images/Logos/Nebula-Favicon.icns"
},
"win": {
"icon": "assets/images/Logos/Nebula-Favicon.ico"
},
"linux": {
"icon": "assets/images/Logos/Nebula-Favicon.png"
}
}
}
-100
View File
@@ -1,100 +0,0 @@
// performance-monitor.js - Monitor and optimize browser performance
const { app } = require('electron');
class PerformanceMonitor {
constructor() {
this.metrics = {
memoryUsage: [],
cpuUsage: [],
loadTimes: []
};
this.startTime = Date.now();
}
// Monitor memory usage
trackMemory() {
const usage = process.memoryUsage();
this.metrics.memoryUsage.push({
timestamp: Date.now(),
rss: usage.rss,
heapUsed: usage.heapUsed,
heapTotal: usage.heapTotal,
external: usage.external
});
// Keep only last 100 measurements
if (this.metrics.memoryUsage.length > 100) {
this.metrics.memoryUsage.shift();
}
}
// Monitor CPU usage
trackCPU() {
const usage = process.cpuUsage();
this.metrics.cpuUsage.push({
timestamp: Date.now(),
user: usage.user,
system: usage.system
});
if (this.metrics.cpuUsage.length > 100) {
this.metrics.cpuUsage.shift();
}
}
// Track page load times
trackLoadTime(url, loadTime) {
this.metrics.loadTimes.push({
timestamp: Date.now(),
url,
loadTime
});
if (this.metrics.loadTimes.length > 50) {
this.metrics.loadTimes.shift();
}
}
// Get performance report
getReport() {
const memAvg = this.metrics.memoryUsage.length > 0
? this.metrics.memoryUsage.reduce((sum, m) => sum + m.heapUsed, 0) / this.metrics.memoryUsage.length
: 0;
const avgLoadTime = this.metrics.loadTimes.length > 0
? this.metrics.loadTimes.reduce((sum, l) => sum + l.loadTime, 0) / this.metrics.loadTimes.length
: 0;
return {
uptime: Date.now() - this.startTime,
averageMemoryUsage: Math.round(memAvg / 1024 / 1024), // MB
averageLoadTime: Math.round(avgLoadTime),
totalPageLoads: this.metrics.loadTimes.length
};
}
// Start monitoring
start() {
// Monitor every 30 seconds
setInterval(() => {
this.trackMemory();
this.trackCPU();
}, 30000);
// Log performance report every 5 minutes
setInterval(() => {
const report = this.getReport();
console.log('Performance Report:', report);
}, 5 * 60 * 1000);
}
// Force garbage collection if available
forceGC() {
if (global.gc) {
global.gc();
console.log('Forced garbage collection');
}
}
}
module.exports = PerformanceMonitor;
-285
View File
@@ -1,285 +0,0 @@
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const { app, session, Menu, ipcMain, BrowserWindow, dialog, shell } = require('electron');
class PluginManager {
constructor() {
this.plugins = []; // { id, dir, manifest, mod, enabled }
this.rendererPreloads = []; // absolute file paths
this.rendererPages = []; // { id, file, pluginId }
this._listeners = {
'app-ready': [],
'window-created': [],
'web-contents-created': [],
'session-configured': [],
};
this._webRequestHandlers = []; // { filter, listener }
this._contextMenuContribs = []; // [function(template, params, sender)]
}
getPluginDirs() {
const appDir = path.join(app.getAppPath(), 'plugins');
const userDir = path.join(app.getPath('userData'), 'plugins');
return [appDir, userDir];
}
ensureUserPluginsDir() {
try {
const userDir = path.join(app.getPath('userData'), 'plugins');
fs.mkdirSync(userDir, { recursive: true });
return userDir;
} catch (_) { return null; }
}
loadAll() {
this.plugins = [];
this.rendererPreloads = [];
this.rendererPages = [];
const dirs = this.getPluginDirs();
for (const root of dirs) {
let entries = [];
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
for (const ent of entries) {
if (!ent.isDirectory()) continue;
const dir = path.join(root, ent.name);
const manifestPath = path.join(dir, 'plugin.json');
let manifest;
try {
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
// Normalize optional fields
const cats = manifest.categories;
if (typeof cats === 'string') manifest.categories = [cats];
else if (Array.isArray(cats)) manifest.categories = cats.filter(x => typeof x === 'string');
else if (cats == null) manifest.categories = [];
const au = manifest.authors;
if (typeof au === 'string') manifest.authors = [au];
else if (Array.isArray(au)) manifest.authors = au.filter(x => (typeof x === 'string') || (x && typeof x === 'object' && typeof x.name === 'string'));
else if (au == null) manifest.authors = [];
} catch { continue; }
const enabled = manifest.enabled !== false; // default true
const id = manifest.id || ent.name;
const record = { id, dir, manifest, enabled, mod: null, mainPath: null };
if (enabled) {
// Load main module if provided
if (manifest.main) {
const mainPath = path.join(dir, manifest.main);
try {
// eslint-disable-next-line import/no-dynamic-require, global-require
record.mod = require(mainPath);
record.mainPath = mainPath;
} catch (e) {
console.error(`[Plugins] Failed to load main for ${id}:`, e);
}
}
// Collect renderer preload if provided
if (manifest.rendererPreload) {
const rp = path.join(dir, manifest.rendererPreload);
try {
if (fs.existsSync(rp)) this.rendererPreloads.push(rp);
} catch {}
}
}
this.plugins.push(record);
}
}
// Activate plugins with activate(ctx)
for (const p of this.plugins) {
if (!p.enabled || !p.mod) continue;
try {
const ctx = this._buildContext(p);
if (typeof p.mod.activate === 'function') {
p.mod.activate(ctx);
} else if (typeof p.mod === 'function') {
// support default export as function(ctx)
p.mod(ctx);
}
} catch (e) {
console.error(`[Plugins] Error activating ${p.id}:`, e);
}
}
}
_buildContext(plugin) {
const manager = this;
const logPrefix = `[Plugin:${plugin.id}]`;
return {
app,
BrowserWindow,
ipcMain,
session,
Menu,
dialog,
shell,
paths: {
appPath: app.getAppPath(),
userData: app.getPath('userData'),
pluginDir: plugin.dir,
},
log: (...args) => console.log(logPrefix, ...args),
warn: (...args) => console.warn(logPrefix, ...args),
error: (...args) => console.error(logPrefix, ...args),
on: (evt, cb) => manager.on(evt, cb),
registerIPC: (channel, handler) => {
try { ipcMain.handle(channel, handler); } catch (e) { console.error(logPrefix, 'registerIPC failed', e); }
},
registerWebRequest: (filter, listener) => {
try { manager._webRequestHandlers.push({ filter, listener }); } catch (e) { console.error(logPrefix, 'registerWebRequest failed', e); }
},
contributeContextMenu: (contribFn) => {
try { manager._contextMenuContribs.push(contribFn); } catch (e) { console.error(logPrefix, 'contributeContextMenu failed', e); }
},
// Register a dedicated internal page (shown via nebula://<id>)
registerRendererPage: ({ id, html }) => {
try {
if (!id || !html) return;
let fileUrl = null;
try { fileUrl = pathToFileURL(html).href; } catch {}
manager.rendererPages.push({ id, file: html, fileUrl, pluginId: plugin.id });
console.log('[Plugins] Registered page:', id, '->', html, 'fileUrl:', fileUrl);
manager.log('registered page:', id, '->', html);
} catch (e) { manager.error('registerRendererPage failed', e); }
}
};
}
getRendererPreloads() {
return Array.from(new Set(this.rendererPreloads));
}
getRendererPages() {
// Return a shallow copy so callers can't mutate internal array
return this.rendererPages.map(p => ({ ...p }));
}
on(evt, cb) {
if (!this._listeners[evt]) this._listeners[evt] = [];
this._listeners[evt].push(cb);
}
emit(evt, ...args) {
const list = this._listeners[evt] || [];
for (const cb of list) {
try { cb(...args); } catch (e) { console.error('[Plugins] listener error for', evt, e); }
}
}
applyWebRequestHandlers(ses) {
try {
if (!ses || !ses.webRequest) return;
for (const { filter, listener } of this._webRequestHandlers) {
try {
ses.webRequest.onBeforeRequest(filter || {}, (details, callback) => {
try {
const res = listener(details);
if (res && typeof res === 'object') callback(res); else callback({ cancel: false });
} catch (e) {
console.error('[Plugins] webRequest handler error:', e);
callback({ cancel: false });
}
});
} catch (e) {
console.error('[Plugins] Failed to attach webRequest handler:', e);
}
}
} catch (e) {
console.error('[Plugins] applyWebRequestHandlers error:', e);
}
}
applyContextMenuContrib(template, params, sender) {
try {
for (const fn of this._contextMenuContribs) {
try { fn(template, params, sender); } catch (e) { console.error('[Plugins] context menu contrib error:', e); }
}
} catch (e) { console.error('[Plugins] applyContextMenuContrib error:', e); }
}
getPluginsInfo() {
return this.plugins.map(p => ({
id: p.id,
name: p.manifest.name || p.id,
version: p.manifest.version || '0.0.0',
description: p.manifest.description || '',
categories: Array.isArray(p.manifest.categories) ? p.manifest.categories : [],
authors: Array.isArray(p.manifest.authors)
? p.manifest.authors.map(x => (typeof x === 'string' ? x : (x && x.name) || '')).filter(Boolean)
: [],
enabled: !!p.enabled,
hasMain: !!p.manifest.main,
hasRendererPreload: !!p.manifest.rendererPreload,
dir: p.dir
}));
}
// Fast discovery that does not activate plugins; always shows disabled items
discoverPlugins() {
const out = [];
for (const root of this.getPluginDirs()) {
let entries = [];
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
for (const ent of entries) {
if (!ent.isDirectory()) continue;
const dir = path.join(root, ent.name);
const manifestPath = path.join(dir, 'plugin.json');
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const cats = manifest.categories;
const categories = typeof cats === 'string' ? [cats] : Array.isArray(cats) ? cats.filter(x => typeof x === 'string') : [];
const au = manifest.authors;
const authors = typeof au === 'string'
? [au]
: Array.isArray(au)
? au.map(x => (typeof x === 'string' ? x : (x && x.name) || null)).filter(Boolean)
: [];
out.push({
id: manifest.id || ent.name,
name: manifest.name || ent.name,
version: manifest.version || '0.0.0',
description: manifest.description || '',
categories,
authors,
enabled: manifest.enabled !== false,
hasMain: !!manifest.main,
hasRendererPreload: !!manifest.rendererPreload,
dir
});
} catch {}
}
}
return out;
}
async setEnabled(id, enabled) {
const p = this.plugins.find(x => x.id === id) || null;
if (!p) throw new Error('Plugin not found: ' + id);
const manifestPath = path.join(p.dir, 'plugin.json');
let manifest;
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch (e) { throw new Error('Manifest read failed: ' + e.message); }
manifest.enabled = !!enabled;
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
return true;
}
_clearRequireCache(p) {
try {
if (p && p.mainPath) {
const k = require.resolve(p.mainPath);
if (require.cache[k]) delete require.cache[k];
}
} catch {}
}
reload(id) {
if (id) {
const p = this.plugins.find(x => x.id === id);
if (p) this._clearRequireCache(p);
} else {
for (const p of this.plugins) this._clearRequireCache(p);
}
this.loadAll();
}
}
module.exports = PluginManager;
-377
View File
@@ -1,377 +0,0 @@
/**
* Portable Data Manager for Nebula Browser
*
* Handles portable user data storage for all platforms (Windows, macOS, Linux).
* Data is stored in a 'user-data' folder within the application directory,
* keeping all user data local to the compiled project.
*
* Security considerations:
* - Data is stored with restricted permissions (0700 for directories, 0600 for files on Unix)
* - Path validation prevents directory traversal attacks
* - Portable mode is enabled by default on all platforms
* - Can be disabled via NEBULA_PORTABLE=0 environment variable
*/
const { app } = require('electron');
const fs = require('fs');
const path = require('path');
const os = require('os');
class PortableDataManager {
constructor() {
this._isPortable = null;
this._portableDataPath = null;
this._initialized = false;
}
/**
* Get the application's root directory
* Works for both development and packaged builds
*/
_getAppRootDir() {
// In packaged app, process.resourcesPath points to resources folder
// We want the parent directory (the app folder itself)
if (app.isPackaged) {
// For packaged apps:
// - Windows: path\to\app\resources -> path\to\app
// - macOS: path/to/App.app/Contents/Resources -> path/to/App.app/Contents
// - Linux: path/to/app/resources -> path/to/app
const resourcesPath = process.resourcesPath;
if (process.platform === 'darwin') {
// On macOS, go up two levels from Resources to get to App.app parent folder
// But we want to store data inside the .app bundle's Contents folder for portability
return path.dirname(resourcesPath); // Contents folder
} else {
// Windows and Linux: go up one level from resources
return path.dirname(resourcesPath);
}
} else {
// Development mode: use __dirname (the directory containing main.js)
return __dirname;
}
}
/**
* Check if we're running in portable mode
* Portable mode is enabled by default on all platforms.
*
* Can be disabled by setting NEBULA_PORTABLE=0 or NEBULA_PORTABLE=false
* Can specify custom path via NEBULA_PORTABLE_PATH environment variable
*/
isPortableMode() {
if (this._isPortable !== null) {
return this._isPortable;
}
// Check if NEBULA_PORTABLE is explicitly set to disable
const portableEnv = process.env.NEBULA_PORTABLE;
if (portableEnv !== undefined) {
const isDisabled = portableEnv === '0' ||
portableEnv.toLowerCase() === 'false' ||
portableEnv.toLowerCase() === 'no';
if (isDisabled) {
this._isPortable = false;
console.log('[Portable] Portable mode disabled via NEBULA_PORTABLE environment variable');
return false;
}
}
// Portable mode is enabled by default on all platforms
this._isPortable = true;
return this._isPortable;
}
/**
* Get the portable data directory path
* Uses NEBULA_PORTABLE_PATH if set, otherwise creates 'user-data' in Documents/My Games/<AppName>
* with a safe fallback to the app directory.
*/
getPortableDataPath() {
if (this._portableDataPath !== null) {
return this._portableDataPath;
}
if (!this.isPortableMode()) {
this._portableDataPath = '';
return '';
}
// First, check if custom path is provided via environment variable
const customPath = process.env.NEBULA_PORTABLE_PATH;
if (customPath) {
const resolvedPath = path.resolve(customPath);
if (this._isPathSafe(resolvedPath)) {
this._portableDataPath = resolvedPath;
console.log(`[Portable] Using custom portable path: ${resolvedPath}`);
return this._portableDataPath;
} else {
console.warn('[Portable] Custom path is unsafe, using default location');
}
}
// Default: prefer Documents/My Games/<AppName>/user-data
let dataPath = '';
try {
const docsDir = app.getPath('documents');
const appName = app.getName() || 'NebulaBrowser';
dataPath = path.join(docsDir, 'My Games', appName, 'user-data');
} catch (err) {
console.warn('[Portable] Failed to resolve Documents path, using app directory');
}
if (!dataPath) {
const appRoot = this._getAppRootDir();
dataPath = path.join(appRoot, 'user-data');
}
// Validate the path
if (this._isPathSafe(dataPath)) {
this._portableDataPath = dataPath;
console.log(`[Portable] Using portable data path: ${dataPath}`);
} else {
console.error('[Portable] Default path is unsafe, falling back to system default');
this._portableDataPath = '';
}
return this._portableDataPath;
}
/**
* Initialize portable data directory with secure permissions
* Must be called before app.ready event
*/
initialize() {
if (this._initialized) {
return true;
}
if (!this.isPortableMode()) {
console.log('[Portable] Not in portable mode, using default paths');
this._initialized = true;
return true;
}
const dataPath = this.getPortableDataPath();
if (!dataPath) {
console.warn('[Portable] No valid portable path, using default paths');
this._initialized = true;
return false;
}
try {
// Create the data directory with secure permissions (owner only: rwx)
this._ensureSecureDirectory(dataPath);
// Create subdirectories for organized storage
// Note: Don't create 'Cache', 'Cookies', 'Network' - Electron manages these internally
const subdirs = ['Local Storage', 'Session Storage', 'IndexedDB'];
for (const subdir of subdirs) {
this._ensureSecureDirectory(path.join(dataPath, subdir));
}
// Set Electron's user data path to our portable location
// This must be done BEFORE app.ready event
app.setPath('userData', dataPath);
app.setPath('sessionData', dataPath);
// Also redirect cache to be portable
const cachePath = path.join(dataPath, 'Cache');
app.setPath('cache', cachePath);
console.log(`[Portable] User data path set to: ${dataPath}`);
console.log(`[Portable] Cache path set to: ${cachePath}`);
this._initialized = true;
return true;
} catch (err) {
console.error('[Portable] Failed to initialize portable data:', err);
this._initialized = true;
return false;
}
}
/**
* Get the path for a data file (bookmarks, history, etc.)
* Returns portable path if in portable mode, otherwise returns __dirname path
*/
getDataFilePath(filename) {
// Validate filename to prevent directory traversal
if (!this._isFilenameSafe(filename)) {
throw new Error(`Invalid filename: ${filename}`);
}
if (this.isPortableMode()) {
const portablePath = this.getPortableDataPath();
if (portablePath) {
return path.join(portablePath, filename);
}
}
// Fallback to __dirname (project directory) for non-portable or if portable not configured
// Note: In production, you might want to use app.getPath('userData') as fallback
return null; // Return null to indicate caller should use their default path
}
/**
* Ensure a directory exists with secure permissions
* On Unix systems (macOS, Linux), applies restricted permissions (0700)
* On Windows, creates directory with default permissions (ACLs handle security)
*/
_ensureSecureDirectory(dirPath) {
if (!fs.existsSync(dirPath)) {
if (process.platform === 'win32') {
// Windows: create directory with default permissions
// Windows ACLs handle security through inheritance
fs.mkdirSync(dirPath, { recursive: true });
} else {
// Unix (macOS, Linux): create with restricted permissions (owner only: rwx------)
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
}
console.log(`[Portable] Created secure directory: ${dirPath}`);
} else {
// Verify and fix permissions on existing directory (Unix only)
if (process.platform !== 'win32') {
try {
const stats = fs.statSync(dirPath);
if (stats.isDirectory()) {
// Set secure permissions
fs.chmodSync(dirPath, 0o700);
}
} catch (err) {
console.warn(`[Portable] Could not verify permissions for ${dirPath}:`, err.message);
}
}
}
}
/**
* Write a file with secure permissions
* On Unix systems, applies restricted permissions (0600)
* On Windows, writes with default permissions
*/
writeSecureFile(filePath, data) {
// Ensure parent directory exists with secure permissions
const dir = path.dirname(filePath);
this._ensureSecureDirectory(dir);
// Write file
if (process.platform === 'win32') {
fs.writeFileSync(filePath, data);
} else {
fs.writeFileSync(filePath, data, { mode: 0o600 });
}
}
/**
* Async version of secure file write
* On Unix systems, applies restricted permissions (0600)
* On Windows, writes with default permissions
*/
async writeSecureFileAsync(filePath, data) {
// Ensure parent directory exists with secure permissions
const dir = path.dirname(filePath);
this._ensureSecureDirectory(dir);
// Write file with restricted permissions (owner only: rw------- on Unix)
if (process.platform === 'win32') {
await fs.promises.writeFile(filePath, data);
} else {
await fs.promises.writeFile(filePath, data, { mode: 0o600 });
}
}
/**
* Validate path safety (prevent directory traversal)
* Works across Windows, macOS, and Linux
*/
_isPathSafe(testPath) {
// Resolve to absolute path
const resolved = path.resolve(testPath);
// Check for directory traversal patterns
if (resolved.includes('..')) {
return false;
}
// Platform-specific system path checks
if (process.platform === 'win32') {
// Windows: block system directories
const dangerousWin = [
'C:\\Windows',
'C:\\Program Files',
'C:\\Program Files (x86)',
'C:\\ProgramData'
];
const resolvedLower = resolved.toLowerCase();
for (const pattern of dangerousWin) {
if (resolvedLower.startsWith(pattern.toLowerCase())) {
return false;
}
}
} else if (process.platform === 'darwin') {
// macOS: block system directories
const dangerousMac = ['/System', '/Library', '/usr', '/bin', '/sbin', '/etc', '/var'];
for (const pattern of dangerousMac) {
if (resolved.startsWith(pattern) && !resolved.includes('.app')) {
return false;
}
}
} else {
// Linux: block system directories
const dangerousLinux = ['~root', '/etc', '/var/run', '/proc', '/sys', '/dev'];
for (const pattern of dangerousLinux) {
if (resolved.includes(pattern)) {
return false;
}
}
const systemPaths = ['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/boot', '/lib', '/lib64'];
for (const sysPath of systemPaths) {
if (resolved.startsWith(sysPath)) {
return false;
}
}
}
return true;
}
/**
* Validate filename safety
*/
_isFilenameSafe(filename) {
// Check for directory traversal
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
return false;
}
// Check for hidden files that might be system files
if (filename.startsWith('.') && !filename.endsWith('.json')) {
return false;
}
return true;
}
/**
* Get status information for debugging
*/
getStatus() {
return {
isPortable: this.isPortableMode(),
portablePath: this.getPortableDataPath(),
appRootDir: this._getAppRootDir(),
initialized: this._initialized,
platform: process.platform,
isPackaged: app.isPackaged,
envPortable: process.env.NEBULA_PORTABLE,
envPath: process.env.NEBULA_PORTABLE_PATH
};
}
}
// Export singleton instance
const portableDataManager = new PortableDataManager();
module.exports = portableDataManager;
-549
View File
@@ -1,549 +0,0 @@
// preload.js - Optimized version
const { contextBridge, ipcRenderer } = require('electron');
let pathModule;
let fsModule;
try {
pathModule = require('path');
fsModule = require('fs');
} catch (err) {
pathModule = null;
fsModule = null;
}
// BrowserView tab id (desktop mode) injected via additionalArguments
let nebulaTabId = null;
try {
const arg = (process?.argv || []).find(a => typeof a === 'string' && a.startsWith('--nebula-tab-id='));
if (arg) nebulaTabId = arg.split('=')[1] || null;
} catch {}
// =============================================================================
// GAMEPAD HANDLER - Steam Deck / SteamOS Support
// =============================================================================
// This is CRITICAL for Steam Deck Game Mode: Steam only stops applying
// Desktop mouse emulation when the app actively reads controller input.
// By continuously polling navigator.getGamepads(), Steam recognizes that
// the app is consuming gamepad events and backs off the mouse emulation layer.
// =============================================================================
const gamepadState = {
initialized: false,
gamepads: {},
connectedCount: 0,
activeGamepadIndex: null,
rafId: null,
buttonStates: {},
listeners: { connect: [], disconnect: [], button: [], axis: [], input: [] },
};
const GAMEPAD_CONFIG = {
STICK_DEADZONE: 0.15,
DEBUG: false,
};
function gamepadLog(...args) {
if (GAMEPAD_CONFIG.DEBUG) {
console.log('[NebulaGamepad]', ...args);
}
}
function initGamepadHandler() {
if (gamepadState.initialized) return;
if (typeof navigator === 'undefined' || !navigator.getGamepads) {
console.warn('[NebulaGamepad] Gamepad API not available');
return;
}
gamepadLog('Initializing gamepad handler');
window.addEventListener('gamepadconnected', handleGamepadConnected);
window.addEventListener('gamepaddisconnected', handleGamepadDisconnected);
// Initial scan for already-connected gamepads
scanGamepads();
// Start polling loop - this is what tells Steam we're consuming gamepad input
startGamepadPolling();
gamepadState.initialized = true;
console.log('[NebulaGamepad] Gamepad handler initialized - Steam will see controller input being consumed');
}
function handleGamepadConnected(event) {
const gamepad = event.gamepad;
gamepadLog('Gamepad connected:', gamepad.index, gamepad.id);
gamepadState.gamepads[gamepad.index] = {
id: gamepad.id,
index: gamepad.index,
connected: true,
mapping: gamepad.mapping,
timestamp: Date.now(),
};
gamepadState.connectedCount++;
if (gamepadState.activeGamepadIndex === null) {
gamepadState.activeGamepadIndex = gamepad.index;
}
gamepadState.buttonStates[gamepad.index] = {};
emitGamepadEvent('connect', { gamepad, index: gamepad.index, id: gamepad.id });
}
function handleGamepadDisconnected(event) {
const gamepad = event.gamepad;
gamepadLog('Gamepad disconnected:', gamepad.index, gamepad.id);
if (gamepadState.gamepads[gamepad.index]) {
delete gamepadState.gamepads[gamepad.index];
gamepadState.connectedCount--;
}
delete gamepadState.buttonStates[gamepad.index];
if (gamepadState.activeGamepadIndex === gamepad.index) {
gamepadState.activeGamepadIndex = null;
const gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length; i++) {
if (gamepads[i]) {
gamepadState.activeGamepadIndex = i;
break;
}
}
}
emitGamepadEvent('disconnect', { index: gamepad.index, id: gamepad.id });
}
function scanGamepads() {
const gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
if (gamepad && !gamepadState.gamepads[gamepad.index]) {
gamepadLog('Found pre-connected gamepad:', gamepad.index, gamepad.id);
gamepadState.gamepads[gamepad.index] = {
id: gamepad.id,
index: gamepad.index,
connected: true,
mapping: gamepad.mapping,
timestamp: Date.now(),
};
gamepadState.connectedCount++;
if (gamepadState.activeGamepadIndex === null) {
gamepadState.activeGamepadIndex = gamepad.index;
}
gamepadState.buttonStates[gamepad.index] = {};
}
}
}
function startGamepadPolling() {
if (gamepadState.rafId !== null) return;
function pollLoop(timestamp) {
// CRITICAL: This call to getGamepads() tells Steam we're consuming gamepad input
const gamepads = navigator.getGamepads();
for (let i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
if (gamepad) {
processGamepadInput(gamepad);
}
}
// Periodic scan for newly connected gamepads
if (timestamp % 1000 < 20) {
scanGamepads();
}
gamepadState.rafId = requestAnimationFrame(pollLoop);
}
gamepadState.rafId = requestAnimationFrame(pollLoop);
gamepadLog('Started gamepad polling');
}
function processGamepadInput(gamepad) {
const index = gamepad.index;
const buttonState = gamepadState.buttonStates[index] || {};
let hasInput = false;
// Process buttons
for (let i = 0; i < gamepad.buttons.length; i++) {
const button = gamepad.buttons[i];
const wasPressed = buttonState[`b${i}`] || false;
const isPressed = button.pressed || button.value > 0.5;
if (isPressed !== wasPressed) {
buttonState[`b${i}`] = isPressed;
hasInput = true;
emitGamepadEvent('button', { gamepad, index, button: i, pressed: isPressed, value: button.value });
}
}
// Process axes
for (let i = 0; i < gamepad.axes.length; i++) {
const value = gamepad.axes[i];
const prevValue = buttonState[`a${i}`] || 0;
if (Math.abs(value - prevValue) > 0.01) {
buttonState[`a${i}`] = value;
if (Math.abs(value) > GAMEPAD_CONFIG.STICK_DEADZONE) {
hasInput = true;
emitGamepadEvent('axis', { gamepad, index, axis: i, value });
}
}
}
gamepadState.buttonStates[index] = buttonState;
if (hasInput) {
emitGamepadEvent('input', { gamepad, index });
}
}
function emitGamepadEvent(type, data) {
// Dispatch as CustomEvent for renderer scripts to listen to
try {
window.dispatchEvent(new CustomEvent(`nebula-gamepad-${type}`, { detail: data }));
} catch (err) {
// Ignore errors if CustomEvent isn't available
}
}
function getActiveGamepad() {
if (gamepadState.activeGamepadIndex === null) return null;
const gamepads = navigator.getGamepads();
return gamepads[gamepadState.activeGamepadIndex] || null;
}
function getConnectedGamepads() {
const gamepads = navigator.getGamepads();
return Array.from(gamepads).filter(gp => gp !== null);
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (gamepadState.rafId !== null) {
cancelAnimationFrame(gamepadState.rafId);
gamepadState.rafId = null;
}
});
// =============================================================================
// EARLY GAMEPAD INITIALIZATION - Critical for Steam Deck
// =============================================================================
// Initialize gamepad polling as EARLY as possible to signal Steam Input
// that this app handles controller input natively. This MUST happen before
// Steam decides to apply mouse/keyboard emulation.
//
// We try to initialize immediately when preload runs, not waiting for DOMContentLoaded,
// because Steam's input layer makes decisions very early in the process lifecycle.
// =============================================================================
// Try immediate initialization (works in most Electron contexts)
try {
if (typeof navigator !== 'undefined' && navigator.getGamepads) {
// Start polling immediately - this is the key signal to Steam
initGamepadHandler();
console.log('[NebulaGamepad] Early initialization successful - Steam should recognize controller input');
}
} catch (e) {
// Will retry on DOMContentLoaded
console.log('[NebulaGamepad] Early init deferred, will retry on DOM ready');
}
// =============================================================================
// DOM READY & INITIALIZATION
// =============================================================================
// Cache DOM references for performance
let domReady = false;
window.addEventListener('DOMContentLoaded', () => {
domReady = true;
console.log("Browser UI loaded.");
// Re-initialize gamepad handler if early init failed
if (!gamepadState.initialized) {
initGamepadHandler();
}
});
// Optimized API exposure with error handling and caching
const electronAPI = {
send: (ch, ...args) => {
try {
return ipcRenderer.send(ch, ...args);
} catch (err) {
console.error('IPC send error:', err);
}
},
// Send message to embedding page (webview host) or to BrowserView host
sendToHost: (ch, ...args) => {
try {
// If running in BrowserView context, ALWAYS use browserview-host-message
if (nebulaTabId) {
return ipcRenderer.send('browserview-host-message', { tabId: nebulaTabId, channel: ch, args });
}
// Otherwise try ipcRenderer.sendToHost (for webview contexts)
if (typeof ipcRenderer.sendToHost === 'function') {
return ipcRenderer.sendToHost(ch, ...args);
}
// Final fallback
return ipcRenderer.send(ch, ...args);
} catch (err) {
console.error('IPC sendToHost error:', err);
}
},
invoke: (ch, ...args) => {
try {
return ipcRenderer.invoke(ch, ...args);
} catch (err) {
console.error('IPC invoke error:', err);
return Promise.reject(err);
}
},
on: (ch, fn) => {
try {
return ipcRenderer.on(ch, (e, ...args) => fn(...args));
} catch (err) {
console.error('IPC on error:', err);
}
},
// Add removeListener for cleanup
removeListener: (ch, fn) => {
try {
return ipcRenderer.removeListener(ch, fn);
} catch (err) {
console.error('IPC removeListener error:', err);
}
},
toggleDevTools: () => {
try {
return ipcRenderer.invoke('open-devtools');
} catch (err) {
console.error('IPC open-devtools error:', err);
return Promise.reject(err);
}
},
openLocalFile: async () => {
try {
return await ipcRenderer.invoke('show-open-file-dialog');
} catch (err) {
console.error('IPC openLocalFile error:', err);
return null;
}
},
showContextMenu: (params) => {
try {
return ipcRenderer.invoke('show-context-menu', params);
} catch (err) {
console.error('IPC showContextMenu error:', err);
}
},
saveImageToDisk: async (suggestedName, dataUrl) => ipcRenderer.invoke('save-image-from-dataurl', { suggestedName, dataUrl }),
saveImageFromNet: async (url) => ipcRenderer.invoke('save-image-from-url', { url })
};
// Provide absolute path to the renderer preload for webview guests so
// webview `preload` attributes use an absolute, resolvable path on all platforms.
const webviewPreloadAbsolutePath = pathModule ? pathModule.join(__dirname, 'preload.js') : null;
electronAPI.getWebviewPreloadPath = () => webviewPreloadAbsolutePath;
// Fixup any static <webview preload="..."> attributes in the DOM early so
// guests receive an absolute path instead of a relative one that may fail
// to resolve inside the guest process.
window.addEventListener('DOMContentLoaded', () => {
try {
if (webviewPreloadAbsolutePath) {
const els = document.querySelectorAll('webview[preload]');
for (const el of els) {
try { el.setAttribute('preload', webviewPreloadAbsolutePath); } catch {};
}
}
} catch (e) {
// non-fatal
}
});
// Cache for bookmarks to reduce IPC calls
let bookmarksCache = null;
let bookmarksCacheTime = 0;
const CACHE_DURATION = 5000; // 5 seconds
const bookmarksAPI = {
load: async () => {
const now = Date.now();
if (bookmarksCache && (now - bookmarksCacheTime) < CACHE_DURATION) {
return bookmarksCache;
}
try {
bookmarksCache = await ipcRenderer.invoke('load-bookmarks');
bookmarksCacheTime = now;
return bookmarksCache;
} catch (err) {
console.error('Bookmarks load error:', err);
return [];
}
},
save: async (data) => {
try {
bookmarksCache = data; // Update cache immediately
bookmarksCacheTime = Date.now();
return await ipcRenderer.invoke('save-bookmarks', data);
} catch (err) {
console.error('Bookmarks save error:', err);
return false;
}
}
};
// Expose APIs to main world
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
contextBridge.exposeInMainWorld('bookmarksAPI', bookmarksAPI);
// Gamepad API - Access to the gamepad handler running in the preload context
// The handler actively polls navigator.getGamepads() to signal to Steam that
// the app is consuming controller input (prevents mouse emulation on Steam Deck)
contextBridge.exposeInMainWorld('gamepadAPI', {
// Check if gamepad handler is initialized
isAvailable: () => gamepadState.initialized,
// Check if any gamepad is connected
isConnected: () => gamepadState.connectedCount > 0,
// Get connected gamepads info
getConnected: () => {
const gamepads = getConnectedGamepads();
return gamepads.map(gp => ({
id: gp.id,
index: gp.index,
mapping: gp.mapping,
buttons: gp.buttons.length,
axes: gp.axes.length,
}));
},
// Get the active gamepad's current state
getActive: () => {
const gp = getActiveGamepad();
if (!gp) return null;
return {
id: gp.id,
index: gp.index,
mapping: gp.mapping,
buttons: Array.from(gp.buttons).map((b, i) => ({ index: i, pressed: b.pressed, value: b.value })),
axes: Array.from(gp.axes),
};
},
// Enable debug mode
setDebug: (enabled) => {
GAMEPAD_CONFIG.DEBUG = !!enabled;
},
// Get handler state for debugging
getState: () => ({
initialized: gamepadState.initialized,
connectedCount: gamepadState.connectedCount,
activeGamepadIndex: gamepadState.activeGamepadIndex,
isPolling: gamepadState.rafId !== null,
}),
});
// Minimal about API for settings page
contextBridge.exposeInMainWorld('aboutAPI', {
getInfo: () => ipcRenderer.invoke('get-about-info')
});
// Big Picture Mode API - Steam Deck / Console UI
// Note: Big Picture Mode now opens in the main window (not a separate window) to keep resources low
// and prevent SteamOS from creating desktop mode alongside when auto-launching.
contextBridge.exposeInMainWorld('bigPictureAPI', {
// Get screen info to determine if Big Picture Mode is recommended
getScreenInfo: () => ipcRenderer.invoke('get-screen-info'),
// Check if device is likely a Steam Deck or handheld
isSuggested: () => ipcRenderer.invoke('is-bigpicture-suggested'),
// Check if currently in Big Picture Mode
isActive: () => ipcRenderer.invoke('is-in-bigpicture'),
// Launch Big Picture Mode (navigates main window to Big Picture UI)
launch: () => ipcRenderer.invoke('launch-bigpicture'),
// Exit Big Picture Mode (navigates main window back to desktop UI)
exit: () => ipcRenderer.invoke('exit-bigpicture'),
// Navigate to URL (from Big Picture Mode)
navigate: (url) => ipcRenderer.send('bigpicture-navigate', url),
// Send input event to a webview (for virtual cursor clicks)
sendInputEvent: (webContentsId, inputEvent) =>
ipcRenderer.invoke('webview-send-input-event', { webContentsId, inputEvent })
});
// Relay context-menu commands from main to active renderer context (open new tabs etc.)
ipcRenderer.on('context-menu-command', (event, payload) => {
window.dispatchEvent(new CustomEvent('nebula-context-command', { detail: payload }));
});
// Downloads API exposed to renderer
contextBridge.exposeInMainWorld('downloadsAPI', {
list: () => ipcRenderer.invoke('downloads-get-all'),
action: (id, action) => ipcRenderer.invoke('downloads-action', { id, action }),
clearCompleted: () => ipcRenderer.invoke('downloads-clear-completed'),
onStarted: (handler) => ipcRenderer.on('downloads-started', (_e, payload) => handler(payload)),
onUpdated: (handler) => ipcRenderer.on('downloads-updated', (_e, payload) => handler(payload)),
onDone: (handler) => ipcRenderer.on('downloads-done', (_e, payload) => handler(payload)),
onCleared: (handler) => ipcRenderer.on('downloads-cleared', handler),
onScanStarted: (handler) => ipcRenderer.on('downloads-scan-started', (_e, payload) => handler(payload)),
onScanResult: (handler) => ipcRenderer.on('downloads-scan-result', (_e, payload) => handler(payload))
});
// Auto-Updater API exposed to renderer
contextBridge.exposeInMainWorld('updaterAPI', {
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
onUpdateStatus: (handler) => ipcRenderer.on('update-status', (_e, payload) => handler(payload))
});
// First-Time Setup API
contextBridge.exposeInMainWorld('api', {
// Check if this is the first run
isFirstRun: () => ipcRenderer.invoke('is-first-run'),
// Get all available themes
getAllThemes: () => ipcRenderer.invoke('get-all-themes'),
// Apply a theme
applyTheme: (themeId) => ipcRenderer.invoke('apply-theme', themeId),
// Check if Nebula is the default browser
isDefaultBrowser: () => ipcRenderer.invoke('is-default-browser'),
// Set Nebula as the default browser
setAsDefaultBrowser: () => ipcRenderer.invoke('set-as-default-browser'),
// Open OS default browser settings
openDefaultBrowserSettings: () => ipcRenderer.invoke('open-default-browser-settings'),
// Complete first-run setup
completeFirstRun: (data) => ipcRenderer.invoke('complete-first-run', data),
// Get first-run data
getFirstRunData: () => ipcRenderer.invoke('get-first-run-data')
});
// ----------------------------------------
// Plugin renderer preloads
// ----------------------------------------
// We request a list of absolute file paths from main and require() them here.
// Each file can optionally call contextBridge.exposeInMainWorld to add APIs.
(async () => {
try {
const preloads = await ipcRenderer.invoke('plugins-get-renderer-preloads');
if (Array.isArray(preloads)) {
for (const p of preloads) {
try {
// eslint-disable-next-line global-require, import/no-dynamic-require
require(p);
} catch (e) {
console.error('[Plugins] Failed to load renderer preload:', p, e);
}
}
}
} catch (e) {
console.warn('[Plugins] No renderer preloads:', e);
}
})();
File diff suppressed because it is too large Load Diff
-215
View File
@@ -1,215 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>GPU Diagnostics - Nebula Browser</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.status.good { background: #d4edda; color: #155724; }
.status.warning { background: #fff3cd; color: #856404; }
.status.error { background: #f8d7da; color: #721c24; }
button {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover { background: #0056b3; }
pre {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
}
.canvas-test {
border: 1px solid #ccc;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>GPU Diagnostics</h1>
<div id="gpu-status" class="status">
<h3>GPU Status</h3>
<p>Loading GPU information...</p>
</div>
<div class="status">
<h3>WebGL Test</h3>
<canvas id="webgl-canvas" class="canvas-test" width="300" height="150"></canvas>
<p id="webgl-status">Testing WebGL...</p>
</div>
<div class="status">
<h3>Canvas 2D Acceleration Test</h3>
<canvas id="canvas2d" class="canvas-test" width="300" height="150"></canvas>
<p id="canvas2d-status">Testing Canvas 2D...</p>
</div>
<div>
<h3>Actions</h3>
<button onclick="refreshGPUInfo()">Refresh GPU Info</button>
<button onclick="forceGC()">Force Garbage Collection</button>
<button onclick="applyFallback(1)">Apply GPU Fallback Level 1</button>
<button onclick="applyFallback(2)">Apply GPU Fallback Level 2</button>
</div>
<div>
<h3>Detailed GPU Information</h3>
<pre id="gpu-details">Loading...</pre>
</div>
</div>
<script>
async function refreshGPUInfo() {
try {
const gpuInfo = await window.electronAPI.invoke('get-gpu-info');
const statusDiv = document.getElementById('gpu-status');
const detailsDiv = document.getElementById('gpu-details');
if (gpuInfo.error) {
statusDiv.className = 'status error';
statusDiv.innerHTML = `<h3>GPU Status</h3><p>Error: ${gpuInfo.error}</p>`;
} else {
const isGPUWorking = checkGPUFeatures(gpuInfo.featureStatus);
statusDiv.className = `status ${isGPUWorking ? 'good' : 'warning'}`;
statusDiv.innerHTML = `
<h3>GPU Status</h3>
<p><strong>Hardware Acceleration:</strong> ${isGPUWorking ? 'Enabled' : 'Disabled/Limited'}</p>
<p><strong>Fallback Level:</strong> ${gpuInfo.fallbackStatus?.fallbackLevel || 0}</p>
<p><strong>GPU Enabled:</strong> ${gpuInfo.fallbackStatus?.gpuEnabled ? 'Yes' : 'No'}</p>
`;
}
detailsDiv.textContent = JSON.stringify(gpuInfo, null, 2);
} catch (err) {
console.error('Failed to get GPU info:', err);
document.getElementById('gpu-status').innerHTML = `<h3>GPU Status</h3><p>Error: ${err.message}</p>`;
}
}
function checkGPUFeatures(features) {
const criticalFeatures = ['gpu_compositing', 'webgl', 'webgl2'];
return criticalFeatures.some(feature =>
features[feature] && !features[feature].includes('disabled')
);
}
async function forceGC() {
try {
await window.electronAPI.invoke('force-gc');
alert('Garbage collection completed');
} catch (err) {
alert('Failed to force GC: ' + err.message);
}
}
async function applyFallback(level) {
try {
const result = await window.electronAPI.invoke('apply-gpu-fallback', level);
if (result.success) {
alert(`Applied GPU fallback level ${level}. App restart may be required.`);
} else {
alert('Failed to apply fallback: ' + result.error);
}
} catch (err) {
alert('Failed to apply fallback: ' + err.message);
}
}
// Test WebGL
function testWebGL() {
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
const status = document.getElementById('webgl-status');
if (gl) {
// Draw a simple triangle
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, `
precision mediump float;
void main() {
gl_Color = vec4(0.0, 1.0, 0.0, 1.0);
}
`);
gl.compileShader(fragmentShader);
status.textContent = 'WebGL: Available ✓';
status.parentElement.className = 'status good';
// Clear with green color to show it's working
gl.clearColor(0.0, 0.8, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
} else {
status.textContent = 'WebGL: Not Available ✗';
status.parentElement.className = 'status error';
}
}
// Test Canvas 2D
function testCanvas2D() {
const canvas = document.getElementById('canvas2d');
const ctx = canvas.getContext('2d');
const status = document.getElementById('canvas2d-status');
try {
// Draw some graphics to test acceleration
const gradient = ctx.createLinearGradient(0, 0, 300, 0);
gradient.addColorStop(0, '#ff0000');
gradient.addColorStop(1, '#0000ff');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 300, 150);
ctx.fillStyle = 'white';
ctx.font = '20px Arial';
ctx.fillText('Canvas 2D Working!', 50, 80);
status.textContent = 'Canvas 2D: Working ✓';
status.parentElement.className = 'status good';
} catch (err) {
status.textContent = 'Canvas 2D: Error - ' + err.message;
status.parentElement.className = 'status error';
}
}
// Initialize tests
window.addEventListener('DOMContentLoaded', () => {
refreshGPUInfo();
testWebGL();
testCanvas2D();
});
</script>
</body>
</html>
-112
View File
@@ -1,112 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Nebula Browser</title>
<link rel="stylesheet" href="style.css">
<style>
/* Removed custom draggable bar CSS to allow use of native title bar */
:root { --resize-border: 8px; }
body {
padding: var(--resize-border);
margin: 0;
height: calc(100vh - 2 * var(--resize-border));
display: flex;
flex-direction: column;
box-sizing: border-box;
}
::placeholder {
color: rgba(255, 255, 255, 0.5);
/* Adjust the color and transparency as needed */
}
#view-host {
flex: 1;
width: 100%;
}
</style>
</head>
<body>
<!-- Windows title bar controls wrapper - sits above tab bar -->
<div id="titlebar-container">
<div id="tab-bar"></div>
<div id="window-controls">
<button id="min-btn" title="Minimize" aria-label="Minimize">
<svg width="10" height="10" viewBox="0 0 10 10">
<path d="M0 5h10" stroke="currentColor" stroke-width="1" fill="none"/>
</svg>
</button>
<button id="max-btn" title="Maximize" aria-label="Maximize">
<svg width="10" height="10" viewBox="0 0 10 10" class="maximize-icon">
<rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" stroke-width="1" fill="none"/>
</svg>
<svg width="10" height="10" viewBox="0 0 10 10" class="restore-icon" style="display:none">
<path d="M2.5 0.5h7v7M0.5 2.5h7v7h-7z" stroke="currentColor" stroke-width="1" fill="none"/>
</svg>
</button>
<button id="close-btn" title="Close" aria-label="Close">
<svg width="10" height="10" viewBox="0 0 10 10">
<path d="M0 0l10 10M10 0l-10 10" stroke="currentColor" stroke-width="1" fill="none"/>
</svg>
</button>
</div>
</div>
<div id="nav">
<div class="nav-left">
<button onclick="goBack()"></button>
<button onclick="goForward()"></button>
<button id="reload-btn"></button>
</div>
<div class="nav-center">
<input id="url" type="text" placeholder="Type URL here" />
<button onclick="navigate()">Go</button>
</div>
<div class="nav-right">
<div class="downloads-wrapper">
<button id="downloads-btn" title="Downloads" aria-label="Downloads">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 3v12"/>
<path d="M7 10l5 5 5-5"/>
<path d="M5 21h14"/>
</svg>
</button>
<div id="downloads-popup" class="hidden">
<div class="downloads-pop-header">
<span>Downloads</span>
<button id="downloads-show-all">Show all</button>
</div>
<div id="downloads-list" class="downloads-pop-list"></div>
<div id="downloads-empty" class="downloads-empty">No downloads</div>
</div>
</div>
<div class="menu-wrapper">
<button id="menu-btn"></button>
<div id="menu-popup" class="hidden">
<button onclick="openSettings()">Settings</button>
<!-- You can add more options here -->
<button id="bigpicture-btn" title="Launch Big Picture Mode (Controller/Steam Deck UI)">🎮 Big Picture Mode</button>
<button id="devtools-btn" title="Open / Close Developer Tools">Toggle Developer Tools</button>
<div class="zoom-controls">
<button id="zoom-out-btn">-</button>
<span id="zoom-percent">100%</span>
<button id="zoom-in-btn">+</button>
</div>
<button id="hard-reload-btn">Hard Reload (Ignore Cache)</button>
<button id="fresh-reload-btn">Reload Fresh (Add Cache-Buster)</button>
</div>
</div>
</div>
</div>
<div id="view-host"></div>
<script src="script.js"></script>
</body>
</html>
-66
View File
@@ -1,66 +0,0 @@
:root {
--bg: #0b0d10;
--primary: #7b2eff;
--accent: #00c6ff;
--text: #e0e0e0;
--url-bar-bg: #1c2030;
--url-bar-border: #3e4652;
--shadow-1: 0 12px 30px rgba(0, 0, 0, 0.35);
--blur: 12px;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: transparent;
color: var(--text);
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
#menu-popup {
background: color-mix(in srgb, var(--url-bar-bg) 92%, var(--text) 8%);
border: 1px solid color-mix(in srgb, var(--primary) 25%, color-mix(in srgb, var(--accent) 18%, transparent));
border-radius: 14px;
padding: 8px;
display: flex;
flex-direction: column;
min-width: 220px;
box-shadow: var(--shadow-1);
-webkit-backdrop-filter: blur(var(--blur));
backdrop-filter: blur(var(--blur));
}
#menu-popup button {
background: transparent;
border: none;
color: var(--text);
text-align: left;
padding: 8px 10px;
border-radius: 10px;
cursor: pointer;
transition: background 120ms ease, filter 120ms ease;
}
#menu-popup button:hover {
background: color-mix(in srgb, var(--text) 8%, transparent);
}
.zoom-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 8px;
}
.zoom-controls button {
width: 28px;
height: 28px;
text-align: center;
}
#zoom-percent {
min-width: 54px;
text-align: center;
}
-23
View File
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Menu</title>
<link rel="stylesheet" href="menu-popup.css" />
</head>
<body>
<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="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>
<span id="zoom-percent">100%</span>
<button data-cmd="zoom-in" aria-label="Zoom in">+</button>
</div>
<button data-cmd="hard-reload" role="menuitem">Hard Reload (Ignore Cache)</button>
<button data-cmd="fresh-reload" role="menuitem">Reload Fresh (Add Cache-Buster)</button>
</div>
<script src="menu-popup.js"></script>
</body>
</html>
-59
View File
@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Nebot</title>
<link rel="stylesheet" href="../plugins/nebot/page.css" onerror="this.remove()"/>
<style>
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:#12141c; color:#e6e8ef; }
.fallback { max-width:620px; margin:60px auto; padding:32px 36px; background:#1d222e; border:1px solid rgba(255,255,255,0.08); border-radius:18px; }
.fallback h1 { margin:0 0 12px; font-size:28px; background:linear-gradient(90deg,#a48bff,#6cb6ff); -webkit-background-clip:text; color:transparent; }
.fallback p { line-height:1.55; }
.tip { background:#232b38; padding:10px 14px; border-radius:10px; font-size:13px; margin-top:18px; border:1px solid rgba(255,255,255,0.08); }
.err { color:#ff6d7d; font-weight:600; }
#mount { min-height:400px; }
</style>
</head>
<body>
<div id="mount"></div>
<script>
(async function(){
// Wait a tick so plugin preload (renderer-preload) runs and exposes window.ollamaChat
const mount = document.getElementById('mount');
function showFallback(reason){
mount.innerHTML = `<div class="fallback">`+
`<h1>Nebot</h1>`+
`<p>The Nebot plugin page could not load automatically.</p>`+
(reason?`<p class='err'>${reason}</p>`:'')+
`<div class="tip"><strong>How to fix</strong><br/>1. Ensure the Nebot plugin folder exists at plugins/nebot.<br/>2. Confirm plugin is enabled (manifest enabled: true).<br/>3. Restart the app so the plugin manager registers pages.</div>`+
`</div>`;
}
try {
// Try to fetch plugin page HTML directly
const res = await fetch('../plugins/nebot/page.html');
if(!res.ok){ showFallback('Missing page.html (status '+res.status+').'); return; }
const html = await res.text();
// Simple sandboxed injection
mount.innerHTML = html;
// The injected page expects its CSS & JS relative to itself; adjust asset paths
const fixLinks = mount.querySelectorAll('link[rel="stylesheet"], script[src]');
fixLinks.forEach(el=>{
const attr = el.tagName==='SCRIPT'?'src':'href';
if(el.getAttribute(attr) && !/plugins\/nebot\//.test(el.getAttribute(attr))){
el.setAttribute(attr,'../plugins/nebot/'+el.getAttribute(attr));
}
});
// Inject JS if not already present
if(!mount.querySelector('script[data-nebot-page]')){
const s=document.createElement('script'); s.dataset.nebotPage='1';
// Pass the current URL hash to the page script for debug mode
s.src='../plugins/nebot/page.js' + window.location.hash;
mount.appendChild(s);
}
} catch(e){
showFallback(e.message||'Unknown error');
}
})();
</script>
</body>
</html>
-1606
View File
File diff suppressed because it is too large Load Diff
-762
View File
@@ -1,762 +0,0 @@
html, body {
height: 100%;
margin: 0;
padding: 0;
/* Background now driven by theme */
background: var(--bg, #0b0d10);
color: var(--text, white);
font-family: 'Segoe UI', system-ui, -apple-system, Ubuntu, Roboto, sans-serif;
}
/* Global variables */
:root {
/* Space reserved on the left for system window controls (traffic lights on macOS).
Applied cross-platform per request to keep a consistent layout. */
--window-controls-offset: 80px; /* adjust if needed */
/* Space reserved on the right for Windows title bar controls */
--window-controls-width: 138px;
/* Design tokens */
--bg: #0b0d10;
--surface-1: #11131a;
--surface-2: #161925;
--surface-3: #1c2030;
--text: #e8e8f0;
--muted: #a4a7b3;
--outline: #2b3040;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--shadow-1: 0 6px 20px rgba(0,0,0,.35);
--shadow-2: 0 12px 36px rgba(0,0,0,.45);
--blur: 12px;
/* Accent palette */
--accent-h: 265;
--accent-s: 86%;
--accent-l: 62%;
--accent: hsl(var(--accent-h) var(--accent-s) var(--accent-l));
--accent-600: hsl(var(--accent-h) var(--accent-s) 52%);
--accent-700: hsl(var(--accent-h) var(--accent-s) 46%);
/* URL bar and tab theme colors (defaults) */
--url-bar-bg: #1C2030;
--url-bar-text: #E0E0E0;
--url-bar-border: #3E4652;
--tab-bg: #161925;
--tab-text: #A4A7B3;
--tab-active: #1C2030;
--tab-active-text: #E0E0E0;
--tab-border: #2B3040;
}
/* TITLEBAR CONTAINER - holds tab bar and window controls */
#titlebar-container {
display: flex;
position: relative;
background: var(--tab-bg);
-webkit-app-region: drag;
}
/* TAB STRIP */
#tab-bar {
display: flex;
align-items: flex-end;
gap: 2px;
flex: 1;
/* Default: small left padding for Windows/Linux (no traffic lights) */
padding: 6px 10px 0 10px;
background: var(--tab-bg);
border-bottom: 1px solid var(--tab-border);
overflow-x: auto; /* scroll when many tabs */
scrollbar-color: #444 #2a2a3c; /* thumb and track for Firefox */
scrollbar-width: thin; /* slimmer track */
/* Inherit drag from container */
-webkit-app-region: drag;
min-height: 38px;
}
/* NAVBAR LAYOUT */
#nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: var(--url-bar-bg);
gap: 12px;
/* flatter header to reduce paint cost */
box-shadow: none;
/* Ensure the nav sits above embedded <webview> surfaces */
position: relative;
z-index: 10000;
-webkit-backdrop-filter: blur(var(--blur));
backdrop-filter: blur(var(--blur));
border-bottom: 1px solid var(--url-bar-border);
}
/* Make the top nav a draggable region on macOS when we use a hidden titlebar.
Interactive controls inside must opt-out with -webkit-app-region: no-drag. */
@supports (-webkit-app-region: drag) {
/* Make the top nav a draggable region, but only in its background/gaps. */
#nav { -webkit-app-region: drag; user-select: none; }
/* Interactive controls must explicitly opt-out of dragging. This keeps
the larger draggable area while preserving normal click behavior. */
#nav button,
#nav input,
#menu-popup,
.tab,
.tab *,
.new-tab-button,
.tab .tab-close,
#window-controls {
-webkit-app-region: no-drag;
}
/* Ensure the new-tab button (which sits on the tab strip) is not draggable */
#tab-bar .new-tab-button {
-webkit-app-region: no-drag;
}
}
.nav-left,
.nav-center,
.nav-right {
display: flex;
align-items: center;
gap: 8px;
}
.nav-center {
flex: 1;
padding: 6px 10px;
border-radius: 999px; /* pill */
/* glassy, accented border */
background: var(--url-bar-bg);
border: 1px solid var(--url-bar-border);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
}
#favicon {
width: 16px;
height: 16px;
margin-right: 4px;
}
#url {
flex: 1;
background: transparent;
border: none;
color: var(--url-bar-text);
font-size: 14px;
outline: none;
}
#url::placeholder {
color: rgba(255, 255, 255, 0.45);
}
/* Iconic circular chrome buttons */
.nav-left button,
.nav-right > button,
#reload-btn,
#menu-btn {
background: color-mix(in srgb, var(--url-bar-bg) 90%, var(--text) 10%);
color: var(--text);
border: 1px solid color-mix(in srgb, var(--text) 15%, transparent);
width: 34px;
height: 34px;
display: inline-grid;
place-items: center;
border-radius: 50%;
cursor: pointer;
/* subtle inner highlight adds edge definition */
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent);
transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
line-height: 0; /* avoid vertical misalignment for glyphs */
padding: 0;
}
#downloads-btn svg { display:block; width: 18px; height: 18px; }
/* Downloads button chrome to match other nav buttons */
#downloads-btn {
background: color-mix(in srgb, var(--url-bar-bg) 90%, var(--text) 10%);
color: var(--text);
border: 1px solid color-mix(in srgb, var(--text) 15%, transparent);
width: 34px;
height: 34px;
display: inline-grid;
place-items: center;
border-radius: 50%;
cursor: pointer;
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent);
transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease, color 120ms ease;
line-height: 0;
padding: 0;
-webkit-appearance: none;
appearance: none;
}
#downloads-btn:hover { filter: brightness(1.05); box-shadow: 0 4px 14px rgba(0,0,0,0.35); color: var(--text); }
#downloads-btn:active { transform: translateY(1px) scale(0.98); }
#downloads-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.55); }
#downloads-btn:focus { outline: none; box-shadow: none; }
/* Match home-active chrome variant */
body:has(#home-container.active) #downloads-btn {
background: color-mix(in srgb, var(--url-bar-bg) 85%, var(--text) 15%);
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent);
}
.nav-left button:hover,
.nav-right > button:hover,
#reload-btn:hover,
#menu-btn:hover {
filter: brightness(1.05);
box-shadow: 0 4px 14px color-mix(in srgb, var(--bg) 50%, transparent);
}
.nav-left button:active,
.nav-right > button:active,
#reload-btn:active,
#menu-btn:active {
transform: translateY(1px) scale(0.98);
}
/* Primary action (Go button) keeps rectangular look but modernized */
.nav-center + button,
.nav-center button {
background: var(--accent);
color: var(--text);
border: 1px solid transparent;
padding: 8px 14px;
border-radius: 10px;
cursor: pointer;
transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
}
.nav-center + button:hover,
.nav-center button:hover { box-shadow: 0 8px 20px color-mix(in srgb, var(--primary) 35%, transparent); }
.nav-center + button:active,
.nav-center button:active { transform: translateY(1px) scale(0.98); }
/* MENU DROPDOWN */
.menu-wrapper {
position: relative;
/* keep wrapper on a higher layer so absolute popup can composit above webviews */
z-index: 10001;
}
#menu-popup {
position: absolute;
top: 30px;
right: 0;
background: color-mix(in srgb, var(--url-bar-bg) 92%, var(--text) 8%);
border: 1px solid color-mix(in srgb, var(--primary) 25%, color-mix(in srgb, var(--accent) 18%, transparent));
border-radius: 14px;
padding: 8px;
display: flex;
flex-direction: column;
min-width: 200px; /* wider dropdown */
box-shadow: var(--shadow-1);
/* Much higher z-index and force its own compositing layer so it renders above <webview> guests */
z-index: 20000;
-webkit-transform: translateZ(0);
transform: translateZ(0);
will-change: transform, opacity;
/* animated open/close */
opacity: 1;
transform-origin: top right;
transition: opacity 160ms ease, transform 160ms ease;
/* ensure interactions only when visible */
visibility: visible;
pointer-events: auto;
-webkit-backdrop-filter: blur(var(--blur));
backdrop-filter: blur(var(--blur));
}
#menu-popup button {
background: transparent;
border: none;
color: var(--text);
text-align: left;
padding: 8px 10px;
border-radius: 10px;
transition: background 120ms ease, filter 120ms ease;
}
#menu-popup button:hover {
background: color-mix(in srgb, var(--text) 8%, transparent);
}
/* Big Picture Mode button special style */
#bigpicture-btn {
background: linear-gradient(135deg, color-mix(in srgb, var(--primary) 15%, transparent) 0%, color-mix(in srgb, var(--accent) 10%, transparent) 100%) !important;
border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent) !important;
margin: 4px 0;
}
#bigpicture-btn:hover {
background: linear-gradient(135deg, rgba(123, 46, 255, 0.25) 0%, rgba(0, 198, 255, 0.15) 100%) !important;
border-color: rgba(123, 46, 255, 0.5) !important;
}
.hidden {
display: none;
}
/* Animate menu dropdown instead of removing from flow */
#menu-popup.hidden {
display: block; /* override global .hidden */
opacity: 0;
transform: translateY(-8px) scale(0.98);
visibility: hidden;
pointer-events: none;
}
/* Downloads mini popup anchored to the downloads button */
.downloads-wrapper { position: relative; z-index: 10002; }
#downloads-popup {
position: absolute;
top: 34px;
right: 0;
background: color-mix(in srgb, var(--url-bar-bg) 95%, var(--text) 5%);
border: 1px solid color-mix(in srgb, var(--primary) 18%, color-mix(in srgb, var(--accent) 14%, transparent));
border-radius: 12px;
min-width: 280px;
box-shadow: var(--shadow-1);
padding: 8px;
-webkit-backdrop-filter: blur(var(--blur));
backdrop-filter: blur(var(--blur));
transition: opacity 160ms ease, transform 160ms ease;
}
#downloads-popup.hidden { opacity: 0; transform: translateY(-6px); visibility: hidden; pointer-events: none; }
.downloads-pop-header { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:4px 2px 8px; }
.downloads-pop-header > span { font-weight: 600; color: var(--text); }
.downloads-pop-header > button { background: transparent; border: none; color: var(--accent); cursor: pointer; }
.downloads-pop-list { display:flex; flex-direction: column; gap: 8px; max-height: 280px; overflow: auto; }
.downloads-empty { color: var(--tab-text); font-size: 12px; text-align: center; padding: 16px 8px; }
.dl-item { display:grid; grid-template-columns: 1fr auto; gap: 6px 8px; background: color-mix(in srgb, var(--text) 3%, transparent); border: 1px solid color-mix(in srgb, var(--text) 6%, transparent); border-radius: 10px; padding: 8px; }
.dl-file { font-size: 12px; color: var(--text); white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
.dl-meta { font-size: 11px; color: var(--tab-text); }
.dl-actions { display:flex; gap:6px; }
.dl-actions button { background: transparent; color: var(--text); border: 1px solid color-mix(in srgb, var(--text) 12%, transparent); border-radius: 8px; padding: 4px 8px; cursor: pointer; }
.dl-progress { height: 4px; background: color-mix(in srgb, var(--text) 8%, transparent); border-radius: 3px; overflow: hidden; grid-column: 1 / -1; }
.dl-bar { height: 100%; background: var(--accent); width: 0%; transition: width .12s linear; }
/* Circular progress ring around downloads button */
#downloads-btn { position: relative; }
#downloads-btn .ring {
position: absolute;
top: 50%;
left: 50%;
width: 40px; /* slightly larger than 34px button for halo */
height: 40px;
transform: translate(-50%, -50%);
border-radius: 50%;
pointer-events: none;
}
#downloads-btn .ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
#downloads-btn .ring circle.bg { stroke: color-mix(in srgb, var(--text) 15%, transparent); stroke-width: 3; fill: none; }
#downloads-btn .ring circle.fg { stroke: var(--accent); stroke-width: 3; fill: none; stroke-linecap: round; transition: stroke-dashoffset .12s linear, opacity .12s ease; }
/* WEBVIEWS */
#webviews {
flex: 1;
display: flex;
width: 100%;
position: relative;
/* make sure webviews render on a separate base layer behind nav */
z-index: 0;
}
#webviews.hidden {
display: none;
}
#webviews webview {
flex: 1;
width: 100%;
height: 100%;
border: none;
display: none;
}
#webviews webview.active {
display: flex;
}
/* When webviews is hidden, collapse its flex size */
#webviews.hidden {
flex: 0;
}
/* HOME CONTAINER */
#home-container {
flex: 1;
display: none;
width: 100%;
position: relative;
z-index: 0;
}
#home-container.active {
display: flex;
}
#home-webview {
width: 100%;
height: 100%;
border: none;
display: none;
flex: 1;
position: relative;
z-index: 0;
}
/* Show home webview when container is active */
#home-container.active > #home-webview {
display: flex;
}
/* TABS */
.tab {
position: relative;
display: flex;
align-items: center;
gap: 8px;
-webkit-app-region: no-drag; /* allow HTML5 DnD to work */
padding: 4px 10px; /* slimmer padding */
margin: 0;
height: 28px; /* reduce overall tab height */
color: var(--tab-text);
/* sleek glass tile */
background: var(--tab-bg);
border: 1px solid var(--tab-border);
border-bottom: none; /* let it visually merge with the strip line */
border-radius: 10px 10px 0 0; /* slightly tighter radius */
cursor: pointer;
user-select: none;
max-width: 260px;
min-width: 120px;
flex: 0 1 180px; /* like Chrome: shrink when crowded */
overflow: hidden;
transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease;
}
.tab:hover {
background: var(--tab-bg);
opacity: 0.85;
}
.tab.active {
color: var(--tab-active-text);
background: var(--tab-active);
box-shadow: 0 8px 22px color-mix(in srgb, var(--bg) 35%, transparent);
}
.tab.active::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 2px;
background: var(--accent);
}
.tab .tab-favicon {
width: 16px;
height: 16px;
border-radius: 2px;
flex: 0 0 auto;
}
.tab .tab-title {
flex: 1 1 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 12px;
}
.tab .tab-close {
flex: 0 0 auto;
width: 22px;
height: 22px;
display: grid;
place-items: center;
border: none;
border-radius: 11px;
background: transparent;
color: var(--tab-text);
opacity: 0; /* hidden by default */
transition: background 120ms ease, color 120ms ease, opacity 120ms ease;
}
.tab:hover .tab-close,
.tab.active .tab-close { opacity: 1; }
.tab .tab-close:hover { background: color-mix(in srgb, var(--text) 20%, transparent); color: var(--text); }
.tab .tab-close:active { background: color-mix(in srgb, var(--text) 15%, transparent); }
/* New tab (+) button aligned to the right end of the strip */
.new-tab-button {
margin-left: 6px;
flex: 0 0 auto;
width: 26px; /* tighter button */
height: 26px;
display: grid;
place-items: center;
border-radius: 13px;
border: 1px solid color-mix(in srgb, var(--text) 18%, transparent);
background: color-mix(in srgb, var(--tab-bg) 90%, var(--text) 10%);
color: var(--text);
cursor: pointer;
transition: transform 120ms ease, background 120ms ease, color 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}
.new-tab-button:hover { filter: brightness(1.06); box-shadow: 0 6px 16px color-mix(in srgb, var(--bg) 35%, transparent); }
.new-tab-button:active { transform: translateY(1px) scale(0.98); }
/* ZOOM CONTROLS */
.zoom-controls {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: color-mix(in srgb, var(--text) 4%, transparent);
border: 1px solid color-mix(in srgb, var(--text) 7%, transparent);
border-radius: 10px;
}
.zoom-controls .zoom-label {
flex: 1;
font-size: 14px;
}
.zoom-controls button {
background: transparent;
border: none;
color: var(--text);
font-size: 16px;
cursor: pointer;
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 8px;
transition: background 120ms ease;
}
.zoom-controls button:hover {
background: rgba(255,255,255,0.06);
}
#zoom-percent {
min-width: 38px;
text-align: center;
font-size: 13px;
color: var(--muted);
}
/* window controls (Windows/Linux only) - Firefox-style next to tabs */
#window-controls {
display: flex;
align-items: stretch;
align-self: stretch;
-webkit-app-region: no-drag;
background: transparent;
border-bottom: 1px solid var(--tab-border);
}
#window-controls button {
width: 46px;
background: transparent;
border: none;
color: var(--text);
cursor: pointer;
transition: background 120ms ease, color 120ms ease;
display: flex;
align-items: center;
justify-content: center;
-webkit-app-region: no-drag;
padding: 0;
}
#window-controls button svg {
width: 12px;
height: 12px;
pointer-events: none;
}
#window-controls button:hover {
background: rgba(255,255,255,0.1);
color: var(--text);
}
#window-controls #close-btn:hover {
background: #e81123;
color: white;
}
#window-controls #close-btn:hover svg path {
stroke: white;
}
/* Hide window controls on macOS via body class set by JS */
body.platform-darwin #window-controls {
display: none !important;
}
/* macOS: add left padding for traffic lights */
body.platform-darwin #tab-bar {
padding-left: var(--window-controls-offset);
}
#tab-bar::-webkit-scrollbar {
height: 8px; /* horizontal scrollbar height */
}
#tab-bar::-webkit-scrollbar-track {
background: #2a2a3c;
border-radius: 4px;
}
#tab-bar::-webkit-scrollbar-thumb {
background: #3f4152;
border-radius: 4px;
}
#tab-bar::-webkit-scrollbar-thumb:hover {
background: #56586a;
}
/* Tab animations */
.tab--flip {
transition: transform 180ms cubic-bezier(0.2, 0, 0, 1);
}
.tab--enter {
animation: tab-enter 160ms ease-out both;
}
.tab--closing {
animation: tab-exit 140ms ease-in both;
}
/* While dragging a tab, lift it slightly for feedback */
.tab--dragging {
transform: translateY(-2px) scale(1.04);
box-shadow: 0 10px 26px rgba(0,0,0,0.45);
z-index: 5;
transition: none !important; /* follow cursor without lag */
will-change: transform;
cursor: grabbing;
}
/* Show an insertion hint on hovered tab and nudge it */
.tab--drop-before,
.tab--drop-after {
position: relative;
}
.tab--drop-before { transform: translateX(-10px); }
.tab--drop-after { transform: translateX(10px); }
.tab--drop-before::before,
.tab--drop-after::after {
content: "";
position: absolute;
top: 4px;
bottom: 0;
width: 2px;
border-radius: 2px;
background: linear-gradient(180deg, var(--accent), var(--accent-600));
opacity: 0.9;
}
.tab--drop-before::before { left: 0; }
.tab--drop-after::after { right: 0; }
@keyframes tab-enter {
from {
opacity: 0;
transform: translateY(6px) scale(0.98);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes tab-exit {
to {
opacity: 0;
transform: translateY(-6px) scale(0.95);
}
}
/* Respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
}
}
/* Focus rings for accessibility */
.nav-left button:focus-visible,
.nav-right > button:focus-visible,
#reload-btn:focus-visible,
.menu-wrapper #menu-btn:focus-visible,
.new-tab-button:focus-visible,
#menu-popup button:focus-visible,
.zoom-controls button:focus-visible,
.tab .tab-close:focus-visible,
#window-controls button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.55);
}
/* Keyboard-only ring around the entire address bar (input + Go) */
.nav-center:has(:focus-visible) {
box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.55), inset 0 1px 0 rgba(255,255,255,0.05);
}
/* Fallback for engines without :has support */
@supports not selector(.nav-center:has(:focus-visible)) {
.nav-center:focus-within {
box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.45), inset 0 1px 0 rgba(255,255,255,0.05);
}
}
/* Remove default click outline but keep keyboard focus via :focus-visible */
.nav-left button:focus,
.nav-right > button:focus,
#reload-btn:focus,
#menu-btn:focus,
#url:focus,
.nav-center button:focus,
.new-tab-button:focus,
#menu-popup button:focus,
.zoom-controls button:focus,
.tab .tab-close:focus,
#window-controls button:focus {
outline: none;
box-shadow: none;
}
/* Stronger chrome contrast when Home is visible - uses theme variables */
body:has(#home-container.active) #tab-bar {
background: color-mix(in srgb, var(--tab-bg) 92%, black);
border-bottom-color: color-mix(in srgb, var(--tab-border) 80%, transparent);
box-shadow: 0 10px 24px -12px rgba(0,0,0,0.6);
}
body:has(#home-container.active) #nav {
background: color-mix(in srgb, var(--url-bar-bg) 92%, black);
border-bottom: 1px solid color-mix(in srgb, var(--url-bar-border) 80%, transparent);
box-shadow: 0 14px 36px -16px rgba(0,0,0,0.7);
}
body:has(#home-container.active) .nav-center {
background: color-mix(in srgb, var(--url-bar-bg) 96%, black);
border: 1px solid color-mix(in srgb, var(--primary, var(--accent)) 45%, transparent);
}
body:has(#home-container.active) .nav-left button,
body:has(#home-container.active) .nav-right > button,
body:has(#home-container.active) #reload-btn,
body:has(#home-container.active) #menu-btn {
/* slightly lighter than nav to pop over Home - uses theme colors */
background: color-mix(in srgb, var(--url-bar-bg) 85%, var(--text) 15%);
border: 1px solid color-mix(in srgb, var(--text) 20%, transparent);
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent);
}
/* Elevate active tab a touch more over home */
body:has(#home-container.active) .tab.active {
box-shadow: 0 10px 26px -8px rgba(0,0,0,0.6);
}
-1
View File
@@ -1 +0,0 @@
[]
-79
View File
@@ -1,79 +0,0 @@
#!/bin/bash
# Nebula Browser Setup Script
# This script installs dependencies and fixes Electron sandbox permissions
# Works on Steam Deck and other Linux systems without sudo
echo "========================================="
echo " Nebula Browser Setup Script"
echo "========================================="
echo ""
# Navigate to the project directory
cd "$(dirname "$0")"
# Run npm install
echo "[1/2] Installing dependencies..."
npm install
if [ $? -ne 0 ]; then
echo "❌ npm install failed!"
exit 1
fi
echo ""
echo "[2/2] Fixing Electron sandbox permissions..."
echo "This requires root access. You may be prompted for your password."
echo ""
# Fix chrome-sandbox permissions
SANDBOX_PATH="$(pwd)/node_modules/electron/dist/chrome-sandbox"
if [ ! -f "$SANDBOX_PATH" ]; then
echo "❌ chrome-sandbox not found at $SANDBOX_PATH"
echo " Make sure npm install completed successfully."
exit 1
fi
# Function to run command as root
run_as_root() {
if command -v sudo &> /dev/null; then
sudo "$@"
elif command -v pkexec &> /dev/null; then
pkexec "$@"
elif command -v doas &> /dev/null; then
doas "$@"
else
echo "No privilege escalation tool found (sudo/pkexec/doas)"
return 1
fi
}
# Try to fix permissions
echo "Attempting to set sandbox permissions..."
run_as_root chown root:root "$SANDBOX_PATH"
CHOWN_RESULT=$?
run_as_root chmod 4755 "$SANDBOX_PATH"
CHMOD_RESULT=$?
if [ $CHOWN_RESULT -eq 0 ] && [ $CHMOD_RESULT -eq 0 ]; then
echo "✅ Sandbox permissions fixed successfully!"
echo ""
echo "========================================="
echo " Setup complete! Run 'npm start' to launch Nebula"
echo "========================================="
echo ""
echo "💡 TIP: For GPU acceleration on Linux, run:"
echo " NEBULA_GPU_ALLOW_LINUX=1 npm start"
echo "========================================="
else
echo "❌ Failed to set sandbox permissions automatically."
echo ""
echo "On Steam Deck, open Konsole and run:"
echo " pkexec bash -c 'chown root:root $SANDBOX_PATH && chmod 4755 $SANDBOX_PATH'"
echo ""
echo "Or switch to desktop mode and run as root:"
echo " su -c 'chown root:root $SANDBOX_PATH && chmod 4755 $SANDBOX_PATH'"
exit 1
fi
+96
View File
@@ -0,0 +1,96 @@
#include "app/first_run_state.h"
#include <cctype>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <string_view>
#include <system_error>
#include "ui/paths.h"
namespace nebula::app {
namespace {
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();
}
bool ReadFirstStartValue(const std::string& json, bool& first_start) {
constexpr std::string_view key = "\"first-start\"";
const size_t key_pos = json.find(key);
if (key_pos == std::string::npos) {
return false;
}
size_t colon = json.find(':', key_pos + key.size());
if (colon == std::string::npos) {
return false;
}
++colon;
while (colon < json.size() && std::isspace(static_cast<unsigned char>(json[colon]))) {
++colon;
}
if (json.compare(colon, 4, "true") == 0) {
first_start = true;
return true;
}
if (json.compare(colon, 5, "false") == 0) {
first_start = false;
return true;
}
return false;
}
} // namespace
bool ShouldShowFirstRunSetup() {
const auto path = nebula::ui::GetFirstRunStatePath();
if (path.empty()) {
return true;
}
const std::string json = ReadFile(path);
if (json.empty()) {
return true;
}
bool first_start = true;
return ReadFirstStartValue(json, first_start) ? first_start : true;
}
bool WriteFirstRunState(bool first_start) {
const auto path = nebula::ui::GetFirstRunStatePath();
if (path.empty()) {
return false;
}
std::filesystem::path temp_path = path;
temp_path += L".tmp";
{
std::ofstream output(temp_path, std::ios::binary | std::ios::trunc);
if (!output) {
return false;
}
output << "{\n \"first-start\": " << (first_start ? "true" : "false") << "\n}\n";
}
std::error_code ec;
std::filesystem::remove(path, ec);
ec.clear();
std::filesystem::rename(temp_path, path, ec);
return !ec;
}
} // namespace nebula::app
+8
View File
@@ -0,0 +1,8 @@
#pragma once
namespace nebula::app {
bool ShouldShowFirstRunSetup();
bool WriteFirstRunState(bool first_start);
} // namespace nebula::app
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
#pragma once
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
#include "app/run.h"
#include "browser/tab_manager.h"
#include "cef/browser_client.h"
#include "platform/types.h"
#include "window/nebula_window.h"
namespace nebula::app {
class NebulaController final : public nebula::window::WindowDelegate,
public nebula::browser::TabObserver,
public nebula::cef::BrowserClientDelegate {
public:
NebulaController(nebula::platform::AppStartup startup,
std::string initial_url,
LaunchOptions launch_options = {});
~NebulaController() override;
bool Create();
void OnWindowCreated() override;
void OnWindowResized(const nebula::window::BrowserLayout& layout) override;
void OnWindowCloseRequested() override;
void OnExternalOpenRequested(const std::string& target) override;
void OnActiveTabChanged(const nebula::browser::NebulaTab& tab) override;
void OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
void OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
void OnChromeCommand(const std::string& command, const std::string& payload) override;
void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) override;
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(std::string url = {});
void ActivateTab(int tab_id);
void CloseTab(int tab_id);
void CreateChromeBrowser();
void CreateBigPictureBrowser();
void CreateContentBrowser();
void EnterBigPictureMode();
void ExitBigPictureMode();
nebula::window::BrowserLayout CurrentBrowserLayout() const;
void ToggleMenuPopup();
void CloseMenuPopup();
void CreateMenuPopupBrowser();
void PositionMenuPopup();
void SendMenuPopupZoom();
void SendThemeToChromeSurfaces(const std::string& theme_json);
void ToggleDevTools();
void AdjustZoom(double delta);
void FreshReload();
void SendBigPictureMouseMove(const std::string& payload);
void SendBigPictureMouseClick(const std::string& payload, bool right_click);
void SendBigPictureMouseWheel(const std::string& payload);
void SendBigPictureText(const std::string& payload);
void SetBigPictureBrowseVisible(bool visible);
void SetContentFullscreen(bool fullscreen);
void CompleteFirstRunSetup();
void SendDefaultBrowserResult(const std::string& request_id,
bool success,
bool needs_user_action,
const std::string& error = {});
void ResizeBrowsers();
void SendChromeState(const nebula::browser::NebulaTab& tab);
void SendBigPictureState(const nebula::browser::NebulaTab& tab);
void RecordSiteHistory(const std::string& url);
void InjectSettingsHistory(CefRefPtr<CefBrowser> browser);
void InjectBigPictureCursor(CefRefPtr<CefBrowser> browser);
void RemoveBigPictureCursor(CefRefPtr<CefBrowser> browser);
void PersistSession() const;
void BeginShutdown();
void MaybeFinishShutdown();
bool ForgetClosingTabBrowser(CefRefPtr<CefBrowser> browser);
void LoadPendingNavigationDelayed();
nebula::platform::AppStartup startup_;
std::string initial_url_;
std::string pending_initial_navigation_;
LaunchOptions launch_options_;
bool closing_ = false;
bool chrome_ready_ = false;
bool big_picture_ready_ = false;
bool big_picture_mode_ = false;
bool big_picture_browse_visible_ = false;
bool content_fullscreen_ = false;
bool first_run_setup_active_ = false;
bool menu_popup_visible_ = false;
std::unique_ptr<nebula::window::NebulaWindow> window_;
nebula::browser::TabManager tabs_;
CefRefPtr<CefBrowser> chrome_browser_;
CefRefPtr<CefBrowser> big_picture_browser_;
CefRefPtr<CefBrowser> menu_popup_browser_;
CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> big_picture_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
std::vector<CefRefPtr<CefBrowser>> closing_tab_browsers_;
std::unordered_set<std::string> insecure_warning_bypasses_;
std::vector<std::string> site_history_;
};
} // namespace nebula::app
+153
View File
@@ -0,0 +1,153 @@
#include "app/run.h"
#include "app/nebula_controller.h"
#include "browser/url_utils.h"
#include "cef/nebula_app.h"
#include "include/cef_app.h"
#include "include/cef_command_line.h"
#include "platform/default_browser.h"
#include "platform/startup.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 {
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) {
nebula::platform::PrepareApp();
const CefMainArgs main_args = nebula::platform::MakeMainArgs(startup);
CefRefPtr<nebula::cef::NebulaApp> app(new nebula::cef::NebulaApp);
const int subprocess_exit_code = CefExecuteProcess(main_args, app, nullptr);
if (subprocess_exit_code >= 0) {
return subprocess_exit_code;
}
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;
}
nebula::platform::EnsureDefaultBrowserRegistration();
CefSettings settings;
settings.no_sandbox = true;
settings.persist_session_cookies = true;
nebula::platform::ConfigureCefSettings(settings);
if (!CefInitialize(main_args, settings, app, nullptr)) {
return CefGetExitCode();
}
NebulaController controller(startup, std::move(initial_url), options);
const bool created = controller.Create();
if (created) {
CefRunMessageLoop();
}
CefShutdown();
return created ? 0 : 1;
}
} // namespace nebula::app
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "platform/types.h"
namespace nebula::app {
enum class AppMode {
Desktop,
BigPicture,
};
struct LaunchOptions {
AppMode mode = AppMode::Desktop;
};
int RunNebula(const nebula::platform::AppStartup& startup, LaunchOptions options = {});
} // namespace nebula::app
+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
+13
View File
@@ -0,0 +1,13 @@
#include "browser/tab.h"
namespace nebula::browser {
bool NebulaTab::CanGoBack() const {
return browser && browser->CanGoBack();
}
bool NebulaTab::CanGoForward() const {
return browser && browser->CanGoForward();
}
} // namespace nebula::browser
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <vector>
#include "include/cef_browser.h"
namespace nebula::browser {
struct NebulaTab {
int id = 1;
std::string url;
std::string title = "New Tab";
bool is_loading = false;
double load_progress = 0.0;
std::string favicon_url;
CefRefPtr<CefBrowser> browser;
bool CanGoBack() const;
bool CanGoForward() const;
};
} // namespace nebula::browser
+273
View File
@@ -0,0 +1,273 @@
#include "browser/tab_manager.h"
#include <algorithm>
#include "browser/url_utils.h"
#include "ui/paths.h"
namespace nebula::browser {
TabManager::TabManager(TabObserver* observer) : observer_(observer) {}
NebulaTab& TabManager::CreateInitialTab(std::string initial_url) {
tabs_.clear();
NebulaTab tab;
tab.id = next_tab_id_++;
tab.url = std::move(initial_url);
tabs_.push_back(std::move(tab));
active_tab_id_ = tabs_.front().id;
Notify();
return tabs_.front();
}
NebulaTab& TabManager::CreateTab(std::string url) {
NebulaTab tab;
tab.id = next_tab_id_++;
tab.url = std::move(url);
tabs_.push_back(std::move(tab));
active_tab_id_ = tabs_.back().id;
Notify();
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_) {
return &tab;
}
}
return nullptr;
}
const NebulaTab* TabManager::ActiveTab() const {
for (const auto& tab : tabs_) {
if (tab.id == active_tab_id_) {
return &tab;
}
}
return nullptr;
}
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;
}
active_tab_id_ = tab_id;
Notify();
return true;
}
CefRefPtr<CefBrowser> TabManager::CloseTab(int tab_id) {
for (auto it = tabs_.begin(); it != tabs_.end(); ++it) {
if (it->id != tab_id) {
continue;
}
CefRefPtr<CefBrowser> browser = it->browser;
const bool was_active = it->id == active_tab_id_;
const auto next_it = tabs_.erase(it);
if (tabs_.empty()) {
active_tab_id_ = 0;
} else if (was_active) {
active_tab_id_ = next_it != tabs_.end() ? next_it->id : tabs_.back().id;
}
Notify();
return browser;
}
return nullptr;
}
void TabManager::SetActiveBrowser(CefRefPtr<CefBrowser> browser) {
if (NebulaTab* tab = ActiveTab()) {
tab->browser = browser;
if (browser && tab->url.empty()) {
tab->url = browser->GetMainFrame()->GetURL();
}
Notify();
}
}
bool TabManager::OwnsBrowser(CefRefPtr<CefBrowser> browser) const {
if (!browser) {
return false;
}
for (const auto& tab : tabs_) {
if (tab.browser && tab.browser->IsSame(browser)) {
return true;
}
}
return false;
}
bool TabManager::HasOpenBrowsers() const {
for (const auto& tab : tabs_) {
if (tab.browser) {
return true;
}
}
return false;
}
void TabManager::ClearBrowser(CefRefPtr<CefBrowser> browser) {
if (NebulaTab* tab = FindTab(browser)) {
tab->browser = nullptr;
Notify();
}
}
void TabManager::LoadURL(const std::string& input) {
NebulaTab* tab = ActiveTab();
if (!tab || !tab->browser) {
return;
}
const std::string target = NormalizeNavigationInput(input);
if (target.empty()) {
return;
}
tab->url = target;
tab->favicon_url.clear();
tab->browser->GetMainFrame()->LoadURL(nebula::ui::ResolveInternalUrl(target));
Notify();
}
void TabManager::GoBack() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser && tab->browser->CanGoBack()) {
tab->browser->GoBack();
}
}
void TabManager::GoForward() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser && tab->browser->CanGoForward()) {
tab->browser->GoForward();
}
}
void TabManager::Reload() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser) {
tab->browser->Reload();
}
}
void TabManager::StopLoad() {
NebulaTab* tab = ActiveTab();
if (tab && tab->browser) {
tab->browser->StopLoad();
}
}
void TabManager::UpdateURL(CefRefPtr<CefBrowser> browser, std::string url) {
if (NebulaTab* tab = FindTab(browser)) {
tab->url = std::move(url);
Notify();
}
}
void TabManager::UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title) {
if (NebulaTab* tab = FindTab(browser)) {
tab->title = title.empty() ? "New Tab" : std::move(title);
Notify();
}
}
void TabManager::UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading) {
if (NebulaTab* tab = FindTab(browser)) {
tab->is_loading = is_loading;
if (is_loading) {
tab->favicon_url.clear();
}
if (!is_loading) {
tab->load_progress = 1.0;
}
Notify();
}
}
void TabManager::UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress) {
if (NebulaTab* tab = FindTab(browser)) {
tab->load_progress = progress;
Notify();
}
}
void TabManager::UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
if (NebulaTab* tab = FindTab(browser)) {
tab->favicon_url = urls.empty() ? std::string{} : urls.front();
Notify();
}
}
void TabManager::Notify() {
const NebulaTab* tab = ActiveTab();
if (observer_ && tab) {
observer_->OnActiveTabChanged(*tab);
}
}
NebulaTab* TabManager::FindTab(int tab_id) {
for (auto& tab : tabs_) {
if (tab.id == tab_id) {
return &tab;
}
}
return nullptr;
}
NebulaTab* TabManager::FindTab(CefRefPtr<CefBrowser> browser) {
if (!browser) {
return nullptr;
}
for (auto& tab : tabs_) {
if (tab.browser && tab.browser->IsSame(browser)) {
return &tab;
}
}
return nullptr;
}
} // namespace nebula::browser
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include "browser/tab.h"
#include "browser/session_state.h"
namespace nebula::browser {
class TabObserver {
public:
virtual ~TabObserver() = default;
virtual void OnActiveTabChanged(const NebulaTab& tab) = 0;
};
class TabManager {
public:
explicit TabManager(TabObserver* observer);
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);
void SetActiveBrowser(CefRefPtr<CefBrowser> browser);
bool OwnsBrowser(CefRefPtr<CefBrowser> browser) const;
void ClearBrowser(CefRefPtr<CefBrowser> browser);
bool HasOpenBrowsers() const;
void LoadURL(const std::string& input);
void GoBack();
void GoForward();
void Reload();
void StopLoad();
void UpdateURL(CefRefPtr<CefBrowser> browser, std::string url);
void UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title);
void UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading);
void UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress);
void UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls);
private:
void Notify();
NebulaTab* FindTab(int tab_id);
NebulaTab* FindTab(CefRefPtr<CefBrowser> browser);
TabObserver* observer_ = nullptr;
std::vector<NebulaTab> tabs_;
int active_tab_id_ = 0;
int next_tab_id_ = 1;
};
} // namespace nebula::browser
+115
View File
@@ -0,0 +1,115 @@
#include "browser/url_utils.h"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <sstream>
namespace nebula::browser {
namespace {
constexpr char kSearchUrl[] = "https://www.google.com/search?q=";
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;
}
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("nebula://");
}
bool LooksLikeHostName(const std::string& value) {
return value.find('.') != std::string::npos &&
value.find_first_of(" \t\r\n") == std::string::npos;
}
std::string UrlEncodeSearch(const std::string& value) {
std::ostringstream encoded;
encoded << std::hex << std::uppercase;
for (unsigned char ch : value) {
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
encoded << static_cast<char>(ch);
} else if (ch == ' ') {
encoded << '+';
} else {
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
}
}
return encoded.str();
}
} // namespace
std::string NormalizeNavigationInput(const std::string& input) {
const std::string value = Trim(input);
if (value.empty()) {
return {};
}
if (StartsWithScheme(value)) {
return value;
}
if (LooksLikeHostName(value)) {
return "https://" + value;
}
return std::string(kSearchUrl) + UrlEncodeSearch(value);
}
std::string JsonEscape(const std::string& value) {
std::ostringstream escaped;
for (unsigned char ch : value) {
switch (ch) {
case '\\':
escaped << "\\\\";
break;
case '"':
escaped << "\\\"";
break;
case '\b':
escaped << "\\b";
break;
case '\f':
escaped << "\\f";
break;
case '\n':
escaped << "\\n";
break;
case '\r':
escaped << "\\r";
break;
case '\t':
escaped << "\\t";
break;
default:
if (ch < 0x20) {
escaped << "\\u" << std::hex << std::uppercase << std::setw(4)
<< std::setfill('0') << static_cast<int>(ch);
} else {
escaped << static_cast<char>(ch);
}
break;
}
}
return escaped.str();
}
} // namespace nebula::browser
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <string>
namespace nebula::browser {
std::string NormalizeNavigationInput(const std::string& input);
std::string JsonEscape(const std::string& value);
} // namespace nebula::browser
+340
View File
@@ -0,0 +1,340 @@
#include "cef/browser_client.h"
#include "include/cef_request.h"
#include "platform/types.h"
#include "include/wrapper/cef_helpers.h"
#include "ui/paths.h"
namespace nebula::cef {
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");
}
bool IsSetupFrame(CefRefPtr<CefFrame> frame) {
if (!frame) {
return false;
}
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://setup");
}
bool IsBigPictureFrame(CefRefPtr<CefFrame> frame) {
if (!frame) {
return false;
}
const std::string url = nebula::ui::ToInternalUrl(frame->GetURL().ToString());
return url.starts_with("nebula://bigpicture") ||
url.starts_with("nebula://big-picture");
}
bool IsBigPictureCommand(const std::string& command) {
return command == "navigate" ||
command == "new-tab" ||
command == "activate-tab" ||
command == "close-tab" ||
command == "back" ||
command == "forward" ||
command == "reload" ||
command == "stop" ||
command == "home" ||
command == "settings" ||
command == "open-settings" ||
command == "big-picture" ||
command == "exit-bigpicture" ||
command == "gpu-diagnostics" ||
command == "zoom-out" ||
command == "zoom-in" ||
command == "clear-site-history" ||
command == "clear-search-history" ||
command == "bigpicture-mouse-move" ||
command == "bigpicture-click" ||
command == "bigpicture-right-click" ||
command == "bigpicture-scroll" ||
command == "bigpicture-text" ||
command == "bigpicture-browse-visible";
}
std::vector<std::string> ToStringVector(const std::vector<CefString>& values) {
std::vector<std::string> result;
result.reserve(values.size());
for (const auto& value : values) {
result.push_back(value.ToString());
}
return result;
}
} // namespace
NebulaBrowserClient::NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate)
: role_(role), delegate_(delegate) {}
bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) {
CEF_REQUIRE_UI_THREAD();
NEBULA_UNUSED(browser);
NEBULA_UNUSED(source_process);
if (!message || message->GetName().ToString() != kChromeCommandMessage) {
return false;
}
if (role_ != BrowserRole::Chrome && role_ != BrowserRole::Content &&
role_ != BrowserRole::BigPicture && role_ != BrowserRole::MenuPopup) {
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 == "new-tab" ||
command == "check-default-browser" ||
command == "set-default-browser" ||
command == "clear-site-history" ||
command == "clear-search-history");
const bool allowed_setup_command =
IsSetupFrame(frame) && (command == "complete-first-run" ||
command == "check-default-browser" ||
command == "set-default-browser");
const bool allowed_big_picture_command =
IsBigPictureFrame(frame) && command == "exit-bigpicture";
if (!allowed_insecure_command && !allowed_settings_command &&
!allowed_setup_command && !allowed_big_picture_command) {
return false;
}
} else if (role_ == BrowserRole::BigPicture) {
if (!IsBigPictureFrame(frame) || !IsBigPictureCommand(command)) {
return false;
}
}
if (delegate_ && !command.empty()) {
delegate_->OnChromeCommand(command, payload);
return true;
}
return false;
}
void NebulaBrowserClient::OnAddressChange(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
const CefString& url) {
CEF_REQUIRE_UI_THREAD();
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
delegate_->OnContentAddressChanged(browser, url.ToString());
}
}
void NebulaBrowserClient::OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) {
CEF_REQUIRE_UI_THREAD();
if (role_ == BrowserRole::Content && delegate_) {
delegate_->OnContentTitleChanged(browser, title.ToString());
}
}
void NebulaBrowserClient::OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
const std::vector<CefString>& icon_urls) {
CEF_REQUIRE_UI_THREAD();
if (role_ == BrowserRole::Content && delegate_) {
delegate_->OnContentFaviconChanged(browser, ToStringVector(icon_urls));
}
}
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,
bool* is_keyboard_shortcut) {
CEF_REQUIRE_UI_THREAD();
NEBULA_UNUSED(os_event);
if (role_ == BrowserRole::Content &&
event.type == KEYEVENT_RAWKEYDOWN &&
(event.modifiers & EVENTFLAG_CONTROL_DOWN) != 0 &&
event.windows_key_code == 'T') {
if (is_keyboard_shortcut) {
*is_keyboard_shortcut = true;
}
if (delegate_) {
delegate_->OnPopupRequested(browser, nebula::ui::GetHomeUrl());
}
return true;
}
return false;
}
bool NebulaBrowserClient::OnBeforePopup(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int popup_id,
const CefString& target_url,
const CefString& target_frame_name,
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
bool user_gesture,
const CefPopupFeatures& popupFeatures,
CefWindowInfo& windowInfo,
CefRefPtr<CefClient>& client,
CefBrowserSettings& settings,
CefRefPtr<CefDictionaryValue>& extra_info,
bool* no_javascript_access) {
CEF_REQUIRE_UI_THREAD();
NEBULA_UNUSED(frame);
NEBULA_UNUSED(popup_id);
NEBULA_UNUSED(target_frame_name);
NEBULA_UNUSED(target_disposition);
NEBULA_UNUSED(user_gesture);
NEBULA_UNUSED(popupFeatures);
NEBULA_UNUSED(windowInfo);
NEBULA_UNUSED(client);
NEBULA_UNUSED(settings);
NEBULA_UNUSED(extra_info);
NEBULA_UNUSED(no_javascript_access);
if (role_ == BrowserRole::Content && delegate_) {
delegate_->OnPopupRequested(browser, target_url.ToString());
return true;
}
return false;
}
void NebulaBrowserClient::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
if (delegate_) {
delegate_->OnBrowserCreated(role_, browser);
}
}
void NebulaBrowserClient::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
if (delegate_) {
delegate_->OnBrowserClosing(role_, browser);
}
}
void NebulaBrowserClient::OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
bool isLoading,
bool canGoBack,
bool canGoForward) {
CEF_REQUIRE_UI_THREAD();
NEBULA_UNUSED(canGoBack);
NEBULA_UNUSED(canGoForward);
if (role_ == BrowserRole::Content && delegate_) {
delegate_->OnContentLoadingStateChanged(browser, isLoading);
}
}
void NebulaBrowserClient::OnLoadStart(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
TransitionType transition_type) {
CEF_REQUIRE_UI_THREAD();
NEBULA_UNUSED(transition_type);
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
delegate_->OnContentLoadProgressChanged(browser, 0.12);
}
}
void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) {
CEF_REQUIRE_UI_THREAD();
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());
}
}
bool NebulaBrowserClient::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool user_gesture,
bool is_redirect) {
CEF_REQUIRE_UI_THREAD();
NEBULA_UNUSED(browser);
NEBULA_UNUSED(user_gesture);
NEBULA_UNUSED(is_redirect);
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;
}
bool NebulaBrowserClient::OnShowPermissionPrompt(
CefRefPtr<CefBrowser> browser,
uint64_t prompt_id,
const CefString& requesting_origin,
uint32_t requested_permissions,
CefRefPtr<CefPermissionPromptCallback> callback) {
CEF_REQUIRE_UI_THREAD();
NEBULA_UNUSED(prompt_id);
NEBULA_UNUSED(requesting_origin);
if (role_ == BrowserRole::Content &&
(requested_permissions & CEF_PERMISSION_TYPE_GEOLOCATION) != 0 &&
browser && callback &&
nebula::ui::IsInternalHomeUrl(browser->GetMainFrame()->GetURL().ToString())) {
callback->Continue(CEF_PERMISSION_RESULT_ACCEPT);
return true;
}
return false;
}
} // namespace nebula::cef
+123
View File
@@ -0,0 +1,123 @@
#pragma once
#include <string>
#include <vector>
#include "include/cef_client.h"
#include "include/cef_display_handler.h"
#include "include/cef_keyboard_handler.h"
#include "include/cef_life_span_handler.h"
#include "include/cef_load_handler.h"
#include "include/cef_permission_handler.h"
#include "include/cef_request_handler.h"
namespace nebula::cef {
enum class BrowserRole {
Chrome,
Content,
BigPicture,
MenuPopup,
};
class BrowserClientDelegate {
public:
virtual ~BrowserClientDelegate() = default;
virtual void OnBrowserCreated(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
virtual void OnBrowserClosing(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
virtual void OnChromeCommand(const std::string& command, const std::string& payload) = 0;
virtual void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
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,
public CefDisplayHandler,
public CefKeyboardHandler,
public CefLifeSpanHandler,
public CefLoadHandler,
public CefPermissionHandler,
public CefRequestHandler {
public:
NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate);
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override { return this; }
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
CefRefPtr<CefPermissionHandler> GetPermissionHandler() override { return this; }
CefRefPtr<CefRequestHandler> GetRequestHandler() override { return this; }
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) override;
void OnAddressChange(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
const CefString& url) override;
void OnTitleChange(CefRefPtr<CefBrowser> browser,
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,
CefEventHandle os_event,
bool* is_keyboard_shortcut) override;
bool OnBeforePopup(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int popup_id,
const CefString& target_url,
const CefString& target_frame_name,
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
bool user_gesture,
const CefPopupFeatures& popupFeatures,
CefWindowInfo& windowInfo,
CefRefPtr<CefClient>& client,
CefBrowserSettings& settings,
CefRefPtr<CefDictionaryValue>& extra_info,
bool* no_javascript_access) override;
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override;
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
bool isLoading,
bool canGoBack,
bool canGoForward) override;
void OnLoadStart(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
TransitionType transition_type) override;
void OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) override;
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool user_gesture,
bool is_redirect) override;
bool OnShowPermissionPrompt(CefRefPtr<CefBrowser> browser,
uint64_t prompt_id,
const CefString& requesting_origin,
uint32_t requested_permissions,
CefRefPtr<CefPermissionPromptCallback> callback) override;
private:
BrowserRole role_;
BrowserClientDelegate* delegate_ = nullptr;
IMPLEMENT_REFCOUNTING(NebulaBrowserClient);
};
} // namespace nebula::cef
+119
View File
@@ -0,0 +1,119 @@
#include "cef/nebula_app.h"
#include "include/cef_process_message.h"
#include "platform/types.h"
#include "include/wrapper/cef_helpers.h"
namespace nebula::cef {
namespace {
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
class NativeBridgeHandler final : public CefV8Handler {
public:
bool Execute(const CefString& name,
CefRefPtr<CefV8Value> object,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) override {
NEBULA_UNUSED(object);
NEBULA_UNUSED(retval);
if (name != "postMessage" && name != "sendToHost" && name != "send") {
return false;
}
if (arguments.empty() || !arguments[0]->IsString()) {
exception = "nebulaNative.postMessage requires a command string.";
return true;
}
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
CefRefPtr<CefBrowser> browser = context ? context->GetBrowser() : nullptr;
CefRefPtr<CefFrame> frame = context ? context->GetFrame() : nullptr;
if (!browser || !frame) {
exception = "No CEF frame is available for native messaging.";
return true;
}
CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kChromeCommandMessage);
CefRefPtr<CefListValue> args = message->GetArgumentList();
args->SetString(0, arguments[0]->GetStringValue());
args->SetString(1, arguments.size() > 1 && arguments[1]->IsString()
? arguments[1]->GetStringValue()
: CefString());
frame->SendProcessMessage(PID_BROWSER, message);
return true;
}
private:
IMPLEMENT_REFCOUNTING(NativeBridgeHandler);
};
} // namespace
void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type,
CefRefPtr<CefCommandLine> command_line) {
NEBULA_UNUSED(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");
#if defined(_WIN32)
command_line->AppendSwitchWithValue("use-gl", "angle");
command_line->AppendSwitchWithValue("use-angle", "d3d11");
#elif defined(__APPLE__)
command_line->AppendSwitchWithValue("use-angle", "metal");
#else
command_line->AppendSwitchWithValue("use-gl", "egl");
#endif
}
void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) {
CEF_REQUIRE_RENDERER_THREAD();
NEBULA_UNUSED(browser);
NEBULA_UNUSED(frame);
CefRefPtr<CefV8Value> global = context->GetGlobal();
CefRefPtr<NativeBridgeHandler> handler = new NativeBridgeHandler();
CefRefPtr<CefV8Value> native = CefV8Value::CreateObject(nullptr, nullptr);
native->SetValue(
"postMessage",
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
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "include/cef_app.h"
#include "include/cef_render_process_handler.h"
#include "include/cef_v8.h"
namespace nebula::cef {
class NebulaApp final : public CefApp,
public CefRenderProcessHandler {
public:
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() override { return this; }
void OnBeforeCommandLineProcessing(const CefString& process_type,
CefRefPtr<CefCommandLine> command_line) override;
void OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) override;
private:
IMPLEMENT_REFCOUNTING(NebulaApp);
};
} // namespace nebula::cef
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <utility>
#include "include/cef_browser.h"
#include "platform/types.h"
namespace nebula::platform {
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect);
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title);
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect);
void SetBrowserVisible(NativeWindow browser_window, bool visible);
void RaiseBrowserWindow(NativeWindow browser_window);
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y);
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout);
std::string CacheBusterToken();
void DestroyTopLevelWindow(NativeWindow window);
int ScaleForParentWindow(NativeWindow parent, int value);
std::pair<int, int> ParentClientSize(NativeWindow parent);
} // namespace nebula::platform
+9
View File
@@ -0,0 +1,9 @@
#pragma once
namespace nebula::platform {
bool IsDefaultBrowser();
bool EnsureDefaultBrowserRegistration();
bool RequestDefaultBrowser();
} // namespace nebula::platform
+77
View File
@@ -0,0 +1,77 @@
#include "platform/browser_host.h"
#include <algorithm>
namespace nebula::platform {
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) {
CefWindowInfo info;
info.SetAsChild(
reinterpret_cast<CefWindowHandle>(parent),
CefRect(rect.x, rect.y, rect.width, rect.height));
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title) {
CefWindowInfo info;
info.SetAsPopup(reinterpret_cast<CefWindowHandle>(parent), title);
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect) {
NEBULA_UNUSED(browser_window);
NEBULA_UNUSED(rect);
}
void SetBrowserVisible(NativeWindow browser_window, bool visible) {
NEBULA_UNUSED(browser_window);
NEBULA_UNUSED(visible);
}
void RaiseBrowserWindow(NativeWindow browser_window) {
NEBULA_UNUSED(browser_window);
}
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
NEBULA_UNUSED(browser_window);
NEBULA_UNUSED(x);
NEBULA_UNUSED(y);
}
int ScaleForParentWindow(NativeWindow parent, int value) {
NEBULA_UNUSED(parent);
return value;
}
std::pair<int, int> ParentClientSize(NativeWindow parent) {
NEBULA_UNUSED(parent);
return {1280, 720};
}
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout) {
const auto client_size = ParentClientSize(parent);
const int width = 260;
const int height = 258;
const int margin = 12;
const int overlap = 2;
const int x = std::max(0, client_size.first - width - margin);
const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap);
return {
x,
y,
std::min(client_size.first, x + width) - x,
std::min(client_size.second, y + height) - y,
};
}
std::string CacheBusterToken() {
return "0";
}
void DestroyTopLevelWindow(NativeWindow window) {
NEBULA_UNUSED(window);
}
} // 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
@@ -0,0 +1,45 @@
#include "window/nebula_window.h"
#include <memory>
namespace nebula::window {
struct nebula::window::NebulaWindowImpl {
WindowDelegate* delegate = nullptr;
};
NebulaWindow::NebulaWindow(WindowDelegate* delegate)
: impl_(std::make_unique<NebulaWindowImpl>()) {
impl_->delegate = delegate;
}
NebulaWindow::~NebulaWindow() = default;
bool NebulaWindow::Create(const platform::AppStartup& startup) {
NEBULA_UNUSED(startup);
return false;
}
platform::NativeWindow NebulaWindow::native_handle() const {
return nullptr;
}
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
NEBULA_UNUSED(show_chrome);
return {};
}
void NebulaWindow::ResizeChild(platform::NativeWindow child, const platform::Rect& rect) const {
NEBULA_UNUSED(child);
NEBULA_UNUSED(rect);
}
void NebulaWindow::Minimize() {}
void NebulaWindow::ToggleMaximize() {}
void NebulaWindow::SetFullscreen(bool fullscreen) { NEBULA_UNUSED(fullscreen); }
void NebulaWindow::Close() {}
void NebulaWindow::BeginDrag() {}
void NebulaWindow::SetTitle(const std::string& title) { NEBULA_UNUSED(title); }
void NebulaWindow::EnableFrameHitTest(platform::NativeWindow child) const { NEBULA_UNUSED(child); }
} // namespace nebula::window
+41
View File
@@ -0,0 +1,41 @@
#include "platform/paths_platform.h"
#include <pwd.h>
#include <unistd.h>
#include <cstdlib>
namespace nebula::platform {
std::filesystem::path ExecutableDirectory() {
char buffer[4096] = {};
const ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
if (length <= 0) {
return {};
}
buffer[length] = '\0';
return std::filesystem::path(buffer).parent_path();
}
std::filesystem::path DefaultUserDataRoot() {
if (const char* xdg_data = std::getenv("XDG_DATA_HOME"); xdg_data && *xdg_data) {
return std::filesystem::path(xdg_data);
}
if (const char* home = std::getenv("HOME")) {
return std::filesystem::path(home) / ".local" / "share";
}
if (passwd* pw = getpwuid(getuid())) {
return std::filesystem::path(pw->pw_dir) / ".local" / "share";
}
return ExecutableDirectory();
}
std::string PathToUtf8(const std::filesystem::path& path) {
return path.string();
}
} // namespace nebula::platform
+55
View File
@@ -0,0 +1,55 @@
#include "platform/startup.h"
#include <fcntl.h>
#include <filesystem>
#include <system_error>
#include <sys/file.h>
#include <unistd.h>
#include "include/cef_command_line.h"
#include "ui/paths.h"
namespace nebula::platform {
namespace {
int g_single_instance_lock = -1;
} // namespace
void PrepareApp() {}
bool TryAcquireSingleInstance(const std::string& launch_target) {
NEBULA_UNUSED(launch_target);
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
std::error_code ec;
std::filesystem::create_directories(lock_path.parent_path(), ec);
g_single_instance_lock = open(lock_path.c_str(), O_CREAT | O_RDWR, 0644);
if (g_single_instance_lock < 0) {
return true;
}
return flock(g_single_instance_lock, LOCK_EX | LOCK_NB) == 0;
}
CefMainArgs MakeMainArgs(const AppStartup& startup) {
return CefMainArgs(startup.argc, startup.argv);
}
void InitCommandLine(CefRefPtr<CefCommandLine> command_line, const AppStartup& startup) {
command_line->InitFromArgv(startup.argc, startup.argv);
}
void ConfigureCefSettings(CefSettings& settings) {
const std::string user_data_dir = nebula::ui::GetUserDataDirectory().string();
const std::string cache_dir = nebula::ui::GetCacheDirectory().string();
if (!user_data_dir.empty()) {
CefString(&settings.root_cache_path).FromString(user_data_dir);
}
if (!cache_dir.empty()) {
CefString(&settings.cache_path).FromString(cache_dir);
}
}
} // namespace nebula::platform
+134
View File
@@ -0,0 +1,134 @@
#include "platform/browser_host.h"
#import <ApplicationServices/ApplicationServices.h>
#import <Cocoa/Cocoa.h>
#include <algorithm>
#include <chrono>
#include <cmath>
namespace nebula::platform {
namespace {
NSView* AsView(NativeWindow window) {
return (__bridge NSView*)window;
}
NSRect ToNativeRect(const Rect& rect) {
return NSMakeRect(rect.x, rect.y, std::max(0, rect.width), std::max(0, rect.height));
}
Rect ToPlatformRect(NSRect rect) {
return {
static_cast<int>(std::round(NSMinX(rect))),
static_cast<int>(std::round(NSMinY(rect))),
std::max(0, static_cast<int>(std::round(NSWidth(rect)))),
std::max(0, static_cast<int>(std::round(NSHeight(rect)))),
};
}
} // namespace
CefWindowInfo MakeChildWindowInfo(NativeWindow parent, const Rect& rect) {
CefWindowInfo info;
info.SetAsChild((__bridge CefWindowHandle)AsView(parent),
CefRect(rect.x, rect.y, rect.width, rect.height));
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
CefWindowInfo MakeDevToolsPopup(NativeWindow parent, const char* title) {
(void)title;
CefWindowInfo info;
info.SetAsChild((__bridge CefWindowHandle)AsView(parent), CefRect(0, 0, 800, 600));
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
return info;
}
void ResizeBrowserWindow(NativeWindow browser_window, const Rect& rect) {
NSView* view = AsView(browser_window);
if (!view) {
return;
}
[view setFrame:ToNativeRect(rect)];
}
void SetBrowserVisible(NativeWindow browser_window, bool visible) {
NSView* view = AsView(browser_window);
if (!view) {
return;
}
[view setHidden:!visible];
if (visible && [view superview]) {
[[view superview] addSubview:view positioned:NSWindowAbove relativeTo:nil];
}
}
void RaiseBrowserWindow(NativeWindow browser_window) {
NSView* view = AsView(browser_window);
if (view && [view superview]) {
[[view superview] addSubview:view positioned:NSWindowAbove relativeTo:nil];
}
}
void MoveCursorToBrowserPoint(NativeWindow browser_window, int x, int y) {
NSView* view = AsView(browser_window);
if (!view || ![view window]) {
return;
}
const NSPoint window_point = [view convertPoint:NSMakePoint(x, y) toView:nil];
const NSPoint screen_point = [[view window] convertPointToScreen:window_point];
const CGFloat screen_height = NSMaxY([[NSScreen mainScreen] frame]);
CGWarpMouseCursorPosition(CGPointMake(screen_point.x, screen_height - screen_point.y));
}
int ScaleForParentWindow(NativeWindow parent, int value) {
(void)parent;
return value;
}
std::pair<int, int> ParentClientSize(NativeWindow parent) {
NSView* view = AsView(parent);
if (!view) {
return {0, 0};
}
const Rect rect = ToPlatformRect([view bounds]);
return {rect.width, rect.height};
}
Rect MenuPopupRect(NativeWindow parent, const BrowserLayout& layout) {
const auto client_size = ParentClientSize(parent);
const int width = 260;
const int height = 258;
const int margin = 12;
const int overlap = 2;
const int x = std::max(0, client_size.first - width - margin);
const int y = std::max(0, layout.chrome.y + layout.chrome.height - overlap);
return {
x,
y,
std::min(client_size.first, x + width) - x,
std::min(client_size.second, y + height) - y,
};
}
std::string CacheBusterToken() {
const auto now = std::chrono::system_clock::now().time_since_epoch();
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
return std::to_string(millis);
}
void DestroyTopLevelWindow(NativeWindow window) {
NSView* view = AsView(window);
NSWindow* native_window = [view window];
if (native_window) {
[native_window setDelegate:nil];
[native_window close];
}
}
} // namespace nebula::platform
+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
+222
View File
@@ -0,0 +1,222 @@
#include "window/nebula_window.h"
#import <Cocoa/Cocoa.h>
#include <algorithm>
#include <cmath>
#include <memory>
namespace nebula::window {
struct NebulaWindowImpl;
} // namespace nebula::window
@interface NebulaContentView : NSView
@end
@implementation NebulaContentView
- (BOOL)isFlipped {
return YES;
}
@end
@interface NebulaWindowDelegate : NSObject <NSWindowDelegate> {
@private
nebula::window::NebulaWindowImpl* owner_;
}
- (instancetype)initWithOwner:(nebula::window::NebulaWindowImpl*)owner;
@end
namespace nebula::window {
namespace {
constexpr CGFloat kChromeHeight = 104.0;
NSRect ToNativeRect(const platform::Rect& rect) {
return NSMakeRect(rect.x, rect.y, std::max(0, rect.width), std::max(0, rect.height));
}
} // namespace
struct NebulaWindowImpl {
WindowDelegate* delegate = nullptr;
NSWindow* window = nil;
NebulaContentView* content_view = nil;
NebulaWindowDelegate* window_delegate = nil;
bool fullscreen = false;
BrowserLayout CurrentLayout(bool show_chrome) const {
const NSRect bounds = content_view ? [content_view bounds] : NSZeroRect;
const int width = std::max(0, static_cast<int>(std::round(NSWidth(bounds))));
const int height = std::max(0, static_cast<int>(std::round(NSHeight(bounds))));
const int chrome_height = show_chrome ? std::min(height, static_cast<int>(kChromeHeight)) : 0;
BrowserLayout layout;
layout.chrome = show_chrome ? platform::Rect{0, 0, width, chrome_height} : platform::Rect{};
layout.content = {0, chrome_height, width, std::max(0, height - chrome_height)};
return layout;
}
void NotifyCreated() const {
if (delegate) {
delegate->OnWindowCreated();
}
}
void NotifyResized() const {
if (delegate) {
delegate->OnWindowResized(CurrentLayout(true));
}
}
void NotifyCloseRequested() const {
if (delegate) {
delegate->OnWindowCloseRequested();
}
}
};
} // namespace nebula::window
@implementation NebulaWindowDelegate
- (instancetype)initWithOwner:(nebula::window::NebulaWindowImpl*)owner {
self = [super init];
if (self) {
owner_ = owner;
}
return self;
}
- (void)windowDidResize:(NSNotification*)notification {
(void)notification;
if (owner_) {
owner_->NotifyResized();
}
}
- (BOOL)windowShouldClose:(id)sender {
(void)sender;
if (owner_) {
owner_->NotifyCloseRequested();
}
return NO;
}
@end
namespace nebula::window {
NebulaWindow::NebulaWindow(WindowDelegate* delegate)
: impl_(std::make_unique<NebulaWindowImpl>()) {
impl_->delegate = delegate;
}
NebulaWindow::~NebulaWindow() = default;
bool NebulaWindow::Create(const platform::AppStartup& startup) {
(void)startup;
@autoreleasepool {
[NSApplication sharedApplication];
const NSRect visible_frame = [[NSScreen mainScreen] visibleFrame];
const CGFloat width = std::min<CGFloat>(1400.0, NSWidth(visible_frame));
const CGFloat height = std::min<CGFloat>(900.0, NSHeight(visible_frame));
const CGFloat x = NSMinX(visible_frame) + (NSWidth(visible_frame) - width) / 2.0;
const CGFloat y = NSMinY(visible_frame) + (NSHeight(visible_frame) - height) / 2.0;
impl_->content_view = [[NebulaContentView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
impl_->window_delegate = [[NebulaWindowDelegate alloc] initWithOwner:impl_.get()];
impl_->window = [[NSWindow alloc] initWithContentRect:NSMakeRect(x, y, width, height)
styleMask:NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable |
NSWindowStyleMaskFullSizeContentView
backing:NSBackingStoreBuffered
defer:NO];
if (!impl_->window) {
return false;
}
[impl_->window setTitle:@"Nebula Browser"];
[impl_->window setTitleVisibility:NSWindowTitleHidden];
[impl_->window setTitlebarAppearsTransparent:YES];
[impl_->window setContentView:impl_->content_view];
[impl_->window setDelegate:impl_->window_delegate];
[impl_->window center];
[impl_->window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
impl_->NotifyCreated();
return true;
}
}
platform::NativeWindow NebulaWindow::native_handle() const {
return (__bridge void*)impl_->content_view;
}
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
return impl_->CurrentLayout(show_chrome);
}
void NebulaWindow::ResizeChild(platform::NativeWindow child, const platform::Rect& rect) const {
NSView* view = (__bridge NSView*)child;
if (!view) {
return;
}
[view setFrame:ToNativeRect(rect)];
}
void NebulaWindow::Minimize() {
[impl_->window miniaturize:nil];
}
void NebulaWindow::ToggleMaximize() {
if (impl_->window && !impl_->fullscreen) {
[impl_->window zoom:nil];
}
}
void NebulaWindow::SetFullscreen(bool fullscreen) {
if (!impl_->window || impl_->fullscreen == fullscreen) {
return;
}
impl_->fullscreen = fullscreen;
[impl_->window toggleFullScreen:nil];
}
void NebulaWindow::Close() {
if (impl_->delegate) {
impl_->delegate->OnWindowCloseRequested();
}
}
void NebulaWindow::BeginDrag() {
if (!impl_->window) {
return;
}
NSEvent* event = [NSApp currentEvent];
if (event) {
[impl_->window performWindowDragWithEvent:event];
}
}
void NebulaWindow::SetTitle(const std::string& title) {
if (!impl_->window) {
return;
}
NSString* native_title = title.empty()
? @"Nebula Browser"
: [[NSString alloc] initWithUTF8String:title.c_str()];
[impl_->window setTitle:native_title ?: @"Nebula Browser"];
}
void NebulaWindow::EnableFrameHitTest(platform::NativeWindow child) const {
(void)child;
}
} // namespace nebula::window
+37
View File
@@ -0,0 +1,37 @@
#include "platform/paths_platform.h"
#include <mach-o/dyld.h>
#include <pwd.h>
#include <unistd.h>
#include <cstdlib>
namespace nebula::platform {
std::filesystem::path ExecutableDirectory() {
char buffer[4096] = {};
uint32_t size = sizeof(buffer);
if (_NSGetExecutablePath(buffer, &size) != 0) {
return {};
}
return std::filesystem::path(buffer).parent_path();
}
std::filesystem::path DefaultUserDataRoot() {
if (const char* home = std::getenv("HOME")) {
return std::filesystem::path(home) / "Library" / "Application Support";
}
if (passwd* pw = getpwuid(getuid())) {
return std::filesystem::path(pw->pw_dir) / "Library" / "Application Support";
}
return ExecutableDirectory();
}
std::string PathToUtf8(const std::filesystem::path& path) {
return path.string();
}
} // namespace nebula::platform
+63
View File
@@ -0,0 +1,63 @@
#include "platform/startup.h"
#import <Cocoa/Cocoa.h>
#include <fcntl.h>
#include <filesystem>
#include <system_error>
#include <sys/file.h>
#include <unistd.h>
#include "include/cef_command_line.h"
#include "ui/paths.h"
namespace nebula::platform {
namespace {
int g_single_instance_lock = -1;
} // namespace
void PrepareApp() {
@autoreleasepool {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
}
}
bool TryAcquireSingleInstance(const std::string& launch_target) {
NEBULA_UNUSED(launch_target);
const auto lock_path = nebula::ui::GetUserDataDirectory() / ".nebula_instance.lock";
std::error_code ec;
std::filesystem::create_directories(lock_path.parent_path(), ec);
g_single_instance_lock = open(lock_path.c_str(), O_CREAT | O_RDWR, 0644);
if (g_single_instance_lock < 0) {
return true;
}
return flock(g_single_instance_lock, LOCK_EX | LOCK_NB) == 0;
}
CefMainArgs MakeMainArgs(const AppStartup& startup) {
return CefMainArgs(startup.argc, startup.argv);
}
void InitCommandLine(CefRefPtr<CefCommandLine> command_line, const AppStartup& startup) {
command_line->InitFromArgv(startup.argc, startup.argv);
}
void ConfigureCefSettings(CefSettings& settings) {
const std::string user_data_dir = nebula::ui::GetUserDataDirectory().string();
const std::string cache_dir = nebula::ui::GetCacheDirectory().string();
if (!user_data_dir.empty()) {
CefString(&settings.root_cache_path).FromString(user_data_dir);
}
if (!cache_dir.empty()) {
CefString(&settings.cache_path).FromString(cache_dir);
}
}
} // namespace nebula::platform

Some files were not shown because too many files have changed in this diff Show More