Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,43 @@ jobs:
- name: Test remote input TLS identity and Unix key permissions
run: cargo test --locked --manifest-path src-tauri/backend-tests/Cargo.toml --test remote_tls

linux-egui:
name: Linux egui host
runs-on: ubuntu-22.04
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
submodules: false

- uses: dtolnay/rust-toolchain@stable

- uses: swatinem/rust-cache@v2
with:
workspaces: 'openless-all/app -> target'

- name: Install egui host build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libasound2-dev \
libdbus-1-dev \
libssl-dev \
libwayland-dev \
libx11-dev \
libxkbcommon-dev \
pkg-config

# 与 release-linux-egui.yml 同款命令:lib + bin + UI headless 渲染测试。
# egui UI 只在 Linux 目标编译,这是 2.0 界面改动的编译门禁。
- name: Test linux-egui (lib + bin + ui)
run: cargo test --locked -p openless-linux-egui --all-targets

- name: Check linux-egui
run: cargo check --locked -p openless-linux-egui --all-targets

cross-platform:
name: ${{ matrix.label }} checks
strategy:
Expand Down
2 changes: 1 addition & 1 deletion docs/linux-egui-handoff/02-gap-register.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
| L06 | 界面:marketplace 服务已接入(`marketplace.rs`),市场页面缺失 | 浏览、安装、上传/下载、设备 OAuth 登录/退出,复用 Core 协议 |
| L07 | 界面+L03 依赖:历史仅只读最近条目,缺完整历史/统计/录音操作 | 浏览与既有历史操作、重润色/重转写、录音播放/导出/清理与统计 |
| L08 | 界面:本地 Qwen 有下载/激活/取消,缺完整模型管理与运行时控制 | 路径/镜像、详情/状态、删除、预载/释放与准备/测试取消;真实推理另附证据 |
| L09 | 界面部分改善:10 页导航、开始/环境引导、独立设置与手机输入页、跨页审批/取消已落地;多数 Linux 适用设置、Agent 检测/模型/路径/权限配置仍缺失 | 设置有实际消费者、revision/错误处理;保留 Less Computer 输出/审批/取消流程 |
| L09 | 界面部分改善:10 页导航、开始/环境引导、跨页审批/取消已落地;2026-09-10 起 2.0 外壳/设置弹窗/双主题视觉已对齐 Tauri 2.0(见 04 §0,待桌面实测);多数 Linux 适用设置、Agent 检测/模型/路径/权限配置仍缺失 | 设置有实际消费者、revision/错误处理;保留 Less Computer 输出/审批/取消流程 |
| L10 | 接线+界面:无托盘/自启(`capabilities.rs` 仅探测 `supports_tray`,未实现);通知仅状态栏;AppImage 能力判定不等于更新器 | 窗口/后台运行、通知、自启、检查/下载/安装更新与重启有真实 Host 效果 |
| L11 | 已实现待实测:fcitx5 输入/选区(`fcitx5.rs`)、CPAL(`audio.rs`)、Secret Service(`credentials.rs`)、Qwen 运行时、CLI 进程、Remote TLS/H5(`remote_input.rs`) | 通过真实桌面/设备矩阵;逐项记录,不把整模块称为缺失 |
| L12 | 已有包构建(deb/rpm/AppImage workflow),待 Linux 产品验收与正式分发 | 完成上述应用缺口与安装/升级/回滚、签名和更新证据;不阻塞 Windows/macOS 首批交付 |
Expand Down
49 changes: 30 additions & 19 deletions docs/linux-egui-handoff/04-ui-domains.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
# 04:页面与领域操作

状态:canonical(2026-09-07 以源码为准重写);更新:2026-09-07。页面定义:`linux-egui/src/ui_state.rs` `Page` 枚举(10 页);主循环与交互:`main.rs`。

## 1. 十页现状

