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
8 changes: 7 additions & 1 deletion src/cli/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func init() {

func createPrintCmd() *cobra.Command {
printCmd := &cobra.Command{
Use: "print [debug|primary|secondary|transient|right|tooltip|valid|error|preview]",
Use: "print [debug|primary|secondary|transient|right|tooltip|valid|error|preview|tmux-left|tmux-right]",
Short: "Print the prompt/context",
Long: "Print one of the prompts based on the location/use-case.",
ValidArgs: []string{
Expand All @@ -55,6 +55,8 @@ func createPrintCmd() *cobra.Command {
prompt.VALID,
prompt.ERROR,
prompt.PREVIEW,
prompt.TMUXLEFT,
prompt.TMUXRIGHT,
},
Args: NoArgsOrOneValidArg,
Run: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -123,6 +125,10 @@ func createPrintCmd() *cobra.Command {
fmt.Print(eng.ExtraPrompt(prompt.Error))
case prompt.PREVIEW:
fmt.Print(eng.Preview())
case prompt.TMUXLEFT:
fmt.Print(eng.TmuxStatusLeft())
case prompt.TMUXRIGHT:
fmt.Print(eng.TmuxStatusRight())
default:
_ = cmd.Help()
}
Expand Down
81 changes: 81 additions & 0 deletions src/color/tmux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package color

import (
"fmt"
"strconv"
"strings"
)

// TmuxColors wraps a String implementation and converts ANSI color codes
// to tmux native format strings (e.g., "fg=#rrggbb", "bg=colour42").
// This allows the rendering engine to emit tmux #[fg=...] / #[bg=...] tokens
// instead of ANSI escape sequences when rendering for the tmux status bar.
type TmuxColors struct {
inner String
}

// NewTmuxColors creates a TmuxColors that wraps the given String implementation.
func NewTmuxColors(inner String) *TmuxColors {
return &TmuxColors{inner: inner}
}

func (t *TmuxColors) Resolve(colorString Ansi) (Ansi, error) {
return t.inner.Resolve(colorString)
}

func (t *TmuxColors) ToAnsi(c Ansi, isBackground bool) Ansi {
ansiCode := t.inner.ToAnsi(c, isBackground)
return convertAnsiToTmux(ansiCode, isBackground)
}

// ansiCodeToColorName maps ANSI numeric codes (both fg and bg variants) to color names.
var ansiCodeToColorName = func() map[Ansi]string {
m := make(map[Ansi]string, len(ansiColorCodes)*2)
for name, codes := range ansiColorCodes {
m[codes[0]] = string(name) // fg code → name
m[codes[1]] = string(name) // bg code → name
}
return m
}()

// convertAnsiToTmux converts an ANSI color code string (as returned by Defaults.ToAnsi)
// into a tmux format token like "fg=#rrggbb" or "bg=colour42".
func convertAnsiToTmux(code Ansi, isBackground bool) Ansi {
if code.IsEmpty() || code.IsTransparent() {
return code
}

prefix := "fg"
if isBackground {
prefix = "bg"
}

s := string(code)

// True color: "38;2;R;G;B" (fg) or "48;2;R;G;B" (bg)
if (strings.HasPrefix(s, "38;2;") || strings.HasPrefix(s, "48;2;")) && strings.Count(s, ";") == 4 {
rgb := s[5:]
parts := strings.SplitN(rgb, ";", 3)
if len(parts) == 3 {
r, errR := strconv.ParseUint(parts[0], 10, 8)
g, errG := strconv.ParseUint(parts[1], 10, 8)
b, errB := strconv.ParseUint(parts[2], 10, 8)
if errR == nil && errG == nil && errB == nil {
return Ansi(fmt.Sprintf("%s=#%02x%02x%02x", prefix, r, g, b))
}
}
}

// 256-color: "38;5;N" (fg) or "48;5;N" (bg)
if strings.HasPrefix(s, "38;5;") || strings.HasPrefix(s, "48;5;") {
n := s[5:]
return Ansi(fmt.Sprintf("%s=colour%s", prefix, n))
}

// Named color (e.g., "30"=black fg, "40"=black bg, "90"=darkGray fg, …)
if name, ok := ansiCodeToColorName[code]; ok {
return Ansi(fmt.Sprintf("%s=%s", prefix, name))
}

return emptyColor
}
232 changes: 232 additions & 0 deletions src/color/tmux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package color

import (
"testing"

"github.com/stretchr/testify/assert"
)

// mockColors is a simple String implementation for testing TmuxColors.
type mockColors struct {
result Ansi
}

func (m *mockColors) ToAnsi(_ Ansi, _ bool) Ansi { return m.result }
func (m *mockColors) Resolve(c Ansi) (Ansi, error) { return c, nil }

func TestConvertAnsiToTmux(t *testing.T) {
tests := []struct {
name string
code Ansi
isBackground bool
expected Ansi
}{
// True color foreground
{
name: "true color fg red",
code: "38;2;255;0;0",
isBackground: false,
expected: "fg=#ff0000",
},
{
name: "true color fg white",
code: "38;2;255;255;255",
isBackground: false,
expected: "fg=#ffffff",
},
// True color background
{
name: "true color bg blue",
code: "48;2;0;0;255",
isBackground: true,
expected: "bg=#0000ff",
},
{
name: "true color bg mixed",
code: "48;2;18;52;86",
isBackground: true,
expected: "bg=#123456",
},
// 256-color foreground
{
name: "256-color fg",
code: "38;5;42",
isBackground: false,
expected: "fg=colour42",
},
// 256-color background
{
name: "256-color bg",
code: "48;5;200",
isBackground: true,
expected: "bg=colour200",
},
// Named colors (foreground)
{
name: "named black fg",
code: "30",
isBackground: false,
expected: "fg=black",
},
{
name: "named red fg",
code: "31",
isBackground: false,
expected: "fg=red",
},
{
name: "named white fg",
code: "37",
isBackground: false,
expected: "fg=white",
},
// Named colors (background)
{
name: "named black bg",
code: "40",
isBackground: true,
expected: "bg=black",
},
{
name: "named green bg",
code: "42",
isBackground: true,
expected: "bg=green",
},
// Bright/high-intensity named colors
{
name: "darkGray fg",
code: "90",
isBackground: false,
expected: "fg=darkGray",
},
{
name: "lightBlue bg",
code: "104",
isBackground: true,
expected: "bg=lightBlue",
},
// Special values pass through
{
name: "empty color",
code: emptyColor,
isBackground: false,
expected: emptyColor,
},
{
name: "transparent",
code: Transparent,
isBackground: false,
expected: Transparent,
},
// Unknown code returns empty
{
name: "unknown code",
code: "99",
isBackground: false,
expected: emptyColor,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := convertAnsiToTmux(tc.code, tc.isBackground)
assert.Equal(t, tc.expected, result)
})
}
}

func TestTmuxColorsToAnsi(t *testing.T) {
tests := []struct {
name string
innerResult Ansi
isBackground bool
expected Ansi
}{
{
name: "hex fg via inner",
innerResult: "38;2;255;128;0",
isBackground: false,
expected: "fg=#ff8000",
},
{
name: "hex bg via inner",
innerResult: "48;2;0;128;255",
isBackground: true,
expected: "bg=#0080ff",
},
{
name: "256-color via inner",
innerResult: "38;5;100",
isBackground: false,
expected: "fg=colour100",
},
{
name: "named color via inner",
innerResult: "32",
isBackground: false,
expected: "fg=green",
},
{
name: "transparent passes through",
innerResult: Transparent,
isBackground: false,
expected: Transparent,
},
{
name: "empty passes through",
innerResult: emptyColor,
isBackground: false,
expected: emptyColor,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mock := &mockColors{result: tc.innerResult}
tc2 := NewTmuxColors(mock)
result := tc2.ToAnsi("anycolor", tc.isBackground)
assert.Equal(t, tc.expected, result)
})
}
}

func TestTmuxColorsResolve(t *testing.T) {
mock := &mockColors{}
tc := NewTmuxColors(mock)
result, err := tc.Resolve("p:mycolor")
assert.NoError(t, err)
assert.Equal(t, Ansi("p:mycolor"), result)
}

func TestTmuxColorsWithDefaults(t *testing.T) {
// Integration test using a real Defaults instance with TrueColor enabled
saved := TrueColor
TrueColor = true
t.Cleanup(func() { TrueColor = saved })

defaults := &Defaults{}
tc := NewTmuxColors(defaults)

// Hex color → tmux format
fg := tc.ToAnsi("#ff0000", false)
assert.Equal(t, Ansi("fg=#ff0000"), fg)

bg := tc.ToAnsi("#0000ff", true)
assert.Equal(t, Ansi("bg=#0000ff"), bg)

// Named color → tmux format
fgRed := tc.ToAnsi("red", false)
assert.Equal(t, Ansi("fg=red"), fgRed)

bgBlue := tc.ToAnsi("blue", true)
assert.Equal(t, Ansi("bg=blue"), bgBlue)

// 256-color → tmux format
fg256 := tc.ToAnsi("42", false)
assert.Equal(t, Ansi("fg=colour42"), fg256)

// Transparent passes through
fgTransparent := tc.ToAnsi(Transparent, false)
assert.Equal(t, Transparent, fgTransparent)
}
3 changes: 2 additions & 1 deletion src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ type Config struct {
PatchPwshBleed bool `json:"patch_pwsh_bleed,omitempty" toml:"patch_pwsh_bleed,omitempty" yaml:"patch_pwsh_bleed,omitempty"`
AutoUpgrade bool `json:"-" toml:"-" yaml:"-"`
EnableCursorPositioning bool `json:"enable_cursor_positioning,omitempty" toml:"enable_cursor_positioning,omitempty" yaml:"enable_cursor_positioning,omitempty"`
Streaming int `json:"streaming,omitempty" toml:"streaming,omitempty" yaml:"streaming,omitempty"`
Streaming int `json:"streaming,omitempty" toml:"streaming,omitempty" yaml:"streaming,omitempty"`
Tmux *TmuxConfig `json:"tmux,omitempty" toml:"tmux,omitempty" yaml:"tmux,omitempty"`
}

func (cfg *Config) MakeColors(env runtime.Environment) color.String {
Expand Down
4 changes: 4 additions & 0 deletions src/config/segment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func init() {
gob.Register(&segments.Terraform{})
gob.Register(&segments.Text{})
gob.Register(&segments.Time{})
gob.Register(&segments.Tmux{})
gob.Register(&segments.Todoist{})
gob.Register(&segments.UI5Tooling{})
gob.Register(&segments.Umbraco{})
Expand Down Expand Up @@ -343,6 +344,8 @@ const (
TEXT SegmentType = "text"
// TIME writes the current timestamp
TIME SegmentType = "time"
// TMUX writes the current tmux session name and optionally the window list
TMUX SegmentType = "tmux"
// TODOIST segment
TODOIST SegmentType = "todoist"
// UI5 Tooling segment
Expand Down Expand Up @@ -470,6 +473,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
TERRAFORM: func() SegmentWriter { return &segments.Terraform{} },
TEXT: func() SegmentWriter { return &segments.Text{} },
TIME: func() SegmentWriter { return &segments.Time{} },
TMUX: func() SegmentWriter { return &segments.Tmux{} },
TODOIST: func() SegmentWriter { return &segments.Todoist{} },
UI5TOOLING: func() SegmentWriter { return &segments.UI5Tooling{} },
UMBRACO: func() SegmentWriter { return &segments.Umbraco{} },
Expand Down
12 changes: 12 additions & 0 deletions src/config/tmux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package config

// TmuxConfig holds the configuration for rendering tmux status bar sections.
type TmuxConfig struct {
StatusLeft TmuxStatusSection `json:"status_left" yaml:"status_left" toml:"status_left"`
StatusRight TmuxStatusSection `json:"status_right" yaml:"status_right" toml:"status_right"`
}

// TmuxStatusSection holds a list of blocks to render for one tmux status section.
type TmuxStatusSection struct {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is never used for custom functions or am I missing something?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This holds the config for the left and right parts of the tmux status bar. This is the equivalent of the claude block for the claude code status line

Blocks []*Block `json:"blocks" yaml:"blocks" toml:"blocks"`
}
Loading