[console] add mainframe that mimics the emacs/vim interface (with vim keystrokes)

Note: mainframe content is just for trying the mainframe and will be improved.

You can try the mainframe with:

python mainframe.py # use ":q<enter>" to quit
authorAlain Leufroy <alain.leufroy@logilab.fr>
changeset30f7e8b7efd1
branchdefault
phasepublic
hiddenno
parent revision#83e1f6d0ffa3 Propagate errors from mercurial.hg.repository() (closes: #73678)
child revision#8d0be4a1af20 [console] allow multiple body (equivalent to buffers in vim/emacs)
files modified by this revision
hgviewlib/curses/__init__.py
hgviewlib/curses/exceptions.py
hgviewlib/curses/mainframe.py
# HG changeset patch
# User Alain Leufroy <alain.leufroy@logilab.fr>
# Date 1309540453 -7200
# Fri Jul 01 19:14:13 2011 +0200
# Node ID 30f7e8b7efd159c24466a2e6201daf07aaee94e4
# Parent 83e1f6d0ffa3be8f15eaed35eac9a83e9e7a09b5
[console] add mainframe that mimics the emacs/vim interface (with vim keystrokes)

Note: mainframe content is just for trying the mainframe and will be improved.

You can try the mainframe with::

python mainframe.py # use ":q<enter>" to quit

