diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2dbd0b4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +This is a **Tauri 2** desktop application (Rust backend + React 19/TypeScript frontend). A single command `pnpm tauri dev` starts both the Vite dev server (port 1420) and the native Tauri window. + +### Prerequisites (system-level, installed once in the VM snapshot) + +- **Tauri Linux system libraries**: `libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf` +- **Rust stable** (1.94+): The default rustup toolchain must be `stable`, not the older system-pinned `1.83.0`. Run `rustup default stable` if `cargo --version` shows < 1.94. +- **Node.js** (v18+) and **pnpm** (v9+): Already available via nvm. + +### Key commands + +| Task | Command | +|---|---| +| Install frontend deps | `pnpm install` | +| TypeScript lint/check | `pnpm exec tsc --noEmit` | +| Frontend build | `pnpm build` | +| Rust check | `cd src-tauri && cargo check` | +| Run dev (full app) | `pnpm tauri dev` | +| Production build | `pnpm tauri build` | + +### Gotchas + +- The first `cargo check` / `pnpm tauri dev` after a clean clone takes ~60s to compile all Rust dependencies. Subsequent builds are incremental and much faster. +- In headless/VM environments, expect `libEGL warning: DRI3 error` messages — these are benign and the app still renders correctly via software rendering. +- `pnpm tauri dev` auto-runs `pnpm dev` (Vite) as a `beforeDevCommand`; you don't need to start Vite separately. +- There are no automated test suites in this repository. Validation is done via TypeScript type checking (`tsc --noEmit`) and building the app. diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 3cec3bd..fbb81d0 100755 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,8 +1,8 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Capability for the main window", - "windows": ["main"], + "description": "Capability for the main window and settings window", + "windows": ["main", "settings"], "permissions": [ "core:default", "opener:default", @@ -16,6 +16,10 @@ "core:window:allow-inner-position", "core:window:allow-show", "core:window:allow-hide", + "core:window:allow-close", + "core:window:allow-set-focus", + "core:window:allow-center", + "core:webview:allow-create-webview-window", "core:app:allow-default-window-icon", "positioner:default", "store:default", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6ca5c3e..47eb6f9 100755 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,6 +12,7 @@ "app": { "windows": [ { + "label": "main", "title": "Clock", "decorations": false, "transparent": true, diff --git a/src/Clock.tsx b/src/Clock.tsx index 8501669..751e1fe 100755 --- a/src/Clock.tsx +++ b/src/Clock.tsx @@ -60,10 +60,11 @@ const Clock = () => { return (
{ {date && ( { + const handleClose = async () => { + const win = getCurrentWindow(); + await win.close(); + }; + + return ( +
+
+

Settings

+ +
+ +
+ ); +}; + +const SettingsWindow: React.FC = () => { + return ( + + + + ); +}; + +export default SettingsWindow; diff --git a/src/TitleBar.tsx b/src/TitleBar.tsx index 95eea72..afc51dc 100755 --- a/src/TitleBar.tsx +++ b/src/TitleBar.tsx @@ -1,18 +1,41 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; +import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import React, { useState, useEffect, useRef } from "react"; import { TrayIcon } from '@tauri-apps/api/tray'; import { defaultWindowIcon } from "@tauri-apps/api/app"; import { Menu } from '@tauri-apps/api/menu'; import { Clock1, PinIcon, Settings as SettingsIcon } from "lucide-react"; -import Settings from "./components/Settings"; -interface TitleBarProps { - onSettingsOpen?: () => void; +async function openSettingsWindow() { + const existing = await WebviewWindow.getByLabel('settings'); + if (existing) { + await existing.setFocus(); + return; + } + + const mainWin = getCurrentWindow(); + const pos = await mainWin.innerPosition(); + const size = await mainWin.innerSize(); + + const webview = new WebviewWindow('settings', { + url: '/', + title: 'Settings', + width: 420, + height: 700, + x: pos.x + size.width + 16, + y: pos.y, + resizable: false, + decorations: false, + transparent: false, + }); + + webview.once('tauri://error', (e) => { + console.error('Failed to create settings window:', e); + }); } -const TitleBar: React.FC = () => { +const TitleBar: React.FC = () => { const [onTop, setOnTop] = useState(null); - const [settingsOpen, setSettingsOpen] = useState(false); const trayRef = useRef(null); useEffect(() => { @@ -39,8 +62,9 @@ const TitleBar: React.FC = () => { { id: 'settings', text: 'Settings', - action: () => { - setTop(false).then(() => setSettingsOpen(true)); + action: async () => { + await setTop(false); + await openSettingsWindow(); }, }, { @@ -90,22 +114,23 @@ const TitleBar: React.FC = () => { ) : onTop ? null : (
-
+
Clock
-
+
)} - setSettingsOpen(false)} /> ); }; diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 63aa9f1..5a11b7d 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,18 +1,10 @@ import React from 'react'; -import { X } from 'lucide-react'; import { useSettings } from '../contexts/SettingsContext'; import { THEMES, FONT_SIZES, TimeFormat, DateFormat, FontSize } from '../types/settings'; -interface SettingsProps { - isOpen: boolean; - onClose: () => void; -} - -const Settings: React.FC = ({ isOpen, onClose }) => { +const Settings: React.FC = () => { const { settings, updateSettings, applyTheme } = useSettings(); - if (!isOpen) return null; - const handleTimeFormatChange = (format: TimeFormat) => { updateSettings({ timeFormat: format }); }; @@ -38,207 +30,195 @@ const Settings: React.FC = ({ isOpen, onClose }) => { } }; + const sectionStyle: React.CSSProperties = { padding: '20px 28px', borderBottom: '2px solid #444' }; + const lastSectionStyle: React.CSSProperties = { padding: '20px 28px' }; + return ( -
-
-
-

Settings

+
+ {/* Time Format */} +
+

Time Format

+
+
- -
- {/* Time Format */} -
-

Time Format

-
- - -
-
- - {/* Show Seconds */} -
- -
- - {/* Date Format */} -
-

Date Display

- updateSettings({ showSeconds: e.target.checked })} + className="w-5 h-5 rounded bg-gray-700 border-gray-600 text-blue-600 focus:ring-blue-500" + /> + +
+ + {/* Date Format */} +
+

Date Display

+ +
+ + {/* Font Size */} +
+

Font Size

+
+ {(Object.keys(FONT_SIZES) as FontSize[]).map((size) => ( +
- - {/* Font Size */} -
-

Font Size

-
- {(Object.keys(FONT_SIZES) as FontSize[]).map((size) => ( - - ))} -
-
- - {/* Theme */} -
-

Theme

-
- {THEMES.map((theme) => ( - - ))} -
-
- - {/* Custom Colors (only when custom theme is selected) */} - {settings.activeTheme === 'custom' && ( -
-

Custom Colors

-
- Text Color - updateSettings({ textColor: e.target.value })} - className="w-10 h-8 rounded cursor-pointer bg-transparent" - /> -
-
- Background Color - updateSettings({ backgroundColor: e.target.value })} - className="w-10 h-8 rounded cursor-pointer bg-transparent" - /> -
-
- )} - - {/* Opacity */} -
-

Opacity

-
-
- Background - {Math.round(settings.backgroundOpacity * 100)}% -
- updateSettings({ backgroundOpacity: parseFloat(e.target.value) })} - className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" - /> -
-
-
- Text - {Math.round(settings.textOpacity * 100)}% -
- updateSettings({ textOpacity: parseFloat(e.target.value) })} - className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" + {size} + + ))} +
+
+ + {/* Theme */} +
+

Theme

+
+ {THEMES.map((theme) => ( +
-
- - {/* Global Shortcut */} -
-

Global Shortcut

+ {theme.name} + + ))} +
+
+ + {/* Custom Colors (only when custom theme is selected) */} + {settings.activeTheme === 'custom' && ( +
+

Custom Colors

+
+ Text Color updateSettings({ globalShortcut: e.target.value })} - placeholder="e.g., CommandOrControl+Shift+C" - className="w-full py-2 px-3 rounded bg-gray-700 text-gray-200 border border-gray-600 focus:outline-none focus:border-blue-500 text-sm" + type="color" + value={settings.textColor} + onChange={(e) => updateSettings({ textColor: e.target.value })} + className="w-10 h-8 rounded cursor-pointer bg-transparent" /> -

Toggle window visibility

-
- - {/* Launch on Startup */} -
- -
+
+
+ Background Color + updateSettings({ backgroundColor: e.target.value })} + className="w-10 h-8 rounded cursor-pointer bg-transparent" + /> +
+ + )} + + {/* Opacity */} +
+

Opacity

+
+
+ Background + {Math.round(settings.backgroundOpacity * 100)}% +
+ updateSettings({ backgroundOpacity: parseFloat(e.target.value) })} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" + /> +
+
+
+ Text + {Math.round(settings.textOpacity * 100)}% +
+ updateSettings({ textOpacity: parseFloat(e.target.value) })} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" + />
-
+ + + {/* Global Shortcut */} +
+

Global Shortcut

+ updateSettings({ globalShortcut: e.target.value })} + placeholder="e.g., CommandOrControl+Shift+C" + className="w-full py-2.5 px-4 rounded-lg bg-gray-700 text-gray-200 border border-gray-600 focus:outline-none focus:border-blue-500 text-sm" + /> +

Toggle window visibility

+
+ + {/* Launch on Startup */} +
+ +
); }; diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx index 72db77a..3493849 100644 --- a/src/contexts/SettingsContext.tsx +++ b/src/contexts/SettingsContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { Settings, DEFAULT_SETTINGS, THEMES } from '../types/settings'; -import { loadSettings, saveSettings } from '../utils/storage'; +import { loadSettings, saveSettings, getStore } from '../utils/storage'; import { enable, disable, isEnabled } from '@tauri-apps/plugin-autostart'; import { register, unregister } from '@tauri-apps/plugin-global-shortcut'; import { getCurrentWindow } from '@tauri-apps/api/window'; @@ -45,6 +45,27 @@ export function SettingsProvider({ children }: SettingsProviderProps) { useEffect(() => { if (isLoading) return; + let unlisten: (() => void) | null = null; + + getStore().then((store) => { + store.onKeyChange>('settings', (newValue) => { + if (newValue) { + setSettings({ ...DEFAULT_SETTINGS, ...newValue }); + } + }).then((fn) => { + unlisten = fn; + }); + }); + + return () => { + unlisten?.(); + }; + }, [isLoading]); + + useEffect(() => { + if (isLoading) return; + if (getCurrentWindow().label !== 'main') return; + const setupAutostart = async () => { const enabled = await isEnabled(); if (settings.launchOnStartup && !enabled) { @@ -59,6 +80,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) { useEffect(() => { if (isLoading || !settings.globalShortcut) return; + if (getCurrentWindow().label !== 'main') return; const setupShortcut = async () => { try { diff --git a/src/main.tsx b/src/main.tsx index 6d966bd..88dff55 100755 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,13 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import App from "./App"; +import SettingsWindow from "./SettingsWindow"; + +const windowLabel = getCurrentWindow().label; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + {windowLabel === "settings" ? : } , ); diff --git a/src/utils/storage.ts b/src/utils/storage.ts index d1c9487..6079a4c 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -4,7 +4,7 @@ import { Settings, DEFAULT_SETTINGS } from '../types/settings'; const STORE_NAME = 'settings.json'; let store: Store | null = null; -async function getStore(): Promise { +export async function getStore(): Promise { if (!store) { store = await load(STORE_NAME); }