aboutsummaryrefslogtreecommitdiff
path: root/hpcu/main.go
blob: 3508f63980604aa4f2d0a6d356128f7ee754e9d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
// hpcu unifies the PRIMARY and CLIPBOARD X11 selections for text contents.
package main

import (
	"errors"
	"janouch.name/haven/nexgb"
	"janouch.name/haven/nexgb/xfixes"
	"janouch.name/haven/nexgb/xproto"
	"log"
)

type selectionState struct {
	name       string           // name of the selection
	inProgress xproto.Timestamp // timestamp of retrieved selection
	buffer     []byte           // UTF-8 text buffer
	incr       bool             // INCR running
	incrFailed bool             // INCR failure indicator
	owning     xproto.Timestamp // since when we own the selection
}

var (
	X      *nexgb.Conn
	setup  *xproto.SetupInfo
	screen *xproto.ScreenInfo

	atomCLIPBOARD  xproto.Atom // X11 atom for CLIPBOARD
	atomUTF8String xproto.Atom // X11 atom for UTF8_STRING
	atomINCR       xproto.Atom // X11 atom for INCR
	atomTARGETS    xproto.Atom // X11 atom for TARGETS
	atomTIMESTAMP  xproto.Atom // X11 atom for TIMESTAMP

	wid        xproto.Window // auxiliary window
	selections map[xproto.Atom]*selectionState
	contents   string // current shared selection contents
)

// resolveAtoms resolves a few required atoms that are not in the core protocol.
func resolveAtoms() error {
	for _, i := range []struct {
		placement *xproto.Atom
		name      string
	}{
		{&atomCLIPBOARD, "CLIPBOARD"},
		{&atomUTF8String, "UTF8_STRING"},
		{&atomINCR, "INCR"},
		{&atomTARGETS, "TARGETS"},
		{&atomTIMESTAMP, "TIMESTAMP"},
	} {
		if reply, err := xproto.InternAtom(X,
			false, uint16(len(i.name)), i.name).Reply(); err != nil {
			return err
		} else {
			*i.placement = reply.Atom
		}
	}
	return nil
}

// setupAuxiliaryWindow creates a window that receives notifications about
// changed selection contents, and serves
func setupAuxiliaryWindow() error {
	var err error
	if wid, err = xproto.NewWindowId(X); err != nil {
		return err
	}

	_ = xproto.CreateWindow(X, screen.RootDepth, wid, screen.Root, 0, 0, 1, 1,
		0, xproto.WindowClassInputOutput, screen.RootVisual, xproto.CwEventMask,
		[]uint32{xproto.EventMaskPropertyChange})

	for _, selection := range []xproto.Atom{xproto.AtomPrimary, atomCLIPBOARD} {
		_ = xfixes.SelectSelectionInput(X, wid, selection,
			xfixes.SelectionEventMaskSetSelectionOwner|
				xfixes.SelectionEventMaskSelectionWindowDestroy|
				xfixes.SelectionEventMaskSelectionClientClose)
	}
	return nil
}

// getProperty reads a window property in a memory-efficient manner.
func getProperty(window xproto.Window, property xproto.Atom) (
	*xproto.GetPropertyReply, error) {
	// xorg-xserver doesn't seem to limit the length of replies or even
	// the length of properties in the first place. It only has a huge
	// (0xffffffff - sizeof(xChangePropertyReq))/4 limit for ChangeProperty
	// requests, even though I can't XChangeProperty more than 0xffffe0
	// bytes at a time.
	//
	// Since the XGB API doesn't let us provide our own buffer for
	// value data, let us avoid multiplying the amount of consumed memory in
	// pathological cases where properties are several gigabytes in size by
	// chunking the requests. This has a cost of losing atomicity, although
	// it shouldn't pose a problem except for timeout-caused INCR races.
	var result xproto.GetPropertyReply
	for (result.Sequence == 0 && result.Length == 0) || result.BytesAfter > 0 {
		reply, err := xproto.GetProperty(X, false, /* delete */
			window, property, xproto.GetPropertyTypeAny,
			uint32(len(result.Value))/4,
			uint32(setup.MaximumRequestLength)).Reply()
		if err != nil {
			return nil, err
		}
		if result.Length != 0 &&
			(reply.Format != result.Format || reply.Type != result.Type) {
			return nil, errors.New("property type changed during read")
		}

		reply.Value = append(result.Value, reply.Value...)
		reply.ValueLen += result.ValueLen
		result = *reply
	}
	return &result, nil
}

