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
101 changes: 101 additions & 0 deletions examples/tabbar/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"fmt"
"os"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/tabbar"
)

type model struct {
tabBar tabbar.TabBar
content []string
width int
height int
}

func initialModel() model {
tabs := []string{"Home", "Projects", "Settings", "About"}

// Create content for each tab
content := []string{
"Welcome to the Home tab!",
"Here are your projects:\n - Project 1\n - Project 2\n - Project 3",
"Settings:\n - Theme: Dark\n - Notifications: On\n - Sounds: Off",
"About:\nThis is a simple example of the tabbar component.",
}

return model{
tabBar: tabbar.New(tabs, 0),
content: content,
width: 80,
height: 24,
}
}

func (m model) Init() tea.Cmd {
return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "right", "tab":
return m, m.tabBar.Next()
case "left", "shift+tab":
return m, m.tabBar.Prev()
case "1", "2", "3", "4":
index := int(msg.Runes[0] - '1')
if index >= 0 && index < 4 {
return m, m.tabBar.Activate(index)
}
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.tabBar.SetWidth(msg.Width)
case tabbar.TabChangeMsg:
// No additional action needed, the tabBar state is already updated
}

return m, nil
}

func (m model) View() string {
// Tab content
activeTab := m.tabBar.ActiveTab()
tabContent := m.content[activeTab]

// Style for the content area
contentStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#0074D9")).
Padding(1, 2).
Width(m.width - 4)

// Help text
helpText := "\nUse ← and → arrows to switch tabs | 1-4 to jump to tab | q to quit"

// Combine everything
return lipgloss.JoinVertical(
lipgloss.Left,
m.tabBar.View(),
"",
contentStyle.Render(tabContent),
helpText,
)
}

func main() {
p := tea.NewProgram(initialModel(), tea.WithAltScreen())

if _, err := p.Run(); err != nil {
fmt.Printf("Error running program: %v", err)
os.Exit(1)
}
}
94 changes: 94 additions & 0 deletions tabbar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# TabBar Component for Bubble Tea

