feat(tmux): Adds ability to use oh-my-posh for the tmux statusline#7323
feat(tmux): Adds ability to use oh-my-posh for the tmux statusline#7323colings86 wants to merge 5 commits intoJanDeDobbeleer:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds tmux status bar integration to Oh My Posh, allowing users to render their entire tmux status bar using the same block/segment/style pipeline that powers the terminal prompt. This addresses the feature request in issue #475 where users wanted TMUX statusline integration similar to Powerline.
Changes:
- Adds two new segments:
tmux_session(displays current tmux session name) andtmux_window_list(renders all windows as a powerline-connected list) - Implements
tmux.status_leftandtmux.status_rightconfiguration sections in the config schema - Adds
oh-my-posh print tmux-leftandtmux-rightCLI commands with--shell tmuxsupport - Provides comprehensive documentation for setup and configuration
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/segments/tmux_session.go | New segment to display tmux session name with fallback to TMUX env var |
| src/segments/tmux_session_test.go | Comprehensive test coverage for tmux_session segment |
| src/segments/tmux_window_list.go | New segment rendering all windows with powerline separators in a single invocation |
| src/segments/tmux_window_list_test.go | Comprehensive test coverage including parsing, rendering, and edge cases |
| src/prompt/tmux.go | Rendering engine for tmux status sections using existing block pipeline |
| src/config/tmux.go | Configuration structure for tmux status sections |
| src/config/config.go | Config migration logic for tmux blocks |
| src/config/segment_types.go | Registration of new segment types and gob encoding |
| src/cli/print.go | CLI command support for tmux-left and tmux-right |
| src/shell/constants.go | Added TMUX shell constant |
| src/prompt/engine.go | Added TMUXLEFT and TMUXRIGHT prompt type constants |
| themes/schema.json | JSON schema definitions for new segments and tmux config |
| website/docs/configuration/tmux.mdx | Complete documentation for tmux integration setup |
| website/docs/segments/cli/tmux-session.mdx | Documentation for tmux_session segment |
| website/docs/segments/cli/tmux-window-list.mdx | Documentation for tmux_window_list segment |
| website/docs/installation/prompt.mdx | Installation instructions for tmux integration |
| website/docs/configuration/general.mdx | Reference to new tmux configuration option |
| website/sidebars.js | Navigation structure updates for new documentation |
| website/package-lock.json | Unrelated npm dependency metadata changes |
Files not reviewed (1)
- website/package-lock.json: Language not supported
| "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz", | ||
| "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", | ||
| "license": "MIT", |
There was a problem hiding this comment.
The package-lock.json file contains extensive changes that appear to be unrelated to the tmux feature being added (removal of "peer": true markers from various dependencies). These changes suggest that npm install or a similar command was run, possibly with a different npm version. Consider whether these changes should be included in this PR, or if they should be reverted to keep the PR focused solely on the tmux integration feature. If these changes are intentional (e.g., due to a dependency update requirement), they should be documented in the PR description.
There was a problem hiding this comment.
This is correct and should be adjusted
Adds two new print types (tmux-left, tmux-right) that render tmux status bar sections using the same block/segment/style pipeline as the terminal prompt. A new top-level `tmux:` config key holds status_left and status_right block definitions. New segments: - tmux_session: reads session name via `tmux display-message` or $TMUX fallback - tmux_window_list: enumerates all windows in one call and builds a complete powerline-connected ANSI string; avoids terminal.Write() to stay safe in concurrent segment goroutines
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 19 changed files in this pull request and generated 4 comments.
Files not reviewed (1)
- website/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)
src/config/config.go:289
- The
toggleSegments()function only processes segments incfg.Blocksbut doesn't process segments incfg.Tmux.StatusLeft.Blocksandcfg.Tmux.StatusRight.Blocks. While segment toggling might be an edge case for tmux status bars, this creates an inconsistency with the pattern established inmigrateSegmentProperties()which does process tmux blocks.
For consistency and completeness, consider adding similar iteration over tmux blocks here, following the same pattern as in migrateSegmentProperties().
// toggleSegments processes all segments in all blocks and adds segments
// with Toggled == true to the toggle cache, effectively toggling them off.
func (cfg *Config) toggleSegments() {
currentToggleSet, _ := cache.Get[map[string]bool](cache.Session, cache.TOGGLECACHE)
if currentToggleSet == nil {
currentToggleSet = make(map[string]bool)
}
for _, block := range cfg.Blocks {
for _, segment := range block.Segments {
if segment.Toggled {
segmentName := segment.Alias
if segmentName == "" {
segmentName = string(segment.Type)
}
currentToggleSet[segmentName] = true
}
}
}
// Update cache with the map directly
cache.Set(cache.Session, cache.TOGGLECACHE, currentToggleSet, cache.INFINITE)
}
src/config/segment_types.go
Outdated
| // TMUXSESSION writes the current tmux session name | ||
| TMUXSESSION SegmentType = "tmux_session" | ||
| // TMUXWINDOWLIST renders all tmux windows as a powerline list | ||
| TMUXWINDOWLIST SegmentType = "tmux_window_list" |
There was a problem hiding this comment.
The TMUXSESSION and TMUXWINDOWLIST constants are out of alphabetical order. They should be placed between TIME and TODOIST to maintain consistency with the rest of the constants in this section.
Move these constant declarations to appear after line 348 (TIME) and before line 349 (TODOIST).
src/config/segment_types.go
Outdated
| TMUXSESSION: func() SegmentWriter { return &segments.TmuxSession{} }, | ||
| TMUXWINDOWLIST: func() SegmentWriter { return &segments.TmuxWindowList{} }, |
There was a problem hiding this comment.
The TMUXSESSION and TMUXWINDOWLIST entries in the Segments map are out of alphabetical order. They should be placed between TIME and TODOIST to maintain consistency with the rest of the map entries.
Move these map entries to appear after line 478 (TIME) and before line 479 (TODOIST).
| 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() | ||
| } |
There was a problem hiding this comment.
The new tmux rendering functions TmuxStatusLeft() and TmuxStatusRight() in the prompt engine lack test coverage. While the individual segments (TmuxSession and TmuxWindowList) have comprehensive tests, the integration of these sections with the prompt rendering engine should also be tested.
Consider adding tests in a new file src/prompt/tmux_test.go to verify that:
- Empty/nil tmux config returns empty strings
- Blocks are correctly rendered via renderTmuxSection
- The output doesn't include shell integration sequences (as mentioned in the comment)
src/config/segment_types.go
Outdated
| gob.Register(&segments.TmuxSession{}) | ||
| gob.Register(&segments.TmuxWindowList{}) |
There was a problem hiding this comment.
The gob.Register calls for TmuxSession and TmuxWindowList are out of alphabetical order. They should be placed between Time and Todoist to maintain consistency with the rest of the file.
Move these lines to appear after line 132 (Time) and before line 133 (Todoist), maintaining alphabetical ordering by struct name.
Adds comprehensive documentation for Oh My Posh's tmux status bar integration, including configuration guide, installation instructions, and segment references. Includes new `tmux_session` and `tmux_window_list` segments, updates general configuration documentation, and reorganizes sidebar navigation.
Add tmux_session and tmux_window_list to the segment type enum, add conditional schema blocks for both segment types (with full options for tmux_window_list), and add a top-level tmux config property for status_left and status_right bar configuration.
766bac9 to
c7b01b6
Compare
website/sidebars.js
Outdated
| "segments/cli/tmux-session", | ||
| "segments/cli/tmux-window-list", | ||
| "segments/cli/tauri", | ||
| "segments/cli/terraform", |
There was a problem hiding this comment.
The tmux segment entries are not in alphabetical order. They should be placed after "tauri" (line 85), not between "talosctl" and "tauri". The correct alphabetical order should be: talosctl, tauri, terraform, tmux-session, tmux-window-list.
| "segments/cli/tmux-session", | |
| "segments/cli/tmux-window-list", | |
| "segments/cli/tauri", | |
| "segments/cli/terraform", | |
| "segments/cli/tauri", | |
| "segments/cli/terraform", | |
| "segments/cli/tmux-session", | |
| "segments/cli/tmux-window-list", |
src/segments/tmux_session.go
Outdated
| // Fallback: parse $TMUX which has the format "/tmp/tmux-NNN/socket,PID,sessionIndex". | ||
| // The session index is not the name, but it is better than nothing. |
There was a problem hiding this comment.
The comment indicates that the $TMUX variable contains a session index in the third field, but based on the tmux documentation, this field is actually a window index, not a session index. The $TMUX environment variable format is "socket_path,session_id,window_index" where session_id is a unique numeric identifier (not the session name) and window_index is the current window number. Using this as a fallback for the session name may not provide useful information to users. Consider either updating the comment to clarify this limitation or exploring alternative fallback approaches.
| // Fallback: parse $TMUX which has the format "/tmp/tmux-NNN/socket,PID,sessionIndex". | |
| // The session index is not the name, but it is better than nothing. | |
| // Fallback: parse $TMUX, which has the format "socket_path,session_id,window_index". | |
| // The third field is the window index, not the session name; we use it only as a last-resort identifier. |
src/prompt/tmux.go
Outdated
| // ansiToTmux converts ANSI SGR escape sequences to tmux format strings. | ||
| // Tmux strips ESC (0x1b) bytes from #(command) output in status bars, so raw | ||
| // ANSI sequences must be translated to tmux's native #[fg=...,bg=...] syntax. | ||
| func ansiToTmux(s string) string { | ||
| return ansiSGRRe.ReplaceAllStringFunc(s, func(match string) string { | ||
| sub := ansiSGRRe.FindStringSubmatch(match) | ||
| if len(sub) < 2 { | ||
| return "" | ||
| } | ||
| return sgrToTmuxAttr(sub[1]) | ||
| }) | ||
| } | ||
|
|
||
| // sgrToTmuxAttr converts a single SGR parameter string (the part between ESC[ and m) | ||
| // into a tmux #[...] style directive. | ||
| func sgrToTmuxAttr(params string) string { | ||
| if params == "" || params == "0" { | ||
| return "#[default]" | ||
| } | ||
|
|
||
| parts := strings.Split(params, ";") | ||
| var attrs []string | ||
|
|
||
| i := 0 | ||
| for i < len(parts) { | ||
| switch parts[i] { | ||
| case "0": | ||
| attrs = append(attrs, "default") | ||
| i++ | ||
| case "1": | ||
| attrs = append(attrs, "bold") | ||
| i++ | ||
| case "2": | ||
| attrs = append(attrs, "dim") | ||
| i++ | ||
| case "3": | ||
| attrs = append(attrs, "italics") | ||
| i++ | ||
| case "4": | ||
| attrs = append(attrs, "underscore") | ||
| i++ | ||
| case "5": | ||
| attrs = append(attrs, "blink") | ||
| i++ | ||
| case "7": | ||
| attrs = append(attrs, "reverse") | ||
| i++ | ||
| case "9": | ||
| attrs = append(attrs, "strikethrough") | ||
| i++ | ||
| case "22": | ||
| attrs = append(attrs, "nobold") | ||
| i++ | ||
| case "23": | ||
| attrs = append(attrs, "noitalics") | ||
| i++ | ||
| case "24": | ||
| attrs = append(attrs, "nounderscore") | ||
| i++ | ||
| case "25": | ||
| attrs = append(attrs, "noblink") | ||
| i++ | ||
| case "27": | ||
| attrs = append(attrs, "noreverse") | ||
| i++ | ||
| case "29": | ||
| attrs = append(attrs, "nostrikethrough") | ||
| i++ | ||
| case "39": | ||
| attrs = append(attrs, "fg=default") | ||
| i++ | ||
| case "49": | ||
| attrs = append(attrs, "bg=default") | ||
| i++ | ||
| case "53": | ||
| attrs = append(attrs, "overline") | ||
| i++ | ||
| case "55": | ||
| attrs = append(attrs, "nooverline") | ||
| i++ | ||
| case "38": | ||
| if i+4 < len(parts) && parts[i+1] == "2" { | ||
| // True color: 38;2;R;G;B | ||
| r, _ := strconv.ParseUint(parts[i+2], 10, 8) | ||
| g, _ := strconv.ParseUint(parts[i+3], 10, 8) | ||
| b, _ := strconv.ParseUint(parts[i+4], 10, 8) | ||
| attrs = append(attrs, fmt.Sprintf("fg=#%02x%02x%02x", r, g, b)) | ||
| i += 5 | ||
| } else if i+2 < len(parts) && parts[i+1] == "5" { | ||
| // 256-color: 38;5;N | ||
| attrs = append(attrs, "fg=colour"+parts[i+2]) | ||
| i += 3 | ||
| } else { | ||
| i++ | ||
| } | ||
| case "48": | ||
| if i+4 < len(parts) && parts[i+1] == "2" { | ||
| // True color: 48;2;R;G;B | ||
| r, _ := strconv.ParseUint(parts[i+2], 10, 8) | ||
| g, _ := strconv.ParseUint(parts[i+3], 10, 8) | ||
| b, _ := strconv.ParseUint(parts[i+4], 10, 8) | ||
| attrs = append(attrs, fmt.Sprintf("bg=#%02x%02x%02x", r, g, b)) | ||
| i += 5 | ||
| } else if i+2 < len(parts) && parts[i+1] == "5" { | ||
| // 256-color: 48;5;N | ||
| attrs = append(attrs, "bg=colour"+parts[i+2]) | ||
| i += 3 | ||
| } else { | ||
| i++ | ||
| } | ||
| default: | ||
| val, err := strconv.ParseUint(parts[i], 10, 64) | ||
| if err != nil { | ||
| i++ | ||
| continue | ||
| } | ||
| names16 := []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"} | ||
| switch { | ||
| case val >= 30 && val <= 37: | ||
| attrs = append(attrs, "fg="+names16[val-30]) | ||
| case val >= 40 && val <= 47: | ||
| attrs = append(attrs, "bg="+names16[val-40]) | ||
| case val >= 90 && val <= 97: | ||
| attrs = append(attrs, "fg=bright"+names16[val-90]) | ||
| case val >= 100 && val <= 107: | ||
| attrs = append(attrs, "bg=bright"+names16[val-100]) | ||
| } | ||
| i++ | ||
| } | ||
| } | ||
|
|
||
| if len(attrs) == 0 { | ||
| return "" | ||
| } | ||
| return "#[" + strings.Join(attrs, ",") + "]" | ||
| } |
There was a problem hiding this comment.
The complex ANSI to tmux format conversion logic in ansiToTmux and sgrToTmuxAttr functions lacks test coverage. This is critical functionality that translates ANSI escape sequences to tmux format strings, handling multiple color modes (16-color, 256-color, true color) and various text attributes. Without tests, regressions in this conversion logic could easily go unnoticed. Consider adding comprehensive unit tests covering various ANSI sequences including basic colors, 256-color mode, true color RGB values, and text attributes like bold, italic, and underline.
src/config/segment_types.go
Outdated
| gob.Register(&segments.Segment{}) | ||
| gob.Register(&segments.TmuxSession{}) | ||
| gob.Register(&segments.TmuxWindowList{}) |
There was a problem hiding this comment.
The gob.Register calls for TmuxSession and TmuxWindowList are placed after Segment{} at the end of the init function, breaking the alphabetical ordering pattern followed by all other segment registrations. These should be inserted in alphabetical order between TalosCTL and Todoist registrations to maintain consistency with the rest of the codebase.
| gob.Register(&segments.Segment{}) | |
| gob.Register(&segments.TmuxSession{}) | |
| gob.Register(&segments.TmuxWindowList{}) | |
| gob.Register(&segments.TmuxSession{}) | |
| gob.Register(&segments.TmuxWindowList{}) | |
| gob.Register(&segments.Segment{}) |
JanDeDobbeleer
left a comment
There was a problem hiding this comment.
Additionally, I'd rather have one TMUX segement that can do all of the things you want to TMUX.
src/config/config.go
Outdated
|
|
||
| for _, block := range cfg.Tmux.StatusLeft.Blocks { | ||
| for _, segment := range block.Segments { | ||
| segment.MigratePropertiesToOptions() |
There was a problem hiding this comment.
this is irrelevant as it's a new feature which starts after the migration.
src/config/config.go
Outdated
|
|
||
| for _, block := range cfg.Tmux.StatusRight.Blocks { | ||
| for _, segment := range block.Segments { | ||
| segment.MigratePropertiesToOptions() |
| } | ||
|
|
||
| // TmuxStatusSection holds a list of blocks to render for one tmux status section. | ||
| type TmuxStatusSection struct { |
There was a problem hiding this comment.
This is never used for custom functions or am I missing something?
There was a problem hiding this comment.
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
src/prompt/tmux.go
Outdated
| // ansiToTmux converts ANSI SGR escape sequences to tmux format strings. | ||
| // Tmux strips ESC (0x1b) bytes from #(command) output in status bars, so raw | ||
| // ANSI sequences must be translated to tmux's native #[fg=...,bg=...] syntax. | ||
| func ansiToTmux(s string) string { |
There was a problem hiding this comment.
This is an interesting hack, can't we "just" extend how we colorize instead?
src/segments/tmux_window_list.go
Outdated
| // renderWindows builds the full powerline-connected ANSI string for all windows. | ||
| // This method accesses terminal.Colors (read-only) and terminal.Plain but does NOT | ||
| // call terminal.Write(), keeping it safe to call from concurrent goroutines. | ||
| func (t *TmuxWindowList) renderWindows(windows []tmuxWindow, activeFG, activeBG, inactiveFG, inactiveBG color.Ansi, symbol string) string { |
There was a problem hiding this comment.
We never color directly from a segment but use color overrides instead so the actual ANSI translation (or TMUX specific coloring) happens from the engine.
|
This is the type of situation/setup where a daemon mode that @po1o proposed, would help, having a common, shared backend that handles tthe processing of the config andcrendering and some simple clients that just request a prompt could greatly improve eficiency and latency, multiple tmux panes and the status bar, all using the same shared backend. I think most of the latency comes from the process start and reading files, with the daemon, the cache can be in memory. |
|
@luisdavim you always need an executable to talk to the daemon. So that advantage is gone. With the streaming mode this can be achieved as well, provided the shell supports it. And here we'll probably conclude it doesn't (which also ruins deamon mode if none of this runs on an interval). |
|
Thanks for the review @JanDeDobbeleer. Will work on the changes over the next couple of days |
The client can be much simpler, doesn't need to load any configs or read any files, just make a request to get a prompt. |
… tmux segment Address PR JanDeDobbeleer#7323 review feedback from JanDeDobbeleer: - Replace separate tmux_session/tmux_window_list segments with a unified Tmux segment exposing SessionName and optionally Windows (fetch_windows option) - Drop direct ANSI building; template controls all rendering - Fix $TMUX fallback comment: third field is window index, not session index - Remove unused migration code for tmux blocks in migrateSegmentProperties - Update segment_types.go, schema.json, docs, and sidebars for new segment - Fix --pwd shell injection (single→double quotes) in tmux.conf examples - Revert unrelated package-lock.json changes
When rendering for --shell tmux, ANSI escape sequences are stripped by the tmux status bar format string parser. This change teaches the engine to emit tmux-native #[fg=...] / #[bg=...] tokens instead. - shell/formats.go: add ColorEscape, ColorReset, ColorBgReset, TransparentStart, TransparentEnd fields to Formats; add TMUX case returning #[%s] / #[default] / #[bg=default]; populate ANSI defaults for all other shells - terminal/writer.go: replace hardcoded colorise/transparentStart/End constants with formats fields; Init() now sets resetStyle.End and backgroundStyle.End from formats and switches knownStyles to tmux text-style tags (<b>→#[bold], <i>→#[italics], etc.) when shell is tmux - color/tmux.go: new TmuxColors decorator that wraps any String implementation and converts ANSI codes to tmux format tokens (38;2;R;G;B → fg=#rrggbb, 38;5;N → fg=colourN, 30 → fg=black, …) - color/tmux_test.go: unit tests for TmuxColors and convertAnsiToTmux - prompt/engine.go: wrap terminal.Colors with TmuxColors when shell is tmux Session name fallback: when tmux is not in PATH (common in the minimal /bin/sh environment used by status bar #(...) commands), display-message fails. Fall back to the tmux format alias #S so tmux expands it to the actual session name when rendering the status bar format string.
colings86
left a comment
There was a problem hiding this comment.
@JanDeDobbeleer Sorry, its taken me a few weeks to get back to this.
There are still some tests to finish adding I think but I wanted to get your thoughts on a couple of the changes here...
- I implemented the tmux colorise but I want to make sure I understood your intention and the implementation is in line with how you were thinking?
- I am doubting the tmux segment now as I have found that in the tmux statusline calls to
tmux display-message ...etc. do not work so I end up using raw tmux formatted templates (e.g.#Sfor session name). This means you can get the same behaviour with the text segment so I am considering removing the dedicated tmux segment and have this PR add tmux statusline support without a dedicated segment. WDYT?
| } | ||
|
|
||
| // TmuxStatusSection holds a list of blocks to render for one tmux status section. | ||
| type TmuxStatusSection struct { |
There was a problem hiding this comment.
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
Prerequisites
Description
This change adds functionality to allow the statusline in tmux to use oh-my-posh for rendering. This is similar to the claude statusline feature but in this case for tmux. There is a previous issue related to this where this kind of functionality was suggested: #475
The feature consists of adding the following pieces:
tmux.status-leftandtmux.status-rightconfiguration sections to enable configuration of blocks and segments to be rendered on the left and right tmux statusline positions (which can support most segment types)tmux_sessionsegment type to render the current tmux session name (could also be configured on terminal prompt and claude statusline if running in tmux)tmux_window_listsegment type to render the window list for the current tmux session (could also be configured on terminal prompt and claude statusline if running in tmux)Current State
This PR contains the functionality, tests and documentation for the new functionality and has been manually tested in tmux sessions. However, before spending more time on this PR I wanted to raise it as a draft to get some feedback on whether this project is interested in adding this functionality and on the approach taken. Happy to work on the PR to get it to a state where it can be merged if this seems in line with the desired project directions. Please let me know what you think.