diff --git a/.github/workflows/build-gpui.yaml b/.github/workflows/build-gpui.yaml
new file mode 100644
index 0000000..106cb48
--- /dev/null
+++ b/.github/workflows/build-gpui.yaml
@@ -0,0 +1,133 @@
+---
+#-------------------------------------------------------------------------------
+# Workflow configuration for GPUI desktop app
+#-------------------------------------------------------------------------------
+
+name: "GPUI Desktop Build"
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/build-gpui.yaml"
+ - "Cargo.toml"
+ - "crates/wsrx-desktop-gpui/**"
+ workflow_dispatch:
+
+env:
+ CARGO_TERM_COLOR: always
+
+#-------------------------------------------------------------------------------
+# Workflow jobs
+#-------------------------------------------------------------------------------
+
+jobs:
+ build-linux:
+ name: "Build GPUI Desktop on Linux"
+ runs-on: ubuntu-22.04
+ steps:
+ # Checkout repository
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ # Get version
+ - name: Get git version
+ id: git_tag_version
+ run: |
+ export BUILD_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0-dev")
+ echo "Build at version $BUILD_VERSION"
+ echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_OUTPUT
+
+ # Install dependencies
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libxcb1-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libgl1-mesa-dev
+
+ # Build application
+ - name: Build GPUI desktop application
+ run: |
+ rustup update stable && rustup default stable
+ cargo build --release -p wsrx-desktop-gpui
+
+ # Upload package
+ - name: Upload package
+ uses: actions/upload-artifact@v4
+ with:
+ name: wsrx-desktop-gpui-${{steps.git_tag_version.outputs.BUILD_VERSION}}-linux-x64
+ path: target/release/wsrx-desktop-gpui
+ compression-level: 6
+
+ build-windows:
+ name: "Build GPUI Desktop on Windows"
+ runs-on: windows-2022
+ steps:
+ # Checkout repository
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ # Get version
+ - name: Get git version
+ id: git_tag_version
+ run: |
+ echo BUILD_VERSION=$(git describe --tags --abbrev=0 2>$null) | Out-File -FilePath $env:GITHUB_OUTPUT -Append
+ if ($LASTEXITCODE -ne 0) {
+ echo "BUILD_VERSION=v0.0.0-dev" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
+ }
+
+ - name: Install NASM for aws-lc-rs
+ uses: ilammy/setup-nasm@v1
+
+ - name: Install ninja-build
+ uses: seanmiddleditch/gha-setup-ninja@v5
+
+ # Build application
+ - name: Build GPUI desktop application
+ run: |
+ rustup update stable && rustup default stable
+ cargo build --release -p wsrx-desktop-gpui
+
+ # Upload package
+ - name: Upload package
+ uses: actions/upload-artifact@v4
+ with:
+ name: wsrx-desktop-gpui-${{steps.git_tag_version.outputs.BUILD_VERSION}}-windows-x64
+ path: target/release/wsrx-desktop-gpui.exe
+ compression-level: 6
+
+ build-mac:
+ name: "Build GPUI Desktop on MacOS"
+ runs-on: macos-latest
+ steps:
+ # Checkout repository
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ # Get version
+ - name: Get git version
+ id: git_tag_version
+ run: |
+ export BUILD_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0-dev")
+ echo "Build at version $BUILD_VERSION"
+ echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_OUTPUT
+
+ # Build for ARM64
+ - name: Build GPUI desktop for ARM64
+ run: |
+ rustup update stable && rustup default stable
+ cargo build --release -p wsrx-desktop-gpui
+
+ # Upload ARM64 package
+ - name: Upload ARM64 package
+ uses: actions/upload-artifact@v4
+ with:
+ name: wsrx-desktop-gpui-${{steps.git_tag_version.outputs.BUILD_VERSION}}-macos-arm64
+ path: target/release/wsrx-desktop-gpui
+ compression-level: 6
diff --git a/crates/wsrx-desktop-gpui/Cargo.toml b/crates/wsrx-desktop-gpui/Cargo.toml
index aec624d..b000485 100644
--- a/crates/wsrx-desktop-gpui/Cargo.toml
+++ b/crates/wsrx-desktop-gpui/Cargo.toml
@@ -50,6 +50,9 @@ chrono = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
+# Internationalization
+rust-i18n = "3"
+
# Network
axum = { workspace = true }
reqwest = { workspace = true }
@@ -65,6 +68,7 @@ build-target = { workspace = true }
git-version = { workspace = true }
rustc_version = { workspace = true }
winres = { workspace = true }
+rust-i18n = "3"
[[bin]]
name = "wsrx-desktop-gpui"
diff --git a/crates/wsrx-desktop-gpui/IMPLEMENTATION_CHECKLIST.md b/crates/wsrx-desktop-gpui/IMPLEMENTATION_CHECKLIST.md
new file mode 100644
index 0000000..0e8e8f4
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/IMPLEMENTATION_CHECKLIST.md
@@ -0,0 +1,211 @@
+# GPUI vs Slint Implementation Checklist
+
+This document compares the GPUI implementation with the original Slint implementation to track progress and identify gaps.
+
+## Design System / Styling
+
+### Colors & Theming
+- [x] **Dark mode palette** - Fully aligned with Slint colors
+ - [x] Window foreground (#cdd6f4)
+ - [x] Window background (#151515)
+ - [x] Window alternate background (#1e1e1e)
+ - [x] Primary background (#0078D6)
+ - [x] Border colors (window, element, popup)
+ - [x] Semantic colors (error, warning, success, info, debug)
+ - [x] Layer colors (layer-1 through layer-5)
+- [ ] **Light mode palette** - Not yet implemented (Slint has it)
+- [ ] **Theme switching** - No auto-detect or toggle mechanism yet
+
+### Typography
+- [x] **Font sizes** - Aligned with Slint sizing (16px base)
+ - [x] XS (12px), SM (14px), Base (16px), LG (18px), XL (20px), 2XL (24px)
+- [ ] **Font family** - Not set (Slint uses "Reverier Mono")
+- [ ] **Font weight variations** - Not implemented
+
+### Spacing System
+- [x] **Padding constants** (p-xs through p-xl) - Fully aligned
+- [x] **Spacing constants** (s-xs through s-xl) - Fully aligned
+- [x] **Border radius** (r-xs through r-xl) - Implemented
+- [x] **Height constants** (h-xs through h-xl) - Implemented
+- [ ] **Line height** - Not explicitly defined
+
+### Animations & Transitions
+- [ ] **Duration constants** - Not implemented (Slint has short/mid/long)
+- [ ] **Easing functions** - Not implemented
+- [ ] **Animated transitions** - Not implemented (Slint has smooth transitions)
+
+## Layout & Structure
+
+### Main Window
+- [x] **Root view** - Implemented with Entity management
+- [x] **Sidebar** - Implemented with navigation
+- [x] **Main content area** - Implemented with page switching
+- [ ] **Frameless window** - Not implemented (Slint has custom window chrome)
+- [ ] **Window controls** - Placeholder only (minimize, maximize, close)
+- [ ] **Title bar** - Placeholder only
+
+### Sidebar
+- [x] **Navigation tabs** - Implemented with 4 pages
+- [x] **Active state indicator** - Implemented with left border + highlight
+- [x] **Hover effects** - Implemented
+- [ ] **Logo/branding** - Not displayed
+- [ ] **Icons** - Not implemented (Slint uses SVG icons)
+- [ ] **Scope selector** - Not implemented (Slint has scope dropdown)
+- [ ] **System info display** - Not implemented in sidebar
+
+## Pages / Views
+
+### Get Started Page
+- [x] **Basic structure** - Placeholder implemented
+- [ ] **Welcome message** - Not styled/detailed
+- [ ] **Update notification** - Not implemented
+- [ ] **Quick actions** - Not implemented
+- [ ] **Onboarding content** - Not implemented
+
+### Connections Page
+- [x] **Tunnel list** - Basic structure implemented
+- [x] **Empty state** - Implemented
+- [x] **Status indicators** - Color-coded dots
+- [x] **Add tunnel button** - Implemented (no functionality)
+- [ ] **Tunnel cards styling** - Basic, needs polish
+- [ ] **Edit/Delete actions** - Not implemented
+- [ ] **Enable/Disable toggle** - Not implemented
+- [ ] **Connection statistics** - Not displayed
+- [ ] **Scope filtering** - Not implemented
+
+### Network Logs Page
+- [x] **Log display** - Basic list implemented
+- [x] **Severity color coding** - Implemented (DEBUG, INFO, WARN, ERROR)
+- [x] **Clear button** - Implemented
+- [ ] **Log filtering** - Not implemented
+- [ ] **Auto-scroll toggle** - Not implemented
+- [ ] **Log export** - Not implemented
+- [ ] **Timestamp formatting** - Basic string display
+- [ ] **Search/filter** - Not implemented
+
+### Settings Page
+- [x] **Settings sections** - Basic structure implemented
+- [x] **Settings display** - Read-only display
+- [ ] **Interactive controls** - Not implemented (toggles, selects)
+- [ ] **Daemon settings** - Display only
+- [ ] **Theme toggle** - Not implemented
+- [ ] **Log level selector** - Not implemented
+- [ ] **Save/Apply buttons** - Not implemented
+- [ ] **Settings persistence** - Bridge exists but not connected
+
+## Components Library
+
+### Implemented Components
+- [x] **Button** - With variants (Primary, Secondary, Danger) - *needs Zed-style refactor*
+- [x] **IconButton** - Icon-only button with styles (Subtle, Filled, Danger) - *NEW*
+- [x] **Checkbox** - Interactive checkbox with label - *NEW*
+- [x] **Modal** - Dialog overlay with backdrop
+- [x] **Input** - Text input with placeholder (not fully functional)
+- [x] **StatusIndicator** - Color-coded status dots
+
+### Missing Components (Slint has)
+- [ ] **ButtonIndicator** - Button with active state indicator
+- [ ] **LineEdit** - Functional text input with editing
+- [ ] **ScrollView** - Scrollable container
+- [x] **ComboBox/Select** - Dropdown selection
+- [ ] **Tab control** - Tabbed interface
+- [ ] **Progress bar** - Loading indicator
+- [ ] **Tooltip** - Hover info display
+
+**Total**: 8 of 14 components implemented (57%)
+
+### Component Improvements Needed
+- [x] **Add prelude module** - Export common types and traits - *DONE*
+- [x] **Refactor Button** - Follow Zed's pattern with traits (Clickable, Disableable, etc.) - *DONE*
+- [x] **Add component traits** - Clickable, Disableable, Fixed, StyledExt, Toggleable - *DONE*
+- [ ] **Improve styling** - Use consistent spacing/color helpers
+
+## Bridge Layer / Integration
+
+### Implemented Bridges
+- [x] **DaemonBridge** - Basic structure (no actual daemon control)
+- [x] **SettingsBridge** - TOML persistence (not connected)
+- [x] **SystemInfoBridge** - CPU/memory monitoring (not displayed)
+
+### Missing Functionality
+- [ ] **Daemon start/stop** - Not functional
+- [ ] **Tunnel management** - Not connected to wsrx core
+- [ ] **Real-time log streaming** - Not implemented
+- [ ] **Settings load/save UI** - Not wired up
+- [ ] **System info display** - Bridge exists but not shown
+- [ ] **WebSocket communication** - Not implemented
+- [ ] **Scope management** - Not implemented
+
+## Internationalization
+- [ ] **i18n support** - Not implemented (Slint has @tr() macros)
+- [ ] **Language switching** - Not implemented
+- [ ] **Translation files** - Not created
+
+## Platform-Specific Features
+
+### macOS
+- [x] **Build configuration** - Fixed linker flag issue
+- [ ] **Custom window chrome** - Not implemented
+- [ ] **Title bar handling** - Not implemented
+- [ ] **DMG packaging** - Not set up for GPUI app
+
+### Windows
+- [x] **Build configuration** - Working with NASM/Ninja
+- [ ] **NSIS installer** - Not set up for GPUI app
+- [ ] **Portable package** - Not set up
+- [ ] **Window chrome** - Not implemented
+
+### Linux
+- [x] **Build configuration** - Working with X11 dependencies
+- [ ] **AppImage** - Not set up for GPUI app
+- [ ] **Desktop file** - Not created
+- [ ] **Window chrome** - Not implemented
+
+## Build & Deployment
+- [x] **GitHub workflow** - Created for Linux/Windows/macOS
+- [x] **macOS build fix** - Removed unsupported linker flag
+- [ ] **Artifact generation** - Workflow ready but untested
+- [ ] **Release automation** - Not configured
+- [ ] **Code signing** - Not configured
+- [ ] **Update mechanism** - Not implemented
+
+## Code Quality & Patterns
+
+### Following Zed Patterns
+- [x] **Component prelude** - Implemented with common imports - *DONE*
+- [ ] **Component traits** - Not implemented (Clickable, Disableable, etc.)
+- [ ] **Styled extensions** - Partial (could be more comprehensive)
+- [ ] **Builder patterns** - Basic (needs enhancement)
+- [ ] **Documentation** - Minimal (Zed has extensive docs)
+
+### Needed Improvements
+- [ ] **Refactor Button** - Use Zed's ButtonLike + traits pattern
+- [ ] **Add animation helpers** - Duration, easing, direction
+- [ ] **Color system** - Add Color enum like Zed
+- [ ] **Spacing helpers** - h_flex, v_flex, h_group, v_group
+- [ ] **Typography traits** - StyledTypography trait
+
+## Summary Statistics
+
+**Overall Progress**: ~50% complete (up from 48%)
+
+### By Category:
+- **Design System**: 60% (colors good, animations missing)
+- **Layout**: 65% (structure done, window controls working)
+- **Pages**: 40% (structure exists, functionality missing)
+- **Components**: 50% (7/14 components, prelude added)
+- **Bridges**: 40% (structure exists, not functional)
+- **i18n**: 30% (framework setup, macro issues)
+- **Platform Features**: 30% (build fixes, window config aligned)
+- **Build System**: 85% (macOS fixed, workflow ready)
+- **Code Patterns**: 30% (prelude added, need traits)
+
+### Priority Items for Next Phase:
+1. **✅ Fix macOS build** - DONE (removed unsupported linker flag)
+2. **Refactor Button with Zed patterns** - Add traits and builder methods
+3. **Add component prelude** - Export common types and traits
+4. **Implement missing components** - Focus on Checkbox, Select first
+5. **Wire up bridges** - Make daemon control functional
+6. **Add animations** - Duration constants and transitions
+7. **Improve documentation** - Add examples and usage docs
+8. **Test build artifacts** - Verify workflow produces working binaries
diff --git a/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md b/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md
index 8059206..cc3cab6 100644
--- a/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md
+++ b/crates/wsrx-desktop-gpui/MIGRATION_PLAN.md
@@ -216,23 +216,25 @@ crates/wsrx-desktop-gpui/
├── Cargo.toml # GPUI dependency configuration
├── build.rs # Build script
├── src/
-│ ├── main.rs # Application entry point
-│ ├── lib.rs # Library root
+│ ├── main.rs # Application entry point with i18n macro
│ ├── logging.rs # Logging initialization
+│ ├── i18n.rs # Internationalization setup
│ ├── models/mod.rs # Data model definitions
│ ├── styles/mod.rs # Themes and styles
│ ├── views/
│ │ ├── mod.rs
-│ │ ├── root.rs # Placeholder
-│ │ ├── get_started.rs # Placeholder
-│ │ ├── connections.rs # Placeholder
-│ │ ├── network_logs.rs # Placeholder
-│ │ ├── settings.rs # Placeholder
-│ │ └── sidebar.rs # Placeholder
+│ │ ├── root.rs # Main window root view
+│ │ ├── get_started.rs # Onboarding page
+│ │ ├── connections.rs # Tunnel management
+│ │ ├── network_logs.rs # Log display
+│ │ ├── settings.rs # Settings page
+│ │ └── sidebar.rs # Navigation sidebar
│ ├── components/
│ │ ├── mod.rs
-│ │ ├── title_bar.rs # Placeholder
-│ │ ├── window_controls.rs # Placeholder
+│ │ ├── prelude.rs # Common component imports
+│ │ ├── title_bar.rs # Window title bar with drag support
+│ │ ├── window_controls.rs # Platform-aware window controls
+
│ │ └── tab_navigation.rs # Placeholder
│ └── bridges/
│ ├── mod.rs
diff --git a/crates/wsrx-desktop-gpui/build.rs b/crates/wsrx-desktop-gpui/build.rs
index 46024be..806c8fb 100644
--- a/crates/wsrx-desktop-gpui/build.rs
+++ b/crates/wsrx-desktop-gpui/build.rs
@@ -40,8 +40,5 @@ pub const TARGET_ARCH: &str = "{arch}";
}
}
- #[cfg(target_os = "macos")]
- {
- println!("cargo:rustc-link-arg=-fapple-link-runtime");
- }
+ // No special macOS linker flags needed - GPUI handles this internally
}
diff --git a/crates/wsrx-desktop-gpui/icons/arrow-sync-off.svg b/crates/wsrx-desktop-gpui/icons/arrow-sync-off.svg
new file mode 100644
index 0000000..3c9d615
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/arrow-sync-off.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/arrow-up-right.svg b/crates/wsrx-desktop-gpui/icons/arrow-up-right.svg
new file mode 100644
index 0000000..6afce3b
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/arrow-up-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/checkbox-unchecked.svg b/crates/wsrx-desktop-gpui/icons/checkbox-unchecked.svg
new file mode 100644
index 0000000..4805c19
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/checkbox-unchecked.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/checkmark.svg b/crates/wsrx-desktop-gpui/icons/checkmark.svg
new file mode 100644
index 0000000..6400e71
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/checkmark.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/code.svg b/crates/wsrx-desktop-gpui/icons/code.svg
new file mode 100644
index 0000000..b8353c3
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/code.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/dismiss.svg b/crates/wsrx-desktop-gpui/icons/dismiss.svg
new file mode 100644
index 0000000..14099b5
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/dismiss.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/globe-star.svg b/crates/wsrx-desktop-gpui/icons/globe-star.svg
new file mode 100644
index 0000000..7a29807
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/globe-star.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/home.svg b/crates/wsrx-desktop-gpui/icons/home.svg
new file mode 100644
index 0000000..3107f93
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/home.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/lock-closed.svg b/crates/wsrx-desktop-gpui/icons/lock-closed.svg
new file mode 100644
index 0000000..76f3b30
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/lock-closed.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/logo-stroked.svg b/crates/wsrx-desktop-gpui/icons/logo-stroked.svg
new file mode 100644
index 0000000..b24b4e5
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/logo-stroked.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/crates/wsrx-desktop-gpui/icons/logo.svg b/crates/wsrx-desktop-gpui/icons/logo.svg
new file mode 100644
index 0000000..b622af6
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/crates/wsrx-desktop-gpui/icons/maximize.svg b/crates/wsrx-desktop-gpui/icons/maximize.svg
new file mode 100644
index 0000000..9b49186
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/maximize.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/navigation.svg b/crates/wsrx-desktop-gpui/icons/navigation.svg
new file mode 100644
index 0000000..9b968e0
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/navigation.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/settings.svg b/crates/wsrx-desktop-gpui/icons/settings.svg
new file mode 100644
index 0000000..fddb389
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/settings.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/subtract.svg b/crates/wsrx-desktop-gpui/icons/subtract.svg
new file mode 100644
index 0000000..2f33f0b
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/subtract.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/icons/warning.svg b/crates/wsrx-desktop-gpui/icons/warning.svg
new file mode 100644
index 0000000..4f4301c
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/icons/warning.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/wsrx-desktop-gpui/locales/en.toml b/crates/wsrx-desktop-gpui/locales/en.toml
new file mode 100644
index 0000000..4b5394c
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/locales/en.toml
@@ -0,0 +1,24 @@
+# English translations
+get_started = "Get Started"
+network_logs = "Network Logs"
+connections = "Connections"
+settings = "Settings"
+
+# Buttons and actions
+add_tunnel = "Add Tunnel"
+clear_logs = "Clear Logs"
+
+# Status messages
+no_tunnels = "No tunnels configured"
+no_logs = "No logs available"
+
+# Settings sections
+application = "Application"
+appearance = "Appearance"
+logging = "Logging"
+about = "About"
+
+# About info
+app_name = "WebSocket Reflector X"
+app_description = "TCP-over-WebSocket tunneling tool"
+version = "Version"
diff --git a/crates/wsrx-desktop-gpui/locales/zh-CN.toml b/crates/wsrx-desktop-gpui/locales/zh-CN.toml
new file mode 100644
index 0000000..4b1c81e
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/locales/zh-CN.toml
@@ -0,0 +1,24 @@
+# Chinese Simplified translations
+get_started = "开始"
+network_logs = "网络日志"
+connections = "连接"
+settings = "设置"
+
+# Buttons and actions
+add_tunnel = "添加隧道"
+clear_logs = "清除日志"
+
+# Status messages
+no_tunnels = "未配置隧道"
+no_logs = "无可用日志"
+
+# Settings sections
+application = "应用程序"
+appearance = "外观"
+logging = "日志"
+about = "关于"
+
+# About info
+app_name = "WebSocket Reflector X"
+app_description = "TCP-over-WebSocket 隧道工具"
+version = "版本"
diff --git a/crates/wsrx-desktop-gpui/src/bridges/daemon.rs b/crates/wsrx-desktop-gpui/src/bridges/daemon.rs
index 051c3d9..9cb1f33 100644
--- a/crates/wsrx-desktop-gpui/src/bridges/daemon.rs
+++ b/crates/wsrx-desktop-gpui/src/bridges/daemon.rs
@@ -1,6 +1,77 @@
// Daemon bridge - Communication with wsrx daemon subprocess
+use std::sync::Arc;
+
+use anyhow::Result;
+use tokio::sync::Mutex;
+
+/// Status of the daemon process
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum DaemonStatus {
+ Stopped,
+ Starting,
+ Running,
+ Stopping,
+ Error,
+}
+
+/// Bridge for managing the wsrx daemon subprocess
pub struct DaemonBridge {
- // TODO: Define state
+ /// Current daemon status
+ status: Arc>,
+
+ /// Process handle (when running)
+ #[allow(dead_code)]
+ process: Arc>>,
+}
+
+impl DaemonBridge {
+ /// Create a new daemon bridge
+ pub fn new() -> Self {
+ Self {
+ status: Arc::new(Mutex::new(DaemonStatus::Stopped)),
+ process: Arc::new(Mutex::new(None)),
+ }
+ }
+
+ /// Get current daemon status
+ pub async fn status(&self) -> DaemonStatus {
+ *self.status.lock().await
+ }
+
+ /// Start the daemon process
+ pub async fn start(&self) -> Result<()> {
+ let mut status = self.status.lock().await;
+ *status = DaemonStatus::Starting;
+
+ // TODO: Implement actual daemon startup
+ // This will spawn the wsrx daemon process and monitor it
+
+ *status = DaemonStatus::Running;
+ Ok(())
+ }
+
+ /// Stop the daemon process
+ pub async fn stop(&self) -> Result<()> {
+ let mut status = self.status.lock().await;
+ *status = DaemonStatus::Stopping;
+
+ // TODO: Implement actual daemon shutdown
+ // This will gracefully stop the daemon process
+
+ *status = DaemonStatus::Stopped;
+ Ok(())
+ }
+
+ /// Restart the daemon process
+ pub async fn restart(&self) -> Result<()> {
+ self.stop().await?;
+ self.start().await?;
+ Ok(())
+ }
}
-// Placeholder - Will be implemented in next phase
+impl Default for DaemonBridge {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/bridges/mod.rs b/crates/wsrx-desktop-gpui/src/bridges/mod.rs
index 87e2b40..73b9dbb 100644
--- a/crates/wsrx-desktop-gpui/src/bridges/mod.rs
+++ b/crates/wsrx-desktop-gpui/src/bridges/mod.rs
@@ -1,10 +1,7 @@
// Bridges - Integration layer between UI and core functionality
-// This module contains the bridges that connect the UI to the wsrx daemon and other services
+// This module contains the bridges that connect the UI to the wsrx daemon and
+// other services
pub mod daemon;
pub mod settings;
pub mod system_info;
-
-pub use daemon::DaemonBridge;
-pub use settings::SettingsBridge;
-pub use system_info::SystemInfoBridge;
diff --git a/crates/wsrx-desktop-gpui/src/bridges/settings.rs b/crates/wsrx-desktop-gpui/src/bridges/settings.rs
index 1e59c42..ffb57e9 100644
--- a/crates/wsrx-desktop-gpui/src/bridges/settings.rs
+++ b/crates/wsrx-desktop-gpui/src/bridges/settings.rs
@@ -1,6 +1,56 @@
// Settings bridge - Application settings persistence
+use std::path::PathBuf;
+
+use anyhow::Result;
+use directories::ProjectDirs;
+
+use crate::models::Settings;
+
+/// Bridge for managing application settings persistence
pub struct SettingsBridge {
- // TODO: Define state
+ /// Path to settings file
+ settings_path: PathBuf,
+}
+
+impl SettingsBridge {
+ /// Create a new settings bridge
+ pub fn new() -> Result {
+ let settings_path = Self::get_settings_path()?;
+ Ok(Self { settings_path })
+ }
+
+ /// Get the settings file path
+ fn get_settings_path() -> Result {
+ let proj_dirs = ProjectDirs::from("org", "xdsec", "wsrx-desktop-gpui")
+ .ok_or_else(|| anyhow::anyhow!("Could not determine settings directory"))?;
+
+ let config_dir = proj_dirs.config_dir();
+ std::fs::create_dir_all(config_dir)?;
+
+ Ok(config_dir.join("settings.toml"))
+ }
+
+ /// Load settings from file
+ pub fn load(&self) -> Result {
+ if !self.settings_path.exists() {
+ return Ok(Settings::default());
+ }
+
+ let content = std::fs::read_to_string(&self.settings_path)?;
+ let settings: Settings = toml::from_str(&content)?;
+ Ok(settings)
+ }
+
+ /// Save settings to file
+ pub fn save(&self, settings: &Settings) -> Result<()> {
+ let content = toml::to_string_pretty(settings)?;
+ std::fs::write(&self.settings_path, content)?;
+ Ok(())
+ }
}
-// Placeholder - Will be implemented in next phase
+impl Default for SettingsBridge {
+ fn default() -> Self {
+ Self::new().expect("Failed to initialize settings bridge")
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/bridges/system_info.rs b/crates/wsrx-desktop-gpui/src/bridges/system_info.rs
index 58d7e01..072e987 100644
--- a/crates/wsrx-desktop-gpui/src/bridges/system_info.rs
+++ b/crates/wsrx-desktop-gpui/src/bridges/system_info.rs
@@ -1,6 +1,76 @@
// System info bridge - System resource monitoring
+use std::sync::{Arc, Mutex};
+
+use sysinfo::System;
+
+/// System information and monitoring
pub struct SystemInfoBridge {
- // TODO: Define state
+ /// System info handle
+ system: Arc>,
+}
+
+impl SystemInfoBridge {
+ /// Create a new system info bridge
+ pub fn new() -> Self {
+ let mut system = System::new_all();
+ system.refresh_all();
+
+ Self {
+ system: Arc::new(Mutex::new(system)),
+ }
+ }
+
+ /// Get CPU usage percentage
+ pub fn cpu_usage(&self) -> f32 {
+ let mut system = self.system.lock().unwrap();
+ system.refresh_cpu_all();
+
+ let cpus = system.cpus();
+ if cpus.is_empty() {
+ return 0.0;
+ }
+
+ cpus.iter().map(|cpu| cpu.cpu_usage()).sum::() / cpus.len() as f32
+ }
+
+ /// Get memory usage percentage
+ pub fn memory_usage(&self) -> f32 {
+ let mut system = self.system.lock().unwrap();
+ system.refresh_memory();
+
+ let total = system.total_memory();
+ let used = system.used_memory();
+
+ if total == 0 {
+ return 0.0;
+ }
+
+ (used as f32 / total as f32) * 100.0
+ }
+
+ /// Get total memory in bytes
+ pub fn total_memory(&self) -> u64 {
+ let mut system = self.system.lock().unwrap();
+ system.refresh_memory();
+ system.total_memory()
+ }
+
+ /// Get used memory in bytes
+ pub fn used_memory(&self) -> u64 {
+ let mut system = self.system.lock().unwrap();
+ system.refresh_memory();
+ system.used_memory()
+ }
+
+ /// Refresh all system information
+ pub fn refresh(&self) {
+ let mut system = self.system.lock().unwrap();
+ system.refresh_all();
+ }
}
-// Placeholder - Will be implemented in next phase
+impl Default for SystemInfoBridge {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/button.rs b/crates/wsrx-desktop-gpui/src/components/button.rs
new file mode 100644
index 0000000..fb37af7
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/button.rs
@@ -0,0 +1,114 @@
+// Button component - Reusable button with consistent styling
+use gpui::{Context, Render, SharedString, Window, div, prelude::*};
+
+use super::traits::{Clickable, Disableable, Styleable};
+use crate::styles::colors;
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum ButtonVariant {
+ Primary,
+ Secondary,
+ Danger,
+}
+
+pub struct Button {
+ label: String,
+ variant: ButtonVariant,
+ disabled: bool,
+ on_click: Option) + Send + Sync>>,
+}
+
+impl Button {
+ pub fn new(label: impl Into) -> Self {
+ Self {
+ label: label.into(),
+ variant: ButtonVariant::Primary,
+ disabled: false,
+ on_click: None,
+ }
+ }
+
+ pub fn variant(mut self, variant: ButtonVariant) -> Self {
+ self.variant = variant;
+ self
+ }
+
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+
+ fn bg_color(&self) -> gpui::Rgba {
+ match self.variant {
+ ButtonVariant::Primary => colors::accent(),
+ ButtonVariant::Secondary => gpui::rgba(0x444444FF),
+ ButtonVariant::Danger => colors::error(),
+ }
+ }
+
+ fn hover_color(&self) -> gpui::Rgba {
+ match self.variant {
+ ButtonVariant::Primary => gpui::rgba(0x0088DDFF),
+ ButtonVariant::Secondary => gpui::rgba(0x555555FF),
+ ButtonVariant::Danger => gpui::rgba(0xFF6655FF),
+ }
+ }
+}
+
+impl Clickable for Button {
+ fn on_click(mut self, handler: F) -> Self
+ where
+ F: Fn(&mut Window, &mut Context) + Send + Sync + 'static,
+ {
+ self.on_click = Some(Box::new(handler));
+ self
+ }
+}
+
+impl Disableable for Button {
+ fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+}
+
+impl Styleable for Button {
+ type Style = ButtonVariant;
+
+ fn style(self, style: Self::Style) -> Self {
+ Self {
+ variant: style,
+ ..self
+ }
+ }
+}
+
+impl Render for Button {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let id = SharedString::from(format!("button-{}", self.label));
+ let label = self.label.clone();
+ let disabled = self.disabled;
+
+ div()
+ .id(id)
+ .px_4()
+ .py_2()
+ .rounded_md()
+ .cursor_pointer()
+ .when(!disabled, |div| {
+ div.bg(self.bg_color())
+ .hover(|div| div.bg(self.hover_color()))
+ .on_click(cx.listener(|this, _event, window, cx| {
+ if let Some(ref callback) = this.on_click {
+ callback(window, cx);
+ }
+ }))
+ })
+ .when(disabled, |div| {
+ div.bg(gpui::rgba(0x333333FF))
+ .text_color(gpui::rgba(0x666666FF))
+ })
+ .text_color(colors::foreground())
+ .child(label)
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/checkbox.rs b/crates/wsrx-desktop-gpui/src/components/checkbox.rs
new file mode 100644
index 0000000..2b1d6be
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/checkbox.rs
@@ -0,0 +1,116 @@
+// Checkbox component
+// Based on Zed's checkbox pattern
+
+use super::prelude::*;
+use crate::styles::{border_radius, colors, sizes};
+
+pub struct Checkbox {
+ id: SharedString,
+ label: SharedString,
+ checked: bool,
+ disabled: bool,
+ on_change: Option>,
+}
+
+impl Checkbox {
+ pub fn new(id: impl Into, label: impl Into) -> Self {
+ Self {
+ id: id.into(),
+ label: label.into(),
+ checked: false,
+ disabled: false,
+ on_change: None,
+ }
+ }
+
+ pub fn checked(mut self, checked: bool) -> Self {
+ self.checked = checked;
+ self
+ }
+
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+
+ pub fn on_change(
+ mut self, handler: impl Fn(bool, &mut Window, &mut App) + Send + Sync + 'static,
+ ) -> Self {
+ self.on_change = Some(Box::new(handler));
+ self
+ }
+}
+
+impl IntoElement for Checkbox {
+ type Element = gpui::AnyElement;
+
+ fn into_element(self) -> Self::Element {
+ let checked = self.checked;
+ let disabled = self.disabled;
+ let id = self.id.clone();
+ let label = self.label.to_string();
+
+ let mut root_div = div()
+ .id(id)
+ .flex()
+ .flex_row()
+ .items_center()
+ .gap(sizes::icon_sm());
+
+ if !disabled {
+ root_div = root_div.cursor_pointer();
+ }
+
+ if disabled {
+ root_div = root_div.cursor_not_allowed().opacity(0.5);
+ }
+
+ if let Some(on_change) = self.on_change {
+ root_div = root_div.on_click(move |_event, window, cx| {
+ if !disabled {
+ on_change(!checked, window, cx);
+ }
+ });
+ }
+
+ let checkbox_box = {
+ let mut box_div = div()
+ .flex()
+ .items_center()
+ .justify_center()
+ .size(sizes::icon_md())
+ .rounded(border_radius::r_xs())
+ .border_1()
+ .border_color(if checked {
+ colors::primary_bg()
+ } else {
+ colors::element_border()
+ })
+ .bg(if checked {
+ colors::primary_bg()
+ } else {
+ gpui::rgba(0x00000000)
+ });
+
+ if checked {
+ box_div = box_div.child(
+ // Checkmark icon
+ svg()
+ .path("icons/checkmark.svg")
+ .size(sizes::icon_xs())
+ .text_color(colors::window_bg()),
+ );
+ }
+
+ box_div
+ };
+
+ root_div
+ .child(checkbox_box)
+ .child(
+ // Label
+ div().text_color(colors::window_fg()).child(label),
+ )
+ .into_any_element()
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/icon_button.rs b/crates/wsrx-desktop-gpui/src/components/icon_button.rs
new file mode 100644
index 0000000..5553846
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/icon_button.rs
@@ -0,0 +1,123 @@
+// Icon-only button component
+// Based on Zed's IconButton pattern
+
+use super::prelude::*;
+use crate::styles::{border_radius, colors, heights, sizes};
+
+#[derive(Clone, Copy, PartialEq)]
+pub enum IconButtonStyle {
+ Subtle, // Default - transparent with hover
+ Filled, // Solid background
+ Danger, // Red-themed for destructive actions
+}
+
+pub struct IconButton {
+ icon_path: &'static str,
+ style: IconButtonStyle,
+ disabled: bool,
+ on_click: Option>,
+}
+
+impl IconButton {
+ pub fn new(icon_path: &'static str) -> Self {
+ Self {
+ icon_path,
+ style: IconButtonStyle::Subtle,
+ disabled: false,
+ on_click: None,
+ }
+ }
+
+ pub fn style(mut self, style: IconButtonStyle) -> Self {
+ self.style = style;
+ self
+ }
+
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+
+ pub fn on_click(
+ mut self, handler: impl Fn(&mut Window, &mut App) + Send + Sync + 'static,
+ ) -> Self {
+ self.on_click = Some(Box::new(handler));
+ self
+ }
+
+ fn background_color(&self) -> gpui::Rgba {
+ match self.style {
+ IconButtonStyle::Subtle => gpui::rgba(0x00000000),
+ IconButtonStyle::Filled => colors::layer_2(),
+ IconButtonStyle::Danger => colors::layer_1(),
+ }
+ }
+
+ fn hover_background_color(&self) -> gpui::Rgba {
+ match self.style {
+ IconButtonStyle::Subtle => colors::layer_1(),
+ IconButtonStyle::Filled => colors::layer_3(),
+ IconButtonStyle::Danger => colors::error_bg(),
+ }
+ }
+
+ fn icon_color(&self) -> gpui::Hsla {
+ if self.disabled {
+ gpui::Hsla::from(colors::window_fg()).opacity(0.3)
+ } else {
+ match self.style {
+ IconButtonStyle::Danger => gpui::Hsla::from(colors::error_fg()),
+ _ => gpui::Hsla::from(colors::window_fg()),
+ }
+ }
+ }
+}
+
+impl IntoElement for IconButton {
+ type Element = gpui::AnyElement;
+
+ fn into_element(self) -> Self::Element {
+ let background_color = self.background_color();
+ let icon_color = self.icon_color();
+
+ let IconButton {
+ icon_path,
+ style: _,
+ disabled,
+ on_click,
+ } = self;
+
+ let id = SharedString::from(format!("icon-button-{}", icon_path));
+
+ let mut div = div()
+ .id(id)
+ .flex()
+ .items_center()
+ .justify_center()
+ .size(heights::h_md())
+ .bg(background_color)
+ .rounded(border_radius::r_sm());
+
+ if !disabled {
+ div = div.cursor_pointer();
+ }
+
+ if disabled {
+ div = div.cursor_not_allowed().opacity(0.5);
+ }
+
+ if let Some(on_click) = on_click {
+ div = div.on_click(move |_event, window, cx| {
+ on_click(window, cx);
+ });
+ }
+
+ div.child(
+ svg()
+ .path(icon_path)
+ .size(sizes::icon_sm())
+ .text_color(icon_color),
+ )
+ .into_any_element()
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/input.rs b/crates/wsrx-desktop-gpui/src/components/input.rs
new file mode 100644
index 0000000..31ffbda
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/input.rs
@@ -0,0 +1,85 @@
+// Input component - Text input field with consistent styling
+use gpui::{Context, Render, SharedString, Window, div, prelude::*};
+
+use crate::styles::colors;
+
+pub struct Input {
+ id: String,
+ placeholder: String,
+ value: String,
+ disabled: bool,
+ on_change: Option) + Send + Sync>>,
+}
+
+impl Input {
+ pub fn new(id: impl Into) -> Self {
+ Self {
+ id: id.into(),
+ placeholder: String::new(),
+ value: String::new(),
+ disabled: false,
+ on_change: None,
+ }
+ }
+
+ pub fn placeholder(mut self, placeholder: impl Into) -> Self {
+ self.placeholder = placeholder.into();
+ self
+ }
+
+ pub fn value(mut self, value: impl Into) -> Self {
+ self.value = value.into();
+ self
+ }
+
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+
+ pub fn on_change(mut self, callback: F) -> Self
+ where
+ F: Fn(String, &mut Window, &mut Context) + Send + Sync + 'static,
+ {
+ self.on_change = Some(Box::new(callback));
+ self
+ }
+}
+
+impl Render for Input {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement {
+ let id = SharedString::from(format!("input-{}", self.id));
+ let placeholder = if self.value.is_empty() {
+ self.placeholder.clone()
+ } else {
+ String::new()
+ };
+ let value = self.value.clone();
+ let disabled = self.disabled;
+
+ div()
+ .id(id)
+ .flex()
+ .items_center()
+ .px_3()
+ .py_2()
+ .rounded_md()
+ .border_1()
+ .when(!disabled, |div| {
+ div.bg(gpui::rgba(0x2A2A2AFF))
+ .border_color(gpui::rgba(0x444444FF))
+ .hover(|div| div.border_color(colors::accent()))
+ })
+ .when(disabled, |div| {
+ div.bg(gpui::rgba(0x1A1A1AFF))
+ .border_color(gpui::rgba(0x333333FF))
+ .text_color(gpui::rgba(0x666666FF))
+ })
+ .text_color(colors::foreground())
+ .child(if value.is_empty() && !placeholder.is_empty() {
+ div().text_color(gpui::rgba(0x888888FF)).child(placeholder)
+ } else {
+ div().child(value)
+ })
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/mod.rs b/crates/wsrx-desktop-gpui/src/components/mod.rs
index e8b2264..a1cbc37 100644
--- a/crates/wsrx-desktop-gpui/src/components/mod.rs
+++ b/crates/wsrx-desktop-gpui/src/components/mod.rs
@@ -1,10 +1,18 @@
// Components - Reusable UI elements built with GPUI
// These are lower-level components used across different views
+pub mod button;
+pub mod checkbox;
+pub mod icon_button;
+pub mod input;
+pub mod modal;
+pub mod prelude;
+pub mod select;
+pub mod status_indicator;
+pub mod tab_navigation;
pub mod title_bar;
+pub mod traits;
pub mod window_controls;
-pub mod tab_navigation;
-pub use title_bar::TitleBar;
+// pub use title_bar::TitleBar;
pub use window_controls::WindowControls;
-pub use tab_navigation::TabNavigation;
diff --git a/crates/wsrx-desktop-gpui/src/components/modal.rs b/crates/wsrx-desktop-gpui/src/components/modal.rs
new file mode 100644
index 0000000..aecd918
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/modal.rs
@@ -0,0 +1,106 @@
+// Modal component - Modal dialog overlay
+use gpui::{AnyElement, Context, Render, Window, div, prelude::*};
+
+use crate::styles::colors;
+
+pub struct Modal {
+ title: String,
+ content: Option,
+ show_close: bool,
+ on_close: Option) + Send + Sync>>,
+}
+
+impl Modal {
+ pub fn new(title: impl Into) -> Self {
+ Self {
+ title: title.into(),
+ content: None,
+ show_close: true,
+ on_close: None,
+ }
+ }
+
+ pub fn content(mut self, content: impl IntoElement) -> Self {
+ self.content = Some(content.into_any_element());
+ self
+ }
+
+ pub fn show_close(mut self, show: bool) -> Self {
+ self.show_close = show;
+ self
+ }
+
+ pub fn on_close(mut self, callback: F) -> Self
+ where
+ F: Fn(&mut Window, &mut Context) + Send + Sync + 'static,
+ {
+ self.on_close = Some(Box::new(callback));
+ self
+ }
+}
+
+impl Render for Modal {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let title = self.title.clone();
+ let show_close = self.show_close;
+
+ div()
+ .flex()
+ .absolute()
+ .top_0()
+ .left_0()
+ .w_full()
+ .h_full()
+ .items_center()
+ .justify_center()
+ .bg(gpui::rgba(0x00000099)) // Semi-transparent overlay
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .w(gpui::relative(0.8))
+ .max_w(gpui::px(600.0))
+ .bg(gpui::rgba(0x2A2A2AFF))
+ .rounded_lg()
+ .shadow_lg()
+ .child(
+ // Header
+ div()
+ .flex()
+ .items_center()
+ .justify_between()
+ .px_6()
+ .py_4()
+ .border_b_1()
+ .border_color(gpui::rgba(0x444444FF))
+ .child(
+ div()
+ .text_xl()
+ .text_color(colors::foreground())
+ .child(title),
+ )
+ .when(show_close, |container| {
+ container.child(
+ div()
+ .id("modal-close")
+ .px_2()
+ .py_1()
+ .cursor_pointer()
+ .hover(|div| div.bg(gpui::rgba(0x444444FF)))
+ .rounded_md()
+ .on_click(cx.listener(|this, _event, window, cx| {
+ if let Some(ref callback) = this.on_close {
+ callback(window, cx);
+ }
+ }))
+ .child("✕"),
+ )
+ }),
+ )
+ .child(
+ // Content
+ div().px_6().py_4().children(self.content.take()),
+ ),
+ )
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/prelude.rs b/crates/wsrx-desktop-gpui/src/components/prelude.rs
new file mode 100644
index 0000000..3b57440
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/prelude.rs
@@ -0,0 +1,10 @@
+// Component prelude - Common imports for all components
+// Following Zed's pattern from crates/ui/src/component_prelude.rs
+
+pub use gpui::{
+ App, AppContext, InteractiveElement, IntoElement, ParentElement, SharedString,
+ StatefulInteractiveElement, Styled, Window, div, prelude::*, svg,
+};
+
+// Component traits
+pub use super::traits::{Clickable, Disableable, Selectable, Styleable};
diff --git a/crates/wsrx-desktop-gpui/src/components/select.rs b/crates/wsrx-desktop-gpui/src/components/select.rs
new file mode 100644
index 0000000..4ddf51a
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/select.rs
@@ -0,0 +1,149 @@
+// Select component - Dropdown selection component
+// Following Zed's pattern with traits for reusability
+
+use gpui::{Context, Render, SharedString, Window, div, prelude::*};
+
+use super::traits::{Disableable, Selectable};
+use crate::styles::colors;
+
+pub struct Select {
+ id: SharedString,
+ placeholder: String,
+ options: Vec,
+ selected_index: Option,
+ disabled: bool,
+ on_select: Option, T) + Send + Sync>>,
+}
+
+impl Select
+where
+ T: Clone + ToString + 'static,
+{
+ pub fn new(id: impl Into) -> Self {
+ Self {
+ id: id.into(),
+ placeholder: "Select an option...".to_string(),
+ options: Vec::new(),
+ selected_index: None,
+ disabled: false,
+ on_select: None,
+ }
+ }
+
+ pub fn placeholder(mut self, placeholder: impl Into) -> Self {
+ self.placeholder = placeholder.into();
+ self
+ }
+
+ pub fn options(mut self, options: Vec) -> Self {
+ self.options = options;
+ self
+ }
+
+ pub fn selected_index(mut self, index: Option) -> Self {
+ self.selected_index = index;
+ self
+ }
+
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+}
+
+impl Selectable for Select
+where
+ T: Clone + ToString + 'static,
+{
+ type Item = T;
+
+ fn selected(mut self, item: Self::Item) -> Self {
+ if let Some(index) = self
+ .options
+ .iter()
+ .position(|opt| opt.to_string() == item.to_string())
+ {
+ self.selected_index = Some(index);
+ }
+ self
+ }
+
+ fn on_select(mut self, handler: F) -> Self
+ where
+ F: Fn(&mut Window, &mut Context, Self::Item) + Send + Sync + 'static,
+ {
+ self.on_select = Some(Box::new(handler));
+ self
+ }
+}
+
+impl Disableable for Select
+where
+ T: Clone + ToString + 'static,
+{
+ fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+}
+
+impl Render for Select
+where
+ T: Clone + ToString + 'static,
+{
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let selected_text = self
+ .selected_index
+ .and_then(|idx| self.options.get(idx))
+ .map(|item| item.to_string())
+ .unwrap_or_else(|| self.placeholder.clone());
+
+ let disabled = self.disabled;
+ let options = self.options.clone();
+
+ div()
+ .id(self.id.clone())
+ .relative()
+ .w_full()
+ .px_3()
+ .py_2()
+ .rounded_md()
+ .bg(colors::background())
+ .text_color(colors::foreground())
+ .cursor_pointer()
+ .when(!disabled, |div| {
+ div.hover(|div| div.bg(gpui::rgba(0x2A2A2AFF)))
+ .on_click(cx.listener(move |this, _event, window, cx| {
+ // For now, just cycle through options on click
+ // TODO: Implement proper dropdown with overlay
+ let next_index = this
+ .selected_index
+ .map(|idx| (idx + 1) % this.options.len())
+ .unwrap_or(0);
+ this.selected_index = Some(next_index);
+
+ if let (Some(index), Some(callback)) =
+ (this.selected_index, &this.on_select)
+ {
+ if let Some(item) = this.options.get(index).cloned() {
+ callback(window, cx, item);
+ }
+ }
+
+ cx.notify();
+ }))
+ })
+ .when(disabled, |div| {
+ div.bg(gpui::rgba(0x1A1A1AFF))
+ .text_color(gpui::rgba(0x666666FF))
+ })
+ .child(
+ div()
+ .flex()
+ .justify_between()
+ .items_center()
+ .child(selected_text)
+ .child("▼"), // Simple dropdown arrow
+ )
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/status_indicator.rs b/crates/wsrx-desktop-gpui/src/components/status_indicator.rs
new file mode 100644
index 0000000..5db7906
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/status_indicator.rs
@@ -0,0 +1,71 @@
+// StatusIndicator component - Visual status indicator with color coding
+use gpui::{Context, Render, Window, div, prelude::*};
+
+use crate::styles::colors;
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum Status {
+ Success,
+ Warning,
+ Error,
+ Info,
+ Inactive,
+}
+
+pub struct StatusIndicator {
+ status: Status,
+ label: Option,
+ size: f32,
+}
+
+impl StatusIndicator {
+ pub fn new(status: Status) -> Self {
+ Self {
+ status,
+ label: None,
+ size: 8.0,
+ }
+ }
+
+ pub fn label(mut self, label: impl Into) -> Self {
+ self.label = Some(label.into());
+ self
+ }
+
+ pub fn size(mut self, size: f32) -> Self {
+ self.size = size;
+ self
+ }
+
+ fn status_color(&self) -> gpui::Rgba {
+ match self.status {
+ Status::Success => colors::success(),
+ Status::Warning => colors::warning(),
+ Status::Error => colors::error(),
+ Status::Info => colors::accent(),
+ Status::Inactive => gpui::rgba(0x666666FF),
+ }
+ }
+}
+
+impl Render for StatusIndicator {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement {
+ div()
+ .flex()
+ .items_center()
+ .gap_2()
+ .child(
+ div()
+ .w(gpui::px(self.size))
+ .h(gpui::px(self.size))
+ .rounded_full()
+ .bg(self.status_color()),
+ )
+ .children(self.label.as_ref().map(|label| {
+ div()
+ .text_sm()
+ .text_color(colors::foreground())
+ .child(label.clone())
+ }))
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/title_bar.rs b/crates/wsrx-desktop-gpui/src/components/title_bar.rs
index 4165a6a..31e67b3 100644
--- a/crates/wsrx-desktop-gpui/src/components/title_bar.rs
+++ b/crates/wsrx-desktop-gpui/src/components/title_bar.rs
@@ -1,6 +1,88 @@
// Title bar component
+use gpui::{prelude::FluentBuilder, *};
+
+use crate::{components::WindowControls, styles};
+
pub struct TitleBar {
- // TODO: Define state
+ window: AnyWindowHandle,
+ show_sidebar_callback: Option>,
}
-// Placeholder - Will be implemented in next phase
+impl TitleBar {
+ pub fn new(window: AnyWindowHandle) -> Self {
+ Self {
+ window,
+ show_sidebar_callback: None,
+ }
+ }
+
+ pub fn set_show_sidebar_callback(&mut self, callback: Box) {
+ self.show_sidebar_callback = Some(callback);
+ }
+}
+
+impl Render for TitleBar {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let window = self.window.clone();
+ let is_macos = cfg!(target_os = "macos");
+
+ div()
+ .id("title-bar")
+ .flex()
+ .flex_row()
+ .items_center()
+ .justify_between()
+ .h(styles::heights::h_md() + styles::padding::p_md() * 2.0)
+ .px(styles::padding::p_md())
+ .py(styles::padding::p_md())
+ .gap(styles::spacing::s_md())
+ .bg(gpui::transparent_black())
+ // Drag area
+ .on_mouse_down(MouseButton::Left, {
+ let window = window.clone();
+ cx.listener(move |_this, _event: &MouseDownEvent, _window, cx| {
+ window
+ .update(cx, |_view, window, _cx| {
+ window.start_window_move();
+ })
+ .ok();
+ })
+ })
+ .child(
+ div()
+ .flex()
+ .flex_row()
+ .gap(styles::spacing::s_md())
+ .when(!is_macos, |this| {
+ this.child(
+ div()
+ .id("toggle-sidebar-btn")
+ .flex()
+ .items_center()
+ .justify_center()
+ .size(styles::heights::h_md())
+ .bg(styles::colors::layer_1())
+ .hover(|this| this.bg(styles::colors::layer_2()))
+ .rounded(styles::border_radius::r_sm())
+ .cursor_pointer()
+ .on_click(cx.listener(|this, _event, _window, cx| {
+ if let Some(ref callback) = this.show_sidebar_callback {
+ callback(cx);
+ }
+ }))
+ .child(
+ svg()
+ .path("icons/navigation.svg")
+ .size(styles::sizes::icon_sm())
+ .text_color(styles::colors::window_fg()),
+ ),
+ )
+ }),
+ )
+ .child(
+ // Center spacer
+ div().flex_1(),
+ )
+ .child(cx.new(|_cx| WindowControls::new(window)))
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/traits.rs b/crates/wsrx-desktop-gpui/src/components/traits.rs
new file mode 100644
index 0000000..999a54c
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/components/traits.rs
@@ -0,0 +1,31 @@
+// Component traits - Common interfaces for UI components
+// Following Zed's pattern for reusable component behavior
+
+use gpui::{Context, Window};
+
+/// Trait for components that can be clicked
+pub trait Clickable {
+ fn on_click(self, handler: F) -> Self
+ where
+ F: Fn(&mut Window, &mut Context) + Send + Sync + 'static;
+}
+
+/// Trait for components that can be disabled
+pub trait Disableable {
+ fn disabled(self, disabled: bool) -> Self;
+}
+
+/// Trait for components with styled variants
+pub trait Styleable {
+ type Style;
+ fn style(self, style: Self::Style) -> Self;
+}
+
+/// Trait for components that can be selected from a list
+pub trait Selectable {
+ type Item;
+ fn selected(self, item: Self::Item) -> Self;
+ fn on_select(self, handler: F) -> Self
+ where
+ F: Fn(&mut Window, &mut Context, Self::Item) + Send + Sync + 'static;
+}
diff --git a/crates/wsrx-desktop-gpui/src/components/window_controls.rs b/crates/wsrx-desktop-gpui/src/components/window_controls.rs
index 7dcc507..b33b586 100644
--- a/crates/wsrx-desktop-gpui/src/components/window_controls.rs
+++ b/crates/wsrx-desktop-gpui/src/components/window_controls.rs
@@ -1,6 +1,106 @@
// Window controls component (minimize, maximize, close buttons)
+use gpui::{prelude::FluentBuilder, *};
+
+use crate::styles;
+
pub struct WindowControls {
- // TODO: Define state
+ window: AnyWindowHandle,
+}
+
+impl WindowControls {
+ pub fn new(window: AnyWindowHandle) -> Self {
+ Self { window }
+ }
}
-// Placeholder - Will be implemented in next phase
+impl Render for WindowControls {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let window = self.window.clone();
+ let is_macos = cfg!(target_os = "macos");
+
+ div()
+ .flex()
+ .flex_row()
+ .gap(styles::spacing::s_md())
+ .when(!is_macos, |this| {
+ this
+ // Minimize button
+ .child(
+ div()
+ .id("minimize-btn")
+ .flex()
+ .items_center()
+ .justify_center()
+ .size(styles::heights::h_md())
+ .bg(styles::colors::layer_1())
+ .hover(|this| this.bg(styles::colors::layer_2()))
+ .cursor_pointer()
+ .on_click({
+ let window = window.clone();
+ cx.listener(move |_this, _event, _window, cx| {
+ window
+ .update(cx, |_view, window, _cx| {
+ window.minimize_window();
+ })
+ .ok();
+ })
+ })
+ .child(
+ svg()
+ .path("icons/subtract.svg")
+ .size(styles::sizes::icon_sm())
+ .text_color(styles::colors::window_fg()),
+ ),
+ )
+ // Maximize button
+ .child(
+ div()
+ .id("maximize-btn")
+ .flex()
+ .items_center()
+ .justify_center()
+ .size(styles::heights::h_md())
+ .bg(styles::colors::layer_1())
+ .hover(|this| this.bg(styles::colors::layer_2()))
+ .cursor_pointer()
+ .on_click({
+ let window = window.clone();
+ cx.listener(move |_this, _event, _window, cx| {
+ window
+ .update(cx, |_view, window, _cx| {
+ window.zoom_window();
+ })
+ .ok();
+ })
+ })
+ .child(
+ svg()
+ .path("icons/maximize.svg")
+ .size(styles::sizes::icon_sm())
+ .text_color(styles::colors::window_fg()),
+ ),
+ )
+ // Close button
+ .child(
+ div()
+ .id("close-btn")
+ .flex()
+ .items_center()
+ .justify_center()
+ .size(styles::heights::h_md())
+ .bg(styles::colors::layer_1())
+ .hover(|this| this.bg(styles::colors::error_bg()))
+ .cursor_pointer()
+ .on_click(cx.listener(move |_this, _event, _window, cx| {
+ cx.quit();
+ }))
+ .child(
+ svg()
+ .path("icons/dismiss.svg")
+ .size(styles::sizes::icon_sm())
+ .text_color(styles::colors::window_fg()),
+ ),
+ )
+ })
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/i18n.rs b/crates/wsrx-desktop-gpui/src/i18n.rs
new file mode 100644
index 0000000..faf5ff2
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/i18n.rs
@@ -0,0 +1,29 @@
+// i18n - Internationalization support using rust-i18n
+
+// Provides multi-language support with YAML locale files
+// NOTE: The i18n! macro is initialized in lib.rs at crate root
+
+// Re-export functions for convenience
+pub use rust_i18n::{locale, set_locale};
+
+/// Set application language
+pub fn set_language(locale_str: &str) {
+ set_locale(locale_str);
+}
+
+/// Get current language
+pub fn current_language() -> impl std::ops::Deref {
+ locale()
+}
+
+/// Detect system locale and set it
+pub fn init_locale() {
+ if let Some(locale) = sys_locale::get_locale() {
+ // Map system locale to supported locales
+ let locale = match locale.as_str() {
+ l if l.starts_with("zh") => "zh-CN",
+ _ => "en",
+ };
+ set_language(locale);
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/icons.rs b/crates/wsrx-desktop-gpui/src/icons.rs
new file mode 100644
index 0000000..ae740c7
--- /dev/null
+++ b/crates/wsrx-desktop-gpui/src/icons.rs
@@ -0,0 +1,42 @@
+// Embedded SVG icons
+// All SVG files are embedded directly into the binary at compile time
+
+pub const HOME: &str = include_str!("../icons/home.svg");
+pub const CODE: &str = include_str!("../icons/code.svg");
+pub const SETTINGS: &str = include_str!("../icons/settings.svg");
+pub const GLOBE_STAR: &str = include_str!("../icons/globe-star.svg");
+pub const NAVIGATION: &str = include_str!("../icons/navigation.svg");
+pub const LOGO: &str = include_str!("../icons/logo.svg");
+pub const LOGO_STROKED: &str = include_str!("../icons/logo-stroked.svg");
+pub const WARNING: &str = include_str!("../icons/warning.svg");
+pub const DISMISS: &str = include_str!("../icons/dismiss.svg");
+pub const MAXIMIZE: &str = include_str!("../icons/maximize.svg");
+pub const SUBTRACT: &str = include_str!("../icons/subtract.svg");
+pub const LOCK_CLOSED: &str = include_str!("../icons/lock-closed.svg");
+pub const CHECKMARK: &str = include_str!("../icons/checkmark.svg");
+pub const CHECKBOX_UNCHECKED: &str = include_str!("../icons/checkbox-unchecked.svg");
+pub const ARROW_UP_RIGHT: &str = include_str!("../icons/arrow-up-right.svg");
+pub const ARROW_SYNC_OFF: &str = include_str!("../icons/arrow-sync-off.svg");
+
+/// Get icon SVG content by name
+pub fn get_icon(name: &str) -> Option<&'static str> {
+ match name {
+ "home" => Some(HOME),
+ "code" => Some(CODE),
+ "settings" => Some(SETTINGS),
+ "globe-star" => Some(GLOBE_STAR),
+ "navigation" => Some(NAVIGATION),
+ "logo" => Some(LOGO),
+ "logo-stroked" => Some(LOGO_STROKED),
+ "warning" => Some(WARNING),
+ "dismiss" => Some(DISMISS),
+ "maximize" => Some(MAXIMIZE),
+ "subtract" => Some(SUBTRACT),
+ "lock-closed" => Some(LOCK_CLOSED),
+ "checkmark" => Some(CHECKMARK),
+ "checkbox-unchecked" => Some(CHECKBOX_UNCHECKED),
+ "arrow-up-right" => Some(ARROW_UP_RIGHT),
+ "arrow-sync-off" => Some(ARROW_SYNC_OFF),
+ _ => None,
+ }
+}
diff --git a/crates/wsrx-desktop-gpui/src/lib.rs b/crates/wsrx-desktop-gpui/src/lib.rs
deleted file mode 100644
index 3820116..0000000
--- a/crates/wsrx-desktop-gpui/src/lib.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-// wsrx-desktop-gpui lib root
-pub mod bridges;
-pub mod components;
-pub mod logging;
-pub mod models;
-pub mod styles;
-pub mod views;
-
-// Include generated constants from build.rs
-include!(concat!(env!("OUT_DIR"), "/constants.rs"));
-
-// Re-export commonly used types
-pub use models::*;
-pub use views::*;
-pub use components::*;
-pub use styles::*;
diff --git a/crates/wsrx-desktop-gpui/src/logging.rs b/crates/wsrx-desktop-gpui/src/logging.rs
index 6e1ee1f..bfd2bdc 100644
--- a/crates/wsrx-desktop-gpui/src/logging.rs
+++ b/crates/wsrx-desktop-gpui/src/logging.rs
@@ -1,9 +1,10 @@
// Logging setup for wsrx-desktop-gpui
+use std::fs;
+
use anyhow::Result;
use directories::ProjectDirs;
-use std::fs;
use tracing_appender::non_blocking;
-use tracing_subscriber::{fmt, prelude::*, EnvFilter};
+use tracing_subscriber::{EnvFilter, fmt, prelude::*};
pub fn setup() -> Result<(
tracing_appender::non_blocking::WorkerGuard,
diff --git a/crates/wsrx-desktop-gpui/src/main.rs b/crates/wsrx-desktop-gpui/src/main.rs
index d012165..d1f583f 100644
--- a/crates/wsrx-desktop-gpui/src/main.rs
+++ b/crates/wsrx-desktop-gpui/src/main.rs
@@ -1,18 +1,95 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use anyhow::Result;
-use gpui::{App, Application, WindowOptions, WindowBounds, Bounds, Size};
+use gpui::{
+ App, AppContext, Application, AssetSource, Bounds, SharedString, TitlebarOptions,
+ WindowBounds, WindowDecorations, WindowKind, WindowOptions, point, px, size,
+};
+mod bridges;
+mod components;
+mod i18n;
+mod icons;
mod logging;
+mod models;
+mod styles;
+mod views;
+
+// Initialize i18n at crate root with TOML locale files
+// The path is relative to CARGO_MANIFEST_DIR (crate root)
+
+#[macro_use]
+extern crate rust_i18n;
+
+i18n!("locales", fallback = "en");
+
+// Include generated constants from build.rs
+include!(concat!(env!("OUT_DIR"), "/constants.rs"));
+
+use views::RootView;
+
+/// Asset source that loads embedded SVG icons from binary
+struct EmbeddedAssets;
+
+impl AssetSource for EmbeddedAssets {
+ fn load(&self, path: &str) -> Result