Compare commits

...

9 commits

Author SHA1 Message Date
Kovid Goyal
f4e9824e18
Fix off by one in if condition
Some checks are pending
CI / Linux (python=3.13 cc=clang sanitize=1) (push) Waiting to run
CI / Linux (python=3.11 cc=gcc sanitize=0) (push) Waiting to run
CI / Linux (python=3.12 cc=gcc sanitize=1) (push) Waiting to run
CI / Linux package (push) Waiting to run
CI / Bundle test (macos-latest) (push) Waiting to run
CI / Bundle test (ubuntu-latest) (push) Waiting to run
CI / macOS Brew (push) Waiting to run
CI / Test ./dev.sh and benchmark (push) Waiting to run
CodeQL / CodeQL-Build (actions, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, macos-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (go, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (python, ubuntu-latest) (push) Waiting to run
Depscan / Scan dependencies for vulnerabilities (push) Waiting to run
2026-05-11 22:06:32 +05:30
Kovid Goyal
02cfa89bae
... 2026-05-11 21:59:45 +05:30
Kovid Goyal
634f13e65f
More work on dnd kitten 2026-05-11 21:51:23 +05:30
Kovid Goyal
43b028bd6a
Finish terminal side port of new dnd sub protocol 2026-05-11 13:39:43 +05:30
Kovid Goyal
114f1ff128
Merge branch 'dependabot/github_actions/actions-937d73b4db' of https://github.com/kovidgoyal/kitty 2026-05-11 09:35:58 +05:30
Kovid Goyal
845aeeffd9
Merge branch 'dependabot/go_modules/all-go-deps-c79d8bb26a' of https://github.com/kovidgoyal/kitty 2026-05-11 09:35:30 +05:30
dependabot[bot]
9993f82d64
Bump github/codeql-action from 4.35.2 to 4.35.3 in the actions group
Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.35.2 to 4.35.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.35.2...v4.35.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 03:52:22 +00:00
dependabot[bot]
15f71ebd37
Bump the all-go-deps group with 4 updates
Bumps the all-go-deps group with 4 updates: [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma), [github.com/klauspost/compress](https://github.com/klauspost/compress), [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) and [golang.org/x/sys](https://github.com/golang/sys).


Updates `github.com/alecthomas/chroma/v2` from 2.23.1 to 2.24.1
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.23.1...v2.24.1)

Updates `github.com/klauspost/compress` from 1.18.5 to 1.18.6
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.5...v1.18.6)

Updates `github.com/shirou/gopsutil/v4` from 4.26.3 to 4.26.4
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.26.3...v4.26.4)

Updates `golang.org/x/sys` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/sys/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.24.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-go-deps
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-go-deps
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-version: 4.26.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-go-deps
- dependency-name: golang.org/x/sys
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-go-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 03:48:34 +00:00
Kovid Goyal
07ec007388
Start work on porting code to new remote drag source protocol 2026-05-10 05:04:46 +05:30
10 changed files with 312 additions and 257 deletions

View file

@ -54,7 +54,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.35.2
uses: github/codeql-action/init@v4.35.3
with:
languages: ${{ matrix.language }}
trap-caching: false
@ -64,7 +64,7 @@ jobs:
run: python3 .github/workflows/ci.py build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.35.2
uses: github/codeql-action/analyze@v4.35.3
- name: Run govulncheck
if: matrix.language == 'go'

View file

@ -391,7 +391,7 @@ examine the :ref:`machine_id` sent with the enable drag offers
from the URI list. To request data for a particular entry, terminals send an
escape code of the form::
OSC _dnd_code ; t=k:x=idx ; base64 encoded file data ST
OSC _dnd_code ; t=k:x=idx ST
Here ``idx`` is the one based index into the list of entries in the
``text/uri-list`` MIME type. Then the client can respond with the data
@ -419,11 +419,6 @@ adding ``Y=parent-handle:y=num`` to the escape codes above. Here
is the one based index into the list of entries in the directory. Thus, the
set of keys ``x, y, Y`` uniquely determine an entry.
Once all data for a dirctory is transmitted, the client informs the terminal emulator of
completion with::
OSC _dnd_code ; t=k:Y=handle ; ST
If any error occurs in the client while reading the data, it can inform
the terminal using::

8
go.mod
View file

