pane_titles.go RawHistoryFolder

1package daemon
2
3import (
4	"strings"
5	"time"
6
7	"github.com/bnema/vev/internal/usecase/layout"
8)
9
10const paneTitleCacheTTL = time.Second
11
12// paneTitleState is guarded by pane.mu. generation changes whenever the
13// displayed title can change, allowing cached composition to damage only a
14// title row rather than the entire stacked layout.
15type paneTitleState struct {
16	processName      string
17	processNameAt    time.Time
18	processNameValid bool
19	terminalTitle    string
20	displayFallback  string
21	generation       uint64
22}
23
24func formatPaneTitle(processName, terminalTitle, fallback string) string {
25	if processName != "" && terminalTitle != "" {
26		prefix := processName + ": "
27		if strings.HasPrefix(terminalTitle, prefix) {
28			return terminalTitle
29		}
30		return prefix + terminalTitle
31	}
32	if processName != "" {
33		return processName
34	}
35	if terminalTitle != "" {
36		return terminalTitle
37	}
38	return fallback
39}
40
41func (p *pane) formattedTitleLocked(fallback string) string {
42	return formatPaneTitle(p.title.processName, p.title.terminalTitle, fallback)
43}
44
45func (p *pane) displayTitleLocked() string {
46	return p.formattedTitleLocked(p.title.displayFallback)
47}
48
49// setDisplayFallback updates the fallback owned by a pane. It must not damage
50// cached composition unless that fallback changes the displayed title.
51func (p *pane) setDisplayFallback(fallback string) {
52	p.mu.Lock()
53	if p.title.displayFallback == fallback {
54		p.mu.Unlock()
55		return
56	}
57	oldTitle := p.displayTitleLocked()
58	p.title.displayFallback = fallback
59	if oldTitle != p.displayTitleLocked() {
60		p.title.generation++
61	}
62	p.mu.Unlock()
63}
64
65// refreshTerminalTitleLocked synchronizes the title retained by the VT parser.
66// It must be called while p.mu is held, including by ptyReader around Write.
67func (p *pane) refreshTerminalTitleLocked() {
68	title := p.screen.TerminalTitle()
69	if p.title.terminalTitle != title {
70		p.title.terminalTitle = title
71		p.title.generation++
72	}
73}
74
75// composeTabTitle renders "name (paneTitle)". An empty paneTitle, or one equal
76// to the tab name, collapses to just the name.
77func composeTabTitle(tabName, paneTitle string) string {
78	return tabName + tabTitleDetail(tabName, paneTitle)
79}
80
81// tabTitleDetail returns the parenthesized " (paneTitle)" suffix for
82// tabName/paneTitle, or "" when paneTitle is empty or equals tabName. This is
83// the single place the name/detail composition rule lives; composeTabTitle
84// and the picker's TabEntry.Detail both build on it.
85func tabTitleDetail(tabName, paneTitle string) string {
86	if paneTitle == "" || paneTitle == tabName {
87		return ""
88	}
89	return " (" + paneTitle + ")"
90}
91
92// focusedPaneTitle returns the focused pane's display title for tab labels.
93// Caller must hold the owning session.mu (tb.mu then pane.mu obey lock order).
94// When includeTerminalTitle is false the OSC title is omitted; the process
95// name (or fallback) still shows.
96func (tb *tab) focusedPaneTitle(includeTerminalTitle bool) string {
97	tb.mu.Lock()
98	p := tb.focusedPane()
99	tb.mu.Unlock()
100	if p == nil {
101		return ""
102	}
103	p.mu.Lock()
104	defer p.mu.Unlock()
105	if includeTerminalTitle {
106		return p.displayTitleLocked()
107	}
108	return formatPaneTitle(p.title.processName, "", p.title.displayFallback)
109}
110
111// refreshSessionFocusedTitles opportunistically refreshes the process-name half
112// of every tab's focused pane. Snapshot panes under locks, then refresh with no
113// lock held (refreshPaneDisplayTitle re-enters p.mu itself). TTL-throttled.
114func (d *Daemon) refreshSessionFocusedTitles(sess *session) {
115	if sess == nil {
116		return
117	}
118	sess.mu.Lock()
119	panes := make([]*pane, 0, len(sess.tabs))
120	for _, tb := range sess.tabs {
121		tb.mu.Lock()
122		if p := tb.focusedPane(); p != nil {
123			panes = append(panes, p)
124		}
125		tb.mu.Unlock()
126	}
127	sess.mu.Unlock()
128	for _, p := range panes {
129		d.refreshPaneDisplayTitle(sess, p, false)
130	}
131}
132
133func (d *Daemon) refreshPaneTitle(sess *session, id layout.PaneID, owningTab ...*tab) string {
134	return d.refreshPaneTitleCached(sess, id, false, owningTab...)
135}
136
137func (d *Daemon) refreshPaneTitleOnFocus(sess *session, id layout.PaneID, owningTab ...*tab) string {
138	return d.refreshPaneTitleCached(sess, id, true, owningTab...)
139}
140
141// refreshPaneTitleCached resolves the pane in its owning tab. A caller with a
142// current tab supplies it explicitly; direct callers refresh every matching
143// tab because layout pane IDs are local to a tab and may repeat.
144func (d *Daemon) refreshPaneTitleCached(sess *session, id layout.PaneID, force bool, owningTab ...*tab) string {
145	if sess == nil {
146		return defaultShellTitle
147	}
148	tabs := owningTab
149	if len(tabs) == 0 || tabs[0] == nil {
150		sess.mu.Lock()
151		tabs = append([]*tab(nil), sess.tabs...)
152		sess.mu.Unlock()
153	}
154	title := ""
155	for _, tb := range tabs {
156		if tb == nil {
157			continue
158		}
159		tb.mu.Lock()
160		p := tb.panes[id]
161		tb.mu.Unlock()
162		if p == nil {
163			continue
164		}
165		title = d.refreshPaneDisplayTitle(sess, p, force)
166	}
167	return title
168}
169
170// refreshPaneDisplayTitle refreshes only the process-name portion of p's
171// display title. It deliberately releases pane.mu before querying the PTY so
172// callers that already render under pane locking cannot recurse on that lock.
173func (d *Daemon) refreshPaneDisplayTitle(_ *session, p *pane, force bool) string {
174	if p == nil {
175		return defaultShellTitle
176	}
177	now := d.clock.Now()
178	p.mu.Lock()
179	if !force && p.title.processNameValid && now.Sub(p.title.processNameAt) < paneTitleCacheTTL {
180		title := p.displayTitleLocked()
181		p.mu.Unlock()
182		return title
183	}
184	p.mu.Unlock()
185
186	// A failed lookup means no process name was discovered. Keep that state
187	// distinct from the display fallback: an OSC title must then stand alone,
188	// and future floating panes can supply their command fallback at format time.
189	processName := ""
190	if d.procComm != nil && p.pty != nil {
191		if pgid, err := p.pty.ForegroundPgid(); err == nil && pgid > 0 {
192			if comm, err := d.procComm(pgid); err == nil && strings.TrimSpace(comm) != "" {
193				processName = strings.TrimSpace(comm)
194			}
195		}
196	}
197
198	p.mu.Lock()
199	oldTitle := p.displayTitleLocked()
200	p.title.processName = processName
201	p.title.processNameAt = now
202	p.title.processNameValid = true
203	title := p.displayTitleLocked()
204	if oldTitle != title {
205		p.title.generation++
206	}
207	p.mu.Unlock()
208	return title
209}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close