mirror of
https://github.com/junegunn/fzf.git
synced 2026-08-12 10:52:39 +08:00
Separate floating pane border label from pane title
--border-label of a tmux floating pane was stored in the pane title and
read back by pane-border-format as '#{pane_title}', so any program in the
pane could replace the border text by setting the title. On Zellij the
name was only set when the pane was created. Either way the built-in
label actions could not touch it, as fzf draws no border of its own
there.
- tmux: hold the label in pane-scoped @fzf-border-label. Set the option
even without a label, so a later change-border-label has somewhere to
write. Drop select-pane -T; the pane title is left to the user
- Zellij: run 'zellij action rename-pane' on label change. 'zellij run'
has no option to set the environment of the new pane, so the command
exports the target itself
- change-border-label and transform-border-label now update the native
border, through the setter nativeLabelSetter picks by multiplexer
- The pane is named in a __FZF_INTERNAL_ variable that runProxy withholds
from the environment replay, so the same name in the outer environment
cannot redirect the update to another pane
This commit is contained in:
@@ -3,6 +3,9 @@ CHANGELOG
|
|||||||
|
|
||||||
0.74.2
|
0.74.2
|
||||||
------
|
------
|
||||||
|
- `change-border-label` and `transform-border-label` now update the label on the native border of a tmux or Zellij floating pane
|
||||||
|
- On tmux, the label no longer goes through the pane title, so a program running in the pane cannot replace it by setting the title
|
||||||
|
- Fixed a Kitty graphics sequence from a preview command being taken by tmux as a request to set the pane title; it is now passed through to the terminal
|
||||||
- Performance optimizations for short queries
|
- Performance optimizations for short queries
|
||||||
- The most critical case: fzf narrows results as you type, so short queries scan the largest candidate sets and the first keystroke scans the whole input.
|
- The most critical case: fzf narrows results as you type, so short queries scan the largest candidate sets and the first keystroke scans the whole input.
|
||||||
- Single-character queries are up to 2.4x faster
|
- Single-character queries are up to 2.4x faster
|
||||||
|
|||||||
+7
-3
@@ -426,9 +426,13 @@ On tmux 3.7 or above and on Zellij, the floating pane is not modal; you can
|
|||||||
switch to other panes and windows while fzf is running, and move and resize
|
switch to other panes and windows while fzf is running, and move and resize
|
||||||
the pane with the mouse. The native border of the pane is the handle for
|
the pane with the mouse. The native border of the pane is the handle for
|
||||||
moving and resizing it, so it is used by default and \fBborder\-native\fR is
|
moving and resizing it, so it is used by default and \fBborder\-native\fR is
|
||||||
implied. On tmux, \fB\-\-border\-label\fR is set as the title of the pane,
|
implied. \fB\-\-border\-label\fR is displayed on the native border, and
|
||||||
and is displayed on the border if \fBpane\-border\-status\fR is enabled in
|
\fBchange\-border\-label\fR and \fBtransform\-border\-label\fR update it
|
||||||
tmux (\fB\-\-border\-label\-pos\fR is ignored).
|
(\fB\-\-border\-label\-pos\fR is ignored). On tmux, fzf holds the label in
|
||||||
|
the \fB@fzf\-border\-label\fR option of the pane and sets its
|
||||||
|
\fBpane\-border\-format\fR to read it back, so the label is displayed if
|
||||||
|
\fBpane\-border\-status\fR is enabled in tmux. The title of the pane is left
|
||||||
|
alone. On Zellij, the label is the name of the pane.
|
||||||
|
|
||||||
fzf draws its own border instead when a border style is explicitly specified
|
fzf draws its own border instead when a border style is explicitly specified
|
||||||
with \fB\-\-border\fR, so that it is the only border shown. \fBnone\fR and
|
with \fB\-\-border\fR, so that it is the only border shown. \fBnone\fR and
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
@@ -19,10 +20,75 @@ import (
|
|||||||
|
|
||||||
const becomeSuffix = ".become"
|
const becomeSuffix = ".become"
|
||||||
|
|
||||||
|
// Withheld from the environment replay below, so an outer value cannot
|
||||||
|
// override what the floating pane set
|
||||||
|
const internalEnvPrefix = "__FZF_INTERNAL_"
|
||||||
|
|
||||||
func escapeSingleQuote(str string) string {
|
func escapeSingleQuote(str string) string {
|
||||||
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
|
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read and remove, so preview and execute commands do not inherit it
|
||||||
|
func takeEnv(name string) string {
|
||||||
|
value := os.Getenv(name)
|
||||||
|
os.Unsetenv(name)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies label updates off the event loop, which would block on the fork
|
||||||
|
type labelUpdater struct {
|
||||||
|
set func(string)
|
||||||
|
mu sync.Mutex
|
||||||
|
pending *string
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// A queue of one: a burst collapses to the newest label. A goroutine per
|
||||||
|
// update could apply them out of order
|
||||||
|
func (u *labelUpdater) update(label string) {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
u.pending = &label
|
||||||
|
if u.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.running = true
|
||||||
|
go u.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ends once caught up, so nothing needs cancelling when Run returns
|
||||||
|
func (u *labelUpdater) run() {
|
||||||
|
for {
|
||||||
|
u.mu.Lock()
|
||||||
|
label := u.pending
|
||||||
|
if label == nil {
|
||||||
|
u.running = false
|
||||||
|
u.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.pending = nil
|
||||||
|
u.mu.Unlock()
|
||||||
|
u.set(*label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the label on the native border, or nil when it is not fzf's to update
|
||||||
|
func nativeLabelSetter() func(string) {
|
||||||
|
// Read both, so neither is left behind
|
||||||
|
tmuxPane, zellijPane := takeEnv(tmuxBorderLabelEnv), takeEnv(zellijBorderLabelEnv)
|
||||||
|
var set func(string)
|
||||||
|
switch {
|
||||||
|
case tmuxPane != "":
|
||||||
|
set = func(label string) { setTmuxBorderLabel(tmuxPane, label) }
|
||||||
|
case zellijPane != "":
|
||||||
|
set = func(label string) { setZellijBorderLabel(zellijPane, label) }
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
updater := &labelUpdater{set: set}
|
||||||
|
return updater.update
|
||||||
|
}
|
||||||
|
|
||||||
func popupArgStr(args []string, opts *Options) (string, string) {
|
func popupArgStr(args []string, opts *Options) (string, string) {
|
||||||
fzf, rest := args[0], args[1:]
|
fzf, rest := args[0], args[1:]
|
||||||
args = []string{"--bind=ctrl-z:ignore"}
|
args = []string{"--bind=ctrl-z:ignore"}
|
||||||
@@ -125,6 +191,9 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
|||||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||||
for _, pairStr := range os.Environ() {
|
for _, pairStr := range os.Environ() {
|
||||||
pair := strings.SplitN(pairStr, "=", 2)
|
pair := strings.SplitN(pairStr, "=", 2)
|
||||||
|
if strings.HasPrefix(pair[0], internalEnvPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if validIdentifier.MatchString(pair[0]) {
|
if validIdentifier.MatchString(pair[0]) {
|
||||||
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
|
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
|
||||||
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
|
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package fzf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLabelUpdater(t *testing.T) {
|
||||||
|
var mu sync.Mutex
|
||||||
|
applied := []int{}
|
||||||
|
// The goroutine can still be running after the test function returns, so
|
||||||
|
// it records the label instead of reporting it, and -1 stands in for one
|
||||||
|
// that could not be parsed
|
||||||
|
updater := &labelUpdater{set: func(label string) {
|
||||||
|
n, err := strconv.Atoi(label)
|
||||||
|
if err != nil {
|
||||||
|
n = -1
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
applied = append(applied, n)
|
||||||
|
mu.Unlock()
|
||||||
|
// Long enough for the burst to pile up behind the first update
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}}
|
||||||
|
|
||||||
|
const last = 99
|
||||||
|
for i := 0; i <= last; i++ {
|
||||||
|
updater.update(strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// However many updates are dropped, the final one is always applied
|
||||||
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
|
for {
|
||||||
|
mu.Lock()
|
||||||
|
done := len(applied) > 0 && applied[len(applied)-1] == last
|
||||||
|
mu.Unlock()
|
||||||
|
if done {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
t.Fatalf("last update was not applied: %v", applied)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
// Updates are never applied out of order, so the border cannot be left
|
||||||
|
// showing a label older than the last one requested
|
||||||
|
for i, n := range applied {
|
||||||
|
if i > 0 && n <= applied[i-1] {
|
||||||
|
t.Errorf("applied out of order: %v", applied)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A burst collapses instead of running one command per update. The
|
||||||
|
// updates are queued in a tight loop while each one takes a millisecond
|
||||||
|
// to apply, so all of them landing would mean no coalescing at all
|
||||||
|
if len(applied) >= last+1 {
|
||||||
|
t.Errorf("expected updates to be coalesced, applied all %d", len(applied))
|
||||||
|
}
|
||||||
|
t.Logf("applied %d of %d updates", len(applied), last+1)
|
||||||
|
}
|
||||||
@@ -470,6 +470,7 @@ type Terminal struct {
|
|||||||
clickFooterLine int
|
clickFooterLine int
|
||||||
clickFooterColumn int
|
clickFooterColumn int
|
||||||
proxyScript string
|
proxyScript string
|
||||||
|
setNativeLabel func(string)
|
||||||
numLinesCache map[int32]numLinesCacheValue
|
numLinesCache map[int32]numLinesCacheValue
|
||||||
raw bool
|
raw bool
|
||||||
lastActivity time.Time
|
lastActivity time.Time
|
||||||
@@ -1142,6 +1143,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
printer: opts.Printer,
|
printer: opts.Printer,
|
||||||
printsep: opts.PrintSep,
|
printsep: opts.PrintSep,
|
||||||
proxyScript: opts.ProxyScript,
|
proxyScript: opts.ProxyScript,
|
||||||
|
setNativeLabel: nativeLabelSetter(),
|
||||||
merger: em,
|
merger: em,
|
||||||
passMerger: em,
|
passMerger: em,
|
||||||
resultMerger: em,
|
resultMerger: em,
|
||||||
@@ -7272,6 +7274,10 @@ func (t *Terminal) Loop() error {
|
|||||||
if t.border != nil {
|
if t.border != nil {
|
||||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
||||||
req(reqRedrawBorderLabel)
|
req(reqRedrawBorderLabel)
|
||||||
|
} else if t.setNativeLabel != nil {
|
||||||
|
// fzf draws no border of its own; the label is on the
|
||||||
|
// native border of the floating pane
|
||||||
|
t.setNativeLabel(label)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
case actChangePreviewLabel, actTransformPreviewLabel, actBgTransformPreviewLabel:
|
case actChangePreviewLabel, actTransformPreviewLabel, actBgTransformPreviewLabel:
|
||||||
|
|||||||
+44
-33
@@ -55,12 +55,26 @@ func escapeTmuxSeparator(str string) string {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape a string for use as the pane title; select-pane -T expands format
|
// '#[...]' is still processed when a substituted value is drawn, so '#' is
|
||||||
// expressions denoted by '#', but not time conversion specifiers
|
// doubled. '#{...}' is already literal
|
||||||
func escapeTmuxTitle(str string) string {
|
func escapeTmuxFormat(str string) string {
|
||||||
return escapeTmuxSeparator(strings.ReplaceAll(str, "#", "##"))
|
return escapeTmuxSeparator(strings.ReplaceAll(str, "#", "##"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The option pane-border-format reads back, and the pane it is set on
|
||||||
|
const (
|
||||||
|
tmuxBorderLabelOption = "@fzf-border-label"
|
||||||
|
tmuxBorderLabelEnv = internalEnvPrefix + "TMUX_LABEL_PANE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errors are ignored. The pane may be gone
|
||||||
|
func setTmuxBorderLabel(pane string, label string) {
|
||||||
|
// Strip ANSI sequences. tmux would draw the escape's remainder as text
|
||||||
|
label, _, _ = extractColor(label, nil, nil)
|
||||||
|
exec.Command("tmux", "set-option", "-p", "-t", pane,
|
||||||
|
tmuxBorderLabelOption, escapeTmuxFormat(label)).Run()
|
||||||
|
}
|
||||||
|
|
||||||
// Convert sizeSpec to the number of cells, clamped between the minimum
|
// Convert sizeSpec to the number of cells, clamped between the minimum
|
||||||
// footprint of 3, including the border, and the window size
|
// footprint of 3, including the border, and the window size
|
||||||
func tmuxDim(spec sizeSpec, window int) int {
|
func tmuxDim(spec sizeSpec, window int) int {
|
||||||
@@ -128,35 +142,28 @@ func runTmuxFloatingPane(argStr string, dir string, windowWidth int, windowHeigh
|
|||||||
// remain-on-exit is on.
|
// remain-on-exit is on.
|
||||||
setup := `tmux set-option -p -t "$TMUX_PANE" remain-on-exit off 2> /dev/null; `
|
setup := `tmux set-option -p -t "$TMUX_PANE" remain-on-exit off 2> /dev/null; `
|
||||||
|
|
||||||
// Set --border-label as the title of the floating pane, and as its
|
// --border-label lives in a pane-scoped user option that
|
||||||
// pane-border-format so that it is displayed on the border when
|
// pane-border-format reads back, so a program in the pane cannot
|
||||||
// pane-border-status is enabled. Without a label, the border text
|
// replace it by setting the title. Set even when empty, so
|
||||||
// is cleared so that the default pane status content (e.g. the
|
// change-border-label can fill it in and the default pane status is
|
||||||
// pane title) is not shown. pane-border-format is pane-scoped,
|
// not shown. pane-border-status is a window option until the next
|
||||||
// but pane-border-status is a window option that only becomes
|
// tmux release, so it is left alone.
|
||||||
// pane-scoped in the next release of tmux, so it is left alone.
|
|
||||||
// https://github.com/tmux/tmux/commit/7a18fa281db3
|
// https://github.com/tmux/tmux/commit/7a18fa281db3
|
||||||
// --border-label-pos is ignored.
|
// --border-label-pos is ignored. With an explicit --border fzf draws
|
||||||
// The label is left to fzf when it draws its own border with the
|
// the label itself, but the native border cannot be removed.
|
||||||
// label on it. '--border=none' is not the case; fzf would not
|
ownsLabel := noBorderSpecified(opts)
|
||||||
// display the label, but the native border of a floating pane
|
label := ""
|
||||||
// cannot be removed, so display the label on it nonetheless.
|
if ownsLabel {
|
||||||
format := ""
|
|
||||||
if opts.BorderLabel.label != "" &&
|
|
||||||
(opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine ||
|
|
||||||
opts.BorderShape == tui.BorderNone) {
|
|
||||||
// Strip ANSI sequences fzf would otherwise render itself
|
// Strip ANSI sequences fzf would otherwise render itself
|
||||||
label, _, _ := extractColor(opts.BorderLabel.label, nil, nil)
|
label, _, _ = extractColor(opts.BorderLabel.label, nil, nil)
|
||||||
if label != "" {
|
|
||||||
setup += fmt.Sprintf(`tmux select-pane -t "$TMUX_PANE" -T %s 2> /dev/null; `,
|
|
||||||
escapeSingleQuote(escapeTmuxTitle(label)))
|
|
||||||
// The title is displayed verbatim; substituted values are
|
|
||||||
// not expanded again
|
|
||||||
format = "#{pane_title}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setup += fmt.Sprintf(`tmux set-option -p -t "$TMUX_PANE" %s %s 2> /dev/null; `,
|
||||||
|
tmuxBorderLabelOption, escapeSingleQuote(escapeTmuxFormat(label)))
|
||||||
setup += fmt.Sprintf(`tmux set-option -p -t "$TMUX_PANE" pane-border-format %s 2> /dev/null; `,
|
setup += fmt.Sprintf(`tmux set-option -p -t "$TMUX_PANE" pane-border-format %s 2> /dev/null; `,
|
||||||
escapeSingleQuote(format))
|
escapeSingleQuote("#{"+tmuxBorderLabelOption+"}"))
|
||||||
|
if ownsLabel {
|
||||||
|
setup += fmt.Sprintf(`export %s="$TMUX_PANE"; `, tmuxBorderLabelEnv)
|
||||||
|
}
|
||||||
paneCmd := fmt.Sprintf("%s%s %s; echo $? > %s; tmux wait-for -S %s",
|
paneCmd := fmt.Sprintf("%s%s %s; echo $? > %s; tmux wait-for -S %s",
|
||||||
setup, escapeSingleQuote(sh), escapeSingleQuote(temp), code, signal)
|
setup, escapeSingleQuote(sh), escapeSingleQuote(temp), code, signal)
|
||||||
// Unzoom the window first; creating a floating pane over a zoomed
|
// Unzoom the window first; creating a floating pane over a zoomed
|
||||||
@@ -193,16 +200,20 @@ exit "$code"`, newPane, code, signal, signal, code, code, code)
|
|||||||
}, opts, true)
|
}, opts, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether no box border was asked for. 'none' and 'line' count as none.
|
||||||
|
// fzf draws no box for either
|
||||||
|
func noBorderSpecified(opts *Options) bool {
|
||||||
|
return opts.BorderShape == tui.BorderUndefined ||
|
||||||
|
opts.BorderShape == tui.BorderLine || opts.BorderShape == tui.BorderNone
|
||||||
|
}
|
||||||
|
|
||||||
// Whether to use the multiplexer's native border for the floating pane. Its
|
// Whether to use the multiplexer's native border for the floating pane. Its
|
||||||
// native border is the handle that makes the pane movable and resizable with
|
// native border is the handle that makes the pane movable and resizable with
|
||||||
// the mouse, so it is the default; 'border-native' forces it. It is not used
|
// the mouse, so it is the default; 'border-native' forces it. It is not used
|
||||||
// when a border style is explicitly specified with --border, so that the
|
// when a border style is explicitly specified with --border, so that the
|
||||||
// fzf-drawn border is the only one shown. 'none' and 'line' are treated as no
|
// fzf-drawn border is the only one shown.
|
||||||
// border; fzf draws no box for either, and 'line' only makes sense with
|
|
||||||
// --height.
|
|
||||||
func nativeBorder(opts *Options) bool {
|
func nativeBorder(opts *Options) bool {
|
||||||
return opts.Tmux.border || opts.BorderShape == tui.BorderUndefined ||
|
return opts.Tmux.border || noBorderSpecified(opts)
|
||||||
opts.BorderShape == tui.BorderLine || opts.BorderShape == tui.BorderNone
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTmux(args []string, opts *Options) (int, error) {
|
func runTmux(args []string, opts *Options) (int, error) {
|
||||||
|
|||||||
+18
-10
@@ -2,37 +2,45 @@ package fzf
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestEscapeTmuxTitle(t *testing.T) {
|
func TestEscapeTmuxFormat(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
given string
|
given string
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{"", ""},
|
{"", ""},
|
||||||
{" fzf ", " fzf "},
|
{" fzf ", " fzf "},
|
||||||
{"#", "##"},
|
|
||||||
{"##", "####"},
|
|
||||||
{" C# notes #S ", " C## notes ##S "},
|
|
||||||
{"100%", "100%"},
|
{"100%", "100%"},
|
||||||
|
// '#[' would be taken as a style directive when the label is drawn
|
||||||
|
{"#[fg=red]x", "##[fg=red]x"},
|
||||||
|
// '#{' is already literal in a substituted value, but doubling it
|
||||||
|
// keeps a label that fzf renders itself looking the same
|
||||||
|
{"#{pane_id}", "##{pane_id}"},
|
||||||
|
{" C# notes #S ", " C## notes ##S "},
|
||||||
{";", `\;`},
|
{";", `\;`},
|
||||||
{"; rm", "; rm"},
|
{"; rm", "; rm"},
|
||||||
{" ; ", " ; "},
|
|
||||||
} {
|
} {
|
||||||
if actual := escapeTmuxTitle(tc.given); actual != tc.expected {
|
if actual := escapeTmuxFormat(tc.given); actual != tc.expected {
|
||||||
t.Errorf("expected %q, got %q", tc.expected, actual)
|
t.Errorf("expected %q, got %q", tc.expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeTmuxTitleSeparator(t *testing.T) {
|
func TestEscapeTmuxSeparator(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
given string
|
given string
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{"#;", "##;"},
|
{"", ""},
|
||||||
{";#", ";##"},
|
{" fzf ", " fzf "},
|
||||||
|
{"#", "#"},
|
||||||
|
{"#{pane_id}", "#{pane_id}"},
|
||||||
|
{"100%", "100%"},
|
||||||
|
{";", `\;`},
|
||||||
|
{"; rm", "; rm"},
|
||||||
|
{" ; ", " ; "},
|
||||||
{";;", ";;"},
|
{";;", ";;"},
|
||||||
} {
|
} {
|
||||||
if actual := escapeTmuxTitle(tc.given); actual != tc.expected {
|
if actual := escapeTmuxSeparator(tc.given); actual != tc.expected {
|
||||||
t.Errorf("expected %q, got %q", tc.expected, actual)
|
t.Errorf("expected %q, got %q", tc.expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-5
@@ -1,11 +1,20 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/tui"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const zellijBorderLabelEnv = internalEnvPrefix + "ZELLIJ_LABEL_PANE"
|
||||||
|
|
||||||
|
// Errors are ignored. The pane may be gone
|
||||||
|
func setZellijBorderLabel(pane string, label string) {
|
||||||
|
// Strip ANSI sequences fzf would otherwise render itself
|
||||||
|
label, _, _ = extractColor(label, nil, nil)
|
||||||
|
// '--' so that a label starting with a hyphen is not parsed as a flag
|
||||||
|
exec.Command("zellij", "action", "rename-pane", "-p", pane, "--", label).Run()
|
||||||
|
}
|
||||||
|
|
||||||
func runZellij(args []string, opts *Options) (int, error) {
|
func runZellij(args []string, opts *Options) (int, error) {
|
||||||
// Use the native Zellij border by default, consistent with tmux, so that
|
// Use the native Zellij border by default, consistent with tmux, so that
|
||||||
// the pane can be moved and resized with the mouse. Set before
|
// the pane can be moved and resized with the mouse. Set before
|
||||||
@@ -20,6 +29,7 @@ func runZellij(args []string, opts *Options) (int, error) {
|
|||||||
"run", "--floating", "--close-on-exit", "--block-until-exit",
|
"run", "--floating", "--close-on-exit", "--block-until-exit",
|
||||||
"--cwd", dir,
|
"--cwd", dir,
|
||||||
}
|
}
|
||||||
|
ownsLabel := false
|
||||||
if !opts.Tmux.border {
|
if !opts.Tmux.border {
|
||||||
zellijArgs = append(zellijArgs, "--borderless", "true")
|
zellijArgs = append(zellijArgs, "--borderless", "true")
|
||||||
} else {
|
} else {
|
||||||
@@ -33,8 +43,8 @@ func runZellij(args []string, opts *Options) (int, error) {
|
|||||||
// stripping ANSI sequences fzf would otherwise render itself.
|
// stripping ANSI sequences fzf would otherwise render itself.
|
||||||
// --border-label-pos is ignored.
|
// --border-label-pos is ignored.
|
||||||
label := ""
|
label := ""
|
||||||
if opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine ||
|
ownsLabel = noBorderSpecified(opts)
|
||||||
opts.BorderShape == tui.BorderNone {
|
if ownsLabel {
|
||||||
label, _, _ = extractColor(opts.BorderLabel.label, nil, nil)
|
label, _, _ = extractColor(opts.BorderLabel.label, nil, nil)
|
||||||
}
|
}
|
||||||
zellijArgs = append(zellijArgs, "--name="+label)
|
zellijArgs = append(zellijArgs, "--name="+label)
|
||||||
@@ -60,7 +70,14 @@ func runZellij(args []string, opts *Options) (int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
zellijArgs = append(zellijArgs, sh, temp)
|
if ownsLabel {
|
||||||
|
// 'zellij run' cannot set the new pane's environment, so the
|
||||||
|
// command exports it itself. exec avoids a lingering shell
|
||||||
|
zellijArgs = append(zellijArgs, sh, "-c", fmt.Sprintf(`export %s="$ZELLIJ_PANE_ID"; exec %s %s`,
|
||||||
|
zellijBorderLabelEnv, escapeSingleQuote(sh), escapeSingleQuote(temp)))
|
||||||
|
} else {
|
||||||
|
zellijArgs = append(zellijArgs, sh, temp)
|
||||||
|
}
|
||||||
return exec.Command("zellij", zellijArgs...), nil
|
return exec.Command("zellij", zellijArgs...), nil
|
||||||
}, opts, true)
|
}, opts, true)
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-6
@@ -17,9 +17,11 @@ class TestTmux < TestInteractive
|
|||||||
def test_floating_pane
|
def test_floating_pane
|
||||||
tmux.send_keys "seq 100 | #{fzf('--popup center,80% --margin 0')}", :Enter
|
tmux.send_keys "seq 100 | #{fzf('--popup center,80% --margin 0')}", :Enter
|
||||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
# Border text is cleared when no label is given
|
# Border text is empty when no label is given
|
||||||
format = IO.popen(['tmux', 'show-options', '-p', '-t', floating_pane, 'pane-border-format'], &:read)
|
pane = floating_pane
|
||||||
assert_includes format, "''"
|
refute_nil pane
|
||||||
|
assert_equal "\#{@fzf-border-label}", pane_option(pane, 'pane-border-format')
|
||||||
|
assert_equal '', pane_option(pane, '@fzf-border-label')
|
||||||
tmux.send_keys '99'
|
tmux.send_keys '99'
|
||||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
@@ -40,10 +42,41 @@ class TestTmux < TestInteractive
|
|||||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
pane = floating_pane
|
pane = floating_pane
|
||||||
refute_nil pane
|
refute_nil pane
|
||||||
|
# The label is held in a user option, not in the pane title, which is
|
||||||
|
# left to the user. '#' is doubled so that the label is drawn as it is
|
||||||
|
# written, instead of '#[...]' being taken as a style directive
|
||||||
|
assert_equal ' ##fzf-label 100% ', pane_option(pane, '@fzf-border-label')
|
||||||
|
assert_equal "\#{@fzf-border-label}", pane_option(pane, 'pane-border-format')
|
||||||
title = IO.popen(['tmux', 'display-message', '-p', '-t', pane, "\#{pane_title}"], &:read)
|
title = IO.popen(['tmux', 'display-message', '-p', '-t', pane, "\#{pane_title}"], &:read)
|
||||||
assert_equal ' #fzf-label 100% ', title.chomp
|
refute_equal ' #fzf-label 100% ', title.chomp
|
||||||
format = IO.popen(['tmux', 'show-options', '-p', '-t', pane, 'pane-border-format'], &:read)
|
tmux.send_keys :Enter
|
||||||
assert_includes format, "\#{pane_title}"
|
assert_equal '1', fzf_output
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_floating_pane_change_border_label
|
||||||
|
tmux.send_keys "seq 100 | #{fzf(%(--popup center,80% --margin 0 --bind 'space:change-border-label( #[fg=red] 100% )'))}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
pane = floating_pane
|
||||||
|
refute_nil pane
|
||||||
|
assert_equal '', pane_option(pane, '@fzf-border-label')
|
||||||
|
tmux.send_keys :Space
|
||||||
|
wait { assert_equal ' ##[fg=red] 100% ', pane_option(pane, '@fzf-border-label') }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
assert_equal '1', fzf_output
|
||||||
|
end
|
||||||
|
|
||||||
|
# A program running in the pane owns the pane title, but not the label
|
||||||
|
def test_floating_pane_border_label_not_affected_by_title
|
||||||
|
tmux.send_keys "seq 100 | #{fzf(%(--popup center,80% --margin 0 --border-label ' label ' --bind 'space:execute-silent(printf "\\033]2;hijacked\\033\\\\" > /dev/tty)'))}", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
pane = floating_pane
|
||||||
|
refute_nil pane
|
||||||
|
tmux.send_keys :Space
|
||||||
|
wait do
|
||||||
|
title = IO.popen(['tmux', 'display-message', '-p', '-t', pane, "\#{pane_title}"], &:read)
|
||||||
|
assert_equal 'hijacked', title.chomp
|
||||||
|
end
|
||||||
|
assert_equal ' label ', pane_option(pane, '@fzf-border-label')
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
assert_equal '1', fzf_output
|
assert_equal '1', fzf_output
|
||||||
end
|
end
|
||||||
@@ -79,6 +112,12 @@ class TestTmux < TestInteractive
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# stderr is merged in so that an unset option, which prints 'invalid
|
||||||
|
# option' to stderr and nothing to stdout, is not read as an empty value
|
||||||
|
def pane_option(pane, name)
|
||||||
|
IO.popen(['tmux', 'show-options', '-p', '-t', pane, '-v', name], err: %i[child out], &:read).chomp
|
||||||
|
end
|
||||||
|
|
||||||
def floating_pane
|
def floating_pane
|
||||||
format = "\#{pane_id} \#{pane_floating_flag}"
|
format = "\#{pane_id} \#{pane_floating_flag}"
|
||||||
lines = IO.popen(['tmux', 'list-panes', '-t', tmux.win, '-F', format]) { |io| io.readlines(chomp: true) }
|
lines = IO.popen(['tmux', 'list-panes', '-t', tmux.win, '-F', format]) { |io| io.readlines(chomp: true) }
|
||||||
|
|||||||
Reference in New Issue
Block a user