Add tab completion support

This commit is contained in:
Andrew Resch 2009-04-18 22:27:54 +00:00
parent df00d28af4
commit 0905695910
2 changed files with 130 additions and 12 deletions

View File

@ -161,7 +161,7 @@ class ConsoleUI(component.Component):
# We want to do an interactive session, so start up the curses screen and # We want to do an interactive session, so start up the curses screen and
# pass it the function that handles commands # pass it the function that handles commands
colors.init_colors() 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.statusbars = StatusBars()
self.screen.topbar = "{{status}}Deluge " + deluge.common.get_version() + " Console" self.screen.topbar = "{{status}}Deluge " + deluge.common.get_version() + " Console"
@ -176,7 +176,19 @@ class ConsoleUI(component.Component):
reactor.run() reactor.run()
def start(self): 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): def update(self):
pass pass
@ -227,3 +239,87 @@ class ConsoleUI(component.Component):
raise raise
except Exception, e: except Exception, e:
self.write("{{error}}" + str(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]

View File

@ -44,16 +44,21 @@ LINES_BUFFER_SIZE = 5000
INPUT_HISTORY_SIZE = 500 INPUT_HISTORY_SIZE = 500
class Screen(CursesStdIO): 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. 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 :param command_parser: a function that will be passed a string when the
user hits enter 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!") log.debug("Screen init!")
# Function to be called with commands # Function to be called with commands
self.command_parser = command_parser self.command_parser = command_parser
self.tab_completer = tab_completer
self.stdscr = stdscr self.stdscr = stdscr
# Make the input calls non-blocking # Make the input calls non-blocking
self.stdscr.nodelay(1) self.stdscr.nodelay(1)
@ -67,6 +72,9 @@ class Screen(CursesStdIO):
self.input_history = [] self.input_history = []
self.input_history_index = 0 self.input_history_index = 0
# Keep track of double-tabs
self.tab_count = 0
# Strings for the 2 status bars # Strings for the 2 status bars
self.topbar = "" self.topbar = ""
self.bottombar = "" self.bottombar = ""
@ -78,13 +86,7 @@ class Screen(CursesStdIO):
# The offset to display lines # The offset to display lines
self.display_lines_offset = 0 self.display_lines_offset = 0
# Create some color pairs, should probably be moved to colors.py # Refresh the screen to display everything right away
# Regular text
#curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
# Status bar
#curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLUE)
self.refresh() self.refresh()
def connectionLost(self, reason): def connectionLost(self, reason):
@ -117,7 +119,7 @@ class Screen(CursesStdIO):
self.lines.extend(text.splitlines()) self.lines.extend(text.splitlines())
while len(self.lines) > LINES_BUFFER_SIZE: while len(self.lines) > LINES_BUFFER_SIZE:
# Remove the oldest line if the max buffer size has been reached # Remove the oldest line if the max buffer size has been reached
self.lines.remove(0) del self.lines[0]
self.refresh() self.refresh()
@ -197,7 +199,7 @@ class Screen(CursesStdIO):
if len(self.input_history) == INPUT_HISTORY_SIZE: if len(self.input_history) == INPUT_HISTORY_SIZE:
# Remove the oldest input history if the max history size # Remove the oldest input history if the max history size
# is reached. # is reached.
self.input_history.remove(0) del self.input_history[0]
self.input_history.append(self.input) self.input_history.append(self.input)
self.input_history_index = len(self.input_history) self.input_history_index = len(self.input_history)
self.input = "" self.input = ""
@ -205,6 +207,22 @@ class Screen(CursesStdIO):
self.input_cursor = 0 self.input_cursor = 0
self.stdscr.refresh() 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 # We use the UP and DOWN keys to cycle through input history
elif c == curses.KEY_UP: elif c == curses.KEY_UP:
if self.input_history_index - 1 >= 0: if self.input_history_index - 1 >= 0:
@ -254,6 +272,10 @@ class Screen(CursesStdIO):
# Move the cursor forward # Move the cursor forward
self.input_cursor += 1 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 # Update the input string on the screen
self.add_string(self.rows - 1, self.input) self.add_string(self.rows - 1, self.input)
self.stdscr.move(self.rows - 1, self.input_cursor) self.stdscr.move(self.rows - 1, self.input_cursor)