diff --git a/README.md b/README.md index 9b0422df..93d7e66a 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,18 @@ The built-in Web UI at `http://127.0.0.1:8848` provides: |:----:|:-------:| | ![Chat](screenshots/chat.png) | ![Terminal](screenshots/terminal.png) | +### Desktop app (macOS · Windows · Linux) + +A native desktop wrapper is available in [`desktop/`](desktop/). It packages +the Web UI in a Tauri v2 shell with a transparent, chrome-free title bar and +native window drag — no browser required. + +```bash +cd desktop && npm install && npm run build +``` + +See [`desktop/README.md`](desktop/README.md) for full setup instructions. + ### Remote access For accessing the Web UI from outside localhost: diff --git a/README.zh-CN.md b/README.zh-CN.md index 9305edbb..15ce7b4e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -236,6 +236,17 @@ CCCC 实现的是 IM 级消息语义,而不是"往终端里粘贴一段文字" |:----:|:----:| | ![Chat](screenshots/chat.png) | ![Terminal](screenshots/terminal.png) | +### 桌面客户端(macOS · Windows · Linux) + +[`desktop/`](desktop/) 目录提供原生桌面套壳,基于 Tauri v2。 +无需打开浏览器,支持透明无边框标题栏与原生窗口拖动。 + +```bash +cd desktop && npm install && npm run build +``` + +详细说明见 [`desktop/README.zh-CN.md`](desktop/README.zh-CN.md)。 + ### 远程访问 从外部访问 Web UI: diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 00000000..e7313098 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +src-tauri/target/ +src-tauri/gen/ diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 00000000..63e0a784 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,142 @@ +# CCCC Desktop + +A native desktop wrapper for the CCCC web UI, built with [Tauri v2](https://tauri.app/). + +> **Status**: Community contribution — macOS tested and working. Windows / Linux +> builds should work but have not been verified yet. + +## What this adds + +| Feature | Detail | +|---------|--------| +| Native `.app` / `.exe` | No browser required — launch CCCC like any other desktop app | +| Transparent title bar (macOS) | `titleBarStyle: Overlay` gives a seamless, chrome-free look | +| Native window drag | Uses `NSWindow.setMovableByWindowBackground` so the entire top bar is draggable, even though the UI is served from a remote origin (`localhost:8848`) | +| Packaged DMG | `tauri build` produces a signed-ready `.app` bundle and `.dmg` installer | + +### Screenshot + +> The transparent title bar blends into the CCCC dark UI — no grey chrome, no +> separate title-bar colour mismatch. + +![cccc desktop](../screenshots/desktop-macos.png) + +--- + +## Prerequisites + +| Tool | Version | +|------|---------| +| [Rust](https://rustup.rs/) | 1.77+ | +| [Node.js](https://nodejs.org/) | 18+ | +| [CCCC daemon](https://github.com/ChesterRa/cccc) | running on `localhost:8848` | + +macOS also needs Xcode Command Line Tools: + +```bash +xcode-select --install +``` + +--- + +## Quick start + +```bash +# 1. Install the Tauri CLI +cd desktop +npm install + +# 2. Make sure the CCCC daemon is running first +cccc # or: cccc daemon start + +# 3. Launch in dev mode (hot-reload from localhost:8848) +npm run dev + +# 4. Build a release bundle +npm run build +# → desktop/src-tauri/target/release/bundle/macos/cccc.app +# → desktop/src-tauri/target/release/bundle/dmg/cccc_x.y.z_x64.dmg +``` + +--- + +## How it works + +The wrapper is a **pure shell** — it contains no frontend code of its own. +`tauri.conf.json` points both the dev URL and the release URL at +`http://localhost:8848/ui/`, so the Tauri webview simply renders whatever +the CCCC daemon serves. + +### Transparent title bar + drag (macOS) + +macOS's `titleBarStyle: Overlay` hides the default chrome and exposes the +traffic-light buttons over the content area. Because the webview loads a +**remote** origin, Tauri's `data-tauri-drag-region` attribute and the JS +`startDragging()` API are both blocked by the renderer sandbox. We work +around this with two layers: + +1. **CSS shim** (injected via `on_page_load`): a 28 px transparent overlay div + is appended to every page so the UI body is pushed down below the + traffic-light buttons. + +2. **Native NSWindow API** (Rust `setup`): `setMovableByWindowBackground: YES` + tells macOS to treat any unobstructed window background pixel as a drag + handle, which works regardless of content origin. + +``` +src-tauri/src/lib.rs + ├── INJECT_SCRIPT — CSS shim injected on every page load + └── setup() + └── #[cfg(target_os = "macos")] + └── NSWindow::setMovableByWindowBackground(true) +``` + +--- + +## Configuration + +All Tauri settings live in `src-tauri/tauri.conf.json`. The most likely +things you might want to change: + +```jsonc +{ + "build": { + // Change if your CCCC daemon runs on a different port + "devUrl": "http://localhost:8848/ui/" + }, + "app": { + "windows": [{ + "width": 1280, // initial window size + "height": 800, + "minWidth": 900, + "minHeight": 600 + }] + } +} +``` + +--- + +## Project layout + +``` +desktop/ +├── package.json npm wrapper (only @tauri-apps/cli) +├── src-tauri/ +│ ├── Cargo.toml Rust crate (cccc-desktop) +│ ├── tauri.conf.json Tauri app configuration +│ ├── capabilities/ +│ │ └── default.json permission set +│ ├── icons/ app icons (all sizes + .icns / .ico) +│ └── src/ +│ ├── main.rs binary entry point +│ └── lib.rs Tauri builder + shim injection +└── README.md this file +``` + +--- + +## Contributing + +Issues and PRs welcome. If you verify the wrapper on **Windows** or +**Linux**, please open an issue or PR to update this README with your findings. diff --git a/desktop/README.zh-CN.md b/desktop/README.zh-CN.md new file mode 100644 index 00000000..6f3bacb0 --- /dev/null +++ b/desktop/README.zh-CN.md @@ -0,0 +1,84 @@ +# CCCC Desktop(桌面端) + +基于 [Tauri v2](https://tauri.app/) 构建的 CCCC Web UI 原生桌面套壳。 + +> **状态**:社区贡献 — macOS 已测试可用。Windows / Linux 构建理论可行,尚未验证。 + +## 新增功能 + +| 功能 | 说明 | +|------|------| +| 原生 `.app` / `.exe` | 无需打开浏览器,像普通桌面应用一样启动 CCCC | +| 透明标题栏(macOS) | `titleBarStyle: Overlay`,界面无边框,视觉更简洁 | +| 原生窗口拖动 | 通过 `NSWindow.setMovableByWindowBackground` 实现,即使 UI 由远程来源(`localhost:8848`)提供服务也能正常拖动 | +| 打包 DMG | `tauri build` 生成可签名的 `.app` 包和 `.dmg` 安装包 | + +--- + +## 前置条件 + +| 工具 | 版本 | +|------|------| +| [Rust](https://rustup.rs/) | 1.77+ | +| [Node.js](https://nodejs.org/) | 18+ | +| [CCCC 守护进程](https://github.com/ChesterRa/cccc) | 运行于 `localhost:8848` | + +macOS 还需要 Xcode 命令行工具: + +```bash +xcode-select --install +``` + +--- + +## 快速开始 + +```bash +# 1. 安装 Tauri CLI +cd desktop +npm install + +# 2. 先确保 CCCC 守护进程正在运行 +cccc # 或: cccc daemon start + +# 3. 开发模式启动(热更新来自 localhost:8848) +npm run dev + +# 4. 构建发布包 +npm run build +# → desktop/src-tauri/target/release/bundle/macos/cccc.app +# → desktop/src-tauri/target/release/bundle/dmg/cccc_x.y.z_x64.dmg +``` + +--- + +## 工作原理 + +本套壳是一个**纯壳**——自身不包含任何前端代码。`tauri.conf.json` 将开发 URL 和发布 URL 都指向 `http://localhost:8848/ui/`,Tauri webview 直接渲染 CCCC 守护进程提供的页面。 + +### 透明标题栏 + 拖动(macOS) + +macOS 的 `titleBarStyle: Overlay` 会隐藏默认的窗口边框,并在内容区域上方叠加红绿灯按钮。由于 webview 加载的是**远程来源**,Tauri 的 `data-tauri-drag-region` 属性和 JS `startDragging()` API 都会被渲染器沙箱阻断。我们通过两层机制解决: + +1. **CSS 垫片**(通过 `on_page_load` 注入):在每个页面追加一个 28px 透明覆盖 div,让 UI body 向下偏移,避开红绿灯按钮区域。 + +2. **原生 NSWindow API**(Rust `setup`):`setMovableByWindowBackground: YES` 告知 macOS 将所有未被遮挡的窗口背景像素视为拖动区域,不受内容来源限制。 + +--- + +## 项目结构 + +``` +desktop/ +├── package.json npm 包装器(仅含 @tauri-apps/cli) +├── src-tauri/ +│ ├── Cargo.toml Rust crate(cccc-desktop) +│ ├── tauri.conf.json Tauri 应用配置 +│ ├── capabilities/ +│ │ └── default.json 权限集 +│ ├── icons/ 应用图标(各尺寸 + .icns / .ico) +│ └── src/ +│ ├── main.rs 可执行文件入口 +│ └── lib.rs Tauri builder + 垫片注入 +└── README.md 英文文档 +``` diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 00000000..7b85bfa5 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,14 @@ +{ + "name": "cccc-desktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "tauri": "tauri", + "build": "tauri build", + "dev": "tauri dev" + }, + "devDependencies": { + "@tauri-apps/cli": "^2" + } +} diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 00000000..c7da8c21 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cccc-desktop" +version = "0.1.0" +description = "Native desktop wrapper for the CCCC web UI (Tauri v2)" +authors = [] +license = "Apache-2.0" +edition = "2021" +rust-version = "1.77.2" + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-log = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +log = "0.4" + +# macOS-only: native NSWindow drag support for remote-origin webviews +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 00000000..795b9b7c --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 00000000..ad30350f --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "core:window:allow-start-dragging" + ] +} diff --git a/desktop/src-tauri/capabilities/remote-localhost.json b/desktop/src-tauri/capabilities/remote-localhost.json new file mode 100644 index 00000000..f162cece --- /dev/null +++ b/desktop/src-tauri/capabilities/remote-localhost.json @@ -0,0 +1,10 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "remote-localhost", + "description": "Allow startDragging IPC from localhost daemon UI", + "remote": { + "urls": ["http://localhost:8848/**"] + }, + "windows": ["main"], + "permissions": ["core:window:allow-start-dragging"] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 00000000..cdd7f479 Binary files /dev/null and b/desktop/src-tauri/icons/128x128.png differ diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..ee5ca243 Binary files /dev/null and b/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png new file mode 100644 index 00000000..cccbeab6 Binary files /dev/null and b/desktop/src-tauri/icons/32x32.png differ diff --git a/desktop/src-tauri/icons/64x64.png b/desktop/src-tauri/icons/64x64.png new file mode 100644 index 00000000..e54c8420 Binary files /dev/null and b/desktop/src-tauri/icons/64x64.png differ diff --git a/desktop/src-tauri/icons/Square107x107Logo.png b/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..46149e67 Binary files /dev/null and b/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/desktop/src-tauri/icons/Square142x142Logo.png b/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..6e9db2e7 Binary files /dev/null and b/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/desktop/src-tauri/icons/Square150x150Logo.png b/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..4ae8d4a4 Binary files /dev/null and b/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/desktop/src-tauri/icons/Square284x284Logo.png b/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..0a2f97f1 Binary files /dev/null and b/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/desktop/src-tauri/icons/Square30x30Logo.png b/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..31f396e3 Binary files /dev/null and b/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/desktop/src-tauri/icons/Square310x310Logo.png b/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..865a7ff7 Binary files /dev/null and b/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/desktop/src-tauri/icons/Square44x44Logo.png b/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..36acfe41 Binary files /dev/null and b/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/desktop/src-tauri/icons/Square71x71Logo.png b/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..6b9e396f Binary files /dev/null and b/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/desktop/src-tauri/icons/Square89x89Logo.png b/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..6ff30292 Binary files /dev/null and b/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/desktop/src-tauri/icons/StoreLogo.png b/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..f19d9da4 Binary files /dev/null and b/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 00000000..caa71179 Binary files /dev/null and b/desktop/src-tauri/icons/icon.icns differ diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 00000000..33e762f0 Binary files /dev/null and b/desktop/src-tauri/icons/icon.ico differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 00000000..adb63cde Binary files /dev/null and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 00000000..05aa56aa --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,48 @@ +/// Inject a lightweight drag-region shim into every page loaded inside the +/// webview. We cannot rely on `data-tauri-drag-region` here because the web UI +/// is served from a remote origin (`http://localhost:8848`), and Tauri only +/// honors that attribute for locally bundled content. Instead we inject a thin +/// transparent overlay div at the top of the page and use the macOS-native +/// `setMovableByWindowBackground` API as the primary drag mechanism. +const INJECT_SCRIPT: &str = r#" + (function () { + if (document.getElementById('_tauri_drag_region')) return; + var s = document.createElement('style'); + s.textContent = [ + 'body { padding-top: 28px !important; box-sizing: border-box; }', + '#_tauri_drag_region {', + ' position: fixed; top: 0; left: 0; right: 0; height: 28px;', + ' z-index: 999999; cursor: default;', + '}' + ].join(''); + document.head.appendChild(s); + var d = document.createElement('div'); + d.id = '_tauri_drag_region'; + d.addEventListener('mousedown', function (e) { + if (e.buttons !== 1 || !window.__TAURI__?.window?.getCurrentWindow) return; + window.__TAURI__.window.getCurrentWindow().startDragging(); + }); + document.body.appendChild(d); + })(); +"#; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .on_page_load(|window, _payload| { + let _ = window.eval(INJECT_SCRIPT); + }) + .setup(|app| { + if cfg!(debug_assertions) { + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + } + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 00000000..183488be --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,7 @@ +// Prevents an additional console window on Windows in release mode. +// DO NOT REMOVE this attribute. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + app_lib::run(); +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 00000000..776f4377 --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,41 @@ +{ + "$schema": "./node_modules/@tauri-apps/cli/config.schema.json", + "productName": "cccc", + "version": "0.1.0", + "identifier": "com.cccc.app", + "build": { + "devUrl": "http://localhost:8848/ui/", + "beforeDevCommand": "" + }, + "app": { + "windows": [ + { + "title": "cccc", + "width": 1280, + "height": 800, + "resizable": true, + "fullscreen": false, + "minWidth": 900, + "minHeight": 600, + "url": "http://localhost:8848/ui/", + "titleBarStyle": "Overlay", + "hiddenTitle": true + } + ], + "withGlobalTauri": true, + "security": { + "csp": "default-src 'self' http://localhost:8848; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:8848; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com http://localhost:8848; font-src 'self' https://fonts.gstatic.com http://localhost:8848; img-src 'self' data: http://localhost:8848; connect-src 'self' http://localhost:8848 ws://localhost:8848" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +}