Testing ground for GUI
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

366 lines
10 KiB

  1. // hpcu unifies the PRIMARY and CLIPBOARD X11 selections for text contents.
  2. package main
  3. import (
  4. "errors"
  5. "janouch.name/haven/nexgb"
  6. "janouch.name/haven/nexgb/xfixes"
  7. "janouch.name/haven/nexgb/xproto"
  8. "log"
  9. )
  10. type selectionState struct {
  11. name string // name of the selection
  12. inProgress xproto.Timestamp // timestamp of retrieved selection
  13. buffer []byte // UTF-8 text buffer
  14. incr bool // INCR running
  15. incrFailed bool // INCR failure indicator
  16. owning xproto.Timestamp // since when we own the selection
  17. }
  18. var (
  19. X *nexgb.Conn
  20. setup *xproto.SetupInfo
  21. screen *xproto.ScreenInfo
  22. atomCLIPBOARD xproto.Atom // X11 atom for CLIPBOARD
  23. atomUTF8String xproto.Atom // X11 atom for UTF8_STRING
  24. atomINCR xproto.Atom // X11 atom for INCR
  25. atomTARGETS xproto.Atom // X11 atom for TARGETS
  26. atomTIMESTAMP xproto.Atom // X11 atom for TIMESTAMP
  27. wid xproto.Window // auxiliary window
  28. selections map[xproto.Atom]*selectionState
  29. contents string // current shared selection contents
  30. )
  31. // resolveAtoms resolves a few required atoms that are not in the core protocol.
  32. func resolveAtoms() error {
  33. for _, i := range []struct {
  34. placement *xproto.Atom
  35. name string
  36. }{
  37. {&atomCLIPBOARD, "CLIPBOARD"},
  38. {&atomUTF8String, "UTF8_STRING"},
  39. {&atomINCR, "INCR"},
  40. {&atomTARGETS, "TARGETS"},
  41. {&atomTIMESTAMP, "TIMESTAMP"},
  42. } {
  43. if reply, err := xproto.InternAtom(X,
  44. false, uint16(len(i.name)), i.name).Reply(); err != nil {
  45. return err
  46. } else {
  47. *i.placement = reply.Atom
  48. }
  49. }
  50. return nil
  51. }
  52. // setupAuxiliaryWindow creates a window that receives notifications about
  53. // changed selection contents, and serves
  54. func setupAuxiliaryWindow() error {
  55. var err error
  56. if wid, err = xproto.NewWindowId(X); err != nil {
  57. return err
  58. }
  59. _ = xproto.CreateWindow(X, screen.RootDepth, wid, screen.Root, 0, 0, 1, 1,
  60. 0, xproto.WindowClassInputOutput, screen.RootVisual, xproto.CwEventMask,
  61. []uint32{xproto.EventMaskPropertyChange})
  62. for _, selection := range []xproto.Atom{xproto.AtomPrimary, atomCLIPBOARD} {
  63. _ = xfixes.SelectSelectionInput(X, wid, selection,
  64. xfixes.SelectionEventMaskSetSelectionOwner|
  65. xfixes.SelectionEventMaskSelectionWindowDestroy|
  66. xfixes.SelectionEventMaskSelectionClientClose)
  67. }
  68. return nil
  69. }
  70. // getProperty reads a window property in a memory-efficient manner.
  71. func getProperty(window xproto.Window, property xproto.Atom) (
  72. *xproto.GetPropertyReply, error) {
  73. // xorg-xserver doesn't seem to limit the length of replies or even
  74. // the length of properties in the first place. It only has a huge
  75. // (0xffffffff - sizeof(xChangePropertyReq))/4 limit for ChangeProperty
  76. // requests, even though I can't XChangeProperty more than 0xffffe0
  77. // bytes at a time.
  78. //
  79. // Since the XGB API doesn't let us provide our own buffer for
  80. // value data, let us avoid multiplying the amount of consumed memory in
  81. // pathological cases where properties are several gigabytes in size by
  82. // chunking the requests. This has a cost of losing atomicity, although
  83. // it shouldn't pose a problem except for timeout-caused INCR races.
  84. var result xproto.GetPropertyReply
  85. for (result.Sequence == 0 && result.Length == 0) || result.BytesAfter > 0 {
  86. reply, err := xproto.GetProperty(X, false, /* delete */
  87. window, property, xproto.GetPropertyTypeAny,
  88. uint32(len(result.Value))/4,
  89. uint32(setup.MaximumRequestLength)).Reply()
  90. if err != nil {
  91. return nil, err
  92. }
  93. if result.Length != 0 &&
  94. (reply.Format != result.Format || reply.Type != result.Type) {
  95. return nil, errors.New("property type changed during read")
  96. }
  97. reply.Value = append(result.Value, reply.Value...)
  98. reply.ValueLen += result.ValueLen
  99. result = *reply
  100. }
  101. return &result, nil
  102. }
  103. // appendText tries to append UTF-8 text to the selection state buffer.
  104. func appendText(state *selectionState, prop *xproto.GetPropertyReply) bool {
  105. if prop.Type == atomUTF8String && prop.Format == 8 {
  106. state.buffer = append(state.buffer, prop.Value...)
  107. return true
  108. }
  109. return false
  110. }
  111. func requestOwnership(origin *selectionState, time xproto.Timestamp) {
  112. contents = string(origin.buffer)
  113. for selection, state := range selections {
  114. // We might want to replace the originator as well but it might have
  115. // undesirable effects, mainly with PRIMARY.
  116. if state != origin {
  117. // No need to GetSelectionOwner, XFIXES is more reliable.
  118. _ = xproto.SetSelectionOwner(X, wid, selection, time)
  119. }
  120. }
  121. }
  122. func handleXfixesSelectionNotify(e xfixes.SelectionNotifyEvent) {
  123. state, ok := selections[e.Selection]
  124. if !ok {
  125. return
  126. }
  127. // Ownership request has been granted, don't ask ourselves for data.
  128. if e.Owner == wid {
  129. state.owning = e.SelectionTimestamp
  130. return
  131. }
  132. // This should always be true.
  133. if state.owning < e.SelectionTimestamp {
  134. state.owning = 0
  135. }
  136. // Not checking whether we should give up when our current retrieval
  137. // attempt is interrupted--the timeout mostly solves this.
  138. if e.Owner == xproto.WindowNone {
  139. return
  140. }
  141. // Don't try to process two things at once. Each request gets a few
  142. // seconds to finish, then we move on, hoping that a property race
  143. // doesn't commence. Ideally we'd set up a separate queue for these
  144. // skipped requests and process them later.
  145. if state.inProgress != 0 && e.Timestamp-state.inProgress < 5000 {
  146. return
  147. }
  148. // ICCCM says we should ensure the named property doesn't exist.
  149. _ = xproto.DeleteProperty(X, e.Window, e.Selection)
  150. _ = xproto.ConvertSelection(X, e.Window, e.Selection,
  151. atomUTF8String, e.Selection, e.Timestamp)
  152. state.inProgress = e.Timestamp
  153. state.incr = false
  154. }
  155. func handleSelectionNotify(e xproto.SelectionNotifyEvent) {
  156. state, ok := selections[e.Selection]
  157. if e.Requestor != wid || !ok || e.Time != state.inProgress {
  158. return
  159. }
  160. state.inProgress = 0
  161. if e.Property == xproto.AtomNone {
  162. return
  163. }
  164. state.buffer = nil
  165. reply, err := getProperty(e.Requestor, e.Property)
  166. if err != nil {
  167. return
  168. }
  169. // When you select a lot of text in VIM, it starts the ICCCM
  170. // INCR mechanism, from which there is no opt-out.
  171. if reply.Type == atomINCR {
  172. state.inProgress = e.Time
  173. state.incr = true
  174. state.incrFailed = false
  175. } else if appendText(state, reply) {
  176. requestOwnership(state, e.Time)
  177. }
  178. _ = xproto.DeleteProperty(X, e.Requestor, e.Property)
  179. }
  180. func handlePropertyNotify(e xproto.PropertyNotifyEvent) {
  181. state, ok := selections[e.Atom]
  182. if e.Window != wid || e.State != xproto.PropertyNewValue ||
  183. !ok || !state.incr {
  184. return
  185. }
  186. reply, err := getProperty(e.Window, e.Atom)
  187. if err != nil {
  188. state.incrFailed = true
  189. return
  190. }
  191. if !appendText(state, reply) {
  192. // We need to keep deleting the property.
  193. state.incrFailed = true
  194. }
  195. if reply.ValueLen == 0 {
  196. if !state.incrFailed {
  197. requestOwnership(state, e.Time)
  198. }
  199. state.inProgress = 0
  200. state.incr = false
  201. }
  202. _ = xproto.DeleteProperty(X, e.Window, e.Atom)
  203. }
  204. func handleSelectionRequest(e xproto.SelectionRequestEvent) {
  205. property := e.Property
  206. if property == xproto.AtomNone {
  207. property = e.Target
  208. }
  209. state, ok := selections[e.Selection]
  210. if e.Owner != wid || !ok {
  211. return
  212. }
  213. var (
  214. typ xproto.Atom
  215. format byte
  216. data []byte
  217. )
  218. // XXX: We should also support the MULTIPLE target but it seems to be
  219. // unimportant and largely abandoned today.
  220. targets := []xproto.Atom{atomTARGETS, atomTIMESTAMP, atomUTF8String}
  221. switch e.Target {
  222. case atomTARGETS:
  223. typ = xproto.AtomAtom
  224. format = 32
  225. data = make([]byte, len(targets)*4)
  226. for i, atom := range targets {
  227. nexgb.Put32(data[i*4:], uint32(atom))
  228. }
  229. case atomTIMESTAMP:
  230. typ = xproto.AtomInteger
  231. format = 32
  232. data = make([]byte, 4)
  233. nexgb.Put32(data, uint32(state.owning))
  234. case atomUTF8String:
  235. typ = atomUTF8String
  236. format = 8
  237. data = []byte(contents)
  238. }
  239. response := xproto.SelectionNotifyEvent{
  240. Time: e.Time,
  241. Requestor: e.Requestor,
  242. Selection: e.Selection,
  243. Target: e.Target,
  244. Property: xproto.AtomNone,
  245. }
  246. if typ == 0 || len(data) > int(setup.MaximumRequestLength)*4-64 ||
  247. state.owning == 0 || e.Time < state.owning {
  248. // TODO: Use the INCR mechanism for large data transfers instead
  249. // of refusing the request, or at least use PropModeAppend.
  250. //
  251. // According to the ICCCM we need to set up a queue for concurrent
  252. // (requestor, selection, target, timestamp) requests that differ
  253. // only in the target property, and process them in order. The ICCCM
  254. // has a nice rationale. It seems to only concern INCR. The queue
  255. // might be a map[(who, what, how, when)][](where, data, offset).
  256. //
  257. // NOTE: Even with BigRequests support, it may technically be
  258. // missing on the particular X server, and XGB copies buffers to yet
  259. // another buffer, making very large transfers a very bad idea.
  260. } else if xproto.ChangePropertyChecked(X, xproto.PropModeReplace,
  261. e.Requestor, property, typ, format,
  262. uint32(len(data)/int(format/8)), data).Check() == nil {
  263. response.Property = property
  264. }
  265. _ = xproto.SendEvent(X, false /* propagate */, e.Requestor,
  266. 0 /* event mask */, string(response.Bytes()))
  267. }
  268. func handleXEvent(ev nexgb.Event) {
  269. switch e := ev.(type) {
  270. case xfixes.SelectionNotifyEvent:
  271. handleXfixesSelectionNotify(e)
  272. case xproto.SelectionNotifyEvent:
  273. handleSelectionNotify(e)
  274. case xproto.PropertyNotifyEvent:
  275. handlePropertyNotify(e)
  276. case xproto.SelectionRequestEvent:
  277. handleSelectionRequest(e)
  278. }
  279. }
  280. func main() {
  281. var err error
  282. if X, err = nexgb.NewConn(); err != nil {
  283. log.Fatalln(err)
  284. }
  285. if err = xfixes.Init(X); err != nil {
  286. log.Fatalln(err)
  287. }
  288. // Enable the extension.
  289. _ = xfixes.QueryVersion(X, xfixes.MajorVersion, xfixes.MinorVersion)
  290. setup = xproto.Setup(X)
  291. screen = setup.DefaultScreen(X)
  292. if err = resolveAtoms(); err != nil {
  293. log.Fatalln(err)
  294. }
  295. if err = setupAuxiliaryWindow(); err != nil {
  296. log.Fatalln(err)
  297. }
  298. // Now that we have our atoms, we can initialize state.
  299. selections = map[xproto.Atom]*selectionState{
  300. xproto.AtomPrimary: {name: "PRIMARY"},
  301. atomCLIPBOARD: {name: "CLIPBOARD"},
  302. }
  303. for {
  304. ev, xerr := X.WaitForEvent()
  305. if xerr != nil {
  306. log.Printf("Error: %s\n", xerr)
  307. return
  308. }
  309. if ev != nil {
  310. handleXEvent(ev)
  311. }
  312. }
  313. }