Files
kitty/kittens/command_palette/main.go
2026-03-03 15:37:55 +05:30

836 lines
23 KiB
Go

// License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
package command_palette
import (
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/kovidgoyal/kitty/tools/cli"
"github.com/kovidgoyal/kitty/tools/fzf"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/tui"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/wcswidth"
)
// JSON data structures matching Python collect_keys_data output
type Binding struct {
Key string `json:"key"`
Action string `json:"action"`
ActionDisplay string `json:"action_display"`
Definition string `json:"definition"`
Help string `json:"help"`
LongHelp string `json:"long_help"`
Category string
Mode string
IsMouse bool
}
type InputData struct {
Modes map[string]map[string][]Binding `json:"modes"`
Mouse []Binding `json:"mouse"`
ModeOrder []string `json:"mode_order"`
CategoryOrder map[string][]string `json:"category_order"`
}
// DisplayItem wraps a binding with its per-column search texts for FZF scoring
type DisplayItem struct {
binding Binding
colTexts [3]string // [0]=key, [1]=action_display, [2]=category
}
// matchInfo stores which column matched and the matched character positions
type matchInfo struct {
colIdx int // which column matched: 0=key, 1=action_display, 2=category
positions []int // rune positions in the matched column text
}
type displayLine struct {
text string
isHeader bool
isModeHdr bool
itemIdx int // index into filtered_idx, -1 for headers
}
const maxKeyDisplayWidth = 30
// unmappedLabel is shown in the key column for actions with no keyboard shortcut.
const unmappedLabel = "(unmapped)"
// 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) + "..."
}
// CachedSettings holds persistent UI settings stored in command-palette.json.
type CachedSettings struct {
ShowUnmapped bool `json:"show_unmapped"`
}
type Handler struct {
lp *loop.Loop
screen_size loop.ScreenSize
all_items []DisplayItem
matcher *fzf.FuzzyMatcher
filtered_idx []int // indices into all_items for current results
match_infos []matchInfo // parallel to filtered_idx, valid when query != ""
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
show_unmapped bool
cv *utils.CachedValues[*CachedSettings]
}
func (h *Handler) initialize() (string, error) {
sz, err := h.lp.ScreenSize()
if err != nil {
return "", err
}
h.screen_size = sz
h.lp.SetCursorVisible(true)
h.lp.SetCursorShape(loop.BAR_CURSOR, true)
h.lp.AllowLineWrapping(false)
h.lp.SetWindowTitle("Command Palette")
// Initialize with ShowUnmapped: true as the default; Load() returns this
// default when no cache file exists yet.
h.cv = utils.NewCachedValues("command-palette", &CachedSettings{ShowUnmapped: true})
settings := h.cv.Load()
h.show_unmapped = settings.ShowUnmapped
if err := h.loadData(); err != nil {
return "", err
}
h.matcher = fzf.NewFuzzyMatcher(fzf.DEFAULT_SCHEME)
h.updateFilter()
h.draw_screen()
h.lp.SendOverlayReady()
return "", nil
}
func (h *Handler) loadData() error {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read stdin: %w", err)
}
if len(data) == 0 {
return fmt.Errorf("no input data received on stdin; this kitten must be launched from kitty")
}
if err := json.Unmarshal(data, &h.input_data); err != nil {
return fmt.Errorf("failed to parse input data: %w", err)
}
h.flattenBindings()
return nil
}
// flattenBindings converts the hierarchical mode/category/binding data into
// a flat list suitable for display and FZF scoring. Uses the explicit ordering
// arrays from Python since Go maps do not preserve insertion order.
func (h *Handler) flattenBindings() {
// Use explicit mode ordering from Python, falling back to sorted keys
modeNames := h.input_data.ModeOrder
if len(modeNames) == 0 {
modeNames = make([]string, 0, len(h.input_data.Modes))
for name := range h.input_data.Modes {
modeNames = append(modeNames, name)
}
sort.Slice(modeNames, func(i, j int) bool {
if modeNames[i] == "" {
return true
}
if modeNames[j] == "" {
return false
}
return modeNames[i] < modeNames[j]
})
}
for _, modeName := range modeNames {
categories, ok := h.input_data.Modes[modeName]
if !ok {
continue
}
// Use explicit category ordering from Python, falling back to sorted keys
catNames := h.input_data.CategoryOrder[modeName]
if len(catNames) == 0 {
catNames = make([]string, 0, len(categories))
for name := range categories {
catNames = append(catNames, name)
}
sort.Strings(catNames)
}
for _, catName := range catNames {
bindings, ok := categories[catName]
if !ok {
continue
}
for _, b := range bindings {
b.Category = catName
b.Mode = modeName
b.IsMouse = false
keyText := b.Key
if keyText == "" {
keyText = unmappedLabel
}
h.all_items = append(h.all_items, DisplayItem{
binding: b,
colTexts: [3]string{keyText, b.ActionDisplay, catName},
})
}
}
}
// Mouse bindings
for _, b := range h.input_data.Mouse {
b.Category = "Mouse actions"
b.Mode = ""
b.IsMouse = true
h.all_items = append(h.all_items, DisplayItem{
binding: b,
colTexts: [3]string{b.Key, b.ActionDisplay, "Mouse actions"},
})
}
}
func (h *Handler) updateFilter() {
if h.query == "" {
// Show all items in original order, respecting the show_unmapped toggle
h.filtered_idx = make([]int, 0, len(h.all_items))
for i, item := range h.all_items {
if !h.show_unmapped && item.binding.Key == "" {
continue
}
h.filtered_idx = append(h.filtered_idx, i)
}
h.match_infos = nil
h.selected_idx = 0
h.scroll_offset = 0
return
}
nItems := len(h.all_items)
// Build per-column text slices for batch FZF scoring
colSlices := [3][]string{
make([]string, nItems),
make([]string, nItems),
make([]string, nItems),
}
for i, item := range h.all_items {
colSlices[0][i] = item.colTexts[0]
colSlices[1][i] = item.colTexts[1]
colSlices[2][i] = item.colTexts[2]
}
// Score each column independently
colResults := [3][]fzf.Result{}
for c := 0; c < 3; c++ {
results, err := h.matcher.Score(colSlices[c], h.query)
if err == nil {
colResults[c] = results
}
}
type scored struct {
idx int
score uint
colIdx int
positions []int
}
var matches []scored
for i := range h.all_items {
if !h.show_unmapped && h.all_items[i].binding.Key == "" {
continue
}
bestScore := uint(0)
bestCol := 0
var bestPositions []int
for c := 0; c < 3; c++ {
if colResults[c] != nil && i < len(colResults[c]) && colResults[c][i].Score > bestScore {
bestScore = colResults[c][i].Score
bestCol = c
bestPositions = colResults[c][i].Positions
}
}
if bestScore > 0 {
matches = append(matches, scored{idx: i, score: bestScore, colIdx: bestCol, positions: bestPositions})
}
}
sort.Slice(matches, func(i, j int) bool {
return matches[i].score > matches[j].score
})
h.filtered_idx = make([]int, len(matches))
h.match_infos = make([]matchInfo, len(matches))
for i, m := range matches {
h.filtered_idx[i] = m.idx
h.match_infos[i] = matchInfo{colIdx: m.colIdx, positions: m.positions}
}
h.selected_idx = 0
h.scroll_offset = 0
}
// highlightMatchedChars returns a string with characters at the given rune
// positions rendered using matchStyle, and the rest rendered using baseStyle
// (or unstyled if baseStyle is empty).
func (h *Handler) highlightMatchedChars(text string, positions []int, baseStyle, matchStyle string) string {
if len(positions) == 0 {
if baseStyle != "" {
return h.lp.SprintStyled(baseStyle, text)
}
return text
}
posSet := make(map[int]bool, len(positions))
for _, p := range positions {
posSet[p] = true
}
runes := []rune(text)
var sb strings.Builder
for i, r := range runes {
ch := string(r)
if posSet[i] {
sb.WriteString(h.lp.SprintStyled(matchStyle, ch))
} else if baseStyle != "" {
sb.WriteString(h.lp.SprintStyled(baseStyle, ch))
} else {
sb.WriteString(ch)
}
}
return sb.String()
}
func (h *Handler) selectedBinding() *Binding {
if h.selected_idx < 0 || h.selected_idx >= len(h.filtered_idx) {
return nil
}
idx := h.filtered_idx[h.selected_idx]
if idx < 0 || idx >= len(h.all_items) {
return nil
}
return &h.all_items[idx].binding
}
func (h *Handler) draw_screen() {
h.lp.StartAtomicUpdate()
defer h.lp.EndAtomicUpdate()
h.lp.ClearScreen()
width := int(h.screen_size.WidthCells)
height := int(h.screen_size.HeightCells)
if width < 10 || height < 5 {
return
}
// Layout: line 1 = search bar, lines 2..height-2 = results,
// line height-1 = help text, line height = key hints
searchBarY := 1
resultsStartY := 2
helpY := height - 1
hintsY := height
resultsHeight := helpY - resultsStartY
if resultsHeight < 1 {
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", "> "))
h.lp.QueueWriteString(h.query)
// Draw results
if h.query == "" {
h.drawGroupedResults(resultsStartY, resultsHeight, width)
} else {
h.drawFlatResults(resultsStartY, resultsHeight, width)
}
// Draw help text for selected binding
h.lp.MoveCursorTo(1, helpY)
if b := h.selectedBinding(); b != nil && b.Help != "" {
helpStr := b.Help
maxLen := width - 2
if maxLen < 3 {
maxLen = 3
}
if wcswidth.Stringwidth(helpStr) > maxLen {
// Truncate by runes to avoid breaking multi-byte characters
runes := []rune(helpStr)
for len(runes) > 0 && wcswidth.Stringwidth(string(runes))+3 > maxLen {
runes = runes[:len(runes)-1]
}
helpStr = string(runes) + "..."
}
h.lp.QueueWriteString(h.lp.SprintStyled("dim italic", " "+helpStr))
}
// Draw key hints footer
h.lp.MoveCursorTo(1, hintsY)
unmappedToggleLabel := "Show"
if h.show_unmapped {
unmappedToggleLabel = "Hide"
}
footer := h.lp.SprintStyled("fg=bright-yellow", "[Enter]") + " Run " +
h.lp.SprintStyled("fg=bright-yellow", "[Esc]") + " Quit " +
h.lp.SprintStyled("fg=bright-yellow", "\u2191\u2193") + " Navigate " +
h.lp.SprintStyled("fg=bright-yellow", "[F12]") + " " + unmappedToggleLabel + " unmapped"
matchInfo := ""
if h.query != "" {
matchInfo = fmt.Sprintf(" %d/%d", len(h.filtered_idx), len(h.all_items))
}
h.lp.QueueWriteString(" " + footer + h.lp.SprintStyled("dim", matchInfo))
// Position cursor at end of search text for typing
h.lp.MoveCursorTo(3+wcswidth.Stringwidth(h.query), searchBarY)
}
func (h *Handler) drawGroupedResults(startY, maxRows, width int) {
var lines []displayLine
lastMode := ""
lastCategory := ""
for fi, idx := range h.filtered_idx {
item := &h.all_items[idx]
b := &item.binding
// Mode header when mode changes
if b.Mode != lastMode {
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(" \u2500\u2500 %s %s", label, sep),
isModeHdr: true, isHeader: true, itemIdx: -1,
})
}
}
// 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})
}
catWidth := wcswidth.Stringwidth(b.Category)
sepLen := max(0, width-catWidth-6)
sep := strings.Repeat("\u2500", sepLen)
lines = append(lines, displayLine{
text: fmt.Sprintf(" \u2500\u2500 %s %s", b.Category, sep),
isHeader: true, itemIdx: -1,
})
}
// Binding line — key column shows "(unmapped)" for actions with no shortcut
keyDisplay := b.Key
if keyDisplay == "" {
keyDisplay = unmappedLabel
}
keyDisplay = truncateToWidth(keyDisplay, maxKeyDisplayWidth)
lines = append(lines, displayLine{
text: fmt.Sprintf(" %-*s %s", maxKeyDisplayWidth, keyDisplay, b.ActionDisplay),
itemIdx: fi,
})
}
h.display_lines = lines
h.drawLines(lines, startY, maxRows, width)
}
func (h *Handler) drawFlatResults(startY, maxRows, width int) {
if len(h.filtered_idx) == 0 {
h.lp.MoveCursorTo(1, startY)
h.lp.QueueWriteString(h.lp.SprintStyled("italic dim", " No matches found"))
h.display_lines = []displayLine{}
return
}
var lines []displayLine
for fi, idx := range h.filtered_idx {
item := &h.all_items[idx]
b := &item.binding
keyDisplay := b.Key
if keyDisplay == "" {
keyDisplay = unmappedLabel
}
keyDisplay = truncateToWidth(keyDisplay, maxKeyDisplayWidth)
catSuffix := ""
if b.Mode != "" {
catSuffix = fmt.Sprintf(" [%s/%s]", b.Mode, b.Category)
} else {
catSuffix = fmt.Sprintf(" [%s]", b.Category)
}
lines = append(lines, displayLine{
text: fmt.Sprintf(" %-*s %-30s%s", maxKeyDisplayWidth, keyDisplay, b.ActionDisplay, catSuffix),
itemIdx: fi,
})
}
h.display_lines = lines
h.drawLines(lines, startY, maxRows, width)
}
func (h *Handler) drawLines(lines []displayLine, startY, maxRows, width int) {
if maxRows <= 0 || len(lines) == 0 {
return
}
// Adjust scroll to keep selected item visible
selectedLineIdx := -1
for i, dl := range lines {
if dl.itemIdx == h.selected_idx {
selectedLineIdx = i
break
}
}
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
}
}
h.scroll_offset = max(0, h.scroll_offset)
h.scroll_offset = min(h.scroll_offset, max(0, len(lines)-maxRows))
end := min(h.scroll_offset+maxRows, len(lines))
for row, li := range lines[h.scroll_offset:end] {
h.lp.MoveCursorTo(1, startY+row)
text := li.text
// Truncate at rune boundary to avoid breaking multi-byte characters
if wcswidth.Stringwidth(text) > width {
runes := []rune(text)
for len(runes) > 0 && wcswidth.Stringwidth(string(runes)) > width {
runes = runes[:len(runes)-1]
}
text = string(runes)
}
if li.isModeHdr {
h.lp.QueueWriteString(h.lp.SprintStyled("bold fg=magenta", text))
} else if li.isHeader {
h.lp.QueueWriteString(h.lp.SprintStyled("fg=bright-blue", text))
} else if li.itemIdx == h.selected_idx {
// Selected item: highlight with reverse video
padded := text
textWidth := wcswidth.Stringwidth(text)
if textWidth < width {
padded += strings.Repeat(" ", width-textWidth)
}
h.lp.QueueWriteString(h.lp.SprintStyled("fg=black bg=white", padded))
} else {
h.drawBindingLine(text, li.itemIdx, width)
}
}
}
func (h *Handler) drawBindingLine(text string, filteredIdx, width int) {
if filteredIdx < 0 || filteredIdx >= len(h.filtered_idx) {
h.lp.QueueWriteString(text)
return
}
idx := h.filtered_idx[filteredIdx]
if idx < 0 || idx >= len(h.all_items) {
h.lp.QueueWriteString(text)
return
}
b := &h.all_items[idx].binding
// Build the key display (using unmappedLabel for items with no shortcut)
rawKey := b.Key
if rawKey == "" {
rawKey = unmappedLabel
}
keyDisplay := truncateToWidth(rawKey, maxKeyDisplayWidth)
// Determine match info for highlighting (only set when a query is active)
var mi *matchInfo
if h.query != "" && filteredIdx < len(h.match_infos) {
mi = &h.match_infos[filteredIdx]
}
const matchStyle = "fg=bright-yellow"
const keyStyle = "fg=green"
const unmappedStyle = "dim fg=green"
// Render key column (4-space indent + key padded to maxKeyDisplayWidth + space)
paddingLen := max(0, maxKeyDisplayWidth-wcswidth.Stringwidth(keyDisplay))
if mi != nil && mi.colIdx == 0 {
ks := keyStyle
if b.Key == "" {
ks = unmappedStyle
}
h.lp.QueueWriteString(" ")
h.lp.QueueWriteString(h.highlightMatchedChars(keyDisplay, mi.positions, ks, matchStyle))
h.lp.QueueWriteString(strings.Repeat(" ", paddingLen) + " ")
} else if b.Key == "" {
h.lp.QueueWriteString(h.lp.SprintStyled(unmappedStyle, " "+keyDisplay+strings.Repeat(" ", paddingLen)+" "))
} else {
h.lp.QueueWriteString(h.lp.SprintStyled(keyStyle, " "+keyDisplay+strings.Repeat(" ", paddingLen)+" "))
}
// Render action display column
if mi != nil && mi.colIdx == 1 {
h.lp.QueueWriteString(h.highlightMatchedChars(b.ActionDisplay, mi.positions, "", matchStyle))
} else {
h.lp.QueueWriteString(b.ActionDisplay)
}
// Render category suffix (only present in flat / search-results mode)
if h.query != "" {
if mi != nil && mi.colIdx == 2 {
if b.Mode != "" {
h.lp.QueueWriteString(fmt.Sprintf(" [%s/", b.Mode))
} else {
h.lp.QueueWriteString(" [")
}
h.lp.QueueWriteString(h.highlightMatchedChars(b.Category, mi.positions, "", matchStyle))
h.lp.QueueWriteString("]")
} else {
if b.Mode != "" {
h.lp.QueueWriteString(fmt.Sprintf(" [%s/%s]", b.Mode, b.Category))
} else {
h.lp.QueueWriteString(fmt.Sprintf(" [%s]", b.Category))
}
}
}
}
// 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
if h.query != "" {
h.query = ""
h.updateFilter()
h.draw_screen()
} else {
h.lp.Quit(0)
}
return nil
}
if ev.MatchesPressOrRepeat("enter") {
ev.Handled = true
h.triggerSelected()
return nil
}
if ev.MatchesPressOrRepeat("up") || ev.MatchesPressOrRepeat("ctrl+k") || ev.MatchesPressOrRepeat("ctrl+p") {
ev.Handled = true
h.moveSelection(-1)
return nil
}
if ev.MatchesPressOrRepeat("down") || ev.MatchesPressOrRepeat("ctrl+j") || ev.MatchesPressOrRepeat("ctrl+n") {
ev.Handled = true
h.moveSelection(1)
return nil
}
if ev.MatchesPressOrRepeat("page_up") {
ev.Handled = true
delta := max(1, int(h.screen_size.HeightCells)-4)
h.moveSelection(-delta)
return nil
}
if ev.MatchesPressOrRepeat("page_down") {
ev.Handled = true
delta := max(1, int(h.screen_size.HeightCells)-4)
h.moveSelection(delta)
return nil
}
if ev.MatchesPressOrRepeat("home") || ev.MatchesPressOrRepeat("ctrl+home") {
ev.Handled = true
h.selected_idx = 0
h.draw_screen()
return nil
}
if ev.MatchesPressOrRepeat("end") || ev.MatchesPressOrRepeat("ctrl+end") {
ev.Handled = true
if len(h.filtered_idx) > 0 {
h.selected_idx = len(h.filtered_idx) - 1
}
h.draw_screen()
return nil
}
if ev.MatchesPressOrRepeat("backspace") {
ev.Handled = true
if h.query != "" {
g := wcswidth.SplitIntoGraphemes(h.query)
h.query = strings.Join(g[:len(g)-1], "")
h.updateFilter()
h.draw_screen()
} else {
h.lp.Beep()
}
return nil
}
if ev.MatchesPressOrRepeat("f12") {
ev.Handled = true
h.show_unmapped = !h.show_unmapped
if h.cv != nil {
h.cv.Opts.ShowUnmapped = h.show_unmapped
h.cv.Save()
}
h.updateFilter()
h.draw_screen()
return nil
}
return nil
}
func (h *Handler) onText(text string, from_key_event bool, in_bracketed_paste bool) error {
h.query += text
h.updateFilter()
h.draw_screen()
return nil
}
func (h *Handler) onResize(old, new_size loop.ScreenSize) error {
h.screen_size = new_size
h.draw_screen()
return nil
}
func (h *Handler) moveSelection(delta int) {
if len(h.filtered_idx) == 0 {
return
}
h.selected_idx += delta
h.selected_idx = max(0, h.selected_idx)
h.selected_idx = min(h.selected_idx, len(h.filtered_idx)-1)
h.draw_screen()
}
func (h *Handler) triggerSelected() {
b := h.selectedBinding()
if b == nil || b.IsMouse {
h.lp.Beep()
return
}
h.result = b.Definition
h.lp.Quit(0)
}
func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
if tty.IsTerminal(os.Stdin.Fd()) {
return 1, fmt.Errorf("This kitten must only be run via a mapping in kitty.conf")
}
output := tui.KittenOutputSerializer()
lp, err := loop.New()
if err != nil {
return 1, err
}
handler := &Handler{lp: lp}
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
lp.OnInitialize = func() (string, error) {
return handler.initialize()
}
lp.OnFinalize = func() string { return "" }
lp.OnKeyEvent = handler.onKeyEvent
lp.OnText = handler.onText
lp.OnResize = handler.onResize
lp.OnMouseEvent = handler.onMouseEvent
err = lp.Run()
if err != nil {
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal:", ds)
lp.KillIfSignalled()
return
}
rc = lp.ExitCode()
if handler.result != "" {
s, serr := output(map[string]string{"action": handler.result})
if serr == nil {
fmt.Println(s)
}
}
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}