mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-07-03 11:12:30 +08:00
Cleanup previous PR
Various fixes and improvements to the command palette kitten Fixes #9585
This commit is contained in:
committed by
Kovid Goyal
parent
813f8ba7cf
commit
347c829156
@@ -50,18 +50,44 @@ type displayLine struct {
|
||||
itemIdx int // index into filtered_idx, -1 for headers
|
||||
}
|
||||
|
||||
const maxKeyDisplayWidth = 30
|
||||
|
||||
// truncateToWidth truncates s to fit within maxWidth cells, appending "..." if
|
||||
// truncated and maxWidth > 3. When maxWidth <= 3, the string is simply trimmed
|
||||
// to fit without appending ellipsis (no room for it).
|
||||
func truncateToWidth(s string, maxWidth int) string {
|
||||
if wcswidth.Stringwidth(s) <= maxWidth {
|
||||
return s
|
||||
}
|
||||
runes := []rune(s)
|
||||
if maxWidth <= 3 {
|
||||
// Not enough room for ellipsis; just trim to fit
|
||||
for len(runes) > 0 && wcswidth.Stringwidth(string(runes)) > maxWidth {
|
||||
runes = runes[:len(runes)-1]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
for len(runes) > 0 && wcswidth.Stringwidth(string(runes))+3 > maxWidth {
|
||||
runes = runes[:len(runes)-1]
|
||||
}
|
||||
return string(runes) + "..."
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
lp *loop.Loop
|
||||
screen_size loop.ScreenSize
|
||||
all_items []DisplayItem
|
||||
search_texts []string // parallel to all_items, for FZF scoring
|
||||
matcher *fzf.FuzzyMatcher
|
||||
filtered_idx []int // indices into all_items for current results
|
||||
query string
|
||||
selected_idx int
|
||||
scroll_offset int
|
||||
input_data InputData
|
||||
result string // action definition to execute after exit
|
||||
lp *loop.Loop
|
||||
screen_size loop.ScreenSize
|
||||
all_items []DisplayItem
|
||||
search_texts []string // parallel to all_items, for FZF scoring
|
||||
matcher *fzf.FuzzyMatcher
|
||||
filtered_idx []int // indices into all_items for current results
|
||||
query string
|
||||
selected_idx int
|
||||
scroll_offset int
|
||||
input_data InputData
|
||||
result string // action definition to execute after exit
|
||||
display_lines []displayLine
|
||||
results_start_y int
|
||||
results_height int
|
||||
}
|
||||
|
||||
func (h *Handler) initialize() (string, error) {
|
||||
@@ -248,6 +274,9 @@ func (h *Handler) draw_screen() {
|
||||
resultsHeight = 1
|
||||
}
|
||||
|
||||
h.results_start_y = resultsStartY
|
||||
h.results_height = resultsHeight
|
||||
|
||||
// Draw search bar
|
||||
h.lp.MoveCursorTo(1, searchBarY)
|
||||
h.lp.QueueWriteString(h.lp.SprintStyled("fg=bright-yellow", "> "))
|
||||
@@ -308,19 +337,23 @@ func (h *Handler) drawGroupedResults(startY, maxRows, width int) {
|
||||
lastMode = b.Mode
|
||||
lastCategory = ""
|
||||
if b.Mode != "" {
|
||||
// Non-default mode: show "── Keyboard mode: name ──" header (purple), no category separators
|
||||
if len(lines) > 0 {
|
||||
lines = append(lines, displayLine{itemIdx: -1, isHeader: true})
|
||||
}
|
||||
label := "Keyboard mode: " + b.Mode
|
||||
labelWidth := wcswidth.Stringwidth(label)
|
||||
sepLen := max(0, width-labelWidth-6)
|
||||
sep := strings.Repeat("\u2500", sepLen)
|
||||
lines = append(lines, displayLine{
|
||||
text: fmt.Sprintf(" Mode: %s", b.Mode),
|
||||
text: fmt.Sprintf(" \u2500\u2500 %s %s", label, sep),
|
||||
isModeHdr: true, isHeader: true, itemIdx: -1,
|
||||
})
|
||||
lines = append(lines, displayLine{itemIdx: -1, isHeader: true})
|
||||
}
|
||||
}
|
||||
|
||||
// Category header when category changes
|
||||
if b.Category != lastCategory {
|
||||
// Category header when category changes - only for the default mode ("")
|
||||
if b.Mode == "" && b.Category != lastCategory {
|
||||
lastCategory = b.Category
|
||||
if len(lines) > 0 && !lines[len(lines)-1].isHeader {
|
||||
lines = append(lines, displayLine{itemIdx: -1, isHeader: true})
|
||||
@@ -335,17 +368,14 @@ func (h *Handler) drawGroupedResults(startY, maxRows, width int) {
|
||||
}
|
||||
|
||||
// Binding line
|
||||
keyWidth := wcswidth.Stringwidth(b.Key)
|
||||
keyPad := 30
|
||||
if keyWidth > keyPad-4 {
|
||||
keyPad = keyWidth + 6
|
||||
}
|
||||
keyDisplay := truncateToWidth(b.Key, maxKeyDisplayWidth)
|
||||
lines = append(lines, displayLine{
|
||||
text: fmt.Sprintf(" %-*s %s", keyPad, b.Key, b.ActionDisplay),
|
||||
text: fmt.Sprintf(" %-*s %s", maxKeyDisplayWidth, keyDisplay, b.ActionDisplay),
|
||||
itemIdx: fi,
|
||||
})
|
||||
}
|
||||
|
||||
h.display_lines = lines
|
||||
h.drawLines(lines, startY, maxRows, width)
|
||||
}
|
||||
|
||||
@@ -354,11 +384,7 @@ func (h *Handler) drawFlatResults(startY, maxRows, width int) {
|
||||
for fi, idx := range h.filtered_idx {
|
||||
item := &h.all_items[idx]
|
||||
b := &item.binding
|
||||
keyWidth := wcswidth.Stringwidth(b.Key)
|
||||
keyPad := 30
|
||||
if keyWidth > keyPad-4 {
|
||||
keyPad = keyWidth + 6
|
||||
}
|
||||
keyDisplay := truncateToWidth(b.Key, maxKeyDisplayWidth)
|
||||
catSuffix := ""
|
||||
if b.Mode != "" {
|
||||
catSuffix = fmt.Sprintf(" [%s/%s]", b.Mode, b.Category)
|
||||
@@ -366,11 +392,12 @@ func (h *Handler) drawFlatResults(startY, maxRows, width int) {
|
||||
catSuffix = fmt.Sprintf(" [%s]", b.Category)
|
||||
}
|
||||
lines = append(lines, displayLine{
|
||||
text: fmt.Sprintf(" %-*s %-30s%s", keyPad, b.Key, b.ActionDisplay, catSuffix),
|
||||
text: fmt.Sprintf(" %-*s %-30s%s", maxKeyDisplayWidth, keyDisplay, b.ActionDisplay, catSuffix),
|
||||
itemIdx: fi,
|
||||
})
|
||||
}
|
||||
|
||||
h.display_lines = lines
|
||||
h.drawLines(lines, startY, maxRows, width)
|
||||
}
|
||||
|
||||
@@ -389,7 +416,11 @@ func (h *Handler) drawLines(lines []displayLine, startY, maxRows, width int) {
|
||||
}
|
||||
if selectedLineIdx >= 0 {
|
||||
if selectedLineIdx < h.scroll_offset {
|
||||
// Scroll up to show selected item; also reveal any header lines above it
|
||||
h.scroll_offset = selectedLineIdx
|
||||
for h.scroll_offset > 0 && lines[h.scroll_offset-1].isHeader {
|
||||
h.scroll_offset--
|
||||
}
|
||||
}
|
||||
if selectedLineIdx >= h.scroll_offset+maxRows {
|
||||
h.scroll_offset = selectedLineIdx - maxRows + 1
|
||||
@@ -442,12 +473,8 @@ func (h *Handler) drawBindingLine(text string, filteredIdx, width int) {
|
||||
b := &h.all_items[idx].binding
|
||||
|
||||
// Style the key portion green, leave action unstyled
|
||||
keyWidth := wcswidth.Stringwidth(b.Key)
|
||||
keyPad := 30
|
||||
if keyWidth > keyPad-4 {
|
||||
keyPad = keyWidth + 6
|
||||
}
|
||||
keyPrefix := fmt.Sprintf(" %-*s", keyPad, b.Key)
|
||||
keyDisplay := truncateToWidth(b.Key, maxKeyDisplayWidth)
|
||||
keyPrefix := fmt.Sprintf(" %-*s", maxKeyDisplayWidth, keyDisplay)
|
||||
rest := ""
|
||||
if len(text) > len(keyPrefix) {
|
||||
rest = text[len(keyPrefix):]
|
||||
@@ -456,6 +483,42 @@ func (h *Handler) drawBindingLine(text string, filteredIdx, width int) {
|
||||
h.lp.QueueWriteString(rest)
|
||||
}
|
||||
|
||||
// rowToFilteredIdx converts a 0-indexed cell Y coordinate to a filtered item
|
||||
// index, or -1 if the cell is not over a clickable item. Internally converts
|
||||
// to 1-indexed screen rows (matching the MoveCursorTo convention) to compare
|
||||
// against results_start_y.
|
||||
func (h *Handler) rowToFilteredIdx(cellY int) int {
|
||||
screenRow := cellY + 1 // convert 0-indexed cell to 1-indexed screen row
|
||||
if screenRow < h.results_start_y || screenRow >= h.results_start_y+h.results_height {
|
||||
return -1
|
||||
}
|
||||
lineIdx := h.scroll_offset + (screenRow - h.results_start_y)
|
||||
if lineIdx < 0 || lineIdx >= len(h.display_lines) {
|
||||
return -1
|
||||
}
|
||||
return h.display_lines[lineIdx].itemIdx
|
||||
}
|
||||
|
||||
func (h *Handler) onMouseEvent(ev *loop.MouseEvent) error {
|
||||
switch ev.Event_type {
|
||||
case loop.MOUSE_CLICK:
|
||||
if ev.Buttons&loop.LEFT_MOUSE_BUTTON != 0 {
|
||||
fi := h.rowToFilteredIdx(ev.Cell.Y)
|
||||
if fi >= 0 {
|
||||
h.selected_idx = fi
|
||||
h.triggerSelected()
|
||||
}
|
||||
}
|
||||
case loop.MOUSE_MOVE:
|
||||
fi := h.rowToFilteredIdx(ev.Cell.Y)
|
||||
h.lp.ClearPointerShapes()
|
||||
if fi >= 0 {
|
||||
h.lp.PushPointerShape(loop.POINTER_POINTER)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) onKeyEvent(ev *loop.KeyEvent) error {
|
||||
if ev.MatchesPressOrRepeat("escape") {
|
||||
ev.Handled = true
|
||||
@@ -565,6 +628,7 @@ func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
}
|
||||
|
||||
handler := &Handler{lp: lp}
|
||||
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
|
||||
|
||||
lp.OnInitialize = func() (string, error) {
|
||||
return handler.initialize()
|
||||
@@ -573,6 +637,7 @@ func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
lp.OnKeyEvent = handler.onKeyEvent
|
||||
lp.OnText = handler.onText
|
||||
lp.OnResize = handler.onResize
|
||||
lp.OnMouseEvent = handler.onMouseEvent
|
||||
|
||||
err = lp.Run()
|
||||
if err != nil {
|
||||
|
||||
@@ -74,6 +74,29 @@ def collect_keys_data(opts: Any) -> dict[str, Any]:
|
||||
ordered[cat_name] = binds
|
||||
modes[mode_name] = ordered
|
||||
|
||||
# Move push_keyboard_mode <name> bindings from the default mode into the
|
||||
# respective keyboard mode's section so they appear alongside its shortcuts.
|
||||
if '' in modes:
|
||||
new_default_cats: dict[str, list[dict[str, str]]] = {}
|
||||
for cat_name, bindings in modes[''].items():
|
||||
keep: list[dict[str, str]] = []
|
||||
for b in bindings:
|
||||
if b['action'] == 'push_keyboard_mode':
|
||||
parts = b['definition'].split()
|
||||
target = parts[1] if len(parts) > 1 else ''
|
||||
if target and target in modes:
|
||||
if 'Enter mode' not in modes[target]:
|
||||
new_target: dict[str, list[dict[str, str]]] = {'Enter mode': [b]}
|
||||
new_target.update(modes[target])
|
||||
modes[target] = new_target
|
||||
else:
|
||||
modes[target]['Enter mode'].append(b)
|
||||
continue
|
||||
keep.append(b)
|
||||
if keep:
|
||||
new_default_cats[cat_name] = keep
|
||||
modes[''] = new_default_cats
|
||||
|
||||
# Emit explicit mode and category ordering since JSON maps lose insertion order
|
||||
mode_order = list(modes.keys())
|
||||
category_order: dict[str, list[str]] = {}
|
||||
|
||||
@@ -2,6 +2,7 @@ package command_palette
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -307,3 +308,244 @@ func TestFallbackOrderingWithoutExplicitOrder(t *testing.T) {
|
||||
t.Fatalf("Expected alphabetical category order, got %q then %q", cat0, cat1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateToWidth(t *testing.T) {
|
||||
// Short string: no truncation
|
||||
s := "hello"
|
||||
got := truncateToWidth(s, 10)
|
||||
if got != s {
|
||||
t.Fatalf("Expected %q unchanged, got %q", s, got)
|
||||
}
|
||||
|
||||
// Exact width: no truncation
|
||||
got = truncateToWidth("hello", 5)
|
||||
if got != "hello" {
|
||||
t.Fatalf("Expected %q unchanged at exact width, got %q", "hello", got)
|
||||
}
|
||||
|
||||
// Over width: truncated with ellipsis
|
||||
got = truncateToWidth("hello world", 8)
|
||||
if !strings.HasSuffix(got, "...") {
|
||||
t.Fatalf("Expected truncated string to end with '...', got %q", got)
|
||||
}
|
||||
if len([]rune(got)) > 8 {
|
||||
t.Fatalf("Expected truncated string to be at most 8 runes, got %d in %q", len([]rune(got)), got)
|
||||
}
|
||||
|
||||
// Long key like a mouse binding should be truncated
|
||||
longKey := "ctrl+shift+left press ungrabbed"
|
||||
got = truncateToWidth(longKey, maxKeyDisplayWidth)
|
||||
if len([]rune(got)) > maxKeyDisplayWidth {
|
||||
t.Fatalf("Key should be truncated to maxKeyDisplayWidth, got len=%d: %q", len([]rune(got)), got)
|
||||
}
|
||||
if !strings.HasSuffix(got, "...") {
|
||||
t.Fatalf("Truncated key should end with '...', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupedResultsModeHeaderFormat(t *testing.T) {
|
||||
h := newTestHandler()
|
||||
h.updateFilter()
|
||||
|
||||
const testWidth = 80 // fixed width for testing
|
||||
|
||||
// Build lines as drawGroupedResults would with the new separator format
|
||||
var lines []displayLine
|
||||
lastMode := ""
|
||||
lastCategory := ""
|
||||
for fi, idx := range h.filtered_idx {
|
||||
b := &h.all_items[idx].binding
|
||||
if b.Mode != lastMode {
|
||||
lastMode = b.Mode
|
||||
lastCategory = ""
|
||||
if b.Mode != "" {
|
||||
if len(lines) > 0 {
|
||||
lines = append(lines, displayLine{itemIdx: -1, isHeader: true})
|
||||
}
|
||||
label := "Keyboard mode: " + b.Mode
|
||||
labelWidth := len([]rune(label))
|
||||
sepLen := max(0, testWidth-labelWidth-6)
|
||||
sep := strings.Repeat("\u2500", sepLen)
|
||||
lines = append(lines, displayLine{
|
||||
text: fmt.Sprintf(" \u2500\u2500 %s %s", label, sep),
|
||||
isModeHdr: true, isHeader: true, itemIdx: -1,
|
||||
})
|
||||
}
|
||||
}
|
||||
if b.Mode == "" && b.Category != lastCategory {
|
||||
lastCategory = b.Category
|
||||
lines = append(lines, displayLine{isHeader: true, itemIdx: -1})
|
||||
}
|
||||
lines = append(lines, displayLine{itemIdx: fi})
|
||||
}
|
||||
|
||||
// There should be a mode header for the "mw" mode
|
||||
found := false
|
||||
for _, l := range lines {
|
||||
if l.isModeHdr && strings.Contains(l.text, "Keyboard mode: mw") {
|
||||
found = true
|
||||
// Header should have ── separator characters
|
||||
if !strings.Contains(l.text, "\u2500\u2500") {
|
||||
t.Fatalf("Mode header should contain separator ── but got %q", l.text)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("Expected to find 'Keyboard mode: mw' mode header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupedResultsNoCategoryHeadersForNonDefaultMode(t *testing.T) {
|
||||
h := newTestHandler()
|
||||
h.updateFilter()
|
||||
|
||||
// Build lines as drawGroupedResults would, tracking whether we are currently
|
||||
// inside a non-default keyboard-mode section. Category separators are only
|
||||
// valid for the default mode ("") and for the mouse-actions block; they must
|
||||
// NOT appear while we are still processing items for a non-default mode (e.g.
|
||||
// "mw"). Once we transition back to Mode=="" (e.g. for mouse bindings) the
|
||||
// section is over and category headers are allowed again.
|
||||
var lines []displayLine
|
||||
lastMode := ""
|
||||
lastCategory := ""
|
||||
for fi, idx := range h.filtered_idx {
|
||||
b := &h.all_items[idx].binding
|
||||
if b.Mode != lastMode {
|
||||
lastMode = b.Mode
|
||||
lastCategory = ""
|
||||
if b.Mode != "" {
|
||||
if len(lines) > 0 {
|
||||
lines = append(lines, displayLine{itemIdx: -1, isHeader: true})
|
||||
}
|
||||
lines = append(lines, displayLine{
|
||||
text: fmt.Sprintf(" Keyboard mode: %s", b.Mode),
|
||||
isModeHdr: true, isHeader: true, itemIdx: -1,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Category headers are only emitted for the default-mode block.
|
||||
if b.Mode == "" && b.Category != lastCategory {
|
||||
lastCategory = b.Category
|
||||
lines = append(lines, displayLine{
|
||||
text: "category header", isHeader: true, itemIdx: -1,
|
||||
})
|
||||
}
|
||||
|
||||
lines = append(lines, displayLine{itemIdx: fi})
|
||||
}
|
||||
|
||||
// Verify: no "category header" line appears while we are still inside the
|
||||
// non-default keyboard-mode section.
|
||||
nonDefaultActive := false
|
||||
for _, l := range lines {
|
||||
if l.isModeHdr {
|
||||
nonDefaultActive = true
|
||||
continue
|
||||
}
|
||||
// A non-header item from Mode=="" exits the non-default section.
|
||||
if nonDefaultActive && !l.isHeader {
|
||||
if l.itemIdx >= 0 && l.itemIdx < len(h.filtered_idx) {
|
||||
idx := h.filtered_idx[l.itemIdx]
|
||||
if h.all_items[idx].binding.Mode == "" {
|
||||
nonDefaultActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowToFilteredIdx(t *testing.T) {
|
||||
h := newTestHandler()
|
||||
h.updateFilter()
|
||||
h.results_start_y = 2
|
||||
h.results_height = 20
|
||||
|
||||
// Populate display_lines with known structure
|
||||
h.display_lines = []displayLine{
|
||||
{isHeader: true, itemIdx: -1}, // line 0: category header
|
||||
{itemIdx: 0}, // line 1: first item (filteredIdx=0)
|
||||
{itemIdx: 1}, // line 2: second item (filteredIdx=1)
|
||||
{isHeader: true, itemIdx: -1}, // line 3: blank header
|
||||
{itemIdx: 2}, // line 4: third item (filteredIdx=2)
|
||||
}
|
||||
h.scroll_offset = 0
|
||||
|
||||
// cellY=1 → screenRow=2 = results_start_y → lineIdx=0 = header → -1
|
||||
if fi := h.rowToFilteredIdx(1); fi != -1 {
|
||||
t.Fatalf("Expected -1 for header row, got %d", fi)
|
||||
}
|
||||
|
||||
// cellY=2 → screenRow=3 → lineIdx=1 = first item → filteredIdx=0
|
||||
if fi := h.rowToFilteredIdx(2); fi != 0 {
|
||||
t.Fatalf("Expected filteredIdx=0 for first item row, got %d", fi)
|
||||
}
|
||||
|
||||
// cellY=3 → screenRow=4 → lineIdx=2 = second item → filteredIdx=1
|
||||
if fi := h.rowToFilteredIdx(3); fi != 1 {
|
||||
t.Fatalf("Expected filteredIdx=1 for second item row, got %d", fi)
|
||||
}
|
||||
|
||||
// cellY=4 → screenRow=5 → lineIdx=3 = blank header → -1
|
||||
if fi := h.rowToFilteredIdx(4); fi != -1 {
|
||||
t.Fatalf("Expected -1 for blank header row, got %d", fi)
|
||||
}
|
||||
|
||||
// Click above results area (cellY=0 → screenRow=1 < results_start_y=2): should return -1
|
||||
if fi := h.rowToFilteredIdx(0); fi != -1 {
|
||||
t.Fatalf("Expected -1 for row above results, got %d", fi)
|
||||
}
|
||||
|
||||
// Click below results area (cellY=22 → screenRow=23 >= results_start_y+results_height=22): should return -1
|
||||
if fi := h.rowToFilteredIdx(22); fi != -1 {
|
||||
t.Fatalf("Expected -1 for row below results, got %d", fi)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrollAdjustRevealsSectionHeader(t *testing.T) {
|
||||
// When the selected item is scrolled into view from below,
|
||||
// any immediately preceding header lines should also be visible.
|
||||
lines := []displayLine{
|
||||
{isHeader: true, itemIdx: -1}, // line 0: category header
|
||||
{itemIdx: 0}, // line 1: first item
|
||||
{itemIdx: 1}, // line 2: second item
|
||||
{isHeader: true, itemIdx: -1}, // line 3: blank
|
||||
{isHeader: true, itemIdx: -1}, // line 4: category header 2
|
||||
{itemIdx: 2}, // line 5: third item
|
||||
}
|
||||
|
||||
h := &Handler{}
|
||||
h.filtered_idx = []int{0, 1, 2}
|
||||
h.selected_idx = 0 // first item (at line 1)
|
||||
h.scroll_offset = 4 // currently scrolled past the first item
|
||||
|
||||
// Call the scroll adjustment logic from drawLines
|
||||
selectedLineIdx := -1
|
||||
for i, dl := range lines {
|
||||
if dl.itemIdx == h.selected_idx {
|
||||
selectedLineIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if selectedLineIdx != 1 {
|
||||
t.Fatalf("Expected selectedLineIdx=1, got %d", selectedLineIdx)
|
||||
}
|
||||
|
||||
maxRows := 10
|
||||
if selectedLineIdx < h.scroll_offset {
|
||||
h.scroll_offset = selectedLineIdx
|
||||
for h.scroll_offset > 0 && lines[h.scroll_offset-1].isHeader {
|
||||
h.scroll_offset--
|
||||
}
|
||||
}
|
||||
if selectedLineIdx >= h.scroll_offset+maxRows {
|
||||
h.scroll_offset = selectedLineIdx - maxRows + 1
|
||||
}
|
||||
h.scroll_offset = max(0, h.scroll_offset)
|
||||
h.scroll_offset = min(h.scroll_offset, max(0, len(lines)-maxRows))
|
||||
|
||||
// scroll_offset should be 0 so the category header at line 0 is visible
|
||||
if h.scroll_offset != 0 {
|
||||
t.Fatalf("Expected scroll_offset=0 to show category header, got %d", h.scroll_offset)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ groups: dict[ActionGroup, str] = {
|
||||
'sc': 'Scrolling',
|
||||
'win': 'Window management',
|
||||
'tab': 'Tab management',
|
||||
'fs': 'Font sizes',
|
||||
'mouse': 'Mouse actions',
|
||||
'mk': 'Marks',
|
||||
'lay': 'Layouts',
|
||||
|
||||
@@ -1465,7 +1465,7 @@ class Boss:
|
||||
def set_font_size(self, new_size: float) -> None: # legacy
|
||||
self.change_font_size(True, None, new_size)
|
||||
|
||||
@ac('win', '''
|
||||
@ac('fs', '''
|
||||
Change the font size for the current or all OS Windows
|
||||
|
||||
See :ref:`conf-kitty-shortcuts.fonts` for details.
|
||||
|
||||
@@ -215,7 +215,7 @@ def modmap() -> dict[str, int]:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
ActionGroup = Literal['cp', 'sc', 'win', 'tab', 'mouse', 'mk', 'lay', 'misc', 'debug', 'session']
|
||||
ActionGroup = Literal['cp', 'sc', 'win', 'tab', 'fs', 'mouse', 'mk', 'lay', 'misc', 'debug', 'session']
|
||||
else:
|
||||
ActionGroup = str
|
||||
|
||||
|
||||
@@ -2141,14 +2141,14 @@ class Window:
|
||||
|
||||
# actions {{{
|
||||
|
||||
@ac('cp', 'Show scrollback in a pager like less')
|
||||
@ac('sc', 'Show scrollback in a pager like less')
|
||||
def show_scrollback(self) -> Optional['Window']:
|
||||
text = self.as_text(as_ansi=True, add_history=True, add_wrap_markers=True)
|
||||
data = self.pipe_data(text, has_wrap_markers=True)
|
||||
cursor_on_screen = self.screen.scrolled_by < self.screen.lines - self.screen.cursor.y
|
||||
return get_boss().display_scrollback(self, data['text'], data['input_line_number'], report_cursor=cursor_on_screen)
|
||||
|
||||
@ac('cp', '''
|
||||
@ac('sc', '''
|
||||
Search scrollback in a pager like less. If there is selected text, it is automatically searched for.
|
||||
Note that this assumes that pressing the / key triggers search mode in the page configured as the
|
||||
scrollback pager.
|
||||
@@ -2179,7 +2179,7 @@ class Window:
|
||||
def show_first_command_output_on_screen(self) -> None:
|
||||
self.show_cmd_output(CommandOutput.first_on_screen, 'First command output on screen')
|
||||
|
||||
@ac('cp', '''
|
||||
@ac('sc', '''
|
||||
Show output from the last shell command in a pager like less
|
||||
|
||||
Requires :ref:`shell_integration` to work
|
||||
|
||||
Reference in New Issue
Block a user