-
-
Notifications
You must be signed in to change notification settings - Fork 275
/
Prompt.hs
1864 lines (1698 loc) · 76.6 KB
/
Prompt.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
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE MultiWayIf #-}
-----------------------------------------------------------------------------
-- |
-- Module : XMonad.Prompt
-- Copyright : (C) 2007 Andrea Rossato, 2015 Evgeny Kurnevsky
-- 2015 Sibi Prabakaran, 2018 Yclept Nemo
-- License : BSD3
--
-- Maintainer : Spencer Janssen <[email protected]>
-- Stability : unstable
-- Portability : unportable
--
-- A module for writing graphical prompts for XMonad
--
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Bugs:
-- if 'alwaysHighlight' is True, and
-- 1 type several characters
-- 2 tab-complete past several entries
-- 3 backspace back to the several characters
-- 4 tab-complete once (results in the entry past the one in [2])
-- 5 tab-complete against this shorter list of completions
-- then the prompt will freeze (XMonad continues however).
-----------------------------------------------------------------------------
module XMonad.Prompt
( -- * Usage
-- $usage
mkXPrompt
, mkXPromptWithReturn
, mkXPromptWithModes
, def
, amberXPConfig
, greenXPConfig
, XPMode
, XPType (..)
, XPColor (..)
, XPPosition (..)
, XPConfig (..)
, XPrompt (..)
, XP
, defaultXPKeymap, defaultXPKeymap'
, emacsLikeXPKeymap, emacsLikeXPKeymap'
, vimLikeXPKeymap, vimLikeXPKeymap'
, quit
, promptSubmap, promptBuffer, toHeadChar, bufferOne
, killBefore, killAfter, startOfLine, endOfLine
, insertString, pasteString, pasteString'
, clipCursor, moveCursor, moveCursorClip
, setInput, getInput, getOffset
, defaultColor, modifyColor, setColor
, resetColor, setBorderColor
, modifyPrompter, setPrompter, resetPrompter
, selectedCompletion, setCurrentCompletions, getCurrentCompletions
, moveWord, moveWord', killWord, killWord'
, changeWord, deleteString
, moveHistory, setSuccess, setDone, setModeDone
, Direction1D(..)
, ComplFunction
, ComplCaseSensitivity(..)
-- * X Utilities
-- $xutils
, mkUnmanagedWindow
, fillDrawable
-- * Other Utilities
-- $utils
, mkComplFunFromList
, mkComplFunFromList'
-- * @nextCompletion@ implementations
, getNextOfLastWord
, getNextCompletion
-- * List utilities
, getLastWord
, skipLastWord
, splitInSubListsAt
, breakAtSpace
, uniqSort
, historyCompletion
, historyCompletionP
-- * History filters
, deleteAllDuplicates
, deleteConsecutive
, HistoryMatches
, initMatches
, historyUpMatching
, historyDownMatching
-- * Types
, XPState
) where
import XMonad hiding (cleanMask, config)
import XMonad.Prelude hiding (toList, fromList)
import qualified XMonad.StackSet as W
import XMonad.Util.Font
import XMonad.Util.Types
import XMonad.Util.XSelection (getSelection)
import Codec.Binary.UTF8.String (decodeString,isUTF8Encoded)
import Control.Arrow (first, (&&&), (***))
import Control.Concurrent (threadDelay)
import Control.Exception as E hiding (handle)
import Control.Monad.State
import Data.Bifunctor (bimap)
import Data.Bits
import Data.IORef
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as M
import Data.Set (fromList, toList)
import System.IO
import System.IO.Unsafe (unsafePerformIO)
import System.Posix.Files
import Data.List.NonEmpty (nonEmpty)
-- $usage
-- For usage examples see "XMonad.Prompt.Shell",
-- "XMonad.Prompt.XMonad" or "XMonad.Prompt.Ssh"
--
-- TODO:
--
-- * scrolling the completions that don't fit in the window (?)
type XP = StateT XPState IO
data XPState =
XPS { dpy :: Display
, rootw :: !Window
, win :: !Window
, screen :: !Rectangle
, winWidth :: !Dimension -- ^ Width of the prompt window
, complWinDim :: Maybe ComplWindowDim
, complIndex :: !(Int,Int)
, complWin :: IORef (Maybe Window)
-- ^ This is an 'IORef' to enable removal of the completion
-- window if an exception occurs, since otherwise the most
-- recent value of 'complWin' would not be available.
, showComplWin :: Bool
, operationMode :: XPOperationMode
, highlightedCompl :: Maybe String
, gcon :: !GC
, fontS :: !XMonadFont
, commandHistory :: W.Stack String
, offset :: !Int
, config :: XPConfig
, successful :: Bool
, cleanMask :: KeyMask -> KeyMask
, done :: Bool
, modeDone :: Bool
, color :: XPColor
, prompter :: String -> String
, eventBuffer :: [(KeySym, String, Event)]
, inputBuffer :: String
, currentCompletions :: Maybe [String]
}
data XPConfig =
XPC { font :: String -- ^ Font. For TrueType fonts, use something like
-- @"xft:Hack:pixelsize=1"@. Alternatively, use X Logical Font
-- Description, i.e. something like
-- @"-*-dejavu sans mono-medium-r-normal--*-80-*-*-*-*-iso10646-1"@.
, bgColor :: String -- ^ Background color
, fgColor :: String -- ^ Font color
, bgHLight :: String -- ^ Background color of a highlighted completion entry
, fgHLight :: String -- ^ Font color of a highlighted completion entry
, borderColor :: String -- ^ Border color
, promptBorderWidth :: !Dimension -- ^ Border width
, position :: XPPosition -- ^ Position: 'Top', 'Bottom', or 'CenteredAt'
, alwaysHighlight :: !Bool -- ^ Always highlight an item, overriden to True with multiple modes
, height :: !Dimension -- ^ Window height
, maxComplRows :: Maybe Dimension
-- ^ Just x: maximum number of rows to show in completion window
, maxComplColumns :: Maybe Dimension
-- ^ Just x: maximum number of columns to show in completion window
, historySize :: !Int -- ^ The number of history entries to be saved
, historyFilter :: [String] -> [String]
-- ^ a filter to determine which
-- history entries to remember
, promptKeymap :: M.Map (KeyMask,KeySym) (XP ())
-- ^ Mapping from key combinations to actions
, completionKey :: (KeyMask, KeySym) -- ^ Key to trigger forward completion
, prevCompletionKey :: (KeyMask, KeySym) -- ^ Key to trigger backward completion
, changeModeKey :: KeySym -- ^ Key to change mode (when the prompt has multiple modes)
, defaultText :: String -- ^ The text by default in the prompt line
, autoComplete :: Maybe Int -- ^ Just x: if only one completion remains, auto-select it,
-- and delay by x microseconds
, showCompletionOnTab :: Bool -- ^ Only show list of completions when Tab was pressed
, complCaseSensitivity :: ComplCaseSensitivity
-- ^ Perform completion in a case-sensitive manner
, searchPredicate :: String -> String -> Bool
-- ^ Given the typed string and a possible
-- completion, is the completion valid?
, defaultPrompter :: String -> String
-- ^ Modifies the prompt given by 'showXPrompt'
, sorter :: String -> [String] -> [String]
-- ^ Used to sort the possible completions by how well they
-- match the search string (see X.P.FuzzyMatch for an
-- example).
}
data XPType = forall p . XPrompt p => XPT p
type ComplFunction = String -> IO [String]
type XPMode = XPType
data XPOperationMode = XPSingleMode ComplFunction XPType | XPMultipleModes (W.Stack XPType)
data ComplCaseSensitivity = CaseSensitive | CaseInSensitive
instance Show XPType where
show (XPT p) = showXPrompt p
instance XPrompt XPType where
showXPrompt = show
nextCompletion (XPT t) = nextCompletion t
commandToComplete (XPT t) = commandToComplete t
completionToCommand (XPT t) = completionToCommand t
completionFunction (XPT t) = completionFunction t
modeAction (XPT t) = modeAction t
-- | A class for an abstract prompt. In order for your data type to be a
-- valid prompt you _must_ make it an instance of this class.
--
-- The minimal complete definition is just 'showXPrompt', i.e. the name
-- of the prompt. This string will be displayed in the command line
-- window (before the cursor).
--
-- As an example of a complete 'XPrompt' instance definition, we can
-- look at the 'XMonad.Prompt.Shell.Shell' prompt from
-- "XMonad.Prompt.Shell":
--
-- > data Shell = Shell
-- >
-- > instance XPrompt Shell where
-- > showXPrompt Shell = "Run: "
class XPrompt t where
{-# MINIMAL showXPrompt #-}
-- | This method is used to print the string to be
-- displayed in the command line window.
showXPrompt :: t -> String
-- | This method is used to generate the next completion to be
-- printed in the command line when tab is pressed, given the
-- string presently in the command line and the list of
-- completion.
-- This function is not used when in multiple modes (because alwaysHighlight in XPConfig is True)
nextCompletion :: t -> String -> [String] -> String
nextCompletion = getNextOfLastWord
-- | This method is used to generate the string to be passed to
-- the completion function.
commandToComplete :: t -> String -> String
commandToComplete _ = getLastWord
-- | This method is used to process each completion in order to
-- generate the string that will be compared with the command
-- presently displayed in the command line. If the prompt is using
-- 'getNextOfLastWord' for implementing 'nextCompletion' (the
-- default implementation), this method is also used to generate,
-- from the returned completion, the string that will form the
-- next command line when tab is pressed.
completionToCommand :: t -> String -> String
completionToCommand _ c = c
-- | When the prompt has multiple modes, this is the function
-- used to generate the autocompletion list.
-- The argument passed to this function is given by `commandToComplete`
-- The default implementation shows an error message.
completionFunction :: t -> ComplFunction
completionFunction t = const $ return ["Completions for " ++ showXPrompt t ++ " could not be loaded"]
-- | When the prompt has multiple modes (created with mkXPromptWithModes), this function is called
-- when the user picks an item from the autocompletion list.
-- The first argument is the prompt (or mode) on which the item was picked
-- The first string argument is the autocompleted item's text.
-- The second string argument is the query made by the user (written in the prompt's buffer).
-- See XMonad/Actions/Launcher.hs for a usage example.
modeAction :: t -> String -> String -> X ()
modeAction _ _ _ = return ()
data XPPosition = Top
| Bottom
-- | Prompt will be placed in the center horizontally and
-- in the certain place of screen vertically. If it's in the upper
-- part of the screen, completion window will be placed below (like
-- in 'Top') and otherwise above (like in 'Bottom')
| CenteredAt { xpCenterY :: Rational
-- ^ Rational between 0 and 1, giving
-- y coordinate of center of the prompt relative to the screen height.
, xpWidth :: Rational
-- ^ Rational between 0 and 1, giving
-- width of the prompt relative to the screen width.
}
deriving (Show,Read)
data XPColor =
XPColor { bgNormal :: String -- ^ Background color
, fgNormal :: String -- ^ Font color
, bgHighlight :: String -- ^ Background color of a highlighted completion entry
, fgHighlight :: String -- ^ Font color of a highlighted completion entry
, border :: String -- ^ Border color
}
amberXPConfig, greenXPConfig :: XPConfig
instance Default XPColor where
def =
XPColor { bgNormal = "grey22"
, fgNormal = "grey80"
, bgHighlight = "grey"
, fgHighlight = "black"
, border = "white"
}
instance Default XPConfig where
def =
#ifdef XFT
XPC { font = "xft:monospace-12"
#else
XPC { font = "-misc-fixed-*-*-*-*-12-*-*-*-*-*-*-*"
#endif
, bgColor = bgNormal def
, fgColor = fgNormal def
, bgHLight = bgHighlight def
, fgHLight = fgHighlight def
, borderColor = border def
, promptBorderWidth = 1
, promptKeymap = defaultXPKeymap
, completionKey = (0, xK_Tab)
, prevCompletionKey = (shiftMask, xK_Tab)
, changeModeKey = xK_grave
, position = Bottom
, height = 18
, maxComplRows = Nothing
, maxComplColumns = Nothing
, historySize = 256
, historyFilter = id
, defaultText = []
, autoComplete = Nothing
, showCompletionOnTab = False
, complCaseSensitivity = CaseSensitive
, searchPredicate = isPrefixOf
, alwaysHighlight = False
, defaultPrompter = id
, sorter = const id
}
greenXPConfig = def { bgColor = "black"
, fgColor = "green"
, promptBorderWidth = 0
}
amberXPConfig = def { bgColor = "black"
, fgColor = "#ca8f2d"
, fgHLight = "#eaaf4c"
}
initState :: Display -> Window -> Window -> Rectangle -> XPOperationMode
-> GC -> XMonadFont -> [String] -> XPConfig -> (KeyMask -> KeyMask)
-> Dimension -> XPState
initState d rw w s opMode gc fonts h c cm width =
XPS { dpy = d
, rootw = rw
, win = w
, screen = s
, winWidth = width
, complWinDim = Nothing
, complWin = unsafePerformIO (newIORef Nothing)
, showComplWin = not (showCompletionOnTab c)
, operationMode = opMode
, highlightedCompl = Nothing
, gcon = gc
, fontS = fonts
, commandHistory = W.Stack { W.focus = defaultText c
, W.up = []
, W.down = h
}
, complIndex = (0,0) --(column index, row index), used when `alwaysHighlight` in XPConfig is True
, offset = length (defaultText c)
, config = c
, successful = False
, done = False
, modeDone = False
, cleanMask = cm
, prompter = defaultPrompter c
, color = defaultColor c
, eventBuffer = []
, inputBuffer = ""
, currentCompletions = Nothing
}
-- Returns the current XPType
currentXPMode :: XPState -> XPType
currentXPMode st = case operationMode st of
XPMultipleModes modes -> W.focus modes
XPSingleMode _ xptype -> xptype
-- When in multiple modes, this function sets the next mode
-- in the list of modes as active
setNextMode :: XPState -> XPState
setNextMode st = case operationMode st of
XPMultipleModes modes -> case W.down modes of
[] -> st -- there is no next mode, return same state
(m:ms) -> let
currentMode = W.focus modes
in st { operationMode = XPMultipleModes W.Stack { W.up = [], W.focus = m, W.down = ms ++ [currentMode]}} --set next and move previous current mode to the of the stack
_ -> st --nothing to do, the prompt's operation has only one mode
-- Returns the highlighted item
highlightedItem :: XPState -> [String] -> Maybe String
highlightedItem st' completions = case complWinDim st' of
Nothing -> Nothing -- when there isn't any compl win, we can't say how many cols,rows there are
Just winDim ->
let
ComplWindowDim{ cwCols, cwRows } = winDim
complMatrix = chunksOf (length cwRows) (take (length cwCols * length cwRows) completions)
(col_index,row_index) = complIndex st'
in case completions of
[] -> Nothing
_ -> complMatrix !? col_index >>= (!? row_index)
-- | Return the selected completion, i.e. the 'String' we actually act
-- upon after the user confirmed their selection (by pressing @Enter@).
selectedCompletion :: XPState -> String
selectedCompletion st
-- If 'alwaysHighlight' is used, look at the currently selected item (if any)
| alwaysHighlight (config st) = fromMaybe (command st) $ highlightedCompl st
-- Otherwise, look at what the user actually wrote so far
| otherwise = command st
-- this would be much easier with functional references
command :: XPState -> String
command = W.focus . commandHistory
setCommand :: String -> XPState -> XPState
setCommand xs s = s { commandHistory = (commandHistory s) { W.focus = xs }}
-- | Sets the input string to the given value.
setInput :: String -> XP ()
setInput = modify . setCommand
-- | Returns the current input string. Intended for use in custom keymaps
-- where 'get' or similar can't be used to retrieve it.
getInput :: XP String
getInput = gets command
-- | Returns the offset of the current input string. Intended for use in custom
-- keys where 'get' or similar can't be used to retrieve it.
getOffset :: XP Int
getOffset = gets offset
-- | Accessor encapsulating disparate color fields of 'XPConfig' into an
-- 'XPColor' (the configuration provides default values).
defaultColor :: XPConfig -> XPColor
defaultColor c = XPColor { bgNormal = bgColor c
, fgNormal = fgColor c
, bgHighlight = bgHLight c
, fgHighlight = fgHLight c
, border = borderColor c
}
-- | Modify the prompt colors.
modifyColor :: (XPColor -> XPColor) -> XP ()
modifyColor c = modify $ \s -> s { color = c $ color s }
-- | Set the prompt colors.
setColor :: XPColor -> XP ()
setColor = modifyColor . const
-- | Reset the prompt colors to those from 'XPConfig'.
resetColor :: XP ()
resetColor = gets (defaultColor . config) >>= setColor
-- | Set the prompt border color.
setBorderColor :: String -> XPColor -> XPColor
setBorderColor bc xpc = xpc { border = bc }
-- | Modify the prompter, i.e. for chaining prompters.
modifyPrompter :: ((String -> String) -> (String -> String)) -> XP ()
modifyPrompter p = modify $ \s -> s { prompter = p $ prompter s }
-- | Set the prompter.
setPrompter :: (String -> String) -> XP ()
setPrompter = modifyPrompter . const
-- | Reset the prompter to the one from 'XPConfig'.
resetPrompter :: XP ()
resetPrompter = gets (defaultPrompter . config) >>= setPrompter
-- | Set the current completion list, or 'Nothing' to invalidate the current
-- completions.
setCurrentCompletions :: Maybe [String] -> XP ()
setCurrentCompletions cs = modify $ \s -> s { currentCompletions = cs }
-- | Get the current completion list.
getCurrentCompletions :: XP (Maybe [String])
getCurrentCompletions = gets currentCompletions
-- | Same as 'mkXPrompt', except that the action function can have
-- type @String -> X a@, for any @a@, and the final action returned
-- by 'mkXPromptWithReturn' will have type @X (Maybe a)@. @Nothing@
-- is yielded if the user cancels the prompt (by e.g. hitting Esc or
-- Ctrl-G). For an example of use, see the 'XMonad.Prompt.Input'
-- module.
mkXPromptWithReturn :: XPrompt p => p -> XPConfig -> ComplFunction -> (String -> X a) -> X (Maybe a)
mkXPromptWithReturn t conf compl action = do
st' <- mkXPromptImplementation (showXPrompt t) conf (XPSingleMode compl (XPT t))
if successful st'
then Just <$> action (selectedCompletion st')
else return Nothing
-- | Creates a prompt given:
--
-- * a prompt type, instance of the 'XPrompt' class.
--
-- * a prompt configuration ('def' can be used as a starting point)
--
-- * a completion function ('mkComplFunFromList' can be used to
-- create a completions function given a list of possible completions)
--
-- * an action to be run: the action must take a string and return 'XMonad.X' ()
mkXPrompt :: XPrompt p => p -> XPConfig -> ComplFunction -> (String -> X ()) -> X ()
mkXPrompt t conf compl action = void $ mkXPromptWithReturn t conf compl action
-- | Creates a prompt with multiple modes given:
--
-- * A non-empty list of modes
-- * A prompt configuration
--
-- The created prompt allows to switch between modes with `changeModeKey` in `conf`. The modes are
-- instances of XPrompt. See XMonad.Actions.Launcher for more details
--
-- The argument supplied to the action to execute is always the current highlighted item,
-- that means that this prompt overrides the value `alwaysHighlight` for its configuration to True.
mkXPromptWithModes :: [XPType] -> XPConfig -> X ()
mkXPromptWithModes [] _ = pure ()
mkXPromptWithModes (defaultMode : modes) conf = do
let modeStack = W.Stack { W.focus = defaultMode -- Current mode
, W.up = []
, W.down = modes -- Other modes
}
om = XPMultipleModes modeStack
st' <- mkXPromptImplementation (showXPrompt defaultMode) conf { alwaysHighlight = True } om
when (successful st') $
case operationMode st' of
XPMultipleModes ms -> let
action = modeAction $ W.focus ms
in action (command st') $ fromMaybe "" (highlightedCompl st')
_ -> error "The impossible occurred: This prompt runs with multiple modes but they could not be found." --we are creating a prompt with multiple modes, so its operationMode should have been constructed with XPMultipleMode
-- Internal function used to implement 'mkXPromptWithReturn' and
-- 'mkXPromptWithModes'.
mkXPromptImplementation :: String -> XPConfig -> XPOperationMode -> X XPState
mkXPromptImplementation historyKey conf om = do
XConf { display = d, theRoot = rw } <- ask
s <- gets $ screenRect . W.screenDetail . W.current . windowset
cleanMask <- cleanKeyMask
cachedir <- asks (cacheDir . directories)
hist <- io $ readHistory conf cachedir
fs <- initXMF (font conf)
let width = getWinWidth s (position conf)
st' <- io $
bracket
(createPromptWin d rw conf s width)
(destroyWindow d)
(\w ->
bracket
(createGC d w)
(freeGC d)
(\gc -> do
selectInput d w $ exposureMask .|. keyPressMask
setGraphicsExposures d gc False
let hs = fromMaybe [] $ M.lookup historyKey hist
st = initState d rw w s om gc fs hs conf cleanMask width
runXP st))
releaseXMF fs
when (successful st') $ do
let prune = take (historySize conf)
io $ writeHistory conf cachedir $
M.insertWith
(\xs ys -> prune . historyFilter conf $ xs ++ ys)
historyKey
-- We need to apply historyFilter before as well, since
-- otherwise the filter would not be applied if there is no
-- history
(prune $ historyFilter conf [selectedCompletion st'])
hist
return st'
where
-- | Based on the ultimate position of the prompt and the screen
-- dimensions, calculate its width.
getWinWidth :: Rectangle -> XPPosition -> Dimension
getWinWidth scr = \case
CenteredAt{ xpWidth } -> floor $ fi (rect_width scr) * xpWidth
_ -> rect_width scr
-- | Inverse of 'Codec.Binary.UTF8.String.utf8Encode', that is, a convenience
-- function that checks to see if the input string is UTF8 encoded before
-- decoding.
utf8Decode :: String -> String
utf8Decode str
| isUTF8Encoded str = decodeString str
| otherwise = str
runXP :: XPState -> IO XPState
runXP st = do
let d = dpy st
w = win st
bracket
(grabKeyboard d w True grabModeAsync grabModeAsync currentTime)
(\_ -> ungrabKeyboard d currentTime)
(\status ->
execStateT
(when (status == grabSuccess) $ do
ah <- gets (alwaysHighlight . config)
when ah $ do
compl <- listToMaybe <$> getCompletions
modify' $ \xpst -> xpst{ highlightedCompl = compl }
updateWindows
eventLoop handleMain evDefaultStop)
st
`finally` (mapM_ (destroyWindow d) =<< readIORef (complWin st))
`finally` sync d False)
type KeyStroke = (KeySym, String)
-- | Check whether the given key stroke is a modifier.
isModifier :: KeyStroke -> Bool
isModifier (_, keyString) = null keyString
-- | Main event "loop". Gives priority to events from the state's event buffer.
eventLoop :: (KeyStroke -> Event -> XP ())
-> XP Bool
-> XP ()
eventLoop handle stopAction = do
b <- gets eventBuffer
(keysym,keystr,event) <- case b of
[] -> do
d <- gets dpy
io $ allocaXEvent $ \e -> do
-- Also capture @buttonPressMask@, see Note [Allow ButtonEvents]
maskEvent d (exposureMask .|. keyPressMask .|. buttonPressMask) e
ev <- getEvent e
if ev_event_type ev == keyPress
then do (_, s) <- lookupString $ asKeyEvent e
ks <- keycodeToKeysym d (ev_keycode ev) 0
return (ks, s, ev)
else return (noSymbol, "", ev)
(l : ls) -> do
modify $ \s -> s { eventBuffer = ls }
return l
handle (keysym,keystr) event
stopAction >>= \stop -> unless stop (eventLoop handle stopAction)
-- | Default event loop stop condition.
evDefaultStop :: XP Bool
evDefaultStop = gets ((||) . modeDone) <*> gets done
-- | Common patterns shared by all event handlers.
handleOther :: KeyStroke -> Event -> XP ()
handleOther _ ExposeEvent{ev_window = w} = do
-- Expose events can be triggered by switching virtual consoles.
st <- get
when (win st == w) updateWindows
handleOther _ ButtonEvent{ev_event_type = t} = do
-- See Note [Allow ButtonEvents]
when (t == buttonPress) $ do
d <- gets dpy
io $ allowEvents d replayPointer currentTime
handleOther _ _ = return ()
{- Note [Allow ButtonEvents]
Some settings (like @clickJustFocuses = False@) set up the passive
pointer grabs that xmonad makes to intercept clicks to unfocused windows
with @pointer_mode = grabModeSync@ and @keyboard_mode = grabModeSync@.
This means that any click in an unfocused window leads to a
pointer/keyboard grab that freezes both devices until 'allowEvents' is
called. But "XMonad.Prompt" has its own X event loop, so 'allowEvents'
is never called and everything remains frozen indefinitely.
This does not happen when the grabs are made with @grabModeAsync@, as
pointer events processing is not frozen and the grab only lasts as long
as the mouse button is pressed.
Hence, in this situation we call 'allowEvents' in the prompts event loop
whenever a button event is received, releasing the pointer grab. In this
case, 'replayPointer' takes care of the fact that these events are not
merely discarded, but passed to the respective application window.
-}
-- | Prompt event handler for the main loop. Dispatches to input, completion
-- and mode switching handlers.
handleMain :: KeyStroke -> Event -> XP ()
handleMain stroke@(keysym, keystr) = \case
KeyEvent{ev_event_type = t, ev_state = m} -> do
(prevCompKey, (compKey, modeKey)) <- gets $
(prevCompletionKey &&& completionKey &&& changeModeKey) . config
keymask <- gets cleanMask <*> pure m
-- haven't subscribed to keyRelease, so just in case
when (t == keyPress) $ if
| (keymask, keysym) == compKey ->
getCurrentCompletions >>= handleCompletionMain Next
| (keymask, keysym) == prevCompKey ->
getCurrentCompletions >>= handleCompletionMain Prev
| otherwise -> do
keymap <- gets (promptKeymap . config)
let mbAction = M.lookup (keymask, keysym) keymap
-- Either run when we can insert a valid character, or the
-- pressed key has an action associated to it.
unless (isModifier stroke && isNothing mbAction) $ do
setCurrentCompletions Nothing
if keysym == modeKey
then modify setNextMode >> updateWindows
else handleInput keymask mbAction
event -> handleOther stroke event
where
-- Prompt input handler for the main loop.
handleInput :: KeyMask -> Maybe (XP ()) -> XP ()
handleInput keymask = \case
Just action -> action >> updateWindows
Nothing -> when (keymask .&. controlMask == 0) $ do
insertString $ utf8Decode keystr
updateWindows
updateHighlightedCompl
complete <- tryAutoComplete
when complete acceptSelection
-- There are two options to store the completion list during the main loop:
-- * Use the State monad, with 'Nothing' as the initial state.
-- * Join the output of the event loop handler to the input of the (same)
-- subsequent handler, using 'Nothing' as the initial input.
-- Both approaches are, under the hood, equivalent.
--
-- | Prompt completion handler for the main loop. Given 'Nothing', generate the
-- current completion list. With the current list, trigger a completion.
handleCompletionMain :: Direction1D -> Maybe [String] -> XP ()
handleCompletionMain dir compls = case compls of
Just cs -> handleCompletion dir cs
Nothing -> do
cs <- getCompletions
when (length cs > 1) $
modify $ \s -> s { showComplWin = True }
setCurrentCompletions $ Just cs
handleCompletion dir cs
handleCompletion :: Direction1D -> [String] -> XP ()
handleCompletion dir cs = do
alwaysHlight <- gets $ alwaysHighlight . config
st <- get
let updateWins = redrawWindows (pure ())
updateState l = if alwaysHlight
then hlComplete (getLastWord $ command st) l st
else simpleComplete l st
case cs of
[] -> updateWindows
[x] -> do updateState [x]
cs' <- getCompletions
updateWins cs'
setCurrentCompletions $ Just cs'
l -> updateState l >> updateWins l
where
-- When alwaysHighlight is off, just complete based on what the
-- user has typed so far.
simpleComplete :: [String] -> XPState -> XP ()
simpleComplete l st = do
let newCommand = nextCompletion (currentXPMode st) (command st) l
modify $ \s -> setCommand newCommand $
s { offset = length newCommand
, highlightedCompl = Just newCommand
}
-- If alwaysHighlight is on, and the user wants the next
-- completion, move to the next completion item and update the
-- buffer to reflect that.
--
--TODO: Scroll or paginate results
hlComplete :: String -> [String] -> XPState -> XP ()
hlComplete prevCompl l st
| -- The current suggestion matches the command and is a
-- proper suffix of the last suggestion, so replace it.
isSuffixOfCmd && isProperSuffixOfLast = replaceCompletion prevCompl
| -- We only have one suggestion, so we need to be a little
-- bit smart in order to avoid a loop.
Just (ch :| []) <- nonEmpty cs =
if command st == hlCompl
then put st
else replaceCompletion ch
-- The current suggestion matches the command, so advance
-- to the next completion and try again.
| isSuffixOfCmd =
hlComplete hlCompl l $ st{ complIndex = complIndex'
, highlightedCompl = nextHlCompl
}
-- If nothing matches at all, delete the suggestion and
-- highlight the next one.
| otherwise = replaceCompletion prevCompl
where
hlCompl :: String = fromMaybe (command st) $ highlightedItem st l
complIndex' :: (Int, Int) = computeComplIndex dir st
nextHlCompl :: Maybe String = highlightedItem st{ complIndex = complIndex' } cs
isSuffixOfCmd :: Bool = hlCompl `isSuffixOf` command st
isProperSuffixOfLast :: Bool = hlCompl `isSuffixOf` prevCompl
&& not (prevCompl `isSuffixOf` hlCompl)
replaceCompletion :: String -> XP () = \str -> do
put st
replicateM_ (length $ words str) $ killWord Prev
insertString' hlCompl
endOfLine
-- | Initiate a prompt sub-map event loop. Submaps are intended to provide
-- alternate keybindings. Accepts a default action and a mapping from key
-- combinations to actions. If no entry matches, the default action is run.
promptSubmap :: XP ()
-> M.Map (KeyMask, KeySym) (XP ())
-> XP ()
promptSubmap defaultAction keymap = do
md <- gets modeDone
setModeDone False
updateWindows
eventLoop (handleSubmap defaultAction keymap) evDefaultStop
setModeDone md
handleSubmap :: XP ()
-> M.Map (KeyMask, KeySym) (XP ())
-> KeyStroke
-> Event
-> XP ()
handleSubmap defaultAction keymap stroke KeyEvent{ev_event_type = t, ev_state = m} = do
keymask <- gets cleanMask <*> pure m
when (t == keyPress) $ handleInputSubmap defaultAction keymap keymask stroke
handleSubmap _ _ stroke event = handleOther stroke event
handleInputSubmap :: XP ()
-> M.Map (KeyMask, KeySym) (XP ())
-> KeyMask
-> KeyStroke
-> XP ()
handleInputSubmap defaultAction keymap keymask stroke@(keysym, _) =
case M.lookup (keymask,keysym) keymap of
Just action -> action >> updateWindows
Nothing -> unless (isModifier stroke) $ defaultAction >> updateWindows
-- | Initiate a prompt input buffer event loop. Input is sent to a buffer and
-- bypasses the prompt. The provided function is given the existing buffer and
-- the input keystring. The first field of the result determines whether the
-- input loop continues (if @True@). The second field determines whether the
-- input is appended to the buffer, or dropped (if @False@). If the loop is to
-- stop without keeping input - that is, @(False,False)@ - the event is
-- prepended to the event buffer to be processed by the parent loop. This
-- allows loop to process both fixed and indeterminate inputs.
--
-- Result given @(continue,keep)@:
--
-- * cont and keep
--
-- * grow input buffer
--
-- * stop and keep
--
-- * grow input buffer
-- * stop loop
--
-- * stop and drop
--
-- * buffer event
-- * stop loop
--
-- * cont and drop
--
-- * do nothing
promptBuffer :: (String -> String -> (Bool,Bool)) -> XP String
promptBuffer f = do
md <- gets modeDone
setModeDone False
eventLoop (handleBuffer f) evDefaultStop
buff <- gets inputBuffer
modify $ \s -> s { inputBuffer = "" }
setModeDone md
return buff
handleBuffer :: (String -> String -> (Bool,Bool))
-> KeyStroke
-> Event
-> XP ()
handleBuffer f stroke event@KeyEvent{ev_event_type = t, ev_state = m} = do
keymask <- gets cleanMask <*> pure m
when (t == keyPress) $ handleInputBuffer f keymask stroke event
handleBuffer _ stroke event = handleOther stroke event
handleInputBuffer :: (String -> String -> (Bool,Bool))
-> KeyMask
-> KeyStroke
-> Event
-> XP ()
handleInputBuffer f keymask stroke@(keysym, keystr) event =
unless (isModifier stroke || keymask .&. controlMask /= 0) $ do
(evB,inB) <- gets (eventBuffer &&& inputBuffer)
let keystr' = utf8Decode keystr
let (cont,keep) = f inB keystr'
when keep $
modify $ \s -> s { inputBuffer = inB ++ keystr' }
unless cont $
setModeDone True
unless (cont || keep) $
modify $ \s -> s { eventBuffer = (keysym,keystr,event) : evB }
-- | Predicate instructing 'promptBuffer' to get (and keep) a single non-empty
-- 'KeyEvent'.
bufferOne :: String -> String -> (Bool,Bool)
bufferOne xs x = (null xs && null x,True)
-- | Return the @(column, row)@ of the desired highlight, or @(0, 0)@ if
-- there is no prompt window or a wrap-around occurs.
computeComplIndex :: Direction1D -> XPState -> (Int, Int)
computeComplIndex dir st = case complWinDim st of
Nothing -> (0, 0) -- no window dimensions (just destroyed or not created)
Just ComplWindowDim{ cwCols, cwRows } ->
if rowm == currentrow + direction
then (currentcol, rowm) -- We are not in the last row, so advance the row
else (colm, rowm) -- otherwise advance to the respective column
where
(currentcol, currentrow) = complIndex st
(colm, rowm) =
( (currentcol + direction) `mod` length cwCols
, (currentrow + direction) `mod` length cwRows
)
direction = case dir of
Next -> 1
Prev -> -1
tryAutoComplete :: XP Bool
tryAutoComplete = do
ac <- gets (autoComplete . config)
case ac of
Just d -> do cs <- getCompletions
case cs of
[c] -> runCompleted c d >> return True
_ -> return False
Nothing -> return False
where runCompleted cmd delay = do
st <- get
let new_command = nextCompletion (currentXPMode st) (command st) [cmd]
modify $ setCommand "autocompleting..."
updateWindows
io $ threadDelay delay
modify $ setCommand new_command
return True
-- KeyPresses
-- | Default key bindings for prompts. Click on the \"Source\" link
-- to the right to see the complete list. See also 'defaultXPKeymap''.
defaultXPKeymap :: M.Map (KeyMask,KeySym) (XP ())
defaultXPKeymap = defaultXPKeymap' isSpace
-- | A variant of 'defaultXPKeymap' which lets you specify a custom
-- predicate for identifying non-word characters, which affects all
-- the word-oriented commands (move\/kill word). The default is
-- 'isSpace'. For example, by default a path like @foo\/bar\/baz@
-- would be considered as a single word. You could use a predicate
-- like @(\\c -> isSpace c || c == \'\/\')@ to move through or
-- delete components of the path one at a time.
defaultXPKeymap' :: (Char -> Bool) -> M.Map (KeyMask,KeySym) (XP ())
defaultXPKeymap' p = M.fromList $
map (first $ (,) controlMask) -- control + <key>
[ (xK_u, killBefore)
, (xK_k, killAfter)
, (xK_a, startOfLine)
, (xK_e, endOfLine)
, (xK_y, pasteString)
-- Retain the pre-0.14 moveWord' behavior:
, (xK_Right, moveWord' p Next >> moveCursor Next)
, (xK_Left, moveCursor Prev >> moveWord' p Prev)
, (xK_Delete, killWord' p Next)
, (xK_BackSpace, killWord' p Prev)
, (xK_w, killWord' p Prev)
, (xK_g, quit)
, (xK_bracketleft, quit)
] ++
map (first $ (,) 0)
[ (xK_Return, acceptSelection)
, (xK_KP_Enter, acceptSelection)
, (xK_BackSpace, deleteString Prev)
, (xK_Delete, deleteString Next)
, (xK_Left, moveCursor Prev)
, (xK_Right, moveCursor Next)