diff --git a/header.go b/header.go index e62558d..51c0128 100644 --- a/header.go +++ b/header.go @@ -1,11 +1,11 @@ package skeleton import ( - "github.com/charmbracelet/bubbles/key" + "strings" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "strings" ) // header is a helper for rendering the header of the terminal. @@ -13,9 +13,6 @@ type header struct { // termReady is control terminal is ready or not, it responsible for the terminal size termReady bool - // lockTabs is control the tabs (headers) are locked or not - lockTabs bool - // currentTab is hold the current tab index currentTab int @@ -35,6 +32,9 @@ type header struct { titleLength int updater *Updater + + // lockedTabs holds the keys of individually locked tabs + lockedTabs map[string]bool } // newHeader returns a new header. @@ -45,6 +45,7 @@ func newHeader() *header { currentTab: 0, keyMap: newKeyMap(), updater: NewUpdater(), + lockedTabs: make(map[string]bool), } } @@ -119,17 +120,6 @@ func (h *header) Update(msg tea.Msg) (*header, tea.Cmd) { h.calculateTitleLength() cmds = append(cmds, h.calculateTitleLength()) - case tea.KeyMsg: - switch { - case key.Matches(msg, h.keyMap.SwitchTabLeft): - if !h.GetLockTabs() { - h.currentTab = max(h.currentTab-1, 0) - } - case key.Matches(msg, h.keyMap.SwitchTabRight): - if !h.GetLockTabs() { - h.currentTab = min(h.currentTab+1, len(h.headers)-1) - } - } } return h, tea.Batch(cmds...) @@ -183,7 +173,7 @@ func (h *header) View() string { if i == h.currentTab { renderedTitles = append(renderedTitles, h.properties.titleStyleActive.Render(hdr.title)) } else { - if h.GetLockTabs() { + if h.GetLockTabs() || h.IsTabLocked(hdr.key) { renderedTitles = append(renderedTitles, h.properties.titleStyleDisabled.Render(hdr.title)) } else { renderedTitles = append(renderedTitles, h.properties.titleStyleInactive.Render(hdr.title)) @@ -251,12 +241,24 @@ func (h *header) SetCurrentTab(tab int) { // SetLockTabs sets the lock tabs status. func (h *header) SetLockTabs(lock bool) { - h.lockTabs = lock + if lock { + for _, header := range h.headers { + h.LockTab(header.key) + } + } else { + h.lockedTabs = make(map[string]bool) + } + h.updater.Update() } // GetLockTabs returns the lock tabs status. func (h *header) GetLockTabs() bool { - return h.lockTabs + for _, header := range h.headers { + if !h.IsTabLocked(header.key) { + return false + } + } + return true } // GetCurrentTab returns the current tab index. @@ -295,3 +297,20 @@ func (h *header) DeleteCommonHeader(key string) { h.calculateTitleLength() h.updater.Update() } + +// IsTabLocked checks if a specific tab is locked +func (h *header) IsTabLocked(key string) bool { + return h.lockedTabs[key] +} + +// LockTab locks a specific tab by its key +func (h *header) LockTab(key string) { + h.lockedTabs[key] = true + h.updater.Update() +} + +// UnlockTab unlocks a specific tab by its key +func (h *header) UnlockTab(key string) { + delete(h.lockedTabs, key) + h.updater.Update() +} diff --git a/skeleton.go b/skeleton.go index aa81122..a2d698c 100644 --- a/skeleton.go +++ b/skeleton.go @@ -1,11 +1,12 @@ package skeleton import ( + "strings" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "strings" ) // Skeleton is a helper for rendering the Skeleton of the terminal. @@ -19,9 +20,6 @@ type Skeleton struct { // termSizeNotEnoughToHandleWidgets is control terminal size is enough to handle widgets termSizeNotEnoughToHandleWidgets bool - // lockTabs is control the tabs (headers) are locked or not - lockTabs bool - // currentTab is hold the current tab index currentTab int @@ -176,23 +174,29 @@ func (s *Skeleton) SetWidgetRightPadding(padding int) *Skeleton { // LockTabs locks the tabs (headers). It prevents switching tabs. It is useful when you want to prevent switching tabs. func (s *Skeleton) LockTabs() *Skeleton { - s.header.SetLockTabs(true) - s.lockTabs = true + for _, header := range s.header.headers { + s.LockTab(header.key) + } s.updater.Update() return s } -// UnlockTabs unlocks the tabs (headers). It allows switching tabs. It is useful when you want to allow switching tabs. +// UnlockTabs unlocks all tabs (both general and individual locks) func (s *Skeleton) UnlockTabs() *Skeleton { s.header.SetLockTabs(false) - s.lockTabs = false + + // Clear all individual tab locks + for _, header := range s.header.headers { + s.UnlockTab(header.key) + } + s.updater.Update() return s } // IsTabsLocked returns the tabs (headers) are locked or not. func (s *Skeleton) IsTabsLocked() bool { - return s.lockTabs + return s.header.GetLockTabs() } // AddPageMsg adds a new page to the Skeleton. @@ -322,23 +326,39 @@ func (s *Skeleton) IAMActivePageCmd() tea.Cmd { } func (s *Skeleton) switchPage(cmds []tea.Cmd, position string) []tea.Cmd { + if s.IsTabsLocked() { + return cmds + } + + currentTab := s.currentTab switch position { case "left": - if !s.IsTabsLocked() { - s.currentTab = max(s.currentTab-1, 0) - cmds = append(cmds, s.IAMActivePageCmd()) + // Start from current position and move left until we find an unlocked tab + for nextTab := currentTab - 1; nextTab >= 0; nextTab-- { + if !s.IsTabLocked(s.header.headers[nextTab].key) { + s.currentTab = nextTab + s.header.SetCurrentTab(nextTab) + return append(cmds, s.IAMActivePageCmd()) + } } case "right": - if !s.IsTabsLocked() { - s.currentTab = min(s.currentTab+1, len(s.pages)-1) - cmds = append(cmds, s.IAMActivePageCmd()) + // Start from current position and move right until we find an unlocked tab + for nextTab := currentTab + 1; nextTab < len(s.pages); nextTab++ { + if !s.IsTabLocked(s.header.headers[nextTab].key) { + s.currentTab = nextTab + s.header.SetCurrentTab(nextTab) + return append(cmds, s.IAMActivePageCmd()) + } } } return cmds } -func (s *Skeleton) updateSkeleton(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd { +func (s *Skeleton) updateSkeleton(msg tea.Msg) []tea.Cmd { + var cmds []tea.Cmd + var cmd tea.Cmd + s.header, cmd = s.header.Update(msg) cmds = append(cmds, cmd) @@ -360,9 +380,6 @@ func (s *Skeleton) Init() tea.Cmd { } func (s *Skeleton) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - var cmd tea.Cmd - s.currentTab = s.header.GetCurrentTab() switch msg := msg.(type) { @@ -375,8 +392,10 @@ func (s *Skeleton) Update(msg tea.Msg) (tea.Model, tea.Cmd) { s.viewport.Width = msg.Width s.viewport.Height = msg.Height - cmds = s.updateSkeleton(msg, cmd, cmds) + return s, tea.Batch(s.updateSkeleton(msg)...) + case tea.KeyMsg: + var cmds []tea.Cmd switch { case key.Matches(msg, s.KeyMap.Quit): return s, tea.Quit @@ -385,26 +404,32 @@ func (s *Skeleton) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, s.KeyMap.SwitchTabRight): cmds = s.switchPage(cmds, "right") } - cmds = s.updateSkeleton(msg, cmd, cmds) + cmds = append(cmds, s.updateSkeleton(msg)...) + return s, tea.Batch(cmds...) + case AddPageMsg: - cmds = append(cmds, msg.Page.Init()) // init the page - cmds = s.updateSkeleton(msg, cmd, cmds) - cmds = append(cmds, s.updater.Listen()) // listen to the update channel + cmds := s.updateSkeleton(msg) + cmds = append(cmds, msg.Page.Init(), s.updater.Listen()) + return s, tea.Batch(cmds...) + case UpdateMsg: - // do nothing, just to trigger the update - cmds = s.updateSkeleton(msg, cmd, cmds) - cmds = append(cmds, s.updater.Listen()) // listen to the update channel + cmds := s.updateSkeleton(msg) + cmds = append(cmds, s.updater.Listen()) + return s, tea.Batch(cmds...) + case HeaderSizeMsg: s.termSizeNotEnoughToHandleHeaders = msg.NotEnoughToHandleHeaders + return s, nil + case WidgetSizeMsg: s.termSizeNotEnoughToHandleWidgets = msg.NotEnoughToHandleWidgets + return s, nil default: - cmds = s.updateSkeleton(msg, cmd, cmds) - cmds = append(cmds, s.updater.Listen()) // listen to the update channel + cmds := s.updateSkeleton(msg) + cmds = append(cmds, s.updater.Listen()) + return s, tea.Batch(cmds...) } - - return s, tea.Batch(cmds...) } func (s *Skeleton) View() string { @@ -437,3 +462,50 @@ func (s *Skeleton) View() string { return lipgloss.JoinVertical(lipgloss.Top, s.header.View(), base.Render(body), s.widget.View()) } + +// LockTab locks a specific tab by its key +func (s *Skeleton) LockTab(key string) *Skeleton { + s.header.LockTab(key) + s.updater.Update() + return s +} + +// UnlockTab unlocks a specific tab by its key +func (s *Skeleton) UnlockTab(key string) *Skeleton { + s.header.UnlockTab(key) + s.updater.Update() + return s +} + +// IsTabLocked checks if a specific tab is locked +func (s *Skeleton) IsTabLocked(key string) bool { + return s.header.IsTabLocked(key) +} + +// LockTabsToTheRight locks all tabs to the right of the current tab +func (s *Skeleton) LockTabsToTheRight() *Skeleton { + if s.currentTab >= len(s.header.headers)-1 { + return s // No tabs to the right + } + + for i := s.currentTab + 1; i < len(s.header.headers); i++ { + s.LockTab(s.header.headers[i].key) + } + + s.updater.Update() + return s +} + +// LockTabsToTheLeft locks all tabs to the left of the current tab +func (s *Skeleton) LockTabsToTheLeft() *Skeleton { + if s.currentTab <= 0 { + return s // No tabs to the left + } + + for i := 0; i < s.currentTab; i++ { + s.LockTab(s.header.headers[i].key) + } + + s.updater.Update() + return s +}