-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(tmux): Adds ability to use oh-my-posh for the tmux statusline #7323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
colings86
wants to merge
5
commits into
JanDeDobbeleer:main
Choose a base branch
from
colings86:feature/tmux
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1885c69
feat(tmux): add native tmux status bar support
colings86 87dfb5f
docs(tmux): add tmux status bar documentation and configuration
colings86 f19b7aa
feat(tmux): add tmux segments to JSON schema
colings86 8d0ebe5
refactor(tmux): merge tmux_session and tmux_window_list into a single…
colings86 a1378ca
feat(tmux): emit native tmux color/style tokens for status bar rendering
colings86 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
| Blocks []*Block `json:"blocks" yaml:"blocks" toml:"blocks"` | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
claudeblock for the claude code status line