Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 283 additions & 0 deletions internal/ui/components/help_menu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
package components

import (
"strings"

"github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

// KeyBinding represents a key binding with its description
type KeyBinding struct {
Key string
Description string
}

// KeyBindingSection represents a section of key bindings
type KeyBindingSection struct {
Title string
Bindings []KeyBinding
}

// HelpMenu represents the help menu component
type HelpMenu struct {
IsVisible bool
Width int
Height int
sections []KeyBindingSection
}

// NewHelpMenu creates a new help menu component
func NewHelpMenu() *HelpMenu {
return &HelpMenu{
IsVisible: false,
sections: buildKeyBindingSections(),
}
}

// buildKeyBindingSections creates the key binding sections
func buildKeyBindingSections() []KeyBindingSection {
return []KeyBindingSection{
{
Title: "Global",
Bindings: []KeyBinding{
{"q, Ctrl+C", "Quit application"},
{"?", "Toggle this help menu"},
{"c", "Create new issue"},
{"e", "Edit selected issue"},
{"r", "Refresh data from Linear"},
{"Ctrl+D", "Toggle detail pane visibility"},
{"Esc", "Return focus to main pane"},
},
},
{
Title: "Navigation",
Bindings: []KeyBinding{
{"Tab", "Move focus to next pane"},
{"Shift+Tab", "Move focus to previous pane"},
{"←→, h/l", "Navigate menu bar items"},
{"↑↓, k/j", "Navigate list items"},
{"Enter", "Select/activate item"},
},
},
{
Title: "Menu Bar",
Bindings: []KeyBinding{
{"←→, h/l", "Navigate between Issues and Projects"},
{"Enter", "Switch to selected view"},
},
},
{
Title: "Main Pane",
Bindings: []KeyBinding{
{"↑↓, k/j", "Navigate through issues/projects"},
{"Enter", "Select item (updates detail pane)"},
},
},
{
Title: "Detail Pane",
Bindings: []KeyBinding{
{"↑↓, k/j", "Scroll content up/down"},
{"PgUp/PgDn", "Scroll page up/down"},
{"Space", "Scroll page down"},
},
},
{
Title: "Create/Edit Modal",
Bindings: []KeyBinding{
{"Tab", "Move to next field"},
{"Shift+Tab", "Move to previous field"},
{"↑↓", "Change dropdown selection"},
{"Enter", "Submit when on Submit button"},
{"Ctrl+Enter", "Submit from any field"},
{"Esc", "Cancel and close modal"},
},
},
}
}

// Show displays the help menu
func (h *HelpMenu) Show() {
h.IsVisible = true
}

// Hide closes the help menu
func (h *HelpMenu) Hide() {
h.IsVisible = false
}

// Toggle toggles the help menu visibility
func (h *HelpMenu) Toggle() {
h.IsVisible = !h.IsVisible
}

// Update handles input for the help menu
func (h *HelpMenu) Update(msg tea.Msg) (*HelpMenu, tea.Cmd) {
if !h.IsVisible {
return h, nil
}

switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "?", "esc":
h.Hide()
return h, nil
case "ctrl+c", "q":
return h, tea.Quit
}
}

return h, nil
}

// View renders the help menu
func (h *HelpMenu) View(styles *Styles) string {
if !h.IsVisible {
return ""
}

// Calculate dimensions (80% of terminal size)
menuWidth := (h.Width * 8) / 10
menuHeight := (h.Height * 8) / 10

// Minimum size constraints
if menuWidth < 70 {
menuWidth = 70
}
if menuHeight < 25 {
menuHeight = 25
}

// Content width (leave space for borders and padding)
contentWidth := menuWidth - 4

var content strings.Builder

// Header
header := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FAFAFA")).
Background(lipgloss.Color("#7D56F4")).
Padding(0, 1).
Width(contentWidth).
Align(lipgloss.Center).
Bold(true).
Render("Key Bindings Help")

content.WriteString(header)
content.WriteString("\n\n")

// Render sections in two columns with proper layout
leftColumn := strings.Builder{}
rightColumn := strings.Builder{}

// Split sections into two columns
sectionsPerColumn := (len(h.sections) + 1) / 2
leftSections := h.sections[:sectionsPerColumn]
rightSections := h.sections[sectionsPerColumn:]

// Calculate column width (leave space for separation)
columnWidth := (contentWidth - 6) / 2 // 6 chars for spacing between columns

// Render left column
for i, section := range leftSections {
if i > 0 {
leftColumn.WriteString("\n")
}
leftColumn.WriteString(h.renderSection(section, columnWidth, styles))
}

// Render right column
for i, section := range rightSections {
if i > 0 {
rightColumn.WriteString("\n")
}
rightColumn.WriteString(h.renderSection(section, columnWidth, styles))
}

// Combine columns using lipgloss JoinHorizontal
leftContent := leftColumn.String()
rightContent := rightColumn.String()

// Use lipgloss to join horizontally with proper spacing
leftStyle := lipgloss.NewStyle().Width(columnWidth).Align(lipgloss.Left)
rightStyle := lipgloss.NewStyle().Width(columnWidth).Align(lipgloss.Left)

combinedContent := lipgloss.JoinHorizontal(
lipgloss.Top,
leftStyle.Render(leftContent),
" ", // 6 spaces between columns
rightStyle.Render(rightContent),
)

content.WriteString(combinedContent)

// Footer
content.WriteString("\n")
footer := lipgloss.NewStyle().
Foreground(lipgloss.Color("#626262")).
Italic(true).
Width(contentWidth).
Align(lipgloss.Center).
Render("Press ? or Esc to close")

content.WriteString(footer)

// Apply modal styling
modalStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#874BFD")).
Padding(1, 1).
Width(menuWidth).
Height(menuHeight)

return modalStyle.Render(content.String())
}

