""" application independent commands """ import logging import urwid from sen.exceptions import NotifyError from sen.tui.buffer import HelpBuffer, TreeBuffer from sen.tui.commands.base import ( register_command, SameThreadCommand, Option, Argument, NoSuchCommand ) from sen.util import log_traceback, log_last_traceback logger = logging.getLogger(__name__) class LogTracebackMixin: @log_traceback def do(self, fn, *args, **kwargs): try: fn(*args, **kwargs) except NotifyError as ex: logger.error(repr(ex)) self.ui.notify_message(str(ex), level="error") except Exception as ex: logger.error(repr(ex)) self.ui.notify_message("Command failed: %s" % str(ex), level="error") raise @register_command class QuitCommand(SameThreadCommand): name = "quit" # TODO: make this configurable by asking whether to quit or not description = "Quit sen. No questions asked." def run(self): self.ui.quit() @register_command class KillBufferCommand(SameThreadCommand): name = "kill-buffer" description = "Remove currently displayed buffer." options_definitions = [ Option("quit-if-no-buffer", "Quit when there's no buffer left", default=False) ] def run(self): buffers_left = self.ui.remove_current_buffer(close_if_no_buffer=self.arguments.quit_if_no_buffer) if buffers_left is None: self.ui.notify_message("Last buffer will not be removed.") elif buffers_left == 0: self.ui.run_command(QuitCommand.name) @register_command class SelectBufferCommand(SameThreadCommand): name = "select-buffer" description = "Display buffer with selected index." arguments_definitions = [ Argument("index", "Index of buffer to display", default=1, action=int) ] def run(self): self.ui.pick_and_display_buffer(self.arguments.index) @register_command class SelectNextBufferCommand(SelectBufferCommand): name = "select-next-buffer" description = "Display next buffer." def run(self): self.arguments.set_argument("index", self.ui.current_buffer_index + 1) super().run() @register_command class SelectPreviousBufferCommand(SelectBufferCommand): name = "select-previous-buffer" description = "Display previous buffer." def run(self): self.arguments.set_argument("index", self.ui.current_buffer_index - 1) super().run() @register_command class DisplayBufferCommand(SameThreadCommand): name = "display-buffer" # TODO: make this a universal display function arguments_definitions = [Argument("buffer", "Buffer instance to show.")] description = "This is an internal command and doesn't work from command line." def run(self): # TODO: doesn't work!, the method expects buffer class, not string self.ui.add_and_display_buffer(self.arguments.buffer) @register_command class DisplayHelpCommand(SameThreadCommand): name = "help" description = "Display help about buffer or command. When 'query' is not specified " + \ "help for current buffer is being displayed." arguments_definitions = [Argument("query", "input string: command, buffer name")] def run(self): if self.arguments.query is None: self.ui.add_and_display_buffer(HelpBuffer(self.ui, self.buffer)) return try: command = self.ui.commander.get_command(self.arguments.query) except NoSuchCommand: self.ui.notify_message("There is no such command: %r" % self.arguments.query) else: self.ui.add_and_display_buffer(HelpBuffer(self.ui, command)) return # TODO: help view for commands could be displayed in footer @register_command class DisplayLayersCommand(DisplayBufferCommand): name = "layers" description = "open a tree view of all image layers (`docker images --tree` equivalent)" def run(self): self.arguments.set_argument("buffer", TreeBuffer(self.ui, self.docker_backend)) super().run() @log_traceback def run_command_callback(ui, docker_object, edit_widget, text_input): logger.debug("%r %r", edit_widget, text_input) if "\n" in text_input: inp = text_input.strip() inp = inp.replace("\n", "") # first restore statusbar and then run the command ui.prompt_bar = None ui.set_focus("body") try: ui.run_command(inp, docker_object=docker_object) except NoSuchCommand as ex: logger.info("non-existent command initiated: %r", inp) ui.notify_message(str(ex), level="error") except Exception as ex: logger.info("command %r failed: %r", inp, ex) ui.notify_message("Error while running command '{}': {}".format( inp, ex ), level="error") log_last_traceback() ui.reload_footer() @register_command class PromptCommand(SameThreadCommand): name = "prompt" description = "Customize and pre-populate prompt with initial data." options_definitions = [ Option("prompt-text", "Text forming the actual prompt", default=":"), Option("initial-text", "Prepopulated text", default="") ] def run(self): """ prompt for text input. """ # set up widgets leftpart = urwid.Text(self.arguments.prompt_text, align='left') editpart = urwid.Edit(multiline=True, edit_text=self.arguments.initial_text) # build promptwidget edit = urwid.Columns([ ('fixed', len(self.arguments.prompt_text), leftpart), ('weight', 1, editpart), ]) self.ui.prompt_bar = urwid.AttrMap(edit, "main_list_dg") self.ui.reload_footer() self.ui.set_focus("footer") urwid.connect_signal(editpart, "change", run_command_callback, user_args=[self.ui, self.docker_object]) @register_command class SearchCommand(SameThreadCommand, LogTracebackMixin): name = "search" description = "search and highlight (provide empty string to disable searching)" arguments_definitions = [ Argument("query", "Input string to search for") ] aliases = ["/"] def run(self): # TODO: implement incsearch # - match needs to be highlighted somehow, not with focus though # - a line could split into a Text with multiple markups query = self.arguments.query if self.arguments.query is not None else "" self.do(self.ui.current_buffer.find_next, query) @register_command class SearchNextCommand(SameThreadCommand, LogTracebackMixin): name = "search-next" description = "next search occurrence" def run(self): self.do(self.ui.current_buffer.find_next) @register_command class SearchPreviousCommand(SameThreadCommand, LogTracebackMixin): name = "search-previous" description = "previous search occurrence" def run(self): self.do(self.ui.current_buffer.find_previous) @register_command class SearchCommand(SameThreadCommand, LogTracebackMixin): name = "filter" description = """\ Display only lines matching provided query (provide empty query to clear filtering) In main listing, you may specify more precise query with these space-separated filters: * t[ype]=c[ontainer[s]] * t[ype]=i[mage[s]] * s[tate]=r[unning]) Examples * "type=container" - show only containers (short equivalent is "t=c") * "type=image fedora" - show images with string "fedora" in name (equivalent "t=i fedora")\ """ arguments_definitions = [ Argument("query", "Input query string", default="") ] def run(self): # TODO: realtime list change would be mindblowing self.do(self.ui.current_buffer.filter, self.arguments.query) @register_command class RefreshCurrentBufferCommand(SameThreadCommand): name = "refresh" description = "Refresh current buffer." def run(self): self.ui.current_buffer.refresh() @register_command class SearchPreviousCommand(SameThreadCommand): name = "toggle-live-updates" description = "Toggle realtime updates of the interface (this is useful when you are " + \ "removing multiple objects and don't want the listing change during " + \ "the process so you accidentally remove something)." def run(self): self.ui.current_buffer.widget.toggle_realtime_events() @register_command class RedrawUI(SameThreadCommand): name = "redraw" description = "Redraw user interface." def run(self): self.ui.loop.screen.clear() self.ui.refresh()