File: autocomplete.py

package info (click to toggle)
python-panwid 0.3.5-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 408 kB
  • sloc: python: 4,904; makefile: 3
file content (332 lines) | stat: -rw-r--r-- 9,813 bytes parent folder | download | duplicates (2)
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
import logging
logger = logging.getLogger(__name__)
import itertools

import urwid

from .highlightable import HighlightableTextMixin
from .keymap import *
from  urwid_readline import ReadlineEdit

@keymapped()
class AutoCompleteEdit(ReadlineEdit):

    signals = ["select", "close", "complete_next", "complete_prev"]

    KEYMAP = {
        "enter": "confirm",
        "esc": "cancel"
    }

    def clear(self):
        self.set_edit_text("")

    def confirm(self):
        self._emit("select")
        self._emit("close")

    def cancel(self):
        self._emit("close")

    def complete_next(self):
        self._emit("complete_next")

    def complete_prev(self):
        self._emit("complete_prev")

    def keypress(self, size, key):
        return super().keypress(size, key)

@keymapped()
class AutoCompleteBar(urwid.WidgetWrap):

    signals = ["change", "complete_prev", "complete_next", "select", "close"]

    prompt_attr = "dropdown_prompt"

    def __init__(self, prompt_attr=None, complete_fn=None):

        self.prompt_attr = prompt_attr or self.prompt_attr
        self.prompt = urwid.Text((self.prompt_attr, "> "))
        self.text = AutoCompleteEdit("")
        if complete_fn:
            self.text.enable_autocomplete(complete_fn)

        # self.text.selectable = lambda x: False
        self.cols = urwid.Columns([
            (2, self.prompt),
            ("weight", 1, self.text)
        ], dividechars=0)
        self.cols.focus_position = 1
        self.filler = urwid.Filler(self.cols, valign="bottom")
        urwid.connect_signal(self.text, "postchange", self.text_changed)
        urwid.connect_signal(self.text, "complete_prev", lambda source: self._emit("complete_prev"))
        urwid.connect_signal(self.text, "complete_next", lambda source: self._emit("complete_next"))
        urwid.connect_signal(self.text, "select", lambda source: self._emit("select"))
        urwid.connect_signal(self.text, "close", lambda source: self._emit("close"))
        super(AutoCompleteBar, self).__init__(self.filler)

    def set_prompt(self, text):

        self.prompt.set_text((self.prompt_attr, text))

    def set_text(self, text):

        self.text.set_edit_text(text)

    def text_changed(self, source, text):
        self._emit("change", text)

    def confirm(self):
        self._emit("select")
        self._emit("close")

    def cancel(self):
        self._emit("close")

    def __len__(self):
        return len(self.body)

    def keypress(self, size, key):
        return super().keypress(size, key)

