mirror of
https://github.com/Valkyrie00/bold-brew.git
synced 2026-03-14 14:25:53 +01:00
feat(ui): add help screen with keyboard shortcuts overlay
Add a help modal accessible via '?' key that displays all keyboard shortcuts organized by category (Navigation, Filters, Actions, Brewfile). Reduce legend bar clutter by hiding less common actions and showing only essential shortcuts. Add HideFromLegend flag to IOAction struct for flexible legend management.
This commit is contained in:
parent
b7d959d9f2
commit
f703f5de3b
3 changed files with 173 additions and 12 deletions
|
|
@ -56,6 +56,7 @@ type IOService struct {
|
|||
ActionUpdateAll *IOAction
|
||||
ActionInstallAll *IOAction
|
||||
ActionRemoveAll *IOAction
|
||||
ActionHelp *IOAction
|
||||
ActionBack *IOAction
|
||||
ActionQuit *IOAction
|
||||
}
|
||||
|
|
@ -70,15 +71,16 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
// Initialize key actions with their respective keys, runes, and names.
|
||||
s.ActionSearch = &IOAction{Key: tcell.KeyRune, Rune: '/', KeySlug: "/", Name: "Search"}
|
||||
s.ActionFilterInstalled = &IOAction{Key: tcell.KeyRune, Rune: 'f', KeySlug: "f", Name: "Installed"}
|
||||
s.ActionFilterOutdated = &IOAction{Key: tcell.KeyRune, Rune: 'o', KeySlug: "o", Name: "Outdated"}
|
||||
s.ActionFilterLeaves = &IOAction{Key: tcell.KeyRune, Rune: 'l', KeySlug: "l", Name: "Leaves"}
|
||||
s.ActionFilterCasks = &IOAction{Key: tcell.KeyRune, Rune: 'c', KeySlug: "c", Name: "Casks"}
|
||||
s.ActionFilterOutdated = &IOAction{Key: tcell.KeyRune, Rune: 'o', KeySlug: "o", Name: "Outdated", HideFromLegend: true}
|
||||
s.ActionFilterLeaves = &IOAction{Key: tcell.KeyRune, Rune: 'l', KeySlug: "l", Name: "Leaves", HideFromLegend: true}
|
||||
s.ActionFilterCasks = &IOAction{Key: tcell.KeyRune, Rune: 'c', KeySlug: "c", Name: "Casks", HideFromLegend: true}
|
||||
s.ActionInstall = &IOAction{Key: tcell.KeyRune, Rune: 'i', KeySlug: "i", Name: "Install"}
|
||||
s.ActionUpdate = &IOAction{Key: tcell.KeyRune, Rune: 'u', KeySlug: "u", Name: "Update"}
|
||||
s.ActionRemove = &IOAction{Key: tcell.KeyRune, Rune: 'r', KeySlug: "r", Name: "Remove"}
|
||||
s.ActionUpdateAll = &IOAction{Key: tcell.KeyCtrlU, Rune: 0, KeySlug: "ctrl+u", Name: "Update All"}
|
||||
s.ActionUpdateAll = &IOAction{Key: tcell.KeyCtrlU, Rune: 0, KeySlug: "ctrl+u", Name: "Update All", HideFromLegend: true}
|
||||
s.ActionInstallAll = &IOAction{Key: tcell.KeyCtrlA, Rune: 0, KeySlug: "ctrl+a", Name: "Install All (Brewfile)"}
|
||||
s.ActionRemoveAll = &IOAction{Key: tcell.KeyCtrlR, Rune: 0, KeySlug: "ctrl+r", Name: "Remove All (Brewfile)"}
|
||||
s.ActionHelp = &IOAction{Key: tcell.KeyRune, Rune: '?', KeySlug: "?", Name: "Help"}
|
||||
s.ActionBack = &IOAction{Key: tcell.KeyEsc, Rune: 0, KeySlug: "esc", Name: "Back to Table", HideFromLegend: true}
|
||||
s.ActionQuit = &IOAction{Key: tcell.KeyRune, Rune: 'q', KeySlug: "q", Name: "Quit", HideFromLegend: true}
|
||||
|
||||
|
|
@ -94,6 +96,7 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
s.ActionUpdateAll.SetAction(s.handleUpdateAllPackagesEvent)
|
||||
s.ActionInstallAll.SetAction(s.handleInstallAllPackagesEvent)
|
||||
s.ActionRemoveAll.SetAction(s.handleRemoveAllPackagesEvent)
|
||||
s.ActionHelp.SetAction(s.handleHelpEvent)
|
||||
s.ActionBack.SetAction(s.handleBack)
|
||||
s.ActionQuit.SetAction(s.handleQuitEvent)
|
||||
|
||||
|
|
@ -109,6 +112,7 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
s.ActionUpdate,
|
||||
s.ActionRemove,
|
||||
s.ActionUpdateAll,
|
||||
s.ActionHelp,
|
||||
s.ActionBack,
|
||||
s.ActionQuit,
|
||||
}
|
||||
|
|
@ -182,6 +186,23 @@ func (s *IOService) handleQuitEvent() {
|
|||
s.appService.GetApp().Stop()
|
||||
}
|
||||
|
||||
// handleHelpEvent shows the help screen with all keyboard shortcuts.
|
||||
func (s *IOService) handleHelpEvent() {
|
||||
helpScreen := s.layout.GetHelpScreen()
|
||||
helpScreen.SetBrewfileMode(s.appService.IsBrewfileMode())
|
||||
helpPages := helpScreen.Build(s.layout.Root())
|
||||
|
||||
// Set up key handler to close help on any key press
|
||||
helpPages.SetInputCapture(func(_ *tcell.EventKey) *tcell.EventKey {
|
||||
// Close help and return to main view
|
||||
s.appService.GetApp().SetRoot(s.layout.Root(), true)
|
||||
s.appService.GetApp().SetFocus(s.layout.GetTable().View())
|
||||
return nil
|
||||
})
|
||||
|
||||
s.appService.GetApp().SetRoot(helpPages, true)
|
||||
}
|
||||
|
||||
// handleFilterEvent toggles the filter for installed or outdated packages based on the provided filter type.
|
||||
func (s *IOService) handleFilterEvent(filterType FilterType) {
|
||||
s.layout.GetLegend().SetLegend(s.legendEntries, "")
|
||||
|
|
|
|||
136
internal/ui/components/help.go
Normal file
136
internal/ui/components/help.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
"bbrew/internal/ui/theme"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// HelpScreen displays a modal overlay with all keyboard shortcuts
|
||||
type HelpScreen struct {
|
||||
pages *tview.Pages
|
||||
theme *theme.Theme
|
||||
isBrewfile bool
|
||||
}
|
||||
|
||||
// NewHelpScreen creates a new help screen component
|
||||
func NewHelpScreen(theme *theme.Theme) *HelpScreen {
|
||||
return &HelpScreen{
|
||||
theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
// View returns the help screen pages (for overlay functionality)
|
||||
func (h *HelpScreen) View() *tview.Pages {
|
||||
return h.pages
|
||||
}
|
||||
|
||||
// SetBrewfileMode sets whether Brewfile-specific commands should be shown
|
||||
func (h *HelpScreen) SetBrewfileMode(enabled bool) {
|
||||
h.isBrewfile = enabled
|
||||
}
|
||||
|
||||
// Build creates the help screen as an overlay on top of the main content
|
||||
func (h *HelpScreen) Build(mainContent tview.Primitive) *tview.Pages {
|
||||
content := h.buildHelpContent()
|
||||
|
||||
textView := tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetText(content).
|
||||
SetTextAlign(tview.AlignLeft)
|
||||
|
||||
textView.SetBackgroundColor(h.theme.ModalBgColor)
|
||||
textView.SetTextColor(h.theme.DefaultTextColor)
|
||||
|
||||
// Create a frame around the text
|
||||
frame := tview.NewFrame(textView).
|
||||
SetBorders(1, 1, 1, 1, 2, 2)
|
||||
frame.SetBackgroundColor(h.theme.ModalBgColor)
|
||||
frame.SetBorderColor(h.theme.BorderColor)
|
||||
frame.SetBorder(true).
|
||||
SetTitle(" Help ").
|
||||
SetTitleAlign(tview.AlignCenter)
|
||||
|
||||
// Calculate box dimensions
|
||||
boxHeight := 22
|
||||
boxWidth := 55
|
||||
if h.isBrewfile {
|
||||
boxHeight = 26 // Extra space for Brewfile section
|
||||
}
|
||||
|
||||
// Center the frame in a flex layout
|
||||
centered := tview.NewFlex().
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(frame, boxHeight, 0, true).
|
||||
AddItem(nil, 0, 1, false),
|
||||
boxWidth, 0, true).
|
||||
AddItem(nil, 0, 1, false)
|
||||
|
||||
// Create pages with main content as background and help as overlay
|
||||
h.pages = tview.NewPages().
|
||||
AddPage("main", mainContent, true, true).
|
||||
AddPage("help", centered, true, true)
|
||||
|
||||
return h.pages
|
||||
}
|
||||
|
||||
// buildHelpContent generates the formatted help text
|
||||
func (h *HelpScreen) buildHelpContent() string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Navigation section
|
||||
sb.WriteString(h.formatSection("NAVIGATION"))
|
||||
sb.WriteString(h.formatKey("↑/↓, j/k", "Navigate list"))
|
||||
sb.WriteString(h.formatKey("/", "Focus search"))
|
||||
sb.WriteString(h.formatKey("Esc", "Back to table"))
|
||||
sb.WriteString(h.formatKey("q", "Quit"))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Filters section
|
||||
sb.WriteString(h.formatSection("FILTERS"))
|
||||
sb.WriteString(h.formatKey("f", "Toggle installed"))
|
||||
sb.WriteString(h.formatKey("o", "Toggle outdated"))
|
||||
sb.WriteString(h.formatKey("l", "Toggle leaves"))
|
||||
sb.WriteString(h.formatKey("c", "Toggle casks"))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Actions section
|
||||
sb.WriteString(h.formatSection("ACTIONS"))
|
||||
sb.WriteString(h.formatKey("i", "Install selected"))
|
||||
sb.WriteString(h.formatKey("u", "Update selected"))
|
||||
sb.WriteString(h.formatKey("r", "Remove selected"))
|
||||
sb.WriteString(h.formatKey("Ctrl+U", "Update all"))
|
||||
|
||||
// Brewfile section (only if in Brewfile mode)
|
||||
if h.isBrewfile {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(h.formatSection("BREWFILE"))
|
||||
sb.WriteString(h.formatKey("Ctrl+A", "Install all"))
|
||||
sb.WriteString(h.formatKey("Ctrl+R", "Remove all"))
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(fmt.Sprintf("[%s]Press any key to close[-]", h.getColorTag(h.theme.LegendColor)))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatSection formats a section header
|
||||
func (h *HelpScreen) formatSection(title string) string {
|
||||
return fmt.Sprintf("[%s::b]%s[-:-:-]\n", h.getColorTag(h.theme.SuccessColor), title)
|
||||
}
|
||||
|
||||
// formatKey formats a key-description pair
|
||||
func (h *HelpScreen) formatKey(key, description string) string {
|
||||
return fmt.Sprintf(" [%s]%-12s[-] %s\n", h.getColorTag(h.theme.WarningColor), key, description)
|
||||
}
|
||||
|
||||
// getColorTag converts a tcell.Color to a tview color tag
|
||||
func (h *HelpScreen) getColorTag(color tcell.Color) string {
|
||||
return fmt.Sprintf("#%06x", color.Hex())
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ type LayoutInterface interface {
|
|||
GetLegend() *components.Legend
|
||||
GetNotifier() *components.Notifier
|
||||
GetModal() *components.Modal
|
||||
GetHelpScreen() *components.HelpScreen
|
||||
}
|
||||
|
||||
type Layout struct {
|
||||
|
|
@ -31,6 +32,7 @@ type Layout struct {
|
|||
legend *components.Legend
|
||||
notifier *components.Notifier
|
||||
modal *components.Modal
|
||||
helpScreen *components.HelpScreen
|
||||
theme *theme.Theme
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +47,7 @@ func NewLayout(theme *theme.Theme) LayoutInterface {
|
|||
legend: components.NewLegend(theme),
|
||||
notifier: components.NewNotifier(theme),
|
||||
modal: components.NewModal(theme),
|
||||
helpScreen: components.NewHelpScreen(theme),
|
||||
theme: theme,
|
||||
}
|
||||
}
|
||||
|
|
@ -103,11 +106,12 @@ func (l *Layout) Root() tview.Primitive {
|
|||
return l.mainContent
|
||||
}
|
||||
|
||||
func (l *Layout) GetHeader() *components.Header { return l.header }
|
||||
func (l *Layout) GetSearch() *components.Search { return l.search }
|
||||
func (l *Layout) GetTable() *components.Table { return l.table }
|
||||
func (l *Layout) GetDetails() *components.Details { return l.details }
|
||||
func (l *Layout) GetOutput() *components.Output { return l.output }
|
||||
func (l *Layout) GetLegend() *components.Legend { return l.legend }
|
||||
func (l *Layout) GetNotifier() *components.Notifier { return l.notifier }
|
||||
func (l *Layout) GetModal() *components.Modal { return l.modal }
|
||||
func (l *Layout) GetHeader() *components.Header { return l.header }
|
||||
func (l *Layout) GetSearch() *components.Search { return l.search }
|
||||
func (l *Layout) GetTable() *components.Table { return l.table }
|
||||
func (l *Layout) GetDetails() *components.Details { return l.details }
|
||||
func (l *Layout) GetOutput() *components.Output { return l.output }
|
||||
func (l *Layout) GetLegend() *components.Legend { return l.legend }
|
||||
func (l *Layout) GetNotifier() *components.Notifier { return l.notifier }
|
||||
func (l *Layout) GetModal() *components.Modal { return l.modal }
|
||||
func (l *Layout) GetHelpScreen() *components.HelpScreen { return l.helpScreen }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue