[PATCH 01/13] Text based UI framework core
Vratislav Podzimek
vpodzime at redhat.com
Wed Aug 8 11:23:57 UTC 2012
On Wed, 2012-08-08 at 10:52 +0200, Martin Sivak wrote:
> ---
> pyanaconda/ui/tui/simpleline/__init__.py | 23 ++
> pyanaconda/ui/tui/simpleline/base.py | 385 ++++++++++++++++++++++++++++++
> pyanaconda/ui/tui/simpleline/widgets.py | 138 +++++++++++
> 3 files changed, 546 insertions(+)
> create mode 100644 pyanaconda/ui/tui/simpleline/__init__.py
> create mode 100644 pyanaconda/ui/tui/simpleline/base.py
> create mode 100644 pyanaconda/ui/tui/simpleline/widgets.py
>
> diff --git a/pyanaconda/ui/tui/simpleline/__init__.py b/pyanaconda/ui/tui/simpleline/__init__.py
> new file mode 100644
> index 0000000..e290e4a
> --- /dev/null
> +++ b/pyanaconda/ui/tui/simpleline/__init__.py
> @@ -0,0 +1,23 @@
> +# Library containing the TUI framework for Anaconda installer.
> +#
> +# Copyright (C) 2012 Red Hat, Inc.
> +#
> +# This copyrighted material is made available to anyone wishing to use,
> +# modify, copy, or redistribute it subject to the terms and conditions of
> +# the GNU General Public License v.2, or (at your option) any later version.
> +# This program is distributed in the hope that it will be useful, but WITHOUT
> +# ANY WARRANTY expressed or implied, including the implied warranties of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
> +# Public License for more details. You should have received a copy of the
> +# GNU General Public License along with this program; if not, write to the
> +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
> +# source code or documentation are not subject to the GNU General Public
> +# License and may only be used or replicated with the express permission of
> +# Red Hat, Inc.
> +#
> +# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
> +#
> +
> +from base import *
> +from widgets import *
> diff --git a/pyanaconda/ui/tui/simpleline/base.py b/pyanaconda/ui/tui/simpleline/base.py
> new file mode 100644
> index 0000000..7b1562a
> --- /dev/null
> +++ b/pyanaconda/ui/tui/simpleline/base.py
> @@ -0,0 +1,385 @@
> +# Base classes for the Anaconda TUI framework.
> +#
> +# Copyright (C) 2012 Red Hat, Inc.
> +#
> +# This copyrighted material is made available to anyone wishing to use,
> +# modify, copy, or redistribute it subject to the terms and conditions of
> +# the GNU General Public License v.2, or (at your option) any later version.
> +# This program is distributed in the hope that it will be useful, but WITHOUT
> +# ANY WARRANTY expressed or implied, including the implied warranties of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
> +# Public License for more details. You should have received a copy of the
> +# GNU General Public License along with this program; if not, write to the
> +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
> +# source code or documentation are not subject to the GNU General Public
> +# License and may only be used or replicated with the express permission of
> +# Red Hat, Inc.
> +#
> +# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
> +#
> +
> +__all__ = ["ExitMainLoop", "App", "UIScreen", "Widget"]
> +
> +class ExitAllMainLoops(Exception):
> + pass
> +
> +class ExitMainLoop(Exception):
> + pass
> +
> +
> +class App(object):
> + def __init__(self, title, yes_or_no_question = None):
> + self._header = title
> + self._spacer = "\n".join(2*[80*"="])
> + self.quit_question = yes_or_no_question
> +
> + # screen stack contains triplets
> + # UIScreen to show
> + # arguments for it's show method
> + # value indicating whether new mainloop is needed - None = do nothing, True = execute, False = already running, exit when window closes
> + self._screens = []
> +
> + def switch_screen(self, ui, args = None):
> + """Schedules a screen to replace the current one."""
> + oldscr, oldattr, oldloop = self._screens.pop()
> + self._screens.append((ui, args, oldloop))
> + self.redraw()
> +
> + def switch_screen_with_return(self, ui, args = None):
> + """Schedules a screen to show, but keeps the current one in stack to return to, when the new one is closed."""
> + self._screens.append((ui, args, None))
> + self.redraw()
> +
> + def switch_screen_modal(self, ui, args = None):
> + """Starts a new screen right away, so the caller can collect data back. When the new screen is closed, the caller is redisplayed."""
> + self._screens.append((ui, args, True))
> + self.redraw()
> +
> + def schedule_screen(self, ui, args = None):
> + """Add screen to the bottom of the stack."""
> + self._screens.insert(0, (ui, args, None))
> +
> + def close_screen(self, scr = None):
> + oldscr, oldattr, oldloop = self._screens.pop()
> + if scr is not None:
> + assert oldscr == scr
> +
> + # we are in modal window, end it's loop
> + assert oldloop != True # this cannot happen, if we are closing the window, the loop must be running or not there
> + if oldloop == False:
> + raise ExitMainLoop()
> +
> + if self._screens:
> + self.redraw()
> + else:
> + raise ExitMainLoop()
> +
> + def _do_redraw(self):
> + """Draws the current screen and returns True if user input is requested.
> + If modal screen is requested, starts a new loop and initiates redraw after it ends."""
> + if not self._screens:
> + raise ExitMainLoop()
> +
> + screen, args, newloop = self._screens[-1]
> + input_needed = screen.refresh(args)
> + screen.window.show_all()
> +
> + if newloop == True:
> + self._screens.pop()
> + self._screens.append((screen, args, False))
> + self.mainloop()
> + self.redraw()
> + input_needed = False # we have to skip input once, to redisplay the screen first
> +
> + self._redraw = False
> +
> + return input_needed
> +
> + def run(self):
> + try:
> + self.mainloop()
> + except ExitAllMainLoops:
> + pass
> +
> + def mainloop(self):
> + self._redraw = True
> + last_screen = None
> + error_counter = 0
> + while self._screens:
> + try:
> + if self._redraw or last_screen != self._screens[-1]:
> + if not self._do_redraw():
> + continue
> +
> + last_screen = self._screens[-1]
> +
> + c = raw_input(last_screen.prompt())
> + if not self.input(c):
> + error_counter += 1
> +
> + if error_counter >= 5:
> + self.redraw()
> +
> + if self._redraw:
> + error_counter = 0
> + print self._spacer
> + except ExitMainLoop:
> + break
> + except ExitAllMainLoops:
> + raise
> +
> + def input(self, key):
> + """Method called to process unhandled input key presses."""
> + if self._screens:
> + key = self._screens[-1][0].input(key)
> + if key is None:
> + return True
> +
> + if self._screens and (key == 'c'):
> + self.close_screen()
> + return True
> +
> + elif self._screens and (key == 'quit'):
Shouldn't (key == 'q') be here?
> + if self.quit_question:
> + self.switch_screen_modal(self.quit_question(u"Do you really want to quit?"))
> + if self.quit_question.answer:
> + raise ExitAllMainLoops()
> + return True
> +
> + return False
> +
> + def redraw(self):
> + self._redraw = True
> +
> + @property
> + def header(self):
> + return self._header
> +
> + @property
> + def store(self):
> + return self._store
> +
> +
> +class UIScreen(object):
> + title = u"Screen.."
> +
> + def __init__(self, app, data):
> + self._app = app
> + self._data = data
> + self._window = []
> +
> + def refresh(self, args = None):
> + """Method which prepares the screen to self._window. If user input is requested, return True."""
> + self.window = [self.title, u""]
> +
> + @property
> + def window(self):
> + return self
> +
> + def show_all(self):
> + for w in self._window:
> + if hasattr(w, "render"):
> + w.render(app.width)
> + print unicode(w)
> +
> + show = show_all
> +
> + def hide(self):
> + pass
> +
> + def input(self, key):
> + """Method called to process input. If the input is not handled here, return it."""
> + return key
> +
> + def prompt(self):
> + return u"\tPlease make your choice from above ['q' to quit]: "
> +
> + @property
> + def data(self):
> + return self._data
> +
> + @property
> + def app(self):
> + return self._app
> +
> + def close(self):
> + self.app.close_screen(self)
> +
> +class Widget(object):
> + def __init__(self, max_width = None, default = None):
> + """Initializes base Widgets buffer.
> +
> + @param max_width server as a hint about screen size to write method with default arguments
> + @type max_width int
> +
> + @param default string containing the default content to fill the buffer with
> + @type default string
> + """
> +
> + self._buffer = []
> + if default:
> + self._buffer = [[c for c in l] for l in default.split("\n")]
> + self._max_width = max_width
> + self._cursor = (0, 0) # row, col
> +
> + @property
> + def height(self):
> + return len(self._buffer)
> +
> + @property
> + def width(self):
> + return reduce(lambda acc,l: max(acc, len(l)), self._buffer, 0)
> +
> + def clear(self):
> + """Clears this widgets buffer and resets cursor."""
> + self._buffer = list()
> + self._cursor = (0, 0)
> +
> + @property
> + def content(self):
> + """This has to return list (rows) of lists (columns) with one character elements."""
> + return self._buffer
> +
> + def render(self, width = None):
> + """This method has to redraw the widget's self._buffer.
> +
> + @param width the width of buffer requested by the caller
> + @type width int
> +
> + This method will commonly call render of child widgets and then draw and write
> + methods to copy their contents to self._buffer
> + """
> + self.clear()
> +
> + def __unicode__(self):
> + return u"\n".join([u"".join(l) for l in self._buffer])
> +
> + def setxy(self, row, col):
> + """Sets cursor position."""
> + self._cursor = (row, col)
> +
> + @property
> + def cursor(self):
> + return self._cursor
> +
> + def setend(self):
> + """Sets the cursor to first column in new line at the end."""
> + self._cursor = (self.height, 0)
> +
> + def draw(self, w, row = None, col = None, block = False):
> + """This method copies w widget's content to this widget's buffer at row, col position.
> +
> + @param w widget to take content from
> + @type w class Widget
> +
> + @param row row number to start at (default is at the cursor position)
> + @type row int
> +
> + @param col column number to start at (default is at the cursor position)
> + @type col int
> +
> + @param block when printing newline, start at column col (True) or at column 0 (False)
> + @type boolean
> + """
> +
> +
> + if row is None:
> + row = self._cursor[0]
> +
> + if col is None:
> + col = self._cursor[1]
> +
> + # fill up rows
> + if self.height < row + w.height:
> + for i in range(row + w.height - self.height):
> + self._buffer.append(list())
> +
> + # append columns
> + for l in range(row, row + w.height):
> + l_len = len(self._buffer[l])
> + w_len = len(w.content[l - row])
> + if l_len < col + w_len:
> + self._buffer[l] += ((col + w_len - l_len) * list(u" "))
> + self._buffer[l][col:col + w_len] = w.content[l - row][:]
> +
> + if block:
> + self._cursor = (row + w.height, col)
> + else:
> + self._cursor = (row + w.height, 0)
> +
> + def write(self, text, row = None, col = None, width = None, block = False):
> + """This method emulates typing machine writing to this widget's buffer.
> +
> + @param text text to type
> + @type text unicode
> +
> + @param row row number to start at (default is at the cursor position)
> + @type row int
> +
> + @param col column number to start at (default is at the cursor position)
> + @type col int
> +
> + @param width wrap at "col" + "width" column (default is at self._max_width)
> + @type width int
> +
> + @param block when printing newline, start at column col (True) or at column 0 (False)
> + @type boolean
> + """
> +
> + if row is None:
> + row = self._cursor[0]
> +
> + if col is None:
> + col = self._cursor[1]
> +
> + if width is None:
> + width = self._max_width - col
> +
> + x = row
> + y = col
> +
> + # emulate typing machine
> + for c in text:
> + # if the line is not in buffer, create it
> + if x >= len(self._buffer):
> + for i in range(x - len(self._buffer) + 1):
> + self._buffer.append(list())
> +
> + # if the line's length is not enough, fill it with spaces
> + if y >= len(self._buffer[x]):
> + self._buffer[x] += ((y - len(self._buffer[x]) + 1) * list(u" "))
> +
> + # process newline
> + if c == "\n":
> + x += 1
> + if block:
> + y = col
> + else:
> + y = 0
> + continue
> +
> + # "type" character
> + self._buffer[x][y] = c
> +
> + # shift to the next char
> + y += 1
> + if y >= col + width:
> + x += 1
> + if block:
> + y = col
> + else:
> + y = 0
> +
> + self._cursor = (x, y)
> +
> +class HelloWorld(UIScreen):
> + def show(self, args = None):
> + print """Hello World\nquit by typing 'quit'"""
> + return True
> +
> +if __name__ == "__main__":
> + a = App("Hello World")
> + s = HelloWorld(a, None)
> + a.schedule_screen(s)
> + a.run()
> diff --git a/pyanaconda/ui/tui/simpleline/widgets.py b/pyanaconda/ui/tui/simpleline/widgets.py
> new file mode 100644
> index 0000000..2d79e3f
> --- /dev/null
> +++ b/pyanaconda/ui/tui/simpleline/widgets.py
> @@ -0,0 +1,138 @@
> +# encoding: utf-8
> +#
> +# Widgets for Anaconda TUI.
> +#
> +# Copyright (C) 2012 Red Hat, Inc.
> +#
> +# This copyrighted material is made available to anyone wishing to use,
> +# modify, copy, or redistribute it subject to the terms and conditions of
> +# the GNU General Public License v.2, or (at your option) any later version.
> +# This program is distributed in the hope that it will be useful, but WITHOUT
> +# ANY WARRANTY expressed or implied, including the implied warranties of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
> +# Public License for more details. You should have received a copy of the
> +# GNU General Public License along with this program; if not, write to the
> +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
> +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
> +# source code or documentation are not subject to the GNU General Public
> +# License and may only be used or replicated with the express permission of
> +# Red Hat, Inc.
> +#
> +# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
> +#
> +
> +__all__ = ["TextWidget", "ColumnWidget", "CheckboxWidget", "CenterWidget"]
> +
> +import base
> +
> +class TextWidget(base.Widget):
> + def __init__(self, text):
> + base.Widget.__init__(self)
> + self._text = text
> +
> + def render(self, width):
> + self.clear()
> + self.write(self._text, width = width)
> +
> +class CenterWidget(base.Widget):
> + def __init__(self, w):
> + base.Widget.__init__(self)
> + self._w = w
> +
> + def render(self, width):
> + self.clear()
> + self._w.render(width)
> + self.draw(self._w, col = (width - self._w.width) / 2)
> +
> +class ColumnWidget(base.Widget):
> + def __init__(self, columns, spacing = 0):
> + """Create text columns
> +
> + @param columns list containing (column width, [list of widgets to put into this column])
> + @type columns [(int, [...]), ...]
> +
> + @param spacing number of spaces to use between columns
> + @type int
> + """
> +
> + base.Widget.__init__(self)
> + self._spacing = spacing
> + self._columns = columns
> +
> + def render(self, width):
> + self.clear()
> +
> + x = 0
> + for col_width,col in self._columns:
> + self.setxy(0, x)
> +
> + if col_width is None:
> + col_max_width = width - self.cursor[1]
> + col_width = 0
> + else:
> + col_max_width = col_width
> +
> + for item in col:
> + item.render(col_max_width)
> + self.draw(item, block = True)
> +
> + x = max((x + col_width), self.width) + self._spacing
> +
> +class CheckboxWidget(base.Widget):
> + def __init__(self, key = None, title = None, text = None, completed = None):
> + base.Widget.__init__(self)
> + self._key = key
> + self._title = title
> + self._text = text
> + self._completed = completed
> +
> + def render(self, width):
> + self.clear()
> +
> + if self.completed:
> + checkchar = "x"
> + else:
> + checkchar = " "
> +
> + checkbox = TextWidget("[%s]" % checkchar)
> +
> + data = []
> +
> + if self.title:
> + data.append(TextWidget(self.title))
> +
> + if self.text:
> + data.append(TextWidget("(%s)" % self.text))
> +
> + cols = ColumnWidget([(3, [checkbox]), (width - 4, data)], 1)
> + cols.render(width)
> + self.draw(cols)
> +
> + @property
> + def title(self):
> + return self._title
> +
> + @property
> + def completed(self):
> + return self._completed
> +
> + @property
> + def text(self):
> + return self._text
> +
> +if __name__ == "__main__":
> + t1 = TextWidget(u"Můj krásný dlouhý text")
> + t2 = TextWidget(u"Test")
> + t3 = TextWidget(u"Test 2")
> + t4 = TextWidget(u"Krásný dlouhý text podruhé")
> + t5 = TextWidget(u"Test 3")
> +
> + c = ColumnWidget([(15, [t1, t2, t3]), (10, [t4, t5])], spacing = 1)
> + c.render(80)
> + print unicode(c)
> +
> + print 80*"-"
> +
> + c = ColumnWidget([(20, [t1, t2, t3]), (25, [t4, t5]), (15, [t1, t2, t3])], spacing = 3)
> + c.render(80)
> + print unicode(c)
--
Vratislav Podzimek
Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic
More information about the anaconda-patches
mailing list