@keymapped()
class AutoCompleteMixin(object):

    auto_complete = None
    prompt_attr = "dropdown_prompt"

    def __init__(self, auto_complete, prompt_attr=None, *args, **kwargs):
        super().__init__(self.complete_container, *args, **kwargs)
        if auto_complete is not None: self.auto_complete = auto_complete
        if prompt_attr is not None:
            self.prompt_attr = prompt_attr
        self.auto_complete_bar = None
        self.completing = False
        self.complete_anywhere = False
        self.case_sensitive = False
        self.last_complete_pos = None
        self.complete_string_location = None
        self.last_filter_text = None

        if self.auto_complete:
            self.auto_complete_bar = AutoCompleteBar(
                prompt_attr=self.prompt_attr,
                complete_fn=self.complete_fn
            )

            urwid.connect_signal(
                self.auto_complete_bar, "change",
                lambda source, text: self.complete()
            )
            urwid.connect_signal(
                self.auto_complete_bar, "complete_prev",
                lambda source: self.complete_prev()
            )
            urwid.connect_signal(
                self.auto_complete_bar, "complete_next",
                lambda source: self.complete_next()
            )

            urwid.connect_signal(
                self.auto_complete_bar, "select", self.on_complete_select
            )
            urwid.connect_signal(
                self.auto_complete_bar, "close", self.on_complete_close
            )

    def keypress(self, size, key):
        return super().keypress(size, key)
        # key = super().keypress(size, key)
        # if self.completing and key == "enter":
        #     self.on_complete_select(self)
        # else:
        #     return key

    @property
    def complete_container(self):
        raise NotImplementedError

    @property
    def complete_container_position(self):
        return 1

    @property
    def complete_body_position(self):
        return 0

    @property
    def complete_body(self):
        raise NotImplementedError

    @property
    def complete_items(self):
        raise NotImplementedError

    def complete_fn(self, text, state):
        tmp = [
            c for c in self.complete_items
            if c and text in c
        ] if text else self.complete_items
        try:
            return str(tmp[state])
        except (IndexError, TypeError):
            return None

    def complete_widget_at_pos(self, pos):
        return self.complete_body[pos]

    def complete_set_focus(self, pos):
        self.focus_position = pos

    @keymap_command()
    def complete_prefix(self):
        self.complete_on()

    @keymap_command()
    def complete_substring(self):
        self.complete_on(anywhere=True)

    def complete_prev(self):
        self.complete(step=-1)

    def complete_next(self):
        self.complete(step=1)

    def complete_on(self, anywhere=False, case_sensitive=False):

        if self.completing:
            return
        self.completing = True
        self.show_bar()
        if anywhere:
            self.complete_anywhere = True
        else:
            self.complete_anywhere = False

        if case_sensitive:
            self.case_sensitive = True
        else:
            self.case_sensitive = False

    def complete_compare_substring(self, search, candidate):
        try:
            return candidate.index(search)
        except ValueError:
            return None

    def complete_compare_fn(self, search, candidate):

        if self.case_sensitive:
            f = lambda x: str(x)
        else:
            f = lambda x: str(x.lower())

        if self.complete_anywhere:
            return self.complete_compare_substring(f(search), f(candidate))
        else:
            return 0 if self.complete_compare_substring(f(search), f(candidate))==0 else None
        # return f(candidate)


    @keymap_command()
    def complete_off(self):

        if not self.completing:
            return
        self.filter_text = ""

        self.hide_bar()
        self.completing = False

    @keymap_command
    def complete(self, step=None, no_wrap=False):

        if not self.filter_text:
            return

        # if not step and self.filter_text == self.last_filter_text:
        #     return

        # logger.info(f"complete: {self.filter_text}")

        if self.last_complete_pos:
            widget = self.complete_widget_at_pos(self.last_complete_pos)
            if isinstance(widget, HighlightableTextMixin):
                widget.unhighlight()

        self.initial_pos = self.complete_body.get_focus()[1]
        positions = itertools.cycle(
            self.complete_body.positions(reverse=(step and step < 0))
        )
        pos = next(positions)
        # logger.info(pos.get_value())
        # import ipdb; ipdb.set_trace()
        while pos != self.initial_pos:
            # logger.info(pos.get_value())
            pos = next(positions)
        for i in range(abs(step or 0)):
            # logger.info(pos.get_value())
            pos = next(positions)

        while True:
            widget = self.complete_widget_at_pos(pos)
            complete_index = self.complete_compare_fn(self.filter_text, str(widget))
            if complete_index is not None:
                self.last_complete_pos = pos
                if isinstance(widget, HighlightableTextMixin):
                    widget.highlight(complete_index, complete_index+len(self.filter_text))
                self.complete_set_focus(pos)
                break
            pos = next(positions)
            if pos == self.initial_pos:
                break

        # logger.info("done")
        self.last_filter_text = self.filter_text

    @keymap_command()
    def cancel(self):
        logger.debug("cancel")
        self.complete_container.focus_position = self.selected_button
        self.close()

    def close(self):
        self._emit("close")

    def show_bar(self):
        pos = self.complete_container_pos
        self.complete_container.contents[pos:pos+1] += [(
            self.auto_complete_bar,
            self.complete_container.options("given", 1)
        )]
        # self.box.height -= 1
        self.complete_container.focus_position = pos

    def hide_bar(self):
        pos = self.complete_container_pos
        widget = self.complete_widget_at_pos(self.complete_body.get_focus()[1])
        if isinstance(widget, HighlightableTextMixin):
            widget.unhighlight()
        self.complete_container.focus_position = self.complete_body_position
        del self.complete_container.contents[pos]
        # self.box.height += 1

    @property
    def filter_text(self):
        return self.auto_complete_bar.text.get_text()[0]

    @filter_text.setter
    def filter_text(self, value):
        return self.auto_complete_bar.set_text(value)

    def on_complete_select(self, source):
        widget = self.complete_widget_at_pos(self.complete_body.get_focus()[1])
        self.complete_off()
        self._emit("select", self.last_complete_pos, widget)
        self._emit("close")

    def on_complete_close(self, source):
        self.complete_off()

__all__ = ["AutoCompleteMixin"]