1# -*- coding: utf-8 -*-
2# Copyright (c) 2010-2011 Aldo Cortesi
3# Copyright (c) 2010 Philip Kranz
4# Copyright (c) 2011 Mounier Florian
5# Copyright (c) 2011 Paul Colomiets
6# Copyright (c) 2011-2012 roger
7# Copyright (c) 2011-2012, 2014 Tycho Andersen
8# Copyright (c) 2012 Dustin Lacewell
9# Copyright (c) 2012 Laurie Clark-Michalek
10# Copyright (c) 2012-2014 Craig Barnes
11# Copyright (c) 2013 Tao Sauvage
12# Copyright (c) 2014 ramnes
13# Copyright (c) 2014 Sean Vig
14# Copyright (C) 2015, Juan Riquelme González
15#
16# Permission is hereby granted, free of charge, to any person obtaining a copy
17# of this software and associated documentation files (the "Software"), to deal
18# in the Software without restriction, including without limitation the rights
19# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20# copies of the Software, and to permit persons to whom the Software is
21# furnished to do so, subject to the following conditions:
22#
23# The above copyright notice and this permission notice shall be included in
24# all copies or substantial portions of the Software.
25#
26# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32# SOFTWARE.
33
34import abc
35import glob
36import os
37import pickle
38import string
39from collections import deque
40from typing import List, Optional, Tuple
41
42from libqtile import bar, hook, pangocffi, utils
43from libqtile.command.base import CommandObject, SelectError
44from libqtile.command.client import InteractiveCommandClient
45from libqtile.command.interface import CommandError, QtileCommandInterface
46from libqtile.log_utils import logger
47from libqtile.widget import base
48
49
50class AbstractCompleter(metaclass=abc.ABCMeta):
51    @abc.abstractmethod
52    def __init__(self, qtile: CommandObject) -> None:
53        pass
54
55    @abc.abstractmethod
56    def actual(self) -> Optional[str]:
57        pass
58
59    @abc.abstractmethod
60    def reset(self) -> None:
61        pass
62
63    @abc.abstractmethod
64    def complete(self, txt: str) -> str:
65        """Perform the requested completion on the given text"""
66        pass  # pragma: no cover
67
68
69class NullCompleter(AbstractCompleter):
70    def __init__(self, qtile) -> None:
71        self.qtile = qtile
72
73    def actual(self) -> str:
74        return ""
75
76    def reset(self) -> None:
77        pass
78
79    def complete(self, txt: str) -> str:
80        return txt
81
82
83class FileCompleter(AbstractCompleter):
84    def __init__(self, qtile, _testing=False) -> None:
85        self._testing = _testing
86        self.qtile = qtile
87        self.thisfinal = None  # type: Optional[str]
88        self.lookup = None  # type: Optional[List[Tuple[str, str]]]
89        self.reset()
90
91    def actual(self) -> Optional[str]:
92        return self.thisfinal
93
94    def reset(self) -> None:
95        self.lookup = None
96
97    def complete(self, txt: str) -> str:
98        """Returns the next completion for txt, or None if there is no completion"""
99        if self.lookup is None:
100            self.lookup = []
101            if txt == "" or txt[0] not in "~/":
102                txt = "~/" + txt
103            path = os.path.expanduser(txt)
104            if os.path.isdir(path):
105                files = glob.glob(os.path.join(path, "*"))
106                prefix = txt
107            else:
108                files = glob.glob(path + "*")
109                prefix = os.path.dirname(txt)
110                prefix = prefix.rstrip("/") or "/"
111            for f in files:
112                display = os.path.join(prefix, os.path.basename(f))
113                if os.path.isdir(f):
114                    display += "/"
115                self.lookup.append((display, f))
116                self.lookup.sort()
117            self.offset = -1
118            self.lookup.append((txt, txt))
119        self.offset += 1
120        if self.offset >= len(self.lookup):
121            self.offset = 0
122        ret = self.lookup[self.offset]
123        self.thisfinal = ret[1]
124        return ret[0]
125
126
127class QshCompleter(AbstractCompleter):
128    def __init__(self, qtile: CommandObject) -> None:
129        q = QtileCommandInterface(qtile)
130        self.client = InteractiveCommandClient(q)
131        self.thisfinal = None  # type: Optional[str]
132        self.reset()
133
134    def actual(self) -> Optional[str]:
135        return self.thisfinal
136
137    def reset(self) -> None:
138        self.lookup = None  # type: Optional[List[Tuple[str, str]]]
139        self.path = ''
140        self.offset = -1
141
142    def complete(self, txt: str) -> str:
143        txt = txt.lower()
144        if self.lookup is None:
145            self.lookup = []
146            path = txt.split('.')[:-1]
147            self.path = '.'.join(path)
148            term = txt.split('.')[-1]
149            if len(self.path) > 0:
150                self.path += '.'
151
152            contains_cmd = 'self.client.%s_contains' % self.path
153            try:
154                contains = eval(contains_cmd)
155            except AttributeError:
156                contains = []
157            for obj in contains:
158                if obj.lower().startswith(term):
159                    self.lookup.append((obj, obj))
160
161            commands_cmd = 'self.client.%scommands()' % self.path
162            try:
163                commands = eval(commands_cmd)
164            except (CommandError, AttributeError):
165                commands = []
166            for cmd in commands:
167                if cmd.lower().startswith(term):
168                    self.lookup.append((cmd + '()', cmd + '()'))
169
170            self.offset = -1
171            self.lookup.append((term, term))
172
173        self.offset += 1
174        if self.offset >= len(self.lookup):
175            self.offset = 0
176        ret = self.lookup[self.offset]
177        self.thisfinal = self.path + ret[0]
178        return self.path + ret[0]
179
180
181class GroupCompleter(AbstractCompleter):
182    def __init__(self, qtile: CommandObject) -> None:
183        self.qtile = qtile
184        self.thisfinal = None  # type: Optional[str]
185        self.lookup = None  # type: Optional[List[Tuple[str, str]]]
186        self.offset = -1
187
188    def actual(self) -> Optional[str]:
189        """Returns the current actual value"""
190        return self.thisfinal
191
192    def reset(self) -> None:
193        self.lookup = None
194        self.offset = -1
195
196    def complete(self, txt: str) -> str:
197        """Returns the next completion for txt, or None if there is no completion"""
198        txt = txt.lower()
199        if not self.lookup:
200            self.lookup = []
201            for group in self.qtile.groups_map.keys():  # type: ignore
202                if group.lower().startswith(txt):
203                    self.lookup.append((group, group))
204
205            self.lookup.sort()
206            self.offset = -1
207            self.lookup.append((txt, txt))
208
209        self.offset += 1
210        if self.offset >= len(self.lookup):
211            self.offset = 0
212        ret = self.lookup[self.offset]
213        self.thisfinal = ret[1]
214        return ret[0]
215
216
217class WindowCompleter(AbstractCompleter):
218    def __init__(self, qtile: CommandObject) -> None:
219        self.qtile = qtile
220        self.thisfinal = None  # type: Optional[str]
221        self.lookup = None  # type: Optional[List[Tuple[str, str]]]
222        self.offset = -1
223
224    def actual(self) -> Optional[str]:
225        """Returns the current actual value"""
226        return self.thisfinal
227
228    def reset(self) -> None:
229        self.lookup = None
230        self.offset = -1
231
232    def complete(self, txt: str) -> str:
233        """Returns the next completion for txt, or None if there is no completion"""
234        if self.lookup is None:
235            self.lookup = []
236            for wid, window in self.qtile.windows_map.items():  # type: ignore
237                if window.group and window.name.lower().startswith(txt):
238                    self.lookup.append((window.name, wid))
239
240            self.lookup.sort()
241            self.offset = -1
242            self.lookup.append((txt, txt))
243
244        self.offset += 1
245        if self.offset >= len(self.lookup):
246            self.offset = 0
247        ret = self.lookup[self.offset]
248        self.thisfinal = ret[1]
249        return ret[0]
250
251
252class CommandCompleter:
253    """
254    Parameters
255    ==========
256    _testing :
257        disables reloading of the lookup table to make testing possible.
258    """
259
260    DEFAULTPATH = "/bin:/usr/bin:/usr/local/bin"
261
262    def __init__(self, qtile, _testing=False):
263        self.lookup = None  # type: Optional[List[Tuple[str, str]]]
264        self.offset = -1
265        self.thisfinal = None  # type: Optional[str]
266        self._testing = _testing
267
268    def actual(self) -> Optional[str]:
269        """Returns the current actual value"""
270        return self.thisfinal
271
272    def executable(self, fpath: str):
273        return os.access(fpath, os.X_OK)
274
275    def reset(self) -> None:
276        self.lookup = None
277        self.offset = -1
278
279    def complete(self, txt: str) -> str:
280        """Returns the next completion for txt, or None if there is no completion"""
281        if self.lookup is None:
282            # Lookup is a set of (display value, actual value) tuples.
283            self.lookup = []
284            if txt and txt[0] in "~/":
285                path = os.path.expanduser(txt)
286                if os.path.isdir(path):
287                    files = glob.glob(os.path.join(path, "*"))
288                    prefix = txt
289                else:
290                    files = glob.glob(path + "*")
291                    prefix = os.path.dirname(txt)
292                prefix = prefix.rstrip("/") or "/"
293                for f in files:
294                    if self.executable(f):
295                        display = os.path.join(prefix, os.path.basename(f))
296                        if os.path.isdir(f):
297                            display += "/"
298                        self.lookup.append((display, f))
299            else:
300                dirs = os.environ.get("PATH", self.DEFAULTPATH).split(":")
301                for d in dirs:
302                    try:
303                        d = os.path.expanduser(d)
304                        for cmd in glob.iglob(os.path.join(d, "%s*" % txt)):
305                            if self.executable(cmd):
306                                self.lookup.append(
307                                    (
308                                        os.path.basename(cmd),
309                                        cmd
310                                    ),
311                                )
312                    except OSError:
313                        pass
314            self.lookup.sort()
315            self.offset = -1
316            self.lookup.append((txt, txt))
317        self.offset += 1
318        if self.offset >= len(self.lookup):
319            self.offset = 0
320        ret = self.lookup[self.offset]
321        self.thisfinal = ret[1]
322        return ret[0]
323
324
325class Prompt(base._TextBox):
326    """A widget that prompts for user input
327
328    Input should be started using the ``.start_input()`` method on this class.
329    """
330    completers = {
331        "file": FileCompleter,
332        "qshell": QshCompleter,
333        "cmd": CommandCompleter,
334        "group": GroupCompleter,
335        "window": WindowCompleter,
336        None: NullCompleter
337    }
338    orientations = base.ORIENTATION_HORIZONTAL
339    defaults = [("cursor", True, "Show a cursor"),
340                ("cursorblink", 0.5, "Cursor blink rate. 0 to disable."),
341                ("cursor_color", "bef098",
342                 "Color for the cursor and text over it."),
343                ("prompt", "{prompt}: ", "Text displayed at the prompt"),
344                ("record_history", True, "Keep a record of executed commands"),
345                ("max_history", 100,
346                 "Commands to keep in history. 0 for no limit."),
347                ("ignore_dups_history", False,
348                 "Don't store duplicates in history"),
349                ("bell_style", "audible",
350                 "Alert at the begin/end of the command history. " +
351                 "Possible values: 'audible' (X11 only), 'visual' and None."),
352                ("visual_bell_color", "ff0000",
353                 "Color for the visual bell (changes prompt background)."),
354                ("visual_bell_time", 0.2,
355                 "Visual bell duration (in seconds).")]
356
357    def __init__(self, name="prompt", **config) -> None:
358        base._TextBox.__init__(self, "", bar.CALCULATED, **config)
359        self.add_defaults(Prompt.defaults)
360        self.name = name
361        self.active = False
362        self.completer = None  # type: Optional[AbstractCompleter]
363
364        # If history record is on, get saved history or create history record
365        if self.record_history:
366            self.history_path = os.path.join(utils.get_cache_dir(),
367                                             'prompt_history')
368            if os.path.exists(self.history_path):
369                with open(self.history_path, 'rb') as f:
370                    try:
371                        self.history = pickle.load(f)
372                        if self.ignore_dups_history:
373                            self._dedup_history()
374                    except:  # noqa: E722
375                        # unfortunately, pickle doesn't wrap its errors, so we
376                        # can't detect what's a pickle error and what's not.
377                        logger.exception("failed to load prompt history")
378                        self.history = {x: deque(maxlen=self.max_history)
379                                        for x in self.completers}
380
381                    # self.history of size does not match.
382                    if len(self.history) != len(self.completers):
383                        self.history = {x: deque(maxlen=self.max_history)
384                                        for x in self.completers}
385
386                    if self.max_history != \
387                       self.history[list(self.history)[0]].maxlen:
388                        self.history = {x: deque(self.history[x],
389                                                 self.max_history)
390                                        for x in self.completers}
391            else:
392                self.history = {x: deque(maxlen=self.max_history)
393                                for x in self.completers}
394
395    def _configure(self, qtile, bar) -> None:
396        self.markup = True
397        base._TextBox._configure(self, qtile, bar)
398
399        def f(win):
400            if self.active and not win == self.bar.window:
401                self._unfocus()
402
403        hook.subscribe.client_focus(f)
404
405        # Define key handlers (action to do when a specific key is hit)
406        keyhandlers = {
407            'Tab': self._trigger_complete,
408            'BackSpace': self._delete_char(),
409            'Delete': self._delete_char(False),
410            'KP_Delete': self._delete_char(False),
411            'Escape': self._unfocus,
412            'Return': self._send_cmd,
413            'KP_Enter': self._send_cmd,
414            'Up': self._get_prev_cmd,
415            'KP_Up': self._get_prev_cmd,
416            'Down': self._get_next_cmd,
417            'KP_Down': self._get_next_cmd,
418            'Left': self._move_cursor(),
419            'KP_Left': self._move_cursor(),
420            'Right': self._move_cursor("right"),
421            'KP_Right': self._move_cursor("right"),
422        }
423        self.keyhandlers = {
424            qtile.core.keysym_from_name(k): v for k, v in keyhandlers.items()
425        }
426        printables = {x: self._write_char for x in range(127) if
427                      chr(x) in string.printable}
428        self.keyhandlers.update(printables)
429        self.tab = qtile.core.keysym_from_name("Tab")
430
431        self.bell_style: str
432        if self.bell_style == "audible" and qtile.core.name != "x11":
433            self.bell_style = "visual"
434            logger.warning("Prompt widget only supports audible bell under X11")
435        if self.bell_style == "visual":
436            self.original_background = self.background
437
438    def start_input(self, prompt, callback, complete=None,
439                    strict_completer=False, allow_empty_input=False) -> None:
440        """Run the prompt
441
442        Displays a prompt and starts to take one line of keyboard input from
443        the user. When done, calls the callback with the input string as
444        argument. If history record is enabled, also allows to browse between
445        previous commands with ↑ and ↓, and execute them (untouched or
446        modified). When history is exhausted, fires an alert. It tries to
447        mimic, in some way, the shell behavior.
448
449        Parameters
450        ==========
451        complete :
452            Tab-completion. Can be None, "cmd", "file", "group", "qshell" or
453            "window".
454        prompt :
455            text displayed at the prompt, e.g. "spawn: "
456        callback :
457            function to call with returned value.
458        complete :
459            completer to use.
460        strict_completer :
461            When True the return value wil be the exact completer result where
462            available.
463        allow_empty_input :
464            When True, an empty value will still call the callback function
465        """
466
467        if self.cursor and self.cursorblink and not self.active:
468            self.timeout_add(self.cursorblink, self._blink)
469        self.display = self.prompt.format(prompt=prompt)
470        self.display = pangocffi.markup_escape_text(self.display)
471        self.active = True
472        self.user_input = ""
473        self.archived_input = ""
474        self.show_cursor = self.cursor
475        self.cursor_position = 0
476        self.callback = callback
477        self.completer = self.completers[complete](self.qtile)
478        self.strict_completer = strict_completer
479        self.allow_empty_input = allow_empty_input
480        self._update()
481        self.bar.widget_grab_keyboard(self)
482        if self.record_history:
483            self.completer_history = self.history[complete]
484            self.position = len(self.completer_history)
485
486    def calculate_length(self) -> int:
487        if self.text:
488            width = min(
489                self.layout.width,
490                self.bar.width
491            ) + self.actual_padding * 2
492            return width
493        else:
494            return 0
495
496    def _blink(self) -> None:
497        self.show_cursor = not self.show_cursor
498        self._update()
499        if self.active:
500            self.timeout_add(self.cursorblink, self._blink)
501
502    def _highlight_text(self, text) -> str:
503        color = utils.hex(self.cursor_color)
504        text = '<span foreground="{0}">{1}</span>'.format(color, text)
505        if self.show_cursor:
506            text = '<u>{}</u>'.format(text)
507        return text
508
509    def _update(self) -> None:
510        if self.active:
511            self.text = self.archived_input or self.user_input
512            cursor = pangocffi.markup_escape_text(" ")
513            if self.cursor_position < len(self.text):
514                txt1 = self.text[:self.cursor_position]
515                txt2 = self.text[self.cursor_position]
516                txt3 = self.text[self.cursor_position + 1:]
517                for text in (txt1, txt2, txt3):
518                    text = pangocffi.markup_escape_text(text)
519                txt2 = self._highlight_text(txt2)
520                self.text = "{0}{1}{2}{3}".format(txt1, txt2, txt3, cursor)
521            else:
522                self.text = pangocffi.markup_escape_text(self.text)
523                self.text += self._highlight_text(cursor)
524            self.text = self.display + self.text
525        else:
526            self.text = ""
527        self.bar.draw()
528
529    def _trigger_complete(self) -> None:
530        # Trigger the auto completion in user input
531        assert self.completer is not None
532        self.user_input = self.completer.complete(self.user_input)
533        self.cursor_position = len(self.user_input)
534
535    def _history_to_input(self) -> None:
536        # Move actual command (when exploring history) to user input and update
537        # history position (right after the end)
538        if self.archived_input:
539            self.user_input = self.archived_input
540            self.archived_input = ""
541            self.position = len(self.completer_history)
542
543    def _insert_before_cursor(self, charcode) -> None:
544        # Insert a character (given their charcode) in input, before the cursor
545        txt1 = self.user_input[:self.cursor_position]
546        txt2 = self.user_input[self.cursor_position:]
547        self.user_input = txt1 + chr(charcode) + txt2
548        self.cursor_position += 1
549
550    def _delete_char(self, backspace=True):
551        # Return a function that deletes character from the input text.
552        # If backspace is True, function will emulate backspace, else Delete.
553        def f():
554            self._history_to_input()
555            step = -1 if backspace else 0
556            if not backspace and self.cursor_position == len(self.user_input):
557                self._alert()
558            elif len(self.user_input) > 0 and self.cursor_position + step > -1:
559                txt1 = self.user_input[:self.cursor_position + step]
560                txt2 = self.user_input[self.cursor_position + step + 1:]
561                self.user_input = txt1 + txt2
562                if step:
563                    self.cursor_position += step
564            else:
565                self._alert()
566        return f
567
568    def _write_char(self):
569        # Add pressed (legal) char key to user input.
570        # No LookupString in XCB... oh, the shame! Unicode users beware!
571        self._history_to_input()
572        self._insert_before_cursor(self.key)
573
574    def _unfocus(self):
575        # Remove focus from the widget
576        self.active = False
577        self._update()
578        self.bar.widget_ungrab_keyboard()
579
580    def _send_cmd(self):
581        # Send the prompted text for execution
582        self._unfocus()
583        if self.strict_completer:
584            self.user_input = self.actual_value or self.user_input
585            del self.actual_value
586        self._history_to_input()
587        if self.user_input or self.allow_empty_input:
588            # If history record is activated, also save command in history
589            if self.record_history:
590                # ensure no dups in history
591                if self.ignore_dups_history and (self.user_input in self.completer_history):
592                    self.completer_history.remove(self.user_input)
593                    self.position -= 1
594
595                self.completer_history.append(self.user_input)
596
597                if self.position < self.max_history:
598                    self.position += 1
599                os.makedirs(os.path.dirname(self.history_path), exist_ok=True)
600                with open(self.history_path, mode='wb') as f:
601                    pickle.dump(self.history, f, protocol=2)
602            self.callback(self.user_input)
603
604    def _alert(self):
605        # Fire an alert (audible or visual), if bell style is not None.
606        if self.bell_style == "audible":
607            self.qtile.core.conn.conn.core.Bell(0)
608        elif self.bell_style == "visual":
609            self.background = self.visual_bell_color
610            self.timeout_add(self.visual_bell_time, self._stop_visual_alert)
611
612    def _stop_visual_alert(self):
613        self.background = self.original_background
614        self._update()
615
616    def _get_prev_cmd(self):
617        # Get the previous command in history.
618        # If there isn't more previous commands, ring system bell
619        if self.record_history:
620            if not self.position:
621                self._alert()
622            else:
623                self.position -= 1
624                self.archived_input = self.completer_history[self.position]
625                self.cursor_position = len(self.archived_input)
626
627    def _get_next_cmd(self):
628        # Get the next command in history.
629        # If the last command was already reached, ring system bell.
630        if self.record_history:
631            if self.position == len(self.completer_history):
632                self._alert()
633            elif self.position < len(self.completer_history):
634                self.position += 1
635                if self.position == len(self.completer_history):
636                    self.archived_input = ""
637                else:
638                    self.archived_input = self.completer_history[self.position]
639                self.cursor_position = len(self.archived_input)
640
641    def _cursor_to_left(self):
642        # Move cursor to left, if possible
643        if self.cursor_position:
644            self.cursor_position -= 1
645        else:
646            self._alert()
647
648    def _cursor_to_right(self):
649        # move cursor to right, if possible
650        command = self.archived_input or self.user_input
651        if self.cursor_position < len(command):
652            self.cursor_position += 1
653        else:
654            self._alert()
655
656    def _move_cursor(self, direction="left"):
657        # Move the cursor to left or right, according to direction
658        if direction == "left":
659            return self._cursor_to_left
660        elif direction == "right":
661            return self._cursor_to_right
662
663    def _get_keyhandler(self, k):
664        # Return the action (a function) to do according the pressed key (k).
665        self.key = k
666        if k in self.keyhandlers:
667            if k != self.tab:
668                self.actual_value = self.completer.actual()
669                self.completer.reset()
670            return self.keyhandlers[k]
671
672    def process_key_press(self, keysym: int):
673        """Key press handler for the minibuffer.
674
675        Currently only supports ASCII characters.
676        """
677        handle_key = self._get_keyhandler(keysym)
678
679        if handle_key:
680            handle_key()
681            del self.key
682        self._update()
683
684    def cmd_fake_keypress(self, key: str) -> None:
685        self.process_key_press(self.qtile.core.keysym_from_name(key))
686
687    def cmd_info(self):
688        """Returns a dictionary of info for this object"""
689        return dict(
690            name=self.name,
691            width=self.width,
692            text=self.text,
693            active=self.active,
694        )
695
696    def cmd_exec_general(
697            self, prompt, object_name, cmd_name, selector=None, completer=None):
698        """
699        Execute a cmd of any object. For example layout, group, window, widget
700        , etc with a string that is obtained from start_input.
701
702        Parameters
703        ==========
704        prompt :
705            Text displayed at the prompt.
706        object_name :
707            Name of a object in Qtile. This string has to be 'layout', 'widget',
708            'bar', 'window' or 'screen'.
709        cmd_name :
710            Execution command of selected object using object_name and selector.
711        selector :
712            This value select a specific object within a object list that is
713            obtained by object_name.
714            If this value is None, current object is selected. e.g. current layout,
715            current window and current screen.
716        completer:
717            Completer to use.
718
719        config example:
720            Key([alt, 'shift'], 'a',
721                lazy.widget['prompt'].exec_general(
722                    'section(add)',
723                    'layout',
724                    'add_section'))
725        """
726        try:
727            obj = self.qtile.select([(object_name, selector)])
728        except SelectError:
729            logger.warning("cannot select a object")
730            return
731        cmd = obj.command(cmd_name)
732        if not cmd:
733            logger.warning("command not found")
734            return
735
736        def f(args):
737            if args:
738                cmd(args)
739
740        self.start_input(prompt, f, completer)
741
742    def _dedup_history(self):
743        """Filter the history deque, clearing all duplicate values."""
744        self.history = {x: self._dedup_deque(self.history[x])
745                        for x in self.completers}
746
747    def _dedup_deque(self, dq):
748        return deque(_LastUpdatedOrderedDict.fromkeys(dq))
749
750
751class _LastUpdatedOrderedDict(dict):
752    """Store items in the order the keys were last added."""
753
754    def __setitem__(self, key, value):
755        if key in self:
756            del self[key]
757        super().__setitem__(key, value)
758