A simple, customizable tab bar component for the [Bubble Tea](https://github.com/charmbracelet/bubbletea) TUI framework.

![TabBar Demo](https://github.com/charmbracelet/bubbles/raw/master/tabbar/demo.gif)

## Features

- Horizontal tabs with customizable styling
- Active tab highlighting
- Keyboard navigation between tabs
- Adjustable width
- Simple API integration with Bubble Tea applications

## Installation

```bash
go get github.com/charmbracelet/bubbles
```

## Usage

```go
import "github.com/charmbracelet/bubbles/tabbar"
```

### Basic Example

```go
// Create tabs
tabs := []string{"Home", "Projects", "Settings", "About"}

// Initialize the tab bar with Home tab active (index 0)
bar := tabbar.New(tabs, 0)

// In your model's Update method:
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "right", "tab":
return m, m.tabBar.Next()
case "left", "shift+tab":
return m, m.tabBar.Prev()
}
case tabbar.TabChangeMsg:
// Handle tab change if needed
}

// In your model's View method:
func (m model) View() string {
return m.tabBar.View()
}
```

### Customization

You can customize the appearance of the tab bar:

```go
// Customize colors
bar.ActiveBorderColor = lipgloss.Color("#ff0000") // Red for active tab
bar.InactiveBorderColor = lipgloss.Color("#333333") // Dark gray for inactive tabs

// Set width to fit your layout
bar.SetWidth(100)
```

## API

### Functions

- `New(tabs []string, activeIndex int) TabBar` - Create a new tab bar
- `(t TabBar) ActiveTab() int` - Get the active tab index
- `(t *TabBar) Next() tea.Cmd` - Move to next tab
- `(t *TabBar) Prev() tea.Cmd` - Move to previous tab
- `(t *TabBar) Activate(index int) tea.Cmd` - Activate tab at specific index
- `(t *TabBar) SetWidth(width int)` - Set width of the tab bar
- `(t TabBar) View() string` - Render the tab bar

### Message Types

- `TabChangeMsg` - Sent when the active tab changes

## Example

See the [example](./example) directory for a complete working example.

## License

[MIT](https://github.com/charmbracelet/bubbles/blob/master/LICENSE)

## Credits

This component was contributed to the Bubble Tea ecosystem by the CloudWorkstation project.
130 changes: 130 additions & 0 deletions tabbar/tabbar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Package tabbar provides a simple tab bar component for Bubble Tea applications.
package tabbar

import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

// TabChangeMsg is sent when the active tab changes.
type TabChangeMsg struct {
Index int
}

// Tab represents a tab in the tab bar.
type Tab struct {
title string
style lipgloss.Style
}

// TabBar is a simple tab bar component that can be used to switch between different views.
// It renders a horizontal list of tabs with customizable styles and borders.
type TabBar struct {
Tabs []Tab
ActiveTabIndex int
ActiveBorderColor lipgloss.Color
InactiveBorderColor lipgloss.Color
Width int
}

// New creates a new tab bar with the given tab titles and active index.
func New(tabs []string, activeIndex int) TabBar {
// Default colors
activeBorderColor := lipgloss.Color("#0074D9") // Blue
inactiveBorderColor := lipgloss.Color("#AAAAAA") // Light gray

// Create tab objects
tabItems := make([]Tab, len(tabs))
for i, title := range tabs {
tabItems[i] = Tab{
title: title,
style: lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(inactiveBorderColor).
Padding(0, 2),
}
}

return TabBar{
Tabs: tabItems,
ActiveTabIndex: activeIndex,
ActiveBorderColor: activeBorderColor,
InactiveBorderColor: inactiveBorderColor,
Width: 80,
}
}

// ActiveTab returns the index of the active tab.
func (t TabBar) ActiveTab() int {
return t.ActiveTabIndex
}

// Next activates the next tab and returns a command that will send a TabChangeMsg.
func (t *TabBar) Next() tea.Cmd {
if t.ActiveTabIndex < len(t.Tabs)-1 {
t.ActiveTabIndex++
} else {
t.ActiveTabIndex = 0
}
return func() tea.Msg {
return TabChangeMsg{Index: t.ActiveTabIndex}
}
}

// Prev activates the previous tab and returns a command that will send a TabChangeMsg.
func (t *TabBar) Prev() tea.Cmd {
if t.ActiveTabIndex > 0 {
t.ActiveTabIndex--
} else {
t.ActiveTabIndex = len(t.Tabs) - 1
}
return func() tea.Msg {
return TabChangeMsg{Index: t.ActiveTabIndex}
}
}

// Activate activates the tab at the given index and returns a command that will send a TabChangeMsg.
func (t *TabBar) Activate(index int) tea.Cmd {
if index >= 0 && index < len(t.Tabs) {
t.ActiveTabIndex = index
return func() tea.Msg {
return TabChangeMsg{Index: t.ActiveTabIndex}
}
}
return nil
}

// SetWidth sets the width of the tab bar.
func (t *TabBar) SetWidth(width int) {
t.Width = width
}

// View renders the tab bar.
func (t TabBar) View() string {
var renderedTabs []string

// Calculate approximate width for each tab
tabWidth := (t.Width / len(t.Tabs)) - 4 // Account for borders and spacing

for i, tab := range t.Tabs {
// Set border color based on active status
borderColor := t.InactiveBorderColor
if i == t.ActiveTabIndex {
borderColor = t.ActiveBorderColor
}

// Set tab style
style := tab.style.Copy().
BorderForeground(borderColor).
Width(tabWidth)

if i == t.ActiveTabIndex {
style = style.Bold(true)
}

// Render tab
renderedTabs = append(renderedTabs, style.Render(tab.title))
}

return lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
}
Loading