diff --git a/src/cli/print.go b/src/cli/print.go
index cbcc692ddac7..7e9308a22e4c 100644
--- a/src/cli/print.go
+++ b/src/cli/print.go
@@ -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{
@@ -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) {
@@ -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()
}
diff --git a/src/color/tmux.go b/src/color/tmux.go
new file mode 100644
index 000000000000..e7c209cac2dc
--- /dev/null
+++ b/src/color/tmux.go
@@ -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
+}
diff --git a/src/color/tmux_test.go b/src/color/tmux_test.go
new file mode 100644
index 000000000000..fa52eeaccc31
--- /dev/null
+++ b/src/color/tmux_test.go
@@ -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)
+}
diff --git a/src/config/config.go b/src/config/config.go
index a4087ba1ec57..858c18bd6956 100644
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -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 {
diff --git a/src/config/segment_types.go b/src/config/segment_types.go
index 782b6671e5ef..82bc19f6de8f 100644
--- a/src/config/segment_types.go
+++ b/src/config/segment_types.go
@@ -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{})
@@ -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
@@ -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{} },
diff --git a/src/config/tmux.go b/src/config/tmux.go
new file mode 100644
index 000000000000..c4f4032d9947
--- /dev/null
+++ b/src/config/tmux.go
@@ -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"`
+}
diff --git a/src/prompt/engine.go b/src/prompt/engine.go
index e97fc6cc5d0e..a065e5fef1a1 100644
--- a/src/prompt/engine.go
+++ b/src/prompt/engine.go
@@ -45,6 +45,8 @@ const (
VALID = "valid"
ERROR = "error"
PREVIEW = "preview"
+ TMUXLEFT = "tmux-left"
+ TMUXRIGHT = "tmux-right"
)
func (e *Engine) write(txt string) {
@@ -568,6 +570,9 @@ func New(flags *runtime.Flags) *Engine {
terminal.Init(sh)
terminal.BackgroundColor = cfg.TerminalBackground.ResolveTemplate()
terminal.Colors = cfg.MakeColors(env)
+ if env.Shell() == shell.TMUX {
+ terminal.Colors = color.NewTmuxColors(terminal.Colors)
+ }
terminal.Plain = flags.Plain
eng := &Engine{
diff --git a/src/prompt/tmux.go b/src/prompt/tmux.go
new file mode 100644
index 000000000000..f7484de46541
--- /dev/null
+++ b/src/prompt/tmux.go
@@ -0,0 +1,38 @@
+package prompt
+
+import "github.com/jandedobbeleer/oh-my-posh/src/config"
+
+// TmuxStatusLeft renders the tmux status-left section from the config's tmux.status_left blocks.
+func (e *Engine) TmuxStatusLeft() string {
+ if e.Config.Tmux == nil {
+ return ""
+ }
+
+ return e.renderTmuxSection(e.Config.Tmux.StatusLeft.Blocks)
+}
+
+// TmuxStatusRight renders the tmux status-right section from the config's tmux.status_right blocks.
+func (e *Engine) TmuxStatusRight() string {
+ if e.Config.Tmux == nil {
+ return ""
+ }
+
+ return e.renderTmuxSection(e.Config.Tmux.StatusRight.Blocks)
+}
+
+// renderTmuxSection renders a slice of blocks using the existing block rendering pipeline
+// and returns the concatenated result. Shell integration sequences are intentionally
+// skipped here since tmux status bars do not use OSC 133 marks.
+func (e *Engine) renderTmuxSection(blocks []*config.Block) string {
+ if len(blocks) == 0 {
+ return ""
+ }
+
+ cycle = &e.Config.Cycle
+
+ for _, block := range blocks {
+ e.renderBlock(block, true)
+ }
+
+ return e.string()
+}
diff --git a/src/segments/tmux.go b/src/segments/tmux.go
new file mode 100644
index 000000000000..130466a1a2f9
--- /dev/null
+++ b/src/segments/tmux.go
@@ -0,0 +1,96 @@
+package segments
+
+import (
+ "strings"
+
+ "github.com/jandedobbeleer/oh-my-posh/src/segments/options"
+)
+
+const (
+ fetchWindows options.Option = "fetch_windows"
+)
+
+// TmuxWindow holds information about a single tmux window.
+type TmuxWindow struct {
+ Index string
+ Name string
+ Active bool
+}
+
+// Tmux displays the current tmux session name and optionally the window list.
+type Tmux struct {
+ Base
+
+ SessionName string
+ Windows []TmuxWindow
+}
+
+func (t *Tmux) Template() string {
+ return " \ue7a2 {{ .SessionName }}{{ if .Windows }} | {{ range .Windows }}{{if .Active}}*{{end}}{{.Index}}:{{.Name}} {{end}}{{end}} "
+}
+
+func (t *Tmux) Enabled() bool {
+ if !t.fetchSessionName() {
+ return false
+ }
+
+ if t.options.Bool(fetchWindows, false) {
+ t.Windows = t.fetchWindowList()
+ }
+
+ return true
+}
+
+func (t *Tmux) fetchSessionName() bool {
+ // Try the tmux command for the exact session name (works when tmux is in PATH).
+ if name, err := t.env.RunCommand("tmux", "display-message", "-p", "#S"); err == nil {
+ t.SessionName = strings.TrimSpace(name)
+ if t.SessionName != "" {
+ return true
+ }
+ }
+
+ // When running from the tmux status bar (#(...) format), the tmux binary may not
+ // be in the minimal PATH used by /bin/sh. Use $TMUX to confirm we are inside tmux,
+ // then fall back to the tmux format alias #S — tmux expands it to the actual session
+ // name when processing the status bar format string.
+ if t.env.Getenv("TMUX") == "" {
+ return false
+ }
+
+ t.SessionName = "#S"
+ return true
+}
+
+func (t *Tmux) fetchWindowList() []TmuxWindow {
+ output, err := t.env.RunCommand("tmux", "list-windows", "-F", "#{window_index}\t#{window_name}\t#{window_active}")
+ if err != nil {
+ return nil
+ }
+
+ return t.parseWindows(output)
+}
+
+func (t *Tmux) parseWindows(output string) []TmuxWindow {
+ lines := strings.Split(strings.TrimSpace(output), "\n")
+ windows := make([]TmuxWindow, 0, len(lines))
+
+ for _, line := range lines {
+ if line == "" {
+ continue
+ }
+
+ parts := strings.Split(line, "\t")
+ if len(parts) < 3 {
+ continue
+ }
+
+ windows = append(windows, TmuxWindow{
+ Index: parts[0],
+ Name: parts[1],
+ Active: parts[2] == "1",
+ })
+ }
+
+ return windows
+}
diff --git a/src/segments/tmux_test.go b/src/segments/tmux_test.go
new file mode 100644
index 000000000000..52642682981d
--- /dev/null
+++ b/src/segments/tmux_test.go
@@ -0,0 +1,170 @@
+package segments
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
+ "github.com/jandedobbeleer/oh-my-posh/src/segments/options"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const tmuxListWindowsFmt = "#{window_index}\t#{window_name}\t#{window_active}"
+
+func TestTmuxEnabled(t *testing.T) {
+ cases := []struct {
+ Case string
+ CommandOut string
+ CommandErr error
+ TmuxEnv string
+ Expected bool
+ SessionName string
+ }{
+ {
+ Case: "tmux command succeeds",
+ CommandOut: "mysession\n",
+ Expected: true,
+ SessionName: "mysession",
+ },
+ {
+ Case: "tmux command succeeds with whitespace",
+ CommandOut: " main \n",
+ Expected: true,
+ SessionName: "main",
+ },
+ {
+ Case: "tmux command fails, fallback to #S when TMUX env set",
+ CommandErr: errors.New("not in tmux"),
+ TmuxEnv: "/tmp/tmux-1000/default,12345,0",
+ Expected: true,
+ SessionName: "#S",
+ },
+ {
+ Case: "tmux command fails, TMUX env set but malformed — still in tmux",
+ CommandErr: errors.New("not in tmux"),
+ TmuxEnv: "bad-value",
+ Expected: true,
+ SessionName: "#S",
+ },
+ {
+ Case: "tmux command fails, TMUX env empty — not in tmux",
+ CommandErr: errors.New("not in tmux"),
+ TmuxEnv: "",
+ Expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ env := new(mock.Environment)
+ env.On("RunCommand", "tmux", []string{"display-message", "-p", "#S"}).
+ Return(tc.CommandOut, tc.CommandErr)
+ env.On("Getenv", "TMUX").Return(tc.TmuxEnv)
+
+ seg := &Tmux{}
+ seg.Init(options.Map{}, env)
+
+ result := seg.Enabled()
+
+ assert.Equal(t, tc.Expected, result, tc.Case)
+ if tc.Expected {
+ assert.Equal(t, tc.SessionName, seg.SessionName, tc.Case)
+ }
+ }
+}
+
+func TestTmuxNoWindowsByDefault(t *testing.T) {
+ env := new(mock.Environment)
+ env.On("RunCommand", "tmux", []string{"display-message", "-p", "#S"}).
+ Return("work\n", nil)
+
+ seg := &Tmux{}
+ seg.Init(options.Map{}, env)
+
+ enabled := seg.Enabled()
+ assert.True(t, enabled)
+ assert.Nil(t, seg.Windows, "Windows should be nil when fetch_windows is false")
+}
+
+func TestTmuxFetchWindows(t *testing.T) {
+ env := new(mock.Environment)
+ env.On("RunCommand", "tmux", []string{"display-message", "-p", "#S"}).
+ Return("work\n", nil)
+ env.On("RunCommand", "tmux", []string{"list-windows", "-F", tmuxListWindowsFmt}).
+ Return("0\tbash\t1\n1\tnvim\t0\n", nil)
+
+ seg := &Tmux{}
+ seg.Init(options.Map{fetchWindows: true}, env)
+
+ enabled := seg.Enabled()
+ assert.True(t, enabled)
+ assert.Len(t, seg.Windows, 2)
+ assert.Equal(t, "0", seg.Windows[0].Index)
+ assert.Equal(t, "bash", seg.Windows[0].Name)
+ assert.True(t, seg.Windows[0].Active)
+ assert.Equal(t, "1", seg.Windows[1].Index)
+ assert.Equal(t, "nvim", seg.Windows[1].Name)
+ assert.False(t, seg.Windows[1].Active)
+}
+
+func TestTmuxFetchWindowsCommandFails(t *testing.T) {
+ env := new(mock.Environment)
+ env.On("RunCommand", "tmux", []string{"display-message", "-p", "#S"}).
+ Return("work\n", nil)
+ env.On("RunCommand", "tmux", []string{"list-windows", "-F", tmuxListWindowsFmt}).
+ Return("", errors.New("not in tmux"))
+
+ seg := &Tmux{}
+ seg.Init(options.Map{fetchWindows: true}, env)
+
+ enabled := seg.Enabled()
+ // Segment is still enabled — session name was fetched successfully.
+ assert.True(t, enabled)
+ assert.Nil(t, seg.Windows)
+}
+
+func TestTmuxFetchWindowsEmptyOutput(t *testing.T) {
+ env := new(mock.Environment)
+ env.On("RunCommand", "tmux", []string{"display-message", "-p", "#S"}).
+ Return("work\n", nil)
+ env.On("RunCommand", "tmux", []string{"list-windows", "-F", tmuxListWindowsFmt}).
+ Return("", nil)
+
+ seg := &Tmux{}
+ seg.Init(options.Map{fetchWindows: true}, env)
+
+ enabled := seg.Enabled()
+ assert.True(t, enabled)
+ assert.Empty(t, seg.Windows)
+}
+
+func TestTmuxParseWindows(t *testing.T) {
+ seg := &Tmux{}
+
+ windows := seg.parseWindows("0\tbash\t1\n1\tnvim\t0\n2\thtop\t0")
+ assert.Len(t, windows, 3)
+ assert.Equal(t, "0", windows[0].Index)
+ assert.Equal(t, "bash", windows[0].Name)
+ assert.True(t, windows[0].Active)
+ assert.Equal(t, "1", windows[1].Index)
+ assert.Equal(t, "nvim", windows[1].Name)
+ assert.False(t, windows[1].Active)
+ assert.Equal(t, "2", windows[2].Index)
+ assert.Equal(t, "htop", windows[2].Name)
+ assert.False(t, windows[2].Active)
+}
+
+func TestTmuxParseWindowsSkipsMalformedLines(t *testing.T) {
+ seg := &Tmux{}
+
+ windows := seg.parseWindows("0\tbash\t1\nbad-line\n1\tnvim\t0")
+ assert.Len(t, windows, 2)
+ assert.Equal(t, "bash", windows[0].Name)
+ assert.Equal(t, "nvim", windows[1].Name)
+}
+
+func TestTmuxTemplate(t *testing.T) {
+ seg := &Tmux{}
+ assert.Contains(t, seg.Template(), "{{ .SessionName }}")
+ assert.Contains(t, seg.Template(), ".Windows")
+}
diff --git a/src/shell/constants.go b/src/shell/constants.go
index d0f06d54f71a..d3865b5ca780 100644
--- a/src/shell/constants.go
+++ b/src/shell/constants.go
@@ -11,4 +11,5 @@ const (
ELVISH = "elvish"
XONSH = "xonsh"
CLAUDE = "claude"
+ TMUX = "tmux"
)
diff --git a/src/shell/formats.go b/src/shell/formats.go
index b47149aaada1..7b41381b66dd 100644
--- a/src/shell/formats.go
+++ b/src/shell/formats.go
@@ -25,6 +25,13 @@ type Formats struct {
ITermPromptMark string
ITermCurrentDir string
ITermRemoteHost string
+
+ // Color format fields
+ ColorEscape string // printf format for a color token, e.g. "\x1b[%sm" or "#[%s]"
+ ColorReset string // full color reset sequence, e.g. "\x1b[0m" or "#[default]"
+ ColorBgReset string // background-only reset, e.g. "\x1b[49m" or "#[bg=default]"
+ TransparentStart string // start of transparent fg effect (may contain %s for bg color)
+ TransparentEnd string // end of transparent fg effect
}
func GetFormats(shell string) *Formats {
@@ -53,6 +60,11 @@ func GetFormats(shell string) *Formats {
EscapeSequences: map[rune]string{
'\\': `\\`,
},
+ ColorEscape: "\x1b[%sm",
+ ColorReset: "\x1b[0m",
+ ColorBgReset: "\x1b[49m",
+ TransparentStart: "\x1b[0m\x1b[%s;49m\x1b[7m",
+ TransparentEnd: "\x1b[27m",
}
case ZSH:
formats = &Formats{
@@ -76,6 +88,20 @@ func GetFormats(shell string) *Formats {
EscapeSequences: map[rune]string{
'%': "%%",
},
+ ColorEscape: "\x1b[%sm",
+ ColorReset: "\x1b[0m",
+ ColorBgReset: "\x1b[49m",
+ TransparentStart: "\x1b[0m\x1b[%s;49m\x1b[7m",
+ TransparentEnd: "\x1b[27m",
+ }
+ case TMUX:
+ formats = &Formats{
+ Escape: "%s",
+ ColorEscape: "#[%s]",
+ ColorReset: "#[default]",
+ ColorBgReset: "#[bg=default]",
+ TransparentStart: "", // no reverse-video transparency in tmux status bar
+ TransparentEnd: "",
}
default:
formats = &Formats{
@@ -90,14 +116,19 @@ func GetFormats(shell string) *Formats {
// when in fish on Linux, it seems hyperlinks ending with \\ print a \
// unlike on macOS. However, this is a fish bug, so do not try to fix it here:
// https://github.com/JanDeDobbeleer/oh-my-posh/pull/3288#issuecomment-1369137068
- HyperlinkStart: "\x1b]8;;",
- HyperlinkCenter: "\x1b\\",
- HyperlinkEnd: "\x1b]8;;\x1b\\",
- Osc99: "\x1b]9;9;%s\x1b\\",
- Osc7: "\x1b]7;file://%s/%s\x1b\\",
- Osc51: "\x1b]51;A%s@%s:%s\x1b\\",
- ITermCurrentDir: "\x1b]1337;CurrentDir=%s\x07",
- ITermRemoteHost: "\x1b]1337;RemoteHost=%s@%s\x07",
+ HyperlinkStart: "\x1b]8;;",
+ HyperlinkCenter: "\x1b\\",
+ HyperlinkEnd: "\x1b]8;;\x1b\\",
+ Osc99: "\x1b]9;9;%s\x1b\\",
+ Osc7: "\x1b]7;file://%s/%s\x1b\\",
+ Osc51: "\x1b]51;A%s@%s:%s\x1b\\",
+ ITermCurrentDir: "\x1b]1337;CurrentDir=%s\x07",
+ ITermRemoteHost: "\x1b]1337;RemoteHost=%s@%s\x07",
+ ColorEscape: "\x1b[%sm",
+ ColorReset: "\x1b[0m",
+ ColorBgReset: "\x1b[49m",
+ TransparentStart: "\x1b[0m\x1b[%s;49m\x1b[7m",
+ TransparentEnd: "\x1b[27m",
}
}
diff --git a/src/terminal/writer.go b/src/terminal/writer.go
index 5074a133bd4b..fc98d489d340 100644
--- a/src/terminal/writer.go
+++ b/src/terminal/writer.go
@@ -25,7 +25,7 @@ type style struct {
}
var (
- knownStyles = []*style{
+ ansiKnownStyles = []*style{
{AnchorStart: ``, AnchorEnd: ``, Start: "\x1b[1m", End: "\x1b[22m"},
{AnchorStart: ``, AnchorEnd: ``, Start: "\x1b[4m", End: "\x1b[24m"},
{AnchorStart: ``, AnchorEnd: ``, Start: "\x1b[53m", End: "\x1b[55m"},
@@ -36,6 +36,19 @@ var (
{AnchorStart: ``, AnchorEnd: ``, Start: "\x1b[7m", End: "\x1b[27m"},
}
+ tmuxKnownStyles = []*style{
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[bold]", End: "#[nobold]"},
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[underscore]", End: "#[nounderscore]"},
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[overline]", End: "#[nooverline]"},
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[italics]", End: "#[noitalics]"},
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[strikethrough]", End: "#[nostrikethrough]"},
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[dim]", End: "#[nodim]"},
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[blink]", End: "#[noblink]"},
+ {AnchorStart: ``, AnchorEnd: ``, Start: "#[reverse]", End: "#[noreverse]"},
+ }
+
+ knownStyles []*style
+
resetStyle = &style{AnchorStart: "RESET", AnchorEnd: `>`, End: "\x1b[0m"}
backgroundStyle = &style{AnchorStart: "BACKGROUND", AnchorEnd: `>`, End: "\x1b[49m"}
@@ -66,11 +79,7 @@ var (
)
const (
- AnchorRegex = `^(?P<(?P[^,<>]+)?,?(?P[^<>]+)?>)`
- colorise = "\x1b[%sm"
- transparentStart = "\x1b[0m\x1b[%s;49m\x1b[7m"
- transparentEnd = "\x1b[27m"
- backgroundEnd = "\x1b[49m"
+ AnchorRegex = `^(?P<(?P[^,<>]+)?,?(?P[^<>]+)?>)`
AnsiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
@@ -110,6 +119,15 @@ func Init(sh string) {
color.TrueColor = Program != AppleTerminal
formats = shell.GetFormats(Shell)
+
+ resetStyle.End = formats.ColorReset
+ backgroundStyle.End = formats.ColorBgReset
+
+ if sh == shell.TMUX {
+ knownStyles = tmuxKnownStyles
+ } else {
+ knownStyles = ansiKnownStyles
+ }
}
func getTerminalName() string {
@@ -442,18 +460,20 @@ func writeSegmentColors() {
switch {
case fg.IsTransparent() && len(BackgroundColor) != 0:
background := Colors.ToAnsi(BackgroundColor, false)
- writeEscapedAnsiString(fmt.Sprintf(colorise, background))
- writeEscapedAnsiString(fmt.Sprintf(colorise, bg.ToForeground()))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, background))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, bg.ToForeground()))
case fg.IsTransparent() && !bg.IsEmpty():
isTransparent = true
- writeEscapedAnsiString(fmt.Sprintf(transparentStart, bg))
+ if formats.TransparentStart != "" {
+ writeEscapedAnsiString(fmt.Sprintf(formats.TransparentStart, bg))
+ }
default:
if !bg.IsEmpty() && !bg.IsTransparent() {
- writeEscapedAnsiString(fmt.Sprintf(colorise, bg))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, bg))
}
if !fg.IsEmpty() && !fg.IsTransparent() {
- writeEscapedAnsiString(fmt.Sprintf(colorise, fg))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, fg))
}
}
@@ -508,28 +528,30 @@ func writeAnchorOverride(match map[string]string, background color.Ansi, i int)
if currentColor.Foreground().IsTransparent() && len(BackgroundColor) != 0 {
background := Colors.ToAnsi(BackgroundColor, false)
- writeEscapedAnsiString(fmt.Sprintf(colorise, background))
- writeEscapedAnsiString(fmt.Sprintf(colorise, currentColor.Background().ToForeground()))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, background))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, currentColor.Background().ToForeground()))
return position
}
if currentColor.Foreground().IsTransparent() && !currentColor.Background().IsTransparent() {
isTransparent = true
- writeEscapedAnsiString(fmt.Sprintf(transparentStart, currentColor.Background()))
+ if formats.TransparentStart != "" {
+ writeEscapedAnsiString(fmt.Sprintf(formats.TransparentStart, currentColor.Background()))
+ }
return position
}
if currentColor.Background() != backgroundColor {
// end the colors in case we have a transparent background
if currentColor.Background().IsTransparent() {
- writeEscapedAnsiString(backgroundEnd)
+ writeEscapedAnsiString(formats.ColorBgReset)
} else {
- writeEscapedAnsiString(fmt.Sprintf(colorise, currentColor.Background()))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, currentColor.Background()))
}
}
if currentColor.Foreground() != foregroundColor {
- writeEscapedAnsiString(fmt.Sprintf(colorise, currentColor.Foreground()))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, currentColor.Foreground()))
}
return position
@@ -557,12 +579,12 @@ func endColorOverride(position int) int {
previousBg := currentColor.Background()
previousFg := currentColor.Foreground()
- if isTransparent {
- writeEscapedAnsiString(transparentEnd)
+ if isTransparent && formats.TransparentEnd != "" {
+ writeEscapedAnsiString(formats.TransparentEnd)
}
if previousBg != bg {
- background := fmt.Sprintf(colorise, previousBg)
+ background := fmt.Sprintf(formats.ColorEscape, previousBg)
if previousBg.IsClear() {
background = backgroundStyle.End
}
@@ -571,7 +593,7 @@ func endColorOverride(position int) int {
}
if previousFg != fg {
- writeEscapedAnsiString(fmt.Sprintf(colorise, previousFg))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, previousFg))
}
return position
@@ -585,8 +607,8 @@ func endColorOverride(position int) int {
return position
}
- if isTransparent {
- writeEscapedAnsiString(transparentEnd)
+ if isTransparent && formats.TransparentEnd != "" {
+ writeEscapedAnsiString(formats.TransparentEnd)
}
if backgroundColor.IsClear() {
@@ -594,11 +616,11 @@ func endColorOverride(position int) int {
}
if currentColor.Background() != backgroundColor && !backgroundColor.IsClear() {
- writeEscapedAnsiString(fmt.Sprintf(colorise, backgroundColor))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, backgroundColor))
}
if (currentColor.Foreground() != foregroundColor || isTransparent) && !foregroundColor.IsClear() {
- writeEscapedAnsiString(fmt.Sprintf(colorise, foregroundColor))
+ writeEscapedAnsiString(fmt.Sprintf(formats.ColorEscape, foregroundColor))
}
isTransparent = false
diff --git a/themes/schema.json b/themes/schema.json
index e4cc3b1740b0..c8fde2966991 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -457,6 +457,7 @@
"terraform",
"text",
"time",
+ "tmux",
"todoist",
"ui5tooling",
"umbraco",
@@ -4472,6 +4473,32 @@
}
}
},
+ {
+ "if": {
+ "properties": {
+ "type": {
+ "const": "tmux"
+ }
+ }
+ },
+ "then": {
+ "title": "Tmux Segment",
+ "description": "https://ohmyposh.dev/docs/segments/cli/tmux",
+ "properties": {
+ "options": {
+ "properties": {
+ "fetch_windows": {
+ "type": "boolean",
+ "title": "Fetch Windows",
+ "description": "Populate the Windows list with the current tmux window list",
+ "default": false
+ }
+ },
+ "unevaluatedProperties": false
+ }
+ }
+ }
+ },
{
"if": {
"properties": {
@@ -5001,6 +5028,35 @@
"description": "https://ohmyposh.dev/docs/experimental/streaming",
"default": 100
},
+ "tmux": {
+ "type": "object",
+ "title": "Tmux Config",
+ "description": "https://ohmyposh.dev/docs/configuration/tmux",
+ "properties": {
+ "status_left": {
+ "type": "object",
+ "title": "Tmux Status Left",
+ "properties": {
+ "blocks": {
+ "type": "array",
+ "title": "Block array",
+ "items": { "$ref": "#/definitions/block" }
+ }
+ }
+ },
+ "status_right": {
+ "type": "object",
+ "title": "Tmux Status Right",
+ "properties": {
+ "blocks": {
+ "type": "array",
+ "title": "Block array",
+ "items": { "$ref": "#/definitions/block" }
+ }
+ }
+ }
+ }
+ },
"blocks": {
"type": "array",
"title": "Block array",
diff --git a/website/docs/configuration/general.mdx b/website/docs/configuration/general.mdx
index bbf9c633be65..c614d071c4bd 100644
--- a/website/docs/configuration/general.mdx
+++ b/website/docs/configuration/general.mdx
@@ -143,6 +143,7 @@ For example, the following is a valid `--config` flag:
| `version` | `int` | `4` | the config version, currently at `4` |
| `extends` | `string` | | the configuration to [extend] from |
| `streaming` | `int` | | enable streaming mode with a timeout in milliseconds for pending segments. See [streaming] |
+| `tmux` | [`TmuxConfig`](/docs/configuration/tmux) | | configure the tmux status bar sections rendered by `oh-my-posh print tmux-left` and `tmux-right` |
### Maps
diff --git a/website/docs/configuration/tmux.mdx b/website/docs/configuration/tmux.mdx
new file mode 100644
index 000000000000..16f8f11459c2
--- /dev/null
+++ b/website/docs/configuration/tmux.mdx
@@ -0,0 +1,158 @@
+---
+id: tmux
+title: Tmux
+sidebar_label: Tmux
+---
+
+## What
+
+Oh My Posh can render your entire [tmux][tmux] status bar — session name, window list, git
+status, path, and clock — using the same block/segment/style pipeline that powers your
+terminal prompt.
+
+The `tmux:` config key holds two independent sections: `status_left` and `status_right`.
+Each section accepts the same `blocks` array as the top-level prompt config, so any
+segment that works in your prompt also works in the tmux status bar.
+
+## How it works
+
+tmux supports `#(command)` in its format strings, which runs a shell command and pastes its
+raw output into the status bar. Oh My Posh's `--shell tmux` flag produces unwrapped ANSI
+escape sequences (no shell-specific prompt wrappers), so the output appears correctly inside
+`status-left` and `status-right`.
+
+The window list is rendered by the [`tmux`][tmux-segment] segment when `fetch_windows: true`
+is set. This calls `tmux list-windows` in a **single invocation**, so all window data is
+available to the template at once.
+
+## Sample Configuration
+
+import Config from "@site/src/components/Config.js";
+import Tabs from "@theme/Tabs";
+import TabItem from "@theme/TabItem";
+
+
+
+## Settings
+
+| Name | Type | Description |
+| -------------- | --------------------------------- | ---------------------------------- |
+| `status_left` | [`TmuxStatusSection`](#section) | blocks rendered for `status-left` |
+| `status_right` | [`TmuxStatusSection`](#section) | blocks rendered for `status-right` |
+
+### TmuxStatusSection {#section}
+
+| Name | Type | Description |
+| -------- | ----------------- | ------------------------------------------------ |
+| `blocks` | `[]`[`Block`][block] | the blocks to render, identical to the top-level `blocks` array |
+
+## tmux.conf integration
+
+Add the following to `~/.tmux.conf` (adjust the config path to match yours):
+
+```bash
+set -g status-interval 5
+set -g status-left-length 400
+set -g status-right-length 200
+
+set -g status-left "#(oh-my-posh print tmux-left --config ~/.config/ohmyposh/config.yaml --shell tmux)"
+set -g status-right "#(oh-my-posh print tmux-right --config ~/.config/ohmyposh/config.yaml --shell tmux --pwd \"#{pane_current_path}\")"
+
+# Clear tmux's own window list formatting so OMP controls it entirely
+set -g window-status-format ""
+set -g window-status-current-format ""
+
+# Refresh the status bar immediately when windows are opened, closed, or renamed
+set-hook -g window-linked "refresh-client -S"
+set-hook -g window-unlinked "refresh-client -S"
+set-hook -g window-renamed "refresh-client -S"
+set-hook -g client-session-changed "refresh-client -S"
+```
+
+Then reload your config:
+
+```bash
+tmux source-file ~/.tmux.conf
+```
+
+:::tip
+`#{pane_current_path}` is a tmux format variable that is expanded *before* the shell command
+runs. Passing it via `--pwd` means segments like [git][git] and [path][path] in `status_right`
+always see the active pane's current directory.
+:::
+
+## Available tmux segments
+
+| Segment | Description |
+| --------------------------- | ---------------------------------------------------------------- |
+| [`tmux`][tmux-segment] | Current session name; set `fetch_windows: true` for window list |
+
+Any other Oh My Posh segment (git, path, time, etc.) also works inside `status_left` and
+`status_right` blocks.
+
+[tmux]: https://github.com/tmux/tmux
+[block]: /docs/configuration/block
+[git]: /docs/segments/scm/git
+[path]: /docs/segments/system/path
+[tmux-segment]: /docs/segments/cli/tmux
diff --git a/website/docs/installation/prompt.mdx b/website/docs/installation/prompt.mdx
index 1c45821ff75f..83668cb2dba7 100644
--- a/website/docs/installation/prompt.mdx
+++ b/website/docs/installation/prompt.mdx
@@ -27,6 +27,7 @@ oh-my-posh get shell
{ label: 'fish', value: 'fish', },
{ label: 'nu', value: 'nu', },
{ label: 'powershell', value: 'powershell', },
+ { label: 'tmux', value: 'tmux', },
{ label: 'xonsh', value: 'xonsh', },
{ label: 'zsh', value: 'zsh', },
]
@@ -192,6 +193,47 @@ Once added, reload your profile for the changes to take effect.
. $PROFILE
```
+
+
+
+To use Oh My Posh as the tmux status bar renderer, add the following to your `~/.tmux.conf`:
+
+```bash
+set -g status-interval 5
+set -g status-left-length 400
+set -g status-right-length 200
+
+# OMP renders both sides; the window list lives inside status-left
+set -g status-left "#(oh-my-posh print tmux-left --config ~/.config/ohmyposh/config.yaml --shell tmux)"
+set -g status-right "#(oh-my-posh print tmux-right --config ~/.config/ohmyposh/config.yaml --shell tmux --pwd \"#{pane_current_path}\")"
+
+# Suppress tmux's own per-window format strings so OMP controls the window list
+set -g window-status-format ""
+set -g window-status-current-format ""
+
+# Refresh the status bar immediately on window changes
+set-hook -g window-linked "refresh-client -S"
+set-hook -g window-unlinked "refresh-client -S"
+set-hook -g window-renamed "refresh-client -S"
+set-hook -g client-session-changed "refresh-client -S"
+```
+
+Once added, reload your tmux config:
+
+```bash
+tmux source-file ~/.tmux.conf
+```
+
+:::tip
+`#{pane_current_path}` is expanded by tmux before running the command, so `--pwd` always
+reflects the active pane's current directory — useful for git and path segments in
+`status-right`.
+:::
+
+For the `tmux:` configuration block in your OMP config file, see [tmux integration][tmux].
+
+[tmux]: /docs/configuration/tmux
+
diff --git a/website/docs/segments/cli/tmux.mdx b/website/docs/segments/cli/tmux.mdx
new file mode 100644
index 000000000000..2f94cdb93dac
--- /dev/null
+++ b/website/docs/segments/cli/tmux.mdx
@@ -0,0 +1,75 @@
+---
+id: tmux
+title: Tmux
+sidebar_label: Tmux
+---
+
+## What
+
+Display the current [tmux][tmux] session name and optionally the window list.
+
+## Sample Configuration
+
+import Config from "@site/src/components/Config.js";
+
+
+
+With windows enabled:
+
+
+
+## Options
+
+| Name | Type | Default | Description |
+| --------------- | ------ | ------- | -------------------------------------------------- |
+| `fetch_windows` | `bool` | `false` | populate `.Windows` with the current window list |
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+ \ue7a2 {{ .SessionName }}{{ if .Windows }} | {{ range .Windows }}{{if .Active}}*{{end}}{{.Index}}:{{.Name}} {{end}}{{end}}
+```
+
+:::
+
+### Properties
+
+| Name | Type | Description |
+| -------------- | --------------- | ------------------------------------------------------------ |
+| `.SessionName` | `string` | the current tmux session name |
+| `.Windows` | `[]TmuxWindow` | list of windows; only populated when `fetch_windows` is true |
+
+### TmuxWindow
+
+| Name | Type | Description |
+| --------- | -------- | ---------------------------------------- |
+| `.Index` | `string` | the window index |
+| `.Name` | `string` | the window name |
+| `.Active` | `bool` | whether this is the currently active window |
+
+[tmux]: https://github.com/tmux/tmux
+[templates]: /docs/configuration/templates
diff --git a/website/sidebars.js b/website/sidebars.js
index 9d29a0ac3ca0..f994df1fbac0 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -38,6 +38,7 @@ export default {
"configuration/secondary-prompt",
"configuration/debug-prompt",
"configuration/transient",
+ "configuration/tmux",
"configuration/line-error",
"configuration/tooltips",
"configuration/sample",
@@ -81,6 +82,7 @@ export default {
"segments/cli/talosctl",
"segments/cli/tauri",
"segments/cli/terraform",
+ "segments/cli/tmux",
"segments/cli/ui5tooling",
"segments/cli/umbraco",
"segments/cli/unity",