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 (#44)
* feat(legend): add HideFromLegend flag to reduce legend size Hide Back and Quit actions from legend bar as they are intuitive keys. This prepares for a more compact legend with a dedicated help screen. * 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
c933f00d00
commit
5e893fe950
3 changed files with 186 additions and 22 deletions
|
|
@ -18,11 +18,12 @@ const (
|
|||
|
||||
// IOAction represents an input/output action that can be triggered by a key event.
|
||||
type IOAction struct {
|
||||
Key tcell.Key
|
||||
Rune rune
|
||||
Name string
|
||||
KeySlug string
|
||||
Action func()
|
||||
Key tcell.Key
|
||||
Rune rune
|
||||
Name string
|
||||
KeySlug string
|
||||
Action func()
|
||||
HideFromLegend bool // If true, this action won't appear in the legend bar
|
||||
}
|
||||
|
||||
func (k *IOAction) SetAction(action func()) {
|
||||
|
|
@ -55,6 +56,7 @@ type IOService struct {
|
|||
ActionUpdateAll *IOAction
|
||||
ActionInstallAll *IOAction
|
||||
ActionRemoveAll *IOAction
|
||||
ActionHelp *IOAction
|
||||
ActionBack *IOAction
|
||||
ActionQuit *IOAction
|
||||
}
|
||||
|
|
@ -69,17 +71,18 @@ 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.ActionBack = &IOAction{Key: tcell.KeyEsc, Rune: 0, KeySlug: "esc", Name: "Back to Table"}
|
||||
s.ActionQuit = &IOAction{Key: tcell.KeyRune, Rune: 'q', KeySlug: "q", Name: "Quit"}
|
||||
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}
|
||||
|
||||
// Define actions for each key input,
|
||||
s.ActionSearch.SetAction(s.handleSearchFieldEvent)
|
||||
|
|
@ -93,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)
|
||||
|
||||
|
|
@ -108,6 +112,7 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
s.ActionUpdate,
|
||||
s.ActionRemove,
|
||||
s.ActionUpdateAll,
|
||||
s.ActionHelp,
|
||||
s.ActionBack,
|
||||
s.ActionQuit,
|
||||
}
|
||||
|
|
@ -119,9 +124,11 @@ var NewIOService = func(appService *AppService) IOServiceInterface {
|
|||
|
||||
// updateLegendEntries updates the legend entries based on current keyActions
|
||||
func (s *IOService) updateLegendEntries() {
|
||||
s.legendEntries = make([]struct{ KeySlug, Name string }, len(s.keyActions))
|
||||
for i, input := range s.keyActions {
|
||||
s.legendEntries[i] = struct{ KeySlug, Name string }{KeySlug: input.KeySlug, Name: input.Name}
|
||||
s.legendEntries = make([]struct{ KeySlug, Name string }, 0, len(s.keyActions))
|
||||
for _, input := range s.keyActions {
|
||||
if !input.HideFromLegend {
|
||||
s.legendEntries = append(s.legendEntries, struct{ KeySlug, Name string }{KeySlug: input.KeySlug, Name: input.Name})
|
||||
}
|
||||
}
|
||||
s.layout.GetLegend().SetLegend(s.legendEntries, "")
|
||||
}
|
||||
|
|
@ -179,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