diff --git a/hgviewlib/curses/__init__.py b/hgviewlib/curses/__init__.py
@@ -0,0 +1,20 @@
1 +# -*- coding: utf-8 -*-
2 +# Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
3 +# http://www.logilab.fr/ -- mailto:contact@logilab.fr
4 +#
5 +# This program is free software; you can redistribute it and/or modify it under
6 +# the terms of the GNU General Public License as published by the Free Software
7 +# Foundation; either version 2 of the License, or (at your option) any later
8 +# version.
9 +#
10 +# This program is distributed in the hope that it will be useful, but WITHOUT
11 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 +#
14 +# You should have received a copy of the GNU General Public License along with
15 +# this program; if not, write to the Free Software Foundation, Inc.,
16 +# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17 +
18 +"""
19 +console interface for hgview.
20 +"""
diff --git a/hgviewlib/curses/exceptions.py b/hgviewlib/curses/exceptions.py
@@ -0,0 +1,28 @@
21 +#! /usr/bin/env python
22 +# -*- coding: utf-8 -*-
23 +# Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
24 +# http://www.logilab.fr/ -- mailto:contact@logilab.fr
25 +#
26 +# This program is free software; you can redistribute it and/or modify it under
27 +# the terms of the GNU General Public License as published by the Free Software
28 +# Foundation; either version 2 of the License, or (at your option) any later
29 +# version.
30 +#
31 +# This program is distributed in the hope that it will be useful, but WITHOUT
32 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
33 +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
34 +#
35 +# You should have received a copy of the GNU General Public License along with
36 +# this program; if not, write to the Free Software Foundation, Inc.,
37 +# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
38 +# -*- coding: iso-8859-1 -*-
39 +"""
40 +Exceptions classes used by hgview curses
41 +"""
42 +
43 +class HgviewCursesException(Exception):
44 +    """Base class for all hgview curses exception """
45 +
46 +class CommandError(ValueError, HgviewCursesException):
47 +    """Error that occures while calling a command"""
48 +
diff --git a/hgviewlib/curses/mainframe.py b/hgviewlib/curses/mainframe.py
@@ -0,0 +1,220 @@
49 +#! /usr/bin/env python
50 +# -*- coding: utf-8 -*-
51 +# Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
52 +# http://www.logilab.fr/ -- mailto:contact@logilab.fr
53 +#
54 +# This program is free software; you can redistribute it and/or modify it under
55 +# the terms of the GNU General Public License as published by the Free Software
56 +# Foundation; either version 2 of the License, or (at your option) any later
57 +# version.
58 +#
59 +# This program is distributed in the hope that it will be useful, but WITHOUT
60 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
61 +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
62 +#
63 +# You should have received a copy of the GNU General Public License along with
64 +# this program; if not, write to the Free Software Foundation, Inc.,
65 +# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
66 +
67 +"""
68 +Module that contains the curses main frame, using urwid, that mimics the 
69 +vim/emacs interface.
70 +
71 ++------------------------------------------------+
72 +|                                                |
73 +|                                                |
74 +| body                                           |
75 +|                                                |
76 +|                                                |
77 ++------------------------------------------------+
78 +| banner                                         |
79 ++------------------------------------------------+
80 +| footer                                         |
81 ++------------------------------------------------+
82 +
83 +* *body* display the main contant
84 +* *banner* display some short information on the current program state
85 +* *footer* display program logs and it is used as input area
86 +
87 +"""
88 +
89 +import urwid
90 +import urwid.raw_display
91 +from urwid import AttrWrap as W
92 +from urwid.decoration import Filler, CanvasCombine
93 +
94 +from hgviewlib.curses.exceptions import CommandError
95 +
96 +class CommandsList(object):
97 +    "basic commands list"
98 +    @staticmethod
99 +    def quit(mainframe):
100 +        "Quit the program"
101 +        raise urwid.ExitMainLoop()
102 +    q = quit
103 +
104 +class MainFrame(urwid.Frame):
105 +    """Main console frame that mimic the vim interface.
106 +    """
107 +    def __init__(self, body, title='', *args, **kwargs):
108 +        header = W(urwid.Text(title), 'banner')
109 +        footer = Footer(self)
110 +        body.mainframe = body.parent = self
111 +        self.__super.__init__(body, header=header, footer=footer,
112 +                              *args, **kwargs)
113 +
114 +    def keypress(self, size, key):
115 +        "allow subclasses to intercept keystrokes"
116 +        key = self.__super.keypress(size, key)
117 +        if key:
118 +            self.unhandled_key(size, key)
119 +        return key
120 +
121 +    def unhandled_key(self, size, key):
122 +        """Override this method to intercept keystrokes in subclasses.
123 +        Default behavior: run command from ':xxx'
124 +        """
125 +        if key == ':':
126 +            self.set_focus('footer')
127 +            self.footer.unhandled_key(size, ':')
128 +        elif key == 'enter':
129 +            self.set_focus('body')
130 +        elif key == 'esc':
131 +            self.set_focus('body')
132 +
133 +    def call_command(self):
134 +        '''Call the command that corresponds to the string given in the edit area
135 +        '''
136 +        cmd = self.footer.get_edit_text().strip()
137 +        if not cmd:
138 +            self.set('default', '', '')
139 +            return
140 +        try:
141 +            cmds = cmd.split()
142 +            name = cmds[0]
143 +            args = cmds[1:]
144 +            getattr(self.body.commands, name)(self, *args)
145 +        except urwid.ExitMainLoop: # exit, so do not catch this
146 +            raise
147 +        except AttributeError:
148 +            self.footer.set('warn', 'unknown command: ', name)
149 +        except Exception, err:
150 +            self.footer.set('warn', err.__class__.__name__ +': ', str(err))
151 +
152 +    # better name for header as we use it as banner
153 +    banner = property(urwid.Frame.get_header, urwid.Frame.set_header, None,
154 +                      'banner widget')
155 +
156 +    def render(self, size, focus=False):
157 +        """Render frame and return it."""
158 +        # Copy the original method code to put the header at bottom :?
159 +        # (vim-like banner)
160 +        maxcol, maxrow = size
161 +        (htrim, ftrim),(hrows, frows) = self.frame_top_bottom(
162 +                                                        (maxcol, maxrow), focus)
163 +
164 +        combinelist = []
165 +
166 +        if ftrim+htrim < maxrow:
167 +            body = self.body.render((maxcol, maxrow-ftrim-htrim),
168 +                                    focus and self.focus_part == 'body')
169 +            combinelist.append((body, 'body', self.focus_part == 'body'))
170 +
171 +        bann = None
172 +        if htrim and htrim < hrows:
173 +            bann = Filler(self.banner, 'bottom').render(
174 +                    (maxcol, htrim), focus and self.focus_part == 'banner')
175 +        elif htrim:
176 +            bann = self.banner.render(
177 +                    (maxcol,), focus and self.focus_part == 'banner')
178 +            assert bann.rows() == hrows, "rows, render mismatch"
179 +        if bann:
180 +            combinelist.append((bann, 'banner', self.focus_part == 'banner'))
181 +
182 +        foot = None
183 +        if ftrim and ftrim < frows:
184 +            foot = Filler(self.footer, 'bottom').render((maxcol, ftrim),
185 +                focus and self.focus_part == 'footer')
186 +        elif ftrim:
187 +            foot = self.footer.render(
188 +                    (maxcol,), focus and self.focus_part == 'footer')
189 +            assert foot.rows() == frows, "rows, render mismatch"
190 +        if foot:
191 +            combinelist.append((foot, 'footer', self.focus_part == 'footer'))
192 +
193 +        return CanvasCombine(combinelist)
194 +
195 +class Footer(urwid.AttrWrap):
196 +    """Footer widget used to display message and for inputs.
197 +    """
198 +    def __init__(self, mainframe, *args, **kwargs):
199 +        self.mainframe = mainframe
200 +        self.__super.__init__(
201 +            urwid.Edit('type ":help<Enter>" for information'),
202 +            'footer_style', *args, **kwargs)
203 +
204 +    def keypress(self, size, key):
205 +        "allow subclasses to intercept keystrokes"
206 +        key = self.__super.keypress(size, key)
207 +        if key:
208 +            self.unhandled_key(size, key)
209 +        return key
210 +
211 +    def set(self, style=None, caption=None, edit=None):
212 +        '''Set the footer content.
213 +
214 +        :param style: a string that corresponds to a palette entry name
215 +        :param caption: a string to display in caption
216 +        :param edit: a string to display in the edit area
217 +        '''
218 +        if style is not None:
219 +            self.set_attr(style)
220 +        if caption is not None:
221 +            self.set_caption(caption)
222 +        if edit is not None:
223 +            self.set_edit_text(edit)
224 +
225 +    def unhandled_key(self, size, key):
226 +        """Overwrite this method to intercept keystrokes in subclasses.
227 +        Default behavior: run command from ':xxx'
228 +        """
229 +        if key == ':':
230 +            self.set('default', ':', '')
231 +        if key == 'enter':
232 +            self.set('default')
233 +            self.mainframe.call_command()
234 +        elif key == 'esc':
235 +            self.set('default', '', '')
236 +
237 +if __name__ == '__main__':
238 +
239 +    from urwid import AttrWrap, Padding, Text, SimpleListWalker, ListBox
240 +    from hgviewlib.util import find_repository
241 +    from mercurial import hg, ui
242 +    PALETTE = [
243 +        ('default','default','default', 'bold'),
244 +        ('warn','white','dark red', 'bold'),
245 +        ('body','white','black', 'standout'),
246 +        ('banner','black','light gray', 'bold'),
247 +        ('focus','black','dark green', 'bold'),
248 +        ('entry', 'dark blue', 'default', 'bold')
249 +        ]
250 +
251 +    class Body(AttrWrap):
252 +        commands = CommandsList
253 +        def __init__(self, *args, **kwargs):
254 +            # XXX: just for trying, shall be removed
255 +            repo = hg.repository(ui.ui(), find_repository('.'))
256 +            def description(rev):
257 +                desc = repo.changectx(rev).description().splitlines()[0]
258 +                return '%i> %s' % (rev, desc)
259 +            lines = [AttrWrap(Padding(Text(('entry', description(rev))),
260 +                                      ('fixed left', 2), ('fixed right', 2),20),
261 +                              'default','focus')
262 +                      for rev in repo.changelog]
263 +            self.__super.__init__(ListBox(SimpleListWalker(lines)), 'body',
264 +                                  *args, **kwargs)
265 +    frame = MainFrame(Body(), 'Type ":q<enter>" to quit')
266 +    screen = urwid.raw_display.Screen()
267 +    urwid.MainLoop(frame, PALETTE, screen).run()
268 +