| 页面 | 已有操作 | 主要缺口 |
| --- | --- | --- |
| Start | 下一步引导、环境准备说明(fcitx/插件、桌面会话、麦克风、Secret Service 的真实可知状态;未连接时保留诊断步骤,不展示成功) | — |
| Dictation | 普通听写(复用 Core `dictation_engine`)、ticket 化落字 | — |
| Qa | QA 文本/语音(`qa.rs`)、全局待办、审批、取消 | 编辑模式/应用/撤回 UI(L04) |
| Selection | 划词润色预览/撤回(`selection.rs`) | Selection Voice 生产触发/捕获/意图路由(L04) |
| Agent | Less Computer 工具调用、审批固定在页面外、后台事件保留 | Agent 检测/模型/路径/权限配置(L09) |
| Services | AI 服务配置、渠道管理(`settings.rs` + Core provider 面) | Omni 有效模式配置入口完整性(L09) |
| Models | 本地 Qwen 下载/激活/取消 | 完整模型管理:路径/镜像/详情/删除/预载/释放(L08) |
| Remote | 手机输入独立页、连接状态、陈旧地址处理 | — |
| History | 只读最近条目 | 完整历史/统计/录音操作(L07,依赖 L03) |
| Settings | 现有设置独立页 | 词典/纠错/风格包/市场无页面(L05/L06);多数设置缺实际消费者(L09) |
状态:canonical(2026-09-07 以源码为准重写;2026-09-10 补 2.0 界面复刻现状);更新:2026-09-10。页面定义:`linux-egui/src/ui_state.rs` `Page` 枚举;主循环与交互:`main.rs`;2.0 视觉系统:`linux-egui/src/design_tokens.rs`(tokens.css 对照表,跨平台单测)+ `linux-egui/src/ui/`(theme/widgets/prefs,仅 Linux 编译)。

## 0. 2.0 界面复刻(2026-09-10,待真实桌面验收)

按 Tauri/React 2.0 UI 在 egui 侧 1:1 复刻视觉与结构,功能接线沿用既有 Core 调用:

