Footer Edit with Urwid, IRC-Client-like input

November 23, 2011

We start with the code from Selectable List with Urwid for Python, check before the Signals with Urwid for Python for a small involvement of the code, for handling signals.

So far, we have a selectable list containing two items each (title and description). We can go through it with up and down key arrows, and select one of them with the enter key. We use urwid.Frame witch give us an useful layout divided in three parts, header, body and footer. In the last post, we used the header to display the selected item, the body to display the list, and the footer wasn’t used at all.

Now, what we want is to use the footer for an Edit widget, exactly like ncurses IRC-client such as IRSSI or weechat. When we will press the e key (for edit) an input widget will appear, taking place at the footer, once validate, with enter, or abort with escape the output will replace the description of the current selected item.

In the following screenshot, the list was generated with random description. the 4th item was replaced with this editing process, and we’re about to replace the 5th item as well. It may seem not a big deal, but it still a good exercise to gain practice with urwid.

Urwid Edit

Let’s talk about the code. The full code is at the end of the post.

self.description = urwid.Text(description)
self.item = [
    ('fixed', 15, urwid.Padding(urwid.AttrWrap( self.title, 'body', 'focus'), left=2)),
    urwid.AttrWrap(self.description, 'body', 'focus'),
]
class CustomEdit(urwid.Edit):

    __metaclass__ = urwid.signals.MetaSignals
    signals = ['done']

    def keypress(self, size, key):
        if key == 'enter':
            urwid.emit_signal(self, 'done', self.get_edit_text())
            return
        elif key == 'esc':
            urwid.emit_signal(self, 'done', None)
            return

        urwid.Edit.keypress(self, size, key)

The edit method is directly call when the e key is pressed. It only create a CustomEdit object, that would be set to the footer, but we need to specified to the frame (view) that the footer have the focus now, otherwise it would not be editable. At last, we connect the signal, to treat the content once the edit is done.

    def edit(self):
        self.foot = CustomEdit(' >> ')
        self.view.set_footer(self.foot)
        self.view.set_focus('footer')
        urwid.connect_signal(self.foot, 'done', self.edit_done)

Once the edit is done, the edit_done function is called as a callback, with the content. Then we modify the content of the description in the list. And set back to None the footer, as we don’t want anything display anymore.

    def edit_done(self, content):
        self.view.set_focus('body')
        urwid.disconnect_signal(self, self.foot, 'done', self.edit_done)
        if content:
            focus = self.listbox.get_focus()[0]
            focus.description.set_text(content)
        self.view.set_footer(None)

That’s it, in this example this code is used to manage a list, but it could be used for a large set of applications.

Here the full code:

import urwid
import random
import urwid.html_fragment

class ItemWidget (urwid.WidgetWrap):

    def __init__ (self, id, description):
        self.id = id
        self.title = urwid.Text('item %s' % str(id))
        self.description = urwid.Text(description)
        self.item = [
            ('fixed', 15, urwid.Padding(urwid.AttrWrap( self.title, 'body', 'focus'), left=2)),
            urwid.AttrWrap(self.description, 'body', 'focus'),
        ]
        self.content = 'item %s: %s...' % (str(id), description[:25])
        w = urwid.Columns(self.item)
        self.__super.__init__(w)

    def selectable (self):
        return True

    def keypress(self, size, key):
        return key

class CustomEdit(urwid.Edit):

    __metaclass__ = urwid.signals.MetaSignals
    signals = ['done']

    def keypress(self, size, key):
        if key == 'enter':
            urwid.emit_signal(self, 'done', self.get_edit_text())
            return
        elif key == 'esc':
            urwid.emit_signal(self, 'done', None)
            return

        urwid.Edit.keypress(self, size, key)

class MyApp(object):

    def __init__(self):

        palette = [
            ('body','dark blue', '', 'standout'),
            ('focus','dark red', '', 'standout'),
            ('head','light red', 'black'),
            ]

        lorem = [
            'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
            'Sed sollicitudin, nulla id viverra pulvinar.',
            'Cras a magna sit amet felis fringilla lobortis.',
        ]


        items = []
        for i in range(100):
            item = ItemWidget(i, random.choice(lorem))
            items.append(item)

        header = urwid.AttrMap(urwid.Text('selected:'), 'head')
        walker = urwid.SimpleListWalker(items)
        self.listbox = urwid.ListBox(walker)
        self.view = urwid.Frame(urwid.AttrWrap(self.listbox, 'body'), header=header)

        loop = urwid.MainLoop(self.view, palette, unhandled_input=self.keystroke)
        urwid.connect_signal(walker, 'modified', self.update)
        loop.run()
    
    def update(self):
        focus = self.listbox.get_focus()[0].content
        self.view.set_header(urwid.AttrWrap(urwid.Text('selected: %s' % str(focus)), 'head'))

    def keystroke (self, input):
        if input in ('q', 'Q'):
            raise urwid.ExitMainLoop()

        if input is 'enter':
            focus = self.listbox.get_focus()[0].content
            self.view.set_header(urwid.AttrWrap(urwid.Text('selected: %s' % str(focus)), 'head'))

        if input == 'e':
            self.edit()

    def edit(self):
        self.foot = CustomEdit(' >> ')
        self.view.set_footer(self.foot)
        self.view.set_focus('footer')
        urwid.connect_signal(self.foot, 'done', self.edit_done)

    def edit_done(self, content):
        self.view.set_focus('body')
        urwid.disconnect_signal(self, self.foot, 'done', self.edit_done)
        if content:
            focus = self.listbox.get_focus()[0]
            focus.description.set_text(content)
        self.view.set_footer(None)

if __name__ == '__main__':
    MyApp()

Hope it can help someone.

Tweet

Related-ish Posts

About

I'm Nicolas Paris, aka Nic0, I like to share about programming and Linux tricks. Follow me on Twitter, where the content is pretty much like here, mainly programming stuff. Or visite my website