feat: Ctrl+M tab switcher overlay menu
All checks were successful
CI / build (push) Successful in 1m45s

Replace Alt+1-5 with Ctrl+M that opens a centered overlay showing
all tabs with descriptions. Navigate with arrows/j/k, select with
enter or 1-5, cancel with esc.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-20 00:14:16 +02:00
parent e6fdec4ff5
commit bb3b3038d3
2 changed files with 128 additions and 49 deletions

View File

@@ -83,7 +83,7 @@ Commands:
help Show this help
TUI Controls:
Alt+1-5 Switch tabs (Dashboard/Chat/Workflow/Agents/Config)
Ctrl+M Open tab switcher (navigate with arrows, select with enter)
Tab / Shift+Tab Cycle tabs
Ctrl+C Show quit confirmation (press twice quickly to force quit)

View File

@@ -208,6 +208,8 @@ type Model struct {
showingQuit bool
confirmCursor int
showingTabMenu bool
tabMenuCursor int
ctrlCCount int
lastCtrlC time.Time
@@ -224,11 +226,7 @@ type keyMap struct {
Quit key.Binding
Confirm key.Binding
Cancel key.Binding
Dashboard key.Binding
Chat key.Binding
Workflow key.Binding
Agents key.Binding
Config key.Binding
TabMenu key.Binding
Install key.Binding
Update key.Binding
Scan key.Binding
@@ -257,25 +255,9 @@ var keys = keyMap{
key.WithKeys("n", "esc"),
key.WithHelp("n/esc", "no"),
),
Dashboard: key.NewBinding(
key.WithKeys("alt+1"),
key.WithHelp("alt+1", "dashboard"),
),
Chat: key.NewBinding(
key.WithKeys("alt+2"),
key.WithHelp("alt+2", "chat"),
),
Workflow: key.NewBinding(
key.WithKeys("alt+3"),
key.WithHelp("alt+3", "workflow"),
),
Agents: key.NewBinding(
key.WithKeys("alt+4"),
key.WithHelp("alt+4", "agents"),
),
Config: key.NewBinding(
key.WithKeys("alt+5"),
key.WithHelp("alt+5", "config"),
TabMenu: key.NewBinding(
key.WithKeys("ctrl+m"),
key.WithHelp("ctrl+m", "switch tab"),
),
Install: key.NewBinding(
key.WithKeys("i"),
@@ -300,13 +282,13 @@ var keys = keyMap{
}
func (k keyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Dashboard, k.Chat, k.Workflow, k.Agents, k.Config, k.Quit}
return []key.Binding{k.TabMenu, k.Tab, k.Quit}
}
func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Dashboard, k.Chat, k.Workflow, k.Agents, k.Config},
{k.Tab, k.Prev, k.Quit},
{k.TabMenu, k.Tab, k.Prev},
{k.Quit},
}
}
@@ -355,6 +337,8 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
spinner: sp,
showingQuit: false,
confirmCursor: 1,
showingTabMenu: false,
tabMenuCursor: 0,
}
}
@@ -490,6 +474,9 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.showingQuit {
return m.handleQuitConfirm(msg)
}
if m.showingTabMenu {
return m.handleTabMenu(msg)
}
switch msg.String() {
case "ctrl+c":
@@ -503,25 +490,10 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.confirmCursor = 1
m.viewport.SetContent(m.renderContent())
return m, nil
case "alt+1":
m.activeTab = tabDashboard
m.resizeViewport()
return m, nil
case "alt+2":
m.activeTab = tabChat
m.resizeViewport()
return m, nil
case "alt+3":
m.activeTab = tabWorkflow
m.resizeViewport()
return m, nil
case "alt+4":
m.activeTab = tabAgents
m.resizeViewport()
return m, nil
case "alt+5":
m.activeTab = tabConfig
m.resizeViewport()
case "ctrl+m":
m.showingTabMenu = true
m.tabMenuCursor = int(m.activeTab)
m.viewport.SetContent(m.renderContent())
return m, nil
case "tab":
m.activeTab = (m.activeTab + 1) % tabCount
@@ -594,6 +566,108 @@ func (m Model) handleQuitConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "ctrl+c", "esc":
m.showingTabMenu = false
m.viewport.SetContent(m.renderContent())
return m, nil
case "up", "k":
if m.tabMenuCursor > 0 {
m.tabMenuCursor--
}
return m, nil
case "down", "j":
if m.tabMenuCursor < int(tabCount)-1 {
m.tabMenuCursor++
}
return m, nil
case "enter":
m.activeTab = tab(m.tabMenuCursor)
m.showingTabMenu = false
m.resizeViewport()
return m, nil
case "1":
m.activeTab = tabDashboard
m.showingTabMenu = false
m.resizeViewport()
return m, nil
case "2":
m.activeTab = tabChat
m.showingTabMenu = false
m.resizeViewport()
return m, nil
case "3":
m.activeTab = tabWorkflow
m.showingTabMenu = false
m.resizeViewport()
return m, nil
case "4":
m.activeTab = tabAgents
m.showingTabMenu = false
m.resizeViewport()
return m, nil
case "5":
m.activeTab = tabConfig
m.showingTabMenu = false
m.resizeViewport()
return m, nil
}
return m, nil
}
func (m Model) renderTabMenuOverlay() string {
tabMenuStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(baseColor).
Background(bgCard).
Padding(1, 3)
tabItemStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#A0A0B0")).
Padding(0, 2)
tabItemActiveStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(baseColor).
Bold(true).
Padding(0, 2)
tabNumStyle := lipgloss.NewStyle().
Foreground(dimColor).
Width(4)
var items []string
icons := []string{"Dashboard", "Chat", "Workflow", "Agents", "Config"}
descs := []string{"system overview & tools", "AI chat & conversation", "plan & execute workflows", "background AI agents", "profile & settings"}
for i, name := range icons {
num := tabNumStyle.Render(fmt.Sprintf(" %d.", i+1))
if i == m.tabMenuCursor {
item := fmt.Sprintf("%s %-12s %s", num, name, lipgloss.NewStyle().Foreground(mutedColor).Render(descs[i]))
items = append(items, tabItemActiveStyle.Render(">"+item))
} else {
item := fmt.Sprintf("%s %-12s %s", num, name, lipgloss.NewStyle().Foreground(dimColor).Render(descs[i]))
items = append(items, tabItemStyle.Render(" "+item))
}
}
content := lipgloss.NewStyle().Foreground(accentColor).Bold(true).Render("Switch Tab") +
"\n\n" +
strings.Join(items, "\n") +
"\n\n" +
lipgloss.NewStyle().Foreground(dimColor).Render("↑↓ navigate enter/select esc cancel")
box := tabMenuStyle.Render(content)
return lipgloss.Place(m.width, m.height,
0.5, 0.5,
box,
lipgloss.WithWhitespaceBackground(bgPanel),
lipgloss.WithWhitespaceForeground(dimColor),
)
}
func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) {
input := m.chatInput
m.chatLog = append(m.chatLog, userMsgStyle.Render("you: "+input))
@@ -920,6 +994,11 @@ func (m Model) View() string {
b.WriteString("\n")
b.WriteString(m.renderFooter())
if m.showingTabMenu {
b = strings.Builder{}
b.WriteString(m.renderTabMenuOverlay())
}
return b.String()
}
@@ -1497,11 +1576,11 @@ func (m Model) renderFooter() string {
case tabDashboard:
helpText = "[i] install [u] update [s] scan [l] lsp [m] mcp"
case tabChat, tabWorkflow:
helpText = "[alt+1-5] tabs [tab] next [ctrl+c] quit"
helpText = "[ctrl+m] switch tab [tab] next [ctrl+c] quit"
case tabAgents:
helpText = "[c] crush [l] claude"
default:
helpText = "[alt+1-5] tabs [tab] next [ctrl+c] quit"
helpText = "[ctrl+m] switch tab [tab] next [ctrl+c] quit"
}
rightR := statusBarStyle.Render(helpText)