@ -6,7 +6,7 @@ toolchain go1.26.3
require (
github.com/ALTree/bigfloat v0.2.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/alecthomas/chroma/v2 v2.24.1
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/dlclark/regexp2 v1.12.0
github.com/ebitengine/purego v0.10.0
@ -14,7 +14,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/klauspost/compress v1.18.5
github.com/klauspost/compress v1.18.6
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
github.com/kovidgoyal/go-parallel v1.1.1
github.com/kovidgoyal/go-shm v1.0.0
@ -22,12 +22,12 @@ require (
github.com/nwaples/rardecode/v2 v2.2.2
github.com/seancfoley/ipaddress-go v1.7.1
github.com/sgtdi/fswatcher v1.2.0
github.com/shirou/gopsutil/v4 v4.26.3
github.com/shirou/gopsutil/v4 v4.26.4
github.com/ulikunitz/xz v0.5.15
github.com/zeebo/xxh3 v1.1.0
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/image v0.39.0
golang.org/x/sys v0.43.0
golang.org/x/sys v0.44.0
golang.org/x/text v0.36.0
howett.net/plist v1.0.1
)

16
go.sum
View file

@ -2,8 +2,8 @@ github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
@ -28,8 +28,8 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4=
@ -56,8 +56,8 @@ github.com/seancfoley/ipaddress-go v1.7.1 h1:fDWryS+L8iaaH5RxIKbY0xB5Z+Zxk8xoXLN
github.com/seancfoley/ipaddress-go v1.7.1/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
github.com/sgtdi/fswatcher v1.2.0 h1:uSJuMc3/Eo/vaPnZWpJ42EFYb5j38cZENmkszOV0yhw=
github.com/sgtdi/fswatcher v1.2.0/go.mod h1:smzXnaqu0SYJQNIwGLLkvRkpH4RdEACB7avMSsSaqjQ=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
@ -78,8 +78,8 @@ golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -22,11 +22,10 @@ import (
var _ = fmt.Print
type data_request struct {
drag_source *drag_source
send_remote_data bool
index int
write_id loop.IdType
base64 streaming_base64.StreamingBase64Encoder
drag_source *drag_source
index int
write_id loop.IdType
base64 streaming_base64.StreamingBase64Encoder
}
type remote_data_item struct {
@ -51,7 +50,7 @@ type drag_status struct {
current_remote_file *remote_data_item
dir_handle_counter int
remote_item_write_id loop.IdType
remote_data_was_sent bool
remote_data_requests []int
}
func find_drag_image(drag_sources map[string]*drag_source) image.Image {
@ -66,12 +65,10 @@ func find_drag_image(drag_sources map[string]*drag_source) image.Image {
}
}
var uri_list []string
if ds := drag_sources["text/uri-list"]; ds != nil && len(ds.data) > 0 {
if q, err := parse_uri_list(string(ds.data)); err == nil {
for _, path := range q {
if path != "" {
uri_list = append(uri_list, path)
}
if ds := drag_sources["text/uri-list"]; ds != nil {
for _, e := range ds.uri_list {
if e.path != "" {
uri_list = append(uri_list, e.path)
}
}
}
@ -102,12 +99,10 @@ func (dnd *dnd) set_drag_image_text() (err error) {
}
}
if icon == "" {
if ds := dnd.drag_sources["text/uri-list"]; ds != nil && len(ds.data) > 0 {
if q, err := parse_uri_list(string(ds.data)); err == nil {
for _, path := range q {
if path != "" && from_path(path) {
break
}
if ds := dnd.drag_sources["text/uri-list"]; ds != nil {
for _, e := range ds.uri_list {
if e.path != "" && from_path(e.path) {
break
}
}
}
@ -219,7 +214,7 @@ func (dnd *dnd) reset_drag() {
dnd.drag_status = drag_status{}
}
func (dnd *dnd) on_drag_event(x, y, operation, Y int) (err error) {
func (dnd *dnd) on_drag_event(x, y, operation int) (err error) {
switch x {
case 1:
dnd.drag_status.accepted_mime = y
@ -230,21 +225,22 @@ func (dnd *dnd) on_drag_event(x, y, operation, Y int) (err error) {
case 4:
was_dropped := dnd.drag_status.dropped
was_move := dnd.drag_status.accepted_operation == 2
was_remote := dnd.drag_status.remote_data_was_sent
dnd.reset_drag()
if was_dropped && dnd.has_exit_on("drag-finish") {
dnd.lp.Quit(0)
}
if was_dropped && was_move && was_remote {
if was_dropped && was_move {
if ds := dnd.drag_sources["text/uri-list"]; ds != nil {
for _, item := range ds.uri_list {
if item.metadata.IsDir() {
err = os.RemoveAll(item.path)
} else {
err = os.Remove(item.path)
}
if err != nil {
return err
if item.was_sent {
if item.metadata.IsDir() {
err = os.RemoveAll(item.path)
} else {
err = os.Remove(item.path)
}
if err != nil {
return err
}
}
}
}
@ -256,7 +252,7 @@ func (dnd *dnd) on_drag_event(x, y, operation, Y int) (err error) {
}
}
case 5:
if err = dnd.handle_data_request(y, Y == 1); err != nil {
if err = dnd.handle_data_request(y); err != nil {
return err
}
}
@ -272,28 +268,24 @@ func (dnd *dnd) finish_drag(errname string) {
dnd.reset_drag()
}
func (dnd *dnd) handle_data_request(idx int, send_remote_data bool) (err error) {
func (dnd *dnd) handle_data_request(idx int) (err error) {
if idx < 0 || idx >= len(dnd.drag_status.offered_mimes) {
dnd.finish_drag("EINVAL")
return fmt.Errorf("terminal asked for drag data from MIME list with out of bounds index: %d", idx)
}
mime := dnd.drag_status.offered_mimes[idx]
ds := dnd.drag_sources[mime]
send_remote_data = send_remote_data && mime == "text/uri-list" && len(ds.uri_list) > 0
for _, dr := range dnd.drag_status.data_requests {
if dr.index == idx {
dnd.finish_drag("EINVAL")
return fmt.Errorf("terminal sent a duplicate drag data request")
}
}
dr := &data_request{drag_source: ds, send_remote_data: send_remote_data, index: idx}
dr := &data_request{drag_source: ds, index: idx}
if ds.path == "" {
dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx, Payload: utils.UnsafeStringToBytes(base64.RawStdEncoding.EncodeToString(ds.data))})
dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx}) // EOF
if !dr.send_remote_data {
return
}
return dnd.start_remote_data_send(ds)
return
} else {
if ds.file != nil {
ds.file.Close()
@ -356,9 +348,7 @@ func (dnd *dnd) on_data_request_finished(i int) (err error) {
dr.drag_source.file = nil
}
dnd.drag_status.data_requests = slices.Delete(dnd.drag_status.data_requests, i, i+1)
if dr.send_remote_data {
err = dnd.start_remote_data_send(dr.drag_source)
} else if len(dnd.drag_status.data_requests) > 0 {
if len(dnd.drag_status.data_requests) > 0 {
err = dnd.send_data_for_data_request(0)
}
return
@ -386,6 +376,9 @@ func (dnd *dnd) send_remote_dir(path string, idx_in_uri_list, parent_dir_handle,
dnd.finish_drag("EIO")
return nil, err
}
for dnd.drag_status.dir_handle_counter < 2 {
dnd.drag_status.dir_handle_counter++
}
handle := dnd.drag_status.dir_handle_counter
dnd.drag_status.dir_handle_counter++
names := make([]string, 0, len(entries))
@ -447,7 +440,11 @@ func (dnd *dnd) send_next_file_chunk() (err error) {
func (dnd *dnd) next_remote_item() (err error) {
if len(dnd.drag_status.remote_items) < 1 {
dnd.lp.QueueDnDData(DC{Type: 'k'}) // inform terminal remote data is finished
// current remote data request finished
dnd.drag_status.remote_data_requests = dnd.drag_status.remote_data_requests[1:]
if len(dnd.drag_status.remote_data_requests) > 0 {
return dnd.send_next_remote_data_request()
}
if len(dnd.drag_status.data_requests) > 0 {
err = dnd.send_data_for_data_request(0)
}
@ -478,26 +475,44 @@ func (dnd *dnd) next_remote_item() (err error) {
return
}
func (dnd *dnd) start_remote_data_send(ds *drag_source) (err error) {
dnd.drag_status.dir_handle_counter = 2
dnd.drag_status.remote_item_write_id = 0
dnd.drag_status.remote_data_was_sent = true
func (dnd *dnd) on_drag_remote_data_request(idx int) (err error) {
ds := dnd.drag_sources["text/uri-list"]
if ds == nil || len(ds.uri_list) < 1 {
dnd.finish_drag("EINVAL")
return fmt.Errorf("terminal asked for drag data from URI list but no list present")
}
if idx < 0 || idx >= len(ds.uri_list) {
dnd.finish_drag("EINVAL")
return fmt.Errorf("terminal asked for drag data from URI list with out of bounds index: %d", idx)
}
ds.uri_list[idx].was_sent = true
dnd.drag_status.remote_data_requests = append(dnd.drag_status.remote_data_requests, idx)
if len(dnd.drag_status.remote_data_requests) == 1 {
err = dnd.send_next_remote_data_request()
}
return
}
func (dnd *dnd) send_next_remote_data_request() (err error) {
if len(dnd.drag_status.remote_data_requests) == 0 {
return nil
}
i := dnd.drag_status.remote_data_requests[0]
x := dnd.drag_sources["text/uri-list"].uri_list[i]
items := []*remote_data_item{}
for i, x := range ds.uri_list {
if x.metadata.IsDir() {
if children, err := dnd.send_remote_dir(x.path, i, 0, i); err != nil {
return err
} else {
items = append(items, children...)
}
} else if x.metadata.Mode().Type()&os.ModeSymlink != 0 {
if err = dnd.send_remote_symlink(x.path, i, 0, i); err != nil {
return err
}
if x.metadata.IsDir() {
if children, err := dnd.send_remote_dir(x.path, i, 0, i); err != nil {
return err
} else {
f := remote_data_item{idx_in_parent: i, idx_in_uri_list: i, metadata: x.metadata, path: x.path}
dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, &f)
items = append(items, children...)
}
} else if x.metadata.Mode().Type()&os.ModeSymlink != 0 {
if err = dnd.send_remote_symlink(x.path, i, 0, i); err != nil {
return err
}
} else {
f := remote_data_item{idx_in_parent: i, idx_in_uri_list: i, metadata: x.metadata, path: x.path}
dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, &f)
}
dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, items...)
if dnd.drag_status.remote_item_write_id == 0 {

View file

@ -31,6 +31,7 @@ type uri_list_item struct {
path, uri, human_name string
file *os.File
metadata os.FileInfo
was_sent bool
}
type drag_source struct {
@ -105,7 +106,7 @@ func (dnd *dnd) send_test_response(payload string) {
}
func (dnd *dnd) has_exit_on(event string) bool {
for _, e := range strings.Split(dnd.opts.ExitOn, ",") {
for e := range strings.SplitSeq(dnd.opts.ExitOn, ",") {
if strings.TrimSpace(e) == event {
return true
}
@ -276,7 +277,9 @@ func (dnd *dnd) run_loop() (err error) {
case 'E':
return dnd.on_drag_error(cmd)
case 'e':
return dnd.on_drag_event(cmd.X, cmd.Y, cmd.Operation, cmd.Yp)
return dnd.on_drag_event(cmd.X, cmd.Y, cmd.Operation)
case 'k':
return dnd.on_drag_remote_data_request(cmd.X - 1)
}
return nil
}

View file

@ -1452,6 +1452,56 @@ expand_png_data(Window *w, size_t idx) {
static size_t last_total_image_size = 0;
static char**
parse_uri_list(Window *w, char *data, const ssize_t sz, size_t *num_uris_out) {
*num_uris_out = 0;
// First pass: count non-comment, non-empty lines
size_t count = 0;
char *p = data;
while (p - data < sz) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
char saved_end = *end; *end = '\0';
if (*p && *p != '#') count++;
*end = saved_end;
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
char **result = calloc((count + 1), sizeof(const char*));
if (!result) { cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL; }
// Second pass: fill in decoded URI strings
size_t idx = 0;
p = data;
while (p - data < sz && idx < count) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
*end = '\0';
if (*p && *p != '#') {
char *decoded = strdup(p);
if (!decoded) {
for (size_t k = 0; k < idx; k++) free((char*)result[k]);
free(result); cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL;
}
result[idx++] = decoded;
}
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
*num_uris_out = idx;
return result;
}
void
drag_start(Window *w) {
if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL, "cannot start drag as drag source is not being built");
@ -1530,6 +1580,13 @@ drag_start(Window *w) {
// Free images and optional_data but keep the items array for later
// data requests from the drop target
for (size_t i = 0; i < ds.num_mimes; i++) {
if (ds.is_remote_client && ds.items[i].is_uri_list) {
if (ds.items[i].optional_data && ds.items[i].data_size) {
ds.items[i].uri_list = parse_uri_list(
w, (char*)ds.items[i].optional_data, ds.items[i].data_size, &ds.items[i].num_uris);
if (!ds.items[i].uri_list) return;
} else abrt(EINVAL, "remote client must pre-send text/uri-list data");
}
free(ds.items[i].optional_data);
ds.items[i].optional_data = NULL;
ds.items[i].data_size = 0;
@ -1581,6 +1638,30 @@ drag_free_data(Window *w, const char *mime_type, const char* data, size_t sz) {
return 0;
}
static bool
is_file_url(const char *url) {
return url != NULL && strlen(url) > sizeof("file://")-1 && memcmp(url, "file://", sizeof("file://")-1) == 0;
}
static bool
request_remote_files(Window *w, size_t i) {
#define mi ds.items[i]
char buf[128];
mi.remote_items = calloc(mi.num_uris, sizeof(mi.remote_items[0]));
if (!mi.remote_items) return false;
mi.num_remote_items = mi.num_uris;
for (size_t k = 0; k < mi.num_remote_items; k++) {
if (is_file_url(mi.uri_list[k])) {
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=k:x=%zu", DND_CODE, k + 1);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
mi.remote_items[k].waiting_for_completion = true;
}
}
return true;
#undef mi
}
const char*
drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) {
*err_code = ENOENT; *sz = 0;
@ -1636,12 +1717,17 @@ drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) {
}
// No fd yet, request data from the client
if (!ds.items[i].data_requested_from_client) {
char buf[128];
ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list;
int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu:Y=%d",
DND_CODE, DRAG_NOTIFY_FINISHED + 2, i, ds.items[i].requested_remote_files);
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
ds.items[i].data_requested_from_client = true;
ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list;
if (ds.items[i].requested_remote_files) {
if (!request_remote_files(w, i)) { *err_code = ENOMEM; return NULL; }
} else {
char buf[128];
int header_sz = snprintf(buf, sizeof(buf),
"\x1b]%d;t=e:x=%d:y=%zu", DND_CODE, DRAG_NOTIFY_FINISHED + 2, i);
queue_payload_to_child(
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false);
}
}
*err_code = EAGAIN;
return NULL;
@ -1752,73 +1838,6 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo
}
}
static char**
parse_uri_list(Window *w, int fd, size_t *num_uris_out) {
*num_uris_out = 0;
// Determine file size and read all data
off_t file_size = lseek(fd, 0, SEEK_END);
if (file_size < 0) { cancel_drag(w, EIO, "failed to read cached uri-list data"); return NULL; }
if (lseek(fd, 0, SEEK_SET) < 0) { cancel_drag(w, EIO, "failed to read cached uri-list data"); return NULL; }
RAII_ALLOC(char, buf, malloc((size_t)file_size + 1));
if (!buf) { cancel_drag(w, ENOMEM, "out of memory processing uri list data"); return NULL; }
size_t total = 0;
while (total < (size_t)file_size) {
ssize_t n = read(fd, buf + total, (size_t)file_size - total);
if (n < 0) {
if (errno == EINTR) continue;
cancel_drag(w, EIO, "failed to read cached uri-list data"); return NULL;
}
if (n == 0) break;
total += (size_t)n;
}
buf[total] = '\0';
// First pass: count non-comment, non-empty lines
size_t count = 0;
char *p = buf;
while (*p) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
char saved_end = *end; *end = '\0';
if (*p && *p != '#') count++;
*end = saved_end;
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
char **result = calloc((count + 1), sizeof(const char*));
if (!result) { cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL; }
// Second pass: fill in decoded URI strings
size_t idx = 0;
p = buf;
while (*p && idx < count) {
char *eol = p + strcspn(p, "\r\n");
char saved = *eol; *eol = '\0';
char *end = eol;
while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--;
*end = '\0';
if (*p && *p != '#') {
char *decoded = strdup(p);
if (!decoded) {
for (size_t k = 0; k < idx; k++) free((char*)result[k]);
free(result); cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL;
}
result[idx++] = decoded;
}
*eol = saved;
if (saved == '\0') break;
p = eol + 1;
while (*p == '\r' || *p == '\n') p++;
}
*num_uris_out = idx;
return result;
}
static int
write_all(int fd, const void *buf, size_t sz) {
size_t pos = 0; const char *p = buf;
@ -1832,6 +1851,11 @@ write_all(int fd, const void *buf, size_t sz) {
static void
finish_remote_data(Window *w, size_t item_idx) {
if (!ds.items[item_idx].fd_plus_one) {
int fd = open_item_tmpfile();
if (fd < 0) abrt(EIO, "failed to open temp file to store modified uri list");
ds.items[item_idx].fd_plus_one = fd + 1;
}
const int fd = ds.items[item_idx].fd_plus_one - 1;
ds.items[item_idx].requested_remote_files = false;
if (safe_ftruncate(fd, 0) != 0) abrt(errno, "error updating uri list after all remote data received");
@ -1919,10 +1943,12 @@ populate_dir_entries(Window *w, DragRemoteItem *ri) {
while (ptr < end) {
const char *p = memchr(ptr, 0, (size_t)(end - ptr));
size_t len = p ? (size_t)(p - ptr) : (size_t)(end - ptr);
DragRemoteItem *child = ri->children + ri->children_sz++;
child->parent = ri;
if (len > 0) {
char *name = strndup(ptr, len);
if (!name) abrt(ENOMEM, "out of memory processing drag source item directory entries");
ri->children[ri->children_sz++].dir_entry_name = name;
child->dir_entry_name = name;
}
ptr = p ? p + 1 : end;
}
@ -1996,11 +2022,6 @@ toplevel_data_for_drag(
Window *w, unsigned mime_item_idx, unsigned uri_item_idx, unsigned item_type,
bool has_more, const uint8_t *payload, size_t payload_sz
) {
if (!mi.remote_items) {
mi.remote_items = calloc(mi.num_uris, sizeof(mi.remote_items[0]));
if (!mi.remote_items) abrt(ENOMEM, "out of memory processing drag source item");
mi.num_remote_items = mi.num_uris;
}
if (!mi.base_dir_for_remote_items) {
int fd;
mi.base_dir_for_remote_items = mktempdir_in_cache("dnd-drag-", &fd);
@ -2050,10 +2071,10 @@ find_by_handle(DragRemoteItem *parent, int handle, char *path_to_parent, size_t
static void
subdir_data_for_drag(
Window *w, unsigned mime_item_idx, unsigned uri_item_idx, int handle, unsigned entry_num, unsigned item_type,
bool has_more, const uint8_t *payload, size_t payload_sz
bool has_more, const uint8_t *payload, size_t payload_sz, DragRemoteItem **ri
) {
if (!mi.remote_items || uri_item_idx >= mi.num_remote_items) abrt(EINVAL, "drag source sub directory item uri list index out of range");
DragRemoteItem *parent = NULL;
DragRemoteItem *parent = NULL; *ri = NULL;
if (mi.currently_open_subdir) {
if (mi.currently_open_subdir->type == handle) parent = mi.currently_open_subdir;
else {
@ -2080,15 +2101,14 @@ subdir_data_for_drag(
}
}
if (entry_num >= parent->children_sz) abrt(EINVAL, "drag source sub diretory index out of bounds");
DragRemoteItem *ri = parent->children + entry_num;
if (!ri->started) {
ri->started = true;
ri->type = item_type;
base64_init_stream_decoder(&ri->base64_state);
*ri = parent->children + entry_num;
if (!(*ri)->started) {
(*ri)->started = true;
(*ri)->type = item_type;
base64_init_stream_decoder(&(*ri)->base64_state);
}
add_payload(w, ri, has_more, payload, payload_sz, parent->fd_plus_one - 1);
add_payload(w, *ri, has_more, payload, payload_sz, parent->fd_plus_one - 1);
}
#undef mi
void
drag_offer_start_to_child(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_x, int32_t pixel_y) {
@ -2099,29 +2119,69 @@ drag_offer_start_to_child(Window *w, int32_t cell_x, int32_t cell_y, int32_t pix
w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_size, NULL, 0, false);
}
static void
finish_remote_data_if_all_items_received(Window *w, unsigned mime_item_idx) {
for (size_t i = 0; i < mi.num_remote_items; i++) {
if (mi.remote_items[i].waiting_for_completion && !mi.remote_items[i].completed) return;
}
finish_remote_data(w, mime_item_idx);
}
static bool
all_children_complete(DragRemoteItem *parent) {
for (size_t i = 0; i < parent->children_sz; i++) {
if (!parent->children[i].completed) return false;
}
return true;
}
void
drag_remote_file_data(
Window *w, int32_t x, int32_t y, int32_t X, int32_t Y, bool has_more, const uint8_t *payload, size_t payload_sz
) {
size_t item_idx = ds.num_mimes;
size_t mime_item_idx = ds.num_mimes;
for (size_t i = 0; i < ds.num_mimes; i++) {
if (ds.items[i].requested_remote_files) {
item_idx = i; break;
mime_item_idx = i; break;
}
}
if (item_idx == ds.num_mimes || ds.items[item_idx].fd_plus_one == 0) abrt(EINVAL, "drag source remote file item index out of bounds");
if (ds.items[item_idx].uri_list == NULL) {
ds.items[item_idx].uri_list = parse_uri_list(w, ds.items[item_idx].fd_plus_one-1, &ds.items[item_idx].num_uris);
if (!ds.items[item_idx].uri_list) return;
if (mime_item_idx == ds.num_mimes) abrt(EINVAL, "drag source no text/uri-list MIME entry data was requested");
if (x < 1) abrt(EINVAL, "drag source remote item x index cannot be less than 1");
const bool all_data_received = !payload_sz && !has_more;
const unsigned uri_item_idx = x - 1;
if (uri_item_idx >= mi.num_remote_items) abrt(EINVAL, "drag source uri list index out of bounds");
DragRemoteItem *ri;
if (!Y) {
toplevel_data_for_drag(w, mime_item_idx, uri_item_idx, X, has_more, payload, payload_sz);
if (all_data_received) {
ri = mi.remote_items + uri_item_idx;
if (ri->waiting_for_completion && all_children_complete(ri)) {
ri->completed = true;
finish_remote_data_if_all_items_received(w, mime_item_idx);
}
}
} else {
if (y < 1) abrt(EINVAL, "drag source remote item y index cannot be less than 1");
subdir_data_for_drag(w, mime_item_idx, x - 1, Y, y - 1, X, has_more, payload, payload_sz, &ri);
if (all_data_received && ri && all_children_complete(ri)) {
ri->completed = true;
while (1) {
ri = ri->parent;
if (ri) {
if (all_children_complete(ri)) ri->completed = true;
else break;
} else {
finish_remote_data_if_all_items_received(w, mime_item_idx);
break;
}
}
}
}
if (X < 0) abrt(EINVAL, "drag source remote item X cannot be negative");
if (!x && !y && !Y) { finish_remote_data(w, item_idx); return; }
if (!Y) toplevel_data_for_drag(w, item_idx, x - 1, X, has_more, payload, payload_sz);
else subdir_data_for_drag(w, item_idx, x - 1, Y, y - 1, X, has_more, payload, payload_sz);
}
#undef img
#undef abrt
#undef ds
#undef mi
// }}}
// DnD testing infrastructure {{{
@ -2294,26 +2354,33 @@ dnd_test_force_drag_dropped(PyObject *self UNUSED, PyObject *args) {
Window *w = window_for_window_id((id_type)window_id);
if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; }
// Simulate what drag_start does on success, without calling start_window_drag
#define ds w->drag_source
for (size_t i = 0; i < w->drag_source.num_mimes; i++) {
free(w->drag_source.items[i].optional_data);
w->drag_source.items[i].optional_data = NULL;
w->drag_source.items[i].data_size = 0;
w->drag_source.items[i].data_capacity = 0;
w->drag_source.items[i].data_decode_initialized = false;
if (ds.is_remote_client && ds.items[i].is_uri_list) {
if (ds.items[i].optional_data && ds.items[i].data_size) {
ds.items[i].uri_list = parse_uri_list(
w, (char*)ds.items[i].optional_data, ds.items[i].data_size, &ds.items[i].num_uris);
}
}
free(ds.items[i].optional_data);
ds.items[i].optional_data = NULL;
ds.items[i].data_size = 0;
ds.items[i].data_capacity = 0;
ds.items[i].data_decode_initialized = false;
}
for (size_t i = 0; i < arraysz(w->drag_source.images); i++) {
if (w->drag_source.images[i].data) free(w->drag_source.images[i].data);
zero_at_ptr(w->drag_source.images + i);
if (ds.images[i].data) free(w->drag_source.images[i].data);
zero_at_ptr(ds.images + i);
}
w->drag_source.state = DRAG_SOURCE_DROPPED;
ds.state = DRAG_SOURCE_DROPPED;
#undef ds
Py_RETURN_NONE;
}
static PyObject *
dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) {
// Simulate what drag_get_data does initially: find the MIME item at the
// given index, set requested_remote_files if appropriate, and return the
// escape code that would be sent to the client.
// given index, set requested_remote_files if appropriate.
unsigned long long window_id;
unsigned idx;
if (!PyArg_ParseTuple(args, "KI", &window_id, &idx)) return NULL;
@ -2323,6 +2390,7 @@ dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) {
PyErr_SetString(PyExc_ValueError, "Invalid state or index"); return NULL;
}
w->drag_source.items[idx].requested_remote_files = w->drag_source.is_remote_client && w->drag_source.items[idx].is_uri_list;
if (w->drag_source.items[idx].requested_remote_files) request_remote_files(w, idx);
Py_RETURN_NONE;
}
@ -2420,6 +2488,19 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) {
if (strcmp(q, "drag_thumbnail_size") == 0) {
return PyLong_FromSize_t(last_total_image_size);
}
if (strcmp(q, "drag_remote_data_complete") == 0) {
for (size_t idx = 0; idx < w->drag_source.num_mimes; idx++) {
#define mi w->drag_source.items[idx]
if (mi.is_uri_list && mi.requested_remote_files) {
for (size_t i = 0; i < mi.num_remote_items; i++) {
if (mi.remote_items[i].waiting_for_completion && !mi.remote_items[i].completed)
return PyUnicode_FromString(mi.remote_items[i].dir_entry_name);
}
}
#undef mi
}
Py_RETURN_NONE;
}
Py_RETURN_NONE;
}

View file

@ -253,7 +253,8 @@ typedef struct DragRemoteItem {
size_t children_sz;
char *dir_entry_name;
base64_state base64_state;
bool started;
bool started, waiting_for_completion, completed;
struct DragRemoteItem *parent;
} DragRemoteItem;
typedef struct Window {

View file

@ -426,7 +426,7 @@ class PTY:
if isinstance(data, str):
data = data.encode('utf-8')
if self.log_data_flow:
print('t -> c:', data)
print('t -> c:', bytes(data))
self.write_buf += data
if flush:
self.process_input_from_child(0)

View file

@ -18,6 +18,7 @@ from kitty.fast_data_types import (
dnd_test_fake_drop_data,
dnd_test_fake_drop_event,
dnd_test_force_drag_dropped,
dnd_test_probe_state,
dnd_test_request_drag_data,
dnd_test_set_mouse_pos,
)
@ -236,14 +237,6 @@ def client_remote_file(
return _osc(meta)
def client_remote_file_finish(client_id: int = 0) -> bytes:
"""Escape code signaling completion of all remote file data (t=k with no keys)."""
meta = f'{DND_CODE};t=k'
if client_id:
meta += f':i={client_id}'
return _osc(meta)
# ---- escape-code decoder used by assertions ---------------------------------
_OSC_RE = re.compile(
@ -2340,18 +2333,19 @@ class TestDnDProtocol(BaseTest):
# Register with a different machine_id to make is_remote_client=True
parse_bytes(screen, _osc(f'{DND_CODE};t=o:x=1;different-machine-id'))
parse_bytes(screen, client_drag_offer_mimes(operations, mimes, client_id=client_id))
cap.consume()
dnd_test_force_drag_dropped(cap.window_id)
# Find the index of text/uri-list
mime_list = mimes.split()
uri_idx = mime_list.index('text/uri-list')
dnd_test_request_drag_data(cap.window_id, uri_idx)
# Send the uri-list data
b64 = standard_b64encode(uri_list_data).decode()
parse_bytes(screen, client_drag_send_data(uri_idx, b64, client_id=client_id))
# End of data
parse_bytes(screen, client_drag_send_data(uri_idx, '', client_id=client_id))
parse_bytes(screen, client_drag_pre_send(uri_idx, b64, client_id=client_id))
cap.consume()
dnd_test_force_drag_dropped(cap.window_id)
dnd_test_request_drag_data(cap.window_id, uri_idx)
events = self._get_events(cap)
expected = 0
for line in uri_list_data.decode().splitlines():
if line.startswith('file://'):
expected += 1
self.assertEqual(expected, len(events))
def test_remote_drag_single_file(self) -> None:
"""Transfer a single regular file via t=k."""
@ -2366,9 +2360,7 @@ class TestDnDProtocol(BaseTest):
# End of data for this file
parse_bytes(screen, client_remote_file(1, '', item_type=0))
self._assert_no_output(cap)
# Completion signal
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_single_symlink(self) -> None:
"""Transfer a symlink via t=k with X=1."""
@ -2383,9 +2375,7 @@ class TestDnDProtocol(BaseTest):
# End of data
parse_bytes(screen, client_remote_file(1, '', item_type=1))
self._assert_no_output(cap)
# Completion signal
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_single_directory(self) -> None:
"""Transfer a directory with entries via t=k with X=handle (>1)."""
@ -2422,10 +2412,7 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(
1, '', item_type=0, parent_handle=2, entry_num=2))
self._assert_no_output(cap)
# Completion signal
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_multiple_uris(self) -> None:
"""Transfer multiple files from a URI list."""
@ -2442,9 +2429,7 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(2, b64, item_type=0))
parse_bytes(screen, client_remote_file(2, '', item_type=0))
self._assert_no_output(cap)
# Completion
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_chunked_file(self) -> None:
"""File data can be sent in multiple chunks with m=1."""
@ -2468,9 +2453,7 @@ class TestDnDProtocol(BaseTest):
# End of data
parse_bytes(screen, client_remote_file(1, '', item_type=0))
self._assert_no_output(cap)
# Completion
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_directory_with_symlink(self) -> None:
"""Directory can contain symlinks (X=1 type for children)."""
@ -2499,10 +2482,7 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(
1, '', item_type=1, parent_handle=2, entry_num=2))
self._assert_no_output(cap)
# Completion
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_deep_directory_tree_breadth_first(self) -> None:
"""Transfer a 3-level deep directory tree in breadth-first order.
@ -2572,10 +2552,7 @@ class TestDnDProtocol(BaseTest):
1, b64, item_type=1, parent_handle=4, entry_num=2))
parse_bytes(screen, client_remote_file(
1, '', item_type=1, parent_handle=4, entry_num=2))
# Completion
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_deep_directory_tree_depth_first(self) -> None:
"""Transfer a 3-level deep directory tree in depth-first order.
@ -2645,8 +2622,7 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(
1, '', item_type=1, parent_handle=4, entry_num=2))
# Completion
parse_bytes(screen, client_remote_file_finish())
self.assert_drag_data_complete(cap)
self._assert_no_output(cap)
def test_remote_drag_completion_signal(self) -> None:
@ -2657,8 +2633,7 @@ class TestDnDProtocol(BaseTest):
b64 = standard_b64encode(b'data').decode()
parse_bytes(screen, client_remote_file(1, b64, item_type=0))
parse_bytes(screen, client_remote_file(1, '', item_type=0))
# Completion
parse_bytes(screen, client_remote_file_finish())
self.assert_drag_data_complete(cap)
self._assert_no_output(cap)
def test_remote_drag_invalid_uri_index(self) -> None:
@ -2726,15 +2701,6 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(1, big_b64, item_type=0))
self.assert_error(cap)
def test_remote_drag_negative_X_rejected(self) -> None:
"""Sending t=k with X < 0 is rejected."""
uri_list = b'file:///home/user/f.txt\r\n'
with dnd_test_window() as (screen, cap):
self._setup_remote_drag(screen, cap, uri_list)
# Directly construct escape code with negative X
parse_bytes(screen, _osc(f'{DND_CODE};t=k:x=1:X=-1'))
self.assert_error(cap)
def test_remote_drag_without_remote_flag_fails(self) -> None:
"""t=k fails if the drag is not from a remote client."""
with dnd_test_window() as (screen, cap):
@ -2780,7 +2746,7 @@ class TestDnDProtocol(BaseTest):
def test_remote_drag_dos_present_data_cap_on_directory(self) -> None:
"""Directory listing data exceeding PRESENT_DATA_CAP triggers EMFILE error."""
uri_list = b'file:///home/user/dir\r\n'
with dnd_test_window(present_data_cap=20) as (screen, cap):
with dnd_test_window(present_data_cap=100) as (screen, cap):
self._setup_remote_drag(screen, cap, uri_list)
# Send a directory listing that will exceed the cap
big_listing = b'\x00'.join([f'file{i}.txt'.encode() for i in range(100)])
@ -2801,6 +2767,10 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(1, b64, item_type=0))
self.assert_error(cap)
def assert_drag_data_complete(self, cap) -> None:
first_incomplete_entry = dnd_test_probe_state(cap.window_id, 'drag_remote_data_complete')
self.assertIsNone(first_incomplete_entry)
def test_remote_drag_three_level_tree_with_verification(self) -> None:
"""Transfer a 3-level directory tree and verify no errors occur.
@ -2875,9 +2845,7 @@ class TestDnDProtocol(BaseTest):
1, '', item_type=1, parent_handle=30, entry_num=2))
self._assert_no_output(cap)
# Completion
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_process_item_data_basic(self) -> None:
"""Basic drag_process_item_data: send data for a MIME type after DROPPED state."""
@ -2954,8 +2922,7 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(3, '', item_type=1))
self._assert_no_output(cap)
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_empty_file(self) -> None:
"""Transfer an empty file (end-of-data immediately after start)."""
@ -2965,8 +2932,7 @@ class TestDnDProtocol(BaseTest):
# Start file transfer, then immediately end (no data chunks)
parse_bytes(screen, client_remote_file(1, '', item_type=0))
self._assert_no_output(cap)
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_empty_directory(self) -> None:
"""Transfer a directory with no entries."""
@ -2977,8 +2943,7 @@ class TestDnDProtocol(BaseTest):
b64 = standard_b64encode(b'').decode()
parse_bytes(screen, client_remote_file(1, b64, item_type=2))
parse_bytes(screen, client_remote_file(1, '', item_type=2))
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
def test_remote_drag_uri_list_with_comments(self) -> None:
"""URI list with comment lines (starting with #) should filter them out."""
@ -3027,9 +2992,7 @@ class TestDnDProtocol(BaseTest):
parse_bytes(screen, client_remote_file(
1, '', item_type=0, parent_handle=2, entry_num=2))
self._assert_no_output(cap)
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
self.assert_drag_data_complete(cap)
# ---- DoS limits tests ---------------------------------------------------
@ -3137,9 +3100,6 @@ class TestDnDProtocol(BaseTest):
# End of data for this file
parse_bytes(screen, client_remote_file(1, '', item_type=0))
self._assert_no_output(cap)
# Completion signal
parse_bytes(screen, client_remote_file_finish())
self._assert_no_output(cap)
# No crash or leak - cleanup happens in context manager exit
# ---- query tests --------------------------------------------------------