// appendText tries to append UTF-8 text to the selection state buffer.
func appendText(state *selectionState, prop *xproto.GetPropertyReply) bool {
	if prop.Type == atomUTF8String && prop.Format == 8 {
		state.buffer = append(state.buffer, prop.Value...)
		return true
	}
	return false
}

func requestOwnership(origin *selectionState, time xproto.Timestamp) {
	contents = string(origin.buffer)
	for selection, state := range selections {
		// We might want to replace the originator as well but it might have
		// undesirable effects, mainly with PRIMARY.
		if state != origin {
			// No need to GetSelectionOwner, XFIXES is more reliable.
			_ = xproto.SetSelectionOwner(X, wid, selection, time)
		}
	}
}

func handleXfixesSelectionNotify(e xfixes.SelectionNotifyEvent) {
	state, ok := selections[e.Selection]
	if !ok {
		return
	}

	// Ownership request has been granted, don't ask ourselves for data.
	if e.Owner == wid {
		state.owning = e.SelectionTimestamp
		return
	}

	// This should always be true.
	if state.owning < e.SelectionTimestamp {
		state.owning = 0
	}

	// Not checking whether we should give up when our current retrieval
	// attempt is interrupted--the timeout mostly solves this.
	if e.Owner == xproto.WindowNone {
		return
	}

	// Don't try to process two things at once. Each request gets a few
	// seconds to finish, then we move on, hoping that a property race
	// doesn't commence. Ideally we'd set up a separate queue for these
	// skipped requests and process them later.
	if state.inProgress != 0 && e.Timestamp-state.inProgress < 5000 {
		return
	}

	// ICCCM says we should ensure the named property doesn't exist.
	_ = xproto.DeleteProperty(X, e.Window, e.Selection)

	_ = xproto.ConvertSelection(X, e.Window, e.Selection,
		atomUTF8String, e.Selection, e.Timestamp)

	state.inProgress = e.Timestamp
	state.incr = false
}

func handleSelectionNotify(e xproto.SelectionNotifyEvent) {
	state, ok := selections[e.Selection]
	if e.Requestor != wid || !ok || e.Time != state.inProgress {
		return
	}

	state.inProgress = 0
	if e.Property == xproto.AtomNone {
		return
	}

	state.buffer = nil
	reply, err := getProperty(e.Requestor, e.Property)
	if err != nil {
		return
	}

	// When you select a lot of text in VIM, it starts the ICCCM
	// INCR mechanism, from which there is no opt-out.
	if reply.Type == atomINCR {
		state.inProgress = e.Time
		state.incr = true
		state.incrFailed = false
	} else if appendText(state, reply) {
		requestOwnership(state, e.Time)
	}

	_ = xproto.DeleteProperty(X, e.Requestor, e.Property)
}

func handlePropertyNotify(e xproto.PropertyNotifyEvent) {
	state, ok := selections[e.Atom]
	if e.Window != wid || e.State != xproto.PropertyNewValue ||
		!ok || !state.incr {
		return
	}

	reply, err := getProperty(e.Window, e.Atom)
	if err != nil {
		state.incrFailed = true
		return
	}

	if !appendText(state, reply) {
		// We need to keep deleting the property.
		state.incrFailed = true
	}

	if reply.ValueLen == 0 {
		if !state.incrFailed {
			requestOwnership(state, e.Time)
		}
		state.inProgress = 0
		state.incr = false
	}

	_ = xproto.DeleteProperty(X, e.Window, e.Atom)

}

