feat: Ctrl+M tab switcher overlay menu
All checks were successful
CI / build (push) Successful in 1m45s
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user