mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-28 11:11:47 +02:00
Rewrite mouse-demo drop to use OnDnDData API; fix QueueDnDData bugs
Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/3359d812-5cb2-44a6-ae6e-ba1d708c490a Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
f8dd59ebb0
commit
2e314ecba9
@@ -3,93 +3,21 @@
|
|||||||
package mouse_demo
|
package mouse_demo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovidgoyal/kitty"
|
|
||||||
"github.com/kovidgoyal/kitty/tools/tty"
|
"github.com/kovidgoyal/kitty/tools/tty"
|
||||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
|
||||||
"github.com/kovidgoyal/kitty/tools/utils/machine_id"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
var debugprintln = tty.DebugPrintln
|
var debugprintln = tty.DebugPrintln
|
||||||
var _ = debugprintln
|
var _ = debugprintln
|
||||||
|
|
||||||
const dnd_accepted_mimes = "text/plain text/uri-list"
|
|
||||||
|
|
||||||
func dnd_escape(metadata, payload string) string {
|
|
||||||
b := strings.Builder{}
|
|
||||||
fmt.Fprintf(&b, "\x1b]%d;", kitty.DndCode)
|
|
||||||
b.WriteString(metadata)
|
|
||||||
if payload != "" {
|
|
||||||
b.WriteByte(';')
|
|
||||||
b.WriteString(payload)
|
|
||||||
}
|
|
||||||
b.WriteString("\x1b\\")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// get_machine_id returns the machine id in the format expected by the DnD
|
|
||||||
// protocol ("1:" followed by HMAC-SHA256 of /etc/machine-id).
|
|
||||||
func get_machine_id() string {
|
|
||||||
ans, err := machine_id.MachineId()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
mac := hmac.New(sha256.New, []byte("tty-dnd-protocol-machine-id"))
|
|
||||||
mac.Write(utils.UnsafeStringToBytes(ans))
|
|
||||||
return "1:" + hex.EncodeToString(mac.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnd_start_accepting(machine_id string) string {
|
|
||||||
result := dnd_escape("t=a", dnd_accepted_mimes)
|
|
||||||
if machine_id != "" {
|
|
||||||
result += dnd_escape("t=a:x=1", machine_id)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnd_stop_accepting() string {
|
|
||||||
return dnd_escape("t=A", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnd_accept_drag(mimes string) string {
|
|
||||||
return dnd_escape("t=m:o=1", mimes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnd_reject_drag() string {
|
|
||||||
return dnd_escape("t=m:o=0", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnd_request_mime_data requests MIME type data by 1-based index.
|
|
||||||
func dnd_request_mime_data(idx int) string {
|
|
||||||
return dnd_escape(fmt.Sprintf("t=r:x=%d", idx), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnd_request_file requests individual file data by MIME index and file subindex.
|
|
||||||
func dnd_request_file(mime_idx, file_idx int) string {
|
|
||||||
return dnd_escape(fmt.Sprintf("t=r:x=%d:y=%d", mime_idx, file_idx), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// dnd_close_dir closes a directory handle by sending t=r:Y=handle.
|
|
||||||
func dnd_close_dir(handle int) string {
|
|
||||||
return dnd_escape(fmt.Sprintf("t=r:Y=%d", handle), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnd_finish() string {
|
|
||||||
return dnd_escape("t=r", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// file_info holds metadata about a dropped file.
|
// file_info holds metadata about a dropped file.
|
||||||
type file_info struct {
|
type file_info struct {
|
||||||
name string
|
name string
|
||||||
@@ -258,8 +186,6 @@ func Run(args []string) (rc int, err error) {
|
|||||||
var current_mouse_event *loop.MouseEvent
|
var current_mouse_event *loop.MouseEvent
|
||||||
var dnd dnd_state
|
var dnd dnd_state
|
||||||
|
|
||||||
machine_id := get_machine_id()
|
|
||||||
|
|
||||||
// build_box_lines computes the drop box content lines based on current state.
|
// build_box_lines computes the drop box content lines based on current state.
|
||||||
build_box_lines := func() []string {
|
build_box_lines := func() []string {
|
||||||
if dnd.drag_over_box {
|
if dnd.drag_over_box {
|
||||||
@@ -428,7 +354,11 @@ func Run(args []string) (rc int, err error) {
|
|||||||
// Request this file via the protocol.
|
// Request this file via the protocol.
|
||||||
dnd.file_read_size = 0
|
dnd.file_read_size = 0
|
||||||
dnd.collecting = "file"
|
dnd.collecting = "file"
|
||||||
lp.QueueWriteString(dnd_request_file(dnd.uri_list_mime_idx, dnd.file_read_idx+1))
|
lp.QueueDnDData(map[string]string{
|
||||||
|
"t": "r",
|
||||||
|
"x": strconv.Itoa(dnd.uri_list_mime_idx),
|
||||||
|
"y": strconv.Itoa(dnd.file_read_idx + 1),
|
||||||
|
}, "", false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Non-file URI: record as-is with no size info.
|
// Non-file URI: record as-is with no size info.
|
||||||
@@ -437,50 +367,30 @@ func Run(args []string) (rc int, err error) {
|
|||||||
}
|
}
|
||||||
// All files processed; finish the drop.
|
// All files processed; finish the drop.
|
||||||
dnd.collecting = ""
|
dnd.collecting = ""
|
||||||
lp.QueueWriteString(dnd_finish())
|
lp.QueueDnDData(map[string]string{"t": "r"}, "", false)
|
||||||
dnd.has_drop_data = true
|
dnd.has_drop_data = true
|
||||||
draw_screen()
|
draw_screen()
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_dnd_osc := func(raw []byte) error {
|
handle_dnd_cmd := func(cmd loop.DndCommand) error {
|
||||||
// raw is the OSC payload after ESC ] and before ST.
|
switch cmd.Type {
|
||||||
// Format: DND_CODE;metadata[;payload]
|
case 'm':
|
||||||
prefix := fmt.Sprintf("%d;", kitty.DndCode)
|
|
||||||
if !bytes.HasPrefix(raw, []byte(prefix)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
rest := string(raw[len(prefix):])
|
|
||||||
// Split into metadata and optional payload.
|
|
||||||
meta, payload, _ := strings.Cut(rest, ";")
|
|
||||||
// Parse metadata key=value pairs separated by ':'.
|
|
||||||
meta_map := make(map[string]string)
|
|
||||||
for kv := range strings.SplitSeq(meta, ":") {
|
|
||||||
k, v, _ := strings.Cut(kv, "=")
|
|
||||||
if k != "" {
|
|
||||||
meta_map[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t := meta_map["t"]
|
|
||||||
switch t {
|
|
||||||
case "m":
|
|
||||||
// Drag move event from terminal.
|
// Drag move event from terminal.
|
||||||
// Check if drag has left the window (x=-1, y=-1).
|
// Check if drag has left the window (x=-1, y=-1).
|
||||||
if meta_map["x"] == "-1" || meta_map["y"] == "-1" {
|
if cmd.X == -1 || cmd.Y == -1 {
|
||||||
dnd.reset_drag()
|
dnd.reset_drag()
|
||||||
draw_screen()
|
draw_screen()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
mimes := strings.Fields(payload)
|
mimes := strings.Fields(string(cmd.Payload))
|
||||||
if len(mimes) > 0 {
|
if len(mimes) > 0 {
|
||||||
dnd.drag_mimes = mimes
|
dnd.drag_mimes = mimes
|
||||||
}
|
}
|
||||||
dnd.dragging = true
|
dnd.dragging = true
|
||||||
cx, _ := strconv.Atoi(meta_map["x"])
|
dnd.drag_cell_x = cmd.X
|
||||||
cy, _ := strconv.Atoi(meta_map["y"])
|
dnd.drag_cell_y = cmd.Y
|
||||||
dnd.drag_cell_x = cx
|
|
||||||
dnd.drag_cell_y = cy
|
|
||||||
|
|
||||||
over_box := dnd.is_over_drop_box(cx, cy)
|
over_box := dnd.is_over_drop_box(cmd.X, cmd.Y)
|
||||||
dnd.drag_over_box = over_box
|
dnd.drag_over_box = over_box
|
||||||
|
|
||||||
if over_box {
|
if over_box {
|
||||||
@@ -492,18 +402,18 @@ func Run(args []string) (rc int, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(accepted_mimes) > 0 {
|
if len(accepted_mimes) > 0 {
|
||||||
lp.QueueWriteString(dnd_accept_drag(strings.Join(accepted_mimes, " ")))
|
lp.QueueDnDData(map[string]string{"t": "m", "o": "1"}, strings.Join(accepted_mimes, " "), false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not over drop region; reject the drag.
|
// Not over drop region; reject the drag.
|
||||||
lp.QueueWriteString(dnd_reject_drag())
|
lp.QueueDnDData(map[string]string{"t": "m", "o": "0"}, "", false)
|
||||||
}
|
}
|
||||||
draw_screen()
|
draw_screen()
|
||||||
case "M":
|
case 'M':
|
||||||
// Drop event from terminal.
|
// Drop event from terminal.
|
||||||
dnd.reset_drag()
|
dnd.reset_drag()
|
||||||
dnd.reset_drop_data()
|
dnd.reset_drop_data()
|
||||||
mimes := strings.Fields(payload)
|
mimes := strings.Fields(string(cmd.Payload))
|
||||||
dnd.drop_mimes = mimes
|
dnd.drop_mimes = mimes
|
||||||
|
|
||||||
// Find the MIME indices we need.
|
// Find the MIME indices we need.
|
||||||
@@ -517,35 +427,32 @@ func Run(args []string) (rc int, err error) {
|
|||||||
for idx, m := range mimes {
|
for idx, m := range mimes {
|
||||||
if m == "text/plain" {
|
if m == "text/plain" {
|
||||||
dnd.collecting = "text/plain"
|
dnd.collecting = "text/plain"
|
||||||
lp.QueueWriteString(dnd_request_mime_data(idx + 1))
|
lp.QueueDnDData(map[string]string{"t": "r", "x": strconv.Itoa(idx + 1)}, "", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dnd.uri_list_mime_idx > 0 {
|
if dnd.uri_list_mime_idx > 0 {
|
||||||
dnd.collecting = "text/uri-list"
|
dnd.collecting = "text/uri-list"
|
||||||
lp.QueueWriteString(dnd_request_mime_data(dnd.uri_list_mime_idx))
|
lp.QueueDnDData(map[string]string{"t": "r", "x": strconv.Itoa(dnd.uri_list_mime_idx)}, "", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Nothing to collect; signal done.
|
// Nothing to collect; signal done.
|
||||||
lp.QueueWriteString(dnd_finish())
|
lp.QueueDnDData(map[string]string{"t": "r"}, "", false)
|
||||||
dnd.has_drop_data = true
|
dnd.has_drop_data = true
|
||||||
draw_screen()
|
draw_screen()
|
||||||
case "r":
|
case 'r':
|
||||||
// Data response from terminal.
|
// Data response from terminal.
|
||||||
resp_y, _ := strconv.Atoi(meta_map["y"])
|
is_file_response := cmd.Y != 0
|
||||||
resp_X, _ := strconv.Atoi(meta_map["X"])
|
|
||||||
|
|
||||||
is_file_response := resp_y != 0
|
|
||||||
if is_file_response {
|
if is_file_response {
|
||||||
// Response for an individual file request (t=r:x=idx:y=subidx).
|
// Response for an individual file request (t=r:x=idx:y=subidx).
|
||||||
if payload == "" {
|
if !cmd.Has_more && len(cmd.Payload) == 0 {
|
||||||
// End of file data.
|
// End of file data.
|
||||||
fi := file_info{}
|
fi := file_info{}
|
||||||
if resp_X > 1 {
|
if cmd.Xp > 1 {
|
||||||
// Directory: close the handle.
|
// Directory: close the handle.
|
||||||
fi.is_dir = true
|
fi.is_dir = true
|
||||||
lp.QueueWriteString(dnd_close_dir(resp_X))
|
lp.QueueDnDData(map[string]string{"t": "r", "Y": strconv.Itoa(cmd.Xp)}, "", false)
|
||||||
} else if resp_X == 1 {
|
} else if cmd.Xp == 1 {
|
||||||
fi.is_link = true
|
fi.is_link = true
|
||||||
fi.size = dnd.file_read_size
|
fi.size = dnd.file_read_size
|
||||||
} else {
|
} else {
|
||||||
@@ -556,7 +463,7 @@ func Run(args []string) (rc int, err error) {
|
|||||||
draw_screen()
|
draw_screen()
|
||||||
start_next_file_request()
|
start_next_file_request()
|
||||||
} else {
|
} else {
|
||||||
decoded, err := base64.RawStdEncoding.DecodeString(payload)
|
decoded, err := base64.RawStdEncoding.DecodeString(string(cmd.Payload))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dnd.file_read_size += int64(len(decoded))
|
dnd.file_read_size += int64(len(decoded))
|
||||||
}
|
}
|
||||||
@@ -565,7 +472,7 @@ func Run(args []string) (rc int, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Response for a MIME type data request.
|
// Response for a MIME type data request.
|
||||||
if payload == "" {
|
if !cmd.Has_more && len(cmd.Payload) == 0 {
|
||||||
// End of MIME type data.
|
// End of MIME type data.
|
||||||
switch dnd.collecting {
|
switch dnd.collecting {
|
||||||
case "text/plain":
|
case "text/plain":
|
||||||
@@ -580,14 +487,14 @@ func Run(args []string) (rc int, err error) {
|
|||||||
// Now request text/uri-list if available.
|
// Now request text/uri-list if available.
|
||||||
if dnd.uri_list_mime_idx > 0 {
|
if dnd.uri_list_mime_idx > 0 {
|
||||||
dnd.collecting = "text/uri-list"
|
dnd.collecting = "text/uri-list"
|
||||||
lp.QueueWriteString(dnd_request_mime_data(dnd.uri_list_mime_idx))
|
lp.QueueDnDData(map[string]string{"t": "r", "x": strconv.Itoa(dnd.uri_list_mime_idx)}, "", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case "text/uri-list":
|
case "text/uri-list":
|
||||||
text := dnd.collect_buf.String()
|
text := dnd.collect_buf.String()
|
||||||
dnd.collect_buf.Reset()
|
dnd.collect_buf.Reset()
|
||||||
// Check if terminal indicated remote files (X=1 in URI list response).
|
// Check if terminal indicated remote files (X=1 in URI list response).
|
||||||
if resp_X == 1 {
|
if cmd.Xp == 1 {
|
||||||
dnd.is_remote = true
|
dnd.is_remote = true
|
||||||
}
|
}
|
||||||
// Parse URI list: lines starting with # are comments.
|
// Parse URI list: lines starting with # are comments.
|
||||||
@@ -605,33 +512,32 @@ func Run(args []string) (rc int, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dnd.collecting = ""
|
dnd.collecting = ""
|
||||||
lp.QueueWriteString(dnd_finish())
|
lp.QueueDnDData(map[string]string{"t": "r"}, "", false)
|
||||||
dnd.has_drop_data = true
|
dnd.has_drop_data = true
|
||||||
draw_screen()
|
draw_screen()
|
||||||
} else {
|
} else {
|
||||||
decoded, err := base64.RawStdEncoding.DecodeString(payload)
|
decoded, err := base64.RawStdEncoding.DecodeString(string(cmd.Payload))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dnd.collect_buf.Write(decoded)
|
dnd.collect_buf.Write(decoded)
|
||||||
// Capture X from URI list response (may be in first chunk).
|
// Capture X from URI list response (may be in first chunk).
|
||||||
if dnd.collecting == "text/uri-list" && resp_X != 0 {
|
if dnd.collecting == "text/uri-list" && cmd.Xp != 0 {
|
||||||
dnd.is_remote = resp_X == 1
|
dnd.is_remote = cmd.Xp == 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "R":
|
case 'R':
|
||||||
// Error response from terminal.
|
// Error response from terminal.
|
||||||
resp_y, _ := strconv.Atoi(meta_map["y"])
|
is_file_response := cmd.Y != 0
|
||||||
is_file_response := resp_y != 0
|
|
||||||
if is_file_response && dnd.collecting == "file" {
|
if is_file_response && dnd.collecting == "file" {
|
||||||
// Record the error for this file.
|
// Record the error for this file.
|
||||||
dnd.file_infos = append(dnd.file_infos, file_info{err_msg: payload})
|
dnd.file_infos = append(dnd.file_infos, file_info{err_msg: string(cmd.Payload)})
|
||||||
dnd.file_read_idx++
|
dnd.file_read_idx++
|
||||||
draw_screen()
|
draw_screen()
|
||||||
start_next_file_request()
|
start_next_file_request()
|
||||||
} else if !is_file_response {
|
} else if !is_file_response {
|
||||||
// Error getting MIME data; finish the drop with what we have.
|
// Error getting MIME data; finish the drop with what we have.
|
||||||
dnd.collecting = ""
|
dnd.collecting = ""
|
||||||
lp.QueueWriteString(dnd_finish())
|
lp.QueueDnDData(map[string]string{"t": "r"}, "", false)
|
||||||
dnd.has_drop_data = true
|
dnd.has_drop_data = true
|
||||||
draw_screen()
|
draw_screen()
|
||||||
}
|
}
|
||||||
@@ -642,13 +548,14 @@ func Run(args []string) (rc int, err error) {
|
|||||||
lp.OnInitialize = func() (string, error) {
|
lp.OnInitialize = func() (string, error) {
|
||||||
lp.SetWindowTitle("kitty mouse features demo")
|
lp.SetWindowTitle("kitty mouse features demo")
|
||||||
lp.SetCursorVisible(false)
|
lp.SetCursorVisible(false)
|
||||||
lp.QueueWriteString(dnd_start_accepting(machine_id))
|
lp.StartAcceptingDrops("text/plain", "text/uri-list")
|
||||||
draw_screen()
|
draw_screen()
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
lp.OnFinalize = func() string {
|
lp.OnFinalize = func() string {
|
||||||
lp.SetCursorVisible(true)
|
lp.SetCursorVisible(true)
|
||||||
return dnd_stop_accepting()
|
lp.StopAcceptingDrops()
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
lp.OnMouseEvent = func(ev *loop.MouseEvent) error {
|
lp.OnMouseEvent = func(ev *loop.MouseEvent) error {
|
||||||
@@ -666,12 +573,7 @@ func Run(args []string) (rc int, err error) {
|
|||||||
draw_screen()
|
draw_screen()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
lp.OnEscapeCode = func(etype loop.EscapeCodeType, raw []byte) error {
|
lp.OnDnDData = handle_dnd_cmd
|
||||||
if etype == loop.OSC {
|
|
||||||
return handle_dnd_osc(raw)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = lp.Run()
|
err = lp.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rc = 1
|
rc = 1
|
||||||
|
|||||||
@@ -673,13 +673,10 @@ func (self *Loop) QueueDnDData(metadata map[string]string, payload string, as_ba
|
|||||||
b := strings.Builder{}
|
b := strings.Builder{}
|
||||||
b.Grow(64)
|
b.Grow(64)
|
||||||
fmt.Fprintf(&b, "\x1b]%d;", kitty.DndCode)
|
fmt.Fprintf(&b, "\x1b]%d;", kitty.DndCode)
|
||||||
is_first := false
|
sep := ""
|
||||||
for key, val := range metadata {
|
for key, val := range metadata {
|
||||||
if !is_first {
|
fmt.Fprintf(&b, "%s%s=%s", sep, key, val)
|
||||||
b.WriteString(":")
|
sep = ":"
|
||||||
}
|
|
||||||
is_first = false
|
|
||||||
fmt.Fprintf(&b, "%s=%s", key, val)
|
|
||||||
}
|
}
|
||||||
payload_sz := len(payload)
|
payload_sz := len(payload)
|
||||||
if payload_sz == 0 {
|
if payload_sz == 0 {
|
||||||
@@ -699,7 +696,7 @@ func (self *Loop) QueueDnDData(metadata map[string]string, payload string, as_ba
|
|||||||
is_last := end >= len(payload)
|
is_last := end >= len(payload)
|
||||||
end = min(end, len(payload))
|
end = min(end, len(payload))
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fmt.Fprintf(&b, "m=%d;", utils.IfElse(is_last, 0, 1))
|
fmt.Fprintf(&b, "%sm=%d;", sep, utils.IfElse(is_last, 0, 1))
|
||||||
self.QueueWriteString(b.String())
|
self.QueueWriteString(b.String())
|
||||||
} else {
|
} else {
|
||||||
self.QueueWriteString(fmt.Sprintf("\x1b]%d;m=%d;", kitty.DndCode, utils.IfElse(is_last, 0, 1)))
|
self.QueueWriteString(fmt.Sprintf("\x1b]%d;m=%d;", kitty.DndCode, utils.IfElse(is_last, 0, 1)))
|
||||||
|
|||||||
Reference in New Issue
Block a user