diff --git a/.gitignore b/.gitignore index af4b07f..149ee60 100644 --- a/.gitignore +++ b/.gitignore @@ -71,20 +71,12 @@ temp/ # Keep lockfiles tracked (do not ignore package-lock.json) # Task list -<<<<<<< HEAD task-list.md -======= tasklist.md ->>>>>>> development # Security reports security_reports/ -<<<<<<< HEAD -# Task lists -tasklist.md - -======= # Regression test checklist REGRESSION_TEST_CHECKLIST.md @@ -93,4 +85,3 @@ release.md # copyparty copyparty_1.19.22.exe ->>>>>>> development diff --git a/README.md b/README.md index cbf2f5b..ccd6635 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Grok-Desktop v1.2.5 ## Description -Grok-Desktop is an Electron-based desktop application for Windows 10/11 and Linux that wraps `grok.com`, providing desktop-application-like access to Grok with real-time API usage monitoring, multi-tab support, and seamless authentication for xAI, Google, and Apple accounts. +Grok-Desktop is an Electron-based desktop application for Windows 10/11, Linux, and macOS that wraps `grok.com`, providing desktop-application-like access to Grok with real-time API usage monitoring, multi-tab support, and seamless authentication for xAI, Google, and Apple accounts. ## Screenshot ![Screenshot](screenshot.png) @@ -15,15 +15,15 @@ Grok-Desktop is an Electron-based desktop application for Windows 10/11 and Linu - **Grok 4 Heavy**: Specialized model usage limits - **Refill Timer**: Shows when limits reset - **Keyboard shortcuts**: - - `Ctrl+T`: Open a new tab - - `Ctrl+Tab` / `Ctrl+Shift+Tab`: Cycle through open tabs (next/previous) + - `Ctrl+T` / `Cmd+T`: Open a new tab + - `Ctrl+Tab` / `Ctrl+Shift+Tab`: Cycle through open tabs (next/previous). `Ctrl+Tab` also works on macOS; `Cmd+Tab` is the system app switcher - `Mouse-wheel-scroll`: Scroll open tabs when they are overflowing on the tab bar - - `Ctrl+Mouse-wheel-scroll`: Cycle through open tabs (next/previous) - - `Ctrl+R`: Reload the active tab - - `Ctrl+I`: Show information/about dialog + - `Ctrl+Mouse-wheel-scroll` / `Cmd+Mouse-wheel-scroll`: Cycle through open tabs (next/previous) + - `Ctrl+R` / `Cmd+R`: Reload the active tab + - `Ctrl+I` / `Cmd+I`: Show information/about dialog - **Authentication support** for xAI, Google, and Apple accounts -- **Clean interface** with no menu bar for distraction-free usage -- **Always-on-top function** with cross-platform support (Windows & Linux) +- **Clean interface** with no menu bar on Windows/Linux; macOS uses the standard application menu +- **Always-on-top function** with cross-platform support (Windows, Linux, and macOS) - **Dark/Light mode support** with system theme detection - **Grok speech mode** support - **Enhanced security** with domain validation and OAuth protection @@ -45,15 +45,16 @@ Grok-Desktop is an Electron-based desktop application for Windows 10/11 and Linu ## System Requirements ### For Using the Application -- **Operating System**: Windows 10/11 or Linux (Rocky Linux 9/10, RHEL 9, Ubuntu, Fedora, etc.) +- **Operating System**: Windows 10/11, macOS 11+, or Linux (Rocky Linux 9/10, RHEL 9, Ubuntu, Fedora, etc.) - **Internet connection** for accessing grok.com - **Grok account** (sign up in-app or use Google/Apple/xAI authentication) - **Linux AOT (Always-on-Top) requirement**: Install `wmctrl` for Always-on-Top functionality: - Rocky Linux/RHEL/Fedora: `sudo dnf install wmctrl` - Ubuntu/Debian: `sudo apt install wmctrl` +- **macOS AOT**: Uses Electron `setAlwaysOnTop` (no extra packages) ### For Building from Source -- **Operating System**: Windows 10/11 or Linux +- **Operating System**: Windows 10/11, macOS 11+, or Linux - **Node.js**: LTS version (20.x recommended) - **Internet connection** for downloading dependencies @@ -66,7 +67,7 @@ Grok-Desktop/ │ ├── renderer.js # Renderer process code │ ├── custom-tabs.js # Custom tabs implementation │ ├── grok.ico # Windows application icon -│ └── grok.png # Linux application icon +│ └── grok.png # Linux/macOS application icon ├── assets/ # Static assets │ ├── icon.png │ └── icon.svg @@ -80,6 +81,7 @@ Grok-Desktop/ ├── about.html # About dialog HTML ├── package.json # Node.js dependencies and build config ├── build.bat # Windows build script +├── build-macos.sh # macOS build/launch script ├── build-linux.sh # Linux build script ├── CHANGELOG.md # Version history and changes ├── LINUX_BUILD_GUIDE.md # Detailed Linux build instructions @@ -87,39 +89,6 @@ Grok-Desktop/ └── README.md # This file ``` -<<<<<<< HEAD -## Build Grok-Desktop -1. Install Node.js from [nodejs.org](https://nodejs.org/). -2. Clone this repository or download the files. -3. Install dependencies if needed: `npm install` -4. Build using npm scripts: - - Directory build (unpacked): `npm run build-dir` - - Portable executable: `npm run build-portable` - - Full installers (NSIS + MSI): `npm run build-installer` - -Notes: -- These scripts use `npx electron-builder@latest` (no global install required). -- All build outputs are written to the `build` directory. - -## Usage -- After building, install `Grok-Desktop` with `Grok-Desktop_Installer-v1.2.3.exe` from the `build` directory -- Launch `Grok-Desktop` from the Start Menu -- Log in via `grok.com`, using Google, Apple, or xAI authentication as needed. -- Use the + button in the top toolbar (or Ctrl+T) to add new tabs. -- Click the AOT button in the top right to toggle always-on-top functionality. -- Use keyboard shortcuts to work faster: - - Ctrl+T: Open a new tab - - Ctrl+Tab / Ctrl+Shift+Tab: Cycle through open tabs (next/previous) - - Ctrl+R: Reload the currently active tab - - Ctrl+I: Show information/about dialog - -## Keyboard Shortcuts -- Ctrl+T: Open a new tab -- Ctrl+Tab: Switch to the next tab -- Ctrl+Shift+Tab: Switch to the previous tab -- Ctrl+R: Reload the active tab (does not reload the entire app window) -- Ctrl+I: Show information/about dialog -======= ## Building from Source ### Prerequisites @@ -128,8 +97,12 @@ Notes: 3. Navigate to the project: `cd Grok-Desktop` 4. Install dependencies: `npm install` +### Run from source +```bash +npm start +``` + ### Build Commands ->>>>>>> development #### Windows ```bash @@ -143,6 +116,26 @@ npm run build-portable npm run build-dir ``` +#### macOS +```bash +# Run from source +npm start + +# Build Grok Desktop.app and launch it +./build-macos.sh --launch +# or: npm run build-mac-app + +# Unsigned DMG + ZIP +./build-macos.sh --dmg +# or: npm run build-mac +``` + +The `.app` lands in `build/mac-arm64/Grok Desktop.app` on Apple Silicon (`build/mac/Grok Desktop.app` on Intel). It is unsigned (`identity` is null). `build-macos.sh --launch` clears the quarantine flag; a Finder launch may still need right-click **Open** the first time: + +```bash +xattr -cr "build/mac-arm64/Grok Desktop.app" +``` + #### Linux ```bash # RPM + AppImage packages @@ -151,7 +144,7 @@ npm run build-linux # RPM only for RHEL/Rocky Linux npm run build-rhel -# All platforms (Windows + Linux) +# All platforms (Windows + Linux + macOS) npm run build-all ``` @@ -175,6 +168,15 @@ cd build sudo dnf install ./Grok-Desktop-v1.2.5.x86_64.rpm ``` +### macOS +Prebuilt macOS binaries are not published on GitHub Releases yet. From this repo: + +```bash +./build-macos.sh --launch +``` + +If Gatekeeper blocks a Finder launch, right-click and choose **Open**, or run `xattr -cr` on the app bundle. + ## Usage Guide ### Getting Started @@ -220,15 +222,16 @@ Thanks to Joshua Wang for his code. ### Keyboard Shortcuts | Shortcut | Action | |----------|--------| -| `Ctrl+T` | Open a new tab | -| `Ctrl+Tab` | Switch to next tab | -| `Ctrl+Shift+Tab` | Switch to previous tab | -| `Ctrl+R` | Reload active tab | -| `Ctrl+I` | Show information/about dialog | +| `Ctrl+T` / `Cmd+T` | Open a new tab | +| `Ctrl+Tab` / `Ctrl+Shift+Tab` | Switch to next/previous tab (`Ctrl+Tab` works on macOS; `Cmd+Tab` is the system app switcher) | +| `Ctrl+R` / `Cmd+R` | Reload active tab | +| `Ctrl+I` / `Cmd+I` | Show information/about dialog | +| `Ctrl+scroll` / `Cmd+scroll` | Cycle tabs when hovering the tab bar | ### Always-on-Top Feature - Click the **AOT** button to toggle always-on-top mode - The button turns green when active +- On Windows and macOS, uses Electron `setAlwaysOnTop()` - On Linux, requires `wmctrl` package (automatically handled) ## Support & Troubleshooting @@ -336,6 +339,10 @@ Cross-platform always-on-top functionality with automatic compatibility handling - Uses Electron's built-in `BrowserWindow.setAlwaysOnTop()` method - Native Windows API integration +#### macOS Implementation +- Uses Electron's built-in `BrowserWindow.setAlwaysOnTop()` method +- No extra packages required + #### Linux Implementation - **Primary**: Uses `wmctrl` command-line tool for reliable window management - **Fallback**: Electron's `setAlwaysOnTop()` method when `wmctrl` unavailable diff --git a/build-macos.sh b/build-macos.sh new file mode 100755 index 0000000..b0484c4 --- /dev/null +++ b/build-macos.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +cd "$ROOT" + +LAUNCH=0 +MAKE_DMG=0 + +usage() { + cat <<'EOF' +Build the unsigned macOS Grok Desktop.app. + +Usage: ./build-macos.sh [--launch] [--dmg] + + --launch Open Grok Desktop.app when the build finishes + --dmg Also emit DMG + ZIP (slower) + (default) Directory build only → build/mac-*/Grok Desktop.app + +Requires Node.js LTS. The app is unsigned; this script clears the +quarantine flag before --launch. Finder may still ask you to right-click +Open the first time. +EOF +} + +for arg in "$@"; do + case "$arg" in + --launch|-l) LAUNCH=1 ;; + --dmg|--release) MAKE_DMG=1 ;; + --dir) MAKE_DMG=0 ;; + -h|--help) usage; exit 0 ;; + *) + echo "Unknown option: $arg" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "This script builds a macOS app and must run on macOS." >&2 + exit 1 +fi + +if ! command -v npm >/dev/null 2>&1; then + echo "npm is not on PATH. Install Node.js LTS from https://nodejs.org/" >&2 + exit 1 +fi + +echo "==> Installing dependencies" +npm install + +export CSC_IDENTITY_AUTO_DISCOVERY=false + +if [[ "$MAKE_DMG" -eq 1 ]]; then + echo "==> Building Grok Desktop.app + DMG + ZIP" + npx --yes electron-builder@latest --mac dmg zip +else + echo "==> Building Grok Desktop.app" + npx --yes electron-builder@latest --mac dir +fi + +APP="" +for candidate in \ + "$ROOT/build/mac-arm64/Grok Desktop.app" \ + "$ROOT/build/mac/Grok Desktop.app" \ + "$ROOT/build/mac-universal/Grok Desktop.app" +do + if [[ -d "$candidate" ]]; then + APP="$candidate" + break + fi +done + +if [[ -z "$APP" ]]; then + echo "Build finished but Grok Desktop.app was not found under build/mac*." >&2 + exit 1 +fi + +echo "==> Built $APP" + +if [[ "$LAUNCH" -eq 1 ]]; then + echo "==> Launching Grok Desktop" + pkill -f "$ROOT/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron" 2>/dev/null || true + osascript -e 'tell application "Grok Desktop" to quit' >/dev/null 2>&1 || true + sleep 0.5 + xattr -cr "$APP" 2>/dev/null || true + open "$APP" +fi diff --git a/index.html b/index.html index 8e689bc..51b1fd4 100644 --- a/index.html +++ b/index.html @@ -757,9 +757,9 @@ // Only handle if we're over the tabs area (not scroll buttons) if (e.target.closest('.tabs-scroll')) return; - // Check if Ctrl is pressed for tab switching - if (e.ctrlKey) { - // Ctrl + scroll = switch tabs + // Check if Ctrl or Command is pressed for tab switching + if (e.ctrlKey || e.metaKey) { + // Ctrl/Cmd + scroll = switch tabs e.preventDefault(); // Prevent any default behavior if (e.deltaY > 0) { // Scroll down = next tab diff --git a/package.json b/package.json index 56623b1..c60a678 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "build-rpm": "npx --yes electron-builder@latest --linux rpm", "build-deb": "npx --yes electron-builder@latest --linux deb", "build-rpm-deb": "npx --yes electron-builder@latest --linux rpm deb", - "build-all": "npx --yes electron-builder@latest --win --x64 --linux" + "build-mac": "npx --yes electron-builder@latest --mac dmg zip", + "build-mac-app": "CSC_IDENTITY_AUTO_DISCOVERY=false npx --yes electron-builder@latest --mac dir", + "build-all": "npx --yes electron-builder@latest --win --x64 --linux --mac" }, "keywords": [ "electron", @@ -41,6 +43,25 @@ "directories": { "output": "build" }, + "mac": { + "target": [ + "dmg", + "zip" + ], + "category": "public.app-category.productivity", + "icon": "build-resources/icons", + "identity": null, + "hardenedRuntime": false, + "gatekeeperAssess": false, + "extendInfo": { + "NSMicrophoneUsageDescription": "Grok Desktop needs the microphone for Grok speech mode.", + "NSCameraUsageDescription": "Grok Desktop needs the camera when Grok requests video." + }, + "artifactName": "Grok-Desktop-v${version}-${arch}.${ext}" + }, + "dmg": { + "artifactName": "Grok-Desktop-v${version}-${arch}.${ext}" + }, "linux": { "target": [ "rpm", diff --git a/src/main.js b/src/main.js index 1631d61..401c2a7 100644 --- a/src/main.js +++ b/src/main.js @@ -4,32 +4,38 @@ const os = require('os'); const fs = require('fs'); const { exec } = require('child_process'); +try { app.setName('Grok Desktop'); } catch (_) {} + // GPU acceleration detection and graceful fallback function configureGpuAcceleration() { - // Check if we're in a headless environment or container - const isHeadless = !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY; - const isContainer = fs.existsSync('/.dockerenv') || process.env.container === 'docker'; + // Linux-only headless/container/device probes. Missing DISPLAY/WAYLAND_DISPLAY + // or /dev/dri on darwin/win32 is normal and must not disable GPU. + let isHeadless = false; + let isContainer = false; + let hasGpu = true; - // Check for NVIDIA/AMD GPU availability (basic detection) - let hasGpu = false; - try { - // Check for NVIDIA GPU - if (fs.existsSync('/dev/nvidia0') || process.env.NVIDIA_VISIBLE_DEVICES) { - hasGpu = true; - } - // Check for AMD GPU - if (fs.existsSync('/dev/dri/card0')) { - hasGpu = true; + if (os.platform() === 'linux') { + isHeadless = !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY; + isContainer = fs.existsSync('/.dockerenv') || process.env.container === 'docker'; + + hasGpu = false; + try { + if (fs.existsSync('/dev/nvidia0') || process.env.NVIDIA_VISIBLE_DEVICES) { + hasGpu = true; + } + if (fs.existsSync('/dev/dri/card0')) { + hasGpu = true; + } + } catch (e) { + // Ignore errors in GPU detection } - } catch (e) { - // Ignore errors in GPU detection } // Disable GPU acceleration if: - // 1. In headless environment - // 2. In container without GPU passthrough - // 3. No GPU detected - // 4. Explicitly requested via environment variable + // 1. In headless environment (Linux) + // 2. In container without GPU passthrough (Linux) + // 3. No GPU detected (Linux) + // 4. Explicitly requested via environment variable (all platforms) const shouldDisableGpu = isHeadless || (isContainer && !hasGpu) || !hasGpu || process.env.GROK_DISABLE_GPU === 'true'; if (shouldDisableGpu) { @@ -223,7 +229,7 @@ if (!gotSingleInstanceLock) { const forcedLightWebContentsIds = new Set(); // Always On Top (AOT) functionality for cross-platform compatibility -// Windows: Uses Electron's built-in setAlwaysOnTop() method +// Windows/macOS: Uses Electron's built-in setAlwaysOnTop() method // Linux: Uses wmctrl command-line tool for better GNOME/Wayland compatibility // If running under Wayland on Linux, automatically restarts with X11 forced @@ -298,6 +304,47 @@ function toggleAlwaysOnTopLinux(mainWindow) { }); } +function createApplicationMenu() { + if (process.platform !== 'darwin') { + Menu.setApplicationMenu(null); + return; + } + + const template = [ + { + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + { role: 'editMenu' }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + { role: 'windowMenu' } + ]; + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + function createWindow() { // Create the browser window mainWindow = new BrowserWindow({ @@ -314,8 +361,10 @@ function createWindow() { icon: path.join(__dirname, 'grok.png') }); - // Disable the menu bar - Menu.setApplicationMenu(null); + createApplicationMenu(); + if (process.platform === 'darwin' && app.dock) { + try { app.dock.setIcon(path.join(__dirname, 'grok.png')); } catch (_) {} + } // Ensure shortcuts work when focus is on the main window UI try { attachShortcutHandlers(mainWindow.webContents); } catch (_) {} @@ -398,7 +447,7 @@ function createWindow() { // Enable right-click context menus setupContextMenus(); - // Set up keyboard shortcuts (Ctrl+T, Ctrl+Tab, Ctrl+R) + // Set up keyboard shortcuts (Ctrl/Cmd+T, Ctrl+Tab, Ctrl/Cmd+R) setupKeyboardShortcuts(); // Ensure newly created webContents/webviews get correct color scheme @@ -754,8 +803,8 @@ function attachShortcutHandlers(contents) { try { contents.on('before-input-event', (event, input) => { try { - // Only handle keyDown with Control on Windows/Linux - if (input.type !== 'keyDown' || !input.control) return; + // Control (Windows/Linux/Mac) or Command (Mac). Ctrl remains an alias on Mac. + if (input.type !== 'keyDown' || !(input.control || input.meta)) return; const key = input.key; // Deliver to the hosting window (handles webviews as well) @@ -763,37 +812,34 @@ function attachShortcutHandlers(contents) { const win = BrowserWindow.fromWebContents(host); if (!win || win.isDestroyed()) return; - // Ctrl+K -> Remap to Ctrl+Shift+K for grok.com search - // grok.com responds to Ctrl+Shift+K, not Ctrl+K (which Chromium intercepts for omnibox) + // Ctrl/Cmd+K -> Remap to Ctrl/Cmd+Shift+K for grok.com search if ((key === 'k' || key === 'K') && !input.shift) { event.preventDefault(); - // Send Ctrl+Shift+K to the webContents using sendInputEvent (creates trusted OS-level events) + const searchModifiers = input.control ? ['control', 'shift'] : ['meta', 'shift']; contents.sendInputEvent({ type: 'keyDown', keyCode: 'K', - modifiers: ['control', 'shift'] + modifiers: searchModifiers }); - // Send keyUp after a short delay setTimeout(() => { try { contents.sendInputEvent({ type: 'keyUp', keyCode: 'K', - modifiers: ['control', 'shift'] + modifiers: searchModifiers }); } catch (_) {} }, 10); return; } - // Don't intercept Ctrl+Shift+K - let it pass through naturally (it already works) - // Ctrl+T -> new tab + // Ctrl/Cmd+T -> new tab if (key === 't' || key === 'T') { event.preventDefault(); win.webContents.send('shortcut-new-tab'); return; } - // Ctrl+Tab -> next tab, Ctrl+Shift+Tab -> previous tab + // Ctrl+Tab / Ctrl+Shift+Tab cycle tabs (Cmd+Tab is the macOS app switcher) if (key === 'Tab') { event.preventDefault(); if (input.shift) { @@ -803,13 +849,13 @@ function attachShortcutHandlers(contents) { } return; } - // Ctrl+R -> reload active tab (override default window reload) + // Ctrl/Cmd+R -> reload active tab (override default window reload) if (key === 'r' || key === 'R') { event.preventDefault(); win.webContents.send('shortcut-reload-tab'); return; } - // Ctrl+I -> show information/about dialog + // Ctrl/Cmd+I -> show information/about dialog if (key === 'i' || key === 'I') { event.preventDefault(); win.webContents.send('shortcut-show-info');