Steam Controller Reg

This commit is contained in:
2025-12-30 19:48:04 +13:00
parent 47970eb0cd
commit ec08213563
7 changed files with 893 additions and 20 deletions
+108
View File
@@ -46,3 +46,111 @@ Notes
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. **Verify gamepad polling is active**: Open DevTools (F12) and run `gamepadAPI.getState()` - check that `isPolling` is `true`
2. **Check gamepad connection**: Run `gamepadAPI.getConnected()` to see detected gamepads
3. **Press a button first**: On Linux, the `gamepadconnected` event may not fire until the first button press
4. **Enable debug mode**: Run `gamepadAPI.setDebug(true)` to see detailed logs
5. **Restart the app**: Close Nebula completely and relaunch from Steam
### Steam Launch Options
```
# Force Big Picture Mode
./Nebula --big-picture
# Disable Big Picture Mode
./Nebula --no-big-picture
# Environment variables also work
NEBULA_BIG_PICTURE=1 ./Nebula
NEBULA_NO_BIG_PICTURE=1 ./Nebula
```
+453
View File
@@ -0,0 +1,453 @@
/**
* 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');
})();
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "nebula",
"version": "1.0.0",
"version": "1.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nebula",
"version": "1.0.0",
"version": "1.3.2",
"license": "ISC",
"dependencies": {
"dompurify": "^3.1.6",
+273
View File
@@ -1,17 +1,241 @@
// 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;
}
// =============================================================================
// 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;
}
});
// =============================================================================
// DOM READY & INITIALIZATION
// =============================================================================
// Cache DOM references for performance
let domReady = false;
window.addEventListener('DOMContentLoaded', () => {
domReady = true;
console.log("Browser UI loaded.");
// Initialize gamepad handler for Steam Deck/SteamOS support
initGamepadHandler();
});
// Optimized API exposure with error handling and caching
@@ -138,6 +362,55 @@ const bookmarksAPI = {
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')
+7
View File
@@ -654,6 +654,13 @@ function initGamepadSupport() {
return;
}
// The global gamepad handler (from gamepad-handler.js injected via preload)
// already polls navigator.getGamepads() continuously. This is what tells Steam
// that we're consuming gamepad input and it should stop mouse emulation.
// Big Picture Mode handles the actual UI navigation and button actions.
console.log('[BigPicture] Global gamepad handler available:', !!window.__nebulaGamepadHandler);
// Note: On Linux (and some controllers like handheld integrated gamepads),
// the `gamepadconnected` event may not fire until the first button press,
// or at all. We rely on continuous polling for robustness.
+41 -13
View File
@@ -2,6 +2,7 @@
# 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"
@@ -26,26 +27,53 @@ echo "This requires root access. You may be prompted for your password."
echo ""
# Fix chrome-sandbox permissions
SANDBOX_PATH="./node_modules/electron/dist/chrome-sandbox"
SANDBOX_PATH="$(pwd)/node_modules/electron/dist/chrome-sandbox"
if [ -f "$SANDBOX_PATH" ]; then
sudo chown root:root "$SANDBOX_PATH"
sudo chmod 4755 "$SANDBOX_PATH"
if [ ! -f "$SANDBOX_PATH" ]; then
echo "❌ chrome-sandbox not found at $SANDBOX_PATH"
echo " Make sure npm install completed successfully."
exit 1
fi
if [ $? -eq 0 ]; then
# 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."
echo " Try running manually:"
echo " sudo chown root:root $SANDBOX_PATH && sudo chmod 4755 $SANDBOX_PATH"
exit 1
fi
else
echo "❌ chrome-sandbox not found at $SANDBOX_PATH"
echo " Make sure npm install completed successfully."
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
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# Start Nebula Browser with GPU acceleration enabled on Linux
cd "$(dirname "$0")"
NEBULA_GPU_ALLOW_LINUX=1 npm start