mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(tui): preview each theme as the /theme picker moves
A bare /theme opens the quick picker; moving repaints, Enter keeps and stores the row, Escape restores the original. /theme list keeps the old listing.
This commit is contained in:
1 parent
903aa09cdc
commit
b5a79cda33
10 files changed
+173
-8
No files matched your search
+6
-1
@@ -573,7 +573,12 @@ groups instead of cutting labels in half. Visible labels and execution-setting v
|
||||
follow `/language`.
|
||||
|
||||
Use `/theme auto|light|dark` to select the terminal background mode, or choose a
|
||||
named accent from `/theme`. Both composer borders, the insertion cursor,
|
||||
named accent from `/theme`.
|
||||
|
||||
- `/theme` alone opens a picker that previews each row as the cursor reaches it.
|
||||
- Enter keeps the row, Esc restores the original, and `/theme list` prints the names.
|
||||
|
||||
Both composer borders, the insertion cursor,
|
||||
selection, scrollbar, and footer use the active CLI theme. See
|
||||
[Keyboard shortcuts](./GUIDE.md#keyboard-shortcuts) for transcript navigation,
|
||||
multiline input, rewind, and clipboard controls.
|
||||
|
||||
+7
-2
@@ -492,7 +492,12 @@ reasonix -p "同时更新两个项目" \
|
||||
设定值会跟随 `/language`。
|
||||
|
||||
使用 `/theme auto|light|dark` 选择终端背景模式,也可以从 `/theme` 列出的命名配色中选择
|
||||
强调色。输入框上下边线、插入光标、选区、滚动条和底栏都会使用当前 CLI 主题。Transcript
|
||||
强调色。
|
||||
|
||||
- 单独输入 `/theme` 会打开选择器,光标移到哪一行就即时预览。
|
||||
- Enter 保留,Esc 还原;`/theme list` 列出名称。
|
||||
|
||||
输入框上下边线、插入光标、选区、滚动条和底栏都会使用当前 CLI 主题。Transcript
|
||||
导航、多行输入、rewind 和剪贴板操作见[快捷键](./GUIDE.zh-CN.md#快捷键)。
|
||||
|
||||
剪贴板操作按内容类型明确分开。本地 transcript 和输入框选区写入系统剪贴板,并且只有写入
|
||||
@@ -515,7 +520,7 @@ SSH 下远端进程无法读取本机剪贴板,请使用终端粘贴快捷键
|
||||
| `/resume [n]` | 搜索最近会话并切换;`/resume <n>` 直接切换到列表中第 n 个会话。 |
|
||||
| `/status` | 显示模型、effort、cache、Git、后台任务,以及执行设定或余额信息。 |
|
||||
| `/preset [balanced\|delivery]` | 查看或切换 Agent 执行设定(不重建 Controller);`/work-mode` 与 `/profile` 为兼容别名,`economy`、`light` 都解析为 `balanced`。 |
|
||||
| `/theme [auto\|light\|dark\|style]` | 查看或切换 CLI 背景模式和强调色。 |
|
||||
| `/theme [auto\|light\|dark\|style\|list]` | 打开选择器预览并切换 CLI 背景模式和强调色。 |
|
||||
| `/currency [auto\|CNY\|USD]` | 查看或切换用户全局费用展示币种,并刷新当前运行时。 |
|
||||
| `/paste-image` | 读取剪贴板图片并插入可编辑的附件标记。 |
|
||||
| `/mouse` | 切换应用内鼠标选区、滚动条和滚轮处理。 |
|
||||
|
||||
@@ -235,7 +235,7 @@ var English = Messages{
|
||||
|
||||
OutputStyleNone: "no output styles available",
|
||||
ThemeHeader: "themes:",
|
||||
ThemeHint: "switch with /theme <auto|light|dark|style>",
|
||||
ThemeHint: "pick with /theme, or switch with /theme <auto|light|dark|style>",
|
||||
ThemeChangedFmt: "theme switched to %s / %s",
|
||||
ThemeUnknownFmt: "unknown theme %q",
|
||||
LanguageHeader: "languages:",
|
||||
|
||||
@@ -235,7 +235,7 @@ var Chinese = Messages{
|
||||
|
||||
OutputStyleNone: "没有可用的输出风格",
|
||||
ThemeHeader: "主题:",
|
||||
ThemeHint: "使用 /theme <auto|light|dark|style> 切换",
|
||||
ThemeHint: "用 /theme 选择,或用 /theme <auto|light|dark|style> 切换",
|
||||
ThemeChangedFmt: "已切换主题为 %s / %s",
|
||||
ThemeUnknownFmt: "未知主题 %q",
|
||||
LanguageHeader: "语言:",
|
||||
|
||||
@@ -232,7 +232,7 @@ var ChineseTraditional = Messages{
|
||||
|
||||
OutputStyleNone: "沒有可用的輸出風格",
|
||||
ThemeHeader: "主題:",
|
||||
ThemeHint: "使用 /theme <auto|light|dark|style> 切換",
|
||||
ThemeHint: "用 /theme 選擇,或用 /theme <auto|light|dark|style> 切換",
|
||||
ThemeChangedFmt: "已切換主題為 %s / %s",
|
||||
ThemeUnknownFmt: "未知主題 %q",
|
||||
LanguageHeader: "語言:",
|
||||
|
||||
@@ -45,3 +45,30 @@ func DescribeThemes() string {
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
// RestoreTheme puts back a palette ActiveTheme returned earlier, so a preview
|
||||
// that was never confirmed leaves nothing behind.
|
||||
func RestoreTheme(p Palette) {
|
||||
activeTheme = p
|
||||
refreshCLIStyles()
|
||||
}
|
||||
|
||||
// ThemeChoice is one row a theme picker offers: a mode or a named style.
|
||||
type ThemeChoice struct {
|
||||
ID string
|
||||
Description string
|
||||
Active bool
|
||||
}
|
||||
|
||||
// ThemeChoices lists the three modes and every style, marking the active style.
|
||||
func ThemeChoices() []ThemeChoice {
|
||||
out := []ThemeChoice{
|
||||
{ID: "auto", Description: "follow the terminal background"},
|
||||
{ID: "light", Description: "light palette, current accent kept when it fits"},
|
||||
{ID: "dark", Description: "dark palette, current accent kept when it fits"},
|
||||
}
|
||||
for _, st := range cliThemeStyles {
|
||||
out = append(out, ThemeChoice{ID: st.name, Description: st.mode + " · " + st.description, Active: st.name == activeTheme.Style})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -76,8 +76,43 @@ func (m *model) toggleDiffFold() {
|
||||
m.tr.AddNotice("info", i18n.M.DiffFoldDisabled)
|
||||
}
|
||||
|
||||
// openThemePicker previews each row as the cursor reaches it and puts the
|
||||
// original back unless a row is confirmed. Browsing writes nothing: the
|
||||
// preference is stored only through setTheme, as typing the name does.
|
||||
func (m *model) openThemePicker() {
|
||||
original := termrender.ActiveTheme()
|
||||
restore := func() {
|
||||
termrender.RestoreTheme(original)
|
||||
termrender.ApplyTextareaTheme(&m.composer)
|
||||
}
|
||||
var items []chooseItem
|
||||
for _, c := range termrender.ThemeChoices() {
|
||||
items = append(items, chooseItem{ID: c.ID, Label: c.ID, Desc: c.Description, Active: c.Active})
|
||||
}
|
||||
p := newQuickPicker(i18n.M.ThemeHeader, items, func(it chooseItem) tea.Cmd {
|
||||
restore()
|
||||
m.setTheme(it.ID)
|
||||
return m.commit()
|
||||
})
|
||||
p.onMove = func(it chooseItem) {
|
||||
restore()
|
||||
if termrender.IsThemeMode(it.ID) {
|
||||
termrender.SetThemeMode(it.ID)
|
||||
} else {
|
||||
termrender.SetThemeStyle(it.ID)
|
||||
}
|
||||
termrender.ApplyTextareaTheme(&m.composer)
|
||||
}
|
||||
p.onCancel = restore
|
||||
m.quick = p
|
||||
}
|
||||
|
||||
func (m *model) setTheme(name string) {
|
||||
if name == "" {
|
||||
switch strings.ToLower(name) {
|
||||
case "":
|
||||
m.openThemePicker()
|
||||
return
|
||||
case "list":
|
||||
m.tr.AddNotice("info", i18n.M.ThemeHeader+"\n"+termrender.DescribeThemes()+"\n"+i18n.M.ThemeHint)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func TestThemeListsSwitchesAndPersists(t *testing.T) {
|
||||
m, k, cfgPath := miscModel(t)
|
||||
prev := termrender.ActiveTheme()
|
||||
t.Cleanup(func() { termrender.SetThemeStyle(prev.Style) })
|
||||
sendLine(m, "/theme")
|
||||
sendLine(m, "/theme list")
|
||||
if got := notices(m); !strings.Contains(got, "graphite") || !strings.Contains(got, i18n.M.ThemeHint) {
|
||||
t.Fatalf("listing = %q", got)
|
||||
}
|
||||
@@ -205,3 +205,81 @@ func TestThemeAndVerboseNeverWriteIntoAProjectFile(t *testing.T) {
|
||||
t.Fatalf("user config missing the preferences:\n%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func themePickerModel(t *testing.T) (*model, string) {
|
||||
t.Helper()
|
||||
m, _, cfgPath := miscModel(t)
|
||||
termrender.SetThemeStyle("graphite")
|
||||
t.Cleanup(func() { termrender.SetThemeStyle("graphite") })
|
||||
sendLine(m, "/theme")
|
||||
if m.quick == nil {
|
||||
t.Fatal("/theme did not open the picker")
|
||||
}
|
||||
return m, cfgPath
|
||||
}
|
||||
|
||||
func TestThemePickerPreviewsWhileMovingAndWritesNothing(t *testing.T) {
|
||||
m, cfgPath := themePickerModel(t)
|
||||
before := termrender.ActiveTheme()
|
||||
run(m, press(m, "down"))
|
||||
moved := m.quick.shown()[m.quick.sel]
|
||||
if got := termrender.ActiveTheme(); got.Style == before.Style && got.Name == before.Name {
|
||||
t.Fatalf("moving to %q did not preview it: still %s/%s", moved.ID, got.Name, got.Style)
|
||||
}
|
||||
if _, err := os.Stat(cfgPath); err == nil {
|
||||
t.Fatal("browsing wrote the configuration")
|
||||
}
|
||||
run(m, press(m, "down"))
|
||||
run(m, press(m, "up"))
|
||||
if _, err := os.Stat(cfgPath); err == nil {
|
||||
t.Fatal("browsing wrote the configuration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemePickerEscapeRestoresTheOriginal(t *testing.T) {
|
||||
m, cfgPath := themePickerModel(t)
|
||||
before := termrender.ActiveTheme()
|
||||
for range 3 {
|
||||
run(m, press(m, "down"))
|
||||
}
|
||||
run(m, press(m, "esc"))
|
||||
if m.quick != nil {
|
||||
t.Fatal("Escape left the picker open")
|
||||
}
|
||||
if got := termrender.ActiveTheme(); got != before {
|
||||
t.Fatalf("Escape left %s/%s, want %s/%s", got.Name, got.Style, before.Name, before.Style)
|
||||
}
|
||||
if _, err := os.Stat(cfgPath); err == nil {
|
||||
t.Fatal("cancelling wrote the configuration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemePickerEnterKeepsTheChoiceAndStoresIt(t *testing.T) {
|
||||
m, cfgPath := themePickerModel(t)
|
||||
for m.quick.shown()[m.quick.sel].ID != "glacier" {
|
||||
run(m, press(m, "down"))
|
||||
}
|
||||
run(m, press(m, "enter"))
|
||||
if m.quick != nil {
|
||||
t.Fatal("Enter left the picker open")
|
||||
}
|
||||
if got := termrender.ActiveTheme(); got.Style != "glacier" || got.Name != "light" {
|
||||
t.Fatalf("active = %s/%s, want light/glacier", got.Name, got.Style)
|
||||
}
|
||||
raw, _ := os.ReadFile(cfgPath)
|
||||
if !strings.Contains(string(raw), "glacier") {
|
||||
t.Fatalf("choice not stored:\n%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemePickerFilterPreviewsTheFirstMatch(t *testing.T) {
|
||||
m, _ := themePickerModel(t)
|
||||
typeText(m, "glac")
|
||||
if got := termrender.ActiveTheme(); got.Style != "glacier" {
|
||||
t.Fatalf("filtering to glacier previewed %s", got.Style)
|
||||
}
|
||||
run(m, press(m, "esc"))
|
||||
if got := termrender.ActiveTheme(); got.Style != "graphite" {
|
||||
t.Fatalf("Escape left %s, want graphite", got.Style)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,10 @@ type quickPicker struct {
|
||||
query string
|
||||
sel int
|
||||
pick func(chooseItem) tea.Cmd
|
||||
// onMove, when set, sees the row under the cursor each time it changes, and
|
||||
// onCancel runs when the picker closes without a choice: a live preview.
|
||||
onMove func(chooseItem)
|
||||
onCancel func()
|
||||
}
|
||||
|
||||
func newQuickPicker(title string, items []chooseItem, pick func(chooseItem) tea.Cmd) *quickPicker {
|
||||
@@ -61,6 +65,10 @@ func (m *model) quickKey(msg tea.KeyPressMsg) (tea.Cmd, bool) {
|
||||
switch k := msg.String(); k {
|
||||
case "esc", "ctrl+c":
|
||||
m.quick = nil
|
||||
if p.onCancel != nil {
|
||||
p.onCancel()
|
||||
}
|
||||
return nil, true
|
||||
case "up", "ctrl+p":
|
||||
p.sel = max(p.sel-1, 0)
|
||||
case "down", "tab", "ctrl+n":
|
||||
@@ -80,6 +88,11 @@ func (m *model) quickKey(msg tea.KeyPressMsg) (tea.Cmd, bool) {
|
||||
p.query, p.sel = p.query+t, 0
|
||||
}
|
||||
}
|
||||
if p.onMove != nil {
|
||||
if rows := p.shown(); p.sel < len(rows) {
|
||||
p.onMove(rows[p.sel])
|
||||
}
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- 终端界面的 `/theme` 现在打开选择器:光标移到哪一行就即时预览,Enter 保留,Esc 还原;`/theme list` 仍列出名称。关联 #12985。
|
||||
- In the terminal UI, `/theme` now opens a picker that previews each row as you move, keeps it on Enter and restores the original on Esc; `/theme list` still prints the names. Refs #12985.
|
||||
Reference in new issue
Block a user