Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions kitty/cell_size.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package kitty

import (
"log"
"os"
"regexp"
"strconv"
"time"

"golang.org/x/term"
)

const (
defaultCellWidth = 8
defaultCellHeight = 16
)

// getCellSize attempts to query the terminal for cell size in pixels.
// Uses CSI 16 t: "Report xterm window character cell size in pixels" -> CSI 6 ; height ; width t
// Returns default values on error.
func getCellSize() (width, height int) {
// Use defaults initially
width = defaultCellWidth
height = defaultCellHeight

query := "\033[16t"
// Use stdin for raw mode check/restore, stdout for writing query
stdinFd := int(os.Stdin.Fd())
stdoutFd := int(os.Stdout.Fd())

// Check if stdin/stdout are terminals
if !term.IsTerminal(stdinFd) || !term.IsTerminal(stdoutFd) {
log.Printf("Warning: Cannot query cell size: stdin/stdout not a terminal.")
return
}

state, err := term.MakeRaw(stdinFd)
if err != nil {
log.Printf("Warning: Cannot query cell size: failed to enter raw mode: %v", err)
return
}
defer term.Restore(stdinFd, state)

// Write query to stdout
_, err = os.Stdout.Write([]byte(query))
if err != nil {
log.Printf("Warning: Cannot query cell size: failed to write query: %v", err)
return
}

// Read response from stdin with timeout
responseChan := make(chan string)
readErrChan := make(chan error)
go func() {
var buf [64]byte // Buffer for response
n, readErr := os.Stdin.Read(buf[:])
if readErr != nil {
readErrChan <- readErr
} else if n > 0 {
responseChan <- string(buf[:n])
} else {
close(responseChan) // Should not happen?
}
}()

var response string
select {
case resp := <-responseChan:
response = resp
case err = <-readErrChan:
log.Printf("Warning: Cannot query cell size: failed to read response: %v", err)
return
case <-time.After(150 * time.Millisecond): // Increased timeout slightly
log.Printf("Warning: Cannot query cell size: timeout waiting for response.")
return
}

// Parse response: \033[6;<height>;<width>t
re := regexp.MustCompile(`\033\[6;(\d+);(\d+)t`)
matches := re.FindStringSubmatch(response)

if len(matches) == 3 {
h, e1 := strconv.Atoi(matches[1])
w, e2 := strconv.Atoi(matches[2])
if e1 == nil && e2 == nil && h > 0 && w > 0 {
width = w
height = h
return
}
}

log.Printf("Warning: Cannot query cell size: failed to parse response: %q", response)
return
}
74 changes: 74 additions & 0 deletions kitty/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package kitty

import (
"fmt"
"os"
"strings"
"time"

"golang.org/x/term"
)

// CheckKittyGraphicsProtocol checks for Kitty graphics protocol support using the query method.
func CheckKittyGraphicsProtocol() bool {
// Use a unique ID for the query, e.g., based on process ID or random
queryID := uint32(os.Getpid() & 0xFFFFFFFF) // Example ID
if queryID == 0 {
queryID = 1 // Ensure non-zero ID
}
// Graphics query command (dummy 1x1 RGB pixel)
// https://sw.kovidgoyal.net/kitty/graphics-protocol/#querying-support-and-available-transmission-mediums
graphicsQuery := fmt.Sprintf("\033_Ga=q,i=%d,s=1,v=1,t=d,f=24;AAAA\033\\", queryID)

// Need raw mode to send/receive control sequences without shell interference
fd := int(os.Stdout.Fd())
state, err := term.MakeRaw(fd)
if err != nil {
return false // Cannot enter raw mode
}
defer term.Restore(fd, state)

// Wrap graphics query for tmux if necessary
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, option settings are required, so I thought it would be a good idea to add a comment like the one below.

	// Requires the allow-passthrough option set to on or all for this to work
	// https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it

isTmux := os.Getenv("TMUX") != ""
if isTmux {
escapedQuery := strings.ReplaceAll(graphicsQuery, "\033", "\033\033")
graphicsQuery = fmt.Sprintf("\033Ptmux;%s\033\\", escapedQuery)
}

// Write only the graphics query
_, err = os.Stdout.Write([]byte(graphicsQuery))
if err != nil {
return false
}
os.Stdout.Sync()

// Read response with timeout
responseChan := make(chan string)
go func() {
var buf [256]byte
n, readErr := os.Stdout.Read(buf[:])
if readErr == nil && n > 0 {
responseChan <- string(buf[:n])
} else {
close(responseChan) // Signal no response or error
}
}()

var response string
select {
case resp, ok := <-responseChan:
if ok {
response = resp
}
case <-time.After(10 * time.Millisecond):
}

// Check if the response is the graphics protocol ACK
// Expected format: \033_Gi=<queryID>;OK\033\ (or an error message)
expectedGraphicsPrefix := fmt.Sprintf("\033_Gi=%d;", queryID)
if strings.HasPrefix(response, expectedGraphicsPrefix) && strings.HasSuffix(response, "\033\\") {
return true // Got a graphics response, assume support
}
// If we didn't get the graphics response, assume no support
return false
}
45 changes: 45 additions & 0 deletions kitty/diacrit_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kitty

