Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Improve tab locking functionality #9

Merged
merged 3 commits into from
Jan 4, 2025
Merged
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
57 changes: 38 additions & 19 deletions header.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
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.
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

Expand All @@ -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.
Expand All @@ -45,6 +45,7 @@ func newHeader() *header {
currentTab: 0,
keyMap: newKeyMap(),
updater: NewUpdater(),
lockedTabs: make(map[string]bool),
}
}

Expand Down Expand Up @@ -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...)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
}
134 changes: 103 additions & 31 deletions skeleton.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Loading