feat: Linear 风格 UI 主题(近黑/纯白扁平调色板、靛蓝主色、14px 密度与 6px 圆角)
Release / Meta (push) Canceled after 0s
Release / Test (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Publish package (push) Canceled after 0s

This commit is contained in:
2026-09-28 22:49:56 +08:00
parent cd6b6f3201
commit 4e70741f55
4 changed files with 650 additions and 2 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
- **连接测试** — 表单内握手测试(超时可配置),保存前验证连通性
- **日志面板** — 连接级事件日志(CONNACK/SUBACK/PUBACK/错误/重连等),按日落盘 `mqttx-YYYY-MM-DD.log`,设置内一键打开日志目录
- **快捷键** — `Ctrl+N` 新建连接、`Ctrl+,` 设置、`Ctrl+Shift+E` 导出、`Ctrl+Shift+I` 导入、`Ctrl+Enter` 发送消息
- **主题** — 浅色 / 深色 / 跟随系统
- **主题** — Linear 风格配色(近黑/纯白扁平底 + 靛蓝 #5e6ad2 主色,14px 紧凑排版、6px 圆角),浅色 / 深色 / 跟随系统
- **设置** — 自动检查更新开关、消息缓存条数、时间戳毫秒、数据目录展示与打开、关于页(版本 / 许可)
- **本地持久化** — 连接、订阅、预设、变量、设置以 JSON 存于系统数据目录
+24 -1
View File
@@ -11,7 +11,7 @@ use gpui_kit::component::menu::{DropdownMenu as _, PopupMenuItem};
use gpui_kit::component::tab::{Tab, TabBar};
use gpui_kit::component::{
h_flex, notification::Notification, v_flex, ActiveTheme as _, Selectable as _, Sizable as _,
StyledExt as _, Theme, ThemeMode, TitleBar, WindowExt as _,
StyledExt as _, Theme, ThemeMode, ThemeRegistry, TitleBar, WindowExt as _,
};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::{
@@ -816,7 +816,14 @@ impl MqttXApp {
/// 应用主题偏好。
///
/// 「跟随系统」读取 gpui 的窗口外观(`App::window_appearance`);系统深浅色变化由
/// Linear 风格主题(内嵌 JSON,模式与 gpui-component 默认主题同构)。
const LINEAR_THEME: &str = include_str!("linear-theme.json");
/// `MqttXApp::new` 里注册的 `Window::observe_window_appearance` 监听并重应用本函数。
///
/// 流程:先 `change` 确保 Theme 全局已建立(注册表 observer 依赖它)→ 注册
/// Linear Light/Dark(重名跳过,幂等)→ 把当前明暗对指向 Linear → 再 `change`
/// 应用其颜色/字号/圆角。
pub fn apply_theme(pref: ThemeModePref, cx: &mut App) {
let mode = match pref {
ThemeModePref::Light => ThemeMode::Light,
@@ -824,6 +831,22 @@ pub fn apply_theme(pref: ThemeModePref, cx: &mut App) {
ThemeModePref::System => ThemeMode::from(cx.window_appearance()),
};
Theme::change(mode, None, cx);
if let Err(e) = ThemeRegistry::global_mut(cx).load_themes_from_str(LINEAR_THEME) {
eprintln!("加载 Linear 主题失败,回退默认主题: {e}");
}
{
let reg = ThemeRegistry::global(cx);
let light = reg.themes().get("Linear Light").cloned();
let dark = reg.themes().get("Linear Dark").cloned();
let theme = Theme::global_mut(cx);
if let Some(l) = light {
theme.light_theme = l;
}
if let Some(d) = dark {
theme.dark_theme = d;
}
}
Theme::change(mode, None, cx);
}
// ─── 渲染 ────────────────────────────────────────────────────────────────────
+423
View File
@@ -0,0 +1,423 @@
{
"themes": [
{
"name": "Linear Light",
"mode": "light",
"font_size": 14,
"radius": 6,
"radius_lg": 10,
"colors": {
"accent.background": "#eeeef0",
"accent.foreground": "#0d0e10",
"accordion.background": "#f7f7f8",
"background": "#ffffff",
"border": "#e5e5e7",
"group_box.background": "#ffffff",
"group_box.foreground": "#0d0e10",
"caret": "#0d0e10",
"chart.1": "#93c5fd",
"chart.2": "#3b82f6",
"chart.3": "#2563eb",
"chart.4": "#1d4ed8",
"chart.5": "#1e40af",
"chart.bullish": "green-600",
"chart.bearish": "red-600",
"danger.background": "#e5484d",
"danger.foreground": "#ffffff",
"description_list_label.foreground": "#6f7076",
"drag_border": "#5e6ad2",
"drop_target.background": "#5e6ad219",
"foreground": "#0d0e10",
"info.background": "#5e6ad2",
"info.foreground": "#ffffff",
"input.border": "#e5e5e7",
"link.foreground": "#0d0e10",
"link.active.foreground": "#3d3e44",
"link.hover.foreground": "#000000",
"list.background": "#ffffff",
"list.active.background": "#5e6ad233",
"list.active.border": "#5e6ad2",
"list.even.background": "#f1f1f266",
"list.head.background": "#f1f1f266",
"list.hover.background": "#f5f5f5",
"muted.background": "#f1f1f2",
"muted.foreground": "#6f7076",
"popover.background": "#ffffff",
"popover.foreground": "#0d0e10",
"primary.background": "#5e6ad2",
"primary.active.background": "#5661c4",
"primary.foreground": "#ffffff",
"primary.hover.background": "#6b76d4",
"progress_bar.background": "#5e6ad2",
"ring": "#5e6ad2",
"scrollbar.background": "#ffffff00",
"scrollbar.thumb.background": "#c9c9cee6",
"scrollbar.thumb.hover.background": "#b5b5bb",
"secondary.background": "#f7f7f8",
"secondary.active.background": "#e7e7e9",
"secondary.foreground": "#0d0e10",
"secondary.hover.background": "#eeeef0",
"selection.background": "#5e6ad2",
"sidebar.background": "#fbfbfb",
"sidebar.accent.background": "#eeeef0",
"sidebar.accent.foreground": "#0d0e10",
"sidebar.border": "#e5e5e7",
"sidebar.foreground": "#0d0e10",
"sidebar.primary.background": "#0d0e10",
"sidebar.primary.foreground": "#ffffff",
"skeleton.background": "#eeeef0",
"slider.bar.background": "#0d0e10",
"slider.thumb.background": "#ffffff",
"success.background": "#4cb782",
"success.foreground": "#ffffff",
"switch.background": "#d7d7db",
"tab.background": "#00000000",
"tab.active.background": "#ffffff",
"tab.active.foreground": "#0d0e10",
"tab_bar.background": "#ffffff",
"tab_bar.segmented.background": "#f1f1f2",
"tab.foreground": "#6f7076",
"table.background": "#ffffff",
"table.active.background": "#bfdbfe33",
"table.active.border": "#60a5fa",
"table.even.background": "#fafafa",
"table.head.background": "#fafafa",
"table.head.foreground": "#6f7076",
"table.hover.background": "#f5f5f5",
"table.row.border": "#ebebed",
"title_bar.background": "#ffffff",
"title_bar.border": "#e5e5e7",
"status_bar.background": "#ffffff",
"status_bar.border": "#e5e5e7",
"warning.background": "#f2a93b",
"warning.foreground": "#3a2504",
"overlay": "#00000033",
"window.border": "#e5e5e7",
"base.red": "red-600",
"base.red.light": "red-400",
"base.green": "green-600",
"base.green.light": "green-400",
"base.blue": "blue-600",
"base.blue.light": "blue-400",
"base.yellow": "yellow-600",
"base.yellow.light": "yellow-400",
"base.magenta": "purple-600",
"base.magenta.light": "purple-400",
"base.cyan": "cyan-600",
"base.cyan.light": "cyan-400",
"description_list_label.background": "#f7f7f8"
},
"highlight": {
"editor.foreground": "#000000",
"editor.background": "#ffffff",
"editor.active_line.background": "#F5F5F5",
"editor.line_number": "#929292",
"editor.active_line_number": "#000000",
"editor.invisible": "#73737366",
"conflict": "#C5060B",
"created": "#1642FF",
"hidden": "#6D6D6D",
"hint": "#9e5dff",
"modified": "#9e7008",
"predictive": "#A4ABB6",
"warning": "#C99401",
"syntax": {
"attribute": {
"color": "#957931"
},
"boolean": {
"color": "#C5060B"
},
"comment": {
"color": "#007fff"
},
"comment.doc": {
"color": "#007fff"
},
"constant": {
"color": "#C5060B"
},
"constructor": {
"color": "#0433ff"
},
"embedded": {
"color": "#333333"
},
"emphasis": {
"font_style": "italic"
},
"emphasis.strong": {
"font_weight": 700
},
"function": {
"color": "#0000A2"
},
"keyword": {
"color": "#0433ff"
},
"link_text": {
"color": "#0000A2",
"font_style": "normal"
},
"link_uri": {
"color": "#6A7293",
"font_style": "italic"
},
"number": {
"color": "#0433ff"
},
"string": {
"color": "#036A07"
},
"string.escape": {
"color": "#036A07"
},
"string.regex": {
"color": "#036A07"
},
"string.special": {
"color": "#d21f07"
},
"string.special.symbol": {
"color": "#d21f07"
},
"tag": {
"color": "#0433ff"
},
"text.literal": {
"color": "#6F42C1"
},
"text.code.span": {
"color": "#6F42C1"
},
"title": {
"color": "#0433FF"
},
"type": {
"color": "#6f42c1"
},
"property": {
"color": "#333333"
},
"variable": {
"color": "#333333"
},
"variable.special": {
"color": "#C5060B"
}
}
}
},
{
"name": "Linear Dark",
"mode": "dark",
"font_size": 14,
"radius": 6,
"radius_lg": 10,
"colors": {
"accent.background": "#1c1d1f",
"accent.foreground": "#f7f8f8",
"accordion.background": "#101012",
"background": "#08090a",
"border": "#2b2c31",
"group_box.background": "#08090a",
"group_box.foreground": "#f7f8f8",
"caret": "#f7f8f8",
"chart.1": "#93c5fd",
"chart.2": "#3b82f6",
"chart.3": "#2563eb",
"chart.4": "#1d4ed8",
"chart.5": "#1e40af",
"chart.bullish": "green-600",
"chart.bearish": "red-600",
"danger.background": "#e5484d",
"danger.foreground": "#ffe9e9",
"description_list_label.background": "#101012",
"description_list_label.foreground": "#8a8f98",
"drag_border": "#5e6ad2",
"drop_target.background": "#5e6ad219",
"foreground": "#f7f8f8",
"info.background": "#5e6ad2",
"info.foreground": "#e0e3ff",
"input.border": "#2b2c31",
"link.foreground": "#f7f8f8",
"link.active.foreground": "#d5d7de",
"link.hover.foreground": "#ffffff",
"list.background": "#08090a",
"list.active.background": "#5e6ad233",
"list.active.border": "#5e6ad2",
"list.even.background": "#10101266",
"list.head.background": "#10101266",
"muted.background": "#1d1e22",
"muted.foreground": "#8a8f98",
"popover.background": "#101012",
"popover.foreground": "#f7f8f8",
"primary.background": "#5e6ad2",
"primary.active.background": "#5661c4",
"primary.foreground": "#ffffff",
"primary.hover.background": "#6e79d6",
"progress_bar.background": "#5e6ad2",
"ring": "#5e6ad2",
"scrollbar.background": "#08090a00",
"scrollbar.thumb.background": "#3a3b42e6",
"scrollbar.thumb.hover.background": "#4a4b54",
"secondary.background": "#101012",
"secondary.active.background": "#232429",
"secondary.foreground": "#f7f8f8",
"secondary.hover.background": "#1c1d1f",
"selection.background": "#5e6ad2",
"sidebar.background": "#08090a",
"sidebar.accent.background": "#1c1d1f",
"sidebar.accent.foreground": "#f7f8f8",
"sidebar.border": "#1f2024",
"sidebar.foreground": "#f7f8f8",
"sidebar.primary.background": "#f7f8f8",
"sidebar.primary.foreground": "#08090a",
"skeleton.background": "#1c1d1f",
"slider.bar.background": "#f7f8f8",
"slider.thumb.background": "#08090a",
"success.background": "#4cb782",
"success.foreground": "#0b3a26",
"switch.background": "#2b2c31",
"tab.background": "#00000000",
"tab.active.background": "#101012",
"tab.active.foreground": "#f7f8f8",
"tab_bar.background": "#08090a",
"tab_bar.segmented.background": "#101012",
"tab.foreground": "#8a8f98",
"table.background": "#08090a",
"table.head.foreground": "#8a8f98",
"table.row.border": "#1f2024b3",
"title_bar.background": "#08090a",
"title_bar.border": "#1f2024",
"status_bar.background": "#08090a",
"status_bar.border": "#1f2024",
"warning.background": "#f2a93b",
"warning.foreground": "#3a2504",
"overlay": "#00000066",
"window.border": "#1f2024",
"base.red": "red-400",
"base.red.light": "red-300",
"base.green": "green-400",
"base.green.light": "green-300",
"base.blue": "blue-400",
"base.blue.light": "blue-300",
"base.yellow": "yellow-400",
"base.yellow.light": "yellow-300",
"base.magenta": "purple-400",
"base.magenta.light": "purple-300",
"base.cyan": "cyan-400",
"base.cyan.light": "cyan-300"
},
"highlight": {
"editor.foreground": "#CACCCA",
"editor.background": "#0a0a0a",
"editor.active_line.background": "#171717",
"editor.line_number": "#8F8F8F",
"editor.active_line_number": "#DDDDDD",
"editor.invisible": "#73737366",
"conflict": "#D2602D",
"created": "#3f72e2",
"created.background": "#0C4619",
"deleted.background": "#46190C",
"error.background": "#46190C",
"error.border": "#E44A4F",
"hidden": "#9E9E9E",
"hint": "#b283f8",
"hint.background": "#250c4b",
"hint.border": "#3f0891",
"info.background": "#0059D1",
"info.border": "#0059D1",
"modified": "#B0A878",
"modified.background": "#3A310E",
"predictive": "#5D5945",
"success.background": "#0C4619",
"warning.background": "#3A310E",
"warning.border": "#7B6508",
"syntax": {
"attribute": {
"color": "#7FAEF9"
},
"boolean": {
"color": "#CC9E00"
},
"comment": {
"color": "#9D9D9D"
},
"comment.doc": {
"color": "#9D9D9D"
},
"constant": {
"color": "#CC9E00"
},
"constructor": {
"color": "#CBA6F7"
},
"embedded": {
"color": "#CACCCA"
},
"emphasis": {
"font_style": "italic"
},
"emphasis.strong": {
"font_weight": 700
},
"function": {
"color": "#B3C5F3"
},
"keyword": {
"color": "#87B1F6"
},
"link_text": {
"color": "#419CFF",
"font_style": "normal"
},
"link_uri": {
"color": "#7faef9",
"font_style": "italic"
},
"number": {
"color": "#CC9E00"
},
"string": {
"color": "#A3E09F"
},
"string.escape": {
"color": "#68DC7C"
},
"string.regex": {
"color": "#68DC7C"
},
"string.special": {
"color": "#A3E09F"
},
"string.special.symbol": {
"color": "#A3E09F"
},
"tag": {
"color": "#419CFF"
},
"text.literal": {
"color": "#A3E09F"
},
"text.code.span": {
"color": "#A3E09F"
},
"title": {
"color": "#CC9E00",
"font_weight": 600
},
"type": {
"color": "#CBA6F7"
},
"property": {
"color": "#BCC4E0"
},
"variable.special": {
"color": "#419CFF"
}
}
}
}
]
}
+202
View File
@@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
"""从 gpui-component 默认主题生成 Linear 风格主题 linear-theme.json。
映射规则见各表;未列出的键(chart/base 等)保留默认值。"""
import json, sys, collections
SRC = sys.argv[1]
DST = sys.argv[2]
with open(SRC, "r", encoding="utf-8") as f:
base = json.load(f)
# ── Linear Dark ────────────────────────────────────────────────────────────
DARK = {
"background": "#08090a",
"foreground": "#f7f8f8",
"accordion.background": "#101012",
"border": "#2b2c31",
"group_box.background": "#08090a",
"group_box.foreground": "#f7f8f8",
"caret": "#f7f8f8",
"accent.background": "#1c1d1f",
"accent.foreground": "#f7f8f8",
"danger.background": "#e5484d",
"danger.foreground": "#ffe9e9",
"description_list_label.background": "#101012",
"description_list_label.foreground": "#8a8f98",
"drag_border": "#5e6ad2",
"drop_target.background": "#5e6ad219",
"input.border": "#2b2c31",
"link.foreground": "#f7f8f8",
"link.active.foreground": "#d5d7de",
"link.hover.foreground": "#ffffff",
"list.background": "#08090a",
"list.active.background": "#5e6ad233",
"list.active.border": "#5e6ad2",
"list.even.background": "#10101266",
"list.head.background": "#10101266",
"muted.background": "#1d1e22",
"muted.foreground": "#8a8f98",
"popover.background": "#101012",
"popover.foreground": "#f7f8f8",
"primary.background": "#5e6ad2",
"primary.hover.background": "#6e79d6",
"primary.active.background": "#5661c4",
"primary.foreground": "#ffffff",
"progress_bar.background": "#5e6ad2",
"ring": "#5e6ad2",
"scrollbar.background": "#08090a00",
"scrollbar.thumb.background": "#3a3b42e6",
"scrollbar.thumb.hover.background": "#4a4b54",
"secondary.background": "#101012",
"secondary.hover.background": "#1c1d1f",
"secondary.active.background": "#232429",
"secondary.foreground": "#f7f8f8",
"selection.background": "#5e6ad2",
"sidebar.background": "#08090a",
"sidebar.accent.background": "#1c1d1f",
"sidebar.accent.foreground": "#f7f8f8",
"sidebar.border": "#1f2024",
"sidebar.foreground": "#f7f8f8",
"sidebar.primary.background": "#f7f8f8",
"sidebar.primary.foreground": "#08090a",
"skeleton.background": "#1c1d1f",
"slider.bar.background": "#f7f8f8",
"slider.thumb.background": "#08090a",
"success.background": "#4cb782",
"success.foreground": "#0b3a26",
"switch.background": "#2b2c31",
"tab.background": "#00000000",
"tab.active.background": "#101012",
"tab.active.foreground": "#f7f8f8",
"tab.foreground": "#8a8f98",
"tab_bar.background": "#08090a",
"tab_bar.segmented.background": "#101012",
"table.background": "#08090a",
"table.head.foreground": "#8a8f98",
"table.row.border": "#1f2024b3",
"title_bar.background": "#08090a",
"title_bar.border": "#1f2024",
"status_bar.background": "#08090a",
"status_bar.border": "#1f2024",
"warning.background": "#f2a93b",
"warning.foreground": "#3a2504",
"info.background": "#5e6ad2",
"info.foreground": "#e0e3ff",
"overlay": "#00000066",
"window.border": "#1f2024",
}
# ── Linear Light ───────────────────────────────────────────────────────────
LIGHT = {
"background": "#ffffff",
"foreground": "#0d0e10",
"accordion.background": "#f7f7f8",
"border": "#e5e5e7",
"group_box.background": "#ffffff",
"group_box.foreground": "#0d0e10",
"caret": "#0d0e10",
"accent.background": "#eeeef0",
"accent.foreground": "#0d0e10",
"danger.background": "#e5484d",
"danger.foreground": "#ffffff",
"description_list_label.background": "#f7f7f8",
"description_list_label.foreground": "#6f7076",
"drag_border": "#5e6ad2",
"drop_target.background": "#5e6ad219",
"input.border": "#e5e5e7",
"link.foreground": "#0d0e10",
"link.active.foreground": "#3d3e44",
"link.hover.foreground": "#000000",
"list.background": "#ffffff",
"list.active.background": "#5e6ad233",
"list.active.border": "#5e6ad2",
"list.even.background": "#f1f1f266",
"list.head.background": "#f1f1f266",
"muted.background": "#f1f1f2",
"muted.foreground": "#6f7076",
"popover.background": "#ffffff",
"popover.foreground": "#0d0e10",
"primary.background": "#5e6ad2",
"primary.hover.background": "#6b76d4",
"primary.active.background": "#5661c4",
"primary.foreground": "#ffffff",
"progress_bar.background": "#5e6ad2",
"ring": "#5e6ad2",
"scrollbar.background": "#ffffff00",
"scrollbar.thumb.background": "#c9c9cee6",
"scrollbar.thumb.hover.background": "#b5b5bb",
"secondary.background": "#f7f7f8",
"secondary.hover.background": "#eeeef0",
"secondary.active.background": "#e7e7e9",
"secondary.foreground": "#0d0e10",
"selection.background": "#5e6ad2",
"sidebar.background": "#fbfbfb",
"sidebar.accent.background": "#eeeef0",
"sidebar.accent.foreground": "#0d0e10",
"sidebar.border": "#e5e5e7",
"sidebar.foreground": "#0d0e10",
"sidebar.primary.background": "#0d0e10",
"sidebar.primary.foreground": "#ffffff",
"skeleton.background": "#eeeef0",
"slider.bar.background": "#0d0e10",
"slider.thumb.background": "#ffffff",
"success.background": "#4cb782",
"success.foreground": "#ffffff",
"switch.background": "#d7d7db",
"tab.background": "#00000000",
"tab.active.background": "#ffffff",
"tab.active.foreground": "#0d0e10",
"tab.foreground": "#6f7076",
"tab_bar.background": "#ffffff",
"tab_bar.segmented.background": "#f1f1f2",
"table.background": "#ffffff",
"table.head.foreground": "#6f7076",
"table.row.border": "#ebebed",
"title_bar.background": "#ffffff",
"title_bar.border": "#e5e5e7",
"status_bar.background": "#ffffff",
"status_bar.border": "#e5e5e7",
"warning.background": "#f2a93b",
"warning.foreground": "#3a2504",
"info.background": "#5e6ad2",
"info.foreground": "#ffffff",
"overlay": "#00000033",
"window.border": "#e5e5e7",
}
def build(theme, overrides, name):
colors = dict(theme.get("colors") or {})
missing = [k for k in overrides if k not in colors]
if missing:
print(f"WARN {name}: 目标主题缺少键 {missing}", file=sys.stderr)
colors.update(overrides)
out = collections.OrderedDict()
# 键序跟随默认主题,新键追加
for k in (theme.get("colors") or {}):
out[k] = colors[k]
for k in overrides:
if k not in out:
out[k] = overrides[k]
cfg = collections.OrderedDict()
cfg["name"] = name
cfg["mode"] = theme["mode"]
cfg["font_size"] = 14
cfg["radius"] = 6
cfg["radius_lg"] = 10
cfg["colors"] = out
if theme.get("highlight"):
cfg["highlight"] = theme["highlight"]
return cfg
by_mode = {t["mode"]: t for t in base["themes"]}
themes = [
build(by_mode["light"], LIGHT, "Linear Light"),
build(by_mode["dark"], DARK, "Linear Dark"),
]
doc = {"themes": themes}
with open(DST, "w", encoding="utf-8") as f:
json.dump(doc, f, ensure_ascii=False, indent=2)
print(f"OK -> {DST} ({len(themes)} themes)")