- 设计令牌:`design_tokens.rs` 是 `src/styles/tokens.css` 的 Rust 对照表(浅/深双主题、圆角阶梯、侧栏/弹窗尺寸),单测锁定与 CSS 字面值一致。
- 主题:`ui/theme.rs` 把令牌转成 egui Style/Visuals(白底 zinc 体系、#2563eb 强调、控件 8px 圆角);CJK 字体自动探测安装(含 `OPENLESS_IME_FONT` 覆盖,移植自 #997 并修正多候选覆盖问题);深浅主题即时切换并持久化到 `$XDG_DATA_HOME/OpenLess/ui-prefs.json`(`ui/prefs.rs`,表现层状态不进 Core 合同)。
- 外壳:226px 主侧栏(版本行 + BETA 徽章 + 扁平导航 + 「工具」可展开分组 + 底部设置),对齐 `FloatingShell`;<760px 回落横向胶囊导航。
- 设置弹窗:环境与设置 / AI 服务 / 本地模型 / 手机输入四分区收纳进居中 960x680 弹窗(遮罩 + 14px 圆角 + 左 rail),对齐 `SettingsModal`;这些入口不再占主导航。
- 组件:`ui/widgets.rs`(卡片/软底卡/状态胶囊/主·蓝·危险·次级按钮/分段控件/开关/键值行/页头);问答页按 #997 验证过的双气泡布局(用户右蓝软底、助手左 surface-2、流式/思考/错误行、底部输入组)。
- 状态:CI ubuntu 已有 linux-egui `cargo test + check` 门禁(`ci.yml` `linux-egui` 任务);真实 X11/Wayland 桌面的视觉与交互证据仍待实测(L11)。

## 1. 页面现状

| 页面 | 位置 | 已有操作 | 主要缺口 |
| --- | --- | --- | --- |
| Start | 主导航 | 2.0 概览卡(Core/管线状态、快速开始、其他任务入口);未连接保留诊断步骤 | — |
| Dictation | 主导航 | 普通听写(Core `dictation_engine`)、状态胶囊、转写卡 | — |
| Qa | 「工具」分组 | 2.0 双气泡会话、文本/语音提问、取消/关闭 | 编辑模式/应用/撤回 UI(L04) |
| Selection | 「工具」分组 | 划词润色预览(可编辑草稿)/确认/取消/撤回 | Selection Voice 生产触发/捕获/意图路由(L04) |
| Agent | 「工具」分组 | Less Computer 任务卡 + 输出卡;审批卡在页内 + 跨页横幅 | Agent 检测/模型/路径/权限配置(L09) |
| History | 主导航 | 只读最近 20 条卡片(时间 + 落字状态胶囊 + 文本) | 完整历史/统计/录音操作(L07,依赖 L03) |
| Services | 设置弹窗 | AI 服务状态胶囊、渠道管理(ASR/LLM 分段切换) | Omni 有效模式配置入口完整性(L09) |
| Models | 设置弹窗 | 本地模型目录卡(display name/家族/模式/语言/体积元数据)+ 下载/激活/取消 | 完整模型管理:路径/镜像/删除/预载/释放(L08) |
| Remote | 设置弹窗 | 启用/端口、连接状态、证书指纹核验、PIN/地址 | — |
| Settings | 设置弹窗 | 外观(深浅主题)、功能开关、环境准备卡 + 官方指南 | 词典/纠错/风格包/市场无页面(L05/L06);多数设置缺实际消费者(L09) |

## 2. 跨页约定

- Agent 审批固定在页面外展示,QA/Selection/Agent 的后台事件保留操作或导航提示
- 事件消费不得因导航停止(见[06](06-events-and-sessions.md));会话取消语义由 Core 承担。
- Agent 审批在 Agent 页内展示;其余页面顶部保留跨页审批横幅(允许/拒绝直达 Core)
- 事件消费不得因导航停止(见[06](06-events-and-sessions.md));会话取消语义由 Core 承担;设置弹窗打开时 Esc 只关弹窗、不触发语音取消
- 每个旧操作保留真实调用;页面增删不改变 Core 接口。

## 3. 新增页面的实施要求

L04–L08 每个领域补完整成功/失败/取消路径后再算完成;领域功能扩展时优先拆分 `main.rs`(当前较大),避免继续单文件增长。
L04–L08 每个领域补完整成功/失败/取消路径后再算完成;领域功能扩展时优先拆分 `main.rs`(当前较大),避免继续单文件增长。新增视觉元素一律引用 `design_tokens` / `ui::widgets`,不在页面里写裸色值。
4 changes: 3 additions & 1 deletion docs/linux-egui-handoff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ Linux Host Adapter 的剩余实现、全局热键、窗口/托盘/权限/更新
- 组装:`linux-egui/src/backend.rs` — `LinuxBackendBuilder::from_shared_providers(BackendConfig)` + `with_*` 注入(recorder/polisher/inserter/credential store/host actions/settings runtime/local ASR runtime/polish failure policy)。
- Host 门面:`lib.rs` `LinuxHost` — `snapshot` / `subscribe` / `save_settings` / `update_settings_strict` / `drain_events` / `backend()`。
- 已实现模块:`audio.rs`(CPAL 录制)、`credentials.rs`(Secret Service)、`fcitx5.rs`(输入/选区)、`hotkeys.rs`(fcitx5 热键监听)、`selection.rs`、`qa.rs`、`coding_agent.rs`、`marketplace.rs`、`remote_input.rs`、`settings.rs`、`single_instance.rs`、`capabilities.rs`、`resources.rs`、`runtime.rs`、`ui_state.rs`。
- 页面:`ui_state.rs` `Page` 枚举 10 页(Start/Dictation/Qa/Selection/Agent/Services/Models/Remote/History/Settings),主循环 `main.rs`。
- 2.0 视觉系统(2026-09-10):`design_tokens.rs`(tokens.css 对照表,跨平台单测)+ `ui/`(`theme.rs` 主题与 CJK 字体、`widgets.rs` 组件库、`prefs.rs` 外观偏好);页面渲染一律取令牌,不写裸色值。
- 页面:`ui_state.rs` `Page` 枚举 10 页(Start/Dictation/Qa/Selection/Agent/Services/Models/Remote/History/Settings),主循环 `main.rs`;主导航 = 开始/听写/历史 + 工具分组(问答/选区/Agent),Services/Models/Remote/Settings 收纳进设置弹窗(对齐 2.0 SettingsModal)。
- CI:`ci.yml` `linux-egui` 任务(ubuntu)跑 `cargo test + check -p openless-linux-egui --all-targets`,是 UI 改动的编译门禁。
- 打包:`release-linux-egui.yml`(deb/rpm/AppImage,独立 manifest)。

## 3. 阅读顺序
Expand Down
230 changes: 230 additions & 0 deletions openless-all/app/linux-egui/src/design_tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//! 2.0 设计令牌(tokens.css 的 Rust 对照表)。
//!
//! 本模块是纯数据:不依赖 egui,任何目标都能编译与单测。egui 侧的
//! 换算(Color32 / Style / Visuals)在 `ui` 模块,仅 Linux 编译。
//!
//! 数值必须与 `src/styles/tokens.css` 一一对应;修改任何一侧都要同步
//! 另一侧并通过本模块的对照测试。CSS 十六进制按 `0xRRGGBB` 转录,
//! 省略 alpha 的透明色按实际叠加效果取实色近似并在字段注释里标明。

/// 圆角阶梯(tokens.css `--ol-r-*`)。egui 0.31 的 `CornerRadius` 以 u8 计,
/// 这里直接存 u8;需要 f32 的场合由使用方转换。
pub mod radius {
/// `--ol-r-sm: 6px`(控件内小组件)
pub const SM: u8 = 6;
/// `--ol-control-radius: 8px`(按钮、导航项)
pub const CONTROL: u8 = 8;
/// `--ol-r-md: 10px`
pub const MD: u8 = 10;
/// `--ol-r-lg` / `--ol-card-radius: 14px`(卡片、设置弹窗)
pub const CARD: u8 = 14;
/// `--ol-panel-radius` / `--ol-r-xl: 18px`(浮层面板)
pub const PANEL: u8 = 18;
/// `--ol-shell-radius: 32px`(窗口外壳;Linux 由 WM 裁剪,这里供自定义绘制参考)
pub const SHELL: u8 = 32;
}

/// 侧栏宽度:FloatingShell `SIDEBAR_WIDTH = 226`。
pub const SIDEBAR_WIDTH: f32 = 226.0;
/// 设置弹窗最大宽度:SettingsModal `maxWidth: 960`。
pub const SETTINGS_MAX_WIDTH: f32 = 960.0;
/// 设置弹窗最大高度:SettingsModal `maxHeight: 680`。
pub const SETTINGS_MAX_HEIGHT: f32 = 680.0;

/// 一个主题的全部颜色令牌。字段名与 tokens.css 的 `--ol-*` 变量对应。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ThemeTokens {
/// `--ol-canvas`
pub canvas: u32,
/// `--ol-surface`(内容底;2.0 为纯白)
pub surface: u32,
/// `--ol-surface-2`(次级底 / 导航激活底)
pub surface_2: u32,
/// `--ol-line`
pub line: u32,
/// `--ol-line-strong`
pub line_strong: u32,
/// `--ol-line-soft`
pub line_soft: u32,
/// `--ol-ink`(主文字)
pub ink: u32,
/// `--ol-ink-2`
pub ink_2: u32,
/// `--ol-ink-3`(次级文字 / 导航未选中)
pub ink_3: u32,
/// `--ol-ink-4`(占位 / 弱化文字)
pub ink_4: u32,
/// `--ol-ink-5`
pub ink_5: u32,
/// `--ol-blue`(强调色)
pub blue: u32,
/// `--ol-blue-hover`
pub blue_hover: u32,
/// `--ol-blue-soft`(蓝色软底;暗色主题是 alpha 色,这里取实色近似)
pub blue_soft: u32,
/// `--ol-on-accent`
pub on_accent: u32,
/// `--ol-ok`
pub ok: u32,
/// `--ol-ok-soft`(暗色主题为 alpha 色,取实色近似)
pub ok_soft: u32,
/// `--ol-warn`
pub warn: u32,
/// `--ol-warn-soft`(暗色主题为 alpha 色,取实色近似)
pub warn_soft: u32,
/// `--ol-err`
pub err: u32,
/// `--ol-sidebar-bg`
pub sidebar_bg: u32,
/// `--ol-settings-rail-bg`
pub settings_rail_bg: u32,
/// `--ol-settings-content-bg`
pub settings_content_bg: u32,
/// `--ol-pill-selected-bg`(导航 / 分段选中深色胶囊;暗色主题为蓝色渐变,取上端色)
pub pill_selected_bg: u32,
/// `--ol-pill-selected-ink`
pub pill_selected_ink: u32,
/// `--ol-segmented-bg`(alpha 色,取实色近似)
pub segmented_bg: u32,
/// `--ol-segmented-active-bg`
pub segmented_active_bg: u32,
/// `--ol-overlay-bg`(设置弹窗遮罩;alpha 在 ui 层叠加,这里存 RGB)
pub overlay_rgb: u32,
}

