-
-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathTerm.hs
More file actions
577 lines (535 loc) · 18.9 KB
/
Copy pathTerm.hs
File metadata and controls
577 lines (535 loc) · 18.9 KB
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
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
{-# LANGUAGE CPP #-}
module Termonad.Term where
import Termonad.Prelude
import Control.Lens ((^.), ix, set)
import Data.Coerce (coerce)
import Data.Colour.SRGB (Colour, RGB(RGB), toSRGB)
import Data.FocusList (appendFL, deleteFL, getFocusItemFL)
import Data.GI.Base (toManagedPtr)
import Data.Text (pack)
import qualified Data.Text as Text
import GI.Gdk
( Event (Event)
, EventButton
, EventKey
, RGBA
, getEventButtonButton
, newZeroRGBA
, setRGBABlue
, setRGBAGreen
, setRGBARed
, pattern BUTTON_SECONDARY
, pattern CURRENT_TIME
)
import GI.Gio
( Cancellable
, actionMapAddAction
, menuAppend
, menuNew
, onSimpleActionActivate
, simpleActionNew
)
import GI.GLib
( SpawnFlags(SpawnFlagsDefault)
)
import GI.Gtk
( Adjustment
, Align(AlignFill)
, ApplicationWindow
, Box
, Button
, IconSize(IconSizeMenu)
, Label
, Notebook
, Orientation(OrientationHorizontal)
, PolicyType(PolicyTypeAlways, PolicyTypeAutomatic, PolicyTypeNever)
, ReliefStyle(ReliefStyleNone)
, ResponseType(ResponseTypeNo, ResponseTypeYes)
, ScrolledWindow
, Window
, applicationGetActiveWindow
, boxNew
, buttonNewFromIconName
, buttonSetRelief
, containerAdd
, dialogAddButton
, dialogGetContentArea
, dialogNew
, dialogRun
, labelNew
, labelSetEllipsize
, labelSetLabel
, labelSetMaxWidthChars
, menuAttachToWidget
, menuNewFromModel
, menuPopupAtPointer
, notebookAppendPage
, notebookDetachTab
, notebookGetNPages
, notebookNextPage
, notebookPageNum
, notebookPrevPage
, notebookSetCurrentPage
, notebookSetShowTabs
, notebookSetTabReorderable
, onButtonClicked
, onWidgetButtonPressEvent
, onWidgetKeyPressEvent
, scrolledWindowNew
, scrolledWindowSetPolicy
, setWidgetMargin
, showUriOnWindow
, widgetDestroy
, widgetGrabFocus
, widgetSetCanFocus
, widgetSetHalign
, widgetSetHexpand
, widgetShow
, windowSetFocus
, windowSetTransientFor
)
import GI.Pango (EllipsizeMode(EllipsizeModeMiddle), FontDescription)
import GI.Vte
( PtyFlags(PtyFlagsDefault)
, Terminal
, onTerminalChildExited
, onTerminalWindowTitleChanged
, regexNewForMatch
, terminalGetAllowHyperlink
, terminalGetWindowTitle
, terminalMatchAddRegex
, terminalMatchCheckEvent
, terminalNew
, terminalSetBoldIsBright
, terminalSetCursorBlinkMode
, terminalSetFont
, terminalSetScrollbackLines
, terminalSetWordCharExceptions
, terminalSpawnSync
, terminalSetAllowBold
)
import System.Directory (getSymbolicLinkTarget)
import System.Environment (lookupEnv)
import System.FilePath ((</>))
import Termonad.Gtk (terminalSetEnableSixelIfExists)
import Termonad.Lenses
( lensConfirmExit
, lensOptions
, lensShowScrollbar
, lensShowTabBar
, lensTMNotebookTabLabel
, lensTMNotebookTabTerm
, lensTMNotebookTabTermContainer
, lensTMNotebookTabs
, lensTMStateApp
, lensTMStateConfig
, lensTerm, lensTMStateWindows, lensTMWindowNotebook
)
import Termonad.Pcre (pcre2Multiline)
import Termonad.Types
( ConfigHooks(createTermHook)
, ConfigOptions(scrollbackLen, wordCharExceptions, cursorBlinkMode, boldIsBright, enableSixel, allowBold)
, ShowScrollbar(..)
, ShowTabBar(..)
, TMConfig(hooks, options)
, TMNotebook
, TMNotebookTab
, TMState
, TMState'(TMState, tmStateConfig, tmStateFontDesc)
, TMTerm
, TMWindowId
, assertInvariantTMState
, createTMNotebookTab
, getNotebookFromTMState
, getTMNotebookFromTMState
, getTMNotebookFromTMState'
, getTMWindowFromTMState
, getTMWindowFromWins
, newTMTerm
, pid
, tmNotebook
, tmNotebookTabTerm
, tmNotebookTabTermContainer
, tmNotebookTabs
, tmStateWindows
, tmWindowAppWin
, tmWindowNotebook
)
-- | Change focus to the next tab.
termNextPage :: TMState -> TMWindowId -> IO ()
termNextPage mvarTMState tmWinId = do
note <- getNotebookFromTMState mvarTMState tmWinId
notebookNextPage note
-- | Change focus to the previous tab.
termPrevPage :: TMState -> TMWindowId -> IO ()
termPrevPage mvarTMState tmWinId = do
note <- getNotebookFromTMState mvarTMState tmWinId
notebookPrevPage note
termExitFocused :: TMState -> TMWindowId -> IO ()
termExitFocused mvarTMState tmWinId = do
note <- getTMNotebookFromTMState mvarTMState tmWinId
let maybeTab = getFocusItemFL $ tmNotebookTabs note
case maybeTab of
Nothing -> pure ()
Just tab -> termClose tab mvarTMState tmWinId
termClose :: TMNotebookTab -> TMState -> TMWindowId -> IO ()
termClose tab mvarTMState tmWindowId = do
tmState <- readMVar mvarTMState
let confirm = tmState ^. lensTMStateConfig . lensOptions . lensConfirmExit
close = if confirm then termExitWithConfirmation else termExit
close tab mvarTMState tmWindowId
termExitWithConfirmation :: TMNotebookTab -> TMState -> TMWindowId -> IO ()
termExitWithConfirmation tab mvarTMState tmWinId = do
tmState <- readMVar mvarTMState
let app = tmState ^. lensTMStateApp
win <- applicationGetActiveWindow app
dialog <- dialogNew
box <- dialogGetContentArea dialog
label <- labelNew (Just "Close tab?")
containerAdd box label
widgetShow label
setWidgetMargin label 10
void $
dialogAddButton
dialog
"No, do NOT close tab"
(fromIntegral (fromEnum ResponseTypeNo))
void $
dialogAddButton
dialog
"Yes, close tab"
(fromIntegral (fromEnum ResponseTypeYes))
windowSetTransientFor dialog win
res <- dialogRun dialog
widgetDestroy dialog
case toEnum (fromIntegral res) of
ResponseTypeYes -> termExit tab mvarTMState tmWinId
_ -> pure ()
termExit :: TMNotebookTab -> TMState -> TMWindowId -> IO ()
termExit tab mvarTMState tmWinId = do
detachTabAction <-
modifyMVar mvarTMState $ \tmState -> do
tmNote <- getTMNotebookFromTMState' tmState tmWinId
let detachTabAction :: IO ()
detachTabAction =
notebookDetachTab
(tmNotebook tmNote)
(tmNotebookTabTermContainer tab)
newTabs = deleteFL tab (tmNotebookTabs tmNote)
newTMState =
set
(lensTMStateWindows . ix tmWinId . lensTMWindowNotebook . lensTMNotebookTabs)
newTabs
tmState
pure (newTMState, detachTabAction)
detachTabAction
tmWin <- getTMWindowFromTMState mvarTMState tmWinId
relabelTabs (tmWindowNotebook tmWin)
relabelTabs :: TMNotebook -> IO ()
relabelTabs tmNote = do
let notebook = tmNotebook tmNote
tabFocusList = tmNotebookTabs tmNote
foldMap (go notebook) tabFocusList
where
go :: Notebook -> TMNotebookTab -> IO ()
go notebook tmNotebookTab = do
let label = tmNotebookTab ^. lensTMNotebookTabLabel
scrolledWin = tmNotebookTab ^. lensTMNotebookTabTermContainer
term' = tmNotebookTab ^. lensTMNotebookTabTerm . lensTerm
relabelTab notebook label scrolledWin term'
-- | Compute the text for a 'Label' for a GTK Notebook tab.
--
-- >>> computeTabLabel 0 (Just "me@machine:~")
-- "1. me@machine:~"
--
-- >>> computeTabLabel 5 (Just "bash process")
-- "6. bash process"
--
-- >>> computeTabLabel 2 Nothing
-- "3. shell"
computeTabLabel
:: Int
-- ^ Tab number. 0 is used for the first tab, 1 for the second, etc.
-> Maybe Text
-- ^ A possible title for a tab. If this is 'Nothing', then the string
-- @shell@ will be used.
-> Text
computeTabLabel pageNum maybeTitle =
let title = fromMaybe "shell" maybeTitle
in tshow (pageNum + 1) <> ". " <> title
-- | Update the given 'Label' for a GTK Notebook tab.
--
-- The new text for the label is determined by the 'computeTabLabel' function.
relabelTab :: Notebook -> Label -> ScrolledWindow -> Terminal -> IO ()
relabelTab notebook label scrolledWin term' = do
tabNum <- notebookPageNum notebook scrolledWin
maybeTitle <- terminalGetWindowTitle term'
let labelText = computeTabLabel (fromIntegral tabNum) maybeTitle
labelSetLabel label labelText
showScrollbarToPolicy :: ShowScrollbar -> PolicyType
showScrollbarToPolicy ShowScrollbarNever = PolicyTypeNever
showScrollbarToPolicy ShowScrollbarIfNeeded = PolicyTypeAutomatic
showScrollbarToPolicy ShowScrollbarAlways = PolicyTypeAlways
createScrolledWin :: TMState -> IO ScrolledWindow
createScrolledWin mvarTMState = do
tmState <- readMVar mvarTMState
let showScrollbarVal =
tmState ^. lensTMStateConfig . lensOptions . lensShowScrollbar
vScrollbarPolicy = showScrollbarToPolicy showScrollbarVal
scrolledWin <-
scrolledWindowNew
(Nothing :: Maybe Adjustment)
(Nothing :: Maybe Adjustment)
widgetShow scrolledWin
scrolledWindowSetPolicy scrolledWin PolicyTypeAutomatic vScrollbarPolicy
pure scrolledWin
createNotebookTabLabel :: IO (Box, Label, Button)
createNotebookTabLabel = do
box <- boxNew OrientationHorizontal 5
label <- labelNew (Just "")
labelSetEllipsize label EllipsizeModeMiddle
labelSetMaxWidthChars label 10
widgetSetHexpand label True
widgetSetHalign label AlignFill
button <-
buttonNewFromIconName
(Just "window-close")
(fromIntegral (fromEnum IconSizeMenu))
buttonSetRelief button ReliefStyleNone
containerAdd box label
containerAdd box button
widgetSetCanFocus button False
widgetSetCanFocus label False
widgetSetCanFocus box False
widgetShow box
widgetShow label
widgetShow button
pure (box, label, button)
setShowTabs :: TMConfig -> Notebook -> IO ()
setShowTabs tmConfig note = do
npages <- notebookGetNPages note
let shouldShowTabs =
case tmConfig ^. lensOptions . lensShowTabBar of
ShowTabBarIfNeeded -> npages > 1
ShowTabBarAlways -> True
ShowTabBarNever -> False
notebookSetShowTabs note shouldShowTabs
toRGBA :: Colour Double -> IO RGBA
toRGBA colour = do
let RGB red green blue = toSRGB colour
rgba <- newZeroRGBA
setRGBARed rgba red
setRGBAGreen rgba green
setRGBABlue rgba blue
pure rgba
-- | TODO: This should probably be implemented in an external package,
-- since it is a generally useful utility.
--
-- It should also be implemented for windows and osx.
cwdOfPid :: Int -> IO (Maybe Text)
cwdOfPid pd = do
#ifdef mingw32_HOST_OS
pure Nothing
#else
#ifdef darwin_HOST_OS
pure Nothing
#else
let pidPath = "/proc" </> show pd </> "cwd"
eitherLinkTarget <- try $ getSymbolicLinkTarget pidPath
case eitherLinkTarget of
Left (_ :: IOException) -> pure Nothing
Right linkTarget -> pure $ Just $ pack linkTarget
#endif
#endif
-- | Get the current working directory from the shell in the focused tab of a
-- notebook.
--
-- Returns 'Nothing' if there is no focused tab of the notebook, or the
-- current working directory could not be detected for the shell.
getCWDFromFocusedTab :: TMNotebook -> IO (Maybe Text)
getCWDFromFocusedTab currNote = do
let maybeFocusedTab = getFocusItemFL (tmNotebookTabs currNote)
case maybeFocusedTab of
Nothing -> pure Nothing
Just focusedNotebookTab -> do
let shellPid = pid (tmNotebookTabTerm focusedNotebookTab)
cwdOfPid shellPid
-- | Create the VTE 'Terminal', set the fonts and options
createAndInitVteTerm :: FontDescription -> ConfigOptions -> IO Terminal
createAndInitVteTerm tmStateFontDesc curOpts = do
vteTerm <- terminalNew
terminalSetFont vteTerm (Just tmStateFontDesc)
terminalSetWordCharExceptions vteTerm $ wordCharExceptions curOpts
terminalSetScrollbackLines vteTerm (fromIntegral (scrollbackLen curOpts))
terminalSetCursorBlinkMode vteTerm (cursorBlinkMode curOpts)
terminalSetBoldIsBright vteTerm (boldIsBright curOpts)
terminalSetEnableSixelIfExists vteTerm (enableSixel curOpts)
terminalSetAllowBold vteTerm (allowBold curOpts)
widgetShow vteTerm
pure vteTerm
-- | Starts a shell in a terminal and return a new TMTerm
launchShell
:: Terminal
-- ^ GTK 'Terminal' to spawn the shell in.
-> Maybe Text
-- ^ An optional path to the current working directory to start the
-- shell in. If 'Nothing', use the current working directory of the
-- termonad process.
-> IO Int
launchShell vteTerm maybeCurrDir = do
-- Should probably use GI.Vte.Functions.getUserShell, but contrary to its
-- documentation it raises an exception rather wrap in Maybe.
mShell <- lookupEnv "SHELL"
let argv = maybe ["/usr/bin/env", "bash"] pure mShell
-- Launch the shell
shellPid <-
terminalSpawnSync
vteTerm
[PtyFlagsDefault]
maybeCurrDir
argv
Nothing
([SpawnFlagsDefault] :: [SpawnFlags])
Nothing
(Nothing :: Maybe Cancellable)
pure (fromIntegral shellPid)
-- | Add a page to the notebook and switch to it.
addPage
:: TMState
-> TMWindowId
-> TMNotebookTab
-> Box
-- ^ The GTK Object holding the label we want to show for the tab of the
-- newly created page of the notebook.
-> IO ()
addPage mvarTMState tmWinId notebookTab tabLabelBox = do
-- Append a new notebook page and update the TMState to reflect this.
(note, pageIndex) <- modifyMVar mvarTMState appendNotebookPage
-- Switch the current Notebook page to the the newly added page.
notebookSetCurrentPage note pageIndex
where
appendNotebookPage :: TMState' -> IO (TMState', (Notebook, Int32))
appendNotebookPage tmState = do
notebook <- getTMNotebookFromTMState' tmState tmWinId
let note = tmNotebook notebook
tabs = tmNotebookTabs notebook
scrolledWin = tmNotebookTabTermContainer notebookTab
pageIndex <- notebookAppendPage note scrolledWin (Just tabLabelBox)
notebookSetTabReorderable note scrolledWin True
setShowTabs (tmState ^. lensTMStateConfig) note
let newTabs = appendFL tabs notebookTab
newTMState =
set
(lensTMStateWindows . ix tmWinId . lensTMWindowNotebook . lensTMNotebookTabs)
newTabs
tmState
pure (newTMState, (note, pageIndex))
-- | Set the keyboard focus on a vte terminal
setFocusOn :: ApplicationWindow -> Terminal -> IO()
setFocusOn tmStateAppWin vteTerm = do
widgetGrabFocus vteTerm
windowSetFocus tmStateAppWin (Just vteTerm)
-- | Create a new 'TMTerm', setting it up and adding it to the GTKNotebook.
createTerm
:: (TMState -> TMWindowId -> EventKey -> IO Bool)
-- ^ Funtion for handling key presses on the terminal.
-> TMState
-> TMWindowId
-> IO TMTerm
createTerm handleKeyPress mvarTMState tmWinId = do
-- Check preconditions
assertInvariantTMState mvarTMState
-- Read needed data in TMVar
-- TMState{tmStateAppWin, tmStateFontDesc, tmStateConfig, tmStateNotebook=currNote} <-
-- readMVar mvarTMState
TMState{tmStateFontDesc, tmStateConfig, tmStateWindows} <-
readMVar mvarTMState
tmWin <- getTMWindowFromWins tmStateWindows tmWinId
let appWin = tmWindowAppWin tmWin
currNote = tmWindowNotebook tmWin
-- Create a new terminal and launch a shell in it
vteTerm <- createAndInitVteTerm tmStateFontDesc (options tmStateConfig)
maybeCurrDir <- getCWDFromFocusedTab currNote
termShellPid <- launchShell vteTerm maybeCurrDir
tmTerm <- newTMTerm vteTerm termShellPid
-- Create the container add the VTE term in it
scrolledWin <- createScrolledWin mvarTMState
containerAdd scrolledWin vteTerm
-- Create the GTK widget for the Notebook tab
(tabLabelBox, tabLabel, tabCloseButton) <- createNotebookTabLabel
-- Create notebook state
let notebookTab = createTMNotebookTab tabLabel scrolledWin tmTerm
-- Add the new notebooktab to the notebook.
addPage mvarTMState tmWinId notebookTab tabLabelBox
-- Setup the initial label for the notebook tab. This needs to happen
-- after we add the new page to the notebook, so that the page can get labelled
-- appropriately.
relabelTab (tmNotebook currNote) tabLabel scrolledWin vteTerm
-- Connect callbacks
void $ onButtonClicked tabCloseButton $ termClose notebookTab mvarTMState tmWinId
void $ onTerminalWindowTitleChanged vteTerm $ do
relabelTab (tmNotebook currNote) tabLabel scrolledWin vteTerm
void $ onWidgetKeyPressEvent vteTerm $ handleKeyPress mvarTMState tmWinId
void $ onWidgetKeyPressEvent scrolledWin $ handleKeyPress mvarTMState tmWinId
void $ onWidgetButtonPressEvent vteTerm $ handleMousePress appWin vteTerm
void $ onTerminalChildExited vteTerm $ \_ -> termExit notebookTab mvarTMState tmWinId
-- Underline URLs so that the user can see they are right-clickable.
--
-- This regex is from https://www.regextester.com/94502
--
-- TODO: Roxterm and gnome-terminal have a much more in-depth set of regexes
-- for URLs and things similar to URLs. At some point it might make sense to
-- switch to something like this:
-- https://github.com/realh/roxterm/blob/30f1faf8be4ccac8ba12b59feb5b8f758bc65a7b/src/roxterm-regex.c
-- and
-- https://github.com/realh/roxterm/blob/30f1faf8be4ccac8ba12b59feb5b8f758bc65a7b/src/terminal-regex.h
let regexPat =
"(?:http(s)?:\\/\\/)[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+"
-- We must set the pcre2Multiline option, otherwise VTE prints a warning.
let pcreFlags = fromIntegral pcre2Multiline
regex <- regexNewForMatch regexPat (fromIntegral $ Text.length regexPat) pcreFlags
void $ terminalMatchAddRegex vteTerm regex 0
-- Put the keyboard focus on the term
setFocusOn appWin vteTerm
-- Make sure the state is still right
assertInvariantTMState mvarTMState
-- Run user-defined hooks for modifying the newly-created VTE Terminal.
createTermHook (hooks tmStateConfig) mvarTMState vteTerm
pure tmTerm
-- | Popup the context menu on right click
handleMousePress :: ApplicationWindow -> Terminal -> EventButton -> IO Bool
handleMousePress win vteTerm eventButton = do
x <- terminalGetAllowHyperlink vteTerm
print x
button <- getEventButtonButton eventButton
let rightClick = button == fromIntegral BUTTON_SECONDARY
when rightClick $ do
menuModel <- menuNew
-- if the user right-clicked on a URL, add an option to open the URL
-- in a browser
(maybeUrl, _regexId) <- terminalMatchCheckEvent vteTerm (eventButtonToEvent eventButton)
case maybeUrl of
Nothing -> pure ()
Just url -> do
openUrlAction <- simpleActionNew "openurl" Nothing
void $ onSimpleActionActivate openUrlAction $ \_ ->
showUriOnWindow (Nothing :: Maybe Window) url (fromIntegral CURRENT_TIME)
-- This will add the openurl action to the Application Window's action
-- map everytime the user right-clicks on a URL. It is okay to add
-- actions multiple times.
actionMapAddAction win openUrlAction
menuAppend menuModel (Just "Open URL in browser") (Just "win.openurl")
menuAppend menuModel (Just "Copy") (Just "app.copy")
menuAppend menuModel (Just "Paste") (Just "app.paste")
menuAppend menuModel (Just "Preferences") (Just "app.preferences")
menu <- menuNewFromModel menuModel
menuAttachToWidget menu vteTerm Nothing
menuPopupAtPointer menu Nothing
pure rightClick
-- The terminalMatchCheckEvent function takes an Event, while we only
-- have an EventButton. It is apparently okay to just cast an EventButton
-- to an Event, since they are just pointers under the hood, and they
-- are laid out the same in memory. See
-- https://github.com/haskell-gi/haskell-gi/issues/109
eventButtonToEvent :: EventButton -> Event
eventButtonToEvent = Event . coerce . toManagedPtr