[PATCH 01/13] Text based UI framework core

Martin Sivak msivak at redhat.com
Wed Aug 8 14:25:22 UTC 2012


Hi,

prompt is a method, because it can handle the prompt differently if you need it to. Look at the PasswordSpoke in one of the later patches.

Martin

----- Original Message -----
> 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'):
> > +            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]: "
> Any reason why this is not a property?
> 
> > +
> > +    @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
> > +           """
> "@type block boolean" should be here (same issues in some other
> docstrings).
> > +
> > +        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
> What are self._w and w here? I'd much prefer some longer and more
> descriptive identifiers (applies for the whole patch).
> 
> > +
> > +    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)
> 
> 
> _______________________________________________
> anaconda-patches mailing list
> anaconda-patches at lists.fedorahosted.org
> https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
> 


More information about the anaconda-patches mailing list