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
|
# 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]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue