Implement searching for menu items
This uses prompt-toolkit's builtin search functionality to search through the buffer representing the menu, allowing the user to navigate faster through the entries
This commit is contained in:
parent
ec07ebe536
commit
f0e5b7bb58
|
@ -3,10 +3,12 @@
|
||||||
from prompt_toolkit.application import Application
|
from prompt_toolkit.application import Application
|
||||||
from prompt_toolkit.buffer import Buffer
|
from prompt_toolkit.buffer import Buffer
|
||||||
from prompt_toolkit.document import Document
|
from prompt_toolkit.document import Document
|
||||||
|
from prompt_toolkit.filters import is_searching
|
||||||
from prompt_toolkit.key_binding import KeyBindings
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
from prompt_toolkit.layout import Layout, Window, HSplit
|
from prompt_toolkit.layout import Layout, Window, HSplit
|
||||||
from prompt_toolkit.layout.controls import BufferControl
|
from prompt_toolkit.layout.controls import BufferControl
|
||||||
from prompt_toolkit.layout.processors import Processor, Transformation
|
from prompt_toolkit.layout.processors import Processor, Transformation
|
||||||
|
from prompt_toolkit import search
|
||||||
from prompt_toolkit.widgets import SearchToolbar
|
from prompt_toolkit.widgets import SearchToolbar
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,6 +141,8 @@ class CliMenu:
|
||||||
return ' ' * (len(self._cursor) + 1 * self._dedent_selection)
|
return ' ' * (len(self._cursor) + 1 * self._dedent_selection)
|
||||||
|
|
||||||
def _transform_line(self, ti):
|
def _transform_line(self, ti):
|
||||||
|
if len(list(ti.fragments)) == 0:
|
||||||
|
return Transformation(ti.fragments)
|
||||||
style, text = list(ti.fragments)[0]
|
style, text = list(ti.fragments)[0]
|
||||||
item = self._items[ti.lineno]
|
item = self._items[ti.lineno]
|
||||||
s = self._style
|
s = self._style
|
||||||
|
@ -160,7 +164,9 @@ class CliMenu:
|
||||||
indent += ' ' * (self._header_indent + len(self._cursor) + 1)
|
indent += ' ' * (self._header_indent + len(self._cursor) + 1)
|
||||||
style = s.header_style
|
style = s.header_style
|
||||||
|
|
||||||
return Transformation([('', indent), (style, prefix + text)])
|
items = [(s if s else style, t) for s, t in ti.fragments]
|
||||||
|
|
||||||
|
return Transformation([('', indent), (style, prefix)] + items)
|
||||||
|
|
||||||
def next_item(self, direction):
|
def next_item(self, direction):
|
||||||
if not any(item.focusable for item in self._items):
|
if not any(item.focusable for item in self._items):
|
||||||
|
@ -175,6 +181,38 @@ class CliMenu:
|
||||||
if self._items[self._pos].focusable:
|
if self._items[self._pos].focusable:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def sync_cursor_to_line(self, line, sync_dir=1):
|
||||||
|
"""Sync cursor to next fousable item starting on `line`"""
|
||||||
|
assert sync_dir in (1, -1)
|
||||||
|
|
||||||
|
self._pos = line
|
||||||
|
while not self._items[self._pos].focusable:
|
||||||
|
self._pos = (self._pos + sync_dir) % len(self._items)
|
||||||
|
self._buf.cursor_position = self._doc.translate_row_col_to_index(self._pos, 0)
|
||||||
|
|
||||||
|
def _get_search_result_lines(self):
|
||||||
|
"""Get a list of all lines that have a match with the current search result"""
|
||||||
|
if not self._bufctrl.search_state.text:
|
||||||
|
return []
|
||||||
|
|
||||||
|
idx_list = []
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
next_idx = self._buf.get_search_position(self._bufctrl.search_state, count=i,
|
||||||
|
include_current_position=False)
|
||||||
|
if next_idx in idx_list:
|
||||||
|
break
|
||||||
|
idx_list.append(next_idx)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for idx in idx_list:
|
||||||
|
line, _ = self._doc.translate_index_to_position(idx)
|
||||||
|
if line not in lines:
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
class MenuColorizer(Processor):
|
class MenuColorizer(Processor):
|
||||||
def apply_transformation(_self, ti):
|
def apply_transformation(_self, ti):
|
||||||
|
@ -183,40 +221,62 @@ class CliMenu:
|
||||||
# keybindings
|
# keybindings
|
||||||
self._kb = KeyBindings()
|
self._kb = KeyBindings()
|
||||||
|
|
||||||
@self._kb.add('q')
|
@self._kb.add('q', filter=~is_searching)
|
||||||
@self._kb.add('c-c')
|
@self._kb.add('c-c')
|
||||||
def quit(event):
|
def quit(event):
|
||||||
event.app.exit()
|
event.app.exit()
|
||||||
|
|
||||||
@self._kb.add('down')
|
@self._kb.add('down', filter=~is_searching)
|
||||||
@self._kb.add('j')
|
@self._kb.add('j', filter=~is_searching)
|
||||||
def down(event):
|
def down(event):
|
||||||
self.next_item(1)
|
self.next_item(1)
|
||||||
|
|
||||||
@self._kb.add('up')
|
@self._kb.add('up', filter=~is_searching)
|
||||||
@self._kb.add('k')
|
@self._kb.add('k', filter=~is_searching)
|
||||||
def up(event):
|
def up(event):
|
||||||
self.next_item(-1)
|
self.next_item(-1)
|
||||||
|
|
||||||
@self._kb.add('right')
|
@self._kb.add('N', filter=~is_searching)
|
||||||
@self._kb.add('c-m')
|
@self._kb.add('n', filter=~is_searching)
|
||||||
@self._kb.add('c-space')
|
def search_inc(event, filter=is_searching):
|
||||||
|
if not self._bufctrl.search_state.text:
|
||||||
|
return
|
||||||
|
|
||||||
|
search_dir = 1 if event.data == 'n' else -1
|
||||||
|
sr_lines = self._get_search_result_lines()
|
||||||
|
if sr_lines:
|
||||||
|
line = sr_lines[search_dir] if len(sr_lines) > 1 else sr_lines[0]
|
||||||
|
self.sync_cursor_to_line(line, search_dir)
|
||||||
|
|
||||||
|
@self._kb.add('c-m', filter=~is_searching)
|
||||||
|
@self._kb.add('right', filter=~is_searching)
|
||||||
|
@self._kb.add('c-space', filter=~is_searching)
|
||||||
def accept(event):
|
def accept(event):
|
||||||
self._success = True
|
self._success = True
|
||||||
event.app.exit()
|
event.app.exit()
|
||||||
|
|
||||||
|
@self._kb.add('c-m', filter=is_searching)
|
||||||
|
def accept_search(event):
|
||||||
|
search.accept_search()
|
||||||
|
new_line, _ = self._doc.translate_index_to_position(self._buf.cursor_position)
|
||||||
|
self.sync_cursor_to_line(new_line)
|
||||||
|
|
||||||
|
self._searchbar = SearchToolbar(ignore_case=True)
|
||||||
|
|
||||||
text = '\n'.join(map(lambda _x: _x.text, self._items))
|
text = '\n'.join(map(lambda _x: _x.text, self._items))
|
||||||
self._doc = Document(text, cursor_position=self._pos)
|
self._doc = Document(text, cursor_position=self._pos)
|
||||||
self._buf = Buffer(read_only=True, document=self._doc)
|
self._buf = Buffer(read_only=True, document=self._doc)
|
||||||
self._bufctrl = BufferControl(self._buf,
|
self._bufctrl = BufferControl(self._buf,
|
||||||
|
search_buffer_control=self._searchbar.control,
|
||||||
|
preview_search=True,
|
||||||
input_processors=[MenuColorizer()])
|
input_processors=[MenuColorizer()])
|
||||||
split = HSplit([Window(self._bufctrl,
|
split = HSplit([Window(self._bufctrl,
|
||||||
wrap_lines=True,
|
wrap_lines=True,
|
||||||
always_hide_cursor=True)])
|
always_hide_cursor=True),
|
||||||
|
self._searchbar])
|
||||||
|
|
||||||
# set initial pos
|
# set initial pos
|
||||||
if not self._items[self._pos].focusable:
|
self.sync_cursor_to_line(0)
|
||||||
self.next_item(1)
|
|
||||||
|
|
||||||
app = Application(layout=Layout(split),
|
app = Application(layout=Layout(split),
|
||||||
key_bindings=self._kb,
|
key_bindings=self._kb,
|
||||||
|
|
Loading…
Reference in New Issue