├── .gitignore ├── README.txt ├── USAGE.txt ├── build.sh ├── setup.py ├── snackwich.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt ├── entry_points.txt ├── not-zip-safe ├── requires.txt └── top_level.txt ├── snackwich ├── __init__.py ├── buttons.py ├── example │ ├── config.py │ └── test.py ├── exceptions.py ├── log_config.py ├── main.py ├── patch.py └── ui_functions.py └── ubuntu.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /dist 3 | /build 4 | snackwich.log 5 | example.py 6 | example.snack.py 7 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Snackwich is an adaptation of the Snack console UI framework. Snack is, itself, 2 | a port of Newt to Python. Newt is the console UI framework used in Redhat 3 | console applications. As there is no readily available console menu system 4 | for Curses under Python, this is the best option out there, and it works like a 5 | dream. 6 | 7 | The only requirement is that the Snack framework is installed. Under Ubuntu, 8 | this is the "python-newt" package. It includes "snack.py", along with the 9 | requisite dynamic library. 10 | 11 | See the "test.py" script in the example/ directory. 12 | 13 | ┌─────────────┤ Title 1 ├─────────────┐ 14 | │ │ 15 | │ Text content 1 │ 16 | │ │ 17 | │ Option 1 ↑ │ 18 | │ Option 2 ▒ │ 19 | │ Option 3 │ 20 | │ Option 4 ▒ │ 21 | │ Option 5 ↓ │ 22 | │ │ 23 | │ ┌────┐ ┌────────┐ │ 24 | │ │ Ok │ │ Cancel │ │ 25 | │ └────┘ └────────┘ │ 26 | │ │ 27 | │ │ 28 | └─────────────────────────────────────┘ 29 | 30 | Snackwich allows you to express your panels declaratively (as a static list-of- 31 | dictionaries), a list of tuples describing callbacks, or a combination of both. 32 | 33 | The config also allows each panel to have a posthook callback to check which 34 | values/choices were made, which button was pressed, or whether ESC was used to 35 | escape the panel. See the example. 36 | 37 | Although the configuration allows you to define the succession of which 38 | panels lead to which, the posthook callback can raise certain exceptions to 39 | jump to different panels, redraw the current panel, quit, etc.. (see 40 | snackwich.exceptions). 41 | 42 | The previous two features can be combined to implement validation: check the 43 | values, navigate to an error panel, and navigate back (the error panel must 44 | have a '_next' describing the name of the original dialog). 45 | 46 | The result of the example menu is the following (the names of the panels are 47 | 'window1', 'window2', and 'window3'): 48 | 49 | {'validation_error': {'button': 'ok', 50 | 'grid': , 51 | 'is_esc': False}, 52 | 'window1': {'button': None, 53 | 'grid': , 54 | 'is_esc': False, 55 | 'selected': 'option5'}, 56 | 'window2': {'button': 'ok', 57 | 'grid': , 58 | 'is_esc': False, 59 | 'values': ('test value', '', '')}, 60 | 'window3': {'button': None, 61 | 'grid': , 62 | 'is_esc': False, 63 | 'selected': 'option1'}} 64 | 65 | -------------------------------------------------------------------------------- /USAGE.txt: -------------------------------------------------------------------------------- 1 | 2 | This purpose of Snackwich is to provide a simple, configuration-based frontend. 3 | Therefore, the coding component is simple. For example, the following executes 4 | and collects the results from the sample menu: 5 | 6 | from snackwich.main import Snackwich 7 | 8 | panels = Snackwich('example.snack.py') 9 | 10 | result = panels.execute() 11 | 12 | The examples should provide a working example of most of what is required by 13 | the user. Keep in mind the following, as you're working with Snackwich. 14 | 15 | > This is an extension of traditional Snack. Instead of directly executing 16 | Python code to invoke Snack, a configuration is given that essentially 17 | identifies the functions (widgets) to be created, and the arguments to use. 18 | 19 | > Arguments with a "_" prefix will not be sent to Snack. These arguments are 20 | "meta-attributes" processed by Snackwich. 21 | 22 | > You may look in snack.py or the source for _snack.so to investigate the 23 | mechanics of how things work. There is very limited documentation for the 24 | project, most being simply the examples that acccompany it. 25 | 26 | > The configuration file is a list of "expressions" (also referred to "panels" 27 | in the Snackwich code). The configuration file can be any Python code, so 28 | long as a list of expressions is assigned to a variable named "config". 29 | 30 | > The simplest use case is to provide a configuration that is simply a list of 31 | expressions, where each expression is a dictionary, and all expressions are 32 | used to render panels in the order that they appear. All expressions must 33 | have a "_name" component describing an arbitrary name to be assigned to that 34 | expression/panel for routing purposes. 35 | 36 | > We have provided a number of modifications in order to do our best to meet 37 | most needs of a menu system. 38 | 39 | > Instead of a dictionary, an expression can be a tuple, where the first item 40 | is the name, and the second is a callback. The callback receives 41 | 42 | (key, results, context) 43 | 44 | key: Name of panel defined alongside callback (the first item of the 45 | tuple) 46 | results: Current set of results for previously presented panels. 47 | context: Arbitrary variables set in previous callbacks. Allows the 48 | callbacks to retain state, if necessary. 49 | 50 | The callback should return a dictionary identical to what would have been 51 | provided if a callback had not been used. 52 | 53 | > A callback may raise GotoPanelException to immediately go to another panel. 54 | > A callback may raise BreakSuccessionException to exit the menu system and 55 | return the collected results. 56 | > A "timer_ms" attribute may be set on widgets to automatically return from 57 | a panel after a certain amount of time. 58 | > A "_next" attribute may be set on widgets to set the next panel that will 59 | be displayed. 60 | 61 | By using the the "timer_ms" and "_next" attributes along with the context 62 | dictionary, you can show progress. 63 | 64 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Make a source distribution. We require this as pip doesn't support egg files, 4 | # and this produces a standard archive. 5 | python setup.py sdist 6 | 7 | if [ "$?" -ne 0 ]; then 8 | echo "Build failed." 9 | exit 1 10 | fi 11 | 12 | python setup.py sdist upload 13 | 14 | if [ "$?" -ne 0 ]; then 15 | echo "Upload failed." 16 | exit 1 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from setuptools import setup, find_packages 4 | import sys, os 5 | 6 | def pre_install(): 7 | print("Verifying Snack availability.") 8 | # TODO: Check for Newt, too. 9 | try: 10 | import snack 11 | except: 12 | print("Could not find snack. Please install the Newt or Snack " 13 | "packages before installing Snackwich.") 14 | return False 15 | 16 | return True 17 | 18 | if not pre_install(): 19 | sys.exit(1) 20 | 21 | version = '1.3.18' 22 | 23 | setup(name='snackwich', 24 | version=version, 25 | description="Configuration-based console UI forms.", 26 | long_description="Configuration-based Snack/Newt adaptation for easy and attractive console UI forms.", 27 | classifiers=['Development Status :: 5 - Production/Stable', 28 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 29 | 'Natural Language :: English', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3.0', 32 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | 'Environment :: Console :: Newt' 34 | ], 35 | keywords='console ui newt snack forms', 36 | author='Dustin Oprea', 37 | author_email='myselfasunder@gmail.com', 38 | url='https://github.com/dsoprea/Snackwich', 39 | license='GPL2', 40 | packages=['snackwich'], 41 | include_package_data=True, 42 | zip_safe=False, 43 | install_requires=[ 44 | ], 45 | entry_points=""" 46 | # -*- Entry points: -*- 47 | """, 48 | ) 49 | 50 | -------------------------------------------------------------------------------- /snackwich.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: snackwich 3 | Version: 1.3.18 4 | Summary: Configuration-based console UI forms. 5 | Home-page: https://github.com/dsoprea/Snackwich 6 | Author: Dustin Oprea 7 | Author-email: myselfasunder@gmail.com 8 | License: New BSD 9 | Description: Configuration-based Snack/Newt adaptation for easy and attractive console UI forms. 10 | Keywords: console ui newt snack forms 11 | Platform: UNKNOWN 12 | Classifier: Development Status :: 5 - Production/Stable 13 | Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2) 14 | Classifier: Natural Language :: English 15 | Classifier: Programming Language :: Python :: 2.7 16 | Classifier: Programming Language :: Python :: 3.0 17 | Classifier: Topic :: Software Development :: Libraries :: Python Modules 18 | Classifier: Environment :: Console :: Newt 19 | -------------------------------------------------------------------------------- /snackwich.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | README.txt 2 | setup.py 3 | snackwich/__init__.py 4 | snackwich/buttons.py 5 | snackwich/exceptions.py 6 | snackwich/log_config.py 7 | snackwich/main.py 8 | snackwich/patch.py 9 | snackwich/ui_functions.py 10 | snackwich.egg-info/PKG-INFO 11 | snackwich.egg-info/SOURCES.txt 12 | snackwich.egg-info/dependency_links.txt 13 | snackwich.egg-info/entry_points.txt 14 | snackwich.egg-info/not-zip-safe 15 | snackwich.egg-info/requires.txt 16 | snackwich.egg-info/top_level.txt -------------------------------------------------------------------------------- /snackwich.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /snackwich.egg-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | 2 | # -*- Entry points: -*- 3 | -------------------------------------------------------------------------------- /snackwich.egg-info/not-zip-safe: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /snackwich.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | setuptools -------------------------------------------------------------------------------- /snackwich.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | snackwich 2 | -------------------------------------------------------------------------------- /snackwich/__init__.py: -------------------------------------------------------------------------------- 1 | from snackwich import log_config 2 | 3 | -------------------------------------------------------------------------------- /snackwich/buttons.py: -------------------------------------------------------------------------------- 1 | BTN_OK = ('Ok', 'ok') 2 | BTN_CANCEL = ('Cancel', 'cancel') 3 | 4 | -------------------------------------------------------------------------------- /snackwich/example/config.py: -------------------------------------------------------------------------------- 1 | from snackwich.buttons import BTN_OK, BTN_CANCEL 2 | from snackwich.exceptions import QuitException, GotoPanelException 3 | 4 | def get_window3(sw, key, context, screen): 5 | return { '_widget': 'list', 6 | 'title': 'Title 3', 7 | 'text': 'Text content 3', 8 | 'items': [ ('Option 1', 'option1') 9 | ], 10 | 'default': 0, 11 | 'scroll': 0, 12 | 'height': 5, 13 | 'timer_ms': 1000 14 | } 15 | 16 | def window2_post_cb(sw, key, result, expression, screen): 17 | if result['button'] == BTN_CANCEL[1] or result['is_esc']: 18 | raise QuitException() 19 | 20 | fields = dict(zip(('prompt1', 'prompt2', 'prompt3'), 21 | result['values'])) 22 | 23 | if fields['prompt1'].strip() == '': 24 | raise GotoPanelException('validation_error') 25 | 26 | # Required in order to Snackwich to find the actual configuration. 27 | config = [{ 28 | '_name': 'window1', 29 | '_widget': 'list', 30 | 'title': 'Title 1', 31 | 'text': 'Text content 1', 32 | 'items': [ ('Option 1', 'option1'), 33 | ('Option 2', 'option2'), 34 | ('Option 3', 'option3'), 35 | ('Option 4', 'option4'), 36 | ('Option 5', 'option5'), 37 | ('Option 6', 'option6'), 38 | ('Option 7', 'option7'), 39 | ('Option 8', 'option8'), 40 | ('Option 9', 'option9') 41 | ], 42 | 'default': 4, 43 | 'scroll': 1, 44 | 'height': 5, 45 | '_next': 'window2', 46 | }, 47 | { 48 | '_name': 'window2', 49 | '_widget': 'entry', 50 | 'title': 'Title 2', 51 | 'text': 'Text content 2', 52 | 'prompts': [ 'Prompt 1', 53 | 'Prompt 2', 54 | 'Prompt 3' 55 | ], 56 | 'width': 70, 57 | '_next': 'window3', 58 | '_post_cb': window2_post_cb, 59 | }, 60 | ('window3', get_window3), 61 | { '_name': 'validation_error', 62 | '_widget': 'choice', 63 | '_next': 'window2', 64 | 'title': 'Error', 65 | 'text': "Please fill the 'Prompt 1' field.", 66 | 'buttons': ['OK'], 67 | }] 68 | 69 | -------------------------------------------------------------------------------- /snackwich/example/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from sys import path 4 | path.insert(0, '.') 5 | 6 | from sys import exit 7 | 8 | from snackwich.main import Snackwich 9 | 10 | from config import config 11 | 12 | panels = Snackwich(config) 13 | 14 | result = panels.execute('window1') 15 | 16 | if result is None: 17 | print("Cancelled.") 18 | exit(1) 19 | 20 | from pprint import pprint 21 | pprint(result) 22 | 23 | -------------------------------------------------------------------------------- /snackwich/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class GotoPanelException(Exception): 3 | _key = None 4 | 5 | def __init__(self, key): 6 | self._key = key 7 | 8 | @property 9 | def key(self): 10 | return self._key 11 | 12 | class BreakSuccessionException(Exception): 13 | pass 14 | 15 | class QuitException(Exception): 16 | pass 17 | 18 | class RedrawException(Exception): 19 | pass 20 | 21 | class QuitAndExecuteException(Exception): 22 | def __init__(self, posthook_cb): 23 | Exception.__init__(self, "Snackwich loop has been explicitly broken " 24 | "with a posthook.") 25 | 26 | self.__posthook_cb = posthook_cb 27 | 28 | @property 29 | def posthook_cb(self): 30 | return self.__posthook_cb 31 | 32 | class PostQuitAndExecuteException(QuitAndExecuteException): 33 | def __init__(self, posthook_cb, result): 34 | QuitAndExecuteException.__init__(self, posthook_cb) 35 | 36 | self.__result = result 37 | 38 | @property 39 | def result(self): 40 | return self.__result 41 | 42 | class PostAndGotoException(Exception): 43 | def __init__(self, result, next_panel=None): 44 | self.__result = result 45 | self.__next_panel = next_panel 46 | 47 | @property 48 | def result(self): 49 | return self.__result 50 | 51 | @property 52 | def next_panel(self): 53 | return self.__next_panel 54 | 55 | -------------------------------------------------------------------------------- /snackwich/log_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | 4 | -------------------------------------------------------------------------------- /snackwich/main.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | import copy 4 | 5 | from random import randint 6 | 7 | from snack import SnackScreen 8 | 9 | from snackwich.patch import ListboxChoiceWindow, ButtonChoiceWindow, \ 10 | EntryWindow 11 | from snackwich.ui_functions import ProgressWindow, MessageWindow, \ 12 | CheckboxListWindow#, RadioListWindow 13 | from snackwich.exceptions import GotoPanelException, \ 14 | BreakSuccessionException, \ 15 | QuitException, \ 16 | RedrawException, \ 17 | QuitAndExecuteException, \ 18 | PostQuitAndExecuteException 19 | from snackwich.buttons import BTN_CANCEL 20 | 21 | class _PanelContainer(object): 22 | __config = None 23 | __keys = None 24 | __keys_r = None 25 | __ns_keys = None 26 | 27 | def __init__(self, config): 28 | """ 29 | config: List of panel expressions. 30 | """ 31 | 32 | if config.__class__ != list: 33 | message = "Config must be a list." 34 | 35 | logging.error(message) 36 | raise TypeError(message) 37 | 38 | self.__config = [] 39 | self.__keys = [] 40 | self.__keys_r = {} 41 | self.__ns_keys = {} 42 | 43 | logging.info("Loading panels.") 44 | 45 | try: 46 | i = 0 47 | for expression in config: 48 | self.add(expression) 49 | i += 1 50 | except: 51 | logging.exception("Could not load panel (%d)." % (i)) 52 | raise 53 | 54 | def exists(self, key): 55 | 56 | return (key in self.__keys_r) 57 | 58 | def get_index_by_key(self, key): 59 | 60 | return self.__keys_r[key] 61 | 62 | def get_copy_by_key(self, key): 63 | 64 | index = self.__keys_r[key] 65 | return copy.deepcopy(self.__config[index]) 66 | 67 | def remove(self, key, ns=None): 68 | logging.info("Deregistering panel [%s] in namespace [%s]." % (key, ns)) 69 | 70 | if key not in self.__keys_r: 71 | raise KeyError("Expression to be removed [%s] is not " 72 | "registered." % (key)) 73 | 74 | if ns != None: 75 | if ns not in self.__ns_keys: 76 | raise KeyError("Namespace [%s] is not valid for removal of " 77 | "keys." % (ns)) 78 | 79 | if key not in self.__ns_keys[ns]: 80 | raise KeyError("Panel [%s] is not indexed under namespace " 81 | "[%s]. Can not remove." % (key, ns)) 82 | 83 | self.__ns_keys[ns].remove(key) 84 | 85 | if not self.__ns_keys[ns]: 86 | del self.__ns_keys[ns] 87 | 88 | index = self.__keys_r[key] 89 | 90 | logging.debug("Removing panel [%s] with index [%d] under namespace " 91 | "[%s]." % (key, index, ns)) 92 | 93 | del self.__keys[index] 94 | del self.__keys_r[key] 95 | del self.__config[index] 96 | 97 | # Decrement the indices that we have for the panels currently having 98 | # indices higher than the one that was deleted. 99 | for other_key, other_index in self.__keys_r.items(): 100 | if other_index > index: 101 | self.__keys_r[other_key] = other_index - 1 102 | 103 | def keys_in_ns(self, ns): 104 | 105 | return self.__ns_keys[ns][:] if ns in self.__ns_keys else [] 106 | 107 | def remove_ns(self, ns): 108 | 109 | logging.info("Deregistering panels under namespace [%s]." % (ns)) 110 | 111 | keys = self.keys_in_ns(ns) 112 | 113 | try: 114 | for key in keys: 115 | self.remove(key, ns) 116 | except: 117 | logging.exception("Could not remove all items under namespace " 118 | "[%s]." % (ns)) 119 | raise 120 | 121 | return keys 122 | 123 | def get_random_panel_name(self, kernel='Random'): 124 | 125 | while 1: 126 | key = ('%s_%d' % (kernel, randint(11111, 99999))) 127 | 128 | if key not in self.__keys_r: 129 | return key 130 | 131 | def add(self, expression, ns=None, assign_random_key=False): 132 | 133 | try: 134 | key = self.__get_current_key(expression) 135 | except: 136 | logging.exception("Could not render key for expression.") 137 | raise 138 | 139 | logging.info("Registering panel [%s]." % (key)) 140 | 141 | if assign_random_key: 142 | if key != None: 143 | message = ("A random key can not be assigned because the key [%s] " 144 | "was already defined." % (key)) 145 | 146 | logging.exception(message) 147 | raise KeyError(message) 148 | 149 | key = self.get_random_panel_name() 150 | 151 | if not assign_random_key and key in self.__keys_r: 152 | raise KeyError("Expression [%s] already registered." % (key)) 153 | 154 | new_index = len(self.__keys_r) 155 | 156 | self.__config.append(expression) 157 | self.__keys.append(key) 158 | self.__keys_r[key] = new_index 159 | 160 | if ns != None: 161 | if ns not in self.__ns_keys: 162 | self.__ns_keys[ns] = [] 163 | 164 | self.__ns_keys[ns].append(key) 165 | 166 | return key 167 | 168 | def __get_current_key(self, expression): 169 | """For a given expression, validate the structure and return the key. 170 | """ 171 | 172 | if isinstance(expression, tuple): 173 | if len(expression) != 2 or \ 174 | not isinstance(expression[0], str) or \ 175 | not callable(expression[1]): 176 | 177 | message = "As a tuple, an expression must be " \ 178 | "(, )." 179 | 180 | logging.error(message) 181 | raise Exception(message) 182 | 183 | (key, expression_call) = expression 184 | elif not isinstance(expression, dict): 185 | message = "Expression is not a callable tuple, nor is it a " \ 186 | "dictionary." 187 | 188 | logging.error(message) 189 | raise Exception(message) 190 | else: 191 | if '_name' not in expression: 192 | message = "'_name' key not found in expression." 193 | 194 | logging.error(message) 195 | raise Exception(message) 196 | 197 | key = expression['_name'] 198 | 199 | return key 200 | 201 | class Snackwich(object): 202 | __panels = None 203 | next_key = None 204 | results = None 205 | 206 | # Some shorthand aliases to make the config nicer. These must be FROM'd 207 | # into the current scope, above. 208 | aliases = { 'list': ListboxChoiceWindow, 209 | 'choice': ButtonChoiceWindow, 210 | 'entry': EntryWindow, 211 | 'progress': ProgressWindow, 212 | 'message': MessageWindow, 213 | 'checklist': CheckboxListWindow, 214 | # 'radiolist': RadioListWindow, 215 | } 216 | 217 | def __init__(self, config): 218 | 219 | try: 220 | self.__panels = _PanelContainer(config) 221 | except: 222 | logging.exception("Could not build panel contaiment.") 223 | raise 224 | 225 | def __process_stanza(self, screen, key, expression, meta_attributes): 226 | """Process a single expression (form) from the config.""" 227 | 228 | if 'widget' not in meta_attributes: 229 | message = "'_widget' value is not in the expression for key " \ 230 | "[%s]." % (key) 231 | 232 | logging.error(message) 233 | raise Exception(message) 234 | 235 | widget = meta_attributes['widget'] 236 | 237 | # We provide a couple of aliases in order to be more intuitive. If 238 | # used, invoke the actual class, now. 239 | if isinstance(widget, str): 240 | if widget not in self.aliases: 241 | message = "Widget with name [%s] is not a valid alias. " \ 242 | "Please provide a correct alias or use an actual " \ 243 | "class." % (widget) 244 | 245 | logging.error(message) 246 | raise Exception(message) 247 | 248 | widget = self.aliases[widget] 249 | 250 | # Create the widget. 251 | 252 | try: 253 | result = widget(screen=screen, **expression) 254 | except: 255 | logging.exception("Could not manufacture widget [%s] under key " 256 | "[%s]." % (widget.__class__.__name__, key)) 257 | raise 258 | 259 | screen.refresh() 260 | 261 | return result 262 | 263 | def __slice_meta_attributes(self, dict_expression): 264 | meta_attributes = { } 265 | to_remove = [ ] 266 | for key in dict_expression: 267 | if key[0] == '_': 268 | meta_attributes[key[1:]] = dict_expression[key] 269 | to_remove.append(key) 270 | 271 | for key in to_remove: 272 | del dict_expression[key] 273 | 274 | return meta_attributes 275 | 276 | def __process_expression(self, expression, results, screen): 277 | 278 | # Allow there to be a function call or lambda that must be 279 | # called to render the actual expression. This call will 280 | # receive the past results as well as the current key. This 281 | # will allow the current form to feed from previous forms. 282 | if isinstance(expression, tuple): 283 | # The expression is a tuple with the key and a callback. 284 | 285 | (key, expression_call) = expression 286 | 287 | try: 288 | expression = expression_call(self, key, results, 289 | screen) 290 | except QuitException as e: 291 | logging.info("Post-callback for key [%s] has " 292 | "requested emergency exit." % (key)) 293 | 294 | return ('break', key, expression, True) 295 | except GotoPanelException as e: 296 | # Go to a different panel. 297 | 298 | new_key = e.key 299 | 300 | logging.info("We were routed from panel with key [%s] " 301 | "to panel with key [%s]." % (key, 302 | new_key)) 303 | 304 | if not self.__panels.exists(key): 305 | message = "We were told to go to panel with " \ 306 | "invalid key [%s] while processing " \ 307 | "panel with key [%s]." % (new_key, key) 308 | 309 | logging.error(message) 310 | raise Exception(message) 311 | 312 | key = new_key 313 | return ('continue', key, expression, False) 314 | except BreakSuccessionException as e: 315 | # Break out of the menu. 316 | 317 | logging.info("We were told to stop presenting panels " 318 | "while processing panel with key [%s]." % 319 | (key)) 320 | return ('break', key, expression, False) 321 | except: 322 | logging.exception("Callable expression for key [%s] " 323 | "threw an exception." % (key)) 324 | raise 325 | 326 | if not isinstance(expression, dict): 327 | message = "Effective expression for key [%s] is not " \ 328 | "a dictionary." % (key) 329 | 330 | logging.exception(message) 331 | raise TypeError(message) 332 | else: 333 | # The expression is a static dictionary. 334 | 335 | key = expression['_name'] 336 | del expression['_name'] 337 | 338 | return (None, key, expression, False) 339 | 340 | def execute(self, first_key): 341 | """Display the panels. This resembles a state-machine, except that the 342 | transitions can be determined on the fly. 343 | """ 344 | 345 | self.results = { } 346 | 347 | screen = SnackScreen() 348 | 349 | try: 350 | # Move through the panels in a linked-list fashion, so that we can 351 | # reroute if told to. 352 | 353 | key = first_key 354 | quit = False 355 | loop = True 356 | posthook_cb = None 357 | loophook_cb = None 358 | while loop: 359 | logging.info("Processing panel expression with key [%s]." % 360 | (key)) 361 | 362 | try: 363 | expression = self.__panels.get_copy_by_key(key) 364 | except: 365 | logging.exception("Could not retrieve expression [%s]." % 366 | (key)) 367 | raise 368 | 369 | # Don't reprocess the expression more than once. 370 | result = self.__process_expression(expression, self.results, screen) 371 | (result_type, key, expression, quit_temp) = result 372 | 373 | if quit_temp: 374 | quit = True 375 | 376 | if result_type == 'break': 377 | break 378 | elif result_type == 'continue': 379 | continue 380 | 381 | try: 382 | meta_attributes = self.__slice_meta_attributes(expression) 383 | except: 384 | logging.exception("Could not slice meta-attributes from " 385 | "panel expression with key [%s]." % 386 | (key)) 387 | raise 388 | 389 | # Determine the panel to succeed this one. We do this early to 390 | # allow callback to adjust this. 391 | self._next_key = meta_attributes['next'] \ 392 | if 'next' in meta_attributes \ 393 | else None 394 | 395 | if not self.__panels.exists(key): 396 | logging.error("Key [%s] set as next from panel with " 397 | "key [%s] does not refer to a valid " 398 | "panel." % (key)) 399 | 400 | logging.info("Processing expression with key [%s]." % (key)) 401 | 402 | try: 403 | result = self.__process_stanza(screen, key, expression, 404 | meta_attributes) 405 | # TODO: Move this to a separate call. 406 | if 'post_cb' in meta_attributes: 407 | callback = meta_attributes['post_cb'] 408 | 409 | if not callable(callback): 410 | message = ("Callback for key [%s] is not " 411 | "callable." % (key)) 412 | 413 | logging.error(message) 414 | raise Exception(message) 415 | 416 | try: 417 | result_temp = callback(self, key, result, 418 | expression, screen) 419 | 420 | # Allow them to adjust the result, but don't 421 | # require it to be returned. 422 | if result_temp: 423 | result = result_temp 424 | 425 | except PostQuitAndExecuteException as e: 426 | 427 | logging.info("Post-callback for key [%s] has " 428 | "requested an exit-and-execute." % 429 | (key)) 430 | 431 | # We can't break the loop the same way as 432 | # everything else, because we have a result that we 433 | # want to have registered. So, just allow us to 434 | # reach the end of the loop and prevent us from 435 | # looping again. 436 | loop = False 437 | 438 | posthook_cb = e.posthook_cb 439 | result = e.result 440 | 441 | except QuitAndExecuteException as e: 442 | 443 | logging.info("Post-callback for key [%s] has " 444 | "requested an exit-and-execute." % 445 | (key)) 446 | 447 | quit = True 448 | posthook_cb = e.posthook_cb 449 | 450 | break 451 | 452 | except GotoPanelException as e: 453 | # Go to a different panel. 454 | 455 | new_key = e.key 456 | 457 | logging.info("We were routed from panel with key " 458 | "[%s] to panel with key [%s] while in" 459 | " post callback." % (key, new_key)) 460 | 461 | if not self.__panels.exists(key): 462 | message = "We were told to go to panel with " \ 463 | "invalid key [%s] while in post-" \ 464 | "callback for panel with key " \ 465 | "[%s]." % (new_key, key) 466 | 467 | logging.error(message) 468 | raise Exception(message) 469 | 470 | key = new_key 471 | continue 472 | 473 | except RedrawException as e: 474 | logging.info("Post-callback for key [%s] has " 475 | "requested a redraw." % (key)) 476 | 477 | # Reset state values that might've been affected by 478 | # a callback. 479 | 480 | quit = False 481 | self._next_key = None 482 | 483 | continue; 484 | 485 | except QuitException as e: 486 | logging.info("Post-callback for key [%s] has " 487 | "requested emergency exit." % (key)) 488 | 489 | quit = True 490 | break 491 | 492 | except BreakSuccessionException as e: 493 | logging.info("Post-callback for key [%s] has " 494 | "instructed us to terminate." % (key)) 495 | break 496 | 497 | except: 498 | logging.exception("An exception occured in the " 499 | "post-callback for key [%s]." % 500 | (key)) 501 | raise 502 | 503 | # Static expression result handler. 504 | else: 505 | # If there's only one button, ignore what method they 506 | # used to close the window. 507 | if 'buttons' in expression and \ 508 | len(expression['buttons']) > 1: 509 | if 'is_esc' in result and result['is_esc'] or \ 510 | 'button' in result and \ 511 | result['button'] == BTN_CANCEL[1]: 512 | break 513 | 514 | if 'collect_results' in meta_attributes and \ 515 | meta_attributes['collect_results']: 516 | if key in self.results: 517 | self.results[key].append(result) 518 | else: 519 | self.results[key] = [result] 520 | else: 521 | self.results[key] = result 522 | 523 | except: 524 | logging.exception("There was an exception while processing" 525 | " the stanza for key [%s]." % (key)) 526 | raise 527 | 528 | if self._next_key: 529 | key = self._next_key 530 | else: 531 | break 532 | except: 533 | logging.exception("There was an exception while processing " 534 | "config stanza with key [%s]." % (key)) 535 | raise 536 | finally: 537 | screen.finish() 538 | 539 | if posthook_cb: 540 | posthook_cb() 541 | 542 | return None if quit else self.results 543 | 544 | def get_next_panel(self): 545 | return self._next_key 546 | 547 | def set_next_panel(self, key): 548 | self._next_key = key 549 | 550 | def get_container(self): 551 | return self.__panels 552 | 553 | def get_results_for_ns(self, ns, allow_missing=False): 554 | results = {} 555 | for key in self.__panels.keys_in_ns(ns): 556 | try: 557 | results[key] = self.get_results(key) 558 | except KeyError: 559 | if not allow_missing: 560 | raise 561 | 562 | return results 563 | 564 | def get_results(self, keys): 565 | 566 | if keys.__class__ == list: 567 | results = {} 568 | for key in keys: 569 | results[key] = self.results[key] 570 | 571 | return results 572 | else: 573 | return self.results[keys] 574 | 575 | -------------------------------------------------------------------------------- /snackwich/patch.py: -------------------------------------------------------------------------------- 1 | """Adjusted versions of the original functions.""" 2 | 3 | from snack import ButtonBar, TextboxReflowed, Listbox, GridFormHelp, Grid, \ 4 | Entry, Label 5 | 6 | from snackwich.ui_functions import RT_EXECUTEANDPOP, RT_EXECUTEONLY, \ 7 | RT_DRAWONLY, ActivateWindow 8 | 9 | 10 | # Added auto-pop disable: Rather than immediately popping the dialog of the 11 | # stack of currently-displayed panels, we allow this to be done manually, 12 | # later. 13 | 14 | def ListboxChoiceWindow(screen, title, text, items, buttons = ('Ok', 'Cancel'), 15 | width=40, scroll=None, height=-1, default=None, help=None, 16 | timer_ms=None, secondary_message=None, 17 | secondary_message_width=None, 18 | run_type=RT_EXECUTEANDPOP): 19 | 20 | # Dustin: Added timer_ms parameter. Added secondary_message arguments. 21 | # Added result boolean for whether ESC was pressed. Results are now 22 | # dictionaries. Added auto_pop parameter. 23 | 24 | if height == -1: 25 | height = len(items) 26 | 27 | if scroll == None: 28 | scroll = len(items) > height 29 | 30 | bb = ButtonBar(screen, buttons) 31 | t = TextboxReflowed(width, text) 32 | l = Listbox(height, scroll = scroll, returnExit = 1) 33 | count = 0 34 | for item in items: 35 | if (type(item) == tuple): 36 | (text, key) = item 37 | else: 38 | text = item 39 | key = count 40 | 41 | if (default == count): 42 | default = key 43 | elif (default == text): 44 | default = key 45 | 46 | l.append(text, key) 47 | count = count + 1 48 | 49 | if (default != None): 50 | l.setCurrent (default) 51 | 52 | g = GridFormHelp(screen, title, help, 1, 3 + (1 if secondary_message else 53 | 0)) 54 | 55 | row = 0 56 | 57 | g.add(t, 0, row) 58 | row += 1 59 | 60 | if secondary_message: 61 | if not secondary_message_width: 62 | secondary_message_width = width 63 | 64 | t2 = TextboxReflowed(secondary_message_width, secondary_message) 65 | g.add(t2, 0, row, padding = (0, 0, 0, 1)) 66 | row += 1 67 | 68 | g.add(l, 0, row, padding = (0, 1, 0, 1)) 69 | row += 1 70 | 71 | g.add(bb, 0, row, growx = 1) 72 | row += 1 73 | 74 | if timer_ms: 75 | g.form.w.settimer(timer_ms) 76 | 77 | (button, is_esc) = ActivateWindow(g, run_type, bb) 78 | 79 | return {'button': button, 80 | 'is_esc': is_esc, 81 | 'grid': g, 82 | 83 | # A hack. There's no count method. 84 | 'selected': l.current() if l.key2item else None, 85 | } 86 | 87 | def ButtonChoiceWindow(screen, title, text, buttons=['Ok', 'Cancel'], width=40, 88 | x=None, y=None, help=None, timer_ms=None, 89 | secondary_message=None, secondary_message_width=None, 90 | run_type=RT_EXECUTEANDPOP): 91 | 92 | # Dustin: Added timer_ms parameter. Added secondary_message arguments. 93 | # Added result boolean for whether ESC was pressed. Results are now 94 | # dictionaries. Added auto_pop parameter. 95 | 96 | bb = ButtonBar(screen, buttons) 97 | t = TextboxReflowed(width, text, maxHeight = screen.height - 12) 98 | 99 | g = GridFormHelp(screen, title, help, 1, 2 + (1 if secondary_message else 100 | 0)) 101 | 102 | row = 0 103 | 104 | g.add(t, 0, row, padding = (0, 0, 0, 1)) 105 | row += 1 106 | 107 | if secondary_message: 108 | if not secondary_message_width: 109 | secondary_message_width = width 110 | 111 | t2 = TextboxReflowed(secondary_message_width, secondary_message) 112 | g.add(t2, 0, row, padding = (0, 0, 0, 1)) 113 | row += 1 114 | 115 | g.add(bb, 0, row, growx = 1) 116 | row += 1 117 | 118 | if timer_ms: 119 | g.form.w.settimer(timer_ms) 120 | 121 | (button, is_esc) = ActivateWindow(g, run_type, bb, x, y) 122 | 123 | return {'button': button, 124 | 'is_esc': is_esc, 125 | 'grid': g, 126 | } 127 | 128 | def EntryWindow(screen, title, text, prompts, allowCancel=1, width=40, 129 | entryWidth=20, buttons=[ 'Ok', 'Cancel' ], help=None, 130 | timer_ms=None, anchorLeft=0, anchorRight=1, 131 | secondary_message=None, secondary_message_width=None, 132 | run_type=RT_EXECUTEANDPOP): 133 | 134 | # Dustin: Added timer_ms parameter. Added secondary_message arguments. 135 | # Added result boolean for whether ESC was pressed. Added 136 | # anchorLeft and anchorRight as arguments to this function. Added 137 | # secondary_message (test below primary text). Results are now 138 | # dictionaries. Added auto_pop parameter. 139 | 140 | bb = ButtonBar(screen, buttons); 141 | t = TextboxReflowed(width, text) 142 | 143 | count = 0 144 | for n in prompts: 145 | count = count + 1 146 | 147 | sg = Grid(2, count) 148 | 149 | count = 0 150 | entryList = [] 151 | for n in prompts: 152 | if (type(n) == tuple): 153 | (n, e) = n 154 | if issubclass(e.__class__, str): 155 | e = Entry(entryWidth, e) 156 | else: 157 | e = Entry(entryWidth) 158 | 159 | sg.setField(Label(n), 0, count, padding=(0, 0, 1, 0), 160 | anchorLeft=anchorLeft, anchorRight=anchorRight) 161 | sg.setField(e, 1, count, anchorLeft = 1) 162 | count = count + 1 163 | entryList.append(e) 164 | 165 | g = GridFormHelp(screen, title, help, 1, 3 + (1 if secondary_message else 166 | 0)) 167 | 168 | row = 0 169 | 170 | g.add(t, 0, row, padding = (0, 0, 0, 1)) 171 | row += 1 172 | 173 | if secondary_message: 174 | if not secondary_message_width: 175 | secondary_message_width = width 176 | 177 | t2 = TextboxReflowed(secondary_message_width, secondary_message) 178 | g.add(t2, 0, row, padding = (0, 0, 0, 1)) 179 | row += 1 180 | 181 | g.add(sg, 0, row, padding = (0, 0, 0, 1)) 182 | row += 1 183 | 184 | g.add(bb, 0, row, growx = 1) 185 | row += 1 186 | 187 | if timer_ms: 188 | g.form.w.settimer(timer_ms) 189 | 190 | (button, is_esc) = ActivateWindow(g, run_type, bb) 191 | 192 | entryValues = [] 193 | count = 0 194 | for n in prompts: 195 | entryValues.append(entryList[count].value()) 196 | count = count + 1 197 | 198 | return {'button': button, 199 | 'is_esc': is_esc, 200 | 'values': tuple(entryValues), 201 | 'grid': g, 202 | } 203 | 204 | -------------------------------------------------------------------------------- /snackwich/ui_functions.py: -------------------------------------------------------------------------------- 1 | """A utility function.""" 2 | 3 | import types 4 | 5 | from snack import ButtonBar, TextboxReflowed, Listbox, GridFormHelp, Scale, \ 6 | Checkbox 7 | 8 | from snackwich.buttons import BTN_OK, BTN_CANCEL 9 | 10 | # Show the dialog, wait for input or timeout, pop, and redraw screen. 11 | RT_EXECUTEANDPOP = 1 12 | 13 | # Show the dialog, wait for input or timeout. The dialog must be popped off the 14 | # screen and redrawn manually. 15 | RT_EXECUTEONLY = 2 16 | 17 | # Show the dialog. The dialog must be popped off the screen and redrawn 18 | # manually. 19 | RT_DRAWONLY = 3 20 | 21 | def ProgressWindow(screen, title, text, progress, max_progress=100, width=40, 22 | help=None, timer_ms=None, show_cancel=False, 23 | run_type=RT_EXECUTEANDPOP): 24 | """ 25 | Render a panel with a progress bar and a "Cancel" button. 26 | """ 27 | 28 | if progress > max_progress: 29 | raise OverflowError("Progress (%d) has exceeded max (%d)." % 30 | (progress, max_progress)) 31 | 32 | scale_proportion = .80 33 | scale_width = int(width * scale_proportion) 34 | scale = Scale(scale_width, max_progress) 35 | scale.set(progress) 36 | 37 | g = GridFormHelp(screen, title, help, 1, 3) 38 | 39 | t = TextboxReflowed(width, text) 40 | g.add(t, 0, 0) 41 | 42 | g.add(scale, 0, 1, padding = (0, 1, 0, 1)) 43 | 44 | if show_cancel: 45 | bb = ButtonBar(screen, [BTN_CANCEL[0]]) 46 | g.add(bb, 0, 2, growx = 1) 47 | 48 | if timer_ms: 49 | g.form.w.settimer(timer_ms) 50 | 51 | (button, is_esc) = ActivateWindow(g, run_type, \ 52 | button_bar=bb if show_cancel else None) 53 | 54 | return {'button': button, 55 | 'is_esc': is_esc, 56 | 'progress': progress, 57 | 'grid': g, 58 | } 59 | 60 | def MessageWindow(screen, title, text, width=40, help=None, timer_ms=None, 61 | run_type=RT_EXECUTEANDPOP): 62 | """ 63 | Render a panel with a message and no buttons. This is intended to 64 | proceed to the next panel, where some action is taken before 65 | returning -its- expression. Meanwhile, this panel is left displayed. 66 | Obviously, this panel's timer shouldn't be large, if not zero. 67 | """ 68 | 69 | g = GridFormHelp(screen, title, help, 1, 3) 70 | 71 | t = TextboxReflowed(width, text) 72 | g.add(t, 0, 0) 73 | 74 | if timer_ms: 75 | g.form.w.settimer(timer_ms) 76 | 77 | (button, is_esc) = ActivateWindow(g, run_type) 78 | 79 | return {'is_esc': is_esc, 80 | 'grid': g, 81 | } 82 | 83 | def ActivateWindow(g, run_type, button_bar=None, x=None, y=None): 84 | global RT_EXECUTEANDPOP, RT_EXECUTEONLY, RT_DRAWONLY 85 | 86 | if run_type == RT_DRAWONLY: 87 | g.draw() 88 | 89 | button = None 90 | is_esc = False 91 | 92 | else: 93 | if run_type == RT_EXECUTEANDPOP: 94 | rc = g.runOnce(x, y) 95 | 96 | elif run_type == RT_EXECUTEONLY: 97 | rc = g.run(x, y) 98 | 99 | button = button_bar.buttonPressed(rc) if button_bar else None 100 | is_esc = (rc == 'ESC') 101 | 102 | return (button, is_esc) 103 | 104 | def ManualPop(screen, refresh=True): 105 | screen.popWindow(refresh) 106 | 107 | def CheckboxListWindow(screen, title, text, items, 108 | buttons=(BTN_OK[0], BTN_CANCEL[0]), width=40, scroll=0, 109 | default=None, help=None, timer_ms=None, 110 | secondary_message=None, secondary_message_width=None, 111 | run_type=RT_EXECUTEANDPOP, default_check_state=False, 112 | default_check_states=None): 113 | 114 | if not default_check_states: 115 | default_check_states = [default_check_state 116 | for i 117 | in xrange(len(items))] 118 | elif len(default_check_states) != len(items): 119 | raise Exception("Number (%d) of check states does not match number of " 120 | "items (%d)." % (len(default_check_states), 121 | len(items))) 122 | 123 | primary_message_height = 1 124 | button_height = 1 125 | checklist_margin = 0 126 | 127 | rows = primary_message_height + \ 128 | button_height + \ 129 | (2 if secondary_message else 0) + \ 130 | len(items) + \ 131 | checklist_margin 132 | 133 | bb = ButtonBar(screen, buttons) 134 | t = TextboxReflowed(width, text) 135 | 136 | g = GridFormHelp(screen, title, help, 1, rows) 137 | 138 | row = 0 139 | 140 | g.add(t, 0, row) 141 | row += 1 142 | 143 | if secondary_message: 144 | if not secondary_message_width: 145 | secondary_message_width = width 146 | 147 | t2 = TextboxReflowed(secondary_message_width, secondary_message) 148 | g.add(t2, 0, row, padding = (0, 1, 0, 0)) 149 | row += 1 150 | 151 | checkboxes = [] 152 | 153 | i = 0 154 | for item in items: 155 | if (type(item) == types.TupleType): 156 | (text, state) = item 157 | else: 158 | text = item 159 | state = default_check_state 160 | 161 | padding = [0, 0, 0, 0] 162 | if i == 0: 163 | padding[1] = 1 164 | 165 | if i == len(items) - 1: 166 | padding[3] = 1 167 | 168 | checkbox = Checkbox(text, int(state)) 169 | checkboxes.append(checkbox) 170 | 171 | g.add(checkbox, 0, row, padding) 172 | row += 1 173 | i += 1 174 | 175 | g.add(bb, 0, row, growx = 1) 176 | row += 1 177 | 178 | if timer_ms: 179 | g.form.w.settimer(timer_ms) 180 | 181 | (button, is_esc) = ActivateWindow(g, run_type, bb) 182 | 183 | values = [checkbox.selected() for checkbox in checkboxes] 184 | 185 | return {'values': values, 186 | 'button': button, 187 | 'is_esc': is_esc, 188 | 'grid': g, 189 | } 190 | 191 | #def RadioListWindow(screen, title, text, items, buttons = ('Ok', 'Cancel'), 192 | # width=40, scroll=0, default=None, help=None, 193 | # timer_ms=None, secondary_message=None, 194 | # secondary_message_width=None, 195 | # run_type=RT_EXECUTEANDPOP, 196 | # default=0): 197 | # 198 | # primary_message_height = 1 199 | # button_height = 1 200 | # radiolist_margin = 0 201 | # 202 | # rows = primary_message_height + \ 203 | # button_height + \ 204 | # (2 if secondary_message else 0) + \ 205 | # len(items) + \ 206 | # radiolist_margin 207 | # 208 | # bb = ButtonBar(screen, buttons) 209 | # t = TextboxReflowed(width, text) 210 | # 211 | # g = GridFormHelp(screen, title, help, 1, rows) 212 | # 213 | # row = 0 214 | # 215 | # g.add(t, 0, row) 216 | # row += 1 217 | # 218 | # if secondary_message: 219 | # if not secondary_message_width: 220 | # secondary_message_width = width 221 | # 222 | # t2 = TextboxReflowed(secondary_message_width, secondary_message) 223 | # g.add(t2, 0, row, padding = (0, 1, 0, 0)) 224 | # row += 1 225 | # 226 | # radios = [] 227 | # rg = RadioGroup() 228 | # 229 | # i = 0 230 | # for text in items: 231 | # state = (i == default) 232 | # 233 | # padding = [0, 0, 0, 0] 234 | # if i == 0: 235 | # padding[1] = 1 236 | # 237 | # if i == len(items) - 1: 238 | # padding[3] = 1 239 | # 240 | # rg.add(text, int(state)) 241 | # 242 | # row += 1 243 | # i += 1 244 | # 245 | # g.add(rg, 0, row, padding) 246 | # g.add(bb, 0, row, growx = 1) 247 | # row += 1 248 | # 249 | # if timer_ms: 250 | # g.form.w.settimer(timer_ms) 251 | # 252 | # (button, is_esc) = ActivateWindow(g, run_type, bb) 253 | # 254 | # values = [checkbox.selected() for radio in radios] 255 | # 256 | # return {'values': values, 257 | # 'button': button, 258 | # 'is_esc': is_esc, 259 | # 'grid': g, 260 | # } 261 | 262 | -------------------------------------------------------------------------------- /ubuntu.txt: -------------------------------------------------------------------------------- 1 | python-newt 2 | --------------------------------------------------------------------------------