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
|
help Show this help
|
||||||
|
|
||||||
TUI Controls:
|
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
|
Tab / Shift+Tab Cycle tabs
|
||||||
Ctrl+C Show quit confirmation (press twice quickly to force quit)
|
Ctrl+C Show quit confirmation (press twice quickly to force quit)
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ type Model struct {
|
|||||||
|
|
||||||
showingQuit bool
|
showingQuit bool
|
||||||
confirmCursor int
|
confirmCursor int
|
||||||
|
showingTabMenu bool
|
||||||
|
tabMenuCursor int
|
||||||
|
|
||||||
ctrlCCount int
|
ctrlCCount int
|
||||||
lastCtrlC time.Time
|
lastCtrlC time.Time
|
||||||
@@ -224,11 +226,7 @@ type keyMap struct {
|
|||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
Confirm key.Binding
|
Confirm key.Binding
|
||||||
Cancel key.Binding
|
Cancel key.Binding
|
||||||
Dashboard key.Binding
|
TabMenu key.Binding
|
||||||
Chat key.Binding
|
|
||||||
Workflow key.Binding
|
|
||||||
Agents key.Binding
|
|
||||||
Config key.Binding
|
|
||||||
Install key.Binding
|
Install key.Binding
|
||||||
Update key.Binding
|
Update key.Binding
|
||||||
Scan key.Binding
|
Scan key.Binding
|
||||||
@@ -257,25 +255,9 @@ var keys = keyMap{
|
|||||||
key.WithKeys("n", "esc"),
|
key.WithKeys("n", "esc"),
|
||||||
key.WithHelp("n/esc", "no"),
|
key.WithHelp("n/esc", "no"),
|
||||||
),
|
),
|
||||||
Dashboard: key.NewBinding(
|
TabMenu: key.NewBinding(
|
||||||
key.WithKeys("alt+1"),
|
key.WithKeys("ctrl+m"),
|
||||||
key.WithHelp("alt+1", "dashboard"),
|
key.WithHelp("ctrl+m", "switch tab"),
|
||||||
),
|
|
||||||
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"),
|
|
||||||
),
|
),
|
||||||
Install: key.NewBinding(
|
Install: key.NewBinding(
|
||||||
key.WithKeys("i"),
|
key.WithKeys("i"),
|
||||||
@@ -300,13 +282,13 @@ var keys = keyMap{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k keyMap) ShortHelp() []key.Binding {
|
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 {
|
func (k keyMap) FullHelp() [][]key.Binding {
|
||||||
return [][]key.Binding{
|
return [][]key.Binding{
|
||||||
{k.Dashboard, k.Chat, k.Workflow, k.Agents, k.Config},
|
{k.TabMenu, k.Tab, k.Prev},
|
||||||
{k.Tab, k.Prev, k.Quit},
|
{k.Quit},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +337,8 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
|
|||||||
spinner: sp,
|
spinner: sp,
|
||||||
showingQuit: false,
|
showingQuit: false,
|
||||||
confirmCursor: 1,
|
confirmCursor: 1,
|
||||||
|
showingTabMenu: false,
|
||||||
|
tabMenuCursor: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,6 +474,9 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
if m.showingQuit {
|
if m.showingQuit {
|
||||||
return m.handleQuitConfirm(msg)
|
return m.handleQuitConfirm(msg)
|
||||||
}
|
}
|
||||||
|
if m.showingTabMenu {
|
||||||
|
return m.handleTabMenu(msg)
|
||||||
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
@@ -503,25 +490,10 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.confirmCursor = 1
|
m.confirmCursor = 1
|
||||||
m.viewport.SetContent(m.renderContent())
|
m.viewport.SetContent(m.renderContent())
|
||||||
return m, nil
|
return m, nil
|
||||||
case "alt+1":
|
case "ctrl+m":
|
||||||
m.activeTab = tabDashboard
|
m.showingTabMenu = true
|
||||||
m.resizeViewport()
|
m.tabMenuCursor = int(m.activeTab)
|
||||||
return m, nil
|
m.viewport.SetContent(m.renderContent())
|
||||||
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()
|
|
||||||
return m, nil
|
return m, nil
|
||||||
case "tab":
|
case "tab":
|
||||||
m.activeTab = (m.activeTab + 1) % tabCount
|
m.activeTab = (m.activeTab + 1) % tabCount
|
||||||
@@ -594,6 +566,108 @@ func (m Model) handleQuitConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
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) {
|
func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) {
|
||||||
input := m.chatInput
|
input := m.chatInput
|
||||||
m.chatLog = append(m.chatLog, userMsgStyle.Render("you: "+input))
|
m.chatLog = append(m.chatLog, userMsgStyle.Render("you: "+input))
|
||||||
@@ -920,6 +994,11 @@ func (m Model) View() string {
|
|||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(m.renderFooter())
|
b.WriteString(m.renderFooter())
|
||||||
|
|
||||||
|
if m.showingTabMenu {
|
||||||
|
b = strings.Builder{}
|
||||||
|
b.WriteString(m.renderTabMenuOverlay())
|
||||||
|
}
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1497,11 +1576,11 @@ func (m Model) renderFooter() string {
|
|||||||
case tabDashboard:
|
case tabDashboard:
|
||||||
helpText = "[i] install [u] update [s] scan [l] lsp [m] mcp"
|
helpText = "[i] install [u] update [s] scan [l] lsp [m] mcp"
|
||||||
case tabChat, tabWorkflow:
|
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:
|
case tabAgents:
|
||||||
helpText = "[c] crush [l] claude"
|
helpText = "[c] crush [l] claude"
|
||||||
default:
|
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)
|
rightR := statusBarStyle.Render(helpText)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user