diff --git a/sen/docker_backend.py b/sen/docker_backend.py index a3d3b1f..1a9d7b1 100644 --- a/sen/docker_backend.py +++ b/sen/docker_backend.py @@ -179,6 +179,10 @@ def short_id(self): def display_time_created(self): return humanize.naturaltime(self.created) + def display_formal_time_created(self): + # http://tools.ietf.org/html/rfc2822.html#section-3.3 + return self.created.strftime("%d %b %Y, %H:%M:%S") + def inspect(self): raise NotImplementedError() @@ -243,6 +247,20 @@ def container_command(self): return " ".join(cmd) return "" + @property + def size(self): + """ + Size of all layers in bytes + + :return: int + """ + return self.data["VirtualSize"] + + @property + def labels(self): + labels = self.data["Labels"] + return labels + @property def names(self): if self._names is None: diff --git a/sen/tui/buffer.py b/sen/tui/buffer.py index d49fdc7..c4a1df5 100644 --- a/sen/tui/buffer.py +++ b/sen/tui/buffer.py @@ -3,6 +3,7 @@ from sen.docker_backend import DockerContainer from sen.exceptions import NotifyError from sen.tui.constants import HELP_TEXT +from sen.tui.widgets.info import ImageInfoWidget from sen.tui.widgets.list.main import MainListBox from sen.tui.widgets.list.util import get_operation_notify_widget from sen.tui.widgets.list.common import AsyncScrollableListBox, ScrollableListBox @@ -59,6 +60,19 @@ def filter(self, s): self.widget.set_body(widgets) +class ImageInfoBuffer(Buffer): + tag = "I" + + def __init__(self, docker_image, ui): + """ + :param docker_image: + :param ui: ui object so we refresh + """ + self.display_name = docker_image.short_name + self.widget = ImageInfoWidget(ui, docker_image) + super().__init__() + + class MainListBuffer(Buffer): display_name = "Listing" tag = "M" diff --git a/sen/tui/init.py b/sen/tui/init.py index 6b7a10c..645172d 100644 --- a/sen/tui/init.py +++ b/sen/tui/init.py @@ -4,11 +4,12 @@ from sen.exceptions import NotifyError from sen.tui.commands import search, filter from sen.tui.statusbar import Footer -from sen.tui.buffer import LogsBuffer, MainListBuffer, InspectBuffer, HelpBuffer +from sen.tui.buffer import LogsBuffer, MainListBuffer, InspectBuffer, HelpBuffer, ImageInfoBuffer from sen.tui.constants import PALLETE from sen.docker_backend import DockerBackend import urwid + from sen.util import log_traceback logger = logging.getLogger(__name__) @@ -159,6 +160,9 @@ def display_and_follow_logs(self, docker_container): def inspect(self, docker_object): self.add_and_display_buffer(InspectBuffer(docker_object)) + def display_image_info(self, docker_image): + self.add_and_display_buffer(ImageInfoBuffer(docker_image, self)) + def refresh_main_buffer(self, refresh_buffer=True): assert self.main_list_buffer is not None if refresh_buffer: diff --git a/sen/tui/widgets/info.py b/sen/tui/widgets/info.py new file mode 100644 index 0000000..04cee91 --- /dev/null +++ b/sen/tui/widgets/info.py @@ -0,0 +1,151 @@ +""" +Info widgets: + * display detailed info about an object +""" +import logging + +import urwid + +from sen.tui.widgets.list.util import get_map, RowWidget +from sen.tui.widgets.table import ResponsiveTable, assemble_rows +from sen.util import humanize_bytes +from sen.tui.widgets.list.base import VimMovementListBox +from sen.tui.widgets.util import AdHocAttrMap, get_basic_image_markup + +logger = logging.getLogger(__name__) + + +class SelectableText(AdHocAttrMap): + def __init__(self, text, maps=None): + maps = maps or get_map() + super().__init__(urwid.Text(text, align="left", wrap="clip"), maps) + + @property + def text(self): + return self._original_widget.text + + def keypress(self, size, key): + """ get rid of tback: `AttributeError: 'Text' object has no attribute 'keypress'` """ + return key + + +class LayerWidget(SelectableText): + def __init__(self, ui, docker_image, index=0): + self.ui = ui + self.docker_image = docker_image + separator = "└─" + if index == 0: + label = [separator] + else: + label = [2 * index * " " + separator] + super().__init__(label + get_basic_image_markup(docker_image)) + + def keypress(self, size, key): + logger.debug("%s %s %s", self.__class__, key, size) + if key == "enter": + self.ui.display_image_info(self.docker_image) + return + elif key == "i": + self.ui.inspect(self.docker_image) # FIXME: do this async + return + return key + + +class TagWidget(SelectableText): + def __init__(self, ui, docker_image, tag): + self.ui = ui + self.docker_image = docker_image + self.tag = tag + super().__init__(str(self.tag)) + + def keypress(self, size, key): + logger.debug("%s %s %s", self.__class__, key, size) + if key == "d": + self.docker_image.remove_tag(self.tag) # FIXME: do this async + # TODO: refresh + return + return key + + +class ImageInfoWidget(VimMovementListBox): + """ + display info about image + """ + def __init__(self, ui, docker_image): + self.ui = ui + self.docker_image = docker_image + + self.walker = urwid.SimpleFocusListWalker([]) + + # self.widgets = [] + + self._basic_data() + self._containers() + self._image_names() + self._layers() + self._labels() + + super().__init__(self.walker) + + self.set_focus(0) # or assemble list first and then stuff it into walker + + def _basic_data(self): + data = [ + [SelectableText("Id", maps=get_map("main_list_green")), + SelectableText(self.docker_image.image_id)], + [SelectableText("Created", maps=get_map("main_list_green")), + SelectableText("{0}, {1}".format(self.docker_image.display_formal_time_created(), + self.docker_image.display_time_created()))], + [SelectableText("Size", maps=get_map("main_list_green")), + SelectableText(humanize_bytes(self.docker_image.size))], + [SelectableText("Command", maps=get_map("main_list_green")), + SelectableText(self.docker_image.container_command)], + ] + self.walker.extend(assemble_rows(data)) + + def _image_names(self): + if not self.docker_image.names: + return + self.walker.append(RowWidget([SelectableText("")])) + self.walker.append(RowWidget([SelectableText("Image Names", maps=get_map("main_list_white"))])) + for n in self.docker_image.names: + self.walker.append(RowWidget([TagWidget(self.ui, self.docker_image, n)])) + + def _layers(self): + self.walker.append(RowWidget([SelectableText("")])) + self.walker.append(RowWidget([SelectableText("Layers", maps=get_map("main_list_white"))])) + + i = self.docker_image + index = 0 + self.walker.append(RowWidget([LayerWidget(self.ui, self.docker_image, index=index)])) + while True: + index += 1 + parent = i.parent_image + if parent: + self.walker.append(RowWidget([LayerWidget(self.ui, parent, index=index)])) + i = parent + else: + break + + def _labels(self): + if not self.docker_image.labels: + return [] + data = [] + self.walker.append(RowWidget([SelectableText("")])) + self.walker.append(RowWidget([SelectableText("Labels", maps=get_map("main_list_white"))])) + for label_key, label_value in self.docker_image.labels.items(): + data.append([SelectableText(label_key, maps=get_map("main_list_green")), SelectableText(label_value)]) + self.walker.extend(assemble_rows(data)) + + def _containers(self): + if not self.docker_image.containers(): + return + self.walker.append(RowWidget([SelectableText("")])) + self.walker.append(RowWidget([SelectableText("Containers", maps=get_map("main_list_white"))])) + for container in self.docker_image.containers(): + self.walker.append(RowWidget([SelectableText(str(container))])) + + def keypress(self, size, key): + logger.debug("%s, %s", key, size) + key = super().keypress(size, key) + return key diff --git a/sen/tui/widgets/list/main.py b/sen/tui/widgets/list/main.py index b78875b..273d1fd 100644 --- a/sen/tui/widgets/list/main.py +++ b/sen/tui/widgets/list/main.py @@ -318,6 +318,9 @@ def do_and_report_on_fail(f, docker_object): elif key == "f": self.ui.run_in_background(do_and_report_on_fail, self.ui.display_and_follow_logs, self.focused_docker_object) return + elif key == "enter": + self.ui.run_in_background(do_and_report_on_fail, self.ui.display_image_info, self.focused_docker_object) + return elif key == "d": self.ui.run_in_background( run_and_report_on_fail, diff --git a/sen/tui/widgets/util.py b/sen/tui/widgets/util.py index 3f191ca..7934913 100644 --- a/sen/tui/widgets/util.py +++ b/sen/tui/widgets/util.py @@ -31,3 +31,16 @@ def set_map(self, attrstring): for a in self.attrs: attr_map[a] = self.maps["focus"] self.set_attr_map(attr_map) + + +def get_basic_image_markup(docker_image): + text_markup = [docker_image.short_id] + + if docker_image.names: + text_markup.append(" ") + text_markup.append(("main_list_lg", docker_image.names[0].to_str())) + + text_markup.append(" ") + text_markup.append(("main_list_ddg", docker_image.container_command)) + + return text_markup diff --git a/sen/util.py b/sen/util.py index 0f27899..82a9d15 100644 --- a/sen/util.py +++ b/sen/util.py @@ -41,3 +41,27 @@ def setup_dirs(): def get_log_file_path(): return os.path.join(setup_dirs(), LOG_FILE_NAME) + + +def humanize_bytes(bytesize, precision=2): + """ + Humanize byte size figures + + https://gist.github.com/moird/3684595 + """ + abbrevs = ( + (1 << 50, 'PB'), + (1 << 40, 'TB'), + (1 << 30, 'GB'), + (1 << 20, 'MB'), + (1 << 10, 'kB'), + (1, 'bytes') + ) + if bytesize == 1: + return '1 byte' + for factor, suffix in abbrevs: + if bytesize >= factor: + break + if factor == 1: + precision = 0 + return '%.*f %s' % (precision, bytesize / float(factor), suffix) \ No newline at end of file