// renderSection renders a single key binding section
func (h *HelpMenu) renderSection(section KeyBindingSection, width int, styles *Styles) string {
var content strings.Builder

// Section title
titleStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FAFAFA")).
Background(lipgloss.Color("#7D56F4")).
Padding(0, 1).
Width(width).
Bold(true)

content.WriteString(titleStyle.Render(section.Title))
content.WriteString("\n")

// Key bindings with proper formatting
for _, binding := range section.Bindings {
keyStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#874BFD")).
Bold(true)

descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FAFAFA"))

// Format with proper spacing: " Key: Description"
line := " " + keyStyle.Render(binding.Key) + ": " + descStyle.Render(binding.Description)

content.WriteString(line)
content.WriteString("\n")
}

return content.String()
}

// SetDimensions sets the help menu dimensions
func (h *HelpMenu) SetDimensions(width, height int) {
h.Width = width
h.Height = height
}

// Helper function for max (already exists in other files, but needed here)
func max(a, b int) int {
if a > b {
return a
}
return b
}
32 changes: 32 additions & 0 deletions internal/ui/components/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Layout struct {
DetailPane *DetailPane
Modal *CreateTicketModal
ErrorModal *ErrorModal
HelpMenu *HelpMenu

// Services
LinearService *services.LinearService
Expand Down Expand Up @@ -70,6 +71,7 @@ func NewLayout() *Layout {
DetailPane: NewDetailPane(),
Modal: NewCreateTicketModal(),
ErrorModal: NewErrorModal(),
HelpMenu: NewHelpMenu(),
LinearService: linearService,
FocusedPane: PaneMain, // Start with main pane focused
AppState: initialState,
Expand Down Expand Up @@ -157,6 +159,17 @@ func (l *Layout) Update(msg tea.Msg) (*Layout, tea.Cmd) {
return l, tea.Batch(cmds...)
}

// If help menu is visible, let it handle all input first
if l.HelpMenu.IsVisible {
helpMenu, helpMenuCmd := l.HelpMenu.Update(msg)
l.HelpMenu = helpMenu
if helpMenuCmd != nil {
cmds = append(cmds, helpMenuCmd)
}
// Return early to prevent background interaction
return l, tea.Batch(cmds...)
}

// If create ticket modal is visible, let it handle all input first
if l.Modal.IsVisible {
modal, modalCmd := l.Modal.Update(msg)
Expand All @@ -172,6 +185,10 @@ func (l *Layout) Update(msg tea.Msg) (*Layout, tea.Cmd) {
case "ctrl+c", "q":
return l, tea.Quit

case "?":
// Toggle help menu
l.HelpMenu.Toggle()

case "c":
// Global hotkey to create new issue
l.Modal.Show()
Expand Down Expand Up @@ -417,6 +434,7 @@ func (l *Layout) View() string {
l.DetailPane.SetDimensions(l.Config.DetailPaneWidth, l.Config.MainContentHeight)
l.Modal.SetDimensions(l.Config.ScreenWidth, l.Config.ScreenHeight)
l.ErrorModal.SetDimensions(l.Config.ScreenWidth, l.Config.ScreenHeight)
l.HelpMenu.SetDimensions(l.Config.ScreenWidth, l.Config.ScreenHeight)

// Render components
menuView := l.MenuBar.View(l.Styles)
Expand Down Expand Up @@ -448,6 +466,20 @@ func (l *Layout) View() string {
return l.ErrorModal.View(l.Styles)
}

// Overlay help menu if visible
if l.HelpMenu.IsVisible {
helpMenuView := l.HelpMenu.View(l.Styles)

// Create help menu overlay with background
overlayStyle := lipgloss.NewStyle().
Width(l.Config.ScreenWidth).
Height(l.Config.ScreenHeight).
Align(lipgloss.Center, lipgloss.Center)

// Combine background and help menu
return overlayStyle.Render(helpMenuView)
}

// Overlay create ticket modal if visible
if l.Modal.IsVisible {
modalView := l.Modal.View(l.Styles)
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/components/mainpane.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (m *MainPane) buildListSection(styles *Styles) string {

// buildHelpSection builds the help text section
func (m *MainPane) buildHelpSection(styles *Styles) string {
return styles.Placeholder.Width(m.getContentWidth()).Render("↑/↓ to navigate • Enter to select • Ctrl+D to toggle details pane")
return styles.Placeholder.Width(m.getContentWidth()).Render("↑/↓ to navigate • Enter to select • Ctrl+D to toggle details pane • ? for help")
}

// getTitle returns the title for the current view
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/components/menubar.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (m *MenuBar) View(styles *Styles) string {

// Add help text on a new line
content.WriteString("\n")
helpText := styles.Placeholder.Render("←/→ to navigate • Enter to select • Tab to switch panes")
helpText := styles.Placeholder.Render("←/→ to navigate • Enter to select • Tab to switch panes • ? for help")
content.WriteString(helpText)

// Apply border and sizing
Expand Down
Binary file added linear-tui
Binary file not shown.