/// 浅色主题(tokens.css `:root`,2.0 默认)。
pub const LIGHT: ThemeTokens = ThemeTokens {
canvas: 0xFFFFFF,
surface: 0xFFFFFF,
surface_2: 0xF4F4F5,
line: 0xE4E4E7,
line_strong: 0xD4D4D8,
line_soft: 0xF4F4F5,
ink: 0x09090B,
ink_2: 0x3F3F46,
ink_3: 0x71717A,
ink_4: 0xA1A1AA,
ink_5: 0xD4D4D8,
blue: 0x2563EB,
blue_hover: 0x1D4ED8,
blue_soft: 0xEFF4FF,
on_accent: 0xFFFFFF,
ok: 0x16A34A,
ok_soft: 0xECFDF5,
warn: 0xD97706,
warn_soft: 0xFFF7ED,
err: 0xDC2626,
sidebar_bg: 0xF0F0F1,
settings_rail_bg: 0xF0F0F1,
settings_content_bg: 0xF7F7F8,
pill_selected_bg: 0x18181B,
pill_selected_ink: 0xFFFFFF,
segmented_bg: 0xF4F4F5,
segmented_active_bg: 0xFFFFFF,
overlay_rgb: 0x0F1116,
};

/// 深色主题(tokens.css `[data-ol-theme='dark']`)。
pub const DARK: ThemeTokens = ThemeTokens {
canvas: 0x0C0C0E,
surface: 0x1C1C1F,
surface_2: 0x2A2A2E,
line: 0x27272A,
line_strong: 0x3F3F46,
line_soft: 0x1F1F23,
ink: 0xFAFAFA,
ink_2: 0xD4D4D8,
ink_3: 0xA1A1AA,
ink_4: 0x71717A,
ink_5: 0x3F3F46,
blue: 0x74B7FF,
blue_hover: 0x93C5FD,
// rgba(116,183,255,0.16) 叠加 surface 的实色近似。
blue_soft: 0x2A3A4E,
on_accent: 0xF8FBFF,
ok: 0x4ADE80,
// rgba(74,222,128,0.14) 叠加 surface 的实色近似。
ok_soft: 0x223529,
warn: 0xF59E0B,
// rgba(245,158,11,0.14) 叠加 surface 的实色近似。
warn_soft: 0x3A2E1C,
err: 0xF87171,
sidebar_bg: 0x141417,
settings_rail_bg: 0x141417,
settings_content_bg: 0x18181B,
// 暗色选中是蓝色渐变 3b82f6→2563eb,取上端。
pill_selected_bg: 0x3B82F6,
pill_selected_ink: 0xF4F7FB,
// rgba(226,232,240,0.07) 叠加 surface 的实色近似。
segmented_bg: 0x26262A,
segmented_active_bg: 0x27272A,
overlay_rgb: 0x05080D,
};

