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 (
+
+ );
+};
+
+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
-
+
+ {/* Show Seconds */}
+
+
+ {/* Date Format */}
+
+ Date Display
+
+
+
+ {/* Font Size */}
+
+ Font Size
+
+ {(Object.keys(FONT_SIZES) as FontSize[]).map((size) => (
+
+ ))}
+
+
+
+ {/* 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 */}
+
+
+ {/* 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);
}