1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2
3# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
4#
5# This file is part of qutebrowser.
6#
7# qutebrowser is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# qutebrowser is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.
19
20"""Command history for the status bar."""
21
22from typing import MutableSequence
23
24from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
25
26from qutebrowser.utils import usertypes, log, standarddir, objreg
27from qutebrowser.misc import lineparser
28
29
30class HistoryEmptyError(Exception):
31
32    """Raised when the history is empty."""
33
34
35class HistoryEndReachedError(Exception):
36
37    """Raised when the end of the history is reached."""
38
39
40class History(QObject):
41
42    """Command history.
43
44    Attributes:
45        history: A list of executed commands, with newer commands at the end.
46        _tmphist: Temporary history for history browsing (as NeighborList)
47
48    Signals:
49        changed: Emitted when an entry was added to the history.
50    """
51
52    changed = pyqtSignal()
53
54    def __init__(self, *, history=None, parent=None):
55        """Constructor.
56
57        Args:
58            history: The initial history to set.
59        """
60        super().__init__(parent)
61        self._tmphist = None
62        if history is None:
63            self.history: MutableSequence[str] = []
64        else:
65            self.history = history
66
67    def __getitem__(self, idx):
68        return self.history[idx]
69
70    def is_browsing(self):
71        """Check _tmphist to see if we're browsing."""
72        return self._tmphist is not None
73
74    def start(self, text):
75        """Start browsing to the history.
76
77        Called when the user presses the up/down key and wasn't browsing the
78        history already.
79
80        Args:
81            text: The preset text.
82        """
83        log.misc.debug("Preset text: '{}'".format(text))
84        if text:
85            items: MutableSequence[str] = [
86                e for e in self.history
87                if e.startswith(text)]
88        else:
89            items = self.history
90        if not items:
91            raise HistoryEmptyError
92        self._tmphist = usertypes.NeighborList(items)
93        return self._tmphist.lastitem()
94
95    @pyqtSlot()
96    def stop(self):
97        """Stop browsing the history."""
98        self._tmphist = None
99
100    def previtem(self):
101        """Get the previous item in the temp history.
102
103        start() needs to be called before calling this.
104        """
105        if not self.is_browsing():
106            raise ValueError("Currently not browsing history")
107        assert self._tmphist is not None
108
109        try:
110            return self._tmphist.previtem()
111        except IndexError:
112            raise HistoryEndReachedError
113
114    def nextitem(self):
115        """Get the next item in the temp history.
116
117        start() needs to be called before calling this.
118        """
119        if not self.is_browsing():
120            raise ValueError("Currently not browsing history")
121        assert self._tmphist is not None
122
123        try:
124            return self._tmphist.nextitem()
125        except IndexError:
126            raise HistoryEndReachedError
127
128    def append(self, text):
129        """Append a new item to the history.
130
131        Args:
132            text: The text to append.
133        """
134        if not self.history or text != self.history[-1]:
135            self.history.append(text)
136            self.changed.emit()
137
138
139def init():
140    """Initialize the LimitLineParser storing the history."""
141    save_manager = objreg.get('save-manager')
142    command_history = lineparser.LimitLineParser(
143        standarddir.data(), 'cmd-history',
144        limit='completion.cmd_history_max_items')
145    objreg.register('command-history', command_history)
146    save_manager.add_saveable('command-history', command_history.save,
147                              command_history.changed)
148