Add --list-border for additional border around the list section

Close #4148
This commit is contained in:
Junegunn Choi
2024-12-31 17:03:18 +09:00
parent b8d2b0df7e
commit 9a2b7f559c
11 changed files with 620 additions and 329 deletions

View File

@@ -297,11 +297,16 @@ type Terminal struct {
listener net.Listener
listenUnsafe bool
borderShape tui.BorderShape
listBorderShape tui.BorderShape
listLabel labelPrinter
listLabelLen int
listLabelOpts labelOpts
cleanExit bool
executor *util.Executor
paused bool
border tui.Window
window tui.Window
wborder tui.Window
pborder tui.Window
pwindow tui.Window
borderWidth int
@@ -389,6 +394,7 @@ const (
reqReinit
reqFullRedraw
reqResize
reqRedrawListLabel
reqRedrawBorderLabel
reqRedrawPreviewLabel
reqClose
@@ -429,6 +435,7 @@ const (
actBackwardWord
actCancel
actChangeBorderLabel
actChangeListLabel
actChangeHeader
actChangeMulti
actChangePreviewLabel
@@ -489,6 +496,7 @@ const (
actTogglePreviewWrap
actTransform
actTransformBorderLabel
actTransformListLabel
actTransformHeader
actTransformPreviewLabel
actTransformPrompt
@@ -823,7 +831,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
listenAddr: opts.ListenAddr,
listenUnsafe: opts.Unsafe,
borderShape: opts.BorderShape,
listBorderShape: opts.ListBorderShape,
borderWidth: 1,
listLabel: nil,
listLabelOpts: opts.ListLabel,
borderLabel: nil,
borderLabelOpts: opts.BorderLabel,
previewLabel: nil,
@@ -880,10 +891,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
executing: util.NewAtomicBool(false),
lastAction: actStart,
lastFocus: minItem.Index()}
// This should be called before accessing tui.Color*
tui.InitTheme(opts.Theme, renderer.DefaultTheme(), opts.Black)
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
// Pre-calculated empty pointer and marker signs
t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
t.markerEmpty = strings.Repeat(" ", t.markerLen)
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(opts.ListLabel.label, &tui.ColListLabel, false)
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(opts.BorderLabel.label, &tui.ColBorderLabel, false)
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(opts.PreviewLabel.label, &tui.ColPreviewLabel, false)
if opts.Separator == nil || len(*opts.Separator) > 0 {
@@ -973,6 +989,7 @@ func (t *Terminal) environ() []string {
env = append(env, "FZF_PROMPT="+string(t.promptString))
env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
env = append(env, "FZF_LIST_LABEL="+t.listLabelOpts.label)
env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length()))
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
@@ -1114,7 +1131,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
// // unless the part has a non-default ANSI state
loc := whiteSuffix.FindStringIndex(trimmed)
if loc != nil {
blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1, nil}}
blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{tui.ColPrompt.Fg(), tui.ColPrompt.Bg(), tui.AttrClear, -1, nil}}
if item.colors != nil {
lastColor := (*item.colors)[len(*item.colors)-1]
if lastColor.offset[1] < int32(loc[1]) {
@@ -1546,6 +1563,9 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if t.window != nil {
t.window = nil
}
if t.wborder != nil {
t.wborder = nil
}
if t.pborder != nil {
t.pborder = nil
}
@@ -1572,10 +1592,10 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
offsets[1] -= 1 + bw
offsets[2] += 1 + bw
}
if t.border == nil && t.borderShape != tui.BorderNone {
if t.border == nil && t.borderShape.Visible() {
t.border = t.tui.NewWindow(
marginInt[0]+offsets[0], marginInt[3]+offsets[1], width+offsets[2], height+offsets[3],
false, tui.MakeBorderStyle(t.borderShape, t.unicode))
tui.WindowBase, tui.MakeBorderStyle(t.borderShape, t.unicode), true)
}
// Add padding to margin
@@ -1585,6 +1605,33 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
width -= paddingInt[1] + paddingInt[3]
height -= paddingInt[0] + paddingInt[2]
hasListBorder := t.listBorderShape.Visible()
innerWidth := width
innerHeight := height
innerMarginInt := marginInt
innerBorderFn := func(top int, left int, width int, height int) {
if hasListBorder {
t.wborder = t.tui.NewWindow(
top, left, width, height, tui.WindowList, tui.MakeBorderStyle(t.listBorderShape, t.unicode), false)
}
}
if hasListBorder {
if t.listBorderShape.HasTop() {
innerHeight--
innerMarginInt[0]++
}
if t.listBorderShape.HasBottom() {
innerHeight--
}
if t.listBorderShape.HasLeft() {
innerWidth -= 2
innerMarginInt[3] += 2
}
if t.listBorderShape.HasRight() {
innerWidth--
}
}
t.areaLines = height
t.areaColumns = width
@@ -1592,6 +1639,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
if forcePreview || t.needPreviewWindow() {
var resizePreviewWindows func(previewOpts *previewOpts)
stickToRight := false
resizePreviewWindows = func(previewOpts *previewOpts) {
t.activePreviewOpts = previewOpts
if previewOpts.size.size == 0 {
@@ -1602,7 +1650,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
pwidth := w
pheight := h
previewBorder := tui.MakeBorderStyle(previewOpts.border, t.unicode)
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
t.pborder = t.tui.NewWindow(y, x, w, h, tui.WindowPreview, previewBorder, false)
pwidth -= borderColumns(previewOpts.border, bw)
pheight -= borderLines(previewOpts.border)
if previewOpts.border.HasLeft() {
@@ -1617,7 +1665,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
pwidth = util.Max(0, pwidth)
pheight = util.Max(0, pheight)
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
if !hadPreviewWindow {
t.pwindow.Erase()
}
@@ -1647,16 +1695,25 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
return
}
if previewOpts.position == posUp {
innerBorderFn(marginInt[0]+pheight, marginInt[3], width, height-pheight)
t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
innerMarginInt[0]+pheight, innerMarginInt[3], innerWidth, innerHeight-pheight, tui.WindowList, noBorder, true)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
} else {
innerBorderFn(marginInt[0], marginInt[3], width, height-pheight)
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
innerMarginInt[0], innerMarginInt[3], innerWidth, innerHeight-pheight, tui.WindowList, noBorder, true)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
}
case posLeft, posRight:
pwidth := calculateSize(width, previewOpts.size, minWidth, minPreviewWidth)
minListWidth := minWidth
if t.listBorderShape.HasLeft() {
minListWidth += 2
}
if t.listBorderShape.HasRight() {
minListWidth++
}
pwidth := calculateSize(width, previewOpts.size, minListWidth, minPreviewWidth)
if hasThreshold && pwidth < previewOpts.threshold {
t.activePreviewOpts = previewOpts.alternative
if forcePreview {
@@ -1675,52 +1732,83 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
}
if previewOpts.position == posLeft {
// Put scrollbar closer to the right border for consistent look
if t.borderShape.HasRight() {
width++
if t.borderShape.HasRight() && !hasListBorder {
innerWidth++
}
// Add a 1-column margin between the preview window and the main window
m := 0
if !hasListBorder {
m = 1
}
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth+1, width-pwidth-1, height, false, noBorder)
innerMarginInt[0], innerMarginInt[3]+pwidth+m, innerWidth-pwidth-m, innerHeight, tui.WindowList, noBorder, true)
// Clear characters on the margin
// fzf --bind 'space:preview(seq 100)' --preview-window left,1
for y := 0; y < height; y++ {
t.window.Move(y, -1)
t.window.Print(" ")
if !hasListBorder {
for y := 0; y < innerHeight; y++ {
t.window.Move(y, -1)
t.window.Print(" ")
}
}
innerBorderFn(marginInt[0], marginInt[3]+pwidth, width-pwidth, height)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
} else {
// NOTE: fzf --preview 'cat {}' --preview-window border-left --border
if !previewOpts.border.HasRight() && t.borderShape.HasRight() {
stickToRight = !previewOpts.border.HasRight() && t.borderShape.HasRight()
if stickToRight {
innerWidth++
width++
}
innerBorderFn(marginInt[0], marginInt[3], width-pwidth, height)
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
innerMarginInt[0], innerMarginInt[3], innerWidth-pwidth, innerHeight, tui.WindowList, noBorder, true)
x := marginInt[3] + width - pwidth
createPreviewWindow(marginInt[0], x, pwidth, height)
}
}
}
resizePreviewWindows(&t.previewOpts)
if t.borderShape.HasRight() && !stickToRight {
// Need to clear the extra margin between the borders
// fzf --preview 'seq 1000' --preview-window border-left --bind space:change-preview-window:border-rounded --border vertical
// fzf --preview 'seq 1000' --preview-window up,hidden --bind space:toggle-preview --border vertical
y := 0
if t.borderShape.HasTop() {
y++
}
maxY := t.border.Height()
if t.borderShape.HasBottom() {
maxY--
}
for ; y < maxY; y++ {
t.border.Move(y, t.border.Width()-2)
t.border.Print(" ")
}
}
} else {
t.activePreviewOpts = &t.previewOpts
}
// Without preview window
if t.window == nil {
if t.borderShape.HasRight() {
if t.borderShape.HasRight() && !hasListBorder {
// Put scrollbar closer to the right border for consistent look
innerWidth++
width++
}
innerBorderFn(marginInt[0], marginInt[3], width, height)
t.window = t.tui.NewWindow(
marginInt[0],
marginInt[3],
width,
height, false, noBorder)
innerMarginInt[0],
innerMarginInt[3],
innerWidth,
innerHeight, tui.WindowList, noBorder, true)
}
// Print border label
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, false)
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.border, false)
}
@@ -1839,6 +1927,9 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
}
func (t *Terminal) printInfo() {
if t.window.Width() <= 1 {
return
}
pos := 0
line := t.promptLine()
maxHeight := t.window.Height()
@@ -1977,7 +2068,9 @@ func (t *Terminal) printInfo() {
} else {
outputPrinter(t.window, maxWidth-1)
}
t.window.Print(" ") // Margin
if fillLength >= 0 {
t.window.Print(" ") // Margin
}
return
}
@@ -2115,7 +2208,7 @@ func (t *Terminal) printList() {
func (t *Terminal) printBar(lineNum int, forceRedraw bool, barRange [2]int) bool {
hasBar := lineNum >= barRange[0] && lineNum < barRange[1]
if hasBar != t.prevLines[lineNum].hasBar || forceRedraw {
if (hasBar != t.prevLines[lineNum].hasBar || forceRedraw) && t.window.Width() > 0 {
t.move(lineNum, t.window.Width()-1, true)
if len(t.scrollbar) > 0 && hasBar {
t.window.CPrint(tui.ColScrollbar, t.scrollbar)
@@ -2212,11 +2305,18 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
}
if current {
preTask := func(marker markerClass) {
w := t.window.Width() - t.pointerLen
if w < 0 {
return
}
if len(label) == 0 {
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCurrentCursor, label)
}
if w-t.markerLen < 0 {
return
}
if selected {
t.window.CPrint(tui.ColCurrentMarker, markerFor(marker))
} else {
@@ -2226,11 +2326,18 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, forceRedraw, preTask, postTask)
} else {
preTask := func(marker markerClass) {
w := t.window.Width() - t.pointerLen
if w < 0 {
return
}
if len(label) == 0 {
t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCursor, label)
}
if w-t.markerLen < 0 {
return
}
if selected {
t.window.CPrint(tui.ColMarker, markerFor(marker))
} else {
@@ -2946,7 +3053,7 @@ func (t *Terminal) flush() {
t.placeCursor()
if !t.suppress {
windows := make([]tui.Window, 0, 4)
if t.borderShape != tui.BorderNone {
if t.borderShape.Visible() {
windows = append(windows, t.border)
}
if t.hasPreviewWindow() {
@@ -3855,6 +3962,8 @@ func (t *Terminal) Loop() error {
if t.hasPreviewer() {
t.previewBox.Set(reqPreviewReady, nil)
}
case reqRedrawListLabel:
t.printLabel(t.wborder, t.listLabel, t.listLabelOpts, t.listLabelLen, t.listBorderShape, true)
case reqRedrawBorderLabel:
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
case reqRedrawPreviewLabel:
@@ -3952,7 +4061,7 @@ func (t *Terminal) Loop() error {
previewDraggingPos := -1
barDragging := false
pbarDragging := false
pborderDragging := false
pborderDragging := -1
wasDown := false
needBarrier := true
@@ -4236,6 +4345,12 @@ func (t *Terminal) Loop() error {
} else {
req(reqHeader)
}
case actChangeListLabel:
t.listLabelOpts.label = a.a
if t.wborder != nil {
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(a.a, &tui.ColListLabel, false)
req(reqRedrawListLabel)
}
case actChangeBorderLabel:
t.borderLabelOpts.label = a.a
if t.border != nil {
@@ -4253,6 +4368,13 @@ func (t *Terminal) Loop() error {
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
return doActions(actions)
}
case actTransformListLabel:
label := t.executeCommand(a.a, false, true, true, true, "")
t.listLabelOpts.label = label
if t.wborder != nil {
t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false)
req(reqRedrawListLabel)
}
case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true, "")
t.borderLabelOpts.label = label
@@ -4684,7 +4806,7 @@ func (t *Terminal) Loop() error {
if !me.Down {
barDragging = false
pbarDragging = false
pborderDragging = false
pborderDragging = -1
previewDraggingPos = -1
}
@@ -4740,20 +4862,36 @@ func (t *Terminal) Loop() error {
}
// Preview border dragging (resizing)
if !pborderDragging && clicked && t.hasPreviewWindow() && t.pborder.Enclose(my, mx) {
if pborderDragging < 0 && clicked && t.hasPreviewWindow() {
switch t.activePreviewOpts.position {
case posUp:
pborderDragging = my == t.pborder.Top()+t.pborder.Height()-1
if t.pborder.Enclose(my, mx) && my == t.pborder.Top()+t.pborder.Height()-1 {
pborderDragging = 0
} else if t.listBorderShape.HasTop() && t.wborder.Enclose(my, mx) && my == t.wborder.Top() {
pborderDragging = 1
}
case posDown:
pborderDragging = my == t.pborder.Top()
if t.pborder.Enclose(my, mx) && my == t.pborder.Top() {
pborderDragging = 0
} else if t.listBorderShape.HasBottom() && t.wborder.Enclose(my, mx) && my == t.wborder.Top()+t.wborder.Height()-1 {
pborderDragging = 1
}
case posLeft:
pborderDragging = mx == t.pborder.Left()+t.pborder.Width()-1
if t.pborder.Enclose(my, mx) && mx == t.pborder.Left()+t.pborder.Width()-1 {
pborderDragging = 0
} else if t.listBorderShape.HasLeft() && t.wborder.Enclose(my, mx) && mx == t.wborder.Left() {
pborderDragging = 1
}
case posRight:
pborderDragging = mx == t.pborder.Left()
if t.pborder.Enclose(my, mx) && mx == t.pborder.Left() {
pborderDragging = 0
} else if t.listBorderShape.HasRight() && t.wborder.Enclose(my, mx) && mx == t.wborder.Left()+t.wborder.Width()-1 {
pborderDragging = 1
}
}
}
if pborderDragging {
if pborderDragging >= 0 {
var newSize int
var prevSize int
switch t.activePreviewOpts.position {
@@ -4774,6 +4912,7 @@ func (t *Terminal) Loop() error {
offset := mx - t.pborder.Left()
newSize = prevSize - offset
}
newSize -= pborderDragging
if newSize < 1 {
newSize = 1
}