-
-
Notifications
You must be signed in to change notification settings - Fork 49
/
Copy pathApp.hs
962 lines (888 loc) · 35.4 KB
/
App.hs
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
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
module Termonad.App where
import Termonad.Prelude
import Config.Dyre (defaultParams, projectName, realMain, showError, wrapMain)
import Control.Lens ((.~), (^.), (^..), over, set, view)
import Control.Monad.Fail (fail)
import Data.FocusList (focusList, moveFromToFL, updateFocusFL)
import Data.Sequence (findIndexR)
import GI.Gdk (castTo, managedForeignPtr, screenGetDefault)
import GI.Gio
( ApplicationFlags(ApplicationFlagsFlagsNone)
, MenuModel(MenuModel)
, actionMapAddAction
, applicationQuit
, applicationRun
, onApplicationActivate
, onApplicationStartup
, onSimpleActionActivate
, simpleActionNew
)
import GI.Gtk
( Application
, ApplicationWindow(ApplicationWindow)
, Box(Box)
, CheckButton(CheckButton)
, ComboBoxText(ComboBoxText)
, Dialog(Dialog)
, Entry(Entry)
, FontButton(FontButton)
, Label(Label)
, PolicyType(PolicyTypeAutomatic)
, PositionType(PositionTypeRight)
, ResponseType(ResponseTypeAccept, ResponseTypeNo, ResponseTypeYes)
, ScrolledWindow(ScrolledWindow)
, SpinButton(SpinButton)
, pattern STYLE_PROVIDER_PRIORITY_APPLICATION
, aboutDialogNew
, adjustmentNew
, applicationAddWindow
, applicationGetActiveWindow
, applicationSetAccelsForAction
, applicationSetMenubar
, applicationWindowSetShowMenubar
, boxPackStart
, builderNewFromString
, builderSetApplication
, comboBoxGetActiveId
, comboBoxSetActiveId
, comboBoxTextAppend
, containerAdd
, cssProviderLoadFromData
, cssProviderNew
, dialogAddButton
, dialogGetContentArea
, dialogNew
, dialogResponse
, dialogRun
, entryBufferGetText
, entryBufferSetText
, entryGetText
, entryNew
, fontChooserSetFontDesc
, fontChooserGetFontDesc
, getEntryBuffer
, gridAttachNextTo
, gridNew
, labelNew
, notebookGetNPages
, notebookNew
, notebookSetShowBorder
, onEntryActivate
, onNotebookPageRemoved
, onNotebookPageReordered
, onNotebookSwitchPage
, onWidgetDeleteEvent
, scrolledWindowSetPolicy
, setWidgetMargin
, spinButtonGetValueAsInt
, spinButtonSetAdjustment
, spinButtonSetValue
, styleContextAddProviderForScreen
, toggleButtonGetActive
, toggleButtonSetActive
, widgetDestroy
, widgetGrabFocus
, widgetSetCanFocus
, widgetSetVisible
, widgetShow
, widgetShowAll
, windowPresent
, windowSetDefaultIconFromFile
, windowSetTitle
, windowSetTransientFor
)
import qualified GI.Gtk as Gtk
import GI.Pango
( FontDescription
, pattern SCALE
, fontDescriptionGetFamily
, fontDescriptionGetSize
, fontDescriptionGetSizeIsAbsolute
, fontDescriptionNew
, fontDescriptionSetFamily
, fontDescriptionSetSize
, fontDescriptionSetAbsoluteSize
)
import GI.Vte
( CursorBlinkMode(..)
, catchRegexError
, regexNewForSearch
, terminalCopyClipboard
, terminalPasteClipboard
, terminalSearchFindNext
, terminalSearchFindPrevious
, terminalSearchSetRegex
, terminalSearchSetWrapAround
, terminalSetBoldIsBright
, terminalSetCursorBlinkMode
, terminalSetFont
, terminalSetScrollbackLines
, terminalSetWordCharExceptions
, terminalSetAllowBold
)
import System.Environment (getExecutablePath)
import System.FilePath (takeFileName)
import Paths_termonad (getDataFileName)
import Termonad.Gtk (appNew, objFromBuildUnsafe, terminalSetEnableSixelIfExists)
import Termonad.Keys (handleKeyPress)
import Termonad.Lenses
( lensBoldIsBright
, lensEnableSixel
, lensAllowBold
, lensConfirmExit
, lensCursorBlinkMode
, lensFontConfig
, lensOptions
, lensShowMenu
, lensShowScrollbar
, lensShowTabBar
, lensScrollbackLen
, lensTMNotebook
, lensTMNotebookTabTermContainer
, lensTMNotebookTabs
, lensTMNotebookTabTerm
, lensTMStateApp
, lensTMStateAppWin
, lensTMStateConfig
, lensTMStateFontDesc
, lensTMStateNotebook
, lensTerm
, lensWordCharExceptions
)
import Termonad.PreferencesFile (saveToPreferencesFile)
import Termonad.Term
( createTerm
, relabelTabs
, termNextPage
, termPrevPage
, termExitFocused
, setShowTabs
, showScrollbarToPolicy
)
import Termonad.Types
( ConfigOptions(..)
, FontConfig(..)
, FontSize(FontSizePoints, FontSizeUnits)
, ShowScrollbar(..)
, ShowTabBar(..)
, TMConfig
, TMNotebookTab
, TMState
, TMState'(TMState)
, getFocusedTermFromState
, modFontSize
, newEmptyTMState
, tmNotebookTabTermContainer
, tmNotebookTabs
, tmStateApp
, tmStateNotebook
)
import Termonad.XML (interfaceText, menuText, preferencesText)
setupScreenStyle :: IO ()
setupScreenStyle = do
maybeScreen <- screenGetDefault
case maybeScreen of
Nothing -> pure ()
Just screen -> do
cssProvider <- cssProviderNew
let (textLines :: [Text]) =
[
"scrollbar {"
-- , " -GtkRange-slider-width: 200px;"
-- , " -GtkRange-stepper-size: 200px;"
-- , " border-width: 200px;"
, " background-color: #aaaaaa;"
-- , " color: #ff0000;"
-- , " min-width: 4px;"
, "}"
-- , "scrollbar trough {"
-- , " -GtkRange-slider-width: 200px;"
-- , " -GtkRange-stepper-size: 200px;"
-- , " border-width: 200px;"
-- , " background-color: #00ff00;"
-- , " color: #00ff00;"
-- , " min-width: 50px;"
-- , "}"
-- , "scrollbar slider {"
-- , " -GtkRange-slider-width: 200px;"
-- , " -GtkRange-stepper-size: 200px;"
-- , " border-width: 200px;"
-- , " background-color: #0000ff;"
-- , " color: #0000ff;"
-- , " min-width: 50px;"
-- , "}"
, "tab {"
, " background-color: transparent;"
, "}"
]
let styleData = encodeUtf8 (unlines textLines :: Text)
cssProviderLoadFromData cssProvider styleData
styleContextAddProviderForScreen
screen
cssProvider
(fromIntegral STYLE_PROVIDER_PRIORITY_APPLICATION)
createFontDescFromConfig :: TMConfig -> IO FontDescription
createFontDescFromConfig tmConfig = do
let fontConf = tmConfig ^. lensOptions . lensFontConfig
createFontDesc (fontSize fontConf) (fontFamily fontConf)
createFontDesc :: FontSize -> Text -> IO FontDescription
createFontDesc fontSz fontFam = do
fontDesc <- fontDescriptionNew
fontDescriptionSetFamily fontDesc fontFam
setFontDescSize fontDesc fontSz
pure fontDesc
setFontDescSize :: FontDescription -> FontSize -> IO ()
setFontDescSize fontDesc (FontSizePoints points) =
fontDescriptionSetSize fontDesc $ fromIntegral (points * fromIntegral SCALE)
setFontDescSize fontDesc (FontSizeUnits units) =
fontDescriptionSetAbsoluteSize fontDesc $ units * fromIntegral SCALE
adjustFontDescSize :: (FontSize -> FontSize) -> FontDescription -> IO ()
adjustFontDescSize f fontDesc = do
currFontSz <- fontSizeFromFontDescription fontDesc
let newFontSz = f currFontSz
setFontDescSize fontDesc newFontSz
modifyFontSizeForAllTerms :: (FontSize -> FontSize) -> TMState -> IO ()
modifyFontSizeForAllTerms modFontSizeFunc mvarTMState = do
tmState <- readMVar mvarTMState
let fontDesc = tmState ^. lensTMStateFontDesc
adjustFontDescSize modFontSizeFunc fontDesc
let terms =
tmState ^..
lensTMStateNotebook .
lensTMNotebookTabs .
traverse .
lensTMNotebookTabTerm .
lensTerm
foldMap (\vteTerm -> terminalSetFont vteTerm (Just fontDesc)) terms
fontSizeFromFontDescription :: FontDescription -> IO FontSize
fontSizeFromFontDescription fontDesc = do
currSize <- fontDescriptionGetSize fontDesc
currAbsolute <- fontDescriptionGetSizeIsAbsolute fontDesc
return $ if currAbsolute
then FontSizeUnits $ fromIntegral currSize / fromIntegral SCALE
else
let fontRatio :: Double = fromIntegral currSize / fromIntegral SCALE
in FontSizePoints $ round fontRatio
fontConfigFromFontDescription :: FontDescription -> IO (Maybe FontConfig)
fontConfigFromFontDescription fontDescription = do
fontSize <- fontSizeFromFontDescription fontDescription
maybeFontFamily <- fontDescriptionGetFamily fontDescription
return $ (`FontConfig` fontSize) <$> maybeFontFamily
compareScrolledWinAndTab :: ScrolledWindow -> TMNotebookTab -> Bool
compareScrolledWinAndTab scrollWin flTab =
let ScrolledWindow managedPtrFLTab = tmNotebookTabTermContainer flTab
foreignPtrFLTab = managedForeignPtr managedPtrFLTab
ScrolledWindow managedPtrScrollWin = scrollWin
foreignPtrScrollWin = managedForeignPtr managedPtrScrollWin
in foreignPtrFLTab == foreignPtrScrollWin
updateFLTabPos :: TMState -> Int -> Int -> IO ()
updateFLTabPos mvarTMState oldPos newPos =
modifyMVar_ mvarTMState $ \tmState -> do
let tabs = tmState ^. lensTMStateNotebook . lensTMNotebookTabs
maybeNewTabs = moveFromToFL oldPos newPos tabs
case maybeNewTabs of
Nothing -> do
putStrLn $
"in updateFLTabPos, Strange error: couldn't move tabs.\n" <>
"old pos: " <> tshow oldPos <> "\n" <>
"new pos: " <> tshow newPos <> "\n" <>
"tabs: " <> tshow tabs <> "\n" <>
"maybeNewTabs: " <> tshow maybeNewTabs <> "\n" <>
"tmState: " <> tshow tmState
pure tmState
Just newTabs ->
pure $
tmState &
lensTMStateNotebook . lensTMNotebookTabs .~ newTabs
-- | Try to figure out whether Termonad should exit. This also used to figure
-- out if Termonad should close a given terminal.
--
-- This reads the 'confirmExit' setting from 'ConfigOptions' to check whether
-- the user wants to be notified when either Termonad or a given terminal is
-- about to be closed.
--
-- If 'confirmExit' is 'True', then a dialog is presented to the user asking
-- them if they really want to exit or close the terminal. Their response is
-- sent back as a 'ResponseType'.
--
-- If 'confirmExit' is 'False', then this function always returns
-- 'ResponseTypeYes'.
{- HLINT ignore "Reduce duplication" -}
askShouldExit :: TMState -> IO ResponseType
askShouldExit mvarTMState = do
tmState <- readMVar mvarTMState
let confirm = tmState ^. lensTMStateConfig . lensOptions . lensConfirmExit
if confirm
then confirmationDialogForExit tmState
else pure ResponseTypeYes
where
-- Show the user a dialog telling them there are still terminals running and
-- asking if they really want to exit.
--
-- Return the user's resposne as a 'ResponseType'.
confirmationDialogForExit :: TMState' -> IO ResponseType
confirmationDialogForExit tmState = do
let app = tmState ^. lensTMStateApp
win <- applicationGetActiveWindow app
dialog <- dialogNew
box <- dialogGetContentArea dialog
label <-
labelNew $
Just
"There are still terminals running. Are you sure you want to exit?"
containerAdd box label
widgetShow label
setWidgetMargin label 10
void $
dialogAddButton
dialog
"No, do NOT exit"
(fromIntegral (fromEnum ResponseTypeNo))
void $
dialogAddButton
dialog
"Yes, exit"
(fromIntegral (fromEnum ResponseTypeYes))
windowSetTransientFor dialog win
res <- dialogRun dialog
widgetDestroy dialog
pure $ toEnum (fromIntegral res)
-- | Force Termonad to exit without asking the user whether or not to do so.
forceQuit :: TMState -> IO ()
forceQuit mvarTMState = do
tmState <- readMVar mvarTMState
let app = tmState ^. lensTMStateApp
applicationQuit app
setupTermonad :: TMConfig -> Application -> ApplicationWindow -> Gtk.Builder -> IO ()
setupTermonad tmConfig app win builder = do
termonadIconPath <- getDataFileName "img/termonad-lambda.png"
windowSetDefaultIconFromFile termonadIconPath
setupScreenStyle
box <- objFromBuildUnsafe builder "content_box" Box
fontDesc <- createFontDescFromConfig tmConfig
note <- notebookNew
widgetSetCanFocus note False
-- If this is not set to False, then there will be a one pixel white border
-- shown around the notebook.
notebookSetShowBorder note False
boxPackStart box note True True 0
mvarTMState <- newEmptyTMState tmConfig app win note fontDesc
terminal <- createTerm handleKeyPress mvarTMState
void $ onNotebookPageRemoved note $ \_ _ -> do
pages <- notebookGetNPages note
if pages == 0
then forceQuit mvarTMState
else setShowTabs tmConfig note
void $ onNotebookSwitchPage note $ \_ pageNum -> do
modifyMVar_ mvarTMState $ \tmState -> do
let notebook = tmStateNotebook tmState
tabs = tmNotebookTabs notebook
maybeNewTabs = updateFocusFL (fromIntegral pageNum) tabs
case maybeNewTabs of
Nothing -> pure tmState
Just (tab, newTabs) -> do
widgetGrabFocus $ tab ^. lensTMNotebookTabTerm . lensTerm
pure $
tmState &
lensTMStateNotebook . lensTMNotebookTabs .~ newTabs
void $ onNotebookPageReordered note $ \childWidg pageNum -> do
maybeScrollWin <- castTo ScrolledWindow childWidg
case maybeScrollWin of
Nothing ->
fail $
"In setupTermonad, in callback for onNotebookPageReordered, " <>
"child widget is not a ScrolledWindow.\n" <>
"Don't know how to continue.\n"
Just scrollWin -> do
TMState{tmStateNotebook} <- readMVar mvarTMState
let fl = tmStateNotebook ^. lensTMNotebookTabs
let maybeOldPosition =
findIndexR (compareScrolledWinAndTab scrollWin) (focusList fl)
case maybeOldPosition of
Nothing ->
fail $
"In setupTermonad, in callback for onNotebookPageReordered, " <>
"the ScrolledWindow is not already in the FocusList.\n" <>
"Don't know how to continue.\n"
Just oldPos -> do
updateFLTabPos mvarTMState oldPos (fromIntegral pageNum)
relabelTabs mvarTMState
newTabAction <- simpleActionNew "newtab" Nothing
void $ onSimpleActionActivate newTabAction $ \_ -> void $ createTerm handleKeyPress mvarTMState
actionMapAddAction app newTabAction
applicationSetAccelsForAction app "app.newtab" ["<Shift><Ctrl>T"]
nextPageAction <- simpleActionNew "nextpage" Nothing
void $ onSimpleActionActivate nextPageAction $ \_ ->
termNextPage mvarTMState
actionMapAddAction app nextPageAction
applicationSetAccelsForAction app "app.nextpage" ["<Ctrl>Page_Down"]
prevPageAction <- simpleActionNew "prevpage" Nothing
void $ onSimpleActionActivate prevPageAction $ \_ ->
termPrevPage mvarTMState
actionMapAddAction app prevPageAction
applicationSetAccelsForAction app "app.prevpage" ["<Ctrl>Page_Up"]
closeTabAction <- simpleActionNew "closetab" Nothing
void $ onSimpleActionActivate closeTabAction $ \_ ->
termExitFocused mvarTMState
actionMapAddAction app closeTabAction
applicationSetAccelsForAction app "app.closetab" ["<Shift><Ctrl>W"]
quitAction <- simpleActionNew "quit" Nothing
void $ onSimpleActionActivate quitAction $ \_ -> do
shouldExit <- askShouldExit mvarTMState
when (shouldExit == ResponseTypeYes) $ forceQuit mvarTMState
actionMapAddAction app quitAction
applicationSetAccelsForAction app "app.quit" ["<Shift><Ctrl>Q"]
copyAction <- simpleActionNew "copy" Nothing
void $ onSimpleActionActivate copyAction $ \_ -> do
maybeTerm <- getFocusedTermFromState mvarTMState
maybe (pure ()) terminalCopyClipboard maybeTerm
actionMapAddAction app copyAction
applicationSetAccelsForAction app "app.copy" ["<Shift><Ctrl>C"]
pasteAction <- simpleActionNew "paste" Nothing
void $ onSimpleActionActivate pasteAction $ \_ -> do
maybeTerm <- getFocusedTermFromState mvarTMState
maybe (pure ()) terminalPasteClipboard maybeTerm
actionMapAddAction app pasteAction
applicationSetAccelsForAction app "app.paste" ["<Shift><Ctrl>V"]
preferencesAction <- simpleActionNew "preferences" Nothing
void $ onSimpleActionActivate preferencesAction (const $ showPreferencesDialog mvarTMState)
actionMapAddAction app preferencesAction
enlargeFontAction <- simpleActionNew "enlargefont" Nothing
void $ onSimpleActionActivate enlargeFontAction $ \_ ->
modifyFontSizeForAllTerms (modFontSize 1) mvarTMState
actionMapAddAction app enlargeFontAction
applicationSetAccelsForAction app "app.enlargefont" ["<Ctrl>plus"]
reduceFontAction <- simpleActionNew "reducefont" Nothing
void $ onSimpleActionActivate reduceFontAction $ \_ ->
modifyFontSizeForAllTerms (modFontSize (-1)) mvarTMState
actionMapAddAction app reduceFontAction
applicationSetAccelsForAction app "app.reducefont" ["<Ctrl>minus"]
findAction <- simpleActionNew "find" Nothing
void $ onSimpleActionActivate findAction $ \_ -> doFind mvarTMState
actionMapAddAction app findAction
applicationSetAccelsForAction app "app.find" ["<Shift><Ctrl>F"]
findAboveAction <- simpleActionNew "findabove" Nothing
void $ onSimpleActionActivate findAboveAction $ \_ -> findAbove mvarTMState
actionMapAddAction app findAboveAction
applicationSetAccelsForAction app "app.findabove" ["<Shift><Ctrl>P"]
findBelowAction <- simpleActionNew "findbelow" Nothing
void $ onSimpleActionActivate findBelowAction $ \_ -> findBelow mvarTMState
actionMapAddAction app findBelowAction
applicationSetAccelsForAction app "app.findbelow" ["<Shift><Ctrl>I"]
aboutAction <- simpleActionNew "about" Nothing
void $ onSimpleActionActivate aboutAction $ \_ -> showAboutDialog app
actionMapAddAction app aboutAction
menuBuilder <- builderNewFromString menuText $ fromIntegral (length menuText)
menuModel <- objFromBuildUnsafe menuBuilder "menubar" MenuModel
applicationSetMenubar app (Just menuModel)
let showMenu = tmConfig ^. lensOptions . lensShowMenu
applicationWindowSetShowMenubar win showMenu
windowSetTitle win "Termonad"
-- This event will happen if the user requests that the top-level Termonad
-- window be closed through their window manager. It will also happen
-- normally when the user tries to close Termonad through normal methods,
-- like clicking "Quit" or closing the last open terminal.
--
-- If you return 'True' from this callback, then Termonad will not exit.
-- If you return 'False' from this callback, then Termonad will continue to
-- exit.
void $ onWidgetDeleteEvent win $ \_ -> do
shouldExit <- askShouldExit mvarTMState
pure $
case shouldExit of
ResponseTypeYes -> False
_ -> True
widgetShowAll win
widgetGrabFocus $ terminal ^. lensTerm
appActivate :: TMConfig -> Application -> IO ()
appActivate tmConfig app = do
uiBuilder <-
builderNewFromString interfaceText $ fromIntegral (length interfaceText)
builderSetApplication uiBuilder app
appWin <- objFromBuildUnsafe uiBuilder "appWin" ApplicationWindow
applicationAddWindow app appWin
setupTermonad tmConfig app appWin uiBuilder
windowPresent appWin
showAboutDialog :: Application -> IO ()
showAboutDialog app = do
win <- applicationGetActiveWindow app
aboutDialog <- aboutDialogNew
windowSetTransientFor aboutDialog win
void $ dialogRun aboutDialog
widgetDestroy aboutDialog
showFindDialog :: Application -> IO (Maybe Text)
showFindDialog app = do
win <- applicationGetActiveWindow app
dialog <- dialogNew
box <- dialogGetContentArea dialog
grid <- gridNew
searchForLabel <- labelNew (Just "Search for regex:")
containerAdd grid searchForLabel
widgetShow searchForLabel
setWidgetMargin searchForLabel 10
searchEntry <- entryNew
gridAttachNextTo grid searchEntry (Just searchForLabel) PositionTypeRight 1 1
widgetShow searchEntry
setWidgetMargin searchEntry 10
-- setWidgetMarginBottom searchEntry 20
void $
onEntryActivate searchEntry $
dialogResponse dialog (fromIntegral (fromEnum ResponseTypeYes))
void $
dialogAddButton
dialog
"Close"
(fromIntegral (fromEnum ResponseTypeNo))
void $
dialogAddButton
dialog
"Find"
(fromIntegral (fromEnum ResponseTypeYes))
containerAdd box grid
widgetShow grid
windowSetTransientFor dialog win
res <- dialogRun dialog
searchString <- entryGetText searchEntry
let maybeSearchString =
case toEnum (fromIntegral res) of
ResponseTypeYes -> Just searchString
_ -> Nothing
widgetDestroy dialog
pure maybeSearchString
doFind :: TMState -> IO ()
doFind mvarTMState = do
tmState <- readMVar mvarTMState
let app = tmStateApp tmState
maybeSearchString <- showFindDialog app
-- putStrLn $ "trying to find: " <> tshow maybeSearchString
maybeTerminal <- getFocusedTermFromState mvarTMState
case (maybeSearchString, maybeTerminal) of
(Just searchString, Just terminal) -> do
-- TODO: Figure out how to import the correct pcre flags.
--
-- If you don't pass the pcre2Multiline flag, VTE gives
-- the following warning:
--
-- (termonad-linux-x86_64:18792): Vte-WARNING **:
-- 21:56:31.193: (vtegtk.cc:2269):void
-- vte_terminal_search_set_regex(VteTerminal*,
-- VteRegex*, guint32): runtime check failed:
-- (regex == nullptr ||
-- _vte_regex_get_compile_flags(regex) & PCRE2_MULTILINE)
--
-- However, if you do add the pcre2Multiline flag,
-- the terminalSearchSetRegex appears to just completely
-- not work.
let pcreFlags = 0
let newRegex =
regexNewForSearch
searchString
(fromIntegral $ length searchString)
pcreFlags
eitherRegex <-
catchRegexError
(fmap Right newRegex)
(\_ errMsg -> pure (Left errMsg))
case eitherRegex of
Left errMsg -> do
let msg = "error when creating regex: " <> errMsg
hPutStrLn stderr msg
Right regex -> do
terminalSearchSetRegex terminal (Just regex) pcreFlags
terminalSearchSetWrapAround terminal True
_matchFound <- terminalSearchFindPrevious terminal
-- TODO: Setup an actual logging framework to show these
-- kinds of log messages. Also make a similar change in
-- findAbove and findBelow.
-- putStrLn $ "was match found: " <> tshow matchFound
pure ()
_ -> pure ()
findAbove :: TMState -> IO ()
findAbove mvarTMState = do
maybeTerminal <- getFocusedTermFromState mvarTMState
case maybeTerminal of
Nothing -> pure ()
Just terminal -> do
_matchFound <- terminalSearchFindPrevious terminal
-- putStrLn $ "was match found: " <> tshow matchFound
pure ()
findBelow :: TMState -> IO ()
findBelow mvarTMState = do
maybeTerminal <- getFocusedTermFromState mvarTMState
case maybeTerminal of
Nothing -> pure ()
Just terminal -> do
_matchFound <- terminalSearchFindNext terminal
-- putStrLn $ "was match found: " <> tshow matchFound
pure ()
setShowMenuBar :: Application -> Bool -> IO ()
setShowMenuBar app visible = do
void $ runMaybeT $ do
win <- MaybeT $ applicationGetActiveWindow app
appWin <- MaybeT $ castTo ApplicationWindow win
lift $ applicationWindowSetShowMenubar appWin visible
-- | Fill a combo box with ids and labels
--
-- The ids are stored in the combobox as 'Text', so their type should be an
-- instance of the 'Show' type class.
comboBoxFill :: forall a. Show a => ComboBoxText -> [(a, Text)] -> IO ()
comboBoxFill comboBox = mapM_ go
where
go :: (a, Text) -> IO ()
go (value, textId) =
comboBoxTextAppend comboBox (Just $ tshow value) textId
-- | Set the current active item in a combobox given an input id.
comboBoxSetActive :: Show a => ComboBoxText -> a -> IO ()
comboBoxSetActive cb item = void $ comboBoxSetActiveId cb (Just $ tshow item)
-- | Get the current active item in a combobox
--
-- The list of values to be searched in the combobox must be given as a
-- parameter. These values are converted to Text then compared to the current
-- id.
comboBoxGetActive
:: forall a. (Show a, Enum a) => ComboBoxText -> [a] -> IO (Maybe a)
comboBoxGetActive cb values = findEnumFromMaybeId <$> comboBoxGetActiveId cb
where
findEnumFromMaybeId :: Maybe Text -> Maybe a
findEnumFromMaybeId maybeId = maybeId >>= findEnumFromId
findEnumFromId :: Text -> Maybe a
findEnumFromId label = find (\x -> tshow x == label) values
applyNewPreferences :: TMState -> IO ()
applyNewPreferences mvarTMState = do
tmState <- readMVar mvarTMState
let appWin = tmState ^. lensTMStateAppWin
config = tmState ^. lensTMStateConfig
notebook = tmState ^. lensTMStateNotebook . lensTMNotebook
tabFocusList = tmState ^. lensTMStateNotebook . lensTMNotebookTabs
showMenu = config ^. lensOptions . lensShowMenu
applicationWindowSetShowMenubar appWin showMenu
setShowTabs config notebook
-- Sets the remaining preferences to each tab
foldMap (applyNewPreferencesToTab mvarTMState) tabFocusList
applyNewPreferencesToTab :: TMState -> TMNotebookTab -> IO ()
applyNewPreferencesToTab mvarTMState tab = do
tmState <- readMVar mvarTMState
let fontDesc = tmState ^. lensTMStateFontDesc
term = tab ^. lensTMNotebookTabTerm . lensTerm
scrolledWin = tab ^. lensTMNotebookTabTermContainer
options = tmState ^. lensTMStateConfig . lensOptions
terminalSetFont term (Just fontDesc)
terminalSetCursorBlinkMode term (cursorBlinkMode options)
terminalSetWordCharExceptions term (wordCharExceptions options)
terminalSetScrollbackLines term (fromIntegral (scrollbackLen options))
terminalSetBoldIsBright term (boldIsBright options)
terminalSetEnableSixelIfExists term (enableSixel options)
terminalSetAllowBold term (allowBold options)
let vScrollbarPolicy = showScrollbarToPolicy (options ^. lensShowScrollbar)
scrolledWindowSetPolicy scrolledWin PolicyTypeAutomatic vScrollbarPolicy
-- | Show the preferences dialog.
--
-- When the user clicks on the Ok button, it copies the new settings to TMState.
-- Then apply them to the current terminals.
showPreferencesDialog :: TMState -> IO ()
showPreferencesDialog mvarTMState = do
-- Get app out of mvar
tmState <- readMVar mvarTMState
let app = tmState ^. lensTMStateApp
-- Create the preference dialog and get some widgets
preferencesBuilder <-
builderNewFromString preferencesText $ fromIntegral (length preferencesText)
preferencesDialog <-
objFromBuildUnsafe preferencesBuilder "preferences" Dialog
confirmExitCheckButton <-
objFromBuildUnsafe preferencesBuilder "confirmExit" CheckButton
showMenuCheckButton <-
objFromBuildUnsafe preferencesBuilder "showMenu" CheckButton
boldIsBrightCheckButton <-
objFromBuildUnsafe preferencesBuilder "boldIsBright" CheckButton
enableSixelCheckButton <-
objFromBuildUnsafe preferencesBuilder "enableSixel" CheckButton
allowBoldCheckButton <-
objFromBuildUnsafe preferencesBuilder "allowBold" CheckButton
wordCharExceptionsEntryBuffer <-
objFromBuildUnsafe preferencesBuilder "wordCharExceptions" Entry >>=
getEntryBuffer
fontButton <- objFromBuildUnsafe preferencesBuilder "font" FontButton
showScrollbarComboBoxText <-
objFromBuildUnsafe preferencesBuilder "showScrollbar" ComboBoxText
comboBoxFill
showScrollbarComboBoxText
[ (ShowScrollbarNever, "Never")
, (ShowScrollbarAlways, "Always")
, (ShowScrollbarIfNeeded, "If needed")
]
showTabBarComboBoxText <-
objFromBuildUnsafe preferencesBuilder "showTabBar" ComboBoxText
comboBoxFill
showTabBarComboBoxText
[ (ShowTabBarNever, "Never")
, (ShowTabBarAlways, "Always")
, (ShowTabBarIfNeeded, "If needed")
]
cursorBlinkModeComboBoxText <-
objFromBuildUnsafe preferencesBuilder "cursorBlinkMode" ComboBoxText
comboBoxFill
cursorBlinkModeComboBoxText
[ (CursorBlinkModeSystem, "System")
, (CursorBlinkModeOn, "On")
, (CursorBlinkModeOff, "Off")
]
scrollbackLenSpinButton <-
objFromBuildUnsafe preferencesBuilder "scrollbackLen" SpinButton
adjustmentNew 0 0 (fromIntegral (maxBound :: Int)) 1 10 0 >>=
spinButtonSetAdjustment scrollbackLenSpinButton
warningLabel <- objFromBuildUnsafe preferencesBuilder "warning" Label
-- We show the warning label only if the user has launched termonad with a
-- termonad.hs file
executablePath <- getExecutablePath
let hasTermonadHs = takeFileName executablePath == "termonad-linux-x86_64"
widgetSetVisible warningLabel hasTermonadHs
-- Make the dialog modal
maybeWin <- applicationGetActiveWindow app
windowSetTransientFor preferencesDialog maybeWin
-- Init with current state
fontChooserSetFontDesc fontButton (tmState ^. lensTMStateFontDesc)
let options = tmState ^. lensTMStateConfig . lensOptions
comboBoxSetActive showScrollbarComboBoxText $ showScrollbar options
comboBoxSetActive showTabBarComboBoxText $ showTabBar options
comboBoxSetActive cursorBlinkModeComboBoxText $ cursorBlinkMode options
spinButtonSetValue scrollbackLenSpinButton (fromIntegral $ scrollbackLen options)
toggleButtonSetActive confirmExitCheckButton $ confirmExit options
toggleButtonSetActive showMenuCheckButton $ showMenu options
toggleButtonSetActive boldIsBrightCheckButton $ boldIsBright options
toggleButtonSetActive enableSixelCheckButton $ enableSixel options
toggleButtonSetActive allowBoldCheckButton $ allowBold options
entryBufferSetText wordCharExceptionsEntryBuffer (wordCharExceptions options) (-1)
-- Run dialog then close
res <- dialogRun preferencesDialog
-- When closing the dialog get the new settings
when (toEnum (fromIntegral res) == ResponseTypeAccept) $ do
maybeFontDesc <- fontChooserGetFontDesc fontButton
maybeFontConfig <-
join <$> mapM fontConfigFromFontDescription maybeFontDesc
maybeShowScrollbar <-
comboBoxGetActive showScrollbarComboBoxText [ShowScrollbarNever ..]
maybeShowTabBar <-
comboBoxGetActive showTabBarComboBoxText [ShowTabBarNever ..]
maybeCursorBlinkMode <-
comboBoxGetActive cursorBlinkModeComboBoxText [CursorBlinkModeSystem ..]
scrollbackLenVal <-
fromIntegral <$> spinButtonGetValueAsInt scrollbackLenSpinButton
confirmExitVal <- toggleButtonGetActive confirmExitCheckButton
showMenuVal <- toggleButtonGetActive showMenuCheckButton
boldIsBrightVal <- toggleButtonGetActive boldIsBrightCheckButton
enableSixelVal <- toggleButtonGetActive enableSixelCheckButton
allowBoldVal <- toggleButtonGetActive allowBoldCheckButton
wordCharExceptionsVal <- entryBufferGetText wordCharExceptionsEntryBuffer
-- Apply the changes to mvarTMState
modifyMVar_ mvarTMState $ pure
. over lensTMStateFontDesc (`fromMaybe` maybeFontDesc)
. over (lensTMStateConfig . lensOptions)
( set lensConfirmExit confirmExitVal
. set lensShowMenu showMenuVal
. set lensBoldIsBright boldIsBrightVal
. set lensEnableSixel enableSixelVal
. set lensAllowBold allowBoldVal
. set lensWordCharExceptions wordCharExceptionsVal
. over lensFontConfig (`fromMaybe` maybeFontConfig)
. set lensScrollbackLen scrollbackLenVal
. over lensShowScrollbar (`fromMaybe` maybeShowScrollbar)
. over lensShowTabBar (`fromMaybe` maybeShowTabBar)
. over lensCursorBlinkMode (`fromMaybe` maybeCursorBlinkMode)
)
-- Save the changes to the preferences files
withMVar mvarTMState $ saveToPreferencesFile . view lensTMStateConfig
-- Update the app with new settings
applyNewPreferences mvarTMState
widgetDestroy preferencesDialog
appStartup :: Application -> IO ()
appStartup _app = pure ()
-- | Run Termonad with the given 'TMConfig'.
--
-- Do not perform any of the recompilation operations that the 'defaultMain'
-- function does.
start :: TMConfig -> IO ()
start tmConfig = do
-- app <- appNew (Just "haskell.termonad") [ApplicationFlagsFlagsNone]
-- Make sure the application is not unique, so we can open multiple copies of it.
app <- appNew Nothing [ApplicationFlagsFlagsNone]
void $ onApplicationStartup app (appStartup app)
void $ onApplicationActivate app (appActivate tmConfig app)
void $ applicationRun app Nothing
-- | Run Termonad with the given 'TMConfig'.
--
-- This function will check if there is a @~\/.config\/termonad\/termonad.hs@ file
-- and a @~\/.cache\/termonad\/termonad-linux-x86_64@ binary. Termonad will
-- perform different actions based on whether or not these two files exist.
--
-- Here are the four different possible actions based on the existence of these
-- two files.
--
-- - @~\/.config\/termonad\/termonad.hs@ exists, @~\/.cache\/termonad\/termonad-linux-x86_64@ exists
--
-- The timestamps of these two files are checked. If the
-- @~\/.config\/termonad\/termonad.hs@ file has been modified after the
-- @~\/.cache\/termonad\/termonad-linux-x86_64@ binary, then Termonad will use
-- GHC to recompile the @~\/.config\/termonad\/termonad.hs@ file, producing a
-- new binary at @~\/.cache\/termonad\/termonad-linux-x86_64@. This new binary
-- will be re-executed. The 'TMConfig' passed to this 'defaultMain' will be
-- effectively thrown away.
--
-- If GHC fails to recompile the @~\/.config\/termonad\/termonad.hs@ file, then
-- Termonad will just execute 'start' with the 'TMConfig' passed in.
--
-- If the @~\/.cache\/termonad\/termonad-linux-x86_64@ binary has been modified
-- after the @~\/.config\/termonad\/termonad.hs@ file, then Termonad will
-- re-exec the @~\/.cache\/termonad\/termonad-linux-x86_64@ binary. The
-- 'TMConfig' passed to this 'defaultMain' will be effectively thrown away.
--
-- - @~\/.config\/termonad\/termonad.hs@ exists, @~\/.cache\/termonad\/termonad-linux-x86_64@ does not exist
--
-- Termonad will use GHC to recompile the @~\/.config\/termonad\/termonad.hs@
-- file, producing a new binary at @~\/.cache\/termonad\/termonad-linux-x86_64@.
-- This new binary will be re-executed. The 'TMConfig' passed to this
-- 'defaultMain' will be effectively thrown away.
--
-- If GHC fails to recompile the @~\/.config\/termonad\/termonad.hs@ file, then
-- Termonad will just execute 'start' with the 'TMConfig' passed in.
--
-- - @~\/.config\/termonad\/termonad.hs@ does not exist, @~\/.cache\/termonad\/termonad-linux-x86_64@ exists
--
-- Termonad will ignore the @~\/.cache\/termonad\/termonad-linux-x86_64@ binary
-- and just run 'start' with the 'TMConfig' passed to this function.
--
-- - @~\/.config\/termonad\/termonad.hs@ does not exist, @~\/.cache\/termonad\/termonad-linux-x86_64@ does not exist
--
-- Termonad will run 'start' with the 'TMConfig' passed to this function.
--
-- Other notes:
--
-- 1. That the locations of @~\/.config\/termonad\/termonad.hs@ and
-- @~\/.cache\/termonad\/termonad-linux-x86_64@ may differ depending on your
-- system.
--
-- 2. In your own @~\/.config\/termonad\/termonad.hs@ file, you can use either
-- 'defaultMain' or 'start'. As long as you always execute the system-wide
-- @termonad@ binary (instead of the binary produced as
-- @~\/.cache\/termonad\/termonad-linux-x86_64@), the effect should be the same.
defaultMain :: TMConfig -> IO ()
defaultMain tmConfig = do
let params =
defaultParams
{ projectName = "termonad"
, showError = \(cfg, oldErrs) newErr -> (cfg, oldErrs <> "\n" <> newErr)
, realMain = \(cfg, errs) -> putStrLn (pack errs) *> start cfg
}
eitherRes <- tryIOError $ wrapMain params (tmConfig, "")
case eitherRes of
Left ioErr
| ioeGetErrorType ioErr == doesNotExistErrorType && ioeGetFileName ioErr == Just "ghc" -> do
putStrLn $
"Could not find ghc on your PATH. Ignoring your termonad.hs " <>
"configuration file and running termonad with default settings."
start tmConfig
| otherwise -> do
putStrLn "IO error occurred when trying to run termonad:"
print ioErr
putStrLn "Don't know how to recover. Exiting."
Right _ -> pure ()