/// 按主题取令牌。
pub const fn tokens(dark: bool) -> &'static ThemeTokens {
if dark {
&DARK
} else {
&LIGHT
}
}

#[cfg(test)]
mod tests {
use super::*;

/// 关键令牌必须与 tokens.css 字面值一致:这组断言是两个 UI 之间的对照合同。
#[test]
fn light_tokens_match_tokens_css() {
assert_eq!(LIGHT.surface, 0xFFFFFF);
assert_eq!(LIGHT.surface_2, 0xF4F4F5);
assert_eq!(LIGHT.line, 0xE4E4E7);
assert_eq!(LIGHT.ink, 0x09090B);
assert_eq!(LIGHT.ink_3, 0x71717A);
assert_eq!(LIGHT.blue, 0x2563EB);
assert_eq!(LIGHT.blue_hover, 0x1D4ED8);
assert_eq!(LIGHT.blue_soft, 0xEFF4FF);
assert_eq!(LIGHT.ok, 0x16A34A);
assert_eq!(LIGHT.warn, 0xD97706);
assert_eq!(LIGHT.err, 0xDC2626);
assert_eq!(LIGHT.sidebar_bg, 0xF0F0F1);
assert_eq!(LIGHT.settings_content_bg, 0xF7F7F8);
assert_eq!(LIGHT.pill_selected_bg, 0x18181B);
}

#[test]
fn dark_tokens_match_tokens_css() {
assert_eq!(DARK.canvas, 0x0C0C0E);
assert_eq!(DARK.surface, 0x1C1C1F);
assert_eq!(DARK.line, 0x27272A);
assert_eq!(DARK.ink, 0xFAFAFA);
assert_eq!(DARK.blue, 0x74B7FF);
assert_eq!(DARK.ok, 0x4ADE80);
assert_eq!(DARK.err, 0xF87171);
assert_eq!(DARK.sidebar_bg, 0x141417);
// 暗色选中胶囊是蓝色渐变(3b82f6→2563eb),ui 层取上端色。
assert_eq!(DARK.pill_selected_bg, 0x3B82F6);
}

/// 明暗两套令牌都自洽:文字色和底色有足够对比(不与底色撞色)。
#[test]
fn themes_keep_ink_distinct_from_surfaces() {
for theme in [&LIGHT, &DARK] {
assert_ne!(theme.ink, theme.surface);
assert_ne!(theme.ink, theme.surface_2);
assert_ne!(theme.line, theme.surface);
assert_ne!(theme.blue, theme.surface);
}
}

#[test]
fn radii_match_tokens_css_scale() {
assert_eq!(radius::SM, 6);
assert_eq!(radius::CONTROL, 8);
assert_eq!(radius::CARD, 14);
assert_eq!(radius::PANEL, 18);
assert_eq!(radius::SHELL, 32);
assert_eq!(SIDEBAR_WIDTH, 226.0);
}
}
6 changes: 6 additions & 0 deletions openless-all/app/linux-egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod backend;
mod capabilities;
mod coding_agent;
mod credentials;
pub mod design_tokens;
mod fcitx5;
mod host_actions;
mod hotkeys;
Expand All @@ -21,6 +22,11 @@ mod selection;
mod settings;
mod single_instance;

/// 2.0 界面承接层(主题 / 组件 / 外观偏好)。egui 是 Linux 目标依赖,
/// 因此整棵子树只在 Linux 编译;颜色令牌本体在 [`design_tokens`](跨平台)。
#[cfg(target_os = "linux")]
pub mod ui;

pub use audio::LinuxCpalRecorder;
pub use backend::{LinuxBackendBuilder, LinuxBackendRuntime};
pub use capabilities::{LinuxCapabilitySnapshot, LinuxDesktopSession, LinuxPlatformApi};
Expand Down
Loading
Loading