Add tab completion support
This commit is contained in:
parent
df00d28af4
commit
0905695910
|
@ -161,7 +161,7 @@ class ConsoleUI(component.Component):
|
|||
# We want to do an interactive session, so start up the curses screen and
|
||||
# pass it the function that handles commands
|
||||
colors.init_colors()
|
||||
self.screen = screen.Screen(stdscr, self.do_command)
|
||||
self.screen = screen.Screen(stdscr, self.do_command, self.tab_completer)
|
||||
self.statusbars = StatusBars()
|
||||
|
||||
self.screen.topbar = "{{status}}Deluge " + deluge.common.get_version() + " Console"
|
||||
|
@ -176,7 +176,19 @@ class ConsoleUI(component.Component):
|
|||
reactor.run()
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
# Maintain a list of (torrent_id, name) for use in tab completion
|
||||
self.torrents = []
|
||||
def on_session_state(result):
|
||||
def on_torrents_status(torrents):
|
||||
for torrent_id, status in torrents.items():
|
||||
self.torrents.append((torrent_id, status["name"]))
|
||||
|
||||
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||
client.core.get_session_state().addCallback(on_session_state)
|
||||
|
||||
# Register some event handlers to keep the torrent list up-to-date
|
||||
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
|
||||
client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event)
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
@ -227,3 +239,87 @@ class ConsoleUI(component.Component):
|
|||
raise
|
||||
except Exception, e:
|
||||
self.write("{{error}}" + str(e))
|
||||
|
||||
def tab_completer(self, line, cursor, second_hit):
|
||||
"""
|
||||
Called when the user hits 'tab' and will autocomplete or show options.
|
||||
|
||||
:param line: str, the current input string
|
||||
:param cursor: int, the cursor position in the line
|
||||
:param second_hit: bool, if this is the second time in a row the tab key
|
||||
has been pressed
|
||||
|
||||
:returns: 2-tuple (string, cursor position)
|
||||
|
||||
"""
|
||||
# First check to see if there is no space, this will mean that it's a
|
||||
# command that needs to be completed.
|
||||
if " " not in line:
|
||||
if len(line) == 0:
|
||||
# We only print these out if it's a second_hit
|
||||
if second_hit:
|
||||
# There is nothing in line so just print out all possible commands
|
||||
# and return.
|
||||
self.write(" ")
|
||||
for cmd in self._commands:
|
||||
self.write(cmd)
|
||||
return ("", 0)
|
||||
# Iterate through the commands looking for ones that startwith the
|
||||
# line.
|
||||
possible_matches = []
|
||||
for cmd in self._commands:
|
||||
if cmd.startswith(line):
|
||||
possible_matches.append(cmd)
|
||||
|
||||
line_prefix = ""
|
||||
|
||||
else:
|
||||
# This isn't a command so treat it as a torrent_id or torrent name
|
||||
name = line.split(" ")[-1]
|
||||
if len(name) == 0:
|
||||
# There is nothing in the string, so just display all possible options
|
||||
if second_hit:
|
||||
self.write(" ")
|
||||
# Display all torrent_ids and torrent names
|
||||
for torrent_id, name in self.torrents:
|
||||
self.write(torrent_id)
|
||||
self.write(name)
|
||||
return (line, cursor)
|
||||
|
||||
# Find all possible matches
|
||||
possible_matches = []
|
||||
for torrent_id, torrent_name in self.torrents:
|
||||
if torrent_id.startswith(name):
|
||||
possible_matches.append(torrent_id)
|
||||
elif torrent_name.startswith(name):
|
||||
possible_matches.append(torrent_name)
|
||||
|
||||
# Set the line prefix that should be prepended to any input line match
|
||||
line_prefix = " ".join(line.split(" ")[:-1]) + " "
|
||||
|
||||
# No matches, so just return what we got passed
|
||||
if len(possible_matches) == 0:
|
||||
return (line, cursor)
|
||||
# If we only have 1 possible match, then just modify the line and
|
||||
# return it, else we need to print out the matches without modifying
|
||||
# the line.
|
||||
elif len(possible_matches) == 1:
|
||||
new_line = line_prefix + possible_matches[0] + " "
|
||||
return (new_line, len(new_line))
|
||||
else:
|
||||
if second_hit:
|
||||
# Only print these out if it's a second_hit
|
||||
self.write(" ")
|
||||
for cmd in possible_matches:
|
||||
self.write(cmd)
|
||||
return (line, cursor)
|
||||
|
||||
def on_torrent_added_event(self, torrent_id):
|
||||
def on_torrent_status(status):
|
||||
self.torrents.append(torrent_id, status["name"])
|
||||
client.get_torrent_status(torrent_id, ["name"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, torrent_id):
|
||||
for index, (tid, name) in enumerate(self.torrents):
|
||||
if torrent_id == tid:
|
||||
del self.torrents[index]
|
||||
|
|
|
@ -44,16 +44,21 @@ LINES_BUFFER_SIZE = 5000
|
|||
INPUT_HISTORY_SIZE = 500
|
||||
|
||||
class Screen(CursesStdIO):
|
||||
def __init__(self, stdscr, command_parser):
|
||||
def __init__(self, stdscr, command_parser, tab_completer=None):
|
||||
"""
|
||||
A curses screen designed to run as a reader in a twisted reactor.
|
||||
|
||||
:param command_parser: a function that will be passed a string when the
|
||||
user hits enter
|
||||
:param tab_completer: a function that is sent the `:prop:input` string when
|
||||
the user hits tab. It's intended purpose is to modify the input string.
|
||||
It should return a 2-tuple (input string, input cursor).
|
||||
|
||||
"""
|
||||
log.debug("Screen init!")
|
||||
# Function to be called with commands
|
||||
self.command_parser = command_parser
|
||||
self.tab_completer = tab_completer
|
||||
self.stdscr = stdscr
|
||||
# Make the input calls non-blocking
|
||||
self.stdscr.nodelay(1)
|
||||
|
@ -67,6 +72,9 @@ class Screen(CursesStdIO):
|
|||
self.input_history = []
|
||||
self.input_history_index = 0
|
||||
|
||||
# Keep track of double-tabs
|
||||
self.tab_count = 0
|
||||
|
||||
# Strings for the 2 status bars
|
||||
self.topbar = ""
|
||||
self.bottombar = ""
|
||||
|
@ -78,13 +86,7 @@ class Screen(CursesStdIO):
|
|||
# The offset to display lines
|
||||
self.display_lines_offset = 0
|
||||
|
||||
# Create some color pairs, should probably be moved to colors.py
|
||||
# Regular text
|
||||
#curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
||||
# Status bar
|
||||
#curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLUE)
|
||||
|
||||
|
||||
# Refresh the screen to display everything right away
|
||||
self.refresh()
|
||||
|
||||
def connectionLost(self, reason):
|
||||
|
@ -117,7 +119,7 @@ class Screen(CursesStdIO):
|
|||
self.lines.extend(text.splitlines())
|
||||
while len(self.lines) > LINES_BUFFER_SIZE:
|
||||
# Remove the oldest line if the max buffer size has been reached
|
||||
self.lines.remove(0)
|
||||
del self.lines[0]
|
||||
|
||||
self.refresh()
|
||||
|
||||
|
@ -197,7 +199,7 @@ class Screen(CursesStdIO):
|
|||
if len(self.input_history) == INPUT_HISTORY_SIZE:
|
||||
# Remove the oldest input history if the max history size
|
||||
# is reached.
|
||||
self.input_history.remove(0)
|
||||
del self.input_history[0]
|
||||
self.input_history.append(self.input)
|
||||
self.input_history_index = len(self.input_history)
|
||||
self.input = ""
|
||||
|
@ -205,6 +207,22 @@ class Screen(CursesStdIO):
|
|||
self.input_cursor = 0
|
||||
self.stdscr.refresh()
|
||||
|
||||
# Run the tab completer function
|
||||
elif c == 9:
|
||||
# Keep track of tab hit count to know when it's double-hit
|
||||
self.tab_count += 1
|
||||
if self.tab_count > 1:
|
||||
second_hit = True
|
||||
self.tab_count = 0
|
||||
else:
|
||||
second_hit = False
|
||||
|
||||
if self.tab_completer:
|
||||
# We only call the tab completer function if we're at the end of
|
||||
# the input string on the cursor is on a space
|
||||
if self.input_cursor == len(self.input) or self.input[self.input_cursor] == " ":
|
||||
self.input, self.input_cursor = self.tab_completer(self.input, self.input_cursor, second_hit)
|
||||
|
||||
# We use the UP and DOWN keys to cycle through input history
|
||||
elif c == curses.KEY_UP:
|
||||
if self.input_history_index - 1 >= 0:
|
||||
|
@ -254,6 +272,10 @@ class Screen(CursesStdIO):
|
|||
# Move the cursor forward
|
||||
self.input_cursor += 1
|
||||
|
||||
# We remove the tab count if the key wasn't a tab
|
||||
if c != 9:
|
||||
self.tab_count = 0
|
||||
|
||||
# Update the input string on the screen
|
||||
self.add_string(self.rows - 1, self.input)
|
||||
self.stdscr.move(self.rows - 1, self.input_cursor)
|
||||
|
|
Loading…
Reference in New Issue