// We only need the first 256 for row/col/id encoding.
var diacriticMap = [256]string{
"\u0305", "\u030d", "\u030e", "\u0310", "\u0312", "\u033d", "\u033e", "\u033f", // 0-7
"\u0346", "\u034a", "\u034b", "\u034c", "\u0350", "\u0351", "\u0352", "\u0357", // 8-15
"\u035b", "\u0363", "\u0364", "\u0365", "\u0366", "\u0367", "\u0368", "\u0369", // 16-23
"\u036a", "\u036b", "\u036c", "\u036d", "\u036e", "\u036f", "\u0483", "\u0484", // 24-31
"\u0485", "\u0486", "\u0487", "\u0592", "\u0593", "\u0594", "\u0595", "\u0597", // 32-39
"\u0598", "\u0599", "\u059c", "\u059d", "\u059e", "\u059f", "\u05a0", "\u05a1", // 40-47
"\u05a8", "\u05a9", "\u05ab", "\u05ac", "\u05af", "\u05c4", "\u0610", "\u0611", // 48-55
"\u0612", "\u0613", "\u0614", "\u0615", "\u0616", "\u0617", "\u0657", "\u0658", // 56-63
"\u0659", "\u065a", "\u065b", "\u065d", "\u065e", "\u06d6", "\u06d7", "\u06d8", // 64-71
"\u06d9", "\u06da", "\u06db", "\u06dc", "\u06df", "\u06e0", "\u06e1", "\u06e2", // 72-79
"\u06e4", "\u06e7", "\u06e8", "\u06eb", "\u06ec", "\u0730", "\u0732", "\u0733", // 80-87
"\u0735", "\u0736", "\u073a", "\u073d", "\u073f", "\u0740", "\u0741", "\u0743", // 88-95
"\u0745", "\u0747", "\u0749", "\u074a", "\u07eb", "\u07ec", "\u07ed", "\u07ee", // 96-103
"\u07ef", "\u07f0", "\u07f1", "\u07f3", "\u0816", "\u0817", "\u0818", "\u0819", // 104-111
"\u081b", "\u081c", "\u081d", "\u081e", "\u081f", "\u0820", "\u0821", "\u0822", // 112-119
"\u0823", "\u0825", "\u0826", "\u0827", "\u0829", "\u082a", "\u082b", "\u082c", // 120-127
"\u082d", "\u0951", "\u0953", "\u0954", "\u0f82", "\u0f83", "\u0f86", "\u0f87", // 128-135
"\u135d", "\u135e", "\u135f", "\u17dd", "\u193a", "\u1a17", "\u1a75", "\u1a76", // 136-143
"\u1a77", "\u1a78", "\u1a79", "\u1a7a", "\u1a7b", "\u1a7c", "\u1b6b", "\u1b6d", // 144-151
"\u1b6e", "\u1b6f", "\u1b70", "\u1b71", "\u1b72", "\u1b73", "\u1cd0", "\u1cd1", // 152-159
"\u1cd2", "\u1cda", "\u1cdb", "\u1ce0", "\u1dc0", "\u1dc1", "\u1dc3", "\u1dc4", // 160-167
"\u1dc5", "\u1dc6", "\u1dc7", "\u1dc8", "\u1dc9", "\u1dcb", "\u1dcc", "\u1dd1", // 168-175
"\u1dd2", "\u1dd3", "\u1dd4", "\u1dd5", "\u1dd6", "\u1dd7", "\u1dd8", "\u1dd9", // 176-183
"\u1dda", "\u1ddb", "\u1ddc", "\u1ddd", "\u1dde", "\u1ddf", "\u1de0", "\u1de1", // 184-191
"\u1de2", "\u1de3", "\u1de4", "\u1de5", "\u1de6", "\u1dfe", "\u20d0", "\u20d1", // 192-199
"\u20d4", "\u20d5", "\u20d6", "\u20d7", "\u20db", "\u20dc", "\u20e1", "\u20e7", // 200-207
"\u20e9", "\u20f0", "\u2cef", "\u2cf0", "\u2cf1", "\u2de0", "\u2de1", "\u2de2", // 208-215
"\u2de3", "\u2de4", "\u2de5", "\u2de6", "\u2de7", "\u2de8", "\u2de9", "\u2dea", // 216-223
"\u2deb", "\u2dec", "\u2ded", "\u2dee", "\u2def", "\u2df0", "\u2df1", "\u2df2", // 224-231
"\u2df3", "\u2df4", "\u2df5", "\u2df6", "\u2df7", "\u2df8", "\u2df9", "\u2dfa", // 232-239
"\u2dfb", "\u2dfc", "\u2dfd", "\u2dfe", "\u2dff", "\ua66f", "\ua67c", "\ua67d", // 240-247
"\ua6f0", "\ua6f1", "\ua8e0", "\ua8e1", "\ua8e2", "\ua8e3", "\ua8e4", "\ua8e5", // 248-255
}

// getDiacritic returns the combining character for a given number 0-255.
func getDiacritic(num int) string {
if num >= 0 && num < len(diacriticMap) {
return diacriticMap[num]
}
return "" // Or return a default/error indicator
}
Loading