func handleSelectionRequest(e xproto.SelectionRequestEvent) {
	property := e.Property
	if property == xproto.AtomNone {
		property = e.Target
	}

	state, ok := selections[e.Selection]
	if e.Owner != wid || !ok {
		return
	}

	var (
		typ    xproto.Atom
		format byte
		data   []byte
	)

	// XXX: We should also support the MULTIPLE target but it seems to be
	// unimportant and largely abandoned today.
	targets := []xproto.Atom{atomTARGETS, atomTIMESTAMP, atomUTF8String}

	switch e.Target {
	case atomTARGETS:
		typ = xproto.AtomAtom
		format = 32

		data = make([]byte, len(targets)*4)
		for i, atom := range targets {
			nexgb.Put32(data[i*4:], uint32(atom))
		}

	case atomTIMESTAMP:
		typ = xproto.AtomInteger
		format = 32

		data = make([]byte, 4)
		nexgb.Put32(data, uint32(state.owning))

	case atomUTF8String:
		typ = atomUTF8String
		format = 8

		data = []byte(contents)
	}

	response := xproto.SelectionNotifyEvent{
		Time:      e.Time,
		Requestor: e.Requestor,
		Selection: e.Selection,
		Target:    e.Target,
		Property:  xproto.AtomNone,
	}

	if typ == 0 || len(data) > int(setup.MaximumRequestLength)*4-64 ||
		state.owning == 0 || e.Time < state.owning {
		// TODO: Use the INCR mechanism for large data transfers instead
		// of refusing the request, or at least use PropModeAppend.
		//
		// According to the ICCCM we need to set up a queue for concurrent
		// (requestor, selection, target, timestamp) requests that differ
		// only in the target property, and process them in order. The ICCCM
		// has a nice rationale. It seems to only concern INCR. The queue
		// might be a map[(who, what, how, when)][](where, data, offset).
		//
		// NOTE: Even with BigRequests support, it may technically be
		// missing on the particular X server, and XGB copies buffers to yet
		// another buffer, making very large transfers a very bad idea.
	} else if xproto.ChangePropertyChecked(X, xproto.PropModeReplace,
		e.Requestor, property, typ, format,
		uint32(len(data)/int(format/8)), data).Check() == nil {
		response.Property = property
	}

	_ = xproto.SendEvent(X, false /* propagate */, e.Requestor,
		0 /* event mask */, string(response.Bytes()))
}

func handleXEvent(ev nexgb.Event) {
	switch e := ev.(type) {
	case xfixes.SelectionNotifyEvent:
		handleXfixesSelectionNotify(e)
	case xproto.SelectionNotifyEvent:
		handleSelectionNotify(e)
	case xproto.PropertyNotifyEvent:
		handlePropertyNotify(e)
	case xproto.SelectionRequestEvent:
		handleSelectionRequest(e)
	}
}

func main() {
	var err error
	if X, err = nexgb.NewConn(); err != nil {
		log.Fatalln(err)
	}
	if err = xfixes.Init(X); err != nil {
		log.Fatalln(err)
	}

	// Enable the extension.
	_ = xfixes.QueryVersion(X, xfixes.MajorVersion, xfixes.MinorVersion)

	setup = xproto.Setup(X)
	screen = setup.DefaultScreen(X)

	if err = resolveAtoms(); err != nil {
		log.Fatalln(err)
	}
	if err = setupAuxiliaryWindow(); err != nil {
		log.Fatalln(err)
	}

	// Now that we have our atoms, we can initialize state.
	selections = map[xproto.Atom]*selectionState{
		xproto.AtomPrimary: {name: "PRIMARY"},
		atomCLIPBOARD:      {name: "CLIPBOARD"},
	}

	for {
		ev, xerr := X.WaitForEvent()
		if xerr != nil {
			log.Printf("Error: %s\n", xerr)
			return
		}
		if ev != nil {
			handleXEvent(ev)
		}
	}
}