mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
More work on the dnd kitten
This commit is contained in:
@@ -13,7 +13,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/kovidgoyal/kitty/tools/cli"
|
"github.com/kovidgoyal/kitty/tools/cli"
|
||||||
"github.com/kovidgoyal/kitty/tools/tty"
|
"github.com/kovidgoyal/kitty/tools/tty"
|
||||||
@@ -141,16 +143,122 @@ func truncate_at_space(text string, width int) (string, string) {
|
|||||||
return text[:p], text[p:]
|
return text[:p], text[p:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dir_handle struct {
|
||||||
|
handle *os.File
|
||||||
|
refcnt int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func new_dir_handle(x *os.File) *dir_handle {
|
||||||
|
return &dir_handle{x, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dir_handle) newref() *dir_handle {
|
||||||
|
atomic.AddInt32(&d.refcnt, 1)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dir_handle) unref() *dir_handle {
|
||||||
|
if atomic.AddInt32(&d.refcnt, -1) <= 0 {
|
||||||
|
d.handle.Close()
|
||||||
|
d.handle = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type remote_dir_entry struct {
|
||||||
|
base_dir *dir_handle
|
||||||
|
name string
|
||||||
|
item_type int
|
||||||
|
children []*remote_dir_entry
|
||||||
|
num_children_finished int
|
||||||
|
|
||||||
|
dest io.WriteCloser
|
||||||
|
b64_decoder streaming_base64.StreamingBase64Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_case_sensitive_filesystem bool = true
|
||||||
|
|
||||||
|
const case_conflict_template = "case-conflict-%d-%s"
|
||||||
|
|
||||||
|
func uniqify_child_names(names []string) []string {
|
||||||
|
if !is_case_sensitive_filesystem {
|
||||||
|
seen := utils.NewSet[string](len(names))
|
||||||
|
for i, x := range names {
|
||||||
|
name := x
|
||||||
|
key := strings.ToLower(name)
|
||||||
|
for q := 0; seen.Has(key); q++ {
|
||||||
|
name = fmt.Sprintf(case_conflict_template, q+1, x)
|
||||||
|
key = strings.ToLower(name)
|
||||||
|
}
|
||||||
|
seen.Add(key)
|
||||||
|
names[i] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *remote_dir_entry) add_remote_data(data []byte, output_buf []byte, has_more bool, parent *remote_dir_entry) error {
|
||||||
|
if len(data) > 0 {
|
||||||
|
for chunk, derr := range d.b64_decoder.Decode(data, output_buf) {
|
||||||
|
if derr != nil {
|
||||||
|
return derr
|
||||||
|
}
|
||||||
|
if _, derr = d.dest.Write(chunk); derr != nil {
|
||||||
|
return derr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !has_more {
|
||||||
|
if chunk, derr := d.b64_decoder.Finish(); derr != nil {
|
||||||
|
return derr
|
||||||
|
} else {
|
||||||
|
if _, derr = d.dest.Write(chunk); derr != nil {
|
||||||
|
return derr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
d.dest.Close()
|
||||||
|
d.dest = nil
|
||||||
|
d.base_dir = d.base_dir.unref()
|
||||||
|
parent.num_children_finished++
|
||||||
|
}()
|
||||||
|
if dest, ok := d.dest.(*bufferWriteCloser); ok {
|
||||||
|
if d.item_type == 1 {
|
||||||
|
if derr := utils.SymlinkAt(d.base_dir.handle, d.name, dest.String()); derr != nil {
|
||||||
|
return derr
|
||||||
|
}
|
||||||
|
} else { // directory
|
||||||
|
if derr := utils.MkdirAt(d.base_dir.handle, d.name, 0o755); derr != nil {
|
||||||
|
return derr
|
||||||
|
}
|
||||||
|
if f, derr := utils.OpenAt(d.base_dir.handle, d.name); derr != nil {
|
||||||
|
return derr
|
||||||
|
} else {
|
||||||
|
handle := new_dir_handle(f)
|
||||||
|
defer handle.unref()
|
||||||
|
s := utils.NewSeparatorScanner("", "\x00")
|
||||||
|
for _, name := range uniqify_child_names(s.Split(dest.String())) {
|
||||||
|
d.children = append(d.children, &remote_dir_entry{name: name, base_dir: handle.newref()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type drop_status struct {
|
type drop_status struct {
|
||||||
offered_mimes []string
|
offered_mimes []string
|
||||||
accepted_mimes []string
|
accepted_mimes []string
|
||||||
uri_list []string
|
uri_list []string
|
||||||
cell_x, cell_y int
|
cell_x, cell_y int
|
||||||
action int
|
action int
|
||||||
in_window bool
|
in_window bool
|
||||||
reading_data bool
|
reading_data bool
|
||||||
is_remote_client bool
|
is_remote_client bool
|
||||||
remote_phase_started bool
|
|
||||||
|
root_remote_dir *remote_dir_entry
|
||||||
|
open_remote_dir *remote_dir_entry
|
||||||
|
pending_remote_dirs []*remote_dir_entry
|
||||||
}
|
}
|
||||||
|
|
||||||
func paragraph_as_lines(text string, width int) (ans []string) {
|
func paragraph_as_lines(text string, width int) (ans []string) {
|
||||||
@@ -183,6 +291,42 @@ func parse_uri_list(src string) (ans []string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[string]drag_source, uri_list_buffer *bytes.Buffer) (err error) {
|
func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[string]drag_source, uri_list_buffer *bytes.Buffer) (err error) {
|
||||||
|
base_dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
base_tdir, err := os.MkdirTemp(base_dir, ".dnd-kitten-drop-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var base_tdir_f *os.File
|
||||||
|
defer func() {
|
||||||
|
if base_tdir_f != nil {
|
||||||
|
utils.RemoveChildren(base_tdir_f)
|
||||||
|
base_tdir_f.Close()
|
||||||
|
}
|
||||||
|
if terr := os.RemoveAll(base_tdir); terr != nil && err == nil {
|
||||||
|
err = terr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
base_tdir_f, err = os.Open(base_tdir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, serr := os.Stat(filepath.Join(base_dir, strings.ToUpper(filepath.Base(base_tdir)))); serr == nil {
|
||||||
|
is_case_sensitive_filesystem = false
|
||||||
|
}
|
||||||
|
tdir_counter := 0
|
||||||
|
new_tdir := func() (dir_file *os.File, err error) {
|
||||||
|
tdir_counter++
|
||||||
|
name := strconv.Itoa(tdir_counter)
|
||||||
|
if err = utils.MkdirAt(base_tdir_f, name, 0o700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dir_file, err = utils.OpenAt(base_tdir_f, name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
allow_drops, allow_drags := len(drop_dests) > 0, len(drag_sources) > 0
|
allow_drops, allow_drags := len(drop_dests) > 0, len(drag_sources) > 0
|
||||||
data_has_been_dropped := false
|
data_has_been_dropped := false
|
||||||
drag_started := false
|
drag_started := false
|
||||||
@@ -315,8 +459,22 @@ func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[
|
|||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
// Drop handling {{{
|
// Drop handling {{{
|
||||||
|
var close_remote_tree func(*remote_dir_entry)
|
||||||
|
close_remote_tree = func(root *remote_dir_entry) {
|
||||||
|
if root.base_dir != nil {
|
||||||
|
root.base_dir = root.base_dir.unref()
|
||||||
|
}
|
||||||
|
for _, child := range root.children {
|
||||||
|
close_remote_tree(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
end_drop := func() {
|
end_drop := func() {
|
||||||
lp.QueueDnDData(DC{Type: 'r'}) // end drop
|
lp.QueueDnDData(DC{Type: 'r'}) // end drop
|
||||||
|
if drop_status.root_remote_dir != nil {
|
||||||
|
close_remote_tree(drop_status.root_remote_dir)
|
||||||
|
drop_status.root_remote_dir = nil
|
||||||
|
}
|
||||||
drop_status = reset_drop_status
|
drop_status = reset_drop_status
|
||||||
render_screen()
|
render_screen()
|
||||||
}
|
}
|
||||||
@@ -333,8 +491,36 @@ func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[
|
|||||||
render_screen()
|
render_screen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
f, err := new_tdir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rd := new_dir_handle(f)
|
||||||
|
defer rd.unref()
|
||||||
|
drop_status.root_remote_dir = &remote_dir_entry{}
|
||||||
if drop_status.is_remote_client {
|
if drop_status.is_remote_client {
|
||||||
// TODO: Handle remote client
|
seen := utils.NewSet[string](len(drop_status.uri_list))
|
||||||
|
idx := slices.Index(drop_status.offered_mimes, "text/uri-list")
|
||||||
|
for i, x := range drop_status.uri_list {
|
||||||
|
var c *remote_dir_entry
|
||||||
|
if x == "" {
|
||||||
|
c = &remote_dir_entry{}
|
||||||
|
} else {
|
||||||
|
name := filepath.Base(x)
|
||||||
|
if !is_case_sensitive_filesystem {
|
||||||
|
key := strings.ToLower(name)
|
||||||
|
for q := 0; seen.Has(key); q++ {
|
||||||
|
name = fmt.Sprintf(case_conflict_template, q+1, filepath.Base(x))
|
||||||
|
key = strings.ToLower(name)
|
||||||
|
}
|
||||||
|
seen.Add(key)
|
||||||
|
}
|
||||||
|
c = &remote_dir_entry{base_dir: rd.newref(), name: name}
|
||||||
|
lp.QueueDnDData(DC{Type: 'r', X: idx + 1, Y: i + 1})
|
||||||
|
}
|
||||||
|
drop_status.root_remote_dir.children = append(drop_status.root_remote_dir.children, c)
|
||||||
|
}
|
||||||
|
drop_status.open_remote_dir = drop_status.root_remote_dir
|
||||||
} else {
|
} else {
|
||||||
// TODO: copy URLs
|
// TODO: copy URLs
|
||||||
}
|
}
|
||||||
@@ -411,15 +597,48 @@ func run_loop(opts *Options, drop_dests map[string]*drop_dest, drag_sources map[
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var current_remote_entry *remote_dir_entry
|
||||||
|
var drop_buf []byte
|
||||||
|
|
||||||
on_remote_drop_data := func(cmd DC) error {
|
on_remote_drop_data := func(cmd DC) error {
|
||||||
// TODO: Implement this
|
if drop_status.open_remote_dir == nil {
|
||||||
|
return fmt.Errorf("got a remote data response form the terminal without an open remote dir")
|
||||||
|
}
|
||||||
|
if cmd.X == 0 && cmd.Y == 0 && cmd.Yp == 0 {
|
||||||
|
if current_remote_entry == nil {
|
||||||
|
return fmt.Errorf("got a remote data response form the terminal without a current remote entry")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
num := utils.IfElse(cmd.Yp != 0 && cmd.Yp != 1, cmd.X, cmd.Y) - 1
|
||||||
|
if num < 0 || num >= len(drop_status.open_remote_dir.children) {
|
||||||
|
return fmt.Errorf("got a remote data response from the terminal for an entry that does not exist")
|
||||||
|
}
|
||||||
|
current_remote_entry = drop_status.open_remote_dir.children[num]
|
||||||
|
}
|
||||||
|
if current_remote_entry.dest == nil {
|
||||||
|
current_remote_entry.item_type = cmd.Xp
|
||||||
|
switch cmd.Xp {
|
||||||
|
case 0:
|
||||||
|
f, err := utils.CreateAt(drop_status.open_remote_dir.base_dir.handle, current_remote_entry.name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current_remote_entry.dest = f
|
||||||
|
default:
|
||||||
|
current_remote_entry.dest = &bufferWriteCloser{&bytes.Buffer{}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz {
|
||||||
|
drop_buf = make([]byte, sz)
|
||||||
|
}
|
||||||
|
if err = current_remote_entry.add_remote_data(cmd.Payload, drop_buf, cmd.Has_more, drop_status.open_remote_dir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var drop_buf []byte
|
|
||||||
|
|
||||||
on_drop_data := func(cmd DC) error {
|
on_drop_data := func(cmd DC) error {
|
||||||
if drop_status.remote_phase_started {
|
if drop_status.root_remote_dir != nil {
|
||||||
return on_remote_drop_data(cmd)
|
return on_remote_drop_data(cmd)
|
||||||
}
|
}
|
||||||
idx := cmd.X - 1
|
idx := cmd.X - 1
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ func OpenAt(dirFile *os.File, name string) (*os.File, error) {
|
|||||||
return openAt(dirFile, name, os.O_RDONLY, 0)
|
return openAt(dirFile, name, os.O_RDONLY, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a symlink named name in the directory pointed to by dirFile. The
|
||||||
|
// target of the symlink is set to target
|
||||||
|
func SymlinkAt(dirFile *os.File, name, target string) error {
|
||||||
|
return unix.Symlinkat(target, int(dirFile.Fd()), name)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateAt creates or truncates a file relative to the directory pointed to by dirFile.
|
// CreateAt creates or truncates a file relative to the directory pointed to by dirFile.
|
||||||
// Matches the behavior of os.Create (read-write, creates if doesn't exist, truncates).
|
// Matches the behavior of os.Create (read-write, creates if doesn't exist, truncates).
|
||||||
func CreateAt(dirFile *os.File, name string) (*os.File, error) {
|
func CreateAt(dirFile *os.File, name string) (*os.File, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user