24 Commits

Author SHA1 Message Date
04a6361d6b typo in readme 2019-04-28 17:48:18 +00:00
1ed1ca1065 mostly support python 3.7, handle API rich text 2019-04-24 15:43:14 -07:00
2a9b674cc5 Use locale for story times 2018-07-01 14:57:51 -07:00
8a86d4b559 Merge branch 'master' of github.com:aarongut/cmdasana 2018-07-01 14:49:47 -07:00
da9dd3578f Format assignee and due date 2018-07-01 14:47:08 -07:00
364a69fb3b Merge commit '52ee057924a162ed423f5ef1592e4230e76e15b7' 2018-06-05 14:26:41 -07:00
47eff82d3f show story creation time in details view 2018-05-21 17:58:30 -07:00
52ee057924 Handle list formatting 2018-03-20 20:55:41 -07:00
1d81648ee4 Add License 2018-03-13 08:45:54 -07:00
1337dc9f80 Cleanup documentation 2018-03-13 08:40:08 -07:00
5571afe977 Add list formatting 2018-03-12 21:02:18 -07:00
c97f79a081 Fix bug with empty description 2018-03-07 18:56:08 -08:00
1b174219d5 Update dependencies 2018-03-07 18:42:25 -08:00
3e65d08bbf Formatting works for comments and description
Supports bold, italic, underline, and links
2018-03-07 09:44:02 -08:00
d7b919701c Out with the old 2018-03-07 08:46:40 -08:00
256d7a9aab Filter out non-comment stories 2018-03-05 21:42:32 -08:00
b872f06394 Make ATM a single scroll list 2018-03-05 21:36:40 -08:00
c1759c904a Bugfixes
- don't crash on mouse events
 - load the correct project when a task is multihomed
2017-12-25 15:28:02 -08:00
4cb283b890 Add support for subtasks 2017-12-11 22:28:04 -08:00
7513035003 Navigation works! 2017-12-10 22:48:14 -08:00
b35346694c Prompt for token 2017-12-06 21:58:53 -08:00
af7d7338b0 Start rewrite 2017-12-05 22:09:52 -08:00
773e5d8914 [WIP] Refactor code to be more modular 2017-10-10 20:08:08 -07:00
Aaron Gutierrez
beec97d0d9 add makefile 2015-08-21 14:25:57 -07:00
62 changed files with 882 additions and 25196 deletions

6
.gitignore vendored
View File

@@ -1,10 +1,12 @@
*.swp
*.pyc
__pycache__
tags
venv
.state
.oauth
secrets.py
urwid-src

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "python-asana"]
path = python-asana
url = git@github.com:Asana/python-asana.git

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
Copyright 2018 Aaron Gutierrez
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

2
Makefile Normal file
View File

@@ -0,0 +1,2 @@
tags: ui/*.py models/*.py main.py auth.py asana_service.py
ctags -R ui models *.py

View File

@@ -1,15 +1,38 @@
# cmdasana
A curses CLI for Asana, using the Asana API.
Requirments:
## Requirments
* python 3
* [python-asana](https://github.com/Asana/python-asana)
* [urwid (included)](http://urwid.org)
* python 2
* [urwid](http://urwid.org)
* [python-dateutil](https://github.com/dateutil/dateutil/)
Usage:
## Setup
### Create an Asana OAuth app
See [instructions from Asana](https://asana.com/developers/documentation/getting-started/auth#register-an-app)
on how to create a new app. Use `urn:ietf:wg:oauth:2.0:oob` as the redirect
URL.
Once you create your app, save your client ID and secret in a file `secrets.py`:
```python
CLIENT_ID='...'
CLIENT_SECRET='...'
```
./cmdasana.py
### Install dependencies
Using `pip`:
```
pip3 install asana urwid python-dateutil
```
## Usage
```
./main.py
```
When you first cmdasana, you will need to authorize the app in your browser.
Copy and paste your OAuth key into the terminal to get started.
## Navigation
Use arrow keys to navigate, `<enter>` to "click", and `<backspace>` to return to
the previous page.

1
asana
View File

@@ -1 +0,0 @@
python-asana/asana

84
asana_service.py Normal file
View File

@@ -0,0 +1,84 @@
from models.models import *
class AsanaService(object):
TASK_FIELDS = [
'name',
'html_notes',
'notes',
'assignee.name',
'assignee_status',
'completed',
'due_on',
'due_at',
'projects.name',
'parent.completed',
'parent.name',
'memberships.section.name',
'memberships.project.name',
'custom_fields.name',
'custom_fields.type',
'custom_fields.text_value',
'custom_fields.number_value',
'custom_fields.enum_value.name',
'subtasks.completed',
'subtasks.name',
]
STORY_FIELDS = [
'created_at',
'created_by.name',
'html_text',
'text',
'type'
]
def __init__(self, client):
self.client = client
self.completed_tasks = False
self.me = User(client.users.me())
self.workspace = self.me.workspaces()[0]
def __wrap__(self, Klass, values):
return map(Klass, values)
def get_my_tasks(self):
return self.__wrap__(
Task,
self.client.tasks.find_all(params = {
'completed_since': '' if self.completed_tasks else 'now',
'opt_fields': self.TASK_FIELDS,
'assignee': self.me.id(),
'workspace': self.workspace.id()
})
)
def get_project(self, project_id):
return Project(
self.client.projects.find_by_id(project_id)
)
def get_task(self, task_id):
return Task(self.client.tasks.find_by_id(
task_id,
params={
'opt_fields': self.TASK_FIELDS
}
))
def get_tasks(self, project_id):
params = {
'completed_since': '' if self.completed_tasks else 'now',
'opt_fields': self.TASK_FIELDS,
}
return self.__wrap__(
Task,
self.client.tasks.find_by_project(project_id, params=params)
)
def get_stories(self, task_id):
stories = self.client.stories.find_by_task(task_id, params = {
'opt_fields': self.STORY_FIELDS
})
filtered_stories = filter(lambda s: s['type'] == 'comment', stories)
return self.__wrap__(Story, filtered_stories)

61
auth.py Normal file
View File

@@ -0,0 +1,61 @@
import json
import os
import asana
from asana.session import AsanaOAuth2Session
from secrets import CLIENT_ID, CLIENT_SECRET
class Auth:
def __init__(self):
try:
f = open(".oauth", "r")
token = json.loads(f.readline())
f.close()
self.client = asana.Client.oauth(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
token=token,
token_updater=self.saveToken,
auto_refresh_url=AsanaOAuth2Session.token_url,
auto_refresh_kwargs={
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
},
)
except IOError:
self.getToken()
def saveToken(self, token):
f = open('.oauth', 'w')
f.write(json.dumps(token))
f.close()
os.chmod('.oauth', 0o600)
def getToken(self):
self.client = asana.Client.oauth(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
redirect_uri='urn:ietf:wg:oauth:2.0:oob',
token_updater=self.saveToken,
auto_refresh_url=AsanaOAuth2Session.token_url,
auto_refresh_kwargs={
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
},
)
(url, state) = self.client.session.authorization_url()
print("Go to the following link and enter the code:")
print(url)
try:
import webbrowser
webbrowser.open(url)
except Exception:
pass
code = sys.stdin.readline().strip()
token = self.client.session.fetch_token(code=code)
self.saveToken(token)
def getClient(self):
return self.client

View File

@@ -1,388 +0,0 @@
#!/usr/bin/env python
# -*- coding: latin-1 -*-
import os
import sys
import json
from threading import Thread
import urwid
import asana
from asana.session import AsanaOAuth2Session
from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
import ui
from secrets import CLIENT_ID, CLIENT_SECRET
# id of the personal projects domain
PERSONAL = 498346170860
class CmdAsana:
loop = None
def __init__(self):
try:
f = open(".oauth", "r")
token = json.loads(f.readline())
f.close()
self.client = asana.Client.oauth(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
token=token,
token_updater=self.saveToken,
auto_refresh_url=AsanaOAuth2Session.token_url,
auto_refresh_kwargs={
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
},
)
except IOError:
self.getToken()
self.me = self.client.users.me()
def saveToken(self, token):
f = open('.oauth', 'w')
f.write(json.dumps(token))
f.close()
def getToken(self):
self.client = asana.Client.oauth(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
redirect_uri='urn:ietf:wg:oauth:2.0:oob',
token_updater=self.saveToken,
auto_refresh_url=AsanaOAuth2Session.token_url,
auto_refresh_kwargs={
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
},
)
(url, state) = self.client.session.authorization_url()
print("Go to the following link and enter the code:")
print(url)
try:
import webbrowser
webbrowser.open(url)
except Exception:
pass
code = sys.stdin.readline().strip()
token = self.client.session.fetch_token(code=code)
self.saveToken(token)
def saveState(self):
f = open('.state', 'w')
f.write(json.dumps(self.state))
f.close()
def loadState(self):
try:
f = open('.state', 'r')
self.state = json.loads(f.readline())
f.close()
except IOError:
workspace_id = self.myWorkspaces()[0]['id']
self.state = {
'view': 'workspace',
'id': workspace_id,
'workspace_id': workspace_id
}
def myWorkspaces(self):
return self.me['workspaces']
def allMyTasks(self, workspace_id):
return self.client.tasks.find_all(params={
'assignee': self.me['id'],
'workspace': workspace_id,
'completed_since': 'now'
})
def allMyProjects(self):
if self.workspace_id != PERSONAL:
return self.client.projects.find_by_workspace(self.workspace_id)
else:
return self.client.projects.find_by_workspace(self.workspace_id,
page_size=None)
def projectTasks(self, project_id):
return self.client.tasks.find_by_project(project_id, params={
'completed_since': 'now'
})
def completeTask(self, task_id):
self.client.tasks.update(task_id, completed=True)
def newTask(self, task_after_id):
def runInThread():
if self.state['view'] == 'project':
task = self.client.tasks.create_in_workspace(
self.state['workspace_id'],
projects=[self.state['id']]
)
if task_after_id != None:
self.client.tasks.add_project(task['id'],
project=self.state['id'],
insert_after=task_after_id)
else:
task = self.client.tasks.create_in_workspace(
self.state['workspace_id'],
assignee=self.me['id']
)
update(task)
def update(task):
task_list,_ = self.frame.contents[1]
task_list.insertNewTask(task)
self.loop.draw_screen()
thread = Thread(target=runInThread)
thread.start()
def updateTask(self, task_id, name):
def runInThread():
self.client.tasks.update(task_id, name=name)
thread = Thread(target=runInThread)
thread.start()
def updateDescription(self, task_id, description):
def runInThread():
self.client.tasks.update(task_id, notes=description)
thread = Thread(target=runInThread)
thread.start()
def assignTask(self, task_id, user_id):
def runInThread():
self.client.tasks.update(task_id, assignee=user_id)
thread = Thread(target=runInThread)
thread.start()
def addComment(self, task_id, comment):
def runInThread():
self.client.stories.create_on_task(task_id, {"text": comment})
self.showDetails(task_id, show_loading=False)
thread = Thread(target=runInThread)
thread.start()
def userTypeAhead(self, text, callback):
def runInThread():
if self.state['workspace_id'] != PERSONAL:
users = self.client.workspaces \
.typeahead(self.state['workspace_id'],
{
'type': 'user',
'query': text,
'count': 5
})
else:
users = [self.me]
callback(users)
self.loop.draw_screen()
thread = Thread(target=runInThread)
thread.start()
def replaceBody(self, widget):
old_widget,_ = self.frame.contents.pop()
if old_widget != None:
self.clearSignals(old_widget)
self.frame.contents.append((widget, self.frame.options()))
self.frame.focus_position = 0
if self.loop != None:
self.loop.draw_screen()
def showMainLoading(self):
text = urwid.Text(('loading', '[loading...]'))
self.replaceBody(urwid.Filler(text))
def showMyTasks(self, workspace_id):
self.state['view'] = 'atm'
self.state['id'] = workspace_id
self.state['workspace_id'] = workspace_id
self.showMainLoading()
def runInThread():
tasks = self.allMyTasks(workspace_id)
update(tasks)
def update(tasks):
task_list = ui.TaskList(tasks)
self.connectTaskListSignals(task_list)
self.replaceBody(task_list)
thread = Thread(target=runInThread)
thread.start()
def showProject(self, project_id):
if project_id == None:
return self.showMyTasks(self.state['workspace_id'])
self.state['view'] = 'project'
self.state['id'] = project_id
self.showMainLoading()
def runInThread():
tasks = self.projectTasks(project_id)
update(tasks)
def update(tasks):
task_list = ui.TaskList(tasks)
self.connectTaskListSignals(task_list)
self.replaceBody(task_list)
thread = Thread(target=runInThread)
thread.start()
def showProjectList(self, workspace_id):
self.state['view'] = 'workspace'
self.state['id'] = workspace_id
self.state['workspace_id'] = workspace_id
self.workspace_id = workspace_id
self.showMainLoading()
def runInThread():
projects = self.allMyProjects()
update(projects)
def update(projects):
project_list = ui.ProjectList(projects)
urwid.connect_signal(project_list, 'loadproject', self.showProject)
self.replaceBody(project_list)
thread = Thread(target=runInThread)
thread.start()
def showDetails(self, task_id, show_loading=True):
self.state['view'] = 'details'
self.state['id'] = task_id
if show_loading:
self.showMainLoading()
def runInThread():
task = self.client.tasks.find_by_id(task_id)
stories = self.client.stories.find_by_task(task_id)
subtasks = self.client.tasks.subtasks(task_id)
update(task, stories, subtasks)
def update(task, stories, subtasks):
task_details = ui.TaskDetails(task, stories, subtasks)
self.connectDetailsSignals(task_details)
self.replaceBody(task_details)
thread = Thread(target=runInThread)
thread.start()
def registerSignals(self):
urwid.register_signal(ui.TaskList, [
'complete',
'newtask',
'updatetask',
'details',
])
urwid.register_signal(ui.TaskEdit, [
'complete',
'newtask',
'updatetask',
'details',
])
urwid.register_signal(ui.TaskDetails, [
'comment',
'loadproject',
'updatedescription',
'updatetask',
'usertypeahead',
'assigntask',
'complete',
'newtask',
'details',
])
urwid.register_signal(ui.AssigneeTypeAhead, [
'usertypeahead',
'assigntask',
])
urwid.register_signal(ui.CommentEdit, ['comment'])
urwid.register_signal(ui.DescriptionEdit, ['updatedescription'])
urwid.register_signal(ui.TaskNameEdit, 'updatetask')
urwid.register_signal(ui.WorkspaceMenu, 'click')
urwid.register_signal(ui.ProjectList, 'loadproject')
def clearSignals(self, widget):
urwid.disconnect_signal(widget, 'complete', self.completeTask)
urwid.disconnect_signal(widget, 'newtask', self.newTask)
urwid.disconnect_signal(widget, 'updatetask', self.updateTask)
urwid.disconnect_signal(widget, 'details', self.showDetails)
urwid.disconnect_signal(widget, 'updatedescription',
self.updateDescription)
urwid.disconnect_signal(widget, 'updatetask', self.updateTask)
urwid.disconnect_signal(widget, 'usertypeahead', self.userTypeAhead)
def connectTaskListSignals(self, task_list):
urwid.connect_signal(task_list, 'complete', self.completeTask)
urwid.connect_signal(task_list, 'newtask', self.newTask)
urwid.connect_signal(task_list, 'updatetask', self.updateTask)
urwid.connect_signal(task_list, 'details', self.showDetails)
def connectDetailsSignals(self, task_details):
urwid.connect_signal(task_details, 'comment', self.addComment)
urwid.connect_signal(task_details, 'loadproject', self.showProject)
urwid.connect_signal(task_details, 'updatedescription',
self.updateDescription)
urwid.connect_signal(task_details, 'updatetask', self.updateTask)
urwid.connect_signal(task_details, 'usertypeahead', self.userTypeAhead)
urwid.connect_signal(task_details, 'assigntask', self.assignTask)
urwid.connect_signal(task_details, 'complete', self.completeTask)
urwid.connect_signal(task_details, 'newtask', self.newTask)
urwid.connect_signal(task_details, 'details', self.showDetails)
def handleInput(self, key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()
def render(self):
urwid.set_encoding("UTF-8")
self.registerSignals()
workspace_menu = ui.WorkspaceMenu(self.myWorkspaces())
urwid.connect_signal(workspace_menu, 'click', self.showProjectList)
self.frame = urwid.Pile([
('pack', urwid.AttrMap(workspace_menu, 'workspace')),
None
])
if self.state['view'] == 'workspace':
self.showProjectList(self.state['id'])
elif self.state['view'] == 'project':
self.showProject(self.state['id'])
elif self.state['view'] == 'atm':
self.showMyTasks(self.state['id'])
elif self.state['view'] == 'details':
self.showDetails(self.state['id'])
else:
raise KeyError
self.loop = urwid.MainLoop(self.frame,
unhandled_input=self.handleInput,
palette=ui.palette
)
self.loop.run()
def main():
cmdasana = CmdAsana()
cmdasana.loadState()
cmdasana.render()
cmdasana.saveState()
if __name__ == "__main__": main()

131
main.py Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
import json
import os
import asana
from asana.session import AsanaOAuth2Session
import urwid
import secrets
from ui.auth import AuthPrompt
from ui.constants import palette
from ui.ui import Ui, loading
from asana_service import AsanaService
class NotAuthedException(Exception):
def __init__(self):
super(NotAuthedException, self)
class CmdAsana(object):
loop = None
nav_stack = []
"""Try to get an Asana client using stored tokens
Raises:
NotAuthedException: the user has not authorized the app
"""
def get_client(self):
try:
f = open('.oauth', 'r')
auth_json = f.read()
f.close()
token = json.loads(auth_json)
self.client = asana.Client.oauth(
client_id=secrets.CLIENT_ID,
client_secret=secrets.CLIENT_SECRET,
token=token,
token_updater=self.save_token,
auto_refresh_url=AsanaOAuth2Session.token_url,
auto_refresh_kwargs={
'client_id': secrets.CLIENT_ID,
'client_secret': secrets.CLIENT_SECRET
}
)
except IOError:
raise NotAuthedException()
def authorize(self):
self.client = asana.Client.oauth(
client_id=secrets.CLIENT_ID,
client_secret=secrets.CLIENT_SECRET,
redirect_uri='urn:ietf:wg:oauth:2.0:oob',
token_updater=self.save_token,
auto_refresh_url=AsanaOAuth2Session.token_url,
auto_refresh_kwargs={
'client_id': secrets.CLIENT_ID,
'client_secret': secrets.CLIENT_SECRET
}
)
(url, _) = self.client.session.authorization_url()
auth = AuthPrompt(url, self.auth_callback)
try:
import webbrowser
webbrowser.open(url)
except Exception:
pass
self.loop = urwid.MainLoop(
auth.component(),
unhandled_input=self.exit_handler,
palette=palette
)
self.loop.run()
def auth_callback(self, code):
self.save_token(
self.client.session.fetch_token(code=code))
raise urwid.ExitMainLoop()
def exit_handler(self, key):
if key in ('q', 'Q', 'esc'):
raise urwid.ExitMainLoop()
if key == 'backspace':
self.ui.go_back()
def get_asana_service(self):
self.asana_service = AsanaService(self.client)
def get_ui(self):
self.ui = Ui(self.asana_service, self.update)
def run(self):
self.placeholder = urwid.WidgetPlaceholder(loading())
self.loop = urwid.MainLoop(
self.placeholder,
unhandled_input=self.exit_handler,
palette=palette
)
self.ui.my_tasks()
def update(self, widget):
self.loop.widget.original_widget = widget
try:
self.loop.draw_screen()
except Exception:
self.loop.run()
def save_token(self, token):
f = open('.oauth', 'w')
f.write(json.dumps(token))
f.close()
os.chmod('.oauth', 0o600)
def main():
cmd_asana = CmdAsana()
try:
cmd_asana.get_client()
except NotAuthedException:
cmd_asana.authorize()
cmd_asana.get_asana_service()
cmd_asana.get_ui()
cmd_asana.run()
if __name__ == "__main__":
main()

228
models/models.py Normal file
View File

@@ -0,0 +1,228 @@
from datetime import timezone
import dateutil.parser
from html.parser import HTMLParser
import sys
class AsanaObject(object):
def __init__(self, object_dict):
self.object_dict = object_dict
def id(self):
return self.object_dict['id']
def name(self):
if 'name' in self.object_dict:
return self.object_dict['name']
else:
return ''
class User(AsanaObject):
def workspaces(self):
return [AsanaObject(w) for w in self.object_dict['workspaces']]
class Task(AsanaObject):
def name(self):
if self.object_dict['completed']:
return '%s' % super(Task, self).name()
return super(Task, self).name()
def assignee(self):
if 'assignee' in self.object_dict and self.object_dict['assignee']:
return User(self.object_dict['assignee'])
else:
return None
def atm_section(self):
return self.object_dict['assignee_status']
def description(self):
if 'html_notes' in self.object_dict:
parser = HTMLTextParser()
parser.feed(self.object_dict['html_notes'])
parser.close()
text = parser.get_formatted_text()
if (len(text) > 0):
return text
else:
return ""
elif 'notes' in self.object_dict:
return self.object_dict['notes']
else:
return ""
def due_date(self):
if 'due_at' in self.object_dict and self.object_dict['due_at']:
datetime = dateutil.parser.parse(self.object_dict['due_at'])
datetime = datetime.replace(tzinfo=timezone.utc).astimezone(tz=None)
return datetime.strftime('%b %d, %Y %H:%M')
elif 'due_on' in self.object_dict and self.object_dict['due_on']:
date = dateutil.parser.parse(self.object_dict['due_on'])
return date.strftime('%b %d, %Y')
else:
return 'no due date'
def parent(self):
if 'parent' in self.object_dict and self.object_dict['parent']:
return Task(self.object_dict['parent'])
else:
return None
def projects(self):
if 'projects' in self.object_dict:
return [Project(p) for p in self.object_dict['projects']]
else:
return []
def subtasks(self):
if 'subtasks' in self.object_dict:
return [Task(t) for t in self.object_dict['subtasks']]
else:
return []
def custom_fields(self):
if 'custom_fields' in self.object_dict:
return [CustomField(c) for c in self.object_dict['custom_fields']]
else:
return []
class Project(AsanaObject):
def description(self):
if 'notes' in self.object_dict:
return self.object_dict['notes']
else:
return ''
class CustomField(AsanaObject):
def string_value(self):
if 'text_value' in self.object_dict:
return str(self.object_dict['text_value'])
elif 'number_value' in self.object_dict:
return str(self.object_dict['number_value'])
elif 'enum_value' in self.object_dict and self.object_dict['enum_value']:
enum_value = AsanaObject(self.object_dict['enum_value'])
return str(enum_value.name())
return ''
class Strong(object):
def __init__(self, body):
self.body = body
def text_format(self):
return ('strong', self.body.text_format())
class Italic(object):
def __init__(self, body):
self.body = body
def text_format(self):
return ('italic', self.body.text_format())
class Underline(object):
def __init__(self, body):
self.body = body
def text_format(self):
return ('underline', self.body.text_format())
class Link(object):
def __init__(self, body):
self.body = body
def text_format(self):
return ('link', self.body.text_format())
class Tag(object):
def __init__(self, body):
self.body = body
def text_format(self):
return self.body.text_format()
class List(object):
def __init__(self, body):
self.body = body
def text_format(self):
return self.body.text_format()
class ListItem(object):
def __init__(self, body, indent):
self.body = body
self.indent = indent
def text_format(self):
return ('', [(' ' * self.indent), '', self.body.text_format(), '\n'])
class Text(object):
def __init__(self, body):
self.body = body
def text_format(self):
return self.body
class HTMLTextParser(HTMLParser):
def __init__(self):
self.text = []
self.tag_stack = []
self.indent = 0
super().__init__()
def handle_starttag(self, tag, attrs):
if tag == 'strong':
self.tag_stack.append(Strong)
elif tag == 'em':
self.tag_stack.append(Italic)
elif tag == 'u':
self.tag_stack.append(Underline)
elif tag == 'a':
self.tag_stack.append(Link)
elif tag == 'ul' or tag == 'ol':
self.indent += 2
self.tag_stack.append(List)
elif tag == 'li':
self.tag_stack.append(ListItem)
else:
self.tag_stack.append(Tag)
def handle_data(self, data):
self.text.append(Text(data))
def handle_endtag(self, tag):
data = self.text.pop() if len(self.text) > 0 else Text("")
Class = self.tag_stack.pop()
if tag == 'ul' or tag =='ol':
self.indent -= 2
if tag == 'li':
self.text.append(Class(data, self.indent))
else:
self.text.append(Class(data))
def get_formatted_text(self):
formatted = [t.text_format() for t in self.text]
return formatted
class Story(AsanaObject):
def creator(self):
if 'created_by' in self.object_dict:
return self.object_dict['created_by']['name'] + ' '
else:
return ''
def created_at(self):
if 'created_at' in self.object_dict:
return dateutil.parser.parse(self.object_dict['created_at']) \
.replace(tzinfo=timezone.utc).astimezone(tz=None)
else:
return ''
def text(self):
if 'html_text' in self.object_dict:
parser = HTMLTextParser()
parser.feed(self.object_dict['html_text'])
parser.close()
return parser.get_formatted_text()
else:
return [self.object_dict['text']]

Submodule python-asana deleted from 66c31afc60

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
asana==0.8.2
python-dateutil==2.8.0
urwid==2.0.1

365
ui.py
View File

@@ -1,365 +0,0 @@
# -*- coding: latin-1 -*-
import urwid
import sys
# TaskEdit modes
EDIT = 'edit'
LIST = 'list'
palette = [
('selected', 'standout', ''),
('selected workspace', 'standout,bold', ''),
('header', 'bold,light green', ''),
('secondary', 'light gray', ''),
('task', 'light green', ''),
('project', 'yellow', ''),
('section', 'white', 'dark green'),
('workspace', 'white', 'dark blue'),
('pager', 'standout', ''),
]
class WorkspaceMenu(urwid.Columns):
def __init__(self, workspaces):
super(WorkspaceMenu, self).__init__([], dividechars=1)
for workspace in workspaces:
button = WorkspaceButton(workspace, self.loadWorkspace)
self.contents.append((urwid.AttrMap(button,
None,
focus_map='selected workspace'),
self.options('given', 24)))
def keypress(self, size, key):
if key == 'j':
key = 'down'
elif key == 'h':
key = 'left'
elif key == 'l':
key = 'right'
return super(WorkspaceMenu, self).keypress(size, key)
def loadWorkspace(self, widget, workspace_id):
urwid.emit_signal(self, 'click', workspace_id)
class WorkspaceButton(urwid.Button):
def __init__(self, workspace, onClick):
super(WorkspaceButton, self).__init__(workspace['name'])
urwid.connect_signal(self, 'click', onClick, workspace['id'])
class PagerButton(urwid.Button):
def __init__(self, loadPage):
super(PagerButton, self).__init__(('pager', 'load more'))
urwid.connect_signal(self, 'click', loadPage)
class TypeAheadButton(urwid.Button):
def __init__(self, item, onClick):
super(TypeAheadButton, self).__init__(item['name'])
urwid.connect_signal(self, 'click', onClick, item)
class ProjectIcon(urwid.SelectableIcon):
def __init__(self, project, onClick):
self.project = project
self.onClick = onClick
super(ProjectIcon, self).__init__(project['name'])
def keypress(self, size, key):
if key in ('enter', 'right', 'l'):
self.onClick(self.project['id'])
else:
return super(ProjectIcon, self).keypress(size, key)
class ProjectList(urwid.ListBox):
def __init__(self, projects):
self.projects = projects
body = urwid.SimpleFocusListWalker(
[urwid.AttrMap(ProjectIcon({'name': 'My Tasks', 'id': None},
self.loadProject), 'project'),
None]
)
super(ProjectList, self).__init__(body)
self.loadPage()
def loadPage(self, opt=None):
self.body.pop()
for i in range(50):
try:
self.body.append(urwid.AttrMap(ProjectIcon(self.projects.next(),
self.loadProject), 'project'))
except StopIteration:
return
self.body.append(PagerButton(self.loadPage))
def keypress(self, size, key):
if key == 'j':
key = 'down'
elif key == 'k':
key = 'up'
return super(ProjectList, self).keypress(size, key)
def loadProject(self, project_id):
urwid.emit_signal(self, 'loadproject', project_id)
class TaskList(urwid.ListBox):
def __init__(self, tasks):
self.tasks = tasks
task_widgets = urwid.Pile(
[TaskEdit(task) for task in tasks]
)
body = urwid.SimpleFocusListWalker([])
for task_widget,_ in task_widgets.contents:
self.connectSignals(task_widget)
style = 'section' if len(task_widget.task['name']) and \
task_widget.task['name'][-1] == ':' else 'task'
body.append(urwid.AttrMap(task_widget, style, focus_map='selected'))
super(TaskList, self).__init__(body)
def insertNewTask(self, task):
task_widget = TaskEdit(task, mode=EDIT)
self.connectSignals(task_widget)
index = self.focus_position + 1
self.body.insert(index,
urwid.AttrMap(task_widget, 'task',
focus_map='selected'))
self.focus_position += 1
def connectSignals(self, task_widget):
urwid.connect_signal(task_widget, 'complete', self.completeTask)
urwid.connect_signal(task_widget, 'newtask', self.newTask)
urwid.connect_signal(task_widget, 'updatetask', self.updateTask)
urwid.connect_signal(task_widget, 'details', self.details)
def completeTask(self, task_id):
urwid.emit_signal(self, 'complete', task_id)
del self.body[self.focus_position]
def newTask(self, task_after_id=None):
urwid.emit_signal(self, 'newtask', task_after_id)
def updateTask(self, task_id, name):
urwid.emit_signal(self, 'updatetask', task_id, name)
def details(self, task_id):
urwid.emit_signal(self, 'details', task_id)
def keypress(self, size, key):
# The ListBox will handle scrolling for us, so we trick it into thinking
# it's being passed arrow keys
if self.focus.original_widget.mode == LIST:
if key == 'j':
key = 'down'
elif key == 'k':
key = 'up'
key = super(TaskList, self).keypress(size, key)
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()
else:
return key
class TaskEdit(urwid.Edit):
completed = False
def __init__(self, task, mode=LIST):
self.task = task
self.mode = mode
super(TaskEdit, self).__init__(task["name"])
def keypress(self, size, key):
if self.mode == EDIT:
key = super(TaskEdit, self).keypress(size, key)
if key in ('esc', 'up', 'down'):
self.mode = LIST
self.set_caption(self.edit_text)
self.set_edit_text('')
urwid.emit_signal(self, 'updatetask', self.task['id'],
self.caption)
if key != 'esc':
return key
else:
if key == 'i':
if self.completed:
return
self.mode = EDIT
self.set_edit_text(self.caption)
self.set_caption('')
elif key == 'tab':
urwid.emit_signal(self, 'complete', self.task['id'])
elif key == 'enter':
urwid.emit_signal(self, 'newtask', self.task['id'])
elif key in ('l', 'right'):
urwid.emit_signal(self, 'details', self.task['id'])
else:
return key
class CommentEdit(urwid.Edit):
def __init__(self, task):
self.task = task
super(CommentEdit, self).__init__(('secondary', u'Add a comment:\n'))
def keypress(self, size, key):
if key != 'enter':
return super(CommentEdit, self).keypress(size, key)
urwid.emit_signal(self, 'comment', self.task['id'], self.edit_text)
class TaskNameEdit(urwid.Edit):
def __init__(self, task):
self.task = task
super(TaskNameEdit, self).__init__(('secondary',
u'#' + str(task['id']) + ' '),
task['name'])
def keypress(self, size, key):
if key in ('enter', 'esc', 'up', 'down'):
if (self.edit_text != self.task['name']):
urwid.emit_signal(self, 'updatetask', self.task['id'],
self.edit_text)
return super(TaskNameEdit, self).keypress(size, key)
class DescriptionEdit(urwid.Edit):
def __init__(self, task):
self.task = task
super(DescriptionEdit, self).__init__(('secondary', u'Description:\n'),
task['notes'],
multiline=True)
def keypress(self, size, key):
if key != 'esc':
return super(DescriptionEdit, self).keypress(size, key)
urwid.emit_signal(self, 'updatedescription', self.task['id'],
self.edit_text)
class AssigneeTypeAhead(urwid.Pile):
def __init__(self, task):
self.task = task
if task['assignee'] != None:
assignee = task['assignee']['name']
else:
assignee = ""
self.edit = urwid.Edit('Assignee: ', assignee)
urwid.connect_signal(self.edit, 'change', self.typeAhead)
body = [('pack', self.edit)]
super(AssigneeTypeAhead, self).__init__(body)
def typeAhead(self, widget, text):
urwid.emit_signal(self, 'usertypeahead', text, self.updateTypeAhead)
def updateTypeAhead(self, users):
users = [(TypeAheadButton(u, self.assign), ('pack', None)) for u in users]
users.insert(0, self.contents[0])
self.contents = users
def assign(self, widget, user):
urwid.emit_signal(self, 'assigntask', self.task['id'], user['id'])
self.contents = [self.contents[0]]
self.edit.set_edit_text(user['name'])
class TaskDetails(urwid.ListBox):
def __init__(self, task, stories, subtasks):
self.task = task
self.stories = stories
comment_edit = CommentEdit(task)
urwid.connect_signal(comment_edit, 'comment', self.comment)
self.description_edit = DescriptionEdit(task)
urwid.connect_signal(self.description_edit, 'updatedescription',
self.updateDescription)
task_name_edit = TaskNameEdit(task)
urwid.connect_signal(task_name_edit, 'updatetask', self.updateTask)
assignee_type_ahead = AssigneeTypeAhead(task)
urwid.connect_signal(assignee_type_ahead, 'usertypeahead',
self.userTypeAhead)
urwid.connect_signal(assignee_type_ahead, 'assigntask', self.assignTask)
projects = [urwid.AttrMap(ProjectIcon(project, self.loadProject),
'project')
for project in task['projects']]
if task['parent'] != None:
parent = TaskEdit(task['parent'])
urwid.connect_signal(parent, 'updatetask', self.updateSubtask)
urwid.connect_signal(parent, 'details', self.showDetails)
#Remap enter to load details of parent
urwid.connect_signal(parent, 'newtask', self.showDetails)
projects.append(parent)
all_subtasks = [t for t in subtasks]
subtask_list = TaskList(all_subtasks)
urwid.connect_signal(subtask_list, 'complete', self.completeTask)
urwid.connect_signal(subtask_list, 'newtask', self.newTask)
urwid.connect_signal(subtask_list, 'updatetask', self.updateSubtask)
urwid.connect_signal(subtask_list, 'details', self.showDetails)
body = projects + \
[
urwid.Divider('='),
task_name_edit,
assignee_type_ahead,
urwid.Divider('-'),
self.description_edit,
urwid.Divider('-'),
urwid.BoxAdapter(subtask_list, len(all_subtasks)),
urwid.Divider('-'),
] + \
[urwid.Text('[' + story['created_by']['name'] + '] ' + \
story['text']) for story in stories] + \
[comment_edit]
super(TaskDetails, self).__init__(body)
def completeTask(self, task_id):
urwid.emit_signal(self, 'complete', task_id)
del self.body[self.focus_position]
def newTask(self, task_after_id=None):
urwid.emit_signal(self, 'newtask', task_after_id)
def updateSubtask(self, task_id, name):
urwid.emit_signal(self, 'updatetask', task_id, name)
def showDetails(self, task_id):
urwid.emit_signal(self, 'details', task_id)
def keypress(self, size, key):
key = super(TaskDetails, self).keypress(size, key)
if self.focus != self.description_edit and \
self.description_edit.edit_text != self.task['notes']:
self.updateDescription(self.task['id'],
self.description_edit.edit_text)
return key
def comment(self, task_id, comment):
urwid.emit_signal(self, 'comment', task_id, comment)
def updateDescription(self, task_id, description):
urwid.emit_signal(self, 'updatedescription', task_id, description)
def updateTask(self, task_id, name):
urwid.emit_signal(self, 'updatetask', task_id, name)
def loadProject(self, project_id):
urwid.emit_signal(self, 'loadproject', project_id)
def userTypeAhead(self, text, callback):
urwid.emit_signal(self, 'usertypeahead', text, callback)
def assignTask(self, task_id, user_id):
urwid.emit_signal(self, 'assigntask', task_id, user_id)

0
ui/__init__.py Normal file
View File

39
ui/auth.py Normal file
View File

@@ -0,0 +1,39 @@
import urwid
"""
Input box that accepts OAuth tokens
"""
class TokenEdit(urwid.Edit):
def __init__(self):
urwid.register_signal(TokenEdit, 'TokenEdit-changed')
prompt = ('seondary', u' Authorization Token: ')
super(TokenEdit, self).__init__(prompt, '')
def keypress(self, size, key):
if key == 'enter':
urwid.emit_signal(self, 'TokenEdit-changed', self.edit_text)
else:
return super(TokenEdit, self).keypress(size, key)
class AuthPrompt(object):
def __init__(self, auth_url, callback):
self.callback = callback
token_input = TokenEdit()
urwid.connect_signal(token_input, 'TokenEdit-changed', self.callback)
self.frame = urwid.Filler(urwid.Padding(
urwid.Pile([
urwid.Text('Visit %s and paste the token below.\n' % auth_url),
token_input,
]),
align='center',
width='pack',
left=2,
right=2
))
def callback(self, token):
self.callback(token)
def component(self):
return self.frame

21
ui/constants.py Normal file
View File

@@ -0,0 +1,21 @@
palette = [
('atm_section', 'white,bold', 'dark blue'),
('author', 'bold,dark blue', ''),
('timestamp', 'underline', ''),
('custom_fields', 'dark red', ''),
('header', 'bold,light green', ''),
('project', 'yellow', ''),
('section', 'dark green,bold', ''),
('selected', 'standout', ''),
('task', 'light green', ''),
('text', '', ''),
('strong', 'bold', ''),
('underline', 'underline', ''),
('link', 'underline,light blue', ''),
('italic', 'italics', ''),
('workspace', 'white', 'dark blue'),
]
keys = {
'select': ['enter', 'space']
}

130
ui/task_details.py Normal file
View File

@@ -0,0 +1,130 @@
import urwid
from datetime import date, datetime
from ui.task_list import TaskRow
class TaskDetails(object):
def __init__(self, task, stories, on_subtask_click, on_project_click,
on_comment, on_assignee_click, on_due_date_click):
self.task = task
self.on_subtask_click = on_subtask_click,
self.on_project_click = on_project_click,
self.on_comment = on_comment
body = [
urwid.Text(('task', task.name())),
urwid.Divider(''),
Memberships(task, on_subtask_click, on_project_click).component(),
urwid.Divider(''),
Assignee(task, on_assignee_click).component(),
DueDate(task, on_due_date_click).component(),
CustomFields(task).component(),
urwid.Divider(''),
urwid.Text(task.description()),
urwid.Divider(''),
]
if task.subtasks():
body.append(urwid.Pile([
TaskRow(t, on_subtask_click) for t in task.subtasks()
]))
stories = list(stories)
if (len(stories) > 0):
body = body + [
urwid.Divider('-'),
Stories(stories).component()
]
self.details = urwid.ListBox(urwid.SimpleFocusListWalker(body))
def component(self):
return self.details
class Assignee(object):
def __init__(self, task, on_click):
if task.assignee():
assignee = task.assignee().name()
else:
assignee = "unassigned"
self.assignee = urwid.SelectableIcon([('strong', 'Assignee: '), ('', assignee)])
self.on_click = on_click
#urwid.connect_signal(self.assignee, 'keypress', self.on_keypress)
def component(self):
return self.assignee
def on_keypress(self, size, key):
if key == "enter":
self.on_click()
else:
return key
class DueDate(object):
def __init__(self, task, on_click):
due_date = task.due_date()
self.due_date = urwid.SelectableIcon([('strong', 'Due: '), ('', str(task.due_date()))])
self.on_click = on_click
#urwid.connect_signal(self.due_date, 'keypress', self.on_keypress)
def component(self):
return self.due_date
def on_keypress(self, size, key):
if key == "enter":
self.on_click()
else:
return key
class Memberships(object):
def __init__(self, task, on_subtask_click, on_project_click):
self.on_project_click = on_project_click
components = [self.membership(p.name(), p.id()) for p in task.projects()]
if task.parent():
components.append(urwid.Button(
('task', 'Subtask of: %s' % task.parent().name()),
on_press = lambda x: on_subtask_click(task.parent().id())
))
self.memberships = urwid.Pile(components)
def membership(self, name, id):
return urwid.Button(('project', name),
on_press = lambda x: self.on_project_click(id)
)
def component(self):
return self.memberships
class CustomFields(object):
def __init__(self, task):
components = [urwid.Text([
('custom_fields', f.name() + ': '),
f.string_value()
]) for f in task.custom_fields()]
self.custom_fields = urwid.Pile(components)
def component(self):
return self.custom_fields
class Stories(object):
def __init__(self, stories):
components = [
urwid.Text([
('timestamp', s.created_at().strftime('%Y-%m-%d %H:%M')),
' ',
('author', s.creator()),
] + s.text())
for s in stories]
self.stories = urwid.Pile(components)
def component(self):
return self.stories

60
ui/task_list.py Normal file
View File

@@ -0,0 +1,60 @@
import urwid
from ui.constants import keys
class TaskList(object):
def __init__(self, tasks, header, on_task_click):
self.callback = on_task_click
self.grid = urwid.Frame(
urwid.ListBox(
urwid.SimpleFocusListWalker(
[TaskRow(t, self.callback) for t in tasks]
)
),
header=urwid.Text(('header', header)),
focus_part='body'
)
def component(self):
return self.grid
class MyTasks(object):
def __init__(self, tasks, on_task_click):
self.callback = on_task_click
all_tasks = [t for t in tasks]
self.new = [t for t in all_tasks if t.atm_section() == 'inbox']
self.today = [t for t in all_tasks if t.atm_section() == 'today']
self.upcoming = [t for t in all_tasks if t.atm_section() == 'upcoming']
self.later = [t for t in all_tasks if t.atm_section() == 'later']
def component(self):
return urwid.Frame(
urwid.ListBox(
urwid.SimpleFocusListWalker([
urwid.Text(('atm_section', 'New'))
] + [TaskRow(t, self.callback) for t in self.new] + [
urwid.Text(('atm_section', 'Today'))
] + [TaskRow(t, self.callback) for t in self.today] + [
urwid.Text(('atm_section', 'Upcoming'))
] + [TaskRow(t, self.callback) for t in self.upcoming] + [
urwid.Text(('atm_section', 'Later'))
] + [TaskRow(t, self.callback) for t in self.later]
)
),
header=urwid.Text(('header', 'My Tasks')),
focus_part='body'
)
class TaskRow(urwid.SelectableIcon):
def __init__(self, task, on_click):
self.on_click = on_click
self.task = task
style = 'section' if task.name() and task.name()[-1] == ':' else 'task'
super(TaskRow, self).__init__((style, task.name()))
def keypress(self, size, key):
if key in keys['select']:
self.on_click(self.task.id())
else:
return key

73
ui/ui.py Normal file
View File

@@ -0,0 +1,73 @@
import urwid
from threading import Thread
from asana_service import AsanaService
from ui.task_list import MyTasks, TaskList
from ui.task_details import TaskDetails
class Ui(object):
nav_stack = []
def __init__(self, asana_service, update):
self.asana_service = asana_service
self.update = update
def my_tasks(self):
self.nav_stack.append(('mytasks', None))
def runInThread():
tasks = self.asana_service.get_my_tasks()
self.update(MyTasks(tasks, self.task_details).component())
thread = Thread(target=runInThread())
thread.start()
def task_details(self, id):
self.nav_stack.append(('task', id))
def runInThread():
task = self.asana_service.get_task(id)
stories = self.asana_service.get_stories(id)
self.update(TaskDetails(task,
stories,
self.task_details,
self.task_list,
None, None, None).component())
thread = Thread(target=runInThread())
thread.start()
def task_list(self, id):
self.nav_stack.append(('project', id))
def runInThread():
project = self.asana_service.get_project(id)
tasks = self.asana_service.get_tasks(id)
self.update(TaskList(tasks,
project.name(),
self.task_details
).component())
thread = Thread(target=runInThread())
thread.run()
def go_back(self):
if len(self.nav_stack) < 2:
return
self.nav_stack.pop()
(location, id) = self.nav_stack.pop()
if location == 'mytasks':
self.my_tasks()
elif location == 'task':
self.task_details(id)
elif location == 'project':
self.task_list(id)
def loading():
return urwid.Overlay(
urwid.BigText('Loading...', urwid.font.HalfBlock5x4Font()),
urwid.SolidFill('#'),
'center',
'pack',
'middle',
'pack'
)

View File

@@ -1,78 +0,0 @@
#!/usr/bin/python
#
# Urwid __init__.py - all the stuff you're likely to care about
#
# Copyright (C) 2004-2012 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from urwid.version import VERSION, __version__
from urwid.widget import (FLOW, BOX, FIXED, LEFT, RIGHT, CENTER, TOP, MIDDLE,
BOTTOM, SPACE, ANY, CLIP, PACK, GIVEN, RELATIVE, RELATIVE_100, WEIGHT,
WidgetMeta,
WidgetError, Widget, FlowWidget, BoxWidget, fixed_size, FixedWidget,
Divider, SolidFill, TextError, Text, EditError, Edit, IntEdit,
delegate_to_widget_mixin, WidgetWrapError, WidgetWrap)
from urwid.decoration import (WidgetDecoration, WidgetPlaceholder,
AttrMapError, AttrMap, AttrWrap, BoxAdapterError, BoxAdapter, PaddingError,
Padding, FillerError, Filler, WidgetDisable)
from urwid.container import (GridFlowError, GridFlow, OverlayError, Overlay,
FrameError, Frame, PileError, Pile, ColumnsError, Columns,
WidgetContainerMixin)
from urwid.wimp import (SelectableIcon, CheckBoxError, CheckBox, RadioButton,
Button, PopUpLauncher, PopUpTarget)
from urwid.listbox import (ListWalkerError, ListWalker, PollingListWalker,
SimpleListWalker, SimpleFocusListWalker, ListBoxError, ListBox)
from urwid.graphics import (BigText, LineBox, BarGraphMeta, BarGraphError,
BarGraph, GraphVScale, ProgressBar, scale_bar_values)
from urwid.canvas import (CanvasCache, CanvasError, Canvas, TextCanvas,
BlankCanvas, SolidCanvas, CompositeCanvas, CanvasCombine, CanvasOverlay,
CanvasJoin)
from urwid.font import (get_all_fonts, Font, Thin3x3Font, Thin4x3Font,
HalfBlock5x4Font, HalfBlock6x5Font, HalfBlockHeavy6x5Font, Thin6x6Font,
HalfBlock7x7Font)
from urwid.signals import (MetaSignals, Signals, emit_signal, register_signal,
connect_signal, disconnect_signal)
from urwid.monitored_list import MonitoredList, MonitoredFocusList
from urwid.command_map import (CommandMap, command_map,
REDRAW_SCREEN, CURSOR_UP, CURSOR_DOWN, CURSOR_LEFT, CURSOR_RIGHT,
CURSOR_PAGE_UP, CURSOR_PAGE_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT,
ACTIVATE)
from urwid.main_loop import (ExitMainLoop, MainLoop, SelectEventLoop,
GLibEventLoop, TornadoEventLoop, AsyncioEventLoop)
try:
from urwid.main_loop import TwistedEventLoop
except ImportError:
pass
from urwid.text_layout import (TextLayout, StandardTextLayout, default_layout,
LayoutSegment)
from urwid.display_common import (UPDATE_PALETTE_ENTRY, DEFAULT, BLACK,
DARK_RED, DARK_GREEN, BROWN, DARK_BLUE, DARK_MAGENTA, DARK_CYAN,
LIGHT_GRAY, DARK_GRAY, LIGHT_RED, LIGHT_GREEN, YELLOW, LIGHT_BLUE,
LIGHT_MAGENTA, LIGHT_CYAN, WHITE, AttrSpecError, AttrSpec, RealTerminal,
ScreenError, BaseScreen)
from urwid.util import (calc_text_pos, calc_width, is_wide_char,
move_next_char, move_prev_char, within_double_byte, detected_encoding,
set_encoding, get_encoding_mode, apply_target_encoding, supports_unicode,
calc_trim_text, TagMarkupException, decompose_tagmarkup, MetaSuper,
int_scale, is_mouse_event)
from urwid.treetools import (TreeWidgetError, TreeWidget, TreeNode,
ParentNode, TreeWalker, TreeListBox)
from urwid.vterm import (TermModes, TermCharset, TermScroller, TermCanvas,
Terminal)
from urwid import raw_display

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +0,0 @@
#!/usr/bin/python
#
# Urwid CommandMap class
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
REDRAW_SCREEN = 'redraw screen'
CURSOR_UP = 'cursor up'
CURSOR_DOWN = 'cursor down'
CURSOR_LEFT = 'cursor left'
CURSOR_RIGHT = 'cursor right'
CURSOR_PAGE_UP = 'cursor page up'
CURSOR_PAGE_DOWN = 'cursor page down'
CURSOR_MAX_LEFT = 'cursor max left'
CURSOR_MAX_RIGHT = 'cursor max right'
ACTIVATE = 'activate'
class CommandMap(object):
"""
dict-like object for looking up commands from keystrokes
Default values (key: command)::
'tab': 'next selectable',
'ctrl n': 'next selectable',
'shift tab': 'prev selectable',
'ctrl p': 'prev selectable',
'ctrl l': 'redraw screen',
'esc': 'menu',
'up': 'cursor up',
'down': 'cursor down',
'left': 'cursor left',
'right': 'cursor right',
'page up': 'cursor page up',
'page down': 'cursor page down',
'home': 'cursor max left',
'end': 'cursor max right',
' ': 'activate',
'enter': 'activate',
"""
_command_defaults = {
'tab': 'next selectable',
'ctrl n': 'next selectable',
'shift tab': 'prev selectable',
'ctrl p': 'prev selectable',
'ctrl l': REDRAW_SCREEN,
'esc': 'menu',
'up': CURSOR_UP,
'down': CURSOR_DOWN,
'left': CURSOR_LEFT,
'right': CURSOR_RIGHT,
'page up': CURSOR_PAGE_UP,
'page down': CURSOR_PAGE_DOWN,
'home': CURSOR_MAX_LEFT,
'end': CURSOR_MAX_RIGHT,
' ': ACTIVATE,
'enter': ACTIVATE,
}
def __init__(self):
self.restore_defaults()
def restore_defaults(self):
self._command = dict(self._command_defaults)
def __getitem__(self, key):
return self._command.get(key, None)
def __setitem__(self, key, command):
self._command[key] = command
def __delitem__(self, key):
del self._command[key]
def clear_command(self, command):
dk = [k for k, v in self._command.items() if v == command]
for k in dk:
del self._command[k]
def copy(self):
"""
Return a new copy of this CommandMap, likely so we can modify
it separate from a shared one.
"""
c = CommandMap()
c._command = dict(self._command)
return c
command_map = CommandMap() # shared command mappings

View File

@@ -1,48 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid python compatibility definitions
# Copyright (C) 2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
import sys
try: # python 2.4 and 2.5 compat
bytes = bytes
except NameError:
bytes = str
PYTHON3 = sys.version_info > (3, 0)
# for iterating over byte strings:
# ord2 calls ord in python2 only
# chr2 converts an ordinal value to a length-1 byte string
# B returns a byte string in all supported python versions
# bytes3 creates a byte string from a list of ordinal values
if PYTHON3:
ord2 = lambda x: x
chr2 = lambda x: bytes([x])
B = lambda x: x.encode('iso8859-1')
bytes3 = bytes
else:
ord2 = ord
chr2 = chr
B = lambda x: x
bytes3 = lambda x: bytes().join([chr(c) for c in x])

File diff suppressed because it is too large Load Diff

View File

@@ -1,619 +0,0 @@
#!/usr/bin/python
#
# Urwid curses output wrapper.. the horror..
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
"""
Curses-based UI implementation
"""
import curses
import _curses
from urwid import escape
from urwid.display_common import BaseScreen, RealTerminal, AttrSpec, \
UNPRINTABLE_TRANS_TABLE
from urwid.compat import bytes, PYTHON3
KEY_RESIZE = 410 # curses.KEY_RESIZE (sometimes not defined)
KEY_MOUSE = 409 # curses.KEY_MOUSE
_curses_colours = {
'default': (-1, 0),
'black': (curses.COLOR_BLACK, 0),
'dark red': (curses.COLOR_RED, 0),
'dark green': (curses.COLOR_GREEN, 0),
'brown': (curses.COLOR_YELLOW, 0),
'dark blue': (curses.COLOR_BLUE, 0),
'dark magenta': (curses.COLOR_MAGENTA, 0),
'dark cyan': (curses.COLOR_CYAN, 0),
'light gray': (curses.COLOR_WHITE, 0),
'dark gray': (curses.COLOR_BLACK, 1),
'light red': (curses.COLOR_RED, 1),
'light green': (curses.COLOR_GREEN, 1),
'yellow': (curses.COLOR_YELLOW, 1),
'light blue': (curses.COLOR_BLUE, 1),
'light magenta': (curses.COLOR_MAGENTA, 1),
'light cyan': (curses.COLOR_CYAN, 1),
'white': (curses.COLOR_WHITE, 1),
}
class Screen(BaseScreen, RealTerminal):
def __init__(self):
super(Screen,self).__init__()
self.curses_pairs = [
(None,None), # Can't be sure what pair 0 will default to
]
self.palette = {}
self.has_color = False
self.s = None
self.cursor_state = None
self._keyqueue = []
self.prev_input_resize = 0
self.set_input_timeouts()
self.last_bstate = 0
self._mouse_tracking_enabled = False
self.register_palette_entry(None, 'default','default')
def set_mouse_tracking(self, enable=True):
"""
Enable mouse tracking.
After calling this function get_input will include mouse
click events along with keystrokes.
"""
enable = bool(enable)
if enable == self._mouse_tracking_enabled:
return
if enable:
curses.mousemask(0
| curses.BUTTON1_PRESSED | curses.BUTTON1_RELEASED
| curses.BUTTON2_PRESSED | curses.BUTTON2_RELEASED
| curses.BUTTON3_PRESSED | curses.BUTTON3_RELEASED
| curses.BUTTON4_PRESSED | curses.BUTTON4_RELEASED
| curses.BUTTON1_DOUBLE_CLICKED | curses.BUTTON1_TRIPLE_CLICKED
| curses.BUTTON2_DOUBLE_CLICKED | curses.BUTTON2_TRIPLE_CLICKED
| curses.BUTTON3_DOUBLE_CLICKED | curses.BUTTON3_TRIPLE_CLICKED
| curses.BUTTON4_DOUBLE_CLICKED | curses.BUTTON4_TRIPLE_CLICKED
| curses.BUTTON_SHIFT | curses.BUTTON_ALT
| curses.BUTTON_CTRL)
else:
raise NotImplementedError()
self._mouse_tracking_enabled = enable
def _start(self):
"""
Initialize the screen and input mode.
"""
self.s = curses.initscr()
self.has_color = curses.has_colors()
if self.has_color:
curses.start_color()
if curses.COLORS < 8:
# not colourful enough
self.has_color = False
if self.has_color:
try:
curses.use_default_colors()
self.has_default_colors=True
except _curses.error:
self.has_default_colors=False
self._setup_colour_pairs()
curses.noecho()
curses.meta(1)
curses.halfdelay(10) # use set_input_timeouts to adjust
self.s.keypad(0)
if not self._signal_keys_set:
self._old_signal_keys = self.tty_signal_keys()
super(Screen, self)._start()
def _stop(self):
"""
Restore the screen.
"""
curses.echo()
self._curs_set(1)
try:
curses.endwin()
except _curses.error:
pass # don't block original error with curses error
if self._old_signal_keys:
self.tty_signal_keys(*self._old_signal_keys)
super(Screen, self)._stop()
def _setup_colour_pairs(self):
"""
Initialize all 63 color pairs based on the term:
bg * 8 + 7 - fg
So to get a color, we just need to use that term and get the right color
pair number.
"""
if not self.has_color:
return
for fg in xrange(8):
for bg in xrange(8):
# leave out white on black
if fg == curses.COLOR_WHITE and \
bg == curses.COLOR_BLACK:
continue
curses.init_pair(bg * 8 + 7 - fg, fg, bg)
def _curs_set(self,x):
if self.cursor_state== "fixed" or x == self.cursor_state:
return
try:
curses.curs_set(x)
self.cursor_state = x
except _curses.error:
self.cursor_state = "fixed"
def _clear(self):
self.s.clear()
self.s.refresh()
def _getch(self, wait_tenths):
if wait_tenths==0:
return self._getch_nodelay()
if wait_tenths is None:
curses.cbreak()
else:
curses.halfdelay(wait_tenths)
self.s.nodelay(0)
return self.s.getch()
def _getch_nodelay(self):
self.s.nodelay(1)
while 1:
# this call fails sometimes, but seems to work when I try again
try:
curses.cbreak()
break
except _curses.error:
pass
return self.s.getch()
def set_input_timeouts(self, max_wait=None, complete_wait=0.1,
resize_wait=0.1):
"""
Set the get_input timeout values. All values have a granularity
of 0.1s, ie. any value between 0.15 and 0.05 will be treated as
0.1 and any value less than 0.05 will be treated as 0. The
maximum timeout value for this module is 25.5 seconds.
max_wait -- amount of time in seconds to wait for input when
there is no input pending, wait forever if None
complete_wait -- amount of time in seconds to wait when
get_input detects an incomplete escape sequence at the
end of the available input
resize_wait -- amount of time in seconds to wait for more input
after receiving two screen resize requests in a row to
stop urwid from consuming 100% cpu during a gradual
window resize operation
"""
def convert_to_tenths( s ):
if s is None:
return None
return int( (s+0.05)*10 )
self.max_tenths = convert_to_tenths(max_wait)
self.complete_tenths = convert_to_tenths(complete_wait)
self.resize_tenths = convert_to_tenths(resize_wait)
def get_input(self, raw_keys=False):
"""Return pending input as a list.
raw_keys -- return raw keycodes as well as translated versions
This function will immediately return all the input since the
last time it was called. If there is no input pending it will
wait before returning an empty list. The wait time may be
configured with the set_input_timeouts function.
If raw_keys is False (default) this function will return a list
of keys pressed. If raw_keys is True this function will return
a ( keys pressed, raw keycodes ) tuple instead.
Examples of keys returned:
* ASCII printable characters: " ", "a", "0", "A", "-", "/"
* ASCII control characters: "tab", "enter"
* Escape sequences: "up", "page up", "home", "insert", "f1"
* Key combinations: "shift f1", "meta a", "ctrl b"
* Window events: "window resize"
When a narrow encoding is not enabled:
* "Extended ASCII" characters: "\\xa1", "\\xb2", "\\xfe"
When a wide encoding is enabled:
* Double-byte characters: "\\xa1\\xea", "\\xb2\\xd4"
When utf8 encoding is enabled:
* Unicode characters: u"\\u00a5", u'\\u253c"
Examples of mouse events returned:
* Mouse button press: ('mouse press', 1, 15, 13),
('meta mouse press', 2, 17, 23)
* Mouse button release: ('mouse release', 0, 18, 13),
('ctrl mouse release', 0, 17, 23)
"""
assert self._started
keys, raw = self._get_input( self.max_tenths )
# Avoid pegging CPU at 100% when slowly resizing, and work
# around a bug with some braindead curses implementations that
# return "no key" between "window resize" commands
if keys==['window resize'] and self.prev_input_resize:
while True:
keys, raw2 = self._get_input(self.resize_tenths)
raw += raw2
if not keys:
keys, raw2 = self._get_input(
self.resize_tenths)
raw += raw2
if keys!=['window resize']:
break
if keys[-1:]!=['window resize']:
keys.append('window resize')
if keys==['window resize']:
self.prev_input_resize = 2
elif self.prev_input_resize == 2 and not keys:
self.prev_input_resize = 1
else:
self.prev_input_resize = 0
if raw_keys:
return keys, raw
return keys
def _get_input(self, wait_tenths):
# this works around a strange curses bug with window resizing
# not being reported correctly with repeated calls to this
# function without a doupdate call in between
curses.doupdate()
key = self._getch(wait_tenths)
resize = False
raw = []
keys = []
while key >= 0:
raw.append(key)
if key==KEY_RESIZE:
resize = True
elif key==KEY_MOUSE:
keys += self._encode_mouse_event()
else:
keys.append(key)
key = self._getch_nodelay()
processed = []
try:
while keys:
run, keys = escape.process_keyqueue(keys, True)
processed += run
except escape.MoreInputRequired:
key = self._getch(self.complete_tenths)
while key >= 0:
raw.append(key)
if key==KEY_RESIZE:
resize = True
elif key==KEY_MOUSE:
keys += self._encode_mouse_event()
else:
keys.append(key)
key = self._getch_nodelay()
while keys:
run, keys = escape.process_keyqueue(keys, False)
processed += run
if resize:
processed.append('window resize')
return processed, raw
def _encode_mouse_event(self):
# convert to escape sequence
last = next = self.last_bstate
(id,x,y,z,bstate) = curses.getmouse()
mod = 0
if bstate & curses.BUTTON_SHIFT: mod |= 4
if bstate & curses.BUTTON_ALT: mod |= 8
if bstate & curses.BUTTON_CTRL: mod |= 16
l = []
def append_button( b ):
b |= mod
l.extend([ 27, ord('['), ord('M'), b+32, x+33, y+33 ])
if bstate & curses.BUTTON1_PRESSED and last & 1 == 0:
append_button( 0 )
next |= 1
if bstate & curses.BUTTON2_PRESSED and last & 2 == 0:
append_button( 1 )
next |= 2
if bstate & curses.BUTTON3_PRESSED and last & 4 == 0:
append_button( 2 )
next |= 4
if bstate & curses.BUTTON4_PRESSED and last & 8 == 0:
append_button( 64 )
next |= 8
if bstate & curses.BUTTON1_RELEASED and last & 1:
append_button( 0 + escape.MOUSE_RELEASE_FLAG )
next &= ~ 1
if bstate & curses.BUTTON2_RELEASED and last & 2:
append_button( 1 + escape.MOUSE_RELEASE_FLAG )
next &= ~ 2
if bstate & curses.BUTTON3_RELEASED and last & 4:
append_button( 2 + escape.MOUSE_RELEASE_FLAG )
next &= ~ 4
if bstate & curses.BUTTON4_RELEASED and last & 8:
append_button( 64 + escape.MOUSE_RELEASE_FLAG )
next &= ~ 8
if bstate & curses.BUTTON1_DOUBLE_CLICKED:
append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG )
if bstate & curses.BUTTON2_DOUBLE_CLICKED:
append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG )
if bstate & curses.BUTTON3_DOUBLE_CLICKED:
append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG )
if bstate & curses.BUTTON4_DOUBLE_CLICKED:
append_button( 64 + escape.MOUSE_MULTIPLE_CLICK_FLAG )
if bstate & curses.BUTTON1_TRIPLE_CLICKED:
append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 )
if bstate & curses.BUTTON2_TRIPLE_CLICKED:
append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 )
if bstate & curses.BUTTON3_TRIPLE_CLICKED:
append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 )
if bstate & curses.BUTTON4_TRIPLE_CLICKED:
append_button( 64 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 )
self.last_bstate = next
return l
def _dbg_instr(self): # messy input string (intended for debugging)
curses.echo()
self.s.nodelay(0)
curses.halfdelay(100)
str = self.s.getstr()
curses.noecho()
return str
def _dbg_out(self,str): # messy output function (intended for debugging)
self.s.clrtoeol()
self.s.addstr(str)
self.s.refresh()
self._curs_set(1)
def _dbg_query(self,question): # messy query (intended for debugging)
self._dbg_out(question)
return self._dbg_instr()
def _dbg_refresh(self):
self.s.refresh()
def get_cols_rows(self):
"""Return the terminal dimensions (num columns, num rows)."""
rows,cols = self.s.getmaxyx()
return cols,rows
def _setattr(self, a):
if a is None:
self.s.attrset(0)
return
elif not isinstance(a, AttrSpec):
p = self._palette.get(a, (AttrSpec('default', 'default'),))
a = p[0]
if self.has_color:
if a.foreground_basic:
if a.foreground_number >= 8:
fg = a.foreground_number - 8
else:
fg = a.foreground_number
else:
fg = 7
if a.background_basic:
bg = a.background_number
else:
bg = 0
attr = curses.color_pair(bg * 8 + 7 - fg)
else:
attr = 0
if a.bold:
attr |= curses.A_BOLD
if a.standout:
attr |= curses.A_STANDOUT
if a.underline:
attr |= curses.A_UNDERLINE
if a.blink:
attr |= curses.A_BLINK
self.s.attrset(attr)
def draw_screen(self, (cols, rows), r ):
"""Paint screen with rendered canvas."""
assert self._started
assert r.rows() == rows, "canvas size and passed size don't match"
y = -1
for row in r.content():
y += 1
try:
self.s.move( y, 0 )
except _curses.error:
# terminal shrunk?
# move failed so stop rendering.
return
first = True
lasta = None
nr = 0
for a, cs, seg in row:
if cs != 'U':
seg = seg.translate(UNPRINTABLE_TRANS_TABLE)
assert isinstance(seg, bytes)
if first or lasta != a:
self._setattr(a)
lasta = a
try:
if cs in ("0", "U"):
for i in range(len(seg)):
self.s.addch( 0x400000 +
ord(seg[i]) )
else:
assert cs is None
if PYTHON3:
assert isinstance(seg, bytes)
self.s.addstr(seg.decode('utf-8'))
else:
self.s.addstr(seg)
except _curses.error:
# it's ok to get out of the
# screen on the lower right
if (y == rows-1 and nr == len(row)-1):
pass
else:
# perhaps screen size changed
# quietly abort.
return
nr += 1
if r.cursor is not None:
x,y = r.cursor
self._curs_set(1)
try:
self.s.move(y,x)
except _curses.error:
pass
else:
self._curs_set(0)
self.s.move(0,0)
self.s.refresh()
self.keep_cache_alive_link = r
def clear(self):
"""
Force the screen to be completely repainted on the next
call to draw_screen().
"""
self.s.clear()
class _test:
def __init__(self):
self.ui = Screen()
self.l = _curses_colours.keys()
self.l.sort()
for c in self.l:
self.ui.register_palette( [
(c+" on black", c, 'black', 'underline'),
(c+" on dark blue",c, 'dark blue', 'bold'),
(c+" on light gray",c,'light gray', 'standout'),
])
self.ui.run_wrapper(self.run)
def run(self):
class FakeRender: pass
r = FakeRender()
text = [" has_color = "+repr(self.ui.has_color),""]
attr = [[],[]]
r.coords = {}
r.cursor = None
for c in self.l:
t = ""
a = []
for p in c+" on black",c+" on dark blue",c+" on light gray":
a.append((p,27))
t=t+ (p+27*" ")[:27]
text.append( t )
attr.append( a )
text += ["","return values from get_input(): (q exits)", ""]
attr += [[],[],[]]
cols,rows = self.ui.get_cols_rows()
keys = None
while keys!=['q']:
r.text=([t.ljust(cols) for t in text]+[""]*rows)[:rows]
r.attr=(attr+[[]]*rows) [:rows]
self.ui.draw_screen((cols,rows),r)
keys, raw = self.ui.get_input( raw_keys = True )
if 'window resize' in keys:
cols, rows = self.ui.get_cols_rows()
if not keys:
continue
t = ""
a = []
for k in keys:
if type(k) == unicode: k = k.encode("utf-8")
t += "'"+k + "' "
a += [(None,1), ('yellow on dark blue',len(k)),
(None,2)]
text.append(t + ": "+ repr(raw))
attr.append(a)
text = text[-rows:]
attr = attr[-rows:]
if '__main__'==__name__:
_test()

File diff suppressed because it is too large Load Diff

View File

@@ -1,894 +0,0 @@
#!/usr/bin/python
# Urwid common display code
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
import os
import sys
try:
import termios
except ImportError:
pass # windows
from urwid.util import StoppingContext, int_scale
from urwid import signals
from urwid.compat import B, bytes3
# for replacing unprintable bytes with '?'
UNPRINTABLE_TRANS_TABLE = B("?") * 32 + bytes3(range(32,256))
# signals sent by BaseScreen
UPDATE_PALETTE_ENTRY = "update palette entry"
INPUT_DESCRIPTORS_CHANGED = "input descriptors changed"
# AttrSpec internal values
_BASIC_START = 0 # first index of basic color aliases
_CUBE_START = 16 # first index of color cube
_CUBE_SIZE_256 = 6 # one side of the color cube
_GRAY_SIZE_256 = 24
_GRAY_START_256 = _CUBE_SIZE_256 ** 3 + _CUBE_START
_CUBE_WHITE_256 = _GRAY_START_256 -1
_CUBE_SIZE_88 = 4
_GRAY_SIZE_88 = 8
_GRAY_START_88 = _CUBE_SIZE_88 ** 3 + _CUBE_START
_CUBE_WHITE_88 = _GRAY_START_88 -1
_CUBE_BLACK = _CUBE_START
# values copied from xterm 256colres.h:
_CUBE_STEPS_256 = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
_GRAY_STEPS_256 = [0x08, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e, 0x58, 0x62,
0x6c, 0x76, 0x80, 0x84, 0x94, 0x9e, 0xa8, 0xb2, 0xbc, 0xc6, 0xd0,
0xda, 0xe4, 0xee]
# values copied from xterm 88colres.h:
_CUBE_STEPS_88 = [0x00, 0x8b, 0xcd, 0xff]
_GRAY_STEPS_88 = [0x2e, 0x5c, 0x73, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7]
# values copied from X11/rgb.txt and XTerm-col.ad:
_BASIC_COLOR_VALUES = [(0,0,0), (205, 0, 0), (0, 205, 0), (205, 205, 0),
(0, 0, 238), (205, 0, 205), (0, 205, 205), (229, 229, 229),
(127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0),
(0x5c, 0x5c, 0xff), (255, 0, 255), (0, 255, 255), (255, 255, 255)]
_COLOR_VALUES_256 = (_BASIC_COLOR_VALUES +
[(r, g, b) for r in _CUBE_STEPS_256 for g in _CUBE_STEPS_256
for b in _CUBE_STEPS_256] +
[(gr, gr, gr) for gr in _GRAY_STEPS_256])
_COLOR_VALUES_88 = (_BASIC_COLOR_VALUES +
[(r, g, b) for r in _CUBE_STEPS_88 for g in _CUBE_STEPS_88
for b in _CUBE_STEPS_88] +
[(gr, gr, gr) for gr in _GRAY_STEPS_88])
assert len(_COLOR_VALUES_256) == 256
assert len(_COLOR_VALUES_88) == 88
_FG_COLOR_MASK = 0x000000ff
_BG_COLOR_MASK = 0x0000ff00
_FG_BASIC_COLOR = 0x00010000
_FG_HIGH_COLOR = 0x00020000
_BG_BASIC_COLOR = 0x00040000
_BG_HIGH_COLOR = 0x00080000
_BG_SHIFT = 8
_HIGH_88_COLOR = 0x00100000
_STANDOUT = 0x02000000
_UNDERLINE = 0x04000000
_BOLD = 0x08000000
_BLINK = 0x10000000
_FG_MASK = (_FG_COLOR_MASK | _FG_BASIC_COLOR | _FG_HIGH_COLOR |
_STANDOUT | _UNDERLINE | _BLINK | _BOLD)
_BG_MASK = _BG_COLOR_MASK | _BG_BASIC_COLOR | _BG_HIGH_COLOR
DEFAULT = 'default'
BLACK = 'black'
DARK_RED = 'dark red'
DARK_GREEN = 'dark green'
BROWN = 'brown'
DARK_BLUE = 'dark blue'
DARK_MAGENTA = 'dark magenta'
DARK_CYAN = 'dark cyan'
LIGHT_GRAY = 'light gray'
DARK_GRAY = 'dark gray'
LIGHT_RED = 'light red'
LIGHT_GREEN = 'light green'
YELLOW = 'yellow'
LIGHT_BLUE = 'light blue'
LIGHT_MAGENTA = 'light magenta'
LIGHT_CYAN = 'light cyan'
WHITE = 'white'
_BASIC_COLORS = [
BLACK,
DARK_RED,
DARK_GREEN,
BROWN,
DARK_BLUE,
DARK_MAGENTA,
DARK_CYAN,
LIGHT_GRAY,
DARK_GRAY,
LIGHT_RED,
LIGHT_GREEN,
YELLOW,
LIGHT_BLUE,
LIGHT_MAGENTA,
LIGHT_CYAN,
WHITE,
]
_ATTRIBUTES = {
'bold': _BOLD,
'underline': _UNDERLINE,
'blink': _BLINK,
'standout': _STANDOUT,
}
def _value_lookup_table(values, size):
"""
Generate a lookup table for finding the closest item in values.
Lookup returns (index into values)+1
values -- list of values in ascending order, all < size
size -- size of lookup table and maximum value
>>> _value_lookup_table([0, 7, 9], 10)
[0, 0, 0, 0, 1, 1, 1, 1, 2, 2]
"""
middle_values = [0] + [(values[i] + values[i + 1] + 1) // 2
for i in range(len(values) - 1)] + [size]
lookup_table = []
for i in range(len(middle_values)-1):
count = middle_values[i + 1] - middle_values[i]
lookup_table.extend([i] * count)
return lookup_table
_CUBE_256_LOOKUP = _value_lookup_table(_CUBE_STEPS_256, 256)
_GRAY_256_LOOKUP = _value_lookup_table([0] + _GRAY_STEPS_256 + [0xff], 256)
_CUBE_88_LOOKUP = _value_lookup_table(_CUBE_STEPS_88, 256)
_GRAY_88_LOOKUP = _value_lookup_table([0] + _GRAY_STEPS_88 + [0xff], 256)
# convert steps to values that will be used by string versions of the colors
# 1 hex digit for rgb and 0..100 for grayscale
_CUBE_STEPS_256_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_256]
_GRAY_STEPS_256_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_256]
_CUBE_STEPS_88_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_88]
_GRAY_STEPS_88_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_88]
# create lookup tables for 1 hex digit rgb and 0..100 for grayscale values
_CUBE_256_LOOKUP_16 = [_CUBE_256_LOOKUP[int_scale(n, 16, 0x100)]
for n in range(16)]
_GRAY_256_LOOKUP_101 = [_GRAY_256_LOOKUP[int_scale(n, 101, 0x100)]
for n in range(101)]
_CUBE_88_LOOKUP_16 = [_CUBE_88_LOOKUP[int_scale(n, 16, 0x100)]
for n in range(16)]
_GRAY_88_LOOKUP_101 = [_GRAY_88_LOOKUP[int_scale(n, 101, 0x100)]
for n in range(101)]
# The functions _gray_num_256() and _gray_num_88() do not include the gray
# values from the color cube so that the gray steps are an even width.
# The color cube grays are available by using the rgb functions. Pure
# white and black are taken from the color cube, since the gray range does
# not include them, and the basic colors are more likely to have been
# customized by an end-user.
def _gray_num_256(gnum):
"""Return ths color number for gray number gnum.
Color cube black and white are returned for 0 and 25 respectively
since those values aren't included in the gray scale.
"""
# grays start from index 1
gnum -= 1
if gnum < 0:
return _CUBE_BLACK
if gnum >= _GRAY_SIZE_256:
return _CUBE_WHITE_256
return _GRAY_START_256 + gnum
def _gray_num_88(gnum):
"""Return ths color number for gray number gnum.
Color cube black and white are returned for 0 and 9 respectively
since those values aren't included in the gray scale.
"""
# gnums start from index 1
gnum -= 1
if gnum < 0:
return _CUBE_BLACK
if gnum >= _GRAY_SIZE_88:
return _CUBE_WHITE_88
return _GRAY_START_88 + gnum
def _color_desc_256(num):
"""
Return a string description of color number num.
0..15 -> 'h0'..'h15' basic colors (as high-colors)
16..231 -> '#000'..'#fff' color cube colors
232..255 -> 'g3'..'g93' grays
>>> _color_desc_256(15)
'h15'
>>> _color_desc_256(16)
'#000'
>>> _color_desc_256(17)
'#006'
>>> _color_desc_256(230)
'#ffd'
>>> _color_desc_256(233)
'g7'
>>> _color_desc_256(234)
'g11'
"""
assert num >= 0 and num < 256, num
if num < _CUBE_START:
return 'h%d' % num
if num < _GRAY_START_256:
num -= _CUBE_START
b, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256
g, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256
r = num % _CUBE_SIZE_256
return '#%x%x%x' % (_CUBE_STEPS_256_16[r], _CUBE_STEPS_256_16[g],
_CUBE_STEPS_256_16[b])
return 'g%d' % _GRAY_STEPS_256_101[num - _GRAY_START_256]
def _color_desc_88(num):
"""
Return a string description of color number num.
0..15 -> 'h0'..'h15' basic colors (as high-colors)
16..79 -> '#000'..'#fff' color cube colors
80..87 -> 'g18'..'g90' grays
>>> _color_desc_88(15)
'h15'
>>> _color_desc_88(16)
'#000'
>>> _color_desc_88(17)
'#008'
>>> _color_desc_88(78)
'#ffc'
>>> _color_desc_88(81)
'g36'
>>> _color_desc_88(82)
'g45'
"""
assert num > 0 and num < 88
if num < _CUBE_START:
return 'h%d' % num
if num < _GRAY_START_88:
num -= _CUBE_START
b, num = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88
g, r= num % _CUBE_SIZE_88, num // _CUBE_SIZE_88
return '#%x%x%x' % (_CUBE_STEPS_88_16[r], _CUBE_STEPS_88_16[g],
_CUBE_STEPS_88_16[b])
return 'g%d' % _GRAY_STEPS_88_101[num - _GRAY_START_88]
def _parse_color_256(desc):
"""
Return a color number for the description desc.
'h0'..'h255' -> 0..255 actual color number
'#000'..'#fff' -> 16..231 color cube colors
'g0'..'g100' -> 16, 232..255, 231 grays and color cube black/white
'g#00'..'g#ff' -> 16, 232...255, 231 gray and color cube black/white
Returns None if desc is invalid.
>>> _parse_color_256('h142')
142
>>> _parse_color_256('#f00')
196
>>> _parse_color_256('g100')
231
>>> _parse_color_256('g#80')
244
"""
if len(desc) > 4:
# keep the length within reason before parsing
return None
try:
if desc.startswith('h'):
# high-color number
num = int(desc[1:], 10)
if num < 0 or num > 255:
return None
return num
if desc.startswith('#') and len(desc) == 4:
# color-cube coordinates
rgb = int(desc[1:], 16)
if rgb < 0:
return None
b, rgb = rgb % 16, rgb // 16
g, r = rgb % 16, rgb // 16
# find the closest rgb values
r = _CUBE_256_LOOKUP_16[r]
g = _CUBE_256_LOOKUP_16[g]
b = _CUBE_256_LOOKUP_16[b]
return _CUBE_START + (r * _CUBE_SIZE_256 + g) * _CUBE_SIZE_256 + b
# Only remaining possibility is gray value
if desc.startswith('g#'):
# hex value 00..ff
gray = int(desc[2:], 16)
if gray < 0 or gray > 255:
return None
gray = _GRAY_256_LOOKUP[gray]
elif desc.startswith('g'):
# decimal value 0..100
gray = int(desc[1:], 10)
if gray < 0 or gray > 100:
return None
gray = _GRAY_256_LOOKUP_101[gray]
else:
return None
if gray == 0:
return _CUBE_BLACK
gray -= 1
if gray == _GRAY_SIZE_256:
return _CUBE_WHITE_256
return _GRAY_START_256 + gray
except ValueError:
return None
def _parse_color_88(desc):
"""
Return a color number for the description desc.
'h0'..'h87' -> 0..87 actual color number
'#000'..'#fff' -> 16..79 color cube colors
'g0'..'g100' -> 16, 80..87, 79 grays and color cube black/white
'g#00'..'g#ff' -> 16, 80...87, 79 gray and color cube black/white
Returns None if desc is invalid.
>>> _parse_color_88('h142')
>>> _parse_color_88('h42')
42
>>> _parse_color_88('#f00')
64
>>> _parse_color_88('g100')
79
>>> _parse_color_88('g#80')
83
"""
if len(desc) > 4:
# keep the length within reason before parsing
return None
try:
if desc.startswith('h'):
# high-color number
num = int(desc[1:], 10)
if num < 0 or num > 87:
return None
return num
if desc.startswith('#') and len(desc) == 4:
# color-cube coordinates
rgb = int(desc[1:], 16)
if rgb < 0:
return None
b, rgb = rgb % 16, rgb // 16
g, r = rgb % 16, rgb // 16
# find the closest rgb values
r = _CUBE_88_LOOKUP_16[r]
g = _CUBE_88_LOOKUP_16[g]
b = _CUBE_88_LOOKUP_16[b]
return _CUBE_START + (r * _CUBE_SIZE_88 + g) * _CUBE_SIZE_88 + b
# Only remaining possibility is gray value
if desc.startswith('g#'):
# hex value 00..ff
gray = int(desc[2:], 16)
if gray < 0 or gray > 255:
return None
gray = _GRAY_88_LOOKUP[gray]
elif desc.startswith('g'):
# decimal value 0..100
gray = int(desc[1:], 10)
if gray < 0 or gray > 100:
return None
gray = _GRAY_88_LOOKUP_101[gray]
else:
return None
if gray == 0:
return _CUBE_BLACK
gray -= 1
if gray == _GRAY_SIZE_88:
return _CUBE_WHITE_88
return _GRAY_START_88 + gray
except ValueError:
return None
class AttrSpecError(Exception):
pass
class AttrSpec(object):
def __init__(self, fg, bg, colors=256):
"""
fg -- a string containing a comma-separated foreground color
and settings
Color values:
'default' (use the terminal's default foreground),
'black', 'dark red', 'dark green', 'brown', 'dark blue',
'dark magenta', 'dark cyan', 'light gray', 'dark gray',
'light red', 'light green', 'yellow', 'light blue',
'light magenta', 'light cyan', 'white'
High-color example values:
'#009' (0% red, 0% green, 60% red, like HTML colors)
'#fcc' (100% red, 80% green, 80% blue)
'g40' (40% gray, decimal), 'g#cc' (80% gray, hex),
'#000', 'g0', 'g#00' (black),
'#fff', 'g100', 'g#ff' (white)
'h8' (color number 8), 'h255' (color number 255)
Setting:
'bold', 'underline', 'blink', 'standout'
Some terminals use 'bold' for bright colors. Most terminals
ignore the 'blink' setting. If the color is not given then
'default' will be assumed.
bg -- a string containing the background color
Color values:
'default' (use the terminal's default background),
'black', 'dark red', 'dark green', 'brown', 'dark blue',
'dark magenta', 'dark cyan', 'light gray'
High-color exaples:
see fg examples above
An empty string will be treated the same as 'default'.
colors -- the maximum colors available for the specification
Valid values include: 1, 16, 88 and 256. High-color
values are only usable with 88 or 256 colors. With
1 color only the foreground settings may be used.
>>> AttrSpec('dark red', 'light gray', 16)
AttrSpec('dark red', 'light gray')
>>> AttrSpec('yellow, underline, bold', 'dark blue')
AttrSpec('yellow,bold,underline', 'dark blue')
>>> AttrSpec('#ddb', '#004', 256) # closest colors will be found
AttrSpec('#dda', '#006')
>>> AttrSpec('#ddb', '#004', 88)
AttrSpec('#ccc', '#000', colors=88)
"""
if colors not in (1, 16, 88, 256):
raise AttrSpecError('invalid number of colors (%d).' % colors)
self._value = 0 | _HIGH_88_COLOR * (colors == 88)
self.foreground = fg
self.background = bg
if self.colors > colors:
raise AttrSpecError(('foreground/background (%s/%s) require ' +
'more colors than have been specified (%d).') %
(repr(fg), repr(bg), colors))
foreground_basic = property(lambda s: s._value & _FG_BASIC_COLOR != 0)
foreground_high = property(lambda s: s._value & _FG_HIGH_COLOR != 0)
foreground_number = property(lambda s: s._value & _FG_COLOR_MASK)
background_basic = property(lambda s: s._value & _BG_BASIC_COLOR != 0)
background_high = property(lambda s: s._value & _BG_HIGH_COLOR != 0)
background_number = property(lambda s: (s._value & _BG_COLOR_MASK)
>> _BG_SHIFT)
bold = property(lambda s: s._value & _BOLD != 0)
underline = property(lambda s: s._value & _UNDERLINE != 0)
blink = property(lambda s: s._value & _BLINK != 0)
standout = property(lambda s: s._value & _STANDOUT != 0)
def _colors(self):
"""
Return the maximum colors required for this object.
Returns 256, 88, 16 or 1.
"""
if self._value & _HIGH_88_COLOR:
return 88
if self._value & (_BG_HIGH_COLOR | _FG_HIGH_COLOR):
return 256
if self._value & (_BG_BASIC_COLOR | _BG_BASIC_COLOR):
return 16
return 1
colors = property(_colors)
def __repr__(self):
"""
Return an executable python representation of the AttrSpec
object.
"""
args = "%r, %r" % (self.foreground, self.background)
if self.colors == 88:
# 88-color mode is the only one that is handled differently
args = args + ", colors=88"
return "%s(%s)" % (self.__class__.__name__, args)
def _foreground_color(self):
"""Return only the color component of the foreground."""
if not (self.foreground_basic or self.foreground_high):
return 'default'
if self.foreground_basic:
return _BASIC_COLORS[self.foreground_number]
if self.colors == 88:
return _color_desc_88(self.foreground_number)
return _color_desc_256(self.foreground_number)
def _foreground(self):
return (self._foreground_color() +
',bold' * self.bold + ',standout' * self.standout +
',blink' * self.blink + ',underline' * self.underline)
def _set_foreground(self, foreground):
color = None
flags = 0
# handle comma-separated foreground
for part in foreground.split(','):
part = part.strip()
if part in _ATTRIBUTES:
# parse and store "settings"/attributes in flags
if flags & _ATTRIBUTES[part]:
raise AttrSpecError(("Setting %s specified more than" +
"once in foreground (%s)") % (repr(part),
repr(foreground)))
flags |= _ATTRIBUTES[part]
continue
# past this point we must be specifying a color
if part in ('', 'default'):
scolor = 0
elif part in _BASIC_COLORS:
scolor = _BASIC_COLORS.index(part)
flags |= _FG_BASIC_COLOR
elif self._value & _HIGH_88_COLOR:
scolor = _parse_color_88(part)
flags |= _FG_HIGH_COLOR
else:
scolor = _parse_color_256(part)
flags |= _FG_HIGH_COLOR
# _parse_color_*() return None for unrecognised colors
if scolor is None:
raise AttrSpecError(("Unrecognised color specification %s " +
"in foreground (%s)") % (repr(part), repr(foreground)))
if color is not None:
raise AttrSpecError(("More than one color given for " +
"foreground (%s)") % (repr(foreground),))
color = scolor
if color is None:
color = 0
self._value = (self._value & ~_FG_MASK) | color | flags
foreground = property(_foreground, _set_foreground)
def _background(self):
"""Return the background color."""
if not (self.background_basic or self.background_high):
return 'default'
if self.background_basic:
return _BASIC_COLORS[self.background_number]
if self._value & _HIGH_88_COLOR:
return _color_desc_88(self.background_number)
return _color_desc_256(self.background_number)
def _set_background(self, background):
flags = 0
if background in ('', 'default'):
color = 0
elif background in _BASIC_COLORS:
color = _BASIC_COLORS.index(background)
flags |= _BG_BASIC_COLOR
elif self._value & _HIGH_88_COLOR:
color = _parse_color_88(background)
flags |= _BG_HIGH_COLOR
else:
color = _parse_color_256(background)
flags |= _BG_HIGH_COLOR
if color is None:
raise AttrSpecError(("Unrecognised color specification " +
"in background (%s)") % (repr(background),))
self._value = (self._value & ~_BG_MASK) | (color << _BG_SHIFT) | flags
background = property(_background, _set_background)
def get_rgb_values(self):
"""
Return (fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue) color
components. Each component is in the range 0-255. Values are taken
from the XTerm defaults and may not exactly match the user's terminal.
If the foreground or background is 'default' then all their compenents
will be returned as None.
>>> AttrSpec('yellow', '#ccf', colors=88).get_rgb_values()
(255, 255, 0, 205, 205, 255)
>>> AttrSpec('default', 'g92').get_rgb_values()
(None, None, None, 238, 238, 238)
"""
if not (self.foreground_basic or self.foreground_high):
vals = (None, None, None)
elif self.colors == 88:
assert self.foreground_number < 88, "Invalid AttrSpec _value"
vals = _COLOR_VALUES_88[self.foreground_number]
else:
vals = _COLOR_VALUES_256[self.foreground_number]
if not (self.background_basic or self.background_high):
return vals + (None, None, None)
elif self.colors == 88:
assert self.background_number < 88, "Invalid AttrSpec _value"
return vals + _COLOR_VALUES_88[self.background_number]
else:
return vals + _COLOR_VALUES_256[self.background_number]
def __eq__(self, other):
return isinstance(other, AttrSpec) and self._value == other._value
def __ne__(self, other):
return not self == other
__hash__ = object.__hash__
class RealTerminal(object):
def __init__(self):
super(RealTerminal,self).__init__()
self._signal_keys_set = False
self._old_signal_keys = None
def tty_signal_keys(self, intr=None, quit=None, start=None,
stop=None, susp=None, fileno=None):
"""
Read and/or set the tty's signal character settings.
This function returns the current settings as a tuple.
Use the string 'undefined' to unmap keys from their signals.
The value None is used when no change is being made.
Setting signal keys is done using the integer ascii
code for the key, eg. 3 for CTRL+C.
If this function is called after start() has been called
then the original settings will be restored when stop()
is called.
"""
if fileno is None:
fileno = sys.stdin.fileno()
if not os.isatty(fileno):
return
tattr = termios.tcgetattr(fileno)
sattr = tattr[6]
skeys = (sattr[termios.VINTR], sattr[termios.VQUIT],
sattr[termios.VSTART], sattr[termios.VSTOP],
sattr[termios.VSUSP])
if intr == 'undefined': intr = 0
if quit == 'undefined': quit = 0
if start == 'undefined': start = 0
if stop == 'undefined': stop = 0
if susp == 'undefined': susp = 0
if intr is not None: tattr[6][termios.VINTR] = intr
if quit is not None: tattr[6][termios.VQUIT] = quit
if start is not None: tattr[6][termios.VSTART] = start
if stop is not None: tattr[6][termios.VSTOP] = stop
if susp is not None: tattr[6][termios.VSUSP] = susp
if intr is not None or quit is not None or \
start is not None or stop is not None or \
susp is not None:
termios.tcsetattr(fileno, termios.TCSADRAIN, tattr)
self._signal_keys_set = True
return skeys
class ScreenError(Exception):
pass
class BaseScreen(object):
"""
Base class for Screen classes (raw_display.Screen, .. etc)
"""
__metaclass__ = signals.MetaSignals
signals = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED]
def __init__(self):
super(BaseScreen,self).__init__()
self._palette = {}
self._started = False
started = property(lambda self: self._started)
def start(self, *args, **kwargs):
"""Set up the screen. If the screen has already been started, does
nothing.
May be used as a context manager, in which case :meth:`stop` will
automatically be called at the end of the block:
with screen.start():
...
You shouldn't override this method in a subclass; instead, override
:meth:`_start`.
"""
if not self._started:
self._start(*args, **kwargs)
self._started = True
return StoppingContext(self)
def _start(self):
pass
def stop(self):
if self._started:
self._stop()
self._started = False
def _stop(self):
pass
def run_wrapper(self, fn, *args, **kwargs):
"""Start the screen, call a function, then stop the screen. Extra
arguments are passed to `start`.
Deprecated in favor of calling `start` as a context manager.
"""
with self.start(*args, **kwargs):
return fn()
def register_palette(self, palette):
"""Register a set of palette entries.
palette -- a list of (name, like_other_name) or
(name, foreground, background, mono, foreground_high,
background_high) tuples
The (name, like_other_name) format will copy the settings
from the palette entry like_other_name, which must appear
before this tuple in the list.
The mono and foreground/background_high values are
optional ie. the second tuple format may have 3, 4 or 6
values. See register_palette_entry() for a description
of the tuple values.
"""
for item in palette:
if len(item) in (3,4,6):
self.register_palette_entry(*item)
continue
if len(item) != 2:
raise ScreenError("Invalid register_palette entry: %s" %
repr(item))
name, like_name = item
if like_name not in self._palette:
raise ScreenError("palette entry '%s' doesn't exist"%like_name)
self._palette[name] = self._palette[like_name]
def register_palette_entry(self, name, foreground, background,
mono=None, foreground_high=None, background_high=None):
"""Register a single palette entry.
name -- new entry/attribute name
foreground -- a string containing a comma-separated foreground
color and settings
Color values:
'default' (use the terminal's default foreground),
'black', 'dark red', 'dark green', 'brown', 'dark blue',
'dark magenta', 'dark cyan', 'light gray', 'dark gray',
'light red', 'light green', 'yellow', 'light blue',
'light magenta', 'light cyan', 'white'
Settings:
'bold', 'underline', 'blink', 'standout'
Some terminals use 'bold' for bright colors. Most terminals
ignore the 'blink' setting. If the color is not given then
'default' will be assumed.
background -- a string containing the background color
Background color values:
'default' (use the terminal's default background),
'black', 'dark red', 'dark green', 'brown', 'dark blue',
'dark magenta', 'dark cyan', 'light gray'
mono -- a comma-separated string containing monochrome terminal
settings (see "Settings" above.)
None = no terminal settings (same as 'default')
foreground_high -- a string containing a comma-separated
foreground color and settings, standard foreground
colors (see "Color values" above) or high-colors may
be used
High-color example values:
'#009' (0% red, 0% green, 60% red, like HTML colors)
'#fcc' (100% red, 80% green, 80% blue)
'g40' (40% gray, decimal), 'g#cc' (80% gray, hex),
'#000', 'g0', 'g#00' (black),
'#fff', 'g100', 'g#ff' (white)
'h8' (color number 8), 'h255' (color number 255)
None = use foreground parameter value
background_high -- a string containing the background color,
standard background colors (see "Background colors" above)
or high-colors (see "High-color example values" above)
may be used
None = use background parameter value
"""
basic = AttrSpec(foreground, background, 16)
if type(mono) == tuple:
# old style of specifying mono attributes was to put them
# in a tuple. convert to comma-separated string
mono = ",".join(mono)
if mono is None:
mono = DEFAULT
mono = AttrSpec(mono, DEFAULT, 1)
if foreground_high is None:
foreground_high = foreground
if background_high is None:
background_high = background
high_256 = AttrSpec(foreground_high, background_high, 256)
# 'hX' where X > 15 are different in 88/256 color, use
# basic colors for 88-color mode if high colors are specified
# in this way (also avoids crash when X > 87)
def large_h(desc):
if not desc.startswith('h'):
return False
if ',' in desc:
desc = desc.split(',',1)[0]
num = int(desc[1:], 10)
return num > 15
if large_h(foreground_high) or large_h(background_high):
high_88 = basic
else:
high_88 = AttrSpec(foreground_high, background_high, 88)
signals.emit_signal(self, UPDATE_PALETTE_ENTRY,
name, basic, mono, high_88, high_256)
self._palette[name] = (basic, mono, high_88, high_256)
def _test():
import doctest
doctest.testmod()
if __name__=='__main__':
_test()

View File

@@ -1,441 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid escape sequences common to curses_display and raw_display
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
"""
Terminal Escape Sequences for input and display
"""
import re
try:
from urwid import str_util
except ImportError:
from urwid import old_str_util as str_util
from urwid.compat import bytes, bytes3
within_double_byte = str_util.within_double_byte
SO = "\x0e"
SI = "\x0f"
IBMPC_ON = "\x1b[11m"
IBMPC_OFF = "\x1b[10m"
DEC_TAG = "0"
DEC_SPECIAL_CHARS = u'▮◆▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·'
ALT_DEC_SPECIAL_CHARS = u"_`abcdefghijklmnopqrstuvwxyz{|}~"
DEC_SPECIAL_CHARMAP = {}
assert len(DEC_SPECIAL_CHARS) == len(ALT_DEC_SPECIAL_CHARS), repr((DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS))
for c, alt in zip(DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS):
DEC_SPECIAL_CHARMAP[ord(c)] = SO + alt + SI
SAFE_ASCII_DEC_SPECIAL_RE = re.compile(u"^[ -~%s]*$" % DEC_SPECIAL_CHARS)
DEC_SPECIAL_RE = re.compile(u"[%s]" % DEC_SPECIAL_CHARS)
###################
## Input sequences
###################
class MoreInputRequired(Exception):
pass
def escape_modifier( digit ):
mode = ord(digit) - ord("1")
return "shift "*(mode&1) + "meta "*((mode&2)//2) + "ctrl "*((mode&4)//4)
input_sequences = [
('[A','up'),('[B','down'),('[C','right'),('[D','left'),
('[E','5'),('[F','end'),('[G','5'),('[H','home'),
('[1~','home'),('[2~','insert'),('[3~','delete'),('[4~','end'),
('[5~','page up'),('[6~','page down'),
('[7~','home'),('[8~','end'),
('[[A','f1'),('[[B','f2'),('[[C','f3'),('[[D','f4'),('[[E','f5'),
('[11~','f1'),('[12~','f2'),('[13~','f3'),('[14~','f4'),
('[15~','f5'),('[17~','f6'),('[18~','f7'),('[19~','f8'),
('[20~','f9'),('[21~','f10'),('[23~','f11'),('[24~','f12'),
('[25~','f13'),('[26~','f14'),('[28~','f15'),('[29~','f16'),
('[31~','f17'),('[32~','f18'),('[33~','f19'),('[34~','f20'),
('OA','up'),('OB','down'),('OC','right'),('OD','left'),
('OH','home'),('OF','end'),
('OP','f1'),('OQ','f2'),('OR','f3'),('OS','f4'),
('Oo','/'),('Oj','*'),('Om','-'),('Ok','+'),
('[Z','shift tab'),
('On', '.'),
('[200~', 'begin paste'), ('[201~', 'end paste'),
] + [
(prefix + letter, modifier + key)
for prefix, modifier in zip('O[', ('meta ', 'shift '))
for letter, key in zip('abcd', ('up', 'down', 'right', 'left'))
] + [
("[" + digit + symbol, modifier + key)
for modifier, symbol in zip(('shift ', 'meta '), '$^')
for digit, key in zip('235678',
('insert', 'delete', 'page up', 'page down', 'home', 'end'))
] + [
('O' + chr(ord('p')+n), str(n)) for n in range(10)
] + [
# modified cursor keys + home, end, 5 -- [#X and [1;#X forms
(prefix+digit+letter, escape_modifier(digit) + key)
for prefix in ("[", "[1;")
for digit in "12345678"
for letter,key in zip("ABCDEFGH",
('up','down','right','left','5','end','5','home'))
] + [
# modified F1-F4 keys -- O#X form
("O"+digit+letter, escape_modifier(digit) + key)
for digit in "12345678"
for letter,key in zip("PQRS",('f1','f2','f3','f4'))
] + [
# modified F1-F13 keys -- [XX;#~ form
("["+str(num)+";"+digit+"~", escape_modifier(digit) + key)
for digit in "12345678"
for num,key in zip(
(3,5,6,11,12,13,14,15,17,18,19,20,21,23,24,25,26,28,29,31,32,33,34),
('delete', 'page up', 'page down',
'f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11',
'f12','f13','f14','f15','f16','f17','f18','f19','f20'))
] + [
# mouse reporting (special handling done in KeyqueueTrie)
('[M', 'mouse'),
# report status response
('[0n', 'status ok')
]
class KeyqueueTrie(object):
def __init__( self, sequences ):
self.data = {}
for s, result in sequences:
assert type(result) != dict
self.add(self.data, s, result)
def add(self, root, s, result):
assert type(root) == dict, "trie conflict detected"
assert len(s) > 0, "trie conflict detected"
if ord(s[0]) in root:
return self.add(root[ord(s[0])], s[1:], result)
if len(s)>1:
d = {}
root[ord(s[0])] = d
return self.add(d, s[1:], result)
root[ord(s)] = result
def get(self, keys, more_available):
result = self.get_recurse(self.data, keys, more_available)
if not result:
result = self.read_cursor_position(keys, more_available)
return result
def get_recurse(self, root, keys, more_available):
if type(root) != dict:
if root == "mouse":
return self.read_mouse_info(keys,
more_available)
return (root, keys)
if not keys:
# get more keys
if more_available:
raise MoreInputRequired()
return None
if keys[0] not in root:
return None
return self.get_recurse(root[keys[0]], keys[1:], more_available)
def read_mouse_info(self, keys, more_available):
if len(keys) < 3:
if more_available:
raise MoreInputRequired()
return None
b = keys[0] - 32
x, y = (keys[1] - 33)%256, (keys[2] - 33)%256 # supports 0-255
prefix = ""
if b & 4: prefix = prefix + "shift "
if b & 8: prefix = prefix + "meta "
if b & 16: prefix = prefix + "ctrl "
if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 1: prefix = prefix + "double "
if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 2: prefix = prefix + "triple "
# 0->1, 1->2, 2->3, 64->4, 65->5
button = ((b&64)/64*3) + (b & 3) + 1
if b & 3 == 3:
action = "release"
button = 0
elif b & MOUSE_RELEASE_FLAG:
action = "release"
elif b & MOUSE_DRAG_FLAG:
action = "drag"
elif b & MOUSE_MULTIPLE_CLICK_MASK:
action = "click"
else:
action = "press"
return ( (prefix + "mouse " + action, button, x, y), keys[3:] )
def read_cursor_position(self, keys, more_available):
"""
Interpret cursor position information being sent by the
user's terminal. Returned as ('cursor position', x, y)
where (x, y) == (0, 0) is the top left of the screen.
"""
if not keys:
if more_available:
raise MoreInputRequired()
return None
if keys[0] != ord('['):
return None
# read y value
y = 0
i = 1
for k in keys[i:]:
i += 1
if k == ord(';'):
if not y:
return None
break
if k < ord('0') or k > ord('9'):
return None
if not y and k == ord('0'):
return None
y = y * 10 + k - ord('0')
if not keys[i:]:
if more_available:
raise MoreInputRequired()
return None
# read x value
x = 0
for k in keys[i:]:
i += 1
if k == ord('R'):
if not x:
return None
return (("cursor position", x-1, y-1), keys[i:])
if k < ord('0') or k > ord('9'):
return None
if not x and k == ord('0'):
return None
x = x * 10 + k - ord('0')
if not keys[i:]:
if more_available:
raise MoreInputRequired()
return None
# This is added to button value to signal mouse release by curses_display
# and raw_display when we know which button was released. NON-STANDARD
MOUSE_RELEASE_FLAG = 2048
# This 2-bit mask is used to check if the mouse release from curses or gpm
# is a double or triple release. 00 means single click, 01 double,
# 10 triple. NON-STANDARD
MOUSE_MULTIPLE_CLICK_MASK = 1536
# This is added to button value at mouse release to differentiate between
# single, double and triple press. Double release adds this times one,
# triple release adds this times two. NON-STANDARD
MOUSE_MULTIPLE_CLICK_FLAG = 512
# xterm adds this to the button value to signal a mouse drag event
MOUSE_DRAG_FLAG = 32
#################################################
# Build the input trie from input_sequences list
input_trie = KeyqueueTrie(input_sequences)
#################################################
_keyconv = {
-1:None,
8:'backspace',
9:'tab',
10:'enter',
13:'enter',
127:'backspace',
# curses-only keycodes follow.. (XXX: are these used anymore?)
258:'down',
259:'up',
260:'left',
261:'right',
262:'home',
263:'backspace',
265:'f1', 266:'f2', 267:'f3', 268:'f4',
269:'f5', 270:'f6', 271:'f7', 272:'f8',
273:'f9', 274:'f10', 275:'f11', 276:'f12',
277:'shift f1', 278:'shift f2', 279:'shift f3', 280:'shift f4',
281:'shift f5', 282:'shift f6', 283:'shift f7', 284:'shift f8',
285:'shift f9', 286:'shift f10', 287:'shift f11', 288:'shift f12',
330:'delete',
331:'insert',
338:'page down',
339:'page up',
343:'enter', # on numpad
350:'5', # on numpad
360:'end',
}
def process_keyqueue(codes, more_available):
"""
codes -- list of key codes
more_available -- if True then raise MoreInputRequired when in the
middle of a character sequence (escape/utf8/wide) and caller
will attempt to send more key codes on the next call.
returns (list of input, list of remaining key codes).
"""
code = codes[0]
if code >= 32 and code <= 126:
key = chr(code)
return [key], codes[1:]
if code in _keyconv:
return [_keyconv[code]], codes[1:]
if code >0 and code <27:
return ["ctrl %s" % chr(ord('a')+code-1)], codes[1:]
if code >27 and code <32:
return ["ctrl %s" % chr(ord('A')+code-1)], codes[1:]
em = str_util.get_byte_encoding()
if (em == 'wide' and code < 256 and
within_double_byte(chr(code),0,0)):
if not codes[1:]:
if more_available:
raise MoreInputRequired()
if codes[1:] and codes[1] < 256:
db = chr(code)+chr(codes[1])
if within_double_byte(db, 0, 1):
return [db], codes[2:]
if em == 'utf8' and code>127 and code<256:
if code & 0xe0 == 0xc0: # 2-byte form
need_more = 1
elif code & 0xf0 == 0xe0: # 3-byte form
need_more = 2
elif code & 0xf8 == 0xf0: # 4-byte form
need_more = 3
else:
return ["<%d>"%code], codes[1:]
for i in range(need_more):
if len(codes)-1 <= i:
if more_available:
raise MoreInputRequired()
else:
return ["<%d>"%code], codes[1:]
k = codes[i+1]
if k>256 or k&0xc0 != 0x80:
return ["<%d>"%code], codes[1:]
s = bytes3(codes[:need_more+1])
assert isinstance(s, bytes)
try:
return [s.decode("utf-8")], codes[need_more+1:]
except UnicodeDecodeError:
return ["<%d>"%code], codes[1:]
if code >127 and code <256:
key = chr(code)
return [key], codes[1:]
if code != 27:
return ["<%d>"%code], codes[1:]
result = input_trie.get(codes[1:], more_available)
if result is not None:
result, remaining_codes = result
return [result], remaining_codes
if codes[1:]:
# Meta keys -- ESC+Key form
run, remaining_codes = process_keyqueue(codes[1:],
more_available)
if run[0] == "esc" or run[0].find("meta ") >= 0:
return ['esc']+run, remaining_codes
return ['meta '+run[0]]+run[1:], remaining_codes
return ['esc'], codes[1:]
####################
## Output sequences
####################
ESC = "\x1b"
CURSOR_HOME = ESC+"[H"
CURSOR_HOME_COL = "\r"
APP_KEYPAD_MODE = ESC+"="
NUM_KEYPAD_MODE = ESC+">"
SWITCH_TO_ALTERNATE_BUFFER = ESC+"7"+ESC+"[?47h"
RESTORE_NORMAL_BUFFER = ESC+"[?47l"+ESC+"8"
#RESET_SCROLL_REGION = ESC+"[;r"
#RESET = ESC+"c"
REPORT_STATUS = ESC + "[5n"
REPORT_CURSOR_POSITION = ESC+"[6n"
INSERT_ON = ESC + "[4h"
INSERT_OFF = ESC + "[4l"
def set_cursor_position( x, y ):
assert type(x) == int
assert type(y) == int
return ESC+"[%d;%dH" %(y+1, x+1)
def move_cursor_right(x):
if x < 1: return ""
return ESC+"[%dC" % x
def move_cursor_up(x):
if x < 1: return ""
return ESC+"[%dA" % x
def move_cursor_down(x):
if x < 1: return ""
return ESC+"[%dB" % x
HIDE_CURSOR = ESC+"[?25l"
SHOW_CURSOR = ESC+"[?25h"
MOUSE_TRACKING_ON = ESC+"[?1000h"+ESC+"[?1002h"
MOUSE_TRACKING_OFF = ESC+"[?1002l"+ESC+"[?1000l"
DESIGNATE_G1_SPECIAL = ESC+")0"
ERASE_IN_LINE_RIGHT = ESC+"[K"

View File

@@ -1,450 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid BigText fonts
# Copyright (C) 2004-2006 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from urwid.escape import SAFE_ASCII_DEC_SPECIAL_RE
from urwid.util import apply_target_encoding, str_util
from urwid.canvas import TextCanvas
def separate_glyphs(gdata, height):
"""return (dictionary of glyphs, utf8 required)"""
gl = gdata.split("\n")
del gl[0]
del gl[-1]
for g in gl:
assert "\t" not in g
assert len(gl) == height+1, repr(gdata)
key_line = gl[0]
del gl[0]
c = None # current character
key_index = 0 # index into character key line
end_col = 0 # column position at end of glyph
start_col = 0 # column position at start of glyph
jl = [0]*height # indexes into lines of gdata (gl)
dout = {}
utf8_required = False
while True:
if c is None:
if key_index >= len(key_line):
break
c = key_line[key_index]
if key_index < len(key_line) and key_line[key_index] == c:
end_col += str_util.get_width(ord(c))
key_index += 1
continue
out = []
for k in range(height):
l = gl[k]
j = jl[k]
y = 0
fill = 0
while y < end_col - start_col:
if j >= len(l):
fill = end_col - start_col - y
break
y += str_util.get_width(ord(l[j]))
j += 1
assert y + fill == end_col - start_col, \
repr((y, fill, end_col))
segment = l[jl[k]:j]
if not SAFE_ASCII_DEC_SPECIAL_RE.match(segment):
utf8_required = True
out.append(segment + " " * fill)
jl[k] = j
start_col = end_col
dout[c] = (y + fill, out)
c = None
return dout, utf8_required
_all_fonts = []
def get_all_fonts():
"""
Return a list of (font name, font class) tuples.
"""
return _all_fonts[:]
def add_font(name, cls):
_all_fonts.append((name, cls))
class Font(object):
def __init__(self):
assert self.height
assert self.data
self.char = {}
self.canvas = {}
self.utf8_required = False
for gdata in self.data:
self.add_glyphs(gdata)
def add_glyphs(self, gdata):
d, utf8_required = separate_glyphs(gdata, self.height)
self.char.update(d)
self.utf8_required |= utf8_required
def characters(self):
l = self.char.keys()
l.sort()
return "".join(l)
def char_width(self, c):
if c in self.char:
return self.char[c][0]
return 0
def char_data(self, c):
return self.char[c][1]
def render(self, c):
if c in self.canvas:
return self.canvas[c]
width, l = self.char[c]
tl = []
csl = []
for d in l:
t, cs = apply_target_encoding(d)
tl.append(t)
csl.append(cs)
canv = TextCanvas(tl, None, csl, maxcol=width,
check_width=False)
self.canvas[c] = canv
return canv
#safe_palette = u"┘┐┌└┼─├┤┴┬│"
#more_palette = u"═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬○"
#block_palette = u"▄#█#▀#▌#▐#▖#▗#▘#▙#▚#▛#▜#▝#▞#▟"
class Thin3x3Font(Font):
height = 3
data = [u"""
000111222333444555666777888999 !
┌─┐ ┐ ┌─┐┌─┐ ┐┌─ ┌─ ┌─┐┌─┐┌─┐ │
│ │ │ ┌─┘ ─┤└─┼└─┐├─┐ ┼├─┤└─┤ │
└─┘ ┴ └─ └─┘ ┴ ─┘└─┘ ┴└─┘ ─┘ .
""", ur"""
"###$$$%%%'*++,--.///:;==???[[\\\]]^__`
" ┼┼┌┼┐O /' /.. _┌─┐┌ \ ┐^ `
┼┼└┼┐ / * ┼ ─ / ., _ ┌┘│ \
└┼┘/ O , ./ . └ \ ┘ ──
"""]
add_font("Thin 3x3",Thin3x3Font)
class Thin4x3Font(Font):
height = 3
data = Thin3x3Font.data + [u"""
0000111122223333444455556666777788889999 ####$$$$
┌──┐ ┐ ┌──┐┌──┐ ┐┌── ┌── ┌──┐┌──┐┌──┐ ┼─┼┌┼┼┐
│ │ │ ┌──┘ ─┤└──┼└──┐├──┐ ┼├──┤└──┤ ┼─┼└┼┼┐
└──┘ ┴ └── └──┘ ┴ ──┘└──┘ ┴└──┘ ──┘ └┼┼┘
"""]
add_font("Thin 4x3",Thin4x3Font)
class HalfBlock5x4Font(Font):
height = 4
data = [u"""
00000111112222233333444445555566666777778888899999 !!
▄▀▀▄ ▄█ ▄▀▀▄ ▄▀▀▄ ▄ █ █▀▀▀ ▄▀▀ ▀▀▀█ ▄▀▀▄ ▄▀▀▄ █
█ █ █ ▄▀ ▄▀ █▄▄█ █▄▄ █▄▄ ▐▌ ▀▄▄▀ ▀▄▄█ █
█ █ █ ▄▀ ▄ █ █ █ █ █ █ █ █ █ ▀
▀▀ ▀▀▀ ▀▀▀▀ ▀▀ ▀ ▀▀▀ ▀▀ ▀ ▀▀ ▀▀ ▀
""", u'''
"""######$$$$$$%%%%%&&&&&((()))******++++++,,,-----..////:::;;
█▐▌ █ █ ▄▀█▀▄ ▐▌▐▌ ▄▀▄ █ █ ▄ ▄ ▄ ▐▌
▀█▀█▀ ▀▄█▄ █ ▀▄▀ ▐▌ ▐▌ ▄▄█▄▄ ▄▄█▄▄ ▄▄▄▄ █ ▀ ▀
▀█▀█▀ ▄ █ █ ▐▌▄ █ ▀▄▌▐▌ ▐▌ ▄▀▄ █ ▐▌ ▀ ▄▀
▀ ▀ ▀▀▀ ▀ ▀ ▀▀ ▀ ▀ ▄▀ ▀ ▀
''', ur"""
<<<<<=====>>>>>?????@@@@@@[[[[\\\\]]]]^^^^____```{{{{||}}}}~~~~''´´´
▄▀ ▀▄ ▄▀▀▄ ▄▀▀▀▄ █▀▀ ▐▌ ▀▀█ ▄▀▄ ▀▄ ▄▀ █ ▀▄ ▄ █ ▄▀
▄▀ ▀▀▀▀ ▀▄ ▄▀ █ █▀█ █ █ █ ▄▀ █ ▀▄ ▐▐▌▌
▀▄ ▀▀▀▀ ▄▀ ▀ █ ▀▀▀ █ ▐▌ █ █ █ █ ▀
▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀▀▀ ▀▀▀▀ ▀ ▀ ▀
""", u'''
AAAAABBBBBCCCCCDDDDDEEEEEFFFFFGGGGGHHHHHIIJJJJJKKKKK
▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ █▀▀▀ █▀▀▀ ▄▀▀▄ █ █ █ █ █ █
█▄▄█ █▄▄▀ █ █ █ █▄▄ █▄▄ █ █▄▄█ █ █ █▄▀
█ █ █ █ █ ▄ █ █ █ █ █ ▀█ █ █ █ ▄ █ █ ▀▄
▀ ▀ ▀▀▀ ▀▀ ▀▀▀ ▀▀▀▀ ▀ ▀▀ ▀ ▀ ▀ ▀▀ ▀ ▀
''', u'''
LLLLLMMMMMMNNNNNOOOOOPPPPPQQQQQRRRRRSSSSSTTTTT
█ █▄ ▄█ ██ █ ▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀▄ ▀▀█▀▀
█ █ ▀ █ █▐▌█ █ █ █▄▄▀ █ █ █▄▄▀ ▀▄▄ █
█ █ █ █ ██ █ █ █ █ ▌█ █ █ ▄ █ █
▀▀▀▀ ▀ ▀ ▀ ▀ ▀▀ ▀ ▀▀▌ ▀ ▀ ▀▀ ▀
''', u'''
UUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZ
█ █ █ █ █ █ █ █ █ █ ▀▀▀█
█ █ ▐▌ ▐▌ █ ▄ █ ▀▄▀ ▀▄▀ ▄▀
█ █ █ █ ▐▌█▐▌ ▄▀ ▀▄ █ █
▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀▀
''', u'''
aaaaabbbbbcccccdddddeeeeeffffggggghhhhhiijjjjkkkkk
█ █ ▄▀▀ █ ▄ ▄ █
▀▀▄ █▀▀▄ ▄▀▀▄ ▄▀▀█ ▄▀▀▄ ▀█▀ ▄▀▀▄ █▀▀▄ ▄ ▄ █ ▄▀
▄▀▀█ █ █ █ ▄ █ █ █▀▀ █ ▀▄▄█ █ █ █ █ █▀▄
▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀▀ ▀ ▄▄▀ ▀ ▀ ▀ ▄▄▀ ▀ ▀
''', u'''
llmmmmmmnnnnnooooopppppqqqqqrrrrssssstttt
█ █
█ █▀▄▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀█ █▀▀ ▄▀▀▀ ▀█▀
█ █ █ █ █ █ █ █ █ █ █ █ █ ▀▀▄ █
▀ ▀ ▀ ▀ ▀ ▀▀ █▀▀ ▀▀█ ▀ ▀▀▀ ▀
''', u'''
uuuuuvvvvvwwwwwwxxxxxxyyyyyzzzzz
█ █ █ █ █ ▄ █ ▀▄ ▄▀ █ █ ▀▀█▀
█ █ ▐▌▐▌ ▐▌█▐▌ ▄▀▄ ▀▄▄█ ▄▀
▀▀ ▀▀ ▀ ▀ ▀ ▀ ▄▄▀ ▀▀▀▀
''']
add_font("Half Block 5x4",HalfBlock5x4Font)
class HalfBlock6x5Font(Font):
height = 5
data = [u"""
000000111111222222333333444444555555666666777777888888999999 ..::////
▄▀▀▀▄ ▄█ ▄▀▀▀▄ ▄▀▀▀▄ ▄ █ █▀▀▀▀ ▄▀▀▀ ▀▀▀▀█ ▄▀▀▀▄ ▄▀▀▀▄ █
█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ ▀ ▐▌
█ █ █ ▄▀ ▀▀▄ ▀▀▀█▀ ▀▀▀▀▄ █▀▀▀▄ █ ▄▀▀▀▄ ▀▀▀█ ▄ █
█ █ █ ▄▀ ▄ █ █ █ █ █ ▐▌ █ █ █ ▐▌
▀▀▀ ▀▀▀ ▀▀▀▀▀ ▀▀▀ ▀ ▀▀▀▀ ▀▀▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀
"""]
add_font("Half Block 6x5",HalfBlock6x5Font)
class HalfBlockHeavy6x5Font(Font):
height = 5
data = [u"""
000000111111222222333333444444555555666666777777888888999999 ..::////
▄███▄ ▐█▌ ▄███▄ ▄███▄ █▌ █████ ▄███▄ █████ ▄███▄ ▄███▄ █▌
█▌ ▐█ ▀█▌ ▀ ▐█ ▀ ▐█ █▌ █▌ █▌ █▌ █▌ █▌ ▐█ █▌ ▐█ █▌ ▐█
█▌ ▐█ █▌ ▄█▀ ██▌ █████ ████▄ ████▄ ▐█ ▐███▌ ▀████ █▌
█▌ ▐█ █▌ ▄█▀ ▄ ▐█ █▌ ▐█ █▌ ▐█ █▌ █▌ ▐█ ▐█ █▌▐█
▀███▀ ███▌ █████ ▀███▀ █▌ ████▀ ▀███▀ ▐█ ▀███▀ ▀███▀ █▌ █▌
"""]
add_font("Half Block Heavy 6x5",HalfBlockHeavy6x5Font)
class Thin6x6Font(Font):
height = 6
data = [u"""
000000111111222222333333444444555555666666777777888888999999''
┌───┐ ┐ ┌───┐ ┌───┐ ┐ ┌─── ┌─── ┌───┐ ┌───┐ ┌───┐ │
│ │ │ │ │ ┌ │ │ │ │ │ │ │ │
│ / │ │ ┌───┘ ─┤ └──┼─ └───┐ ├───┐ ┼ ├───┤ └───┤
│ │ │ │ │ │ │ │ │ │ │ │ │
└───┘ ┴ └─── └───┘ ┴ ───┘ └───┘ ┴ └───┘ ───┘
""", ur'''
!! """######$$$$$$%%%%%%&&&&&&((()))******++++++
│ ││ ┌ ┌ ┌─┼─┐ ┌┐ / ┌─┐ / \
│ ─┼─┼─ │ │ └┘ / │ │ │ │ \ / │
│ │ │ └─┼─┐ / ┌─\┘ │ │ ──X── ──┼──
│ ─┼─┼─ │ │ / ┌┐ │ \, │ │ / \
. ┘ ┘ └─┼─┘ / └┘ └───\ \ /
''', ur"""
,,-----..//////::;;<<<<=====>>>>??????@@@@@@
/ ┌───┐ ┌───┐
/ . . / ──── \ │ │┌──┤
──── / / \ ┌─┘ ││ │
/ . , \ ──── / │ │└──┘
, . / \ / . └───┘
""", ur"""
[[\\\\\\]]^^^____``{{||}}~~~~~~
\ ┐ /\ \ ┌ │ ┐
\ │ │ │ │ ┌─┐
\ │ ┤ │ ├ └─┘
\ │ │ │ │
\ ┘ ──── └ │ ┘
""", u"""
AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFFGGGGGGHHHHHHIIJJJJJJ
┌───┐ ┬───┐ ┌───┐ ┬───┐ ┬───┐ ┬───┐ ┌───┐ ┬ ┬ ┬ ┬
│ │ │ │ │ │ │ │ │ │ │ │ │ │
├───┤ ├───┤ │ │ │ ├── ├── │ ──┬ ├───┤ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ ┬ │
┴ ┴ ┴───┘ └───┘ ┴───┘ ┴───┘ ┴ └───┘ ┴ ┴ ┴ └───┘
""", u"""
KKKKKKLLLLLLMMMMMMNNNNNNOOOOOOPPPPPPQQQQQQRRRRRRSSSSSS
┬ ┬ ┬ ┌─┬─┐ ┬─┐ ┬ ┌───┐ ┬───┐ ┌───┐ ┬───┐ ┌───┐
│ ┌─┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
├─┴┐ │ │ │ │ │ │ │ │ │ ├───┘ │ │ ├─┬─┘ └───┐
│ └┐ │ │ │ │ │ │ │ │ │ │ ┐│ │ └─┐ │
┴ ┴ ┴───┘ ┴ ┴ ┴ └─┴ └───┘ ┴ └──┼┘ ┴ ┴ └───┘
""", u"""
TTTTTTUUUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZZ
┌─┬─┐ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┌───┐
│ │ │ │ │ │ │ └┐ ┌┘ │ │ ┌─┘
│ │ │ │ │ │ │ │ ├─┤ └─┬─┘ ┌┘
│ │ │ └┐ ┌┘ │ │ │ ┌┘ └┐ │ ┌┘
┴ └───┘ └─┘ └─┴─┘ ┴ ┴ ┴ └───┘
""", u"""
aaaaaabbbbbbccccccddddddeeeeeefffgggggghhhhhhiijjj
┌─┐
│ │ │ │ . .
┌───┐ ├───┐ ┌───┐ ┌───┤ ┌───┐ ┼ ┌───┐ ├───┐ ┐ ┐
┌───┤ │ │ │ │ │ ├───┘ │ │ │ │ │ │ │
└───┴ └───┘ └───┘ └───┘ └───┘ ┴ └───┤ ┴ ┴ ┴ │
└───┘ ─┘
""", u"""
kkkkkkllmmmmmmnnnnnnooooooppppppqqqqqqrrrrrssssss
│ │
│ ┌─ │ ┬─┬─┐ ┬───┐ ┌───┐ ┌───┐ ┌───┐ ┬──┐ ┌───┐
├─┴┐ │ │ │ │ │ │ │ │ │ │ │ │ │ └───┐
┴ └─ └ ┴ ┴ ┴ ┴ └───┘ ├───┘ └───┤ ┴ └───┘
│ │
""", u"""
ttttuuuuuuvvvvvvwwwwwwxxxxxxyyyyyyzzzzzz
─┼─ ┬ ┬ ┬ ┬ ┬ ┬ ─┐ ┌─ ┬ ┬ ────┬
│ │ │ └┐ ┌┘ │ │ │ ├─┤ │ │ ┌───┘
└─ └───┴ └─┘ └─┴─┘ ─┘ └─ └───┤ ┴────
└───┘
"""]
add_font("Thin 6x6",Thin6x6Font)
class HalfBlock7x7Font(Font):
height = 7
data = [u"""
0000000111111122222223333333444444455555556666666777777788888889999999'''
▄███▄ ▐█▌ ▄███▄ ▄███▄ █▌ ▐█████▌ ▄███▄ ▐█████▌ ▄███▄ ▄███▄ ▐█
▐█ █▌ ▀█▌ ▐█ █▌▐█ █▌▐█ █▌ ▐█ ▐█ ▐█ ▐█ █▌▐█ █▌▐█
▐█ ▐ █▌ █▌ █▌ ▐██ ▐█████▌▐████▄ ▐████▄ █▌ █████ ▀████▌
▐█ ▌ █▌ █▌ ▄█▀ █▌ █▌ █▌▐█ █▌ ▐█ ▐█ █▌ █▌
▐█ █▌ █▌ ▄█▀ ▐█ █▌ █▌ █▌▐█ █▌ █▌ ▐█ █▌ █▌
▀███▀ ███▌ ▐█████▌ ▀███▀ █▌ ▐████▀ ▀███▀ ▐█ ▀███▀ ▀███▀
""", u'''
!!! """""#######$$$$$$$%%%%%%%&&&&&&&(((())))*******++++++
▐█ ▐█ █▌ ▐█ █▌ █ ▄ █▌ ▄█▄ █▌▐█ ▄▄ ▄▄
▐█ ▐█ █▌▐█████▌ ▄███▄ ▐█▌▐█ ▐█ █▌ ▐█ █▌ ▀█▄█▀ ▐█
▐█ ▐█ █▌ ▐█▄█▄▄ ▀ █▌ ███ █▌ ▐█ ▐█████▌ ████▌
▐█ ▐█████▌ ▀▀█▀█▌ ▐█ ▄ ███▌▄ █▌ ▐█ ▄█▀█▄ ▐█
▐█ █▌ ▀███▀ █▌▐█▌▐█ █▌ ▐█ █▌ ▀▀ ▀▀
▐█ █ ▐█ ▀ ▀██▀█▌ █▌▐█
''', u"""
,,,------.../////:::;;;<<<<<<<======>>>>>>>???????@@@@@@@
█▌ ▄█▌ ▐█▄ ▄███▄ ▄███▄
▐█ ▐█ ▐█ ▄█▀ ▐████▌ ▀█▄ ▐█ █▌▐█ ▄▄█▌
▐████▌ █▌ ▐██ ██▌ █▌ ▐█▐█▀█▌
▐█ ▐█ ▐█ ▀█▄ ▐████▌ ▄█▀ █▌ ▐█▐█▄█▌
█▌ ▀ ▀█▌ ▐█▀ ▐█ ▀▀▀
▐█ ▐█ ▐█ █▌ ▀███▀
""", ur"""
[[[[\\\\\]]]]^^^^^^^_____```{{{{{|||}}}}}~~~~~~~´´´
▐██▌▐█ ▐██▌ ▐█▌ ▐█ █▌▐█ ▐█ █▌
▐█ █▌ █▌ ▐█ █▌ █▌ █▌ ▐█ ▐█ ▄▄ ▐█
▐█ ▐█ █▌▐█ █▌ ▄█▌ ▐█ ▐█▄ ▐▀▀█▄▄▌
▐█ █▌ █▌ ▀█▌ ▐█ ▐█▀ ▀▀
▐█ ▐█ █▌ █▌ ▐█ ▐█
▐██▌ █▌▐██▌ █████ █▌▐█ ▐█
""", u"""
AAAAAAABBBBBBBCCCCCCCDDDDDDDEEEEEEEFFFFFFFGGGGGGGHHHHHHHIIIIJJJJJJJ
▄███▄ ▐████▄ ▄███▄ ▐████▄ ▐█████▌▐█████▌ ▄███▄ ▐█ █▌ ██▌ █▌
▐█ █▌▐█ █▌▐█ ▐█ █▌▐█ ▐█ ▐█ ▐█ █▌ ▐█ █▌
▐█████▌▐█████ ▐█ ▐█ █▌▐████ ▐████ ▐█ ▐█████▌ ▐█ █▌
▐█ █▌▐█ █▌▐█ ▐█ █▌▐█ ▐█ ▐█ ██▌▐█ █▌ ▐█ █▌
▐█ █▌▐█ █▌▐█ ▐█ █▌▐█ ▐█ ▐█ █▌▐█ █▌ ▐█ ▐█ █▌
▐█ █▌▐████▀ ▀███▀ ▐████▀ ▐█████▌▐█ ▀███▀ ▐█ █▌ ██▌ ▀███▀
""", u"""
KKKKKKKLLLLLLLMMMMMMMMNNNNNNNOOOOOOOPPPPPPPQQQQQQQRRRRRRRSSSSSSS
▐█ █▌▐█ ▄█▌▐█▄ ▐██ █▌ ▄███▄ ▐████▄ ▄███▄ ▐████▄ ▄███▄
▐█ █▌ ▐█ ▐█ ▐▌ █▌▐██▌ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█
▐█▄█▌ ▐█ ▐█ ▐▌ █▌▐█▐█ █▌▐█ █▌▐████▀ ▐█ █▌▐█████ ▀███▄
▐█▀█▌ ▐█ ▐█ █▌▐█ █▌█▌▐█ █▌▐█ ▐█ █▌▐█ █▌ █▌
▐█ █▌ ▐█ ▐█ █▌▐█ ▐██▌▐█ █▌▐█ ▐█ █▌█▌▐█ █▌ █▌
▐█ █▌▐█████▌▐█ █▌▐█ ██▌ ▀███▀ ▐█ ▀███▀ ▐█ █▌ ▀███▀
▀▀
""", u"""
TTTTTTTUUUUUUUVVVVVVVWWWWWWWWXXXXXXXYYYYYYYZZZZZZZ
█████▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌ █▌ █▌▐█████▌
█▌ ▐█ █▌ █▌ ▐█ ▐█ █▌ ▐█ █▌ ▐█ ▐█ █▌
█▌ ▐█ █▌ ▐█ █▌ ▐█ █▌ ▐█▌ ▐██ █▌
█▌ ▐█ █▌ ███ ▐█ ▐▌ █▌ ███ █▌ █▌
█▌ ▐█ █▌ ▐█▌ ▐█ ▐▌ █▌ █▌ ▐█ █▌ █▌
█▌ ▀███▀ █ ▀█▌▐█▀ ▐█ █▌ █▌ ▐█████▌
""", u"""
aaaaaaabbbbbbbcccccccdddddddeeeeeeefffffggggggghhhhhhhiiijjjj
▐█ █▌ ▄█▌ ▐█ █▌ █▌
▐█ █▌ ▐█ ▐█
▄███▄ ▐████▄ ▄███▄ ▄████▌ ▄███▄ ▐███ ▄███▄ ▐████▄ ▐█▌ ▐█▌
▄▄▄█▌▐█ █▌▐█ ▐█ █▌▐█▄▄▄█▌ ▐█ ▐█ █▌▐█ █▌ █▌ █▌
▐█▀▀▀█▌▐█ █▌▐█ ▐█ █▌▐█▀▀▀ ▐█ ▐█▄▄▄█▌▐█ █▌ █▌ █▌
▀████▌▐████▀ ▀███▀ ▀████▌ ▀███▀ ▐█ ▀▀▀█▌▐█ █▌ █▌ █▌
▀███▀ ▐██
""", u"""
kkkkkkkllllmmmmmmmmnnnnnnnooooooopppppppqqqqqqqrrrrrrsssssss
▐█ ██
▐█ ▐█
▐█ ▄█▌ ▐█ ▄█▌▐█▄ ▐████▄ ▄███▄ ▐████▄ ▄████▌ ▄███▌ ▄███▄
▐█▄█▀ ▐█ ▐█ ▐▌ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█ ▐█▄▄▄
▐█▀▀█▄ ▐█ ▐█ ▐▌ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█ ▀▀▀█▌
▐█ █▌ ▐█▌▐█ █▌▐█ █▌ ▀███▀ ▐████▀ ▀████▌▐█ ▀███▀
▐█ █▌
""", u"""
tttttuuuuuuuvvvvvvvwwwwwwwwxxxxxxxyyyyyyyzzzzzzz
█▌
█▌
███▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█ █▌▐█████▌
█▌ ▐█ █▌ █▌ ▐█ ▐█ █▌ ▀█▄█▀ ▐█ █▌ ▄█▀
█▌ ▐█ █▌ ███ ▐█ ▐▌ █▌ ▄█▀█▄ ▐█▄▄▄█▌ ▄█▀
█▌ ▀███▀ ▐█▌ ▀█▌▐█▀ ▐█ █▌ ▀▀▀█▌▐█████▌
▀███▀
"""]
add_font("Half Block 7x7",HalfBlock7x7Font)
if __name__ == "__main__":
l = get_all_fonts()
all_ascii = "".join([chr(x) for x in range(32, 127)])
print "Available Fonts: (U) = UTF-8 required"
print "----------------"
for n,cls in l:
f = cls()
u = ""
if f.utf8_required:
u = "(U)"
print ("%-20s %3s " % (n,u)),
c = f.characters()
if c == all_ascii:
print "Full ASCII"
elif c.startswith(all_ascii):
print "Full ASCII + " + c[len(all_ascii):]
else:
print "Characters: " + c

View File

@@ -1,911 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid graphics widgets
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from urwid.util import decompose_tagmarkup, get_encoding_mode
from urwid.canvas import CompositeCanvas, CanvasJoin, TextCanvas, \
CanvasCombine, SolidCanvas
from urwid.widget import WidgetMeta, Widget, BOX, FIXED, FLOW, \
nocache_widget_render, nocache_widget_render_instance, fixed_size, \
WidgetWrap, Divider, SolidFill, Text, CENTER, CLIP
from urwid.container import Pile, Columns
from urwid.display_common import AttrSpec
from urwid.decoration import WidgetDecoration
class BigText(Widget):
_sizing = frozenset([FIXED])
def __init__(self, markup, font):
"""
markup -- same as Text widget markup
font -- instance of a Font class
"""
self.set_font(font)
self.set_text(markup)
def set_text(self, markup):
self.text, self.attrib = decompose_tagmarkup(markup)
self._invalidate()
def get_text(self):
"""
Returns (text, attributes).
"""
return self.text, self.attrib
def set_font(self, font):
self.font = font
self._invalidate()
def pack(self, size=None, focus=False):
rows = self.font.height
cols = 0
for c in self.text:
cols += self.font.char_width(c)
return cols, rows
def render(self, size, focus=False):
fixed_size(size) # complain if parameter is wrong
a = None
ai = ak = 0
o = []
rows = self.font.height
attrib = self.attrib + [(None, len(self.text))]
for ch in self.text:
if not ak:
a, ak = attrib[ai]
ai += 1
ak -= 1
width = self.font.char_width(ch)
if not width:
# ignore invalid characters
continue
c = self.font.render(ch)
if a is not None:
c = CompositeCanvas(c)
c.fill_attr(a)
o.append((c, None, False, width))
if o:
canv = CanvasJoin(o)
else:
canv = TextCanvas([""] * rows, maxcol=0,
check_width=False)
canv = CompositeCanvas(canv)
canv.set_depends([])
return canv
class LineBox(WidgetDecoration, WidgetWrap):
def __init__(self, original_widget, title="",
tlcorner=u'', tline=u'', lline=u'',
trcorner=u'', blcorner=u'', rline=u'',
bline=u'', brcorner=u''):
"""
Draw a line around original_widget.
Use 'title' to set an initial title text with will be centered
on top of the box.
You can also override the widgets used for the lines/corners:
tline: top line
bline: bottom line
lline: left line
rline: right line
tlcorner: top left corner
trcorner: top right corner
blcorner: bottom left corner
brcorner: bottom right corner
"""
tline, bline = Divider(tline), Divider(bline)
lline, rline = SolidFill(lline), SolidFill(rline)
tlcorner, trcorner = Text(tlcorner), Text(trcorner)
blcorner, brcorner = Text(blcorner), Text(brcorner)
self.title_widget = Text(self.format_title(title))
self.tline_widget = Columns([
tline,
('flow', self.title_widget),
tline,
])
top = Columns([
('fixed', 1, tlcorner),
self.tline_widget,
('fixed', 1, trcorner)
])
middle = Columns([
('fixed', 1, lline),
original_widget,
('fixed', 1, rline),
], box_columns=[0, 2], focus_column=1)
bottom = Columns([
('fixed', 1, blcorner), bline, ('fixed', 1, brcorner)
])
pile = Pile([('flow', top), middle, ('flow', bottom)], focus_item=1)
WidgetDecoration.__init__(self, original_widget)
WidgetWrap.__init__(self, pile)
def format_title(self, text):
if len(text) > 0:
return " %s " % text
else:
return ""
def set_title(self, text):
self.title_widget.set_text(self.format_title(text))
self.tline_widget._invalidate()
class BarGraphMeta(WidgetMeta):
"""
Detect subclass get_data() method and dynamic change to
get_data() method and disable caching in these cases.
This is for backwards compatibility only, new programs
should use set_data() instead of overriding get_data().
"""
def __init__(cls, name, bases, d):
super(BarGraphMeta, cls).__init__(name, bases, d)
if "get_data" in d:
cls.render = nocache_widget_render(cls)
cls._get_data = cls.get_data
cls.get_data = property(
lambda self: self._get_data,
nocache_bargraph_get_data)
def nocache_bargraph_get_data(self, get_data_fn):
"""
Disable caching on this bargraph because get_data_fn needs
to be polled to get the latest data.
"""
self.render = nocache_widget_render_instance(self)
self._get_data = get_data_fn
class BarGraphError(Exception):
pass
class BarGraph(Widget):
__metaclass__ = BarGraphMeta
_sizing = frozenset([BOX])
ignore_focus = True
eighths = u' ▁▂▃▄▅▆▇'
hlines = u'_⎺⎻─⎼⎽'
def __init__(self, attlist, hatt=None, satt=None):
"""
Create a bar graph with the passed display characteristics.
see set_segment_attributes for a description of the parameters.
"""
self.set_segment_attributes(attlist, hatt, satt)
self.set_data([], 1, None)
self.set_bar_width(None)
def set_segment_attributes(self, attlist, hatt=None, satt=None):
"""
:param attlist: list containing display attribute or
(display attribute, character) tuple for background,
first segment, and optionally following segments.
ie. len(attlist) == num segments+1
character defaults to ' ' if not specified.
:param hatt: list containing attributes for horizontal lines. First
element is for lines on background, second is for lines
on first segment, third is for lines on second segment
etc.
:param satt: dictionary containing attributes for smoothed
transitions of bars in UTF-8 display mode. The values
are in the form:
(fg,bg) : attr
fg and bg are integers where 0 is the graph background,
1 is the first segment, 2 is the second, ...
fg > bg in all values. attr is an attribute with a
foreground corresponding to fg and a background
corresponding to bg.
If satt is not None and the bar graph is being displayed in
a terminal using the UTF-8 encoding then the character cell
that is shared between the segments specified will be smoothed
with using the UTF-8 vertical eighth characters.
eg: set_segment_attributes( ['no', ('unsure',"?"), 'yes'] )
will use the attribute 'no' for the background (the area from
the top of the graph to the top of the bar), question marks
with the attribute 'unsure' will be used for the topmost
segment of the bar, and the attribute 'yes' will be used for
the bottom segment of the bar.
"""
self.attr = []
self.char = []
if len(attlist) < 2:
raise BarGraphError("attlist must include at least background and seg1: %r" % (attlist,))
assert len(attlist) >= 2, 'must at least specify bg and fg!'
for a in attlist:
if type(a) != tuple:
self.attr.append(a)
self.char.append(' ')
else:
attr, ch = a
self.attr.append(attr)
self.char.append(ch)
self.hatt = []
if hatt is None:
hatt = [self.attr[0]]
elif type(hatt) != list:
hatt = [hatt]
self.hatt = hatt
if satt is None:
satt = {}
for i in satt.items():
try:
(fg, bg), attr = i
except ValueError:
raise BarGraphError("satt not in (fg,bg:attr) form: %r" % (i,))
if type(fg) != int or fg >= len(attlist):
raise BarGraphError("fg not valid integer: %r" % (fg,))
if type(bg) != int or bg >= len(attlist):
raise BarGraphError("bg not valid integer: %r" % (fg,))
if fg <= bg:
raise BarGraphError("fg (%s) not > bg (%s)" % (fg, bg))
self.satt = satt
def set_data(self, bardata, top, hlines=None):
"""
Store bar data, bargraph top and horizontal line positions.
bardata -- a list of bar values.
top -- maximum value for segments within bardata
hlines -- None or a bar value marking horizontal line positions
bar values are [ segment1, segment2, ... ] lists where top is
the maximal value corresponding to the top of the bar graph and
segment1, segment2, ... are the values for the top of each
segment of this bar. Simple bar graphs will only have one
segment in each bar value.
Eg: if top is 100 and there is a bar value of [ 80, 30 ] then
the top of this bar will be at 80% of full height of the graph
and it will have a second segment that starts at 30%.
"""
if hlines is not None:
hlines = hlines[:] # shallow copy
hlines.sort()
hlines.reverse()
self.data = bardata, top, hlines
self._invalidate()
def _get_data(self, size):
"""
Return (bardata, top, hlines)
This function is called by render to retrieve the data for
the graph. It may be overloaded to create a dynamic bar graph.
This implementation will truncate the bardata list returned
if not all bars will fit within maxcol.
"""
(maxcol, maxrow) = size
bardata, top, hlines = self.data
widths = self.calculate_bar_widths((maxcol, maxrow), bardata)
if len(bardata) > len(widths):
return bardata[:len(widths)], top, hlines
return bardata, top, hlines
def set_bar_width(self, width):
"""
Set a preferred bar width for calculate_bar_widths to use.
width -- width of bar or None for automatic width adjustment
"""
assert width is None or width > 0
self.bar_width = width
self._invalidate()
def calculate_bar_widths(self, size, bardata):
"""
Return a list of bar widths, one for each bar in data.
If self.bar_width is None this implementation will stretch
the bars across the available space specified by maxcol.
"""
(maxcol, maxrow) = size
if self.bar_width is not None:
return [self.bar_width] * min(
len(bardata), maxcol / self.bar_width)
if len(bardata) >= maxcol:
return [1] * maxcol
widths = []
grow = maxcol
remain = len(bardata)
for row in bardata:
w = int(float(grow) / remain + 0.5)
widths.append(w)
grow -= w
remain -= 1
return widths
def selectable(self):
"""
Return False.
"""
return False
def use_smoothed(self):
return self.satt and get_encoding_mode() == "utf8"
def calculate_display(self, size):
"""
Calculate display data.
"""
(maxcol, maxrow) = size
bardata, top, hlines = self.get_data((maxcol, maxrow))
widths = self.calculate_bar_widths((maxcol, maxrow), bardata)
if self.use_smoothed():
disp = calculate_bargraph_display(bardata, top, widths,
maxrow * 8)
disp = self.smooth_display(disp)
else:
disp = calculate_bargraph_display(bardata, top, widths,
maxrow)
if hlines:
disp = self.hlines_display(disp, top, hlines, maxrow)
return disp
def hlines_display(self, disp, top, hlines, maxrow):
"""
Add hlines to display structure represented as bar_type tuple
values:
(bg, 0-5)
bg is the segment that has the hline on it
0-5 is the hline graphic to use where 0 is a regular underscore
and 1-5 are the UTF-8 horizontal scan line characters.
"""
if self.use_smoothed():
shiftr = 0
r = [(0.2, 1),
(0.4, 2),
(0.6, 3),
(0.8, 4),
(1.0, 5), ]
else:
shiftr = 0.5
r = [(1.0, 0), ]
# reverse the hlines to match screen ordering
rhl = []
for h in hlines:
rh = float(top - h) * maxrow / top - shiftr
if rh < 0:
continue
rhl.append(rh)
# build a list of rows that will have hlines
hrows = []
last_i = -1
for rh in rhl:
i = int(rh)
if i == last_i:
continue
f = rh - i
for spl, chnum in r:
if f < spl:
hrows.append((i, chnum))
break
last_i = i
# fill hlines into disp data
def fill_row(row, chnum):
rout = []
for bar_type, width in row:
if (type(bar_type) == int and
len(self.hatt) > bar_type):
rout.append(((bar_type, chnum), width))
continue
rout.append((bar_type, width))
return rout
o = []
k = 0
rnum = 0
for y_count, row in disp:
if k >= len(hrows):
o.append((y_count, row))
continue
end_block = rnum + y_count
while k < len(hrows) and hrows[k][0] < end_block:
i, chnum = hrows[k]
if i - rnum > 0:
o.append((i - rnum, row))
o.append((1, fill_row(row, chnum)))
rnum = i + 1
k += 1
if rnum < end_block:
o.append((end_block - rnum, row))
rnum = end_block
#assert 0, o
return o
def smooth_display(self, disp):
"""
smooth (col, row*8) display into (col, row) display using
UTF vertical eighth characters represented as bar_type
tuple values:
( fg, bg, 1-7 )
where fg is the lower segment, bg is the upper segment and
1-7 is the vertical eighth character to use.
"""
o = []
r = 0 # row remainder
def seg_combine((bt1, w1), (bt2, w2)):
if (bt1, w1) == (bt2, w2):
return (bt1, w1), None, None
wmin = min(w1, w2)
l1 = l2 = None
if w1 > w2:
l1 = (bt1, w1 - w2)
elif w2 > w1:
l2 = (bt2, w2 - w1)
if type(bt1) == tuple:
return (bt1, wmin), l1, l2
if (bt2, bt1) not in self.satt:
if r < 4:
return (bt2, wmin), l1, l2
return (bt1, wmin), l1, l2
return ((bt2, bt1, 8 - r), wmin), l1, l2
def row_combine_last(count, row):
o_count, o_row = o[-1]
row = row[:] # shallow copy, so we don't destroy orig.
o_row = o_row[:]
l = []
while row:
(bt, w), l1, l2 = seg_combine(
o_row.pop(0), row.pop(0))
if l and l[-1][0] == bt:
l[-1] = (bt, l[-1][1] + w)
else:
l.append((bt, w))
if l1:
o_row = [l1] + o_row
if l2:
row = [l2] + row
assert not o_row
o[-1] = (o_count + count, l)
# regroup into actual rows (8 disp rows == 1 actual row)
for y_count, row in disp:
if r:
count = min(8 - r, y_count)
row_combine_last(count, row)
y_count -= count
r += count
r = r % 8
if not y_count:
continue
assert r == 0
# copy whole blocks
if y_count > 7:
o.append((y_count // 8 * 8, row))
y_count = y_count % 8
if not y_count:
continue
o.append((y_count, row))
r = y_count
return [(y // 8, row) for (y, row) in o]
def render(self, size, focus=False):
"""
Render BarGraph.
"""
(maxcol, maxrow) = size
disp = self.calculate_display((maxcol, maxrow))
combinelist = []
for y_count, row in disp:
l = []
for bar_type, width in row:
if type(bar_type) == tuple:
if len(bar_type) == 3:
# vertical eighths
fg, bg, k = bar_type
a = self.satt[(fg, bg)]
t = self.eighths[k] * width
else:
# horizontal lines
bg, k = bar_type
a = self.hatt[bg]
t = self.hlines[k] * width
else:
a = self.attr[bar_type]
t = self.char[bar_type] * width
l.append((a, t))
c = Text(l).render((maxcol,))
assert c.rows() == 1, "Invalid characters in BarGraph!"
combinelist += [(c, None, False)] * y_count
canv = CanvasCombine(combinelist)
return canv
def calculate_bargraph_display(bardata, top, bar_widths, maxrow):
"""
Calculate a rendering of the bar graph described by data, bar_widths
and height.
bardata -- bar information with same structure as BarGraph.data
top -- maximal value for bardata segments
bar_widths -- list of integer column widths for each bar
maxrow -- rows for display of bargraph
Returns a structure as follows:
[ ( y_count, [ ( bar_type, width), ... ] ), ... ]
The outer tuples represent a set of identical rows. y_count is
the number of rows in this set, the list contains the data to be
displayed in the row repeated through the set.
The inner tuple describes a run of width characters of bar_type.
bar_type is an integer starting from 0 for the background, 1 for
the 1st segment, 2 for the 2nd segment etc..
This function should complete in approximately O(n+m) time, where
n is the number of bars displayed and m is the number of rows.
"""
assert len(bardata) == len(bar_widths)
maxcol = sum(bar_widths)
# build intermediate data structure
rows = [None] * maxrow
def add_segment(seg_num, col, row, width, rows=rows):
if rows[row]:
last_seg, last_col, last_end = rows[row][-1]
if last_end > col:
if last_col >= col:
del rows[row][-1]
else:
rows[row][-1] = (last_seg,
last_col, col)
elif last_seg == seg_num and last_end == col:
rows[row][-1] = (last_seg, last_col,
last_end + width)
return
elif rows[row] is None:
rows[row] = []
rows[row].append((seg_num, col, col + width))
col = 0
barnum = 0
for bar in bardata:
width = bar_widths[barnum]
if width < 1:
continue
# loop through in reverse order
tallest = maxrow
segments = scale_bar_values(bar, top, maxrow)
for k in range(len(bar) - 1, -1, -1):
s = segments[k]
if s >= maxrow:
continue
if s < 0:
s = 0
if s < tallest:
# add only properly-overlapped bars
tallest = s
add_segment(k + 1, col, s, width)
col += width
barnum += 1
#print repr(rows)
# build rowsets data structure
rowsets = []
y_count = 0
last = [(0, maxcol)]
for r in rows:
if r is None:
y_count = y_count + 1
continue
if y_count:
rowsets.append((y_count, last))
y_count = 0
i = 0 # index into "last"
la, ln = last[i] # last attribute, last run length
c = 0 # current column
o = [] # output list to be added to rowsets
for seg_num, start, end in r:
while start > c + ln:
o.append((la, ln))
i += 1
c += ln
la, ln = last[i]
if la == seg_num:
# same attribute, can combine
o.append((la, end - c))
else:
if start - c > 0:
o.append((la, start - c))
o.append((seg_num, end - start))
if end == maxcol:
i = len(last)
break
# skip past old segments covered by new one
while end >= c + ln:
i += 1
c += ln
la, ln = last[i]
if la != seg_num:
ln = c + ln - end
c = end
continue
# same attribute, can extend
oa, on = o[-1]
on += c + ln - end
o[-1] = oa, on
i += 1
c += ln
if c == maxcol:
break
assert i < len(last), repr((on, maxcol))
la, ln = last[i]
if i < len(last):
o += [(la, ln)] + last[i + 1:]
last = o
y_count += 1
if y_count:
rowsets.append((y_count, last))
return rowsets
class GraphVScale(Widget):
_sizing = frozenset([BOX])
def __init__(self, labels, top):
"""
GraphVScale( [(label1 position, label1 markup),...], top )
label position -- 0 < position < top for the y position
label markup -- text markup for this label
top -- top y position
This widget is a vertical scale for the BarGraph widget that
can correspond to the BarGraph's horizontal lines
"""
self.set_scale(labels, top)
def set_scale(self, labels, top):
"""
set_scale( [(label1 position, label1 markup),...], top )
label position -- 0 < position < top for the y position
label markup -- text markup for this label
top -- top y position
"""
labels = labels[:] # shallow copy
labels.sort()
labels.reverse()
self.pos = []
self.txt = []
for y, markup in labels:
self.pos.append(y)
self.txt.append(Text(markup))
self.top = top
def selectable(self):
"""
Return False.
"""
return False
def render(self, size, focus=False):
"""
Render GraphVScale.
"""
(maxcol, maxrow) = size
pl = scale_bar_values(self.pos, self.top, maxrow)
combinelist = []
rows = 0
for p, t in zip(pl, self.txt):
p -= 1
if p >= maxrow:
break
if p < rows:
continue
c = t.render((maxcol,))
if p > rows:
run = p - rows
c = CompositeCanvas(c)
c.pad_trim_top_bottom(run, 0)
rows += c.rows()
combinelist.append((c, None, False))
if not combinelist:
return SolidCanvas(" ", size[0], size[1])
c = CanvasCombine(combinelist)
if maxrow - rows:
c.pad_trim_top_bottom(0, maxrow - rows)
return c
def scale_bar_values( bar, top, maxrow ):
"""
Return a list of bar values aliased to integer values of maxrow.
"""
return [maxrow - int(float(v) * maxrow / top + 0.5) for v in bar]
class ProgressBar(Widget):
_sizing = frozenset([FLOW])
eighths = u' ▏▎▍▌▋▊▉'
text_align = CENTER
def __init__(self, normal, complete, current=0, done=100, satt=None):
"""
:param normal: display attribute for incomplete part of progress bar
:param complete: display attribute for complete part of progress bar
:param current: current progress
:param done: progress amount at 100%
:param satt: display attribute for smoothed part of bar where the
foreground of satt corresponds to the normal part and the
background corresponds to the complete part. If satt
is ``None`` then no smoothing will be done.
"""
self.normal = normal
self.complete = complete
self._current = current
self._done = done
self.satt = satt
def set_completion(self, current):
"""
current -- current progress
"""
self._current = current
self._invalidate()
current = property(lambda self: self._current, set_completion)
def _set_done(self, done):
"""
done -- progress amount at 100%
"""
self._done = done
self._invalidate()
done = property(lambda self: self._done, _set_done)
def rows(self, size, focus=False):
return 1
def get_text(self):
"""
Return the progress bar percentage text.
"""
percent = min(100, max(0, int(self.current * 100 / self.done)))
return str(percent) + " %"
def render(self, size, focus=False):
"""
Render the progress bar.
"""
(maxcol,) = size
txt = Text(self.get_text(), self.text_align, CLIP)
c = txt.render((maxcol,))
cf = float(self.current) * maxcol / self.done
ccol = int(cf)
cs = 0
if self.satt is not None:
cs = int((cf - ccol) * 8)
if ccol < 0 or (ccol == 0 and cs == 0):
c._attr = [[(self.normal, maxcol)]]
elif ccol >= maxcol:
c._attr = [[(self.complete, maxcol)]]
elif cs and c._text[0][ccol] == " ":
t = c._text[0]
cenc = self.eighths[cs].encode("utf-8")
c._text[0] = t[:ccol] + cenc + t[ccol + 1:]
a = []
if ccol > 0:
a.append((self.complete, ccol))
a.append((self.satt, len(cenc)))
if maxcol - ccol - 1 > 0:
a.append((self.normal, maxcol - ccol - 1))
c._attr = [a]
c._cs = [[(None, len(c._text[0]))]]
else:
c._attr = [[(self.complete, ccol),
(self.normal, maxcol - ccol)]]
return c
class PythonLogo(Widget):
_sizing = frozenset([FIXED])
def __init__(self):
"""
Create canvas containing an ASCII version of the Python
Logo and store it.
"""
blu = AttrSpec('light blue', 'default')
yel = AttrSpec('yellow', 'default')
width = 17
self._canvas = Text([
(blu, " ______\n"),
(blu, " _|_o__ |"), (yel, "__\n"),
(blu, " | _____|"), (yel, " |\n"),
(blu, " |__| "), (yel, "______|\n"),
(yel, " |____o_|")]).render((width,))
def pack(self, size=None, focus=False):
"""
Return the size from our pre-rendered canvas.
"""
return self._canvas.cols(), self._canvas.rows()
def render(self, size, focus=False):
"""
Return the pre-rendered canvas.
"""
fixed_size(size)
return self._canvas

View File

@@ -1,245 +0,0 @@
#!/usr/bin/python
#
# Urwid html fragment output wrapper for "screen shots"
# Copyright (C) 2004-2007 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
"""
HTML PRE-based UI implementation
"""
from urwid import util
from urwid.main_loop import ExitMainLoop
from urwid.display_common import AttrSpec, BaseScreen
# replace control characters with ?'s
_trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)])
_default_foreground = 'black'
_default_background = 'light gray'
class HtmlGeneratorSimulationError(Exception):
pass
class HtmlGenerator(BaseScreen):
# class variables
fragments = []
sizes = []
keys = []
started = True
def __init__(self):
super(HtmlGenerator, self).__init__()
self.colors = 16
self.bright_is_bold = False # ignored
self.has_underline = True # ignored
self.register_palette_entry(None,
_default_foreground, _default_background)
def set_terminal_properties(self, colors=None, bright_is_bold=None,
has_underline=None):
if colors is None:
colors = self.colors
if bright_is_bold is None:
bright_is_bold = self.bright_is_bold
if has_underline is None:
has_underline = self.has_underline
self.colors = colors
self.bright_is_bold = bright_is_bold
self.has_underline = has_underline
def set_mouse_tracking(self, enable=True):
"""Not yet implemented"""
pass
def set_input_timeouts(self, *args):
pass
def reset_default_terminal_palette(self, *args):
pass
def draw_screen(self, (cols, rows), r ):
"""Create an html fragment from the render object.
Append it to HtmlGenerator.fragments list.
"""
# collect output in l
l = []
assert r.rows() == rows
if r.cursor is not None:
cx, cy = r.cursor
else:
cx = cy = None
y = -1
for row in r.content():
y += 1
col = 0
for a, cs, run in row:
run = run.translate(_trans_table)
if isinstance(a, AttrSpec):
aspec = a
else:
aspec = self._palette[a][
{1: 1, 16: 0, 88:2, 256:3}[self.colors]]
if y == cy and col <= cx:
run_width = util.calc_width(run, 0,
len(run))
if col+run_width > cx:
l.append(html_span(run,
aspec, cx-col))
else:
l.append(html_span(run, aspec))
col += run_width
else:
l.append(html_span(run, aspec))
l.append("\n")
# add the fragment to the list
self.fragments.append( "<pre>%s</pre>" % "".join(l) )
def clear(self):
"""
Force the screen to be completely repainted on the next
call to draw_screen().
(does nothing for html_fragment)
"""
pass
def get_cols_rows(self):
"""Return the next screen size in HtmlGenerator.sizes."""
if not self.sizes:
raise HtmlGeneratorSimulationError, "Ran out of screen sizes to return!"
return self.sizes.pop(0)
def get_input(self, raw_keys=False):
"""Return the next list of keypresses in HtmlGenerator.keys."""
if not self.keys:
raise ExitMainLoop()
if raw_keys:
return (self.keys.pop(0), [])
return self.keys.pop(0)
_default_aspec = AttrSpec(_default_foreground, _default_background)
(_d_fg_r, _d_fg_g, _d_fg_b, _d_bg_r, _d_bg_g, _d_bg_b) = (
_default_aspec.get_rgb_values())
def html_span(s, aspec, cursor = -1):
fg_r, fg_g, fg_b, bg_r, bg_g, bg_b = aspec.get_rgb_values()
# use real colours instead of default fg/bg
if fg_r is None:
fg_r, fg_g, fg_b = _d_fg_r, _d_fg_g, _d_fg_b
if bg_r is None:
bg_r, bg_g, bg_b = _d_bg_r, _d_bg_g, _d_bg_b
html_fg = "#%02x%02x%02x" % (fg_r, fg_g, fg_b)
html_bg = "#%02x%02x%02x" % (bg_r, bg_g, bg_b)
if aspec.standout:
html_fg, html_bg = html_bg, html_fg
extra = (";text-decoration:underline" * aspec.underline +
";font-weight:bold" * aspec.bold)
def html_span(fg, bg, s):
if not s: return ""
return ('<span style="color:%s;'
'background:%s%s">%s</span>' %
(fg, bg, extra, html_escape(s)))
if cursor >= 0:
c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor)
c2_off = util.move_next_char(s, c_off, len(s))
return (html_span(html_fg, html_bg, s[:c_off]) +
html_span(html_bg, html_fg, s[c_off:c2_off]) +
html_span(html_fg, html_bg, s[c2_off:]))
else:
return html_span(html_fg, html_bg, s)
def html_escape(text):
"""Escape text so that it will be displayed safely within HTML"""
text = text.replace('&','&amp;')
text = text.replace('<','&lt;')
text = text.replace('>','&gt;')
return text
def screenshot_init( sizes, keys ):
"""
Replace curses_display.Screen and raw_display.Screen class with
HtmlGenerator.
Call this function before executing an application that uses
curses_display.Screen to have that code use HtmlGenerator instead.
sizes -- list of ( columns, rows ) tuples to be returned by each call
to HtmlGenerator.get_cols_rows()
keys -- list of lists of keys to be returned by each call to
HtmlGenerator.get_input()
Lists of keys may include "window resize" to force the application to
call get_cols_rows and read a new screen size.
For example, the following call will prepare an application to:
1. start in 80x25 with its first call to get_cols_rows()
2. take a screenshot when it calls draw_screen(..)
3. simulate 5 "down" keys from get_input()
4. take a screenshot when it calls draw_screen(..)
5. simulate keys "a", "b", "c" and a "window resize"
6. resize to 20x10 on its second call to get_cols_rows()
7. take a screenshot when it calls draw_screen(..)
8. simulate a "Q" keypress to quit the application
screenshot_init( [ (80,25), (20,10) ],
[ ["down"]*5, ["a","b","c","window resize"], ["Q"] ] )
"""
try:
for (row,col) in sizes:
assert type(row) == int
assert row>0 and col>0
except (AssertionError, ValueError):
raise Exception, "sizes must be in the form [ (col1,row1), (col2,row2), ...]"
try:
for l in keys:
assert type(l) == list
for k in l:
assert type(k) == str
except (AssertionError, ValueError):
raise Exception, "keys must be in the form [ [keyA1, keyA2, ..], [keyB1, ..], ...]"
import curses_display
curses_display.Screen = HtmlGenerator
import raw_display
raw_display.Screen = HtmlGenerator
HtmlGenerator.sizes = sizes
HtmlGenerator.keys = keys
def screenshot_collect():
"""Return screenshots as a list of HTML fragments."""
l = HtmlGenerator.fragments
HtmlGenerator.fragments = []
return l

View File

@@ -1,485 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid LCD display module
# Copyright (C) 2010 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from display_common import BaseScreen
import time
class LCDScreen(BaseScreen):
def set_terminal_properties(self, colors=None, bright_is_bold=None,
has_underline=None):
pass
def set_mouse_tracking(self, enable=True):
pass
def set_input_timeouts(self, *args):
pass
def reset_default_terminal_palette(self, *args):
pass
def draw_screen(self, (cols, rows), r ):
pass
def clear(self):
pass
def get_cols_rows(self):
return self.DISPLAY_SIZE
class CFLCDScreen(LCDScreen):
"""
Common methods for Crystal Fontz LCD displays
"""
KEYS = [None, # no key with code 0
'up_press', 'down_press', 'left_press',
'right_press', 'enter_press', 'exit_press',
'up_release', 'down_release', 'left_release',
'right_release', 'enter_release', 'exit_release',
'ul_press', 'ur_press', 'll_press', 'lr_press',
'ul_release', 'ur_release', 'll_release', 'lr_release']
CMD_PING = 0
CMD_VERSION = 1
CMD_CLEAR = 6
CMD_CGRAM = 9
CMD_CURSOR_POSITION = 11 # data = [col, row]
CMD_CURSOR_STYLE = 12 # data = [style (0-4)]
CMD_LCD_CONTRAST = 13 # data = [contrast (0-255)]
CMD_BACKLIGHT = 14 # data = [power (0-100)]
CMD_LCD_DATA = 31 # data = [col, row] + text
CMD_GPO = 34 # data = [pin(0-12), value(0-100)]
# sent from device
CMD_KEY_ACTIVITY = 0x80
CMD_ACK = 0x40 # in high two bits ie. & 0xc0
CURSOR_NONE = 0
CURSOR_BLINKING_BLOCK = 1
CURSOR_UNDERSCORE = 2
CURSOR_BLINKING_BLOCK_UNDERSCORE = 3
CURSOR_INVERTING_BLINKING_BLOCK = 4
MAX_PACKET_DATA_LENGTH = 22
colors = 1
has_underline = False
def __init__(self, device_path, baud):
"""
device_path -- eg. '/dev/ttyUSB0'
baud -- baud rate
"""
super(CFLCDScreen, self).__init__()
self.device_path = device_path
from serial import Serial
self._device = Serial(device_path, baud, timeout=0)
self._unprocessed = ""
@classmethod
def get_crc(cls, buf):
# This seed makes the output of this shift based algorithm match
# the table based algorithm. The center 16 bits of the 32-bit
# "newCRC" are used for the CRC. The MSB of the lower byte is used
# to see what bit was shifted out of the center 16 bit CRC
# accumulator ("carry flag analog");
newCRC = 0x00F32100
for byte in buf:
# Push this bytes bits through a software
# implementation of a hardware shift & xor.
for bit_count in range(8):
# Shift the CRC accumulator
newCRC >>= 1
# The new MSB of the CRC accumulator comes
# from the LSB of the current data byte.
if ord(byte) & (0x01 << bit_count):
newCRC |= 0x00800000
# If the low bit of the current CRC accumulator was set
# before the shift, then we need to XOR the accumulator
# with the polynomial (center 16 bits of 0x00840800)
if newCRC & 0x00000080:
newCRC ^= 0x00840800
# All the data has been done. Do 16 more bits of 0 data.
for bit_count in range(16):
# Shift the CRC accumulator
newCRC >>= 1
# If the low bit of the current CRC accumulator was set
# before the shift we need to XOR the accumulator with
# 0x00840800.
if newCRC & 0x00000080:
newCRC ^= 0x00840800
# Return the center 16 bits, making this CRC match the ones
# complement that is sent in the packet.
return ((~newCRC)>>8) & 0xffff
def _send_packet(self, command, data):
"""
low-level packet sending.
Following the protocol requires waiting for ack packet between
sending each packet to the device.
"""
buf = chr(command) + chr(len(data)) + data
crc = self.get_crc(buf)
buf = buf + chr(crc & 0xff) + chr(crc >> 8)
self._device.write(buf)
def _read_packet(self):
"""
low-level packet reading.
returns (command/report code, data) or None
This method stored data read and tries to resync when bad data
is received.
"""
# pull in any new data available
self._unprocessed = self._unprocessed + self._device.read()
while True:
try:
command, data, unprocessed = self._parse_data(self._unprocessed)
self._unprocessed = unprocessed
return command, data
except self.MoreDataRequired:
return
except self.InvalidPacket:
# throw out a byte and try to parse again
self._unprocessed = self._unprocessed[1:]
class InvalidPacket(Exception):
pass
class MoreDataRequired(Exception):
pass
@classmethod
def _parse_data(cls, data):
"""
Try to read a packet from the start of data, returning
(command/report code, packet_data, remaining_data)
or raising InvalidPacket or MoreDataRequired
"""
if len(data) < 2:
raise cls.MoreDataRequired
command = ord(data[0])
plen = ord(data[1])
if plen > cls.MAX_PACKET_DATA_LENGTH:
raise cls.InvalidPacket("length value too large")
if len(data) < plen + 4:
raise cls.MoreDataRequired
crc = cls.get_crc(data[:2 + plen])
pcrc = ord(data[2 + plen]) + (ord(data[3 + plen]) << 8 )
if crc != pcrc:
raise cls.InvalidPacket("CRC doesn't match")
return (command, data[2:2 + plen], data[4 + plen:])
class KeyRepeatSimulator(object):
"""
Provide simulated repeat key events when given press and
release events.
If two or more keys are pressed disable repeating until all
keys are released.
"""
def __init__(self, repeat_delay, repeat_next):
"""
repeat_delay -- seconds to wait before starting to repeat keys
repeat_next -- time between each repeated key
"""
self.repeat_delay = repeat_delay
self.repeat_next = repeat_next
self.pressed = {}
self.multiple_pressed = False
def press(self, key):
if self.pressed:
self.multiple_pressed = True
self.pressed[key] = time.time()
def release(self, key):
if key not in self.pressed:
return # ignore extra release events
del self.pressed[key]
if not self.pressed:
self.multiple_pressed = False
def next_event(self):
"""
Return (remaining, key) where remaining is the number of seconds
(float) until the key repeat event should be sent, or None if no
events are pending.
"""
if len(self.pressed) != 1 or self.multiple_pressed:
return
for key in self.pressed:
return max(0, self.pressed[key] + self.repeat_delay
- time.time()), key
def sent_event(self):
"""
Cakk this method when you have sent a key repeat event so the
timer will be reset for the next event
"""
if len(self.pressed) != 1:
return # ignore event that shouldn't have been sent
for key in self.pressed:
self.pressed[key] = (
time.time() - self.repeat_delay + self.repeat_next)
return
class CF635Screen(CFLCDScreen):
u"""
Crystal Fontz 635 display
20x4 character display + cursor
no foreground/background colors or settings supported
see CGROM for list of close unicode matches to characters available
6 button input
up, down, left, right, enter (check mark), exit (cross)
"""
DISPLAY_SIZE = (20, 4)
# ① through ⑧ are programmable CGRAM (chars 0-7, repeated at 8-15)
# double arrows (⇑⇓) appear as double arrowheads (chars 18, 19)
# ⑴ resembles a bell
# ⑵ resembles a filled-in "Y"
# ⑶ is the letters "Pt" together
# partial blocks (▇▆▄▃▁) are actually shorter versions of (▉▋▌▍▏)
# both groups are intended to draw horizontal bars with pixel
# precision, use ▇*[▆▄▃▁]? for a thin bar or ▉*[▋▌▍▏]? for a thick bar
CGROM = (
u"①②③④⑤⑥⑦⑧①②③④⑤⑥⑦⑧"
u"►◄⇑⇓«»↖↗↙↘▲▼↲^ˇ█"
u" !\"%&'()*+,-./"
u"0123456789:;<=>?"
u"¡ABCDEFGHIJKLMNO"
u"PQRSTUVWXYZÄÖÑܧ"
u"¿abcdefghijklmno"
u"pqrstuvwxyzäöñüà"
u"⁰¹²³⁴⁵⁶⁷⁸⁹½¼±≥≤μ"
u"♪♫⑴♥♦⑵⌜⌟“”()αɛδ∞"
u"@£$¥èéùìòÇᴾØøʳÅå"
u"⌂¢ΦτλΩπΨΣθΞ♈ÆæßÉ"
u"ΓΛΠϒ_ÈÊêçğŞşİι~◊"
u"▇▆▄▃▁ƒ▉▋▌▍▏⑶◽▪↑→"
u"↓←ÁÍÓÚÝáíóúýÔôŮů"
u"ČĔŘŠŽčĕřšž[\]{|}")
cursor_style = CFLCDScreen.CURSOR_INVERTING_BLINKING_BLOCK
def __init__(self, device_path, baud=115200,
repeat_delay=0.5, repeat_next=0.125,
key_map=['up', 'down', 'left', 'right', 'enter', 'esc']):
"""
device_path -- eg. '/dev/ttyUSB0'
baud -- baud rate
repeat_delay -- seconds to wait before starting to repeat keys
repeat_next -- time between each repeated key
key_map -- the keys to send for this device's buttons
"""
super(CF635Screen, self).__init__(device_path, baud)
self.repeat_delay = repeat_delay
self.repeat_next = repeat_next
self.key_repeat = KeyRepeatSimulator(repeat_delay, repeat_next)
self.key_map = key_map
self._last_command = None
self._last_command_time = 0
self._command_queue = []
self._screen_buf = None
self._previous_canvas = None
self._update_cursor = False
def get_input_descriptors(self):
"""
return the fd from our serial device so we get called
on input and responses
"""
return [self._device.fd]
def get_input_nonblocking(self):
"""
Return a (next_input_timeout, keys_pressed, raw_keycodes)
tuple.
The protocol for our device requires waiting for acks between
each command, so this method responds to those as well as key
press and release events.
Key repeat events are simulated here as the device doesn't send
any for us.
raw_keycodes are the bytes of messages we received, which might
not seem to have any correspondence to keys_pressed.
"""
input = []
raw_input = []
timeout = None
while True:
packet = self._read_packet()
if not packet:
break
command, data = packet
if command == self.CMD_KEY_ACTIVITY and data:
d0 = ord(data[0])
if 1 <= d0 <= 12:
release = d0 > 6
keycode = d0 - (release * 6) - 1
key = self.key_map[keycode]
if release:
self.key_repeat.release(key)
else:
input.append(key)
self.key_repeat.press(key)
raw_input.append(d0)
elif command & 0xc0 == 0x40: # "ACK"
if command & 0x3f == self._last_command:
self._send_next_command()
next_repeat = self.key_repeat.next_event()
if next_repeat:
timeout, key = next_repeat
if not timeout:
input.append(key)
self.key_repeat.sent_event()
timeout = None
return timeout, input, []
def _send_next_command(self):
"""
send out the next command in the queue
"""
if not self._command_queue:
self._last_command = None
return
command, data = self._command_queue.pop(0)
self._send_packet(command, data)
self._last_command = command # record command for ACK
self._last_command_time = time.time()
def queue_command(self, command, data):
self._command_queue.append((command, data))
# not waiting? send away!
if self._last_command is None:
self._send_next_command()
def draw_screen(self, size, canvas):
assert size == self.DISPLAY_SIZE
if self._screen_buf:
osb = self._screen_buf
else:
osb = []
sb = []
y = 0
for row in canvas.content():
text = []
for a, cs, run in row:
text.append(run)
if not osb or osb[y] != text:
self.queue_command(self.CMD_LCD_DATA, chr(0) + chr(y) +
"".join(text))
sb.append(text)
y += 1
if (self._previous_canvas and
self._previous_canvas.cursor == canvas.cursor and
(not self._update_cursor or not canvas.cursor)):
pass
elif canvas.cursor is None:
self.queue_command(self.CMD_CURSOR_STYLE, chr(self.CURSOR_NONE))
else:
x, y = canvas.cursor
self.queue_command(self.CMD_CURSOR_POSITION, chr(x) + chr(y))
self.queue_command(self.CMD_CURSOR_STYLE, chr(self.cursor_style))
self._update_cursor = False
self._screen_buf = sb
self._previous_canvas = canvas
def program_cgram(self, index, data):
"""
Program character data. Characters available as chr(0) through
chr(7), and repeated as chr(8) through chr(15).
index -- 0 to 7 index of character to program
data -- list of 8, 6-bit integer values top to bottom with MSB
on the left side of the character.
"""
assert 0 <= index <= 7
assert len(data) == 8
self.queue_command(self.CMD_CGRAM, chr(index) +
"".join([chr(x) for x in data]))
def set_cursor_style(self, style):
"""
style -- CURSOR_BLINKING_BLOCK, CURSOR_UNDERSCORE,
CURSOR_BLINKING_BLOCK_UNDERSCORE or
CURSOR_INVERTING_BLINKING_BLOCK
"""
assert 1 <= style <= 4
self.cursor_style = style
self._update_cursor = True
def set_backlight(self, value):
"""
Set backlight brightness
value -- 0 to 100
"""
assert 0 <= value <= 100
self.queue_command(self.CMD_BACKLIGHT, chr(value))
def set_lcd_contrast(self, value):
"""
value -- 0 to 255
"""
assert 0 <= value <= 255
self.queue_command(self.CMD_LCD_CONTRAST, chr(value))
def set_led_pin(self, led, rg, value):
"""
led -- 0 to 3
rg -- 0 for red, 1 for green
value -- 0 to 100
"""
assert 0 <= led <= 3
assert rg in (0, 1)
assert 0 <= value <= 100
self.queue_command(self.CMD_GPO, chr(12 - 2 * led - rg) +
chr(value))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,496 +0,0 @@
#!/usr/bin/python
#
# Urwid MonitoredList class
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from urwid.compat import PYTHON3
def _call_modified(fn):
def call_modified_wrapper(self, *args, **kwargs):
rval = fn(self, *args, **kwargs)
self._modified()
return rval
return call_modified_wrapper
class MonitoredList(list):
"""
This class can trigger a callback any time its contents are changed
with the usual list operations append, extend, etc.
"""
def _modified(self):
pass
def set_modified_callback(self, callback):
"""
Assign a callback function with no parameters that is called any
time the list is modified. Callback's return value is ignored.
>>> import sys
>>> ml = MonitoredList([1,2,3])
>>> ml.set_modified_callback(lambda: sys.stdout.write("modified\\n"))
>>> ml
MonitoredList([1, 2, 3])
>>> ml.append(10)
modified
>>> len(ml)
4
>>> ml += [11, 12, 13]
modified
>>> ml[:] = ml[:2] + ml[-2:]
modified
>>> ml
MonitoredList([1, 2, 12, 13])
"""
self._modified = callback
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, list(self))
__add__ = _call_modified(list.__add__)
__delitem__ = _call_modified(list.__delitem__)
if not PYTHON3:
__delslice__ = _call_modified(list.__delslice__)
__iadd__ = _call_modified(list.__iadd__)
__imul__ = _call_modified(list.__imul__)
__rmul__ = _call_modified(list.__rmul__)
__setitem__ = _call_modified(list.__setitem__)
if not PYTHON3:
__setslice__ = _call_modified(list.__setslice__)
append = _call_modified(list.append)
extend = _call_modified(list.extend)
insert = _call_modified(list.insert)
pop = _call_modified(list.pop)
remove = _call_modified(list.remove)
reverse = _call_modified(list.reverse)
sort = _call_modified(list.sort)
if hasattr(list, 'clear'):
clear = _call_modified(list.clear)
class MonitoredFocusList(MonitoredList):
"""
This class can trigger a callback any time its contents are modified,
before and/or after modification, and any time the focus index is changed.
"""
def __init__(self, *argl, **argd):
"""
This is a list that tracks one item as the focus item. If items
are inserted or removed it will update the focus.
>>> ml = MonitoredFocusList([10, 11, 12, 13, 14], focus=3)
>>> ml
MonitoredFocusList([10, 11, 12, 13, 14], focus=3)
>>> del(ml[1])
>>> ml
MonitoredFocusList([10, 12, 13, 14], focus=2)
>>> ml[:2] = [50, 51, 52, 53]
>>> ml
MonitoredFocusList([50, 51, 52, 53, 13, 14], focus=4)
>>> ml[4] = 99
>>> ml
MonitoredFocusList([50, 51, 52, 53, 99, 14], focus=4)
>>> ml[:] = []
>>> ml
MonitoredFocusList([], focus=None)
"""
focus = argd.pop('focus', 0)
super(MonitoredFocusList, self).__init__(*argl, **argd)
self._focus = focus
self._focus_modified = lambda ml, indices, new_items: None
def __repr__(self):
return "%s(%r, focus=%r)" % (
self.__class__.__name__, list(self), self.focus)
def _get_focus(self):
"""
Return the index of the item "in focus" or None if
the list is empty.
>>> MonitoredFocusList([1,2,3], focus=2)._get_focus()
2
>>> MonitoredFocusList()._get_focus()
"""
if not self:
return None
return self._focus
def _set_focus(self, index):
"""
index -- index into this list, any index out of range will
raise an IndexError, except when the list is empty and
the index passed is ignored.
This function may call self._focus_changed when the focus
is modified, passing the new focus position to the
callback just before changing the old focus setting.
That method may be overridden on the
instance with set_focus_changed_callback().
>>> ml = MonitoredFocusList([9, 10, 11])
>>> ml._set_focus(2); ml._get_focus()
2
>>> ml._set_focus(0); ml._get_focus()
0
>>> ml._set_focus(-2)
Traceback (most recent call last):
...
IndexError: focus index is out of range: -2
"""
if not self:
self._focus = 0
return
if index < 0 or index >= len(self):
raise IndexError('focus index is out of range: %s' % (index,))
if index != int(index):
raise IndexError('invalid focus index: %s' % (index,))
index = int(index)
if index != self._focus:
self._focus_changed(index)
self._focus = index
focus = property(_get_focus, _set_focus, doc="""
Get/set the focus index. This value is read as None when the list
is empty, and may only be set to a value between 0 and len(self)-1
or an IndexError will be raised.
""")
def _focus_changed(self, new_focus):
pass
def set_focus_changed_callback(self, callback):
"""
Assign a callback to be called when the focus index changes
for any reason. The callback is in the form:
callback(new_focus)
new_focus -- new focus index
>>> import sys
>>> ml = MonitoredFocusList([1,2,3], focus=1)
>>> ml.set_focus_changed_callback(lambda f: sys.stdout.write("focus: %d\\n" % (f,)))
>>> ml
MonitoredFocusList([1, 2, 3], focus=1)
>>> ml.append(10)
>>> ml.insert(1, 11)
focus: 2
>>> ml
MonitoredFocusList([1, 11, 2, 3, 10], focus=2)
>>> del ml[:2]
focus: 0
>>> ml[:0] = [12, 13, 14]
focus: 3
>>> ml.focus = 5
focus: 5
>>> ml
MonitoredFocusList([12, 13, 14, 2, 3, 10], focus=5)
"""
self._focus_changed = callback
def _validate_contents_modified(self, indices, new_items):
return None
def set_validate_contents_modified(self, callback):
"""
Assign a callback function to handle validating changes to the list.
This may raise an exception if the change should not be performed.
It may also return an integer position to be the new focus after the
list is modified, or None to use the default behaviour.
The callback is in the form:
callback(indices, new_items)
indices -- a (start, stop, step) tuple whose range covers the
items being modified
new_items -- an iterable of items replacing those at range(*indices),
empty if items are being removed, if step==1 this list may
contain any number of items
"""
self._validate_contents_modified = callback
def _adjust_focus_on_contents_modified(self, slc, new_items=()):
"""
Default behaviour is to move the focus to the item following
any removed items, unless that item was simply replaced.
Failing that choose the last item in the list.
returns focus position for after change is applied
"""
num_new_items = len(new_items)
start, stop, step = indices = slc.indices(len(self))
num_removed = len(range(*indices))
focus = self._validate_contents_modified(indices, new_items)
if focus is not None:
return focus
focus = self._focus
if step == 1:
if start + num_new_items <= focus < stop:
focus = stop
# adjust for added/removed items
if stop <= focus:
focus += num_new_items - (stop - start)
else:
if not num_new_items:
# extended slice being removed
if focus in range(start, stop, step):
focus += 1
# adjust for removed items
focus -= len(range(start, min(focus, stop), step))
return min(focus, len(self) + num_new_items - num_removed -1)
# override all the list methods that modify the list
def __delitem__(self, y):
"""
>>> ml = MonitoredFocusList([0,1,2,3,4], focus=2)
>>> del ml[3]; ml
MonitoredFocusList([0, 1, 2, 4], focus=2)
>>> del ml[-1]; ml
MonitoredFocusList([0, 1, 2], focus=2)
>>> del ml[0]; ml
MonitoredFocusList([1, 2], focus=1)
>>> del ml[1]; ml
MonitoredFocusList([1], focus=0)
>>> del ml[0]; ml
MonitoredFocusList([], focus=None)
>>> ml = MonitoredFocusList([5,4,6,4,5,4,6,4,5], focus=4)
>>> del ml[1::2]; ml
MonitoredFocusList([5, 6, 5, 6, 5], focus=2)
>>> del ml[::2]; ml
MonitoredFocusList([6, 6], focus=1)
>>> ml = MonitoredFocusList([0,1,2,3,4,6,7], focus=2)
>>> del ml[-2:]; ml
MonitoredFocusList([0, 1, 2, 3, 4], focus=2)
>>> del ml[-4:-2]; ml
MonitoredFocusList([0, 3, 4], focus=1)
>>> del ml[:]; ml
MonitoredFocusList([], focus=None)
"""
if isinstance(y, slice):
focus = self._adjust_focus_on_contents_modified(y)
else:
focus = self._adjust_focus_on_contents_modified(slice(y,
y+1 or None))
rval = super(MonitoredFocusList, self).__delitem__(y)
self._set_focus(focus)
return rval
def __setitem__(self, i, y):
"""
>>> def modified(indices, new_items):
... print "range%r <- %r" % (indices, new_items)
>>> ml = MonitoredFocusList([0,1,2,3], focus=2)
>>> ml.set_validate_contents_modified(modified)
>>> ml[0] = 9
range(0, 1, 1) <- [9]
>>> ml[2] = 6
range(2, 3, 1) <- [6]
>>> ml.focus
2
>>> ml[-1] = 8
range(3, 4, 1) <- [8]
>>> ml
MonitoredFocusList([9, 1, 6, 8], focus=2)
>>> ml[1::2] = [12, 13]
range(1, 4, 2) <- [12, 13]
>>> ml[::2] = [10, 11]
range(0, 4, 2) <- [10, 11]
>>> ml[-3:-1] = [21, 22, 23]
range(1, 3, 1) <- [21, 22, 23]
>>> ml
MonitoredFocusList([10, 21, 22, 23, 13], focus=2)
>>> ml[:] = []
range(0, 5, 1) <- []
>>> ml
MonitoredFocusList([], focus=None)
"""
if isinstance(i, slice):
focus = self._adjust_focus_on_contents_modified(i, y)
else:
focus = self._adjust_focus_on_contents_modified(slice(i, i+1 or None), [y])
rval = super(MonitoredFocusList, self).__setitem__(i, y)
self._set_focus(focus)
return rval
if not PYTHON3:
def __delslice__(self, i, j):
return self.__delitem__(slice(i,j))
def __setslice__(self, i, j, y):
return self.__setitem__(slice(i, j), y)
def __imul__(self, n):
"""
>>> def modified(indices, new_items):
... print "range%r <- %r" % (indices, list(new_items))
>>> ml = MonitoredFocusList([0,1,2], focus=2)
>>> ml.set_validate_contents_modified(modified)
>>> ml *= 3
range(3, 3, 1) <- [0, 1, 2, 0, 1, 2]
>>> ml
MonitoredFocusList([0, 1, 2, 0, 1, 2, 0, 1, 2], focus=2)
>>> ml *= 0
range(0, 9, 1) <- []
>>> print ml.focus
None
"""
if n > 0:
focus = self._adjust_focus_on_contents_modified(
slice(len(self), len(self)), list(self)*(n-1))
else: # all contents are being removed
focus = self._adjust_focus_on_contents_modified(slice(0, len(self)))
rval = super(MonitoredFocusList, self).__imul__(n)
self._set_focus(focus)
return rval
def append(self, item):
"""
>>> def modified(indices, new_items):
... print "range%r <- %r" % (indices, new_items)
>>> ml = MonitoredFocusList([0,1,2], focus=2)
>>> ml.set_validate_contents_modified(modified)
>>> ml.append(6)
range(3, 3, 1) <- [6]
"""
focus = self._adjust_focus_on_contents_modified(
slice(len(self), len(self)), [item])
rval = super(MonitoredFocusList, self).append(item)
self._set_focus(focus)
return rval
def extend(self, items):
"""
>>> def modified(indices, new_items):
... print "range%r <- %r" % (indices, list(new_items))
>>> ml = MonitoredFocusList([0,1,2], focus=2)
>>> ml.set_validate_contents_modified(modified)
>>> ml.extend((6,7,8))
range(3, 3, 1) <- [6, 7, 8]
"""
focus = self._adjust_focus_on_contents_modified(
slice(len(self), len(self)), items)
rval = super(MonitoredFocusList, self).extend(items)
self._set_focus(focus)
return rval
def insert(self, index, item):
"""
>>> ml = MonitoredFocusList([0,1,2,3], focus=2)
>>> ml.insert(-1, -1); ml
MonitoredFocusList([0, 1, 2, -1, 3], focus=2)
>>> ml.insert(0, -2); ml
MonitoredFocusList([-2, 0, 1, 2, -1, 3], focus=3)
>>> ml.insert(3, -3); ml
MonitoredFocusList([-2, 0, 1, -3, 2, -1, 3], focus=4)
"""
focus = self._adjust_focus_on_contents_modified(slice(index, index),
[item])
rval = super(MonitoredFocusList, self).insert(index, item)
self._set_focus(focus)
return rval
def pop(self, index=-1):
"""
>>> ml = MonitoredFocusList([-2,0,1,-3,2,3], focus=4)
>>> ml.pop(3); ml
-3
MonitoredFocusList([-2, 0, 1, 2, 3], focus=3)
>>> ml.pop(0); ml
-2
MonitoredFocusList([0, 1, 2, 3], focus=2)
>>> ml.pop(-1); ml
3
MonitoredFocusList([0, 1, 2], focus=2)
>>> ml.pop(2); ml
2
MonitoredFocusList([0, 1], focus=1)
"""
focus = self._adjust_focus_on_contents_modified(slice(index,
index+1 or None))
rval = super(MonitoredFocusList, self).pop(index)
self._set_focus(focus)
return rval
def remove(self, value):
"""
>>> ml = MonitoredFocusList([-2,0,1,-3,2,-1,3], focus=4)
>>> ml.remove(-3); ml
MonitoredFocusList([-2, 0, 1, 2, -1, 3], focus=3)
>>> ml.remove(-2); ml
MonitoredFocusList([0, 1, 2, -1, 3], focus=2)
>>> ml.remove(3); ml
MonitoredFocusList([0, 1, 2, -1], focus=2)
"""
index = self.index(value)
focus = self._adjust_focus_on_contents_modified(slice(index,
index+1 or None))
rval = super(MonitoredFocusList, self).remove(value)
self._set_focus(focus)
return rval
def reverse(self):
"""
>>> ml = MonitoredFocusList([0,1,2,3,4], focus=1)
>>> ml.reverse(); ml
MonitoredFocusList([4, 3, 2, 1, 0], focus=3)
"""
rval = super(MonitoredFocusList, self).reverse()
self._set_focus(max(0, len(self) - self._focus - 1))
return rval
def sort(self, **kwargs):
"""
>>> ml = MonitoredFocusList([-2,0,1,-3,2,-1,3], focus=4)
>>> ml.sort(); ml
MonitoredFocusList([-3, -2, -1, 0, 1, 2, 3], focus=5)
"""
if not self:
return
value = self[self._focus]
rval = super(MonitoredFocusList, self).sort(**kwargs)
self._set_focus(self.index(value))
return rval
if hasattr(list, 'clear'):
def clear(self):
focus = self._adjust_focus_on_contents_modified(slice(0, 0))
rval = super(MonitoredFocusList, self).clear()
self._set_focus(focus)
return rval
def _test():
import doctest
doctest.testmod()
if __name__=='__main__':
_test()

View File

@@ -1,368 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid unicode character processing tables
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from __future__ import print_function
import re
from urwid.compat import bytes, B, ord2
SAFE_ASCII_RE = re.compile(u"^[ -~]*$")
SAFE_ASCII_BYTES_RE = re.compile(B("^[ -~]*$"))
_byte_encoding = None
# GENERATED DATA
# generated from
# http://www.unicode.org/Public/4.0-Update/EastAsianWidth-4.0.0.txt
widths = [
(126, 1),
(159, 0),
(687, 1),
(710, 0),
(711, 1),
(727, 0),
(733, 1),
(879, 0),
(1154, 1),
(1161, 0),
(4347, 1),
(4447, 2),
(7467, 1),
(7521, 0),
(8369, 1),
(8426, 0),
(9000, 1),
(9002, 2),
(11021, 1),
(12350, 2),
(12351, 1),
(12438, 2),
(12442, 0),
(19893, 2),
(19967, 1),
(55203, 2),
(63743, 1),
(64106, 2),
(65039, 1),
(65059, 0),
(65131, 2),
(65279, 1),
(65376, 2),
(65500, 1),
(65510, 2),
(120831, 1),
(262141, 2),
(1114109, 1),
]
# ACCESSOR FUNCTIONS
def get_width( o ):
"""Return the screen column width for unicode ordinal o."""
global widths
if o == 0xe or o == 0xf:
return 0
for num, wid in widths:
if o <= num:
return wid
return 1
def decode_one( text, pos ):
"""
Return (ordinal at pos, next position) for UTF-8 encoded text.
"""
assert isinstance(text, bytes), text
b1 = ord2(text[pos])
if not b1 & 0x80:
return b1, pos+1
error = ord("?"), pos+1
lt = len(text)
lt = lt-pos
if lt < 2:
return error
if b1 & 0xe0 == 0xc0:
b2 = ord2(text[pos+1])
if b2 & 0xc0 != 0x80:
return error
o = ((b1&0x1f)<<6)|(b2&0x3f)
if o < 0x80:
return error
return o, pos+2
if lt < 3:
return error
if b1 & 0xf0 == 0xe0:
b2 = ord2(text[pos+1])
if b2 & 0xc0 != 0x80:
return error
b3 = ord2(text[pos+2])
if b3 & 0xc0 != 0x80:
return error
o = ((b1&0x0f)<<12)|((b2&0x3f)<<6)|(b3&0x3f)
if o < 0x800:
return error
return o, pos+3
if lt < 4:
return error
if b1 & 0xf8 == 0xf0:
b2 = ord2(text[pos+1])
if b2 & 0xc0 != 0x80:
return error
b3 = ord2(text[pos+2])
if b3 & 0xc0 != 0x80:
return error
b4 = ord2(text[pos+2])
if b4 & 0xc0 != 0x80:
return error
o = ((b1&0x07)<<18)|((b2&0x3f)<<12)|((b3&0x3f)<<6)|(b4&0x3f)
if o < 0x10000:
return error
return o, pos+4
return error
def decode_one_uni(text, i):
"""
decode_one implementation for unicode strings
"""
return ord(text[i]), i+1
def decode_one_right(text, pos):
"""
Return (ordinal at pos, next position) for UTF-8 encoded text.
pos is assumed to be on the trailing byte of a utf-8 sequence.
"""
assert isinstance(text, bytes), text
error = ord("?"), pos-1
p = pos
while p >= 0:
if ord2(text[p])&0xc0 != 0x80:
o, next = decode_one( text, p )
return o, p-1
p -=1
if p == p-4:
return error
def set_byte_encoding(enc):
assert enc in ('utf8', 'narrow', 'wide')
global _byte_encoding
_byte_encoding = enc
def get_byte_encoding():
return _byte_encoding
def calc_text_pos(text, start_offs, end_offs, pref_col):
"""
Calculate the closest position to the screen column pref_col in text
where start_offs is the offset into text assumed to be screen column 0
and end_offs is the end of the range to search.
text may be unicode or a byte string in the target _byte_encoding
Returns (position, actual_col).
"""
assert start_offs <= end_offs, repr((start_offs, end_offs))
utfs = isinstance(text, bytes) and _byte_encoding == "utf8"
unis = not isinstance(text, bytes)
if unis or utfs:
decode = [decode_one, decode_one_uni][unis]
i = start_offs
sc = 0
n = 1 # number to advance by
while i < end_offs:
o, n = decode(text, i)
w = get_width(o)
if w+sc > pref_col:
return i, sc
i = n
sc += w
return i, sc
assert type(text) == bytes, repr(text)
# "wide" and "narrow"
i = start_offs+pref_col
if i >= end_offs:
return end_offs, end_offs-start_offs
if _byte_encoding == "wide":
if within_double_byte(text, start_offs, i) == 2:
i -= 1
return i, i-start_offs
def calc_width(text, start_offs, end_offs):
"""
Return the screen column width of text between start_offs and end_offs.
text may be unicode or a byte string in the target _byte_encoding
Some characters are wide (take two columns) and others affect the
previous character (take zero columns). Use the widths table above
to calculate the screen column width of text[start_offs:end_offs]
"""
assert start_offs <= end_offs, repr((start_offs, end_offs))
utfs = isinstance(text, bytes) and _byte_encoding == "utf8"
unis = not isinstance(text, bytes)
if (unis and not SAFE_ASCII_RE.match(text)
) or (utfs and not SAFE_ASCII_BYTES_RE.match(text)):
decode = [decode_one, decode_one_uni][unis]
i = start_offs
sc = 0
n = 1 # number to advance by
while i < end_offs:
o, n = decode(text, i)
w = get_width(o)
i = n
sc += w
return sc
# "wide", "narrow" or all printable ASCII, just return the character count
return end_offs - start_offs
def is_wide_char(text, offs):
"""
Test if the character at offs within text is wide.
text may be unicode or a byte string in the target _byte_encoding
"""
if isinstance(text, unicode):
o = ord(text[offs])
return get_width(o) == 2
assert isinstance(text, bytes)
if _byte_encoding == "utf8":
o, n = decode_one(text, offs)
return get_width(o) == 2
if _byte_encoding == "wide":
return within_double_byte(text, offs, offs) == 1
return False
def move_prev_char(text, start_offs, end_offs):
"""
Return the position of the character before end_offs.
"""
assert start_offs < end_offs
if isinstance(text, unicode):
return end_offs-1
assert isinstance(text, bytes)
if _byte_encoding == "utf8":
o = end_offs-1
while ord2(text[o])&0xc0 == 0x80:
o -= 1
return o
if _byte_encoding == "wide" and within_double_byte(text,
start_offs, end_offs-1) == 2:
return end_offs-2
return end_offs-1
def move_next_char(text, start_offs, end_offs):
"""
Return the position of the character after start_offs.
"""
assert start_offs < end_offs
if isinstance(text, unicode):
return start_offs+1
assert isinstance(text, bytes)
if _byte_encoding == "utf8":
o = start_offs+1
while o<end_offs and ord2(text[o])&0xc0 == 0x80:
o += 1
return o
if _byte_encoding == "wide" and within_double_byte(text,
start_offs, start_offs) == 1:
return start_offs +2
return start_offs+1
def within_double_byte(text, line_start, pos):
"""Return whether pos is within a double-byte encoded character.
text -- byte string in question
line_start -- offset of beginning of line (< pos)
pos -- offset in question
Return values:
0 -- not within dbe char, or double_byte_encoding == False
1 -- pos is on the 1st half of a dbe char
2 -- pos is on the 2nd half of a dbe char
"""
assert isinstance(text, bytes)
v = ord2(text[pos])
if v >= 0x40 and v < 0x7f:
# might be second half of big5, uhc or gbk encoding
if pos == line_start: return 0
if ord2(text[pos-1]) >= 0x81:
if within_double_byte(text, line_start, pos-1) == 1:
return 2
return 0
if v < 0x80: return 0
i = pos -1
while i >= line_start:
if ord2(text[i]) < 0x80:
break
i -= 1
if (pos - i) & 1:
return 1
return 2
# TABLE GENERATION CODE
def process_east_asian_width():
import sys
out = []
last = None
for line in sys.stdin.readlines():
if line[:1] == "#": continue
line = line.strip()
hex,rest = line.split(";",1)
wid,rest = rest.split(" # ",1)
word1 = rest.split(" ",1)[0]
if "." in hex:
hex = hex.split("..")[1]
num = int(hex, 16)
if word1 in ("COMBINING","MODIFIER","<control>"):
l = 0
elif wid in ("W", "F"):
l = 2
else:
l = 1
if last is None:
out.append((0, l))
last = l
if last == l:
out[-1] = (num, l)
else:
out.append( (num, l) )
last = l
print("widths = [")
for o in out[1:]: # treat control characters same as ascii
print("\t%r," % (o,))
print("]")
if __name__ == "__main__":
process_east_asian_width()

File diff suppressed because it is too large Load Diff

View File

@@ -1,302 +0,0 @@
#!/usr/bin/python
#
# Urwid signal dispatching
# Copyright (C) 2004-2012 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
import itertools
import weakref
class MetaSignals(type):
"""
register the list of signals in the class varable signals,
including signals in superclasses.
"""
def __init__(cls, name, bases, d):
signals = d.get("signals", [])
for superclass in cls.__bases__:
signals.extend(getattr(superclass, 'signals', []))
signals = dict([(x,None) for x in signals]).keys()
d["signals"] = signals
register_signal(cls, signals)
super(MetaSignals, cls).__init__(name, bases, d)
def setdefaultattr(obj, name, value):
# like dict.setdefault() for object attributes
if hasattr(obj, name):
return getattr(obj, name)
setattr(obj, name, value)
return value
class Key(object):
"""
Minimal class, whose only purpose is to produce objects with a
unique hash
"""
__slots__ = []
class Signals(object):
_signal_attr = '_urwid_signals' # attribute to attach to signal senders
def __init__(self):
self._supported = {}
def register(self, sig_cls, signals):
"""
:param sig_class: the class of an object that will be sending signals
:type sig_class: class
:param signals: a list of signals that may be sent, typically each
signal is represented by a string
:type signals: signal names
This function must be called for a class before connecting any
signal callbacks or emiting any signals from that class' objects
"""
self._supported[sig_cls] = signals
def connect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None):
"""
:param obj: the object sending a signal
:type obj: object
:param name: the signal to listen for, typically a string
:type name: signal name
:param callback: the function to call when that signal is sent
:type callback: function
:param user_arg: deprecated additional argument to callback (appended
after the arguments passed when the signal is
emitted). If None no arguments will be added.
Don't use this argument, use user_args instead.
:param weak_args: additional arguments passed to the callback
(before any arguments passed when the signal
is emitted and before any user_args).
These arguments are stored as weak references
(but converted back into their original value
before passing them to callback) to prevent
any objects referenced (indirectly) from
weak_args from being kept alive just because
they are referenced by this signal handler.
Use this argument only as a keyword argument,
since user_arg might be removed in the future.
:type weak_args: iterable
:param user_args: additional arguments to pass to the callback,
(before any arguments passed when the signal
is emitted but after any weak_args).
Use this argument only as a keyword argument,
since user_arg might be removed in the future.
:type user_args: iterable
When a matching signal is sent, callback will be called. The
arguments it receives will be the user_args passed at connect
time (as individual arguments) followed by all the positional
parameters sent with the signal.
As an example of using weak_args, consider the following snippet:
>>> import urwid
>>> debug = urwid.Text('')
>>> def handler(widget, newtext):
... debug.set_text("Edit widget changed to %s" % newtext)
>>> edit = urwid.Edit('')
>>> key = urwid.connect_signal(edit, 'change', handler)
If you now build some interface using "edit" and "debug", the
"debug" widget will show whatever you type in the "edit" widget.
However, if you remove all references to the "debug" widget, it
will still be kept alive by the signal handler. This because the
signal handler is a closure that (implicitly) references the
"edit" widget. If you want to allow the "debug" widget to be
garbage collected, you can create a "fake" or "weak" closure
(it's not really a closure, since it doesn't reference any
outside variables, so it's just a dynamic function):
>>> debug = urwid.Text('')
>>> def handler(weak_debug, widget, newtext):
... weak_debug.set_text("Edit widget changed to %s" % newtext)
>>> edit = urwid.Edit('')
>>> key = urwid.connect_signal(edit, 'change', handler, weak_args=[debug])
Here the weak_debug parameter in print_debug is the value passed
in the weak_args list to connect_signal. Note that the
weak_debug value passed is not a weak reference anymore, the
signals code transparently dereferences the weakref parameter
before passing it to print_debug.
Returns a key associated by this signal handler, which can be
used to disconnect the signal later on using
urwid.disconnect_signal_by_key. Alternatively, the signal
handler can also be disconnected by calling
urwid.disconnect_signal, which doesn't need this key.
"""
sig_cls = obj.__class__
if not name in self._supported.get(sig_cls, []):
raise NameError("No such signal %r for object %r" %
(name, obj))
# Just generate an arbitrary (but unique) key
key = Key()
signals = setdefaultattr(obj, self._signal_attr, {})
handlers = signals.setdefault(name, [])
# Remove the signal handler when any of the weakref'd arguments
# are garbage collected. Note that this means that the handlers
# dictionary can be modified _at any time_, so it should never
# be iterated directly (e.g. iterate only over .keys() and
# .items(), never over .iterkeys(), .iteritems() or the object
# itself).
# We let the callback keep a weakref to the object as well, to
# prevent a circular reference between the handler and the
# object (via the weakrefs, which keep strong references to
# their callbacks) from existing.
obj_weak = weakref.ref(obj)
def weakref_callback(weakref):
o = obj_weak()
if o:
try:
del getattr(o, self._signal_attr, {})[name][key]
except KeyError:
pass
user_args = self._prepare_user_args(weak_args, user_args, weakref_callback)
handlers.append((key, callback, user_arg, user_args))
return key
def _prepare_user_args(self, weak_args, user_args, callback = None):
# Turn weak_args into weakrefs and prepend them to user_args
return [weakref.ref(a, callback) for a in (weak_args or [])] + (user_args or [])
def disconnect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None):
"""
:param obj: the object to disconnect the signal from
:type obj: object
:param name: the signal to disconnect, typically a string
:type name: signal name
:param callback: the callback function passed to connect_signal
:type callback: function
:param user_arg: the user_arg parameter passed to connect_signal
:param weak_args: the weak_args parameter passed to connect_signal
:param user_args: the weak_args parameter passed to connect_signal
This function will remove a callback from the list connected
to a signal with connect_signal(). The arguments passed should
be exactly the same as those passed to connect_signal().
If the callback is not connected or already disconnected, this
function will simply do nothing.
"""
signals = setdefaultattr(obj, self._signal_attr, {})
if name not in signals:
return
handlers = signals[name]
# Do the same processing as in connect, so we can compare the
# resulting tuple.
user_args = self._prepare_user_args(weak_args, user_args)
# Remove the given handler
for h in handlers:
if h[1:] == (callback, user_arg, user_args):
return self.disconnect_by_key(obj, name, h[0])
def disconnect_by_key(self, obj, name, key):
"""
:param obj: the object to disconnect the signal from
:type obj: object
:param name: the signal to disconnect, typically a string
:type name: signal name
:param key: the key for this signal handler, as returned by
connect_signal().
:type key: Key
This function will remove a callback from the list connected
to a signal with connect_signal(). The key passed should be the
value returned by connect_signal().
If the callback is not connected or already disconnected, this
function will simply do nothing.
"""
signals = setdefaultattr(obj, self._signal_attr, {})
handlers = signals.get(name, [])
handlers[:] = [h for h in handlers if h[0] is not key]
def emit(self, obj, name, *args):
"""
:param obj: the object sending a signal
:type obj: object
:param name: the signal to send, typically a string
:type name: signal name
:param \*args: zero or more positional arguments to pass to the signal
callback functions
This function calls each of the callbacks connected to this signal
with the args arguments as positional parameters.
This function returns True if any of the callbacks returned True.
"""
result = False
signals = getattr(obj, self._signal_attr, {})
handlers = signals.get(name, [])
for key, callback, user_arg, user_args in handlers:
result |= self._call_callback(callback, user_arg, user_args, args)
return result
def _call_callback(self, callback, user_arg, user_args, emit_args):
if user_args:
args_to_pass = []
for arg in user_args:
if isinstance(arg, weakref.ReferenceType):
arg = arg()
if arg is None:
# If the weakref is None, the referenced object
# was cleaned up. We just skip the entire
# callback in this case. The weakref cleanup
# handler will have removed the callback when
# this happens, so no need to actually remove
# the callback here.
return False
args_to_pass.append(arg)
args_to_pass.extend(emit_args)
else:
# Optimization: Don't create a new list when there are
# no user_args
args_to_pass = emit_args
# The deprecated user_arg argument was added to the end
# instead of the beginning.
if user_arg is not None:
args_to_pass = itertools.chain(args_to_pass, (user_arg,))
return bool(callback(*args_to_pass))
_signals = Signals()
emit_signal = _signals.emit
register_signal = _signals.register
connect_signal = _signals.connect
disconnect_signal = _signals.disconnect
disconnect_signal_by_key = _signals.disconnect_by_key

View File

@@ -1,149 +0,0 @@
#!/usr/bin/python
#
# Urwid split_repr helper functions
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from inspect import getargspec
from urwid.compat import PYTHON3, bytes
def split_repr(self):
"""
Return a helpful description of the object using
self._repr_words() and self._repr_attrs() to add
to the description. This function may be used by
adding code to your class like this:
>>> class Foo(object):
... __repr__ = split_repr
... def _repr_words(self):
... return ["words", "here"]
... def _repr_attrs(self):
... return {'attrs': "appear too"}
>>> Foo()
<Foo words here attrs='appear too'>
>>> class Bar(Foo):
... def _repr_words(self):
... return Foo._repr_words(self) + ["too"]
... def _repr_attrs(self):
... return dict(Foo._repr_attrs(self), barttr=42)
>>> Bar()
<Bar words here too attrs='appear too' barttr=42>
"""
alist = [(str(k), normalize_repr(v))
for k, v in self._repr_attrs().items()]
alist.sort()
words = self._repr_words()
if not words and not alist:
# if we're just going to print the classname fall back
# to the previous __repr__ implementation instead
return super(self.__class__, self).__repr__()
if words and alist: words.append("")
return "<%s %s>" % (self.__class__.__name__,
" ".join(words) +
" ".join(["%s=%s" % itm for itm in alist]))
def normalize_repr(v):
"""
Return dictionary repr sorted by keys, leave others unchanged
>>> normalize_repr({1:2,3:4,5:6,7:8})
'{1: 2, 3: 4, 5: 6, 7: 8}'
>>> normalize_repr('foo')
"'foo'"
"""
if isinstance(v, dict):
items = [(repr(k), repr(v)) for k, v in v.items()]
items.sort()
return "{" + ", ".join([
"%s: %s" % itm for itm in items]) + "}"
return repr(v)
def python3_repr(v):
"""
Return strings and byte strings as they appear in Python 3
>>> python3_repr(u"text")
"'text'"
>>> python3_repr(bytes())
"b''"
"""
r = repr(v)
if not PYTHON3:
if isinstance(v, bytes):
return 'b' + r
if r.startswith('u'):
return r[1:]
return r
def remove_defaults(d, fn):
"""
Remove keys in d that are set to the default values from
fn. This method is used to unclutter the _repr_attrs()
return value.
d will be modified by this function.
Returns d.
>>> class Foo(object):
... def __init__(self, a=1, b=2):
... self.values = a, b
... __repr__ = split_repr
... def _repr_words(self):
... return ["object"]
... def _repr_attrs(self):
... d = dict(a=self.values[0], b=self.values[1])
... return remove_defaults(d, Foo.__init__)
>>> Foo(42, 100)
<Foo object a=42 b=100>
>>> Foo(10, 2)
<Foo object a=10>
>>> Foo()
<Foo object>
"""
args, varargs, varkw, defaults = getargspec(fn)
# ignore *varargs and **kwargs
if varkw:
del args[-1]
if varargs:
del args[-1]
# create a dictionary of args with default values
ddict = dict(zip(args[len(args) - len(defaults):], defaults))
for k, v in d.items():
if k in ddict:
# remove values that match their defaults
if ddict[k] == v:
del d[k]
return d
def _test():
import doctest
doctest.testmod()
if __name__=='__main__':
_test()

View File

@@ -1,391 +0,0 @@
import unittest
from urwid import canvas
from urwid.compat import B
import urwid
class CanvasCacheTest(unittest.TestCase):
def setUp(self):
# purge the cache
urwid.CanvasCache._widgets.clear()
def cct(self, widget, size, focus, expected):
got = urwid.CanvasCache.fetch(widget, urwid.Widget, size, focus)
assert expected==got, "got: %s expected: %s"%(got, expected)
def test1(self):
a = urwid.Text("")
b = urwid.Text("")
blah = urwid.TextCanvas()
blah.finalize(a, (10,1), False)
blah2 = urwid.TextCanvas()
blah2.finalize(a, (15,1), False)
bloo = urwid.TextCanvas()
bloo.finalize(b, (20,2), True)
urwid.CanvasCache.store(urwid.Widget, blah)
urwid.CanvasCache.store(urwid.Widget, blah2)
urwid.CanvasCache.store(urwid.Widget, bloo)
self.cct(a, (10,1), False, blah)
self.cct(a, (15,1), False, blah2)
self.cct(a, (15,1), True, None)
self.cct(a, (10,2), False, None)
self.cct(b, (20,2), True, bloo)
self.cct(b, (21,2), True, None)
urwid.CanvasCache.invalidate(a)
self.cct(a, (10,1), False, None)
self.cct(a, (15,1), False, None)
self.cct(b, (20,2), True, bloo)
class CanvasTest(unittest.TestCase):
def ct(self, text, attr, exp_content):
c = urwid.TextCanvas([B(t) for t in text], attr)
content = list(c.content())
assert content == exp_content, "got: %r expected: %r" % (content,
exp_content)
def ct2(self, text, attr, left, top, cols, rows, def_attr, exp_content):
c = urwid.TextCanvas([B(t) for t in text], attr)
content = list(c.content(left, top, cols, rows, def_attr))
assert content == exp_content, "got: %r expected: %r" % (content,
exp_content)
def test1(self):
self.ct(["Hello world"], None, [[(None, None, B("Hello world"))]])
self.ct(["Hello world"], [[("a",5)]],
[[("a", None, B("Hello")), (None, None, B(" world"))]])
self.ct(["Hi","There"], None,
[[(None, None, B("Hi "))], [(None, None, B("There"))]])
def test2(self):
self.ct2(["Hello"], None, 0, 0, 5, 1, None,
[[(None, None, B("Hello"))]])
self.ct2(["Hello"], None, 1, 0, 4, 1, None,
[[(None, None, B("ello"))]])
self.ct2(["Hello"], None, 0, 0, 4, 1, None,
[[(None, None, B("Hell"))]])
self.ct2(["Hi","There"], None, 1, 0, 3, 2, None,
[[(None, None, B("i "))], [(None, None, B("her"))]])
self.ct2(["Hi","There"], None, 0, 0, 5, 1, None,
[[(None, None, B("Hi "))]])
self.ct2(["Hi","There"], None, 0, 1, 5, 1, None,
[[(None, None, B("There"))]])
class ShardBodyTest(unittest.TestCase):
def sbt(self, shards, shard_tail, expected):
result = canvas.shard_body(shards, shard_tail, False)
assert result == expected, "got: %r expected: %r" % (result, expected)
def sbttail(self, num_rows, sbody, expected):
result = canvas.shard_body_tail(num_rows, sbody)
assert result == expected, "got: %r expected: %r" % (result, expected)
def sbtrow(self, sbody, expected):
result = list(canvas.shard_body_row(sbody))
assert result == expected, "got: %r expected: %r" % (result, expected)
def test1(self):
cviews = [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")]
self.sbt(cviews, [],
[(0, None, (0,0,10,5,None,"foo")),
(0, None, (0,0,5,5,None,"bar"))])
self.sbt(cviews, [(0, 3, None, (0,0,5,8,None,"baz"))],
[(3, None, (0,0,5,8,None,"baz")),
(0, None, (0,0,10,5,None,"foo")),
(0, None, (0,0,5,5,None,"bar"))])
self.sbt(cviews, [(10, 3, None, (0,0,5,8,None,"baz"))],
[(0, None, (0,0,10,5,None,"foo")),
(3, None, (0,0,5,8,None,"baz")),
(0, None, (0,0,5,5,None,"bar"))])
self.sbt(cviews, [(15, 3, None, (0,0,5,8,None,"baz"))],
[(0, None, (0,0,10,5,None,"foo")),
(0, None, (0,0,5,5,None,"bar")),
(3, None, (0,0,5,8,None,"baz"))])
def test2(self):
sbody = [(0, None, (0,0,10,5,None,"foo")),
(0, None, (0,0,5,5,None,"bar")),
(3, None, (0,0,5,8,None,"baz"))]
self.sbttail(5, sbody, [])
self.sbttail(3, sbody,
[(0, 3, None, (0,0,10,5,None,"foo")),
(0, 3, None, (0,0,5,5,None,"bar")),
(0, 6, None, (0,0,5,8,None,"baz"))])
sbody = [(0, None, (0,0,10,3,None,"foo")),
(0, None, (0,0,5,5,None,"bar")),
(3, None, (0,0,5,9,None,"baz"))]
self.sbttail(3, sbody,
[(10, 3, None, (0,0,5,5,None,"bar")),
(0, 6, None, (0,0,5,9,None,"baz"))])
def test3(self):
self.sbtrow([(0, None, (0,0,10,5,None,"foo")),
(0, None, (0,0,5,5,None,"bar")),
(3, None, (0,0,5,8,None,"baz"))],
[20])
self.sbtrow([(0, iter("foo"), (0,0,10,5,None,"foo")),
(0, iter("bar"), (0,0,5,5,None,"bar")),
(3, iter("zzz"), (0,0,5,8,None,"baz"))],
["f","b","z"])
class ShardsTrimTest(unittest.TestCase):
def sttop(self, shards, top, expected):
result = canvas.shards_trim_top(shards, top)
assert result == expected, "got: %r expected: %r" (result, expected)
def strows(self, shards, rows, expected):
result = canvas.shards_trim_rows(shards, rows)
assert result == expected, "got: %r expected: %r" (result, expected)
def stsides(self, shards, left, cols, expected):
result = canvas.shards_trim_sides(shards, left, cols)
assert result == expected, "got: %r expected: %r" (result, expected)
def test1(self):
shards = [(5, [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")])]
self.sttop(shards, 2,
[(3, [(0,2,10,3,None,"foo"),(0,2,5,3,None,"bar")])])
self.strows(shards, 2,
[(2, [(0,0,10,2,None,"foo"),(0,0,5,2,None,"bar")])])
shards = [(5, [(0,0,10,5,None,"foo")]),(3,[(0,0,10,3,None,"bar")])]
self.sttop(shards, 2,
[(3, [(0,2,10,3,None,"foo")]),(3,[(0,0,10,3,None,"bar")])])
self.sttop(shards, 5,
[(3, [(0,0,10,3,None,"bar")])])
self.sttop(shards, 7,
[(1, [(0,2,10,1,None,"bar")])])
self.strows(shards, 7,
[(5, [(0,0,10,5,None,"foo")]),(2, [(0,0,10,2,None,"bar")])])
self.strows(shards, 5,
[(5, [(0,0,10,5,None,"foo")])])
self.strows(shards, 4,
[(4, [(0,0,10,4,None,"foo")])])
shards = [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]),
(3,[(0,0,10,3,None,"bar")])]
self.sttop(shards, 2,
[(3, [(0,2,10,3,None,"foo"), (0,2,5,6,None,"baz")]),
(3,[(0,0,10,3,None,"bar")])])
self.sttop(shards, 5,
[(3, [(0,0,10,3,None,"bar"), (0,5,5,3,None,"baz")])])
self.sttop(shards, 7,
[(1, [(0,2,10,1,None,"bar"), (0,7,5,1,None,"baz")])])
self.strows(shards, 7,
[(5, [(0,0,10,5,None,"foo"), (0,0,5,7,None,"baz")]),
(2, [(0,0,10,2,None,"bar")])])
self.strows(shards, 5,
[(5, [(0,0,10,5,None,"foo"), (0,0,5,5,None,"baz")])])
self.strows(shards, 4,
[(4, [(0,0,10,4,None,"foo"), (0,0,5,4,None,"baz")])])
def test2(self):
shards = [(5, [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")])]
self.stsides(shards, 0, 15,
[(5, [(0,0,10,5,None,"foo"),(0,0,5,5,None,"bar")])])
self.stsides(shards, 6, 9,
[(5, [(6,0,4,5,None,"foo"),(0,0,5,5,None,"bar")])])
self.stsides(shards, 6, 6,
[(5, [(6,0,4,5,None,"foo"),(0,0,2,5,None,"bar")])])
self.stsides(shards, 0, 10,
[(5, [(0,0,10,5,None,"foo")])])
self.stsides(shards, 10, 5,
[(5, [(0,0,5,5,None,"bar")])])
self.stsides(shards, 1, 7,
[(5, [(1,0,7,5,None,"foo")])])
shards = [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]),
(3,[(0,0,10,3,None,"bar")])]
self.stsides(shards, 0, 15,
[(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]),
(3,[(0,0,10,3,None,"bar")])])
self.stsides(shards, 2, 13,
[(5, [(2,0,8,5,None,"foo"), (0,0,5,8,None,"baz")]),
(3,[(2,0,8,3,None,"bar")])])
self.stsides(shards, 2, 10,
[(5, [(2,0,8,5,None,"foo"), (0,0,2,8,None,"baz")]),
(3,[(2,0,8,3,None,"bar")])])
self.stsides(shards, 2, 8,
[(5, [(2,0,8,5,None,"foo")]),
(3,[(2,0,8,3,None,"bar")])])
self.stsides(shards, 2, 6,
[(5, [(2,0,6,5,None,"foo")]),
(3,[(2,0,6,3,None,"bar")])])
self.stsides(shards, 10, 5,
[(8, [(0,0,5,8,None,"baz")])])
self.stsides(shards, 11, 3,
[(8, [(1,0,3,8,None,"baz")])])
class ShardsJoinTest(unittest.TestCase):
def sjt(self, shard_lists, expected):
result = canvas.shards_join(shard_lists)
assert result == expected, "got: %r expected: %r" (result, expected)
def test(self):
shards1 = [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]),
(3,[(0,0,10,3,None,"bar")])]
shards2 = [(3, [(0,0,10,3,None,"aaa")]),
(5,[(0,0,10,5,None,"bbb")])]
shards3 = [(3, [(0,0,10,3,None,"111")]),
(2,[(0,0,10,3,None,"222")]),
(3,[(0,0,10,3,None,"333")])]
self.sjt([shards1], shards1)
self.sjt([shards1, shards2],
[(3, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz"),
(0,0,10,3,None,"aaa")]),
(2, [(0,0,10,5,None,"bbb")]),
(3, [(0,0,10,3,None,"bar")])])
self.sjt([shards1, shards3],
[(3, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz"),
(0,0,10,3,None,"111")]),
(2, [(0,0,10,3,None,"222")]),
(3, [(0,0,10,3,None,"bar"), (0,0,10,3,None,"333")])])
self.sjt([shards1, shards2, shards3],
[(3, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz"),
(0,0,10,3,None,"aaa"), (0,0,10,3,None,"111")]),
(2, [(0,0,10,5,None,"bbb"), (0,0,10,3,None,"222")]),
(3, [(0,0,10,3,None,"bar"), (0,0,10,3,None,"333")])])
class CanvasJoinTest(unittest.TestCase):
def cjtest(self, desc, l, expected):
l = [(c, None, False, n) for c, n in l]
result = list(urwid.CanvasJoin(l).content())
assert result == expected, "%s expected %r, got %r"%(
desc, expected, result)
def test(self):
C = urwid.TextCanvas
hello = C([B("hello")])
there = C([B("there")], [[("a",5)]])
a = C([B("a")])
hi = C([B("hi")])
how = C([B("how")], [[("a",1)]])
dy = C([B("dy")])
how_you = C([B("how"), B("you")])
self.cjtest("one", [(hello, 5)],
[[(None, None, B("hello"))]])
self.cjtest("two", [(hello, 5), (there, 5)],
[[(None, None, B("hello")), ("a", None, B("there"))]])
self.cjtest("two space", [(hello, 7), (there, 5)],
[[(None, None, B("hello")),(None,None,B(" ")),
("a", None, B("there"))]])
self.cjtest("three space", [(hi, 4), (how, 3), (dy, 2)],
[[(None, None, B("hi")),(None,None,B(" ")),("a",None, B("h")),
(None,None,B("ow")),(None,None,B("dy"))]])
self.cjtest("four space", [(a, 2), (hi, 3), (dy, 3), (a, 1)],
[[(None, None, B("a")),(None,None,B(" ")),
(None, None, B("hi")),(None,None,B(" ")),
(None, None, B("dy")),(None,None,B(" ")),
(None, None, B("a"))]])
self.cjtest("pile 2", [(how_you, 4), (hi, 2)],
[[(None, None, B('how')), (None, None, B(' ')),
(None, None, B('hi'))],
[(None, None, B('you')), (None, None, B(' ')),
(None, None, B(' '))]])
self.cjtest("pile 2r", [(hi, 4), (how_you, 3)],
[[(None, None, B('hi')), (None, None, B(' ')),
(None, None, B('how'))],
[(None, None, B(' ')),
(None, None, B('you'))]])
class CanvasOverlayTest(unittest.TestCase):
def cotest(self, desc, bgt, bga, fgt, fga, l, r, et):
bgt = B(bgt)
fgt = B(fgt)
bg = urwid.CompositeCanvas(
urwid.TextCanvas([bgt],[bga]))
fg = urwid.CompositeCanvas(
urwid.TextCanvas([fgt],[fga]))
bg.overlay(fg, l, 0)
result = list(bg.content())
assert result == et, "%s expected %r, got %r"%(
desc, et, result)
def test1(self):
self.cotest("left", "qxqxqxqx", [], "HI", [], 0, 6,
[[(None, None, B("HI")),(None,None,B("qxqxqx"))]])
self.cotest("right", "qxqxqxqx", [], "HI", [], 6, 0,
[[(None, None, B("qxqxqx")),(None,None,B("HI"))]])
self.cotest("center", "qxqxqxqx", [], "HI", [], 3, 3,
[[(None, None, B("qxq")),(None,None,B("HI")),
(None,None,B("xqx"))]])
self.cotest("center2", "qxqxqxqx", [], "HI ", [], 2, 2,
[[(None, None, B("qx")),(None,None,B("HI ")),
(None,None,B("qx"))]])
self.cotest("full", "rz", [], "HI", [], 0, 0,
[[(None, None, B("HI"))]])
def test2(self):
self.cotest("same","asdfghjkl",[('a',9)],"HI",[('a',2)],4,3,
[[('a',None,B("asdf")),('a',None,B("HI")),('a',None,B("jkl"))]])
self.cotest("diff","asdfghjkl",[('a',9)],"HI",[('b',2)],4,3,
[[('a',None,B("asdf")),('b',None,B("HI")),('a',None,B("jkl"))]])
self.cotest("None end","asdfghjkl",[('a',9)],"HI ",[('a',2)],
2,3,
[[('a',None,B("as")),('a',None,B("HI")),
(None,None,B(" ")),('a',None,B("jkl"))]])
self.cotest("float end","asdfghjkl",[('a',3)],"HI",[('a',2)],
4,3,
[[('a',None,B("asd")),(None,None,B("f")),
('a',None,B("HI")),(None,None,B("jkl"))]])
self.cotest("cover 2","asdfghjkl",[('a',5),('c',4)],"HI",
[('b',2)],4,3,
[[('a',None,B("asdf")),('b',None,B("HI")),('c',None,B("jkl"))]])
self.cotest("cover 2-2","asdfghjkl",
[('a',4),('d',1),('e',1),('c',3)],
"HI",[('b',2)], 4, 3,
[[('a',None,B("asdf")),('b',None,B("HI")),('c',None,B("jkl"))]])
def test3(self):
urwid.set_encoding("euc-jp")
self.cotest("db0","\xA1\xA1\xA1\xA1\xA1\xA1",[],"HI",[],2,2,
[[(None,None,B("\xA1\xA1")),(None,None,B("HI")),
(None,None,B("\xA1\xA1"))]])
self.cotest("db1","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHI",[],1,2,
[[(None,None,B(" ")),(None,None,B("OHI")),
(None,None,B("\xA1\xA1"))]])
self.cotest("db2","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHI",[],2,1,
[[(None,None,B("\xA1\xA1")),(None,None,B("OHI")),
(None,None,B(" "))]])
self.cotest("db3","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHIO",[],1,1,
[[(None,None,B(" ")),(None,None,B("OHIO")),(None,None,B(" "))]])
class CanvasPadTrimTest(unittest.TestCase):
def cptest(self, desc, ct, ca, l, r, et):
ct = B(ct)
c = urwid.CompositeCanvas(
urwid.TextCanvas([ct], [ca]))
c.pad_trim_left_right(l, r)
result = list(c.content())
assert result == et, "%s expected %r, got %r"%(
desc, et, result)
def test1(self):
self.cptest("none", "asdf", [], 0, 0,
[[(None,None,B("asdf"))]])
self.cptest("left pad", "asdf", [], 2, 0,
[[(None,None,B(" ")),(None,None,B("asdf"))]])
self.cptest("right pad", "asdf", [], 0, 2,
[[(None,None,B("asdf")),(None,None,B(" "))]])
def test2(self):
self.cptest("left trim", "asdf", [], -2, 0,
[[(None,None,B("df"))]])
self.cptest("right trim", "asdf", [], 0, -2,
[[(None,None,B("as"))]])

View File

@@ -1,638 +0,0 @@
import unittest
from urwid.tests.util import SelectableText
import urwid
class FrameTest(unittest.TestCase):
def ftbtest(self, desc, focus_part, header_rows, footer_rows, size,
focus, top, bottom):
class FakeWidget:
def __init__(self, rows, want_focus):
self.ret_rows = rows
self.want_focus = want_focus
def rows(self, size, focus=False):
assert self.want_focus == focus
return self.ret_rows
header = footer = None
if header_rows:
header = FakeWidget(header_rows,
focus and focus_part == 'header')
if footer_rows:
footer = FakeWidget(footer_rows,
focus and focus_part == 'footer')
f = urwid.Frame(None, header, footer, focus_part)
rval = f.frame_top_bottom(size, focus)
exp = (top, bottom), (header_rows, footer_rows)
assert exp == rval, "%s expected %r but got %r"%(
desc,exp,rval)
def test(self):
self.ftbtest("simple", 'body', 0, 0, (9, 10), True, 0, 0)
self.ftbtest("simple h", 'body', 3, 0, (9, 10), True, 3, 0)
self.ftbtest("simple f", 'body', 0, 3, (9, 10), True, 0, 3)
self.ftbtest("simple hf", 'body', 3, 3, (9, 10), True, 3, 3)
self.ftbtest("almost full hf", 'body', 4, 5, (9, 10),
True, 4, 5)
self.ftbtest("full hf", 'body', 5, 5, (9, 10),
True, 4, 5)
self.ftbtest("x full h+1f", 'body', 6, 5, (9, 10),
False, 4, 5)
self.ftbtest("full h+1f", 'body', 6, 5, (9, 10),
True, 4, 5)
self.ftbtest("full hf+1", 'body', 5, 6, (9, 10),
True, 3, 6)
self.ftbtest("F full h+1f", 'footer', 6, 5, (9, 10),
True, 5, 5)
self.ftbtest("F full hf+1", 'footer', 5, 6, (9, 10),
True, 4, 6)
self.ftbtest("F full hf+5", 'footer', 5, 11, (9, 10),
True, 0, 10)
self.ftbtest("full hf+5", 'body', 5, 11, (9, 10),
True, 0, 9)
self.ftbtest("H full hf+1", 'header', 5, 6, (9, 10),
True, 5, 5)
self.ftbtest("H full h+1f", 'header', 6, 5, (9, 10),
True, 6, 4)
self.ftbtest("H full h+5f", 'header', 11, 5, (9, 10),
True, 10, 0)
class PileTest(unittest.TestCase):
def ktest(self, desc, l, focus_item, key,
rkey, rfocus, rpref_col):
p = urwid.Pile( l, focus_item )
rval = p.keypress( (20,), key )
assert rkey == rval, "%s key expected %r but got %r" %(
desc, rkey, rval)
new_focus = l.index(p.get_focus())
assert new_focus == rfocus, "%s focus expected %r but got %r" %(
desc, rfocus, new_focus)
new_pref = p.get_pref_col((20,))
assert new_pref == rpref_col, (
"%s pref_col expected %r but got %r" % (
desc, rpref_col, new_pref))
def test_select_change(self):
T,S,E = urwid.Text, SelectableText, urwid.Edit
self.ktest("simple up", [S("")], 0, "up", "up", 0, 0)
self.ktest("simple down", [S("")], 0, "down", "down", 0, 0)
self.ktest("ignore up", [T(""),S("")], 1, "up", "up", 1, 0)
self.ktest("ignore down", [S(""),T("")], 0, "down",
"down", 0, 0)
self.ktest("step up", [S(""),S("")], 1, "up", None, 0, 0)
self.ktest("step down", [S(""),S("")], 0, "down",
None, 1, 0)
self.ktest("skip step up", [S(""),T(""),S("")], 2, "up",
None, 0, 0)
self.ktest("skip step down", [S(""),T(""),S("")], 0, "down",
None, 2, 0)
self.ktest("pad skip step up", [T(""),S(""),T(""),S("")], 3,
"up", None, 1, 0)
self.ktest("pad skip step down", [S(""),T(""),S(""),T("")], 0,
"down", None, 2, 0)
self.ktest("padi skip step up", [S(""),T(""),S(""),T(""),S("")],
4, "up", None, 2, 0)
self.ktest("padi skip step down", [S(""),T(""),S(""),T(""),
S("")], 0, "down", None, 2, 0)
e = E("","abcd", edit_pos=1)
e.keypress((20,),"right") # set a pref_col
self.ktest("pref step up", [S(""),T(""),e], 2, "up",
None, 0, 2)
self.ktest("pref step down", [e,T(""),S("")], 0, "down",
None, 2, 2)
z = E("","1234")
self.ktest("prefx step up", [z,T(""),e], 2, "up",
None, 0, 2)
assert z.get_pref_col((20,)) == 2
z = E("","1234")
self.ktest("prefx step down", [e,T(""),z], 0, "down",
None, 2, 2)
assert z.get_pref_col((20,)) == 2
def test_init_with_a_generator(self):
urwid.Pile(urwid.Text(c) for c in "ABC")
def test_change_focus_with_mouse(self):
p = urwid.Pile([urwid.Edit(), urwid.Edit()])
self.assertEqual(p.focus_position, 0)
p.mouse_event((10,), 'button press', 1, 1, 1, True)
self.assertEqual(p.focus_position, 1)
def test_zero_weight(self):
p = urwid.Pile([
urwid.SolidFill('a'),
('weight', 0, urwid.SolidFill('d')),
])
p.render((5, 4))
def test_mouse_event_in_empty_pile(self):
p = urwid.Pile([])
p.mouse_event((5,), 'button press', 1, 1, 1, False)
p.mouse_event((5,), 'button press', 1, 1, 1, True)
class ColumnsTest(unittest.TestCase):
def cwtest(self, desc, l, divide, size, exp, focus_column=0):
c = urwid.Columns(l, divide, focus_column)
rval = c.column_widths( size )
assert rval == exp, "%s expected %s, got %s"%(desc,exp,rval)
def test_widths(self):
x = urwid.Text("") # sample "column"
self.cwtest( "simple 1", [x], 0, (20,), [20] )
self.cwtest( "simple 2", [x,x], 0, (20,), [10,10] )
self.cwtest( "simple 2+1", [x,x], 1, (20,), [10,9] )
self.cwtest( "simple 3+1", [x,x,x], 1, (20,), [6,6,6] )
self.cwtest( "simple 3+2", [x,x,x], 2, (20,), [5,6,5] )
self.cwtest( "simple 3+2", [x,x,x], 2, (21,), [6,6,5] )
self.cwtest( "simple 4+1", [x,x,x,x], 1, (25,), [6,5,6,5] )
self.cwtest( "squish 4+1", [x,x,x,x], 1, (7,), [1,1,1,1] )
self.cwtest( "squish 4+1", [x,x,x,x], 1, (6,), [1,2,1] )
self.cwtest( "squish 4+1", [x,x,x,x], 1, (4,), [2,1] )
self.cwtest( "fixed 3", [('fixed',4,x),('fixed',6,x),
('fixed',2,x)], 1, (25,), [4,6,2] )
self.cwtest( "fixed 3 cut", [('fixed',4,x),('fixed',6,x),
('fixed',2,x)], 1, (13,), [4,6] )
self.cwtest( "fixed 3 cut2", [('fixed',4,x),('fixed',6,x),
('fixed',2,x)], 1, (10,), [4] )
self.cwtest( "mixed 4", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (14,), [2,5,1,3] )
self.cwtest( "mixed 4 a", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (12,), [1,5,1,2] )
self.cwtest( "mixed 4 b", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (10,), [2,5,1] )
self.cwtest( "mixed 4 c", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (20,), [4,5,2,6] )
def test_widths_focus_end(self):
x = urwid.Text("") # sample "column"
self.cwtest("end simple 2", [x,x], 0, (20,), [10,10], 1)
self.cwtest("end simple 2+1", [x,x], 1, (20,), [10,9], 1)
self.cwtest("end simple 3+1", [x,x,x], 1, (20,), [6,6,6], 2)
self.cwtest("end simple 3+2", [x,x,x], 2, (20,), [5,6,5], 2)
self.cwtest("end simple 3+2", [x,x,x], 2, (21,), [6,6,5], 2)
self.cwtest("end simple 4+1", [x,x,x,x], 1, (25,), [6,5,6,5], 3)
self.cwtest("end squish 4+1", [x,x,x,x], 1, (7,), [1,1,1,1], 3)
self.cwtest("end squish 4+1", [x,x,x,x], 1, (6,), [0,1,2,1], 3)
self.cwtest("end squish 4+1", [x,x,x,x], 1, (4,), [0,0,2,1], 3)
self.cwtest("end fixed 3", [('fixed',4,x),('fixed',6,x),
('fixed',2,x)], 1, (25,), [4,6,2], 2)
self.cwtest("end fixed 3 cut", [('fixed',4,x),('fixed',6,x),
('fixed',2,x)], 1, (13,), [0,6,2], 2)
self.cwtest("end fixed 3 cut2", [('fixed',4,x),('fixed',6,x),
('fixed',2,x)], 1, (8,), [0,0,2], 2)
self.cwtest("end mixed 4", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (14,), [2,5,1,3], 3)
self.cwtest("end mixed 4 a", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (12,), [1,5,1,2], 3)
self.cwtest("end mixed 4 b", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (10,), [0,5,1,2], 3)
self.cwtest("end mixed 4 c", [('weight',2,x),('fixed',5,x),
x, ('weight',3,x)], 1, (20,), [4,5,2,6], 3)
def mctest(self, desc, l, divide, size, col, row, exp, f_col, pref_col):
c = urwid.Columns( l, divide )
rval = c.move_cursor_to_coords( size, col, row )
assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval)
assert c.focus_col == f_col, "%s expected focus_col %s got %s"%(
desc, f_col, c.focus_col)
pc = c.get_pref_col( size )
assert pc == pref_col, "%s expected pref_col %s, got %s"%(
desc, pref_col, pc)
def test_move_cursor(self):
e, s, x = urwid.Edit("",""),SelectableText(""), urwid.Text("")
self.mctest("nothing selectbl",[x,x,x],1,(20,),9,0,False,0,None)
self.mctest("dead on",[x,s,x],1,(20,),9,0,True,1,9)
self.mctest("l edge",[x,s,x],1,(20,),6,0,True,1,6)
self.mctest("r edge",[x,s,x],1,(20,),13,0,True,1,13)
self.mctest("l off",[x,s,x],1,(20,),2,0,True,1,2)
self.mctest("r off",[x,s,x],1,(20,),17,0,True,1,17)
self.mctest("l off 2",[x,x,s],1,(20,),2,0,True,2,2)
self.mctest("r off 2",[s,x,x],1,(20,),17,0,True,0,17)
self.mctest("l between",[s,s,x],1,(20,),6,0,True,0,6)
self.mctest("r between",[x,s,s],1,(20,),13,0,True,1,13)
self.mctest("l between 2l",[s,s,x],2,(22,),6,0,True,0,6)
self.mctest("r between 2l",[x,s,s],2,(22,),14,0,True,1,14)
self.mctest("l between 2r",[s,s,x],2,(22,),7,0,True,1,7)
self.mctest("r between 2r",[x,s,s],2,(22,),15,0,True,2,15)
# unfortunate pref_col shifting
self.mctest("l e edge",[x,e,x],1,(20,),6,0,True,1,7)
self.mctest("r e edge",[x,e,x],1,(20,),13,0,True,1,12)
# 'left'/'right' special cases
self.mctest("right", [e, e, e], 0, (12,), 'right', 0, True, 2, 'right')
self.mctest("left", [e, e, e], 0, (12,), 'left', 0, True, 0, 'left')
def test_init_with_a_generator(self):
urwid.Columns(urwid.Text(c) for c in "ABC")
def test_old_attributes(self):
c = urwid.Columns([urwid.Text(u'a'), urwid.SolidFill(u'x')],
box_columns=[1])
self.assertEqual(c.box_columns, [1])
c.box_columns=[]
self.assertEqual(c.box_columns, [])
def test_box_column(self):
c = urwid.Columns([urwid.Filler(urwid.Edit()),urwid.Text('')],
box_columns=[0])
c.keypress((10,), 'x')
c.get_cursor_coords((10,))
c.move_cursor_to_coords((10,), 0, 0)
c.mouse_event((10,), 'foo', 1, 0, 0, True)
c.get_pref_col((10,))
class OverlayTest(unittest.TestCase):
def test_old_params(self):
o1 = urwid.Overlay(urwid.SolidFill(u'X'), urwid.SolidFill(u'O'),
('fixed left', 5), ('fixed right', 4),
('fixed top', 3), ('fixed bottom', 2),)
self.assertEqual(o1.contents[1][1], (
'left', None, 'relative', 100, None, 5, 4,
'top', None, 'relative', 100, None, 3, 2))
o2 = urwid.Overlay(urwid.SolidFill(u'X'), urwid.SolidFill(u'O'),
('fixed right', 5), ('fixed left', 4),
('fixed bottom', 3), ('fixed top', 2),)
self.assertEqual(o2.contents[1][1], (
'right', None, 'relative', 100, None, 4, 5,
'bottom', None, 'relative', 100, None, 2, 3))
def test_get_cursor_coords(self):
self.assertEqual(urwid.Overlay(urwid.Filler(urwid.Edit()),
urwid.SolidFill(u'B'),
'right', 1, 'bottom', 1).get_cursor_coords((2,2)), (1,1))
class GridFlowTest(unittest.TestCase):
def test_cell_width(self):
gf = urwid.GridFlow([], 5, 0, 0, 'left')
self.assertEqual(gf.cell_width, 5)
def test_basics(self):
repr(urwid.GridFlow([], 5, 0, 0, 'left')) # should not fail
def test_v_sep(self):
gf = urwid.GridFlow([urwid.Text("test")], 10, 3, 1, "center")
self.assertEqual(gf.rows((40,), False), 1)
class WidgetSquishTest(unittest.TestCase):
def wstest(self, w):
c = w.render((80,0), focus=False)
assert c.rows() == 0
c = w.render((80,0), focus=True)
assert c.rows() == 0
c = w.render((80,1), focus=False)
assert c.rows() == 1
c = w.render((0, 25), focus=False)
c = w.render((1, 25), focus=False)
def fwstest(self, w):
def t(cols, focus):
wrows = w.rows((cols,), focus)
c = w.render((cols,), focus)
assert c.rows() == wrows, (c.rows(), wrows)
if focus and hasattr(w, 'get_cursor_coords'):
gcc = w.get_cursor_coords((cols,))
assert c.cursor == gcc, (c.cursor, gcc)
t(0, False)
t(1, False)
t(0, True)
t(1, True)
def test_listbox(self):
self.wstest(urwid.ListBox([]))
self.wstest(urwid.ListBox([urwid.Text("hello")]))
def test_bargraph(self):
self.wstest(urwid.BarGraph(['foo','bar']))
def test_graphvscale(self):
self.wstest(urwid.GraphVScale([(0,"hello")], 1))
self.wstest(urwid.GraphVScale([(5,"hello")], 1))
def test_solidfill(self):
self.wstest(urwid.SolidFill())
def test_filler(self):
self.wstest(urwid.Filler(urwid.Text("hello")))
def test_overlay(self):
self.wstest(urwid.Overlay(
urwid.BigText("hello",urwid.Thin6x6Font()),
urwid.SolidFill(),
'center', None, 'middle', None))
self.wstest(urwid.Overlay(
urwid.Text("hello"), urwid.SolidFill(),
'center', ('relative', 100), 'middle', None))
def test_frame(self):
self.wstest(urwid.Frame(urwid.SolidFill()))
self.wstest(urwid.Frame(urwid.SolidFill(),
header=urwid.Text("hello")))
self.wstest(urwid.Frame(urwid.SolidFill(),
header=urwid.Text("hello"),
footer=urwid.Text("hello")))
def test_pile(self):
self.wstest(urwid.Pile([urwid.SolidFill()]))
self.wstest(urwid.Pile([('flow', urwid.Text("hello"))]))
self.wstest(urwid.Pile([]))
def test_columns(self):
self.wstest(urwid.Columns([urwid.SolidFill()]))
self.wstest(urwid.Columns([(4, urwid.SolidFill())]))
def test_buttons(self):
self.fwstest(urwid.Button(u"hello"))
self.fwstest(urwid.RadioButton([], u"hello"))
class CommonContainerTest(unittest.TestCase):
def test_pile(self):
t1 = urwid.Text(u'one')
t2 = urwid.Text(u'two')
t3 = urwid.Text(u'three')
sf = urwid.SolidFill('x')
p = urwid.Pile([])
self.assertEqual(p.focus, None)
self.assertRaises(IndexError, lambda: getattr(p, 'focus_position'))
self.assertRaises(IndexError, lambda: setattr(p, 'focus_position',
None))
self.assertRaises(IndexError, lambda: setattr(p, 'focus_position', 0))
p.contents = [(t1, ('pack', None)), (t2, ('pack', None)),
(sf, ('given', 3)), (t3, ('pack', None))]
p.focus_position = 1
del p.contents[0]
self.assertEqual(p.focus_position, 0)
p.contents[0:0] = [(t3, ('pack', None)), (t2, ('pack', None))]
p.contents.insert(3, (t1, ('pack', None)))
self.assertEqual(p.focus_position, 2)
self.assertRaises(urwid.PileError, lambda: p.contents.append(t1))
self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, None)))
self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, 'given')))
p = urwid.Pile([t1, t2])
self.assertEqual(p.focus, t1)
self.assertEqual(p.focus_position, 0)
p.focus_position = 1
self.assertEqual(p.focus, t2)
self.assertEqual(p.focus_position, 1)
p.focus_position = 0
self.assertRaises(IndexError, lambda: setattr(p, 'focus_position', -1))
self.assertRaises(IndexError, lambda: setattr(p, 'focus_position', 2))
# old methods:
p.set_focus(0)
self.assertRaises(IndexError, lambda: p.set_focus(-1))
self.assertRaises(IndexError, lambda: p.set_focus(2))
p.set_focus(t2)
self.assertEqual(p.focus_position, 1)
self.assertRaises(ValueError, lambda: p.set_focus('nonexistant'))
self.assertEqual(p.widget_list, [t1, t2])
self.assertEqual(p.item_types, [('weight', 1), ('weight', 1)])
p.widget_list = [t2, t1]
self.assertEqual(p.widget_list, [t2, t1])
self.assertEqual(p.contents, [(t2, ('weight', 1)), (t1, ('weight', 1))])
self.assertEqual(p.focus_position, 1) # focus unchanged
p.item_types = [('flow', None), ('weight', 2)]
self.assertEqual(p.item_types, [('flow', None), ('weight', 2)])
self.assertEqual(p.contents, [(t2, ('pack', None)), (t1, ('weight', 2))])
self.assertEqual(p.focus_position, 1) # focus unchanged
p.widget_list = [t1]
self.assertEqual(len(p.contents), 1)
self.assertEqual(p.focus_position, 0)
p.widget_list.extend([t2, t1])
self.assertEqual(len(p.contents), 3)
self.assertEqual(p.item_types, [
('flow', None), ('weight', 1), ('weight', 1)])
p.item_types[:] = [('weight', 2)]
self.assertEqual(len(p.contents), 1)
def test_columns(self):
t1 = urwid.Text(u'one')
t2 = urwid.Text(u'two')
t3 = urwid.Text(u'three')
sf = urwid.SolidFill('x')
c = urwid.Columns([])
self.assertEqual(c.focus, None)
self.assertRaises(IndexError, lambda: getattr(c, 'focus_position'))
self.assertRaises(IndexError, lambda: setattr(c, 'focus_position',
None))
self.assertRaises(IndexError, lambda: setattr(c, 'focus_position', 0))
c.contents = [
(t1, ('pack', None, False)),
(t2, ('weight', 1, False)),
(sf, ('weight', 2, True)),
(t3, ('given', 10, False))]
c.focus_position = 1
del c.contents[0]
self.assertEqual(c.focus_position, 0)
c.contents[0:0] = [
(t3, ('given', 10, False)),
(t2, ('weight', 1, False))]
c.contents.insert(3, (t1, ('pack', None, False)))
self.assertEqual(c.focus_position, 2)
self.assertRaises(urwid.ColumnsError, lambda: c.contents.append(t1))
self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, None)))
self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, 'given')))
c = urwid.Columns([t1, t2])
self.assertEqual(c.focus, t1)
self.assertEqual(c.focus_position, 0)
c.focus_position = 1
self.assertEqual(c.focus, t2)
self.assertEqual(c.focus_position, 1)
c.focus_position = 0
self.assertRaises(IndexError, lambda: setattr(c, 'focus_position', -1))
self.assertRaises(IndexError, lambda: setattr(c, 'focus_position', 2))
# old methods:
c = urwid.Columns([t1, ('weight', 3, t2), sf], box_columns=[2])
c.set_focus(0)
self.assertRaises(IndexError, lambda: c.set_focus(-1))
self.assertRaises(IndexError, lambda: c.set_focus(3))
c.set_focus(t2)
self.assertEqual(c.focus_position, 1)
self.assertRaises(ValueError, lambda: c.set_focus('nonexistant'))
self.assertEqual(c.widget_list, [t1, t2, sf])
self.assertEqual(c.column_types, [
('weight', 1), ('weight', 3), ('weight', 1)])
self.assertEqual(c.box_columns, [2])
c.widget_list = [t2, t1, sf]
self.assertEqual(c.widget_list, [t2, t1, sf])
self.assertEqual(c.box_columns, [2])
self.assertEqual(c.contents, [
(t2, ('weight', 1, False)),
(t1, ('weight', 3, False)),
(sf, ('weight', 1, True))])
self.assertEqual(c.focus_position, 1) # focus unchanged
c.column_types = [
('flow', None), # use the old name
('weight', 2),
('fixed', 5)]
self.assertEqual(c.column_types, [
('flow', None),
('weight', 2),
('fixed', 5)])
self.assertEqual(c.contents, [
(t2, ('pack', None, False)),
(t1, ('weight', 2, False)),
(sf, ('given', 5, True))])
self.assertEqual(c.focus_position, 1) # focus unchanged
c.widget_list = [t1]
self.assertEqual(len(c.contents), 1)
self.assertEqual(c.focus_position, 0)
c.widget_list.extend([t2, t1])
self.assertEqual(len(c.contents), 3)
self.assertEqual(c.column_types, [
('flow', None), ('weight', 1), ('weight', 1)])
c.column_types[:] = [('weight', 2)]
self.assertEqual(len(c.contents), 1)
def test_list_box(self):
lb = urwid.ListBox(urwid.SimpleFocusListWalker([]))
self.assertEqual(lb.focus, None)
self.assertRaises(IndexError, lambda: getattr(lb, 'focus_position'))
self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position',
None))
self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', 0))
t1 = urwid.Text(u'one')
t2 = urwid.Text(u'two')
lb = urwid.ListBox(urwid.SimpleListWalker([t1, t2]))
self.assertEqual(lb.focus, t1)
self.assertEqual(lb.focus_position, 0)
lb.focus_position = 1
self.assertEqual(lb.focus, t2)
self.assertEqual(lb.focus_position, 1)
lb.focus_position = 0
self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', -1))
self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', 2))
def test_grid_flow(self):
gf = urwid.GridFlow([], 5, 1, 0, 'left')
self.assertEqual(gf.focus, None)
self.assertEqual(gf.contents, [])
self.assertRaises(IndexError, lambda: getattr(gf, 'focus_position'))
self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position',
None))
self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position', 0))
self.assertEqual(gf.options(), ('given', 5))
self.assertEqual(gf.options(width_amount=9), ('given', 9))
self.assertRaises(urwid.GridFlowError, lambda: gf.options(
'pack', None))
t1 = urwid.Text(u'one')
t2 = urwid.Text(u'two')
gf = urwid.GridFlow([t1, t2], 5, 1, 0, 'left')
self.assertEqual(gf.focus, t1)
self.assertEqual(gf.focus_position, 0)
self.assertEqual(gf.contents, [(t1, ('given', 5)), (t2, ('given', 5))])
gf.focus_position = 1
self.assertEqual(gf.focus, t2)
self.assertEqual(gf.focus_position, 1)
gf.contents.insert(0, (t2, ('given', 5)))
self.assertEqual(gf.focus_position, 2)
self.assertRaises(urwid.GridFlowError, lambda: gf.contents.append(()))
self.assertRaises(urwid.GridFlowError, lambda: gf.contents.insert(1,
(t1, ('pack', None))))
gf.focus_position = 0
self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position', -1))
self.assertRaises(IndexError, lambda: setattr(gf, 'focus_position', 3))
# old methods:
gf.set_focus(0)
self.assertRaises(IndexError, lambda: gf.set_focus(-1))
self.assertRaises(IndexError, lambda: gf.set_focus(3))
gf.set_focus(t1)
self.assertEqual(gf.focus_position, 1)
self.assertRaises(ValueError, lambda: gf.set_focus('nonexistant'))
def test_overlay(self):
s1 = urwid.SolidFill(u'1')
s2 = urwid.SolidFill(u'2')
o = urwid.Overlay(s1, s2,
'center', ('relative', 50), 'middle', ('relative', 50))
self.assertEqual(o.focus, s1)
self.assertEqual(o.focus_position, 1)
self.assertRaises(IndexError, lambda: setattr(o, 'focus_position',
None))
self.assertRaises(IndexError, lambda: setattr(o, 'focus_position', 2))
self.assertEqual(o.contents[0], (s2,
urwid.Overlay._DEFAULT_BOTTOM_OPTIONS))
self.assertEqual(o.contents[1], (s1, (
'center', None, 'relative', 50, None, 0, 0,
'middle', None, 'relative', 50, None, 0, 0)))
def test_frame(self):
s1 = urwid.SolidFill(u'1')
f = urwid.Frame(s1)
self.assertEqual(f.focus, s1)
self.assertEqual(f.focus_position, 'body')
self.assertRaises(IndexError, lambda: setattr(f, 'focus_position',
None))
self.assertRaises(IndexError, lambda: setattr(f, 'focus_position',
'header'))
t1 = urwid.Text(u'one')
t2 = urwid.Text(u'two')
t3 = urwid.Text(u'three')
f = urwid.Frame(s1, t1, t2, 'header')
self.assertEqual(f.focus, t1)
self.assertEqual(f.focus_position, 'header')
f.focus_position = 'footer'
self.assertEqual(f.focus, t2)
self.assertEqual(f.focus_position, 'footer')
self.assertRaises(IndexError, lambda: setattr(f, 'focus_position', -1))
self.assertRaises(IndexError, lambda: setattr(f, 'focus_position', 2))
del f.contents['footer']
self.assertEqual(f.footer, None)
self.assertEqual(f.focus_position, 'body')
f.contents.update(footer=(t3, None), header=(t2, None))
self.assertEqual(f.header, t2)
self.assertEqual(f.footer, t3)
def set1():
f.contents['body'] = t1
self.assertRaises(urwid.FrameError, set1)
def set2():
f.contents['body'] = (t1, 'given')
self.assertRaises(urwid.FrameError, set2)
def test_focus_path(self):
# big tree of containers
t = urwid.Text(u'x')
e = urwid.Edit(u'?')
c = urwid.Columns([t, e, t, t])
p = urwid.Pile([t, t, c, t])
a = urwid.AttrMap(p, 'gets ignored')
s = urwid.SolidFill(u'/')
o = urwid.Overlay(e, s, 'center', 'pack', 'middle', 'pack')
lb = urwid.ListBox(urwid.SimpleFocusListWalker([t, a, o, t]))
lb.focus_position = 1
g = urwid.GridFlow([t, t, t, t, e, t], 10, 0, 0, 'left')
g.focus_position = 4
f = urwid.Frame(lb, header=t, footer=g)
self.assertEqual(f.get_focus_path(), ['body', 1, 2, 1])
f.set_focus_path(['footer']) # same as f.focus_position = 'footer'
self.assertEqual(f.get_focus_path(), ['footer', 4])
f.set_focus_path(['body', 1, 2, 2])
self.assertEqual(f.get_focus_path(), ['body', 1, 2, 2])
self.assertRaises(IndexError, lambda: f.set_focus_path([0, 1, 2]))
self.assertRaises(IndexError, lambda: f.set_focus_path(['body', 2, 2]))
f.set_focus_path(['body', 2]) # focus the overlay
self.assertEqual(f.get_focus_path(), ['body', 2, 1])

View File

@@ -1,149 +0,0 @@
import unittest
import urwid
class PaddingTest(unittest.TestCase):
def ptest(self, desc, align, width, maxcol, left, right,min_width=None):
p = urwid.Padding(None, align, width, min_width)
l, r = p.padding_values((maxcol,),False)
assert (l,r)==(left,right), "%s expected %s but got %s"%(
desc, (left,right), (l,r))
def petest(self, desc, align, width):
self.assertRaises(urwid.PaddingError, lambda:
urwid.Padding(None, align, width))
def test_create(self):
self.petest("invalid pad",6,5)
self.petest("invalid pad type",('bad',2),5)
self.petest("invalid width",'center','42')
self.petest("invalid width type",'center',('gouranga',4))
def test_values(self):
self.ptest("left align 5 7",'left',5,7,0,2)
self.ptest("left align 7 7",'left',7,7,0,0)
self.ptest("left align 9 7",'left',9,7,0,0)
self.ptest("right align 5 7",'right',5,7,2,0)
self.ptest("center align 5 7",'center',5,7,1,1)
self.ptest("fixed left",('fixed left',3),5,10,3,2)
self.ptest("fixed left reduce",('fixed left',3),8,10,2,0)
self.ptest("fixed left shrink",('fixed left',3),18,10,0,0)
self.ptest("fixed left, right",
('fixed left',3),('fixed right',4),17,3,4)
self.ptest("fixed left, right, min_width",
('fixed left',3),('fixed right',4),10,3,2,5)
self.ptest("fixed left, right, min_width 2",
('fixed left',3),('fixed right',4),10,2,0,8)
self.ptest("fixed right",('fixed right',3),5,10,2,3)
self.ptest("fixed right reduce",('fixed right',3),8,10,0,2)
self.ptest("fixed right shrink",('fixed right',3),18,10,0,0)
self.ptest("fixed right, left",
('fixed right',3),('fixed left',4),17,4,3)
self.ptest("fixed right, left, min_width",
('fixed right',3),('fixed left',4),10,2,3,5)
self.ptest("fixed right, left, min_width 2",
('fixed right',3),('fixed left',4),10,0,2,8)
self.ptest("relative 30",('relative',30),5,10,1,4)
self.ptest("relative 50",('relative',50),5,10,2,3)
self.ptest("relative 130 edge",('relative',130),5,10,5,0)
self.ptest("relative -10 edge",('relative',-10),4,10,0,6)
self.ptest("center relative 70",'center',('relative',70),
10,1,2)
self.ptest("center relative 70 grow 8",'center',('relative',70),
10,1,1,8)
def mctest(self, desc, left, right, size, cx, innercx):
class Inner:
def __init__(self, desc, innercx):
self.desc = desc
self.innercx = innercx
def move_cursor_to_coords(self,size,cx,cy):
assert cx==self.innercx, desc
i = Inner(desc,innercx)
p = urwid.Padding(i, ('fixed left',left),
('fixed right',right))
p.move_cursor_to_coords(size, cx, 0)
def test_cursor(self):
self.mctest("cursor left edge",2,2,(10,2),2,0)
self.mctest("cursor left edge-1",2,2,(10,2),1,0)
self.mctest("cursor right edge",2,2,(10,2),7,5)
self.mctest("cursor right edge+1",2,2,(10,2),8,5)
def test_reduced_padding_cursor(self):
# FIXME: This is at least consistent now, but I don't like it.
# pack() on an Edit should leave room for the cursor
# fixing this gets deep into things like Edit._shift_view_to_cursor
# though, so this might not get fixed for a while
p = urwid.Padding(urwid.Edit(u'',u''), width='pack', left=4)
self.assertEqual(p.render((10,), True).cursor, None)
self.assertEqual(p.get_cursor_coords((10,)), None)
self.assertEqual(p.render((4,), True).cursor, None)
self.assertEqual(p.get_cursor_coords((4,)), None)
p = urwid.Padding(urwid.Edit(u'',u''), width=('relative', 100), left=4)
self.assertEqual(p.render((10,), True).cursor, (4, 0))
self.assertEqual(p.get_cursor_coords((10,)), (4, 0))
self.assertEqual(p.render((4,), True).cursor, None)
self.assertEqual(p.get_cursor_coords((4,)), None)
class FillerTest(unittest.TestCase):
def ftest(self, desc, valign, height, maxrow, top, bottom,
min_height=None):
f = urwid.Filler(None, valign, height, min_height)
t, b = f.filler_values((20,maxrow), False)
assert (t,b)==(top,bottom), "%s expected %s but got %s"%(
desc, (top,bottom), (t,b))
def fetest(self, desc, valign, height):
self.assertRaises(urwid.FillerError, lambda:
urwid.Filler(None, valign, height))
def test_create(self):
self.fetest("invalid pad",6,5)
self.fetest("invalid pad type",('bad',2),5)
self.fetest("invalid width",'middle','42')
self.fetest("invalid width type",'middle',('gouranga',4))
self.fetest("invalid combination",('relative',20),
('fixed bottom',4))
self.fetest("invalid combination 2",('relative',20),
('fixed top',4))
def test_values(self):
self.ftest("top align 5 7",'top',5,7,0,2)
self.ftest("top align 7 7",'top',7,7,0,0)
self.ftest("top align 9 7",'top',9,7,0,0)
self.ftest("bottom align 5 7",'bottom',5,7,2,0)
self.ftest("middle align 5 7",'middle',5,7,1,1)
self.ftest("fixed top",('fixed top',3),5,10,3,2)
self.ftest("fixed top reduce",('fixed top',3),8,10,2,0)
self.ftest("fixed top shrink",('fixed top',3),18,10,0,0)
self.ftest("fixed top, bottom",
('fixed top',3),('fixed bottom',4),17,3,4)
self.ftest("fixed top, bottom, min_width",
('fixed top',3),('fixed bottom',4),10,3,2,5)
self.ftest("fixed top, bottom, min_width 2",
('fixed top',3),('fixed bottom',4),10,2,0,8)
self.ftest("fixed bottom",('fixed bottom',3),5,10,2,3)
self.ftest("fixed bottom reduce",('fixed bottom',3),8,10,0,2)
self.ftest("fixed bottom shrink",('fixed bottom',3),18,10,0,0)
self.ftest("fixed bottom, top",
('fixed bottom',3),('fixed top',4),17,4,3)
self.ftest("fixed bottom, top, min_height",
('fixed bottom',3),('fixed top',4),10,2,3,5)
self.ftest("fixed bottom, top, min_height 2",
('fixed bottom',3),('fixed top',4),10,0,2,8)
self.ftest("relative 30",('relative',30),5,10,1,4)
self.ftest("relative 50",('relative',50),5,10,2,3)
self.ftest("relative 130 edge",('relative',130),5,10,5,0)
self.ftest("relative -10 edge",('relative',-10),4,10,0,6)
self.ftest("middle relative 70",'middle',('relative',70),
10,1,2)
self.ftest("middle relative 70 grow 8",'middle',('relative',70),
10,1,1,8)
def test_repr(self):
repr(urwid.Filler(urwid.Text(u'hai')))

View File

@@ -1,22 +0,0 @@
import unittest
import doctest
import urwid
def load_tests(loader, tests, ignore):
module_doctests = [
urwid.widget,
urwid.wimp,
urwid.decoration,
urwid.display_common,
urwid.main_loop,
urwid.monitored_list,
urwid.raw_display,
'urwid.split_repr', # override function with same name
urwid.util,
urwid.signals,
]
for m in module_doctests:
tests.addTests(doctest.DocTestSuite(m,
optionflags=doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL))
return tests

View File

@@ -1,147 +0,0 @@
import os
import unittest
import platform
import urwid
from urwid.compat import PYTHON3
class EventLoopTestMixin(object):
def test_event_loop(self):
rd, wr = os.pipe()
evl = self.evl
out = []
def step1():
out.append("writing")
os.write(wr, "hi".encode('ascii'))
def step2():
out.append(os.read(rd, 2).decode('ascii'))
raise urwid.ExitMainLoop
handle = evl.alarm(0, step1)
handle = evl.watch_file(rd, step2)
evl.run()
self.assertEqual(out, ["writing", "hi"])
def test_remove_alarm(self):
evl = self.evl
handle = evl.alarm(50, lambda: None)
self.assertTrue(evl.remove_alarm(handle))
self.assertFalse(evl.remove_alarm(handle))
def test_remove_watch_file(self):
evl = self.evl
handle = evl.watch_file(5, lambda: None)
self.assertTrue(evl.remove_watch_file(handle))
self.assertFalse(evl.remove_watch_file(handle))
_expected_idle_handle = 1
def test_run(self):
evl = self.evl
out = []
rd, wr = os.pipe()
self.assertEqual(os.write(wr, "data".encode('ascii')), 4)
def say_hello():
out.append("hello")
def say_waiting():
out.append("waiting")
def exit_clean():
out.append("clean exit")
raise urwid.ExitMainLoop
def exit_error():
1/0
handle = evl.alarm(0.01, exit_clean)
handle = evl.alarm(0.005, say_hello)
idle_handle = evl.enter_idle(say_waiting)
if self._expected_idle_handle is not None:
self.assertEqual(idle_handle, 1)
evl.run()
self.assertTrue("hello" in out, out)
self.assertTrue("clean exit"in out, out)
handle = evl.watch_file(rd, exit_clean)
del out[:]
evl.run()
self.assertEqual(out, ["clean exit"])
self.assertTrue(evl.remove_watch_file(handle))
handle = evl.alarm(0, exit_error)
self.assertRaises(ZeroDivisionError, evl.run)
handle = evl.watch_file(rd, exit_error)
self.assertRaises(ZeroDivisionError, evl.run)
class SelectEventLoopTest(unittest.TestCase, EventLoopTestMixin):
def setUp(self):
self.evl = urwid.SelectEventLoop()
try:
import gi.repository
except ImportError:
pass
else:
class GLibEventLoopTest(unittest.TestCase, EventLoopTestMixin):
def setUp(self):
self.evl = urwid.GLibEventLoop()
try:
import tornado
except ImportError:
pass
else:
class TornadoEventLoopTest(unittest.TestCase, EventLoopTestMixin):
def setUp(self):
from tornado.ioloop import IOLoop
self.evl = urwid.TornadoEventLoop(IOLoop())
try:
import twisted
except ImportError:
pass
else:
class TwistedEventLoopTest(unittest.TestCase, EventLoopTestMixin):
def setUp(self):
self.evl = urwid.TwistedEventLoop()
# can't restart twisted reactor, so use shortened tests
def test_event_loop(self):
pass
def test_run(self):
evl = self.evl
out = []
rd, wr = os.pipe()
self.assertEqual(os.write(wr, "data".encode('ascii')), 4)
def step2():
out.append(os.read(rd, 2).decode('ascii'))
def say_hello():
out.append("hello")
def say_waiting():
out.append("waiting")
def exit_clean():
out.append("clean exit")
raise urwid.ExitMainLoop
def exit_error():
1/0
handle = evl.watch_file(rd, step2)
handle = evl.alarm(0.01, exit_clean)
handle = evl.alarm(0.005, say_hello)
self.assertEqual(evl.enter_idle(say_waiting), 1)
evl.run()
self.assertTrue("da" in out, out)
self.assertTrue("ta" in out, out)
self.assertTrue("hello" in out, out)
self.assertTrue("clean exit" in out, out)
try:
import asyncio
except ImportError:
pass
else:
class AsyncioEventLoopTest(unittest.TestCase, EventLoopTestMixin):
def setUp(self):
self.evl = urwid.AsyncioEventLoop()
_expected_idle_handle = None

View File

@@ -1,97 +0,0 @@
import unittest
from urwid import graphics
from urwid.compat import B
import urwid
class LineBoxTest(unittest.TestCase):
def border(self, tl, t, tr, l, r, bl, b, br):
return [bytes().join([tl, t, tr]),
bytes().join([l, B(" "), r]),
bytes().join([bl, b, br]),]
def test_linebox_border(self):
urwid.set_encoding("utf-8")
t = urwid.Text("")
l = urwid.LineBox(t).render((3,)).text
# default
self.assertEqual(l,
self.border(B("\xe2\x94\x8c"), B("\xe2\x94\x80"),
B("\xe2\x94\x90"), B("\xe2\x94\x82"), B("\xe2\x94\x82"),
B("\xe2\x94\x94"), B("\xe2\x94\x80"), B("\xe2\x94\x98")))
nums = [B(str(n)) for n in range(8)]
b = dict(zip(["tlcorner", "tline", "trcorner", "lline", "rline",
"blcorner", "bline", "brcorner"], nums))
l = urwid.LineBox(t, **b).render((3,)).text
self.assertEqual(l, self.border(*nums))
class BarGraphTest(unittest.TestCase):
def bgtest(self, desc, data, top, widths, maxrow, exp ):
rval = graphics.calculate_bargraph_display(data,top,widths,maxrow)
assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval)
def test1(self):
self.bgtest('simplest',[[0]],5,[1],1,
[(1,[(0,1)])] )
self.bgtest('simpler',[[0],[0]],5,[1,2],5,
[(5,[(0,3)])] )
self.bgtest('simple',[[5]],5,[1],1,
[(1,[(1,1)])] )
self.bgtest('2col-1',[[2],[0]],5,[1,2],5,
[(3,[(0,3)]), (2,[(1,1),(0,2)]) ] )
self.bgtest('2col-2',[[0],[2]],5,[1,2],5,
[(3,[(0,3)]), (2,[(0,1),(1,2)]) ] )
self.bgtest('2col-3',[[2],[3]],5,[1,2],5,
[(2,[(0,3)]), (1,[(0,1),(1,2)]), (2,[(1,3)]) ] )
self.bgtest('3col-1',[[5],[3],[0]],5,[2,1,1],5,
[(2,[(1,2),(0,2)]), (3,[(1,3),(0,1)]) ] )
self.bgtest('3col-2',[[4],[4],[4]],5,[2,1,1],5,
[(1,[(0,4)]), (4,[(1,4)]) ] )
self.bgtest('3col-3',[[1],[2],[3]],5,[2,1,1],5,
[(2,[(0,4)]), (1,[(0,3),(1,1)]), (1,[(0,2),(1,2)]),
(1,[(1,4)]) ] )
self.bgtest('3col-4',[[4],[2],[4]],5,[1,2,1],5,
[(1,[(0,4)]), (2,[(1,1),(0,2),(1,1)]), (2,[(1,4)]) ] )
def test2(self):
self.bgtest('simple1a',[[2,0],[2,1]],2,[1,1],2,
[(1,[(1,2)]),(1,[(1,1),(2,1)]) ] )
self.bgtest('simple1b',[[2,1],[2,0]],2,[1,1],2,
[(1,[(1,2)]),(1,[(2,1),(1,1)]) ] )
self.bgtest('cross1a',[[2,2],[1,2]],2,[1,1],2,
[(2,[(2,2)]) ] )
self.bgtest('cross1b',[[1,2],[2,2]],2,[1,1],2,
[(2,[(2,2)]) ] )
self.bgtest('mix1a',[[3,2,1],[2,2,2],[1,2,3]],3,[1,1,1],3,
[(1,[(1,1),(0,1),(3,1)]),(1,[(2,1),(3,2)]),
(1,[(3,3)]) ] )
self.bgtest('mix1b',[[1,2,3],[2,2,2],[3,2,1]],3,[1,1,1],3,
[(1,[(3,1),(0,1),(1,1)]),(1,[(3,2),(2,1)]),
(1,[(3,3)]) ] )
class SmoothBarGraphTest(unittest.TestCase):
def sbgtest(self, desc, data, top, exp ):
urwid.set_encoding('utf-8')
g = urwid.BarGraph( ['black','red','blue'],
None, {(1,0):'red/black', (2,1):'blue/red'})
g.set_data( data, top )
rval = g.calculate_display((5,3))
assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval)
def test1(self):
self.sbgtest('simple', [[3]], 5,
[(1, [(0, 5)]), (1, [((1, 0, 6), 5)]), (1, [(1, 5)])] )
self.sbgtest('boring', [[4,2]], 6,
[(1, [(0, 5)]), (1, [(1, 5)]), (1, [(2,5)]) ] )
self.sbgtest('two', [[4],[2]], 6,
[(1, [(0, 5)]), (1, [(1, 3), (0, 2)]), (1, [(1, 5)]) ] )
self.sbgtest('twos', [[3],[4]], 6,
[(1, [(0, 5)]), (1, [((1,0,4), 3), (1, 2)]), (1, [(1,5)]) ] )
self.sbgtest('twof', [[4],[3]], 6,
[(1, [(0, 5)]), (1, [(1,3), ((1,0,4), 2)]), (1, [(1,5)]) ] )

View File

@@ -1,804 +0,0 @@
import unittest
from urwid.compat import B
from urwid.tests.util import SelectableText
import urwid
class ListBoxCalculateVisibleTest(unittest.TestCase):
def cvtest(self, desc, body, focus, offset_rows, inset_fraction,
exp_offset_inset, exp_cur ):
lbox = urwid.ListBox(body)
lbox.body.set_focus( focus )
lbox.offset_rows = offset_rows
lbox.inset_fraction = inset_fraction
middle, top, bottom = lbox.calculate_visible((4,5),focus=1)
offset_inset, focus_widget, focus_pos, _ign, cursor = middle
if cursor is not None:
x, y = cursor
y += offset_inset
cursor = x, y
assert offset_inset == exp_offset_inset, "%s got: %r expected: %r" %(desc,offset_inset,exp_offset_inset)
assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
def test1_simple(self):
T = urwid.Text
l = [T(""),T(""),T("\n"),T("\n\n"),T("\n"),T(""),T("")]
self.cvtest( "simple top position",
l, 3, 0, (0,1), 0, None )
self.cvtest( "simple middle position",
l, 3, 1, (0,1), 1, None )
self.cvtest( "simple bottom postion",
l, 3, 2, (0,1), 2, None )
self.cvtest( "straddle top edge",
l, 3, 0, (1,2), -1, None )
self.cvtest( "straddle bottom edge",
l, 3, 4, (0,1), 4, None )
self.cvtest( "off bottom edge",
l, 3, 5, (0,1), 4, None )
self.cvtest( "way off bottom edge",
l, 3, 100, (0,1), 4, None )
self.cvtest( "gap at top",
l, 0, 2, (0,1), 0, None )
self.cvtest( "gap at top and off bottom edge",
l, 2, 5, (0,1), 2, None )
self.cvtest( "gap at bottom",
l, 6, 1, (0,1), 4, None )
self.cvtest( "gap at bottom and straddling top edge",
l, 4, 0, (1,2), 1, None )
self.cvtest( "gap at bottom cannot completely fill",
[T(""),T(""),T("")], 1, 0, (0,1), 1, None )
self.cvtest( "gap at top and bottom",
[T(""),T(""),T("")], 1, 2, (0,1), 1, None )
def test2_cursor(self):
T, E = urwid.Text, urwid.Edit
l1 = [T(""),T(""),T("\n"),E("","\n\nX"),T("\n"),T(""),T("")]
l2 = [T(""),T(""),T("\n"),E("","YY\n\n"),T("\n"),T(""),T("")]
l2[3].set_edit_pos(2)
self.cvtest( "plain cursor in view",
l1, 3, 1, (0,1), 1, (1,3) )
self.cvtest( "cursor off top",
l2, 3, 0, (1,3), 0, (2, 0) )
self.cvtest( "cursor further off top",
l2, 3, 0, (2,3), 0, (2, 0) )
self.cvtest( "cursor off bottom",
l1, 3, 3, (0,1), 2, (1, 4) )
self.cvtest( "cursor way off bottom",
l1, 3, 100, (0,1), 2, (1, 4) )
class ListBoxChangeFocusTest(unittest.TestCase):
def cftest(self, desc, body, pos, offset_inset,
coming_from, cursor, snap_rows,
exp_offset_rows, exp_inset_fraction, exp_cur ):
lbox = urwid.ListBox(body)
lbox.change_focus( (4,5), pos, offset_inset, coming_from,
cursor, snap_rows )
exp = exp_offset_rows, exp_inset_fraction
act = lbox.offset_rows, lbox.inset_fraction
cursor = None
focus_widget, focus_pos = lbox.body.get_focus()
if focus_widget.selectable():
if hasattr(focus_widget,'get_cursor_coords'):
cursor=focus_widget.get_cursor_coords((4,))
assert act == exp, "%s got: %s expected: %s" %(desc, act, exp)
assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
def test1unselectable(self):
T = urwid.Text
l = [T("\n"),T("\n\n"),T("\n\n"),T("\n\n"),T("\n")]
self.cftest( "simple unselectable",
l, 2, 0, None, None, None, 0, (0,1), None )
self.cftest( "unselectable",
l, 2, 1, None, None, None, 1, (0,1), None )
self.cftest( "unselectable off top",
l, 2, -2, None, None, None, 0, (2,3), None )
self.cftest( "unselectable off bottom",
l, 3, 2, None, None, None, 2, (0,1), None )
def test2selectable(self):
T, S = urwid.Text, SelectableText
l = [T("\n"),T("\n\n"),S("\n\n"),T("\n\n"),T("\n")]
self.cftest( "simple selectable",
l, 2, 0, None, None, None, 0, (0,1), None )
self.cftest( "selectable",
l, 2, 1, None, None, None, 1, (0,1), None )
self.cftest( "selectable at top",
l, 2, 0, 'below', None, None, 0, (0,1), None )
self.cftest( "selectable at bottom",
l, 2, 2, 'above', None, None, 2, (0,1), None )
self.cftest( "selectable off top snap",
l, 2, -1, 'below', None, None, 0, (0,1), None )
self.cftest( "selectable off bottom snap",
l, 2, 3, 'above', None, None, 2, (0,1), None )
self.cftest( "selectable off top no snap",
l, 2, -1, 'above', None, None, 0, (1,3), None )
self.cftest( "selectable off bottom no snap",
l, 2, 3, 'below', None, None, 3, (0,1), None )
def test3large_selectable(self):
T, S = urwid.Text, SelectableText
l = [T("\n"),S("\n\n\n\n\n\n"),T("\n")]
self.cftest( "large selectable no snap",
l, 1, -1, None, None, None, 0, (1,7), None )
self.cftest( "large selectable snap up",
l, 1, -2, 'below', None, None, 0, (0,1), None )
self.cftest( "large selectable snap up2",
l, 1, -2, 'below', None, 2, 0, (0,1), None )
self.cftest( "large selectable almost snap up",
l, 1, -2, 'below', None, 1, 0, (2,7), None )
self.cftest( "large selectable snap down",
l, 1, 0, 'above', None, None, 0, (2,7), None )
self.cftest( "large selectable snap down2",
l, 1, 0, 'above', None, 2, 0, (2,7), None )
self.cftest( "large selectable almost snap down",
l, 1, 0, 'above', None, 1, 0, (0,1), None )
m = [T("\n\n\n\n"), S("\n\n\n\n\n"), T("\n\n\n\n")]
self.cftest( "large selectable outside view down",
m, 1, 4, 'above', None, None, 0, (0,1), None )
self.cftest( "large selectable outside view up",
m, 1, -5, 'below', None, None, 0, (1,6), None )
def test4cursor(self):
T,E = urwid.Text, urwid.Edit
#...
def test5set_focus_valign(self):
T,E = urwid.Text, urwid.Edit
lbox = urwid.ListBox(urwid.SimpleFocusListWalker([
T(''), T('')]))
lbox.set_focus_valign('middle')
# TODO: actually test the result
class ListBoxRenderTest(unittest.TestCase):
def ltest(self,desc,body,focus,offset_inset_rows,exp_text,exp_cur):
exp_text = [B(t) for t in exp_text]
lbox = urwid.ListBox(body)
lbox.body.set_focus( focus )
lbox.shift_focus((4,10), offset_inset_rows )
canvas = lbox.render( (4,5), focus=1 )
text = [bytes().join([t for at, cs, t in ln]) for ln in canvas.content()]
cursor = canvas.cursor
assert text == exp_text, "%s (text) got: %r expected: %r" %(desc,text,exp_text)
assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
def test1_simple(self):
T = urwid.Text
self.ltest( "simple one text item render",
[T("1\n2")], 0, 0,
["1 ","2 "," "," "," "],None)
self.ltest( "simple multi text item render off bottom",
[T("1"),T("2"),T("3\n4"),T("5"),T("6")], 2, 2,
["1 ","2 ","3 ","4 ","5 "],None)
self.ltest( "simple multi text item render off top",
[T("1"),T("2"),T("3\n4"),T("5"),T("6")], 2, 1,
["2 ","3 ","4 ","5 ","6 "],None)
def test2_trim(self):
T = urwid.Text
self.ltest( "trim unfocused bottom",
[T("1\n2"),T("3\n4"),T("5\n6")], 1, 2,
["1 ","2 ","3 ","4 ","5 "],None)
self.ltest( "trim unfocused top",
[T("1\n2"),T("3\n4"),T("5\n6")], 1, 1,
["2 ","3 ","4 ","5 ","6 "],None)
self.ltest( "trim none full focus",
[T("1\n2\n3\n4\n5")], 0, 0,
["1 ","2 ","3 ","4 ","5 "],None)
self.ltest( "trim focus bottom",
[T("1\n2\n3\n4\n5\n6")], 0, 0,
["1 ","2 ","3 ","4 ","5 "],None)
self.ltest( "trim focus top",
[T("1\n2\n3\n4\n5\n6")], 0, -1,
["2 ","3 ","4 ","5 ","6 "],None)
self.ltest( "trim focus top and bottom",
[T("1\n2\n3\n4\n5\n6\n7")], 0, -1,
["2 ","3 ","4 ","5 ","6 "],None)
def test3_shift(self):
T,E = urwid.Text, urwid.Edit
self.ltest( "shift up one fit",
[T("1\n2"),T("3"),T("4"),T("5"),T("6")], 4, 5,
["2 ","3 ","4 ","5 ","6 "],None)
e = E("","ab\nc",1)
e.set_edit_pos( 2 )
self.ltest( "shift down one cursor over edge",
[e,T("3"),T("4"),T("5\n6")], 0, -1,
["ab ","c ","3 ","4 ","5 "], (2,0))
self.ltest( "shift up one cursor over edge",
[T("1\n2"),T("3"),T("4"),E("","d\ne")], 3, 4,
["2 ","3 ","4 ","d ","e "], (1,4))
self.ltest( "shift none cursor top focus over edge",
[E("","ab\n"),T("3"),T("4"),T("5\n6")], 0, -1,
[" ","3 ","4 ","5 ","6 "], (0,0))
e = E("","abc\nd")
e.set_edit_pos( 3 )
self.ltest( "shift none cursor bottom focus over edge",
[T("1\n2"),T("3"),T("4"),e], 3, 4,
["1 ","2 ","3 ","4 ","abc "], (3,4))
def test4_really_large_contents(self):
T,E = urwid.Text, urwid.Edit
self.ltest("really large edit",
[T(u"hello"*100)], 0, 0,
["hell","ohel","lohe","lloh","ello"], None)
self.ltest("really large edit",
[E(u"", u"hello"*100)], 0, 0,
["hell","ohel","lohe","lloh","llo "], (3,4))
class ListBoxKeypressTest(unittest.TestCase):
def ktest(self, desc, key, body, focus, offset_inset,
exp_focus, exp_offset_inset, exp_cur, lbox = None):
if lbox is None:
lbox = urwid.ListBox(body)
lbox.body.set_focus( focus )
lbox.shift_focus((4,10), offset_inset )
ret_key = lbox.keypress((4,5),key)
middle, top, bottom = lbox.calculate_visible((4,5),focus=1)
offset_inset, focus_widget, focus_pos, _ign, cursor = middle
if cursor is not None:
x, y = cursor
y += offset_inset
cursor = x, y
exp = exp_focus, exp_offset_inset
act = focus_pos, offset_inset
assert act == exp, "%s got: %r expected: %r" %(desc,act,exp)
assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
return ret_key,lbox
def test1_up(self):
T,S,E = urwid.Text, SelectableText, urwid.Edit
self.ktest( "direct selectable both visible", 'up',
[S(""),S("")], 1, 1,
0, 0, None )
self.ktest( "selectable skip one all visible", 'up',
[S(""),T(""),S("")], 2, 2,
0, 0, None )
key,lbox = self.ktest( "nothing above no scroll", 'up',
[S("")], 0, 0,
0, 0, None )
assert key == 'up'
key, lbox = self.ktest( "unselectable above no scroll", 'up',
[T(""),T(""),S("")], 2, 2,
2, 2, None )
assert key == 'up'
self.ktest( "unselectable above scroll 1", 'up',
[T(""),S(""),T("\n\n\n")], 1, 0,
1, 1, None )
self.ktest( "selectable above scroll 1", 'up',
[S(""),S(""),T("\n\n\n")], 1, 0,
0, 0, None )
self.ktest( "selectable above too far", 'up',
[S(""),T(""),S(""),T("\n\n\n")], 2, 0,
2, 1, None )
self.ktest( "selectable above skip 1 scroll 1", 'up',
[S(""),T(""),S(""),T("\n\n\n")], 2, 1,
0, 0, None )
self.ktest( "tall selectable above scroll 2", 'up',
[S(""),S("\n"),S(""),T("\n\n\n")], 2, 0,
1, 0, None )
self.ktest( "very tall selectable above scroll 5", 'up',
[S(""),S("\n\n\n\n"),S(""),T("\n\n\n\n")], 2, 0,
1, 0, None )
self.ktest( "very tall selected scroll within 1", 'up',
[S(""),S("\n\n\n\n\n")], 1, -1,
1, 0, None )
self.ktest( "edit above pass cursor", 'up',
[E("","abc"),E("","de")], 1, 1,
0, 0, (2, 0) )
key,lbox = self.ktest( "edit too far above pass cursor A", 'up',
[E("","abc"),T("\n\n\n\n"),E("","de")], 2, 4,
1, 0, None )
self.ktest( "edit too far above pass cursor B", 'up',
None, None, None,
0, 0, (2,0), lbox )
self.ktest( "within focus cursor made not visible", 'up',
[T("\n\n\n"),E("hi\n","ab")], 1, 3,
0, 0, None )
self.ktest( "within focus cursor made not visible (2)", 'up',
[T("\n\n\n\n"),E("hi\n","ab")], 1, 3,
0, -1, None )
self.ktest( "force focus unselectable" , 'up',
[T("\n\n\n\n"),S("")], 1, 4,
0, 0, None )
self.ktest( "pathological cursor widget", 'up',
[T("\n"),E("\n\n\n\n\n","a")], 1, 4,
0, -1, None )
self.ktest( "unselectable to unselectable", 'up',
[T(""),T(""),T(""),T(""),T(""),T(""),T("")], 2, 0,
1, 0, None )
self.ktest( "unselectable over edge to same", 'up',
[T(""),T("12\n34"),T(""),T(""),T(""),T("")],1,-1,
1, 0, None )
key,lbox = self.ktest( "edit short between pass cursor A", 'up',
[E("","abcd"),E("","a"),E("","def")], 2, 2,
1, 1, (1,1) )
self.ktest( "edit short between pass cursor B", 'up',
None, None, None,
0, 0, (3,0), lbox )
e = E("","\n\n\n\n\n")
e.set_edit_pos(1)
key,lbox = self.ktest( "edit cursor force scroll", 'up',
[e], 0, -1,
0, 0, (0,0) )
assert lbox.inset_fraction[0] == 0
def test2_down(self):
T,S,E = urwid.Text, SelectableText, urwid.Edit
self.ktest( "direct selectable both visible", 'down',
[S(""),S("")], 0, 0,
1, 1, None )
self.ktest( "selectable skip one all visible", 'down',
[S(""),T(""),S("")], 0, 0,
2, 2, None )
key,lbox = self.ktest( "nothing below no scroll", 'down',
[S("")], 0, 0,
0, 0, None )
assert key == 'down'
key, lbox = self.ktest( "unselectable below no scroll", 'down',
[S(""),T(""),T("")], 0, 0,
0, 0, None )
assert key == 'down'
self.ktest( "unselectable below scroll 1", 'down',
[T("\n\n\n"),S(""),T("")], 1, 4,
1, 3, None )
self.ktest( "selectable below scroll 1", 'down',
[T("\n\n\n"),S(""),S("")], 1, 4,
2, 4, None )
self.ktest( "selectable below too far", 'down',
[T("\n\n\n"),S(""),T(""),S("")], 1, 4,
1, 3, None )
self.ktest( "selectable below skip 1 scroll 1", 'down',
[T("\n\n\n"),S(""),T(""),S("")], 1, 3,
3, 4, None )
self.ktest( "tall selectable below scroll 2", 'down',
[T("\n\n\n"),S(""),S("\n"),S("")], 1, 4,
2, 3, None )
self.ktest( "very tall selectable below scroll 5", 'down',
[T("\n\n\n\n"),S(""),S("\n\n\n\n"),S("")], 1, 4,
2, 0, None )
self.ktest( "very tall selected scroll within 1", 'down',
[S("\n\n\n\n\n"),S("")], 0, 0,
0, -1, None )
self.ktest( "edit below pass cursor", 'down',
[E("","de"),E("","abc")], 0, 0,
1, 1, (2, 1) )
key,lbox=self.ktest( "edit too far below pass cursor A", 'down',
[E("","de"),T("\n\n\n\n"),E("","abc")], 0, 0,
1, 0, None )
self.ktest( "edit too far below pass cursor B", 'down',
None, None, None,
2, 4, (2,4), lbox )
odd_e = E("","hi\nab")
odd_e.set_edit_pos( 2 )
# disble cursor movement in odd_e object
odd_e.move_cursor_to_coords = lambda s,c,xy: 0
self.ktest( "within focus cursor made not visible", 'down',
[odd_e,T("\n\n\n\n")], 0, 0,
1, 1, None )
self.ktest( "within focus cursor made not visible (2)", 'down',
[odd_e,T("\n\n\n\n"),], 0, 0,
1, 1, None )
self.ktest( "force focus unselectable" , 'down',
[S(""),T("\n\n\n\n")], 0, 0,
1, 0, None )
odd_e.set_edit_text( "hi\n\n\n\n\n" )
self.ktest( "pathological cursor widget", 'down',
[odd_e,T("\n")], 0, 0,
1, 4, None )
self.ktest( "unselectable to unselectable", 'down',
[T(""),T(""),T(""),T(""),T(""),T(""),T("")], 4, 4,
5, 4, None )
self.ktest( "unselectable over edge to same", 'down',
[T(""),T(""),T(""),T(""),T("12\n34"),T("")],4,4,
4, 3, None )
key,lbox=self.ktest( "edit short between pass cursor A", 'down',
[E("","abc"),E("","a"),E("","defg")], 0, 0,
1, 1, (1,1) )
self.ktest( "edit short between pass cursor B", 'down',
None, None, None,
2, 2, (3,2), lbox )
e = E("","\n\n\n\n\n")
e.set_edit_pos(4)
key,lbox = self.ktest( "edit cursor force scroll", 'down',
[e], 0, 0,
0, -1, (0,4) )
assert lbox.inset_fraction[0] == 1
def test3_page_up(self):
T,S,E = urwid.Text, SelectableText, urwid.Edit
self.ktest( "unselectable aligned to aligned", 'page up',
[T(""),T("\n"),T("\n\n"),T(""),T("\n"),T("\n\n")], 3, 0,
1, 0, None )
self.ktest( "unselectable unaligned to aligned", 'page up',
[T(""),T("\n"),T("\n"),T("\n"),T("\n"),T("\n\n")], 3,-1,
1, 0, None )
self.ktest( "selectable to unselectable", 'page up',
[T(""),T("\n"),T("\n"),T("\n"),S("\n"),T("\n\n")], 4, 1,
1, -1, None )
self.ktest( "selectable to cut off selectable", 'page up',
[S("\n\n"),T("\n"),T("\n"),S("\n"),T("\n\n")], 3, 1,
0, -1, None )
self.ktest( "seletable to selectable", 'page up',
[T("\n\n"),S("\n"),T("\n"),S("\n"),T("\n\n")], 3, 1,
1, 1, None )
self.ktest( "within very long selectable", 'page up',
[S(""),S("\n\n\n\n\n\n\n\n"),T("\n")], 1, -6,
1, -1, None )
e = E("","\n\nab\n\n\n\n\ncd\n")
e.set_edit_pos(11)
self.ktest( "within very long cursor widget", 'page up',
[S(""),e,T("\n")], 1, -6,
1, -2, (2, 0) )
self.ktest( "pathological cursor widget", 'page up',
[T(""),E("\n\n\n\n\n\n\n\n","ab"),T("")], 1, -5,
0, 0, None )
e = E("","\nab\n\n\n\n\ncd\n")
e.set_edit_pos(10)
self.ktest( "very long cursor widget snap", 'page up',
[T(""),e,T("\n")], 1, -5,
1, 0, (2, 1) )
self.ktest( "slight scroll selectable", 'page up',
[T("\n"),S("\n"),T(""),S(""),T("\n\n\n"),S("")], 5, 4,
3, 0, None )
self.ktest( "scroll into snap region", 'page up',
[T("\n"),S("\n"),T(""),T(""),T("\n\n\n"),S("")], 5, 4,
1, 0, None )
self.ktest( "mid scroll short", 'page up',
[T("\n"),T(""),T(""),S(""),T(""),T("\n"),S(""),T("\n")],
6, 2, 3, 1, None )
self.ktest( "mid scroll long", 'page up',
[T("\n"),S(""),T(""),S(""),T(""),T("\n"),S(""),T("\n")],
6, 2, 1, 0, None )
self.ktest( "mid scroll perfect", 'page up',
[T("\n"),S(""),S(""),S(""),T(""),T("\n"),S(""),T("\n")],
6, 2, 2, 0, None )
self.ktest( "cursor move up fail short", 'page up',
[T("\n"),T("\n"),E("","\nab"),T(""),T("")], 2, 1,
2, 4, (0, 4) )
self.ktest( "cursor force fail short", 'page up',
[T("\n"),T("\n"),E("\n","ab"),T(""),T("")], 2, 1,
0, 0, None )
odd_e = E("","hi\nab")
odd_e.set_edit_pos( 2 )
# disble cursor movement in odd_e object
odd_e.move_cursor_to_coords = lambda s,c,xy: 0
self.ktest( "cursor force fail long", 'page up',
[odd_e,T("\n"),T("\n"),T("\n"),S(""),T("\n")], 4, 2,
1, -1, None )
self.ktest( "prefer not cut off", 'page up',
[S("\n"),T("\n"),S(""),T("\n\n"),S(""),T("\n")], 4, 2,
2, 1, None )
self.ktest( "allow cut off", 'page up',
[S("\n"),T("\n"),T(""),T("\n\n"),S(""),T("\n")], 4, 2,
0, -1, None )
self.ktest( "at top fail", 'page up',
[T("\n\n"),T("\n"),T("\n\n\n")], 0, 0,
0, 0, None )
self.ktest( "all visible fail", 'page up',
[T("a"),T("\n")], 0, 0,
0, 0, None )
self.ktest( "current ok fail", 'page up',
[T("\n\n"),S("hi")], 1, 3,
1, 3, None )
self.ktest( "all visible choose top selectable", 'page up',
[T(""),S("a"),S("b"),S("c")], 3, 3,
1, 1, None )
self.ktest( "bring in edge choose top", 'page up',
[S("b"),T("-"),S("-"),T("c"),S("d"),T("-")],4,3,
0, 0, None )
self.ktest( "bring in edge choose top selectable", 'page up',
[T("b"),S("-"),S("-"),T("c"),S("d"),T("-")],4,3,
1, 1, None )
def test4_page_down(self):
T,S,E = urwid.Text, SelectableText, urwid.Edit
self.ktest( "unselectable aligned to aligned", 'page down',
[T("\n\n"),T("\n"),T(""),T("\n\n"),T("\n"),T("")], 2, 4,
4, 3, None )
self.ktest( "unselectable unaligned to aligned", 'page down',
[T("\n\n"),T("\n"),T("\n"),T("\n"),T("\n"),T("")], 2, 4,
4, 3, None )
self.ktest( "selectable to unselectable", 'page down',
[T("\n\n"),S("\n"),T("\n"),T("\n"),T("\n"),T("")], 1, 2,
4, 4, None )
self.ktest( "selectable to cut off selectable", 'page down',
[T("\n\n"),S("\n"),T("\n"),T("\n"),S("\n\n")], 1, 2,
4, 3, None )
self.ktest( "seletable to selectable", 'page down',
[T("\n\n"),S("\n"),T("\n"),S("\n"),T("\n\n")], 1, 1,
3, 2, None )
self.ktest( "within very long selectable", 'page down',
[T("\n"),S("\n\n\n\n\n\n\n\n"),S("")], 1, 2,
1, -3, None )
e = E("","\nab\n\n\n\n\ncd\n\n")
e.set_edit_pos(2)
self.ktest( "within very long cursor widget", 'page down',
[T("\n"),e,S("")], 1, 2,
1, -2, (1, 4) )
odd_e = E("","ab\n\n\n\n\n\n\n\n\n")
odd_e.set_edit_pos( 1 )
# disble cursor movement in odd_e object
odd_e.move_cursor_to_coords = lambda s,c,xy: 0
self.ktest( "pathological cursor widget", 'page down',
[T(""),odd_e,T("")], 1, 1,
2, 4, None )
e = E("","\nab\n\n\n\n\ncd\n")
e.set_edit_pos(2)
self.ktest( "very long cursor widget snap", 'page down',
[T("\n"),e,T("")], 1, 2,
1, -3, (1, 3) )
self.ktest( "slight scroll selectable", 'page down',
[S(""),T("\n\n\n"),S(""),T(""),S("\n"),T("\n")], 0, 0,
2, 4, None )
self.ktest( "scroll into snap region", 'page down',
[S(""),T("\n\n\n"),T(""),T(""),S("\n"),T("\n")], 0, 0,
4, 3, None )
self.ktest( "mid scroll short", 'page down',
[T("\n"),S(""),T("\n"),T(""),S(""),T(""),T(""),T("\n")],
1, 2, 4, 3, None )
self.ktest( "mid scroll long", 'page down',
[T("\n"),S(""),T("\n"),T(""),S(""),T(""),S(""),T("\n")],
1, 2, 6, 4, None )
self.ktest( "mid scroll perfect", 'page down',
[T("\n"),S(""),T("\n"),T(""),S(""),S(""),S(""),T("\n")],
1, 2, 5, 4, None )
e = E("","hi\nab")
e.set_edit_pos( 1 )
self.ktest( "cursor move up fail short", 'page down',
[T(""),T(""),e,T("\n"),T("\n")], 2, 1,
2, -1, (1, 0) )
odd_e = E("","hi\nab")
odd_e.set_edit_pos( 1 )
# disble cursor movement in odd_e object
odd_e.move_cursor_to_coords = lambda s,c,xy: 0
self.ktest( "cursor force fail short", 'page down',
[T(""),T(""),odd_e,T("\n"),T("\n")], 2, 2,
4, 3, None )
self.ktest( "cursor force fail long", 'page down',
[T("\n"),S(""),T("\n"),T("\n"),T("\n"),E("hi\n","ab")],
1, 2, 4, 4, None )
self.ktest( "prefer not cut off", 'page down',
[T("\n"),S(""),T("\n\n"),S(""),T("\n"),S("\n")], 1, 2,
3, 3, None )
self.ktest( "allow cut off", 'page down',
[T("\n"),S(""),T("\n\n"),T(""),T("\n"),S("\n")], 1, 2,
5, 4, None )
self.ktest( "at bottom fail", 'page down',
[T("\n\n"),T("\n"),T("\n\n\n")], 2, 1,
2, 1, None )
self.ktest( "all visible fail", 'page down',
[T("a"),T("\n")], 1, 1,
1, 1, None )
self.ktest( "current ok fail", 'page down',
[S("hi"),T("\n\n")], 0, 0,
0, 0, None )
self.ktest( "all visible choose last selectable", 'page down',
[S("a"),S("b"),S("c"),T("")], 0, 0,
2, 2, None )
self.ktest( "bring in edge choose last", 'page down',
[T("-"),S("d"),T("c"),S("-"),T("-"),S("b")],1,1,
5,4, None )
self.ktest( "bring in edge choose last selectable", 'page down',
[T("-"),S("d"),T("c"),S("-"),S("-"),T("b")],1,1,
4,3, None )
class ZeroHeightContentsTest(unittest.TestCase):
def test_listbox_pile(self):
lb = urwid.ListBox(urwid.SimpleListWalker(
[urwid.Pile([])]))
lb.render((40,10), focus=True)
def test_listbox_text_pile_page_down(self):
lb = urwid.ListBox(urwid.SimpleListWalker(
[urwid.Text(u'above'), urwid.Pile([])]))
lb.keypress((40,10), 'page down')
self.assertEqual(lb.get_focus()[1], 0)
lb.keypress((40,10), 'page down') # second one caused ListBox failure
self.assertEqual(lb.get_focus()[1], 0)
def test_listbox_text_pile_page_up(self):
lb = urwid.ListBox(urwid.SimpleListWalker(
[urwid.Pile([]), urwid.Text(u'below')]))
lb.set_focus(1)
lb.keypress((40,10), 'page up')
self.assertEqual(lb.get_focus()[1], 1)
lb.keypress((40,10), 'page up') # second one caused pile failure
self.assertEqual(lb.get_focus()[1], 1)
def test_listbox_text_pile_down(self):
sp = urwid.Pile([])
sp.selectable = lambda: True # abuse our Pile
lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(u'above'), sp]))
lb.keypress((40,10), 'down')
self.assertEqual(lb.get_focus()[1], 0)
lb.keypress((40,10), 'down')
self.assertEqual(lb.get_focus()[1], 0)
def test_listbox_text_pile_up(self):
sp = urwid.Pile([])
sp.selectable = lambda: True # abuse our Pile
lb = urwid.ListBox(urwid.SimpleListWalker([sp, urwid.Text(u'below')]))
lb.set_focus(1)
lb.keypress((40,10), 'up')
self.assertEqual(lb.get_focus()[1], 1)
lb.keypress((40,10), 'up')
self.assertEqual(lb.get_focus()[1], 1)

View File

@@ -1,37 +0,0 @@
import unittest
from urwid.compat import B
from urwid.escape import str_util
class DecodeOneTest(unittest.TestCase):
def gwt(self, ch, exp_ord, exp_pos):
ch = B(ch)
o, pos = str_util.decode_one(ch,0)
assert o==exp_ord, " got:%r expected:%r" % (o, exp_ord)
assert pos==exp_pos, " got:%r expected:%r" % (pos, exp_pos)
def test1byte(self):
self.gwt("ab", ord("a"), 1)
self.gwt("\xc0a", ord("?"), 1) # error
def test2byte(self):
self.gwt("\xc2", ord("?"), 1) # error
self.gwt("\xc0\x80", ord("?"), 1) # error
self.gwt("\xc2\x80", 0x80, 2)
self.gwt("\xdf\xbf", 0x7ff, 2)
def test3byte(self):
self.gwt("\xe0", ord("?"), 1) # error
self.gwt("\xe0\xa0", ord("?"), 1) # error
self.gwt("\xe0\x90\x80", ord("?"), 1) # error
self.gwt("\xe0\xa0\x80", 0x800, 3)
self.gwt("\xef\xbf\xbf", 0xffff, 3)
def test4byte(self):
self.gwt("\xf0", ord("?"), 1) # error
self.gwt("\xf0\x90", ord("?"), 1) # error
self.gwt("\xf0\x90\x80", ord("?"), 1) # error
self.gwt("\xf0\x80\x80\x80", ord("?"), 1) # error
self.gwt("\xf0\x90\x80\x80", 0x10000, 4)
self.gwt("\xf3\xbf\xbf\xbf", 0xfffff, 4)

View File

@@ -1,342 +0,0 @@
import unittest
from urwid import text_layout
from urwid.compat import B
import urwid
class CalcBreaksTest(object):
def cbtest(self, width, exp):
result = text_layout.default_layout.calculate_text_segments(
B(self.text), width, self.mode )
assert len(result) == len(exp), repr((result, exp))
for l,e in zip(result, exp):
end = l[-1][-1]
assert end == e, repr((result,exp))
def test(self):
for width, exp in self.do:
self.cbtest( width, exp )
class CalcBreaksCharTest(CalcBreaksTest, unittest.TestCase):
mode = 'any'
text = "abfghsdjf askhtrvs\naltjhgsdf ljahtshgf"
# tests
do = [
( 100, [18,38] ),
( 6, [6, 12, 18, 25, 31, 37, 38] ),
( 10, [10, 18, 29, 38] ),
]
class CalcBreaksDBCharTest(CalcBreaksTest, unittest.TestCase):
def setUp(self):
urwid.set_encoding("euc-jp")
mode = 'any'
text = "abfgh\xA1\xA1j\xA1\xA1xskhtrvs\naltjhgsdf\xA1\xA1jahtshgf"
# tests
do = [
( 10, [10, 18, 28, 38] ),
( 6, [5, 11, 17, 18, 25, 31, 37, 38] ),
( 100, [18, 38]),
]
class CalcBreaksWordTest(CalcBreaksTest, unittest.TestCase):
mode = 'space'
text = "hello world\nout there. blah"
# tests
do = [
( 10, [5, 11, 22, 27] ),
( 5, [5, 11, 17, 22, 27] ),
( 100, [11, 27] ),
]
class CalcBreaksWordTest2(CalcBreaksTest, unittest.TestCase):
mode = 'space'
text = "A simple set of words, really...."
do = [
( 10, [8, 15, 22, 33]),
( 17, [15, 33]),
( 13, [12, 22, 33]),
]
class CalcBreaksDBWordTest(CalcBreaksTest, unittest.TestCase):
def setUp(self):
urwid.set_encoding("euc-jp")
mode = 'space'
text = "hel\xA1\xA1 world\nout-\xA1\xA1tre blah"
# tests
do = [
( 10, [5, 11, 21, 26] ),
( 5, [5, 11, 16, 21, 26] ),
( 100, [11, 26] ),
]
class CalcBreaksUTF8Test(CalcBreaksTest, unittest.TestCase):
def setUp(self):
urwid.set_encoding("utf-8")
mode = 'space'
text = '\xe6\x9b\xbf\xe6\xb4\xbc\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba'
do = [
(4, [6, 12, 15] ),
(10, [15] ),
(5, [6, 12, 15] ),
]
class CalcBreaksCantDisplayTest(unittest.TestCase):
def test(self):
urwid.set_encoding("euc-jp")
self.assertRaises(text_layout.CanNotDisplayText,
text_layout.default_layout.calculate_text_segments,
B('\xA1\xA1'), 1, 'space' )
urwid.set_encoding("utf-8")
self.assertRaises(text_layout.CanNotDisplayText,
text_layout.default_layout.calculate_text_segments,
B('\xe9\xa2\x96'), 1, 'space' )
class SubsegTest(unittest.TestCase):
def setUp(self):
urwid.set_encoding("euc-jp")
def st(self, seg, text, start, end, exp):
text = B(text)
s = urwid.LayoutSegment(seg)
result = s.subseg( text, start, end )
assert result == exp, "Expected %r, got %r"%(exp,result)
def test1_padding(self):
self.st( (10, None), "", 0, 8, [(8, None)] )
self.st( (10, None), "", 2, 10, [(8, None)] )
self.st( (10, 0), "", 3, 7, [(4, 0)] )
self.st( (10, 0), "", 0, 20, [(10, 0)] )
def test2_text(self):
self.st( (10, 0, B("1234567890")), "", 0, 8, [(8,0,B("12345678"))] )
self.st( (10, 0, B("1234567890")), "", 2, 10, [(8,0,B("34567890"))] )
self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 2, 8,
[(6, 0, B("\xA1\xA156\xA1\xA1"))] )
self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 3, 8,
[(5, 0, B(" 56\xA1\xA1"))] )
self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 2, 7,
[(5, 0, B("\xA1\xA156 "))] )
self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 3, 7,
[(4, 0, B(" 56 "))] )
self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 0, 20,
[(10, 0, B("12\xA1\xA156\xA1\xA190"))] )
def test3_range(self):
t = "1234567890"
self.st( (10, 0, 10), t, 0, 8, [(8, 0, 8)] )
self.st( (10, 0, 10), t, 2, 10, [(8, 2, 10)] )
self.st( (6, 2, 8), t, 1, 6, [(5, 3, 8)] )
self.st( (6, 2, 8), t, 0, 5, [(5, 2, 7)] )
self.st( (6, 2, 8), t, 1, 5, [(4, 3, 7)] )
t = "12\xA1\xA156\xA1\xA190"
self.st( (10, 0, 10), t, 0, 8, [(8, 0, 8)] )
self.st( (10, 0, 10), t, 2, 10, [(8, 2, 10)] )
self.st( (6, 2, 8), t, 1, 6, [(1, 3), (4, 4, 8)] )
self.st( (6, 2, 8), t, 0, 5, [(4, 2, 6), (1, 6)] )
self.st( (6, 2, 8), t, 1, 5, [(1, 3), (2, 4, 6), (1, 6)] )
class CalcTranslateTest(object):
def setUp(self):
urwid.set_encoding("utf-8")
def test1_left(self):
result = urwid.default_layout.layout( self.text,
self.width, 'left', self.mode)
assert result == self.result_left, result
def test2_right(self):
result = urwid.default_layout.layout( self.text,
self.width, 'right', self.mode)
assert result == self.result_right, result
def test3_center(self):
result = urwid.default_layout.layout( self.text,
self.width, 'center', self.mode)
assert result == self.result_center, result
class CalcTranslateCharTest(CalcTranslateTest, unittest.TestCase):
text = "It's out of control!\nYou've got to"
mode = 'any'
width = 15
result_left = [
[(15, 0, 15)],
[(5, 15, 20), (0, 20)],
[(13, 21, 34), (0, 34)]]
result_right = [
[(15, 0, 15)],
[(10, None), (5, 15, 20), (0,20)],
[(2, None), (13, 21, 34), (0,34)]]
result_center = [
[(15, 0, 15)],
[(5, None), (5, 15, 20), (0,20)],
[(1, None), (13, 21, 34), (0,34)]]
class CalcTranslateWordTest(CalcTranslateTest, unittest.TestCase):
text = "It's out of control!\nYou've got to"
mode = 'space'
width = 14
result_left = [
[(11, 0, 11), (0, 11)],
[(8, 12, 20), (0, 20)],
[(13, 21, 34), (0, 34)]]
result_right = [
[(3, None), (11, 0, 11), (0, 11)],
[(6, None), (8, 12, 20), (0, 20)],
[(1, None), (13, 21, 34), (0, 34)]]
result_center = [
[(2, None), (11, 0, 11), (0, 11)],
[(3, None), (8, 12, 20), (0, 20)],
[(1, None), (13, 21, 34), (0, 34)]]
class CalcTranslateWordTest2(CalcTranslateTest, unittest.TestCase):
text = "It's out of control!\nYou've got to "
mode = 'space'
width = 14
result_left = [
[(11, 0, 11), (0, 11)],
[(8, 12, 20), (0, 20)],
[(14, 21, 35), (0, 35)]]
result_right = [
[(3, None), (11, 0, 11), (0, 11)],
[(6, None), (8, 12, 20), (0, 20)],
[(14, 21, 35), (0, 35)]]
result_center = [
[(2, None), (11, 0, 11), (0, 11)],
[(3, None), (8, 12, 20), (0, 20)],
[(14, 21, 35), (0, 35)]]
class CalcTranslateWordTest3(CalcTranslateTest, unittest.TestCase):
def setUp(self):
urwid.set_encoding('utf-8')
text = B('\xe6\x9b\xbf\xe6\xb4\xbc\n\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba')
width = 10
mode = 'space'
result_left = [
[(4, 0, 6), (0, 6)],
[(6, 7, 16), (0, 16)]]
result_right = [
[(6, None), (4, 0, 6), (0, 6)],
[(4, None), (6, 7, 16), (0, 16)]]
result_center = [
[(3, None), (4, 0, 6), (0, 6)],
[(2, None), (6, 7, 16), (0, 16)]]
class CalcTranslateWordTest4(CalcTranslateTest, unittest.TestCase):
text = ' Die Gedank'
width = 3
mode = 'space'
result_left = [
[(0, 0)],
[(3, 1, 4), (0, 4)],
[(3, 5, 8)],
[(3, 8, 11), (0, 11)]]
result_right = [
[(3, None), (0, 0)],
[(3, 1, 4), (0, 4)],
[(3, 5, 8)],
[(3, 8, 11), (0, 11)]]
result_center = [
[(2, None), (0, 0)],
[(3, 1, 4), (0, 4)],
[(3, 5, 8)],
[(3, 8, 11), (0, 11)]]
class CalcTranslateWordTest5(CalcTranslateTest, unittest.TestCase):
text = ' Word.'
width = 3
mode = 'space'
result_left = [[(3, 0, 3)], [(3, 3, 6), (0, 6)]]
result_right = [[(3, 0, 3)], [(3, 3, 6), (0, 6)]]
result_center = [[(3, 0, 3)], [(3, 3, 6), (0, 6)]]
class CalcTranslateClipTest(CalcTranslateTest, unittest.TestCase):
text = "It's out of control!\nYou've got to\n\nturn it off!!!"
mode = 'clip'
width = 14
result_left = [
[(20, 0, 20), (0, 20)],
[(13, 21, 34), (0, 34)],
[(0, 35)],
[(14, 36, 50), (0, 50)]]
result_right = [
[(-6, None), (20, 0, 20), (0, 20)],
[(1, None), (13, 21, 34), (0, 34)],
[(14, None), (0, 35)],
[(14, 36, 50), (0, 50)]]
result_center = [
[(-3, None), (20, 0, 20), (0, 20)],
[(1, None), (13, 21, 34), (0, 34)],
[(7, None), (0, 35)],
[(14, 36, 50), (0, 50)]]
class CalcTranslateCantDisplayTest(CalcTranslateTest, unittest.TestCase):
text = B('Hello\xe9\xa2\x96')
mode = 'space'
width = 1
result_left = [[]]
result_right = [[]]
result_center = [[]]
class CalcPosTest(unittest.TestCase):
def setUp(self):
self.text = "A" * 27
self.trans = [
[(2,None),(7,0,7),(0,7)],
[(13,8,21),(0,21)],
[(3,None),(5,22,27),(0,27)]]
self.mytests = [(1,0, 0), (2,0, 0), (11,0, 7),
(-3,1, 8), (-2,1, 8), (1,1, 9), (31,1, 21),
(1,2, 22), (11,2, 27) ]
def tests(self):
for x,y, expected in self.mytests:
got = text_layout.calc_pos( self.text, self.trans, x, y )
assert got == expected, "%r got:%r expected:%r" % ((x, y), got,
expected)
class Pos2CoordsTest(unittest.TestCase):
pos_list = [5, 9, 20, 26]
text = "1234567890" * 3
mytests = [
( [[(15,0,15)], [(15,15,30),(0,30)]],
[(5,0),(9,0),(5,1),(11,1)] ),
( [[(9,0,9)], [(12,9,21)], [(9,21,30),(0,30)]],
[(5,0),(0,1),(11,1),(5,2)] ),
( [[(2,None), (15,0,15)], [(2,None), (15,15,30),(0,30)]],
[(7,0),(11,0),(7,1),(13,1)] ),
( [[(3, 6, 9),(0,9)], [(5, 20, 25),(0,25)]],
[(0,0),(3,0),(0,1),(5,1)] ),
( [[(10, 0, 10),(0,10)]],
[(5,0),(9,0),(10,0),(10,0)] ),
]
def test(self):
for t, answer in self.mytests:
for pos,a in zip(self.pos_list,answer) :
r = text_layout.calc_coords( self.text, t, pos)
assert r==a, "%r got: %r expected: %r"%(t,r,a)

View File

@@ -1,178 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
import urwid
from urwid import util
from urwid.compat import B
class CalcWidthTest(unittest.TestCase):
def wtest(self, desc, s, exp):
s = B(s)
result = util.calc_width( s, 0, len(s))
assert result==exp, "%s got:%r expected:%r" % (desc, result, exp)
def test1(self):
util.set_encoding("utf-8")
self.wtest("narrow", "hello", 5)
self.wtest("wide char", '\xe6\x9b\xbf', 2)
self.wtest("invalid", '\xe6', 1)
self.wtest("zero width", '\xcc\x80', 0)
self.wtest("mixed", 'hello\xe6\x9b\xbf\xe6\x9b\xbf', 9)
def test2(self):
util.set_encoding("euc-jp")
self.wtest("narrow", "hello", 5)
self.wtest("wide", "\xA1\xA1\xA1\xA1", 4)
self.wtest("invalid", "\xA1", 1)
class ConvertDecSpecialTest(unittest.TestCase):
def ctest(self, desc, s, exp, expcs):
exp = B(exp)
util.set_encoding('ascii')
c = urwid.Text(s).render((5,))
result = c._text[0]
assert result==exp, "%s got:%r expected:%r" % (desc, result, exp)
resultcs = c._cs[0]
assert resultcs==expcs, "%s got:%r expected:%r" % (desc,
resultcs, expcs)
def test1(self):
self.ctest("no conversion", u"hello", "hello", [(None,5)])
self.ctest("only special", u"£££££", "}}}}}", [("0",5)])
self.ctest("mix left", u"££abc", "}}abc", [("0",2),(None,3)])
self.ctest("mix right", u"abc££", "abc}}", [(None,3),("0",2)])
self.ctest("mix inner", u"a££bc", "a}}bc",
[(None,1),("0",2),(None,2)] )
self.ctest("mix well", u"£a£b£", "}a}b}",
[("0",1),(None,1),("0",1),(None,1),("0",1)] )
class WithinDoubleByteTest(unittest.TestCase):
def setUp(self):
urwid.set_encoding("euc-jp")
def wtest(self, s, ls, pos, expected, desc):
result = util.within_double_byte(B(s), ls, pos)
assert result==expected, "%s got:%r expected: %r" % (desc,
result, expected)
def test1(self):
self.wtest("mnopqr",0,2,0,'simple no high bytes')
self.wtest("mn\xA1\xA1qr",0,2,1,'simple 1st half')
self.wtest("mn\xA1\xA1qr",0,3,2,'simple 2nd half')
self.wtest("m\xA1\xA1\xA1\xA1r",0,3,1,'subsequent 1st half')
self.wtest("m\xA1\xA1\xA1\xA1r",0,4,2,'subsequent 2nd half')
self.wtest("mn\xA1@qr",0,3,2,'simple 2nd half lo')
self.wtest("mn\xA1\xA1@r",0,4,0,'subsequent not 2nd half lo')
self.wtest("m\xA1\xA1\xA1@r",0,4,2,'subsequent 2nd half lo')
def test2(self):
self.wtest("\xA1\xA1qr",0,0,1,'begin 1st half')
self.wtest("\xA1\xA1qr",0,1,2,'begin 2nd half')
self.wtest("\xA1@qr",0,1,2,'begin 2nd half lo')
self.wtest("\xA1\xA1\xA1\xA1r",0,2,1,'begin subs. 1st half')
self.wtest("\xA1\xA1\xA1\xA1r",0,3,2,'begin subs. 2nd half')
self.wtest("\xA1\xA1\xA1@r",0,3,2,'begin subs. 2nd half lo')
self.wtest("\xA1@\xA1@r",0,3,2,'begin subs. 2nd half lo lo')
self.wtest("@\xA1\xA1@r",0,3,0,'begin subs. not 2nd half lo')
def test3(self):
self.wtest("abc \xA1\xA1qr",4,4,1,'newline 1st half')
self.wtest("abc \xA1\xA1qr",4,5,2,'newline 2nd half')
self.wtest("abc \xA1@qr",4,5,2,'newline 2nd half lo')
self.wtest("abc \xA1\xA1\xA1\xA1r",4,6,1,'newl subs. 1st half')
self.wtest("abc \xA1\xA1\xA1\xA1r",4,7,2,'newl subs. 2nd half')
self.wtest("abc \xA1\xA1\xA1@r",4,7,2,'newl subs. 2nd half lo')
self.wtest("abc \xA1@\xA1@r",4,7,2,'newl subs. 2nd half lo lo')
self.wtest("abc @\xA1\xA1@r",4,7,0,'newl subs. not 2nd half lo')
class CalcTextPosTest(unittest.TestCase):
def ctptest(self, text, tests):
text = B(text)
for s,e,p, expected in tests:
got = util.calc_text_pos( text, s, e, p )
assert got == expected, "%r got:%r expected:%r" % ((s,e,p),
got, expected)
def test1(self):
text = "hello world out there"
tests = [
(0,21,0, (0,0)),
(0,21,5, (5,5)),
(0,21,21, (21,21)),
(0,21,50, (21,21)),
(2,15,50, (15,13)),
(6,21,0, (6,0)),
(6,21,3, (9,3)),
]
self.ctptest(text, tests)
def test2_wide(self):
util.set_encoding("euc-jp")
text = "hel\xA1\xA1 world out there"
tests = [
(0,21,0, (0,0)),
(0,21,4, (3,3)),
(2,21,2, (3,1)),
(2,21,3, (5,3)),
(6,21,0, (6,0)),
]
self.ctptest(text, tests)
def test3_utf8(self):
util.set_encoding("utf-8")
text = "hel\xc4\x83 world \xe2\x81\x81 there"
tests = [
(0,21,0, (0,0)),
(0,21,4, (5,4)),
(2,21,1, (3,1)),
(2,21,2, (5,2)),
(2,21,3, (6,3)),
(6,21,7, (15,7)),
(6,21,8, (16,8)),
]
self.ctptest(text, tests)
def test4_utf8(self):
util.set_encoding("utf-8")
text = "he\xcc\x80llo \xe6\x9b\xbf world"
tests = [
(0,15,0, (0,0)),
(0,15,1, (1,1)),
(0,15,2, (4,2)),
(0,15,4, (6,4)),
(8,15,0, (8,0)),
(8,15,1, (8,0)),
(8,15,2, (11,2)),
(8,15,5, (14,5)),
]
self.ctptest(text, tests)
class TagMarkupTest(unittest.TestCase):
mytests = [
("simple one", "simple one", []),
(('blue',"john"), "john", [('blue',4)]),
(["a ","litt","le list"], "a little list", []),
(["mix",('high',[" it ",('ital',"up a")])," little"],
"mix it up a little",
[(None,3),('high',4),('ital',4)]),
([u"££", u"x££"], u"££x££", []),
([B("\xc2\x80"), B("\xc2\x80")], B("\xc2\x80\xc2\x80"), []),
]
def test(self):
for input, text, attr in self.mytests:
restext,resattr = urwid.decompose_tagmarkup( input )
assert restext == text, "got: %r expected: %r" % (restext, text)
assert resattr == attr, "got: %r expected: %r" % (resattr, attr)
def test_bad_tuple(self):
self.assertRaises(urwid.TagMarkupException, lambda:
urwid.decompose_tagmarkup((1,2,3)))
def test_bad_type(self):
self.assertRaises(urwid.TagMarkupException, lambda:
urwid.decompose_tagmarkup(5))

View File

@@ -1,334 +0,0 @@
# Urwid terminal emulation widget unit tests
# Copyright (C) 2010 aszlig
# Copyright (C) 2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
import os
import sys
import unittest
from itertools import dropwhile
from urwid import vterm
from urwid import signals
from urwid.compat import B
class DummyCommand(object):
QUITSTRING = B('|||quit|||')
def __init__(self):
self.reader, self.writer = os.pipe()
def __call__(self):
# reset
stdout = getattr(sys.stdout, 'buffer', sys.stdout)
stdout.write(B('\x1bc'))
while True:
data = os.read(self.reader, 1024)
if self.QUITSTRING == data:
break
stdout.write(data)
stdout.flush()
def write(self, data):
os.write(self.writer, data)
def quit(self):
self.write(self.QUITSTRING)
class TermTest(unittest.TestCase):
def setUp(self):
self.command = DummyCommand()
self.term = vterm.Terminal(self.command)
self.resize(80, 24)
def tearDown(self):
self.command.quit()
def connect_signal(self, signal):
self._sig_response = None
def _set_signal_response(widget, *args, **kwargs):
self._sig_response = (args, kwargs)
self._set_signal_response = _set_signal_response
signals.connect_signal(self.term, signal, self._set_signal_response)
def expect_signal(self, *args, **kwargs):
self.assertEqual(self._sig_response, (args, kwargs))
def disconnect_signal(self, signal):
signals.disconnect_signal(self.term, signal, self._set_signal_response)
def caught_beep(self, obj):
self.beeped = True
def resize(self, width, height, soft=False):
self.termsize = (width, height)
if not soft:
self.term.render(self.termsize, focus=False)
def write(self, data):
data = B(data)
self.command.write(data.replace(B('\e'), B('\x1b')))
def flush(self):
self.write(chr(0x7f))
def read(self, raw=False):
self.term.wait_and_feed()
rendered = self.term.render(self.termsize, focus=False)
if raw:
is_empty = lambda c: c == (None, None, B(' '))
content = list(rendered.content())
lines = [list(dropwhile(is_empty, reversed(line)))
for line in content]
return [list(reversed(line)) for line in lines if len(line)]
else:
content = rendered.text
lines = [line.rstrip() for line in content]
return B('\n').join(lines).rstrip()
def expect(self, what, desc=None, raw=False):
if not isinstance(what, list):
what = B(what)
got = self.read(raw=raw)
if desc is None:
desc = ''
else:
desc += '\n'
desc += 'Expected:\n%r\nGot:\n%r' % (what, got)
self.assertEqual(got, what, desc)
def test_simplestring(self):
self.write('hello world')
self.expect('hello world')
def test_linefeed(self):
self.write('hello\x0aworld')
self.expect('hello\nworld')
def test_linefeed2(self):
self.write('aa\b\b\eDbb')
self.expect('aa\nbb')
def test_carriage_return(self):
self.write('hello\x0dworld')
self.expect('world')
def test_insertlines(self):
self.write('\e[0;0flast\e[0;0f\e[10L\e[0;0ffirst\nsecond\n\e[11D')
self.expect('first\nsecond\n\n\n\n\n\n\n\n\nlast')
def test_deletelines(self):
self.write('1\n2\n3\n4\e[2;1f\e[2M')
self.expect('1\n4')
def test_movement(self):
self.write('\e[10;20H11\e[10;0f\e[20C\e[K')
self.expect('\n' * 9 + ' ' * 19 + '1')
self.write('\e[A\e[B\e[C\e[D\b\e[K')
self.expect('')
self.write('\e[50A2')
self.expect(' ' * 19 + '2')
self.write('\b\e[K\e[50B3')
self.expect('\n' * 23 + ' ' * 19 + '3')
self.write('\b\e[K' + '\eM' * 30 + '\e[100C4')
self.expect(' ' * 79 + '4')
self.write('\e[100D\e[K5')
self.expect('5')
def edgewall(self):
edgewall = '1-\e[1;%(x)df-2\e[%(y)d;1f3-\e[%(y)d;%(x)df-4\x0d'
self.write(edgewall % {'x': self.termsize[0] - 1,
'y': self.termsize[1] - 1})
def test_horizontal_resize(self):
self.resize(80, 24)
self.edgewall()
self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22
+ '3-' + ' ' * 76 + '-4')
self.resize(78, 24, soft=True)
self.flush()
self.expect('1-' + '\n' * 22 + '3-')
self.resize(80, 24, soft=True)
self.flush()
self.expect('1-' + '\n' * 22 + '3-')
def test_vertical_resize(self):
self.resize(80, 24)
self.edgewall()
self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22
+ '3-' + ' ' * 76 + '-4')
for y in xrange(23, 1, -1):
self.resize(80, y, soft=True)
self.write('\e[%df\e[J3-\e[%d;%df-4' % (y, y, 79))
desc = "try to rescale to 80x%d." % y
self.expect('\n' * (y - 2) + '3-' + ' ' * 76 + '-4', desc)
self.resize(80, 24, soft=True)
self.flush()
self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22
+ '3-' + ' ' * 76 + '-4')
def write_movements(self, arg):
fmt = 'XXX\n\e[faaa\e[Bccc\e[Addd\e[Bfff\e[Cbbb\e[A\e[Deee'
self.write(fmt.replace('\e[', '\e['+arg))
def test_defargs(self):
self.write_movements('')
self.expect('aaa ddd eee\n ccc fff bbb')
def test_nullargs(self):
self.write_movements('0')
self.expect('aaa ddd eee\n ccc fff bbb')
def test_erase_line(self):
self.write('1234567890\e[5D\e[K\n1234567890\e[5D\e[1K\naaaaaaaaaaaaaaa\e[2Ka')
self.expect('12345\n 7890\n a')
def test_erase_display(self):
self.write('1234567890\e[5D\e[Ja')
self.expect('12345a')
self.write('98765\e[8D\e[1Jx')
self.expect(' x5a98765')
def test_scrolling_region_simple(self):
self.write('\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
self.expect('aa' + '\n' * 9 + '2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12')
def test_scrolling_region_reverse(self):
self.write('\e[2J\e[1;2r\e[5Baaa\r\eM\eM\eMbbb\nXXX')
self.expect('\n\nbbb\nXXX\n\naaa')
def test_scrolling_region_move(self):
self.write('\e[10;20r\e[2J\e[10Bfoo\rbar\rblah\rmooh\r\e[10Aone\r\eM\eMtwo\r\eM\eMthree\r\eM\eMa')
self.expect('ahree\n\n\n\n\n\n\n\n\n\nmooh')
def test_scrolling_twice(self):
self.write('\e[?6h\e[10;20r\e[2;5rtest')
self.expect('\ntest')
def test_cursor_scrolling_region(self):
self.write('\e[?6h\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
self.expect('\n' * 9 + 'aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12')
def test_relative_region_jump(self):
self.write('\e[21H---\e[10;20r\e[?6h\e[18Htest')
self.expect('\n' * 19 + 'test\n---')
def test_set_multiple_modes(self):
self.write('\e[?6;5htest')
self.expect('test')
self.assertTrue(self.term.term_modes.constrain_scrolling)
self.assertTrue(self.term.term_modes.reverse_video)
self.write('\e[?6;5l')
self.expect('test')
self.assertFalse(self.term.term_modes.constrain_scrolling)
self.assertFalse(self.term.term_modes.reverse_video)
def test_wrap_simple(self):
self.write('\e[?7h\e[1;%dHtt' % self.term.width)
self.expect(' ' * (self.term.width - 1) + 't\nt')
def test_wrap_backspace_tab(self):
self.write('\e[?7h\e[1;%dHt\b\b\t\ta' % self.term.width)
self.expect(' ' * (self.term.width - 1) + 'a')
def test_cursor_visibility(self):
self.write('\e[?25linvisible')
self.expect('invisible')
self.assertEqual(self.term.term.cursor, None)
self.write('\rvisible\e[?25h\e[K')
self.expect('visible')
self.assertNotEqual(self.term.term.cursor, None)
def test_get_utf8_len(self):
length = self.term.term.get_utf8_len(int("11110000", 2))
self.assertEqual(length, 3)
length = self.term.term.get_utf8_len(int("11000000", 2))
self.assertEqual(length, 1)
length = self.term.term.get_utf8_len(int("11111101", 2))
self.assertEqual(length, 5)
def test_encoding_unicode(self):
vterm.util._target_encoding = 'utf-8'
self.write('\e%G\xe2\x80\x94')
self.expect('\xe2\x80\x94')
def test_encoding_unicode_ascii(self):
vterm.util._target_encoding = 'ascii'
self.write('\e%G\xe2\x80\x94')
self.expect('?')
def test_encoding_wrong_unicode(self):
vterm.util._target_encoding = 'utf-8'
self.write('\e%G\xc0\x99')
self.expect('')
def test_encoding_vt100_graphics(self):
vterm.util._target_encoding = 'ascii'
self.write('\e)0\e(0\x0fg\x0eg\e)Bn\e)0g\e)B\e(B\x0fn')
self.expect([[
(None, '0', B('g')), (None, '0', B('g')),
(None, None, B('n')), (None, '0', B('g')),
(None, None, B('n'))
]], raw=True)
def test_ibmpc_mapping(self):
vterm.util._target_encoding = 'ascii'
self.write('\e[11m\x18\e[10m\x18')
self.expect([[(None, 'U', B('\x18'))]], raw=True)
self.write('\ec\e)U\x0e\x18\x0f\e[3h\x18\e[3l\x18')
self.expect([[(None, None, B('\x18'))]], raw=True)
self.write('\ec\e[11m\xdb\x18\e[10m\xdb')
self.expect([[
(None, 'U', B('\xdb')), (None, 'U', B('\x18')),
(None, None, B('\xdb'))
]], raw=True)
def test_set_title(self):
self._the_title = None
def _change_title(widget, title):
self._the_title = title
self.connect_signal('title')
self.write('\e]666parsed right?\e\\te\e]0;test title\007st1')
self.expect('test1')
self.expect_signal(B('test title'))
self.write('\e]3;stupid title\e\\\e[0G\e[2Ktest2')
self.expect('test2')
self.expect_signal(B('stupid title'))
self.disconnect_signal('title')
def test_set_leds(self):
self.connect_signal('leds')
self.write('\e[0qtest1')
self.expect('test1')
self.expect_signal('clear')
self.write('\e[3q\e[H\e[Ktest2')
self.expect('test2')
self.expect_signal('caps_lock')
self.disconnect_signal('leds')

View File

@@ -1,153 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from urwid.compat import B
import urwid
class TextTest(unittest.TestCase):
def setUp(self):
self.t = urwid.Text("I walk the\ncity in the night")
def test1_wrap(self):
expected = [B(t) for t in "I walk the","city in ","the night "]
got = self.t.render((10,))._text
assert got == expected, "got: %r expected: %r" % (got, expected)
def test2_left(self):
self.t.set_align_mode('left')
expected = [B(t) for t in "I walk the ","city in the night "]
got = self.t.render((18,))._text
assert got == expected, "got: %r expected: %r" % (got, expected)
def test3_right(self):
self.t.set_align_mode('right')
expected = [B(t) for t in " I walk the"," city in the night"]
got = self.t.render((18,))._text
assert got == expected, "got: %r expected: %r" % (got, expected)
def test4_center(self):
self.t.set_align_mode('center')
expected = [B(t) for t in " I walk the "," city in the night"]
got = self.t.render((18,))._text
assert got == expected, "got: %r expected: %r" % (got, expected)
def test5_encode_error(self):
urwid.set_encoding("ascii")
expected = [B("? ")]
got = urwid.Text(u'û').render((3,))._text
assert got == expected, "got: %r expected: %r" % (got, expected)
class EditTest(unittest.TestCase):
def setUp(self):
self.t1 = urwid.Edit(B(""),"blah blah")
self.t2 = urwid.Edit(B("stuff:"), "blah blah")
self.t3 = urwid.Edit(B("junk:\n"),"blah blah\n\nbloo",1)
self.t4 = urwid.Edit(u"better:")
def ktest(self, e, key, expected, pos, desc):
got= e.keypress((12,),key)
assert got == expected, "%s. got: %r expected:%r" % (desc, got,
expected)
assert e.edit_pos == pos, "%s. pos: %r expected pos: " % (
desc, e.edit_pos, pos)
def test1_left(self):
self.t1.set_edit_pos(0)
self.ktest(self.t1,'left','left',0,"left at left edge")
self.ktest(self.t2,'left',None,8,"left within text")
self.t3.set_edit_pos(10)
self.ktest(self.t3,'left',None,9,"left after newline")
def test2_right(self):
self.ktest(self.t1,'right','right',9,"right at right edge")
self.t2.set_edit_pos(8)
self.ktest(self.t2,'right',None,9,"right at right edge-1")
self.t3.set_edit_pos(0)
self.t3.keypress((12,),'right')
assert self.t3.get_pref_col((12,)) == 1
def test3_up(self):
self.ktest(self.t1,'up','up',9,"up at top")
self.t2.set_edit_pos(2)
self.t2.keypress((12,),"left")
assert self.t2.get_pref_col((12,)) == 7
self.ktest(self.t2,'up','up',1,"up at top again")
assert self.t2.get_pref_col((12,)) == 7
self.t3.set_edit_pos(10)
self.ktest(self.t3,'up',None,0,"up at top+1")
def test4_down(self):
self.ktest(self.t1,'down','down',9,"down single line")
self.t3.set_edit_pos(5)
self.ktest(self.t3,'down',None,10,"down line 1 to 2")
self.ktest(self.t3,'down',None,15,"down line 2 to 3")
self.ktest(self.t3,'down','down',15,"down at bottom")
def test_utf8_input(self):
urwid.set_encoding("utf-8")
self.t1.set_edit_text('')
self.t1.keypress((12,), u'û')
self.assertEqual(self.t1.edit_text, u'û'.encode('utf-8'))
self.t4.keypress((12,), u'û')
self.assertEqual(self.t4.edit_text, u'û')
class EditRenderTest(unittest.TestCase):
def rtest(self, w, expected_text, expected_cursor):
expected_text = [B(t) for t in expected_text]
get_cursor = w.get_cursor_coords((4,))
assert get_cursor == expected_cursor, "got: %r expected: %r" % (
get_cursor, expected_cursor)
r = w.render((4,), focus = 1)
text = [t for a, cs, t in [ln[0] for ln in r.content()]]
assert text == expected_text, "got: %r expected: %r" % (text,
expected_text)
assert r.cursor == expected_cursor, "got: %r expected: %r" % (
r.cursor, expected_cursor)
def test1_SpaceWrap(self):
w = urwid.Edit("","blah blah")
w.set_edit_pos(0)
self.rtest(w,["blah","blah"],(0,0))
w.set_edit_pos(4)
self.rtest(w,["lah ","blah"],(3,0))
w.set_edit_pos(5)
self.rtest(w,["blah","blah"],(0,1))
w.set_edit_pos(9)
self.rtest(w,["blah","lah "],(3,1))
def test2_ClipWrap(self):
w = urwid.Edit("","blah\nblargh",1)
w.set_wrap_mode('clip')
w.set_edit_pos(0)
self.rtest(w,["blah","blar"],(0,0))
w.set_edit_pos(10)
self.rtest(w,["blah","argh"],(3,1))
w.set_align_mode('right')
w.set_edit_pos(6)
self.rtest(w,["blah","larg"],(0,1))
def test3_AnyWrap(self):
w = urwid.Edit("","blah blah")
w.set_wrap_mode('any')
self.rtest(w,["blah"," bla","h "],(1,2))
def test4_CursorNudge(self):
w = urwid.Edit("","hi",align='right')
w.keypress((4,),'end')
self.rtest(w,[" hi "],(3,0))
w.keypress((4,),'left')
self.rtest(w,[" hi"],(3,0))

View File

@@ -1,8 +0,0 @@
import urwid
class SelectableText(urwid.Text):
def selectable(self):
return 1
def keypress(self, size, key):
return key

View File

@@ -1,506 +0,0 @@
#!/usr/bin/python
#
# Urwid Text Layout classes
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from urwid.util import calc_width, calc_text_pos, calc_trim_text, is_wide_char, \
move_prev_char, move_next_char
from urwid.compat import bytes, PYTHON3, B
class TextLayout:
def supports_align_mode(self, align):
"""Return True if align is a supported align mode."""
return True
def supports_wrap_mode(self, wrap):
"""Return True if wrap is a supported wrap mode."""
return True
def layout(self, text, width, align, wrap ):
"""
Return a layout structure for text.
:param text: string in current encoding or unicode string
:param width: number of screen columns available
:param align: align mode for text
:param wrap: wrap mode for text
Layout structure is a list of line layouts, one per output line.
Line layouts are lists than may contain the following tuples:
* (column width of text segment, start offset, end offset)
* (number of space characters to insert, offset or None)
* (column width of insert text, offset, "insert text")
The offset in the last two tuples is used to determine the
attribute used for the inserted spaces or text respectively.
The attribute used will be the same as the attribute at that
text offset. If the offset is None when inserting spaces
then no attribute will be used.
"""
raise NotImplementedError("This function must be overridden by a real"
" text layout class. (see StandardTextLayout)")
class CanNotDisplayText(Exception):
pass
class StandardTextLayout(TextLayout):
def __init__(self):#, tab_stops=(), tab_stop_every=8):
pass
#"""
#tab_stops -- list of screen column indexes for tab stops
#tab_stop_every -- repeated interval for following tab stops
#"""
#assert tab_stop_every is None or type(tab_stop_every)==int
#if not tab_stops and tab_stop_every:
# self.tab_stops = (tab_stop_every,)
#self.tab_stops = tab_stops
#self.tab_stop_every = tab_stop_every
def supports_align_mode(self, align):
"""Return True if align is 'left', 'center' or 'right'."""
return align in ('left', 'center', 'right')
def supports_wrap_mode(self, wrap):
"""Return True if wrap is 'any', 'space' or 'clip'."""
return wrap in ('any', 'space', 'clip')
def layout(self, text, width, align, wrap ):
"""Return a layout structure for text."""
try:
segs = self.calculate_text_segments( text, width, wrap )
return self.align_layout( text, width, segs, wrap, align )
except CanNotDisplayText:
return [[]]
def pack(self, maxcol, layout):
"""
Return a minimal maxcol value that would result in the same
number of lines for layout. layout must be a layout structure
returned by self.layout().
"""
maxwidth = 0
assert layout, "huh? empty layout?: "+repr(layout)
for l in layout:
lw = line_width(l)
if lw >= maxcol:
return maxcol
maxwidth = max(maxwidth, lw)
return maxwidth
def align_layout( self, text, width, segs, wrap, align ):
"""Convert the layout segs to an aligned layout."""
out = []
for l in segs:
sc = line_width(l)
if sc == width or align=='left':
out.append(l)
continue
if align == 'right':
out.append([(width-sc, None)] + l)
continue
assert align == 'center'
out.append([((width-sc+1) // 2, None)] + l)
return out
def calculate_text_segments(self, text, width, wrap):
"""
Calculate the segments of text to display given width screen
columns to display them.
text - unicode text or byte string to display
width - number of available screen columns
wrap - wrapping mode used
Returns a layout structure without alignment applied.
"""
nl, nl_o, sp_o = "\n", "\n", " "
if PYTHON3 and isinstance(text, bytes):
nl = B(nl) # can only find bytes in python3 bytestrings
nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value
sp_o = ord(sp_o)
b = []
p = 0
if wrap == 'clip':
# no wrapping to calculate, so it's easy.
while p<=len(text):
n_cr = text.find(nl, p)
if n_cr == -1:
n_cr = len(text)
sc = calc_width(text, p, n_cr)
l = [(0,n_cr)]
if p!=n_cr:
l = [(sc, p, n_cr)] + l
b.append(l)
p = n_cr+1
return b
while p<=len(text):
# look for next eligible line break
n_cr = text.find(nl, p)
if n_cr == -1:
n_cr = len(text)
sc = calc_width(text, p, n_cr)
if sc == 0:
# removed character hint
b.append([(0,n_cr)])
p = n_cr+1
continue
if sc <= width:
# this segment fits
b.append([(sc,p,n_cr),
# removed character hint
(0,n_cr)])
p = n_cr+1
continue
pos, sc = calc_text_pos( text, p, n_cr, width )
if pos == p: # pathological width=1 double-byte case
raise CanNotDisplayText(
"Wide character will not fit in 1-column width")
if wrap == 'any':
b.append([(sc,p,pos)])
p = pos
continue
assert wrap == 'space'
if text[pos] == sp_o:
# perfect space wrap
b.append([(sc,p,pos),
# removed character hint
(0,pos)])
p = pos+1
continue
if is_wide_char(text, pos):
# perfect next wide
b.append([(sc,p,pos)])
p = pos
continue
prev = pos
while prev > p:
prev = move_prev_char(text, p, prev)
if text[prev] == sp_o:
sc = calc_width(text,p,prev)
l = [(0,prev)]
if p!=prev:
l = [(sc,p,prev)] + l
b.append(l)
p = prev+1
break
if is_wide_char(text,prev):
# wrap after wide char
next = move_next_char(text, prev, pos)
sc = calc_width(text,p,next)
b.append([(sc,p,next)])
p = next
break
else:
# unwrap previous line space if possible to
# fit more text (we're breaking a word anyway)
if b and (len(b[-1]) == 2 or ( len(b[-1])==1
and len(b[-1][0])==2 )):
# look for removed space above
if len(b[-1]) == 1:
[(h_sc, h_off)] = b[-1]
p_sc = 0
p_off = p_end = h_off
else:
[(p_sc, p_off, p_end),
(h_sc, h_off)] = b[-1]
if (p_sc < width and h_sc==0 and
text[h_off] == sp_o):
# combine with previous line
del b[-1]
p = p_off
pos, sc = calc_text_pos(
text, p, n_cr, width )
b.append([(sc,p,pos)])
# check for trailing " " or "\n"
p = pos
if p < len(text) and (
text[p] in (sp_o, nl_o)):
# removed character hint
b[-1].append((0,p))
p += 1
continue
# force any char wrap
b.append([(sc,p,pos)])
p = pos
return b
######################################
# default layout object to use
default_layout = StandardTextLayout()
######################################
class LayoutSegment:
def __init__(self, seg):
"""Create object from line layout segment structure"""
assert type(seg) == tuple, repr(seg)
assert len(seg) in (2,3), repr(seg)
self.sc, self.offs = seg[:2]
assert type(self.sc) == int, repr(self.sc)
if len(seg)==3:
assert type(self.offs) == int, repr(self.offs)
assert self.sc > 0, repr(seg)
t = seg[2]
if type(t) == bytes:
self.text = t
self.end = None
else:
assert type(t) == int, repr(t)
self.text = None
self.end = t
else:
assert len(seg) == 2, repr(seg)
if self.offs is not None:
assert self.sc >= 0, repr(seg)
assert type(self.offs)==int
self.text = self.end = None
def subseg(self, text, start, end):
"""
Return a "sub-segment" list containing segment structures
that make up a portion of this segment.
A list is returned to handle cases where wide characters
need to be replaced with a space character at either edge
so two or three segments will be returned.
"""
if start < 0: start = 0
if end > self.sc: end = self.sc
if start >= end:
return [] # completely gone
if self.text:
# use text stored in segment (self.text)
spos, epos, pad_left, pad_right = calc_trim_text(
self.text, 0, len(self.text), start, end )
return [ (end-start, self.offs, bytes().ljust(pad_left) +
self.text[spos:epos] + bytes().ljust(pad_right)) ]
elif self.end:
# use text passed as parameter (text)
spos, epos, pad_left, pad_right = calc_trim_text(
text, self.offs, self.end, start, end )
l = []
if pad_left:
l.append((1,spos-1))
l.append((end-start-pad_left-pad_right, spos, epos))
if pad_right:
l.append((1,epos))
return l
else:
# simple padding adjustment
return [(end-start,self.offs)]
def line_width( segs ):
"""
Return the screen column width of one line of a text layout structure.
This function ignores any existing shift applied to the line,
represented by an (amount, None) tuple at the start of the line.
"""
sc = 0
seglist = segs
if segs and len(segs[0])==2 and segs[0][1]==None:
seglist = segs[1:]
for s in seglist:
sc += s[0]
return sc
def shift_line( segs, amount ):
"""
Return a shifted line from a layout structure to the left or right.
segs -- line of a layout structure
amount -- screen columns to shift right (+ve) or left (-ve)
"""
assert type(amount)==int, repr(amount)
if segs and len(segs[0])==2 and segs[0][1]==None:
# existing shift
amount += segs[0][0]
if amount:
return [(amount,None)]+segs[1:]
return segs[1:]
if amount:
return [(amount,None)]+segs
return segs
def trim_line( segs, text, start, end ):
"""
Return a trimmed line of a text layout structure.
text -- text to which this layout structure applies
start -- starting screen column
end -- ending screen column
"""
l = []
x = 0
for seg in segs:
sc = seg[0]
if start or sc < 0:
if start >= sc:
start -= sc
x += sc
continue
s = LayoutSegment(seg)
if x+sc >= end:
# can all be done at once
return s.subseg( text, start, end-x )
l += s.subseg( text, start, sc )
start = 0
x += sc
continue
if x >= end:
break
if x+sc > end:
s = LayoutSegment(seg)
l += s.subseg( text, 0, end-x )
break
l.append( seg )
return l
def calc_line_pos( text, line_layout, pref_col ):
"""
Calculate the closest linear position to pref_col given a
line layout structure. Returns None if no position found.
"""
closest_sc = None
closest_pos = None
current_sc = 0
if pref_col == 'left':
for seg in line_layout:
s = LayoutSegment(seg)
if s.offs is not None:
return s.offs
return
elif pref_col == 'right':
for seg in line_layout:
s = LayoutSegment(seg)
if s.offs is not None:
closest_pos = s
s = closest_pos
if s is None:
return
if s.end is None:
return s.offs
return calc_text_pos( text, s.offs, s.end, s.sc-1)[0]
for seg in line_layout:
s = LayoutSegment(seg)
if s.offs is not None:
if s.end is not None:
if (current_sc <= pref_col and
pref_col < current_sc + s.sc):
# exact match within this segment
return calc_text_pos( text,
s.offs, s.end,
pref_col - current_sc )[0]
elif current_sc <= pref_col:
closest_sc = current_sc + s.sc - 1
closest_pos = s
if closest_sc is None or ( abs(pref_col-current_sc)
< abs(pref_col-closest_sc) ):
# this screen column is closer
closest_sc = current_sc
closest_pos = s.offs
if current_sc > closest_sc:
# we're moving past
break
current_sc += s.sc
if closest_pos is None or type(closest_pos) == int:
return closest_pos
# return the last positions in the segment "closest_pos"
s = closest_pos
return calc_text_pos( text, s.offs, s.end, s.sc-1)[0]
def calc_pos( text, layout, pref_col, row ):
"""
Calculate the closest linear position to pref_col and row given a
layout structure.
"""
if row < 0 or row >= len(layout):
raise Exception("calculate_pos: out of layout row range")
pos = calc_line_pos( text, layout[row], pref_col )
if pos is not None:
return pos
rows_above = range(row-1,-1,-1)
rows_below = range(row+1,len(layout))
while rows_above and rows_below:
if rows_above:
r = rows_above.pop(0)
pos = calc_line_pos(text, layout[r], pref_col)
if pos is not None: return pos
if rows_below:
r = rows_below.pop(0)
pos = calc_line_pos(text, layout[r], pref_col)
if pos is not None: return pos
return 0
def calc_coords( text, layout, pos, clamp=1 ):
"""
Calculate the coordinates closest to position pos in text with layout.
text -- raw string or unicode string
layout -- layout structure applied to text
pos -- integer position into text
clamp -- ignored right now
"""
closest = None
y = 0
for line_layout in layout:
x = 0
for seg in line_layout:
s = LayoutSegment(seg)
if s.offs is None:
x += s.sc
continue
if s.offs == pos:
return x,y
if s.end is not None and s.offs<=pos and s.end>pos:
x += calc_width( text, s.offs, pos )
return x,y
distance = abs(s.offs - pos)
if s.end is not None and s.end<pos:
distance = pos - (s.end-1)
if closest is None or distance < closest[0]:
closest = distance, (x,y)
x += s.sc
y += 1
if closest:
return closest[1]
return 0,0

View File

@@ -1,486 +0,0 @@
#!/usr/bin/python
#
# Generic TreeWidget/TreeWalker class
# Copyright (c) 2010 Rob Lanphier
# Copyright (C) 2004-2010 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
"""
Urwid tree view
Features:
- custom selectable widgets for trees
- custom list walker for displaying widgets in a tree fashion
"""
import urwid
from urwid.wimp import SelectableIcon
class TreeWidgetError(RuntimeError):
pass
class TreeWidget(urwid.WidgetWrap):
"""A widget representing something in a nested tree display."""
indent_cols = 3
unexpanded_icon = SelectableIcon('+', 0)
expanded_icon = SelectableIcon('-', 0)
def __init__(self, node):
self._node = node
self._innerwidget = None
self.is_leaf = not hasattr(node, 'get_first_child')
self.expanded = True
widget = self.get_indented_widget()
self.__super.__init__(widget)
def selectable(self):
"""
Allow selection of non-leaf nodes so children may be (un)expanded
"""
return not self.is_leaf
def get_indented_widget(self):
widget = self.get_inner_widget()
if not self.is_leaf:
widget = urwid.Columns([('fixed', 1,
[self.unexpanded_icon, self.expanded_icon][self.expanded]),
widget], dividechars=1)
indent_cols = self.get_indent_cols()
return urwid.Padding(widget,
width=('relative', 100), left=indent_cols)
def update_expanded_icon(self):
"""Update display widget text for parent widgets"""
# icon is first element in columns indented widget
self._w.base_widget.widget_list[0] = [
self.unexpanded_icon, self.expanded_icon][self.expanded]
def get_indent_cols(self):
return self.indent_cols * self.get_node().get_depth()
def get_inner_widget(self):
if self._innerwidget is None:
self._innerwidget = self.load_inner_widget()
return self._innerwidget
def load_inner_widget(self):
return urwid.Text(self.get_display_text())
def get_node(self):
return self._node
def get_display_text(self):
return (self.get_node().get_key() + ": " +
str(self.get_node().get_value()))
def next_inorder(self):
"""Return the next TreeWidget depth first from this one."""
# first check if there's a child widget
firstchild = self.first_child()
if firstchild is not None:
return firstchild
# now we need to hunt for the next sibling
thisnode = self.get_node()
nextnode = thisnode.next_sibling()
depth = thisnode.get_depth()
while nextnode is None and depth > 0:
# keep going up the tree until we find an ancestor next sibling
thisnode = thisnode.get_parent()
nextnode = thisnode.next_sibling()
depth -= 1
assert depth == thisnode.get_depth()
if nextnode is None:
# we're at the end of the tree
return None
else:
return nextnode.get_widget()
def prev_inorder(self):
"""Return the previous TreeWidget depth first from this one."""
thisnode = self._node
prevnode = thisnode.prev_sibling()
if prevnode is not None:
# we need to find the last child of the previous widget if its
# expanded
prevwidget = prevnode.get_widget()
lastchild = prevwidget.last_child()
if lastchild is None:
return prevwidget
else:
return lastchild
else:
# need to hunt for the parent
depth = thisnode.get_depth()
if prevnode is None and depth == 0:
return None
elif prevnode is None:
prevnode = thisnode.get_parent()
return prevnode.get_widget()
def keypress(self, size, key):
"""Handle expand & collapse requests (non-leaf nodes)"""
if self.is_leaf:
return key
if key in ("+", "right"):
self.expanded = True
self.update_expanded_icon()
elif key == "-":
self.expanded = False
self.update_expanded_icon()
elif self._w.selectable():
return self.__super.keypress(size, key)
else:
return key
def mouse_event(self, size, event, button, col, row, focus):
if self.is_leaf or event != 'mouse press' or button!=1:
return False
if row == 0 and col == self.get_indent_cols():
self.expanded = not self.expanded
self.update_expanded_icon()
return True
return False
def first_child(self):
"""Return first child if expanded."""
if self.is_leaf or not self.expanded:
return None
else:
if self._node.has_children():
firstnode = self._node.get_first_child()
return firstnode.get_widget()
else:
return None
def last_child(self):
"""Return last child if expanded."""
if self.is_leaf or not self.expanded:
return None
else:
if self._node.has_children():
lastchild = self._node.get_last_child().get_widget()
else:
return None
# recursively search down for the last descendant
lastdescendant = lastchild.last_child()
if lastdescendant is None:
return lastchild
else:
return lastdescendant
class TreeNode(object):
"""
Store tree contents and cache TreeWidget objects.
A TreeNode consists of the following elements:
* key: accessor token for parent nodes
* value: subclass-specific data
* parent: a TreeNode which contains a pointer back to this object
* widget: The widget used to render the object
"""
def __init__(self, value, parent=None, key=None, depth=None):
self._key = key
self._parent = parent
self._value = value
self._depth = depth
self._widget = None
def get_widget(self, reload=False):
""" Return the widget for this node."""
if self._widget is None or reload == True:
self._widget = self.load_widget()
return self._widget
def load_widget(self):
return TreeWidget(self)
def get_depth(self):
if self._depth is None and self._parent is None:
self._depth = 0
elif self._depth is None:
self._depth = self._parent.get_depth() + 1
return self._depth
def get_index(self):
if self.get_depth() == 0:
return None
else:
key = self.get_key()
parent = self.get_parent()
return parent.get_child_index(key)
def get_key(self):
return self._key
def set_key(self, key):
self._key = key
def change_key(self, key):
self.get_parent().change_child_key(self._key, key)
def get_parent(self):
if self._parent == None and self.get_depth() > 0:
self._parent = self.load_parent()
return self._parent
def load_parent(self):
"""Provide TreeNode with a parent for the current node. This function
is only required if the tree was instantiated from a child node
(virtual function)"""
raise TreeWidgetError("virtual function. Implement in subclass")
def get_value(self):
return self._value
def is_root(self):
return self.get_depth() == 0
def next_sibling(self):
if self.get_depth() > 0:
return self.get_parent().next_child(self.get_key())
else:
return None
def prev_sibling(self):
if self.get_depth() > 0:
return self.get_parent().prev_child(self.get_key())
else:
return None
def get_root(self):
root = self
while root.get_parent() is not None:
root = root.get_parent()
return root
class ParentNode(TreeNode):
"""Maintain sort order for TreeNodes."""
def __init__(self, value, parent=None, key=None, depth=None):
TreeNode.__init__(self, value, parent=parent, key=key, depth=depth)
self._child_keys = None
self._children = {}
def get_child_keys(self, reload=False):
"""Return a possibly ordered list of child keys"""
if self._child_keys is None or reload == True:
self._child_keys = self.load_child_keys()
return self._child_keys
def load_child_keys(self):
"""Provide ParentNode with an ordered list of child keys (virtual
function)"""
raise TreeWidgetError("virtual function. Implement in subclass")
def get_child_widget(self, key):
"""Return the widget for a given key. Create if necessary."""
child = self.get_child_node(key)
return child.get_widget()
def get_child_node(self, key, reload=False):
"""Return the child node for a given key. Create if necessary."""
if key not in self._children or reload == True:
self._children[key] = self.load_child_node(key)
return self._children[key]
def load_child_node(self, key):
"""Load the child node for a given key (virtual function)"""
raise TreeWidgetError("virtual function. Implement in subclass")
def set_child_node(self, key, node):
"""Set the child node for a given key. Useful for bottom-up, lazy
population of a tree.."""
self._children[key]=node
def change_child_key(self, oldkey, newkey):
if newkey in self._children:
raise TreeWidgetError("%s is already in use" % newkey)
self._children[newkey] = self._children.pop(oldkey)
self._children[newkey].set_key(newkey)
def get_child_index(self, key):
try:
return self.get_child_keys().index(key)
except ValueError:
errorstring = ("Can't find key %s in ParentNode %s\n" +
"ParentNode items: %s")
raise TreeWidgetError(errorstring % (key, self.get_key(),
str(self.get_child_keys())))
def next_child(self, key):
"""Return the next child node in index order from the given key."""
index = self.get_child_index(key)
# the given node may have just been deleted
if index is None:
return None
index += 1
child_keys = self.get_child_keys()
if index < len(child_keys):
# get the next item at same level
return self.get_child_node(child_keys[index])
else:
return None
def prev_child(self, key):
"""Return the previous child node in index order from the given key."""
index = self.get_child_index(key)
if index is None:
return None
child_keys = self.get_child_keys()
index -= 1
if index >= 0:
# get the previous item at same level
return self.get_child_node(child_keys[index])
else:
return None
def get_first_child(self):
"""Return the first TreeNode in the directory."""
child_keys = self.get_child_keys()
return self.get_child_node(child_keys[0])
def get_last_child(self):
"""Return the last TreeNode in the directory."""
child_keys = self.get_child_keys()
return self.get_child_node(child_keys[-1])
def has_children(self):
"""Does this node have any children?"""
return len(self.get_child_keys())>0
class TreeWalker(urwid.ListWalker):
"""ListWalker-compatible class for displaying TreeWidgets
positions are TreeNodes."""
def __init__(self, start_from):
"""start_from: TreeNode with the initial focus."""
self.focus = start_from
def get_focus(self):
widget = self.focus.get_widget()
return widget, self.focus
def set_focus(self, focus):
self.focus = focus
self._modified()
def get_next(self, start_from):
widget = start_from.get_widget()
target = widget.next_inorder()
if target is None:
return None, None
else:
return target, target.get_node()
def get_prev(self, start_from):
widget = start_from.get_widget()
target = widget.prev_inorder()
if target is None:
return None, None
else:
return target, target.get_node()
class TreeListBox(urwid.ListBox):
"""A ListBox with special handling for navigation and
collapsing of TreeWidgets"""
def keypress(self, size, key):
key = self.__super.keypress(size, key)
return self.unhandled_input(size, key)
def unhandled_input(self, size, input):
"""Handle macro-navigation keys"""
if input == 'left':
self.move_focus_to_parent(size)
elif input == '-':
self.collapse_focus_parent(size)
elif input == 'home':
self.focus_home(size)
elif input == 'end':
self.focus_end(size)
else:
return input
def collapse_focus_parent(self, size):
"""Collapse parent directory."""
widget, pos = self.body.get_focus()
self.move_focus_to_parent(size)
pwidget, ppos = self.body.get_focus()
if pos != ppos:
self.keypress(size, "-")
def move_focus_to_parent(self, size):
"""Move focus to parent of widget in focus."""
widget, pos = self.body.get_focus()
parentpos = pos.get_parent()
if parentpos is None:
return
middle, top, bottom = self.calculate_visible( size )
row_offset, focus_widget, focus_pos, focus_rows, cursor = middle
trim_top, fill_above = top
for widget, pos, rows in fill_above:
row_offset -= rows
if pos == parentpos:
self.change_focus(size, pos, row_offset)
return
self.change_focus(size, pos.get_parent())
def focus_home(self, size):
"""Move focus to very top."""
widget, pos = self.body.get_focus()
rootnode = pos.get_root()
self.change_focus(size, rootnode)
def focus_end( self, size ):
"""Move focus to far bottom."""
maxrow, maxcol = size
widget, pos = self.body.get_focus()
rootnode = pos.get_root()
rootwidget = rootnode.get_widget()
lastwidget = rootwidget.last_child()
lastnode = lastwidget.get_node()
self.change_focus(size, lastnode, maxrow-1)

View File

@@ -1,474 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid utility functions
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from urwid import escape
from urwid.compat import bytes
import codecs
str_util = escape.str_util
# bring str_util functions into our namespace
calc_text_pos = str_util.calc_text_pos
calc_width = str_util.calc_width
is_wide_char = str_util.is_wide_char
move_next_char = str_util.move_next_char
move_prev_char = str_util.move_prev_char
within_double_byte = str_util.within_double_byte
def detect_encoding():
# Try to determine if using a supported double-byte encoding
import locale
try:
try:
locale.setlocale(locale.LC_ALL, "")
except locale.Error:
pass
return locale.getlocale()[1] or ""
except ValueError as e:
# with invalid LANG value python will throw ValueError
if e.args and e.args[0].startswith("unknown locale"):
return ""
else:
raise
if 'detected_encoding' not in locals():
detected_encoding = detect_encoding()
else:
assert 0, "It worked!"
_target_encoding = None
_use_dec_special = True
def set_encoding( encoding ):
"""
Set the byte encoding to assume when processing strings and the
encoding to use when converting unicode strings.
"""
encoding = encoding.lower()
global _target_encoding, _use_dec_special
if encoding in ( 'utf-8', 'utf8', 'utf' ):
str_util.set_byte_encoding("utf8")
_use_dec_special = False
elif encoding in ( 'euc-jp' # JISX 0208 only
, 'euc-kr', 'euc-cn', 'euc-tw' # CNS 11643 plain 1 only
, 'gb2312', 'gbk', 'big5', 'cn-gb', 'uhc'
# these shouldn't happen, should they?
, 'eucjp', 'euckr', 'euccn', 'euctw', 'cncb' ):
str_util.set_byte_encoding("wide")
_use_dec_special = True
else:
str_util.set_byte_encoding("narrow")
_use_dec_special = True
# if encoding is valid for conversion from unicode, remember it
_target_encoding = 'ascii'
try:
if encoding:
u"".encode(encoding)
_target_encoding = encoding
except LookupError: pass
def get_encoding_mode():
"""
Get the mode Urwid is using when processing text strings.
Returns 'narrow' for 8-bit encodings, 'wide' for CJK encodings
or 'utf8' for UTF-8 encodings.
"""
return str_util.get_byte_encoding()
def apply_target_encoding( s ):
"""
Return (encoded byte string, character set rle).
"""
if _use_dec_special and type(s) == unicode:
# first convert drawing characters
try:
s = s.translate( escape.DEC_SPECIAL_CHARMAP )
except NotImplementedError:
# python < 2.4 needs to do this the hard way..
for c, alt in zip(escape.DEC_SPECIAL_CHARS,
escape.ALT_DEC_SPECIAL_CHARS):
s = s.replace( c, escape.SO+alt+escape.SI )
if type(s) == unicode:
s = s.replace(escape.SI+escape.SO, u"") # remove redundant shifts
s = codecs.encode(s, _target_encoding, 'replace')
assert isinstance(s, bytes)
SO = escape.SO.encode('ascii')
SI = escape.SI.encode('ascii')
sis = s.split(SO)
assert isinstance(sis[0], bytes)
sis0 = sis[0].replace(SI, bytes())
sout = []
cout = []
if sis0:
sout.append( sis0 )
cout.append( (None,len(sis0)) )
if len(sis)==1:
return sis0, cout
for sn in sis[1:]:
assert isinstance(sn, bytes)
assert isinstance(SI, bytes)
sl = sn.split(SI, 1)
if len(sl) == 1:
sin = sl[0]
assert isinstance(sin, bytes)
sout.append(sin)
rle_append_modify(cout, (escape.DEC_TAG.encode('ascii'), len(sin)))
continue
sin, son = sl
son = son.replace(SI, bytes())
if sin:
sout.append(sin)
rle_append_modify(cout, (escape.DEC_TAG, len(sin)))
if son:
sout.append(son)
rle_append_modify(cout, (None, len(son)))
outstr = bytes().join(sout)
return outstr, cout
######################################################################
# Try to set the encoding using the one detected by the locale module
set_encoding( detected_encoding )
######################################################################
def supports_unicode():
"""
Return True if python is able to convert non-ascii unicode strings
to the current encoding.
"""
return _target_encoding and _target_encoding != 'ascii'
def calc_trim_text( text, start_offs, end_offs, start_col, end_col ):
"""
Calculate the result of trimming text.
start_offs -- offset into text to treat as screen column 0
end_offs -- offset into text to treat as the end of the line
start_col -- screen column to trim at the left
end_col -- screen column to trim at the right
Returns (start, end, pad_left, pad_right), where:
start -- resulting start offset
end -- resulting end offset
pad_left -- 0 for no pad or 1 for one space to be added
pad_right -- 0 for no pad or 1 for one space to be added
"""
spos = start_offs
pad_left = pad_right = 0
if start_col > 0:
spos, sc = calc_text_pos( text, spos, end_offs, start_col )
if sc < start_col:
pad_left = 1
spos, sc = calc_text_pos( text, start_offs,
end_offs, start_col+1 )
run = end_col - start_col - pad_left
pos, sc = calc_text_pos( text, spos, end_offs, run )
if sc < run:
pad_right = 1
return ( spos, pos, pad_left, pad_right )
def trim_text_attr_cs( text, attr, cs, start_col, end_col ):
"""
Return ( trimmed text, trimmed attr, trimmed cs ).
"""
spos, epos, pad_left, pad_right = calc_trim_text(
text, 0, len(text), start_col, end_col )
attrtr = rle_subseg( attr, spos, epos )
cstr = rle_subseg( cs, spos, epos )
if pad_left:
al = rle_get_at( attr, spos-1 )
rle_append_beginning_modify( attrtr, (al, 1) )
rle_append_beginning_modify( cstr, (None, 1) )
if pad_right:
al = rle_get_at( attr, epos )
rle_append_modify( attrtr, (al, 1) )
rle_append_modify( cstr, (None, 1) )
return (bytes().rjust(pad_left) + text[spos:epos] +
bytes().rjust(pad_right), attrtr, cstr)
def rle_get_at( rle, pos ):
"""
Return the attribute at offset pos.
"""
x = 0
if pos < 0:
return None
for a, run in rle:
if x+run > pos:
return a
x += run
return None
def rle_subseg( rle, start, end ):
"""Return a sub segment of an rle list."""
l = []
x = 0
for a, run in rle:
if start:
if start >= run:
start -= run
x += run
continue
x += start
run -= start
start = 0
if x >= end:
break
if x+run > end:
run = end-x
x += run
l.append( (a, run) )
return l
def rle_len( rle ):
"""
Return the number of characters covered by a run length
encoded attribute list.
"""
run = 0
for v in rle:
assert type(v) == tuple, repr(rle)
a, r = v
run += r
return run
def rle_append_beginning_modify(rle, a_r):
"""
Append (a, r) (unpacked from *a_r*) to BEGINNING of rle.
Merge with first run when possible
MODIFIES rle parameter contents. Returns None.
"""
a, r = a_r
if not rle:
rle[:] = [(a, r)]
else:
al, run = rle[0]
if a == al:
rle[0] = (a,run+r)
else:
rle[0:0] = [(al, r)]
def rle_append_modify(rle, a_r):
"""
Append (a, r) (unpacked from *a_r*) to the rle list rle.
Merge with last run when possible.
MODIFIES rle parameter contents. Returns None.
"""
a, r = a_r
if not rle or rle[-1][0] != a:
rle.append( (a,r) )
return
la,lr = rle[-1]
rle[-1] = (a, lr+r)
def rle_join_modify( rle, rle2 ):
"""
Append attribute list rle2 to rle.
Merge last run of rle with first run of rle2 when possible.
MODIFIES attr parameter contents. Returns None.
"""
if not rle2:
return
rle_append_modify(rle, rle2[0])
rle += rle2[1:]
def rle_product( rle1, rle2 ):
"""
Merge the runs of rle1 and rle2 like this:
eg.
rle1 = [ ("a", 10), ("b", 5) ]
rle2 = [ ("Q", 5), ("P", 10) ]
rle_product: [ (("a","Q"), 5), (("a","P"), 5), (("b","P"), 5) ]
rle1 and rle2 are assumed to cover the same total run.
"""
i1 = i2 = 1 # rle1, rle2 indexes
if not rle1 or not rle2: return []
a1, r1 = rle1[0]
a2, r2 = rle2[0]
l = []
while r1 and r2:
r = min(r1, r2)
rle_append_modify( l, ((a1,a2),r) )
r1 -= r
if r1 == 0 and i1< len(rle1):
a1, r1 = rle1[i1]
i1 += 1
r2 -= r
if r2 == 0 and i2< len(rle2):
a2, r2 = rle2[i2]
i2 += 1
return l
def rle_factor( rle ):
"""
Inverse of rle_product.
"""
rle1 = []
rle2 = []
for (a1, a2), r in rle:
rle_append_modify( rle1, (a1, r) )
rle_append_modify( rle2, (a2, r) )
return rle1, rle2
class TagMarkupException(Exception): pass
def decompose_tagmarkup(tm):
"""Return (text string, attribute list) for tagmarkup passed."""
tl, al = _tagmarkup_recurse(tm, None)
# join as unicode or bytes based on type of first element
text = tl[0][:0].join(tl)
if al and al[-1][0] is None:
del al[-1]
return text, al
def _tagmarkup_recurse( tm, attr ):
"""Return (text list, attribute list) for tagmarkup passed.
tm -- tagmarkup
attr -- current attribute or None"""
if type(tm) == list:
# for lists recurse to process each subelement
rtl = []
ral = []
for element in tm:
tl, al = _tagmarkup_recurse( element, attr )
if ral:
# merge attributes when possible
last_attr, last_run = ral[-1]
top_attr, top_run = al[0]
if last_attr == top_attr:
ral[-1] = (top_attr, last_run + top_run)
del al[-1]
rtl += tl
ral += al
return rtl, ral
if type(tm) == tuple:
# tuples mark a new attribute boundary
if len(tm) != 2:
raise TagMarkupException("Tuples must be in the form (attribute, tagmarkup): %r" % (tm,))
attr, element = tm
return _tagmarkup_recurse( element, attr )
if not isinstance(tm,(basestring, bytes)):
raise TagMarkupException("Invalid markup element: %r" % tm)
# text
return [tm], [(attr, len(tm))]
def is_mouse_event( ev ):
return type(ev) == tuple and len(ev)==4 and ev[0].find("mouse")>=0
def is_mouse_press( ev ):
return ev.find("press")>=0
class MetaSuper(type):
"""adding .__super"""
def __init__(cls, name, bases, d):
super(MetaSuper, cls).__init__(name, bases, d)
if hasattr(cls, "_%s__super" % name):
raise AttributeError("Class has same name as one of its super classes")
setattr(cls, "_%s__super" % name, super(cls))
def int_scale(val, val_range, out_range):
"""
Scale val in the range [0, val_range-1] to an integer in the range
[0, out_range-1]. This implementation uses the "round-half-up" rounding
method.
>>> "%x" % int_scale(0x7, 0x10, 0x10000)
'7777'
>>> "%x" % int_scale(0x5f, 0x100, 0x10)
'6'
>>> int_scale(2, 6, 101)
40
>>> int_scale(1, 3, 4)
2
"""
num = int(val * (out_range-1) * 2 + (val_range-1))
dem = ((val_range-1) * 2)
# if num % dem == 0 then we are exactly half-way and have rounded up.
return num // dem
class StoppingContext(object):
"""Context manager that calls ``stop`` on a given object on exit. Used to
make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as
context managers.
"""
def __init__(self, wrapped):
self._wrapped = wrapped
def __enter__(self):
return self
def __exit__(self, *exc_info):
self._wrapped.stop()

View File

@@ -1,5 +0,0 @@
VERSION = (1, 3, 1, 'dev')
__version__ = ''.join(['-.'[type(x) == int]+str(x) for x in VERSION])[1:]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,664 +0,0 @@
#!/usr/bin/python
#
# Urwid Window-Icon-Menu-Pointer-style widget classes
# Copyright (C) 2004-2011 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from urwid.widget import (Text, WidgetWrap, delegate_to_widget_mixin, BOX,
FLOW)
from urwid.canvas import CompositeCanvas
from urwid.signals import connect_signal
from urwid.container import Columns, Overlay
from urwid.util import is_mouse_press
from urwid.text_layout import calc_coords
from urwid.signals import disconnect_signal # doctests
from urwid.split_repr import python3_repr
from urwid.decoration import WidgetDecoration
from urwid.command_map import ACTIVATE
class SelectableIcon(Text):
_selectable = True
def __init__(self, text, cursor_position=1):
"""
:param text: markup for this widget; see :class:`Text` for
description of text markup
:param cursor_position: position the cursor will appear in the
text when this widget is in focus
This is a text widget that is selectable. A cursor
displayed at a fixed location in the text when in focus.
This widget has no special handling of keyboard or mouse input.
"""
self.__super.__init__(text)
self._cursor_position = cursor_position
def render(self, size, focus=False):
"""
Render the text content of this widget with a cursor when
in focus.
>>> si = SelectableIcon(u"[!]")
>>> si
<SelectableIcon selectable flow widget '[!]'>
>>> si.render((4,), focus=True).cursor
(1, 0)
>>> si = SelectableIcon("((*))", 2)
>>> si.render((8,), focus=True).cursor
(2, 0)
>>> si.render((2,), focus=True).cursor
(0, 1)
"""
c = self.__super.render(size, focus)
if focus:
# create a new canvas so we can add a cursor
c = CompositeCanvas(c)
c.cursor = self.get_cursor_coords(size)
return c
def get_cursor_coords(self, size):
"""
Return the position of the cursor if visible. This method
is required for widgets that display a cursor.
"""
if self._cursor_position > len(self.text):
return None
# find out where the cursor will be displayed based on
# the text layout
(maxcol,) = size
trans = self.get_line_translation(maxcol)
x, y = calc_coords(self.text, trans, self._cursor_position)
if maxcol <= x:
return None
return x, y
def keypress(self, size, key):
"""
No keys are handled by this widget. This method is
required for selectable widgets.
"""
return key
class CheckBoxError(Exception):
pass
class CheckBox(WidgetWrap):
def sizing(self):
return frozenset([FLOW])
states = {
True: SelectableIcon("[X]"),
False: SelectableIcon("[ ]"),
'mixed': SelectableIcon("[#]") }
reserve_columns = 4
# allow users of this class to listen for change events
# sent when the state of this widget is modified
# (this variable is picked up by the MetaSignals metaclass)
signals = ["change"]
def __init__(self, label, state=False, has_mixed=False,
on_state_change=None, user_data=None):
"""
:param label: markup for check box label
:param state: False, True or "mixed"
:param has_mixed: True if "mixed" is a state to cycle through
:param on_state_change: shorthand for connect_signal()
function call for a single callback
:param user_data: user_data for on_state_change
Signals supported: ``'change'``
Register signal handler with::
urwid.connect_signal(check_box, 'change', callback, user_data)
where callback is callback(check_box, new_state [,user_data])
Unregister signal handlers with::
urwid.disconnect_signal(check_box, 'change', callback, user_data)
>>> CheckBox(u"Confirm")
<CheckBox selectable flow widget 'Confirm' state=False>
>>> CheckBox(u"Yogourt", "mixed", True)
<CheckBox selectable flow widget 'Yogourt' state='mixed'>
>>> cb = CheckBox(u"Extra onions", True)
>>> cb
<CheckBox selectable flow widget 'Extra onions' state=True>
>>> cb.render((20,), focus=True).text # ... = b in Python 3
[...'[X] Extra onions ']
"""
self.__super.__init__(None) # self.w set by set_state below
self._label = Text("")
self.has_mixed = has_mixed
self._state = None
# The old way of listening for a change was to pass the callback
# in to the constructor. Just convert it to the new way:
if on_state_change:
connect_signal(self, 'change', on_state_change, user_data)
self.set_label(label)
self.set_state(state)
def _repr_words(self):
return self.__super._repr_words() + [
python3_repr(self.label)]
def _repr_attrs(self):
return dict(self.__super._repr_attrs(),
state=self.state)
def set_label(self, label):
"""
Change the check box label.
label -- markup for label. See Text widget for description
of text markup.
>>> cb = CheckBox(u"foo")
>>> cb
<CheckBox selectable flow widget 'foo' state=False>
>>> cb.set_label(('bright_attr', u"bar"))
>>> cb
<CheckBox selectable flow widget 'bar' state=False>
"""
self._label.set_text(label)
# no need to call self._invalidate(). WidgetWrap takes care of
# that when self.w changes
def get_label(self):
"""
Return label text.
>>> cb = CheckBox(u"Seriously")
>>> print cb.get_label()
Seriously
>>> print cb.label
Seriously
>>> cb.set_label([('bright_attr', u"flashy"), u" normal"])
>>> print cb.label # only text is returned
flashy normal
"""
return self._label.text
label = property(get_label)
def set_state(self, state, do_callback=True):
"""
Set the CheckBox state.
state -- True, False or "mixed"
do_callback -- False to supress signal from this change
>>> changes = []
>>> def callback_a(cb, state, user_data):
... changes.append("A %r %r" % (state, user_data))
>>> def callback_b(cb, state):
... changes.append("B %r" % state)
>>> cb = CheckBox('test', False, False)
>>> key1 = connect_signal(cb, 'change', callback_a, "user_a")
>>> key2 = connect_signal(cb, 'change', callback_b)
>>> cb.set_state(True) # both callbacks will be triggered
>>> cb.state
True
>>> disconnect_signal(cb, 'change', callback_a, "user_a")
>>> cb.state = False
>>> cb.state
False
>>> cb.set_state(True)
>>> cb.state
True
>>> cb.set_state(False, False) # don't send signal
>>> changes
["A True 'user_a'", 'B True', 'B False', 'B True']
"""
if self._state == state:
return
if state not in self.states:
raise CheckBoxError("%s Invalid state: %s" % (
repr(self), repr(state)))
# self._state is None is a special case when the CheckBox
# has just been created
if do_callback and self._state is not None:
self._emit('change', state)
self._state = state
# rebuild the display widget with the new state
self._w = Columns( [
('fixed', self.reserve_columns, self.states[state] ),
self._label ] )
self._w.focus_col = 0
def get_state(self):
"""Return the state of the checkbox."""
return self._state
state = property(get_state, set_state)
def keypress(self, size, key):
"""
Toggle state on 'activate' command.
>>> assert CheckBox._command_map[' '] == 'activate'
>>> assert CheckBox._command_map['enter'] == 'activate'
>>> size = (10,)
>>> cb = CheckBox('press me')
>>> cb.state
False
>>> cb.keypress(size, ' ')
>>> cb.state
True
>>> cb.keypress(size, ' ')
>>> cb.state
False
"""
if self._command_map[key] != ACTIVATE:
return key
self.toggle_state()
def toggle_state(self):
"""
Cycle to the next valid state.
>>> cb = CheckBox("3-state", has_mixed=True)
>>> cb.state
False
>>> cb.toggle_state()
>>> cb.state
True
>>> cb.toggle_state()
>>> cb.state
'mixed'
>>> cb.toggle_state()
>>> cb.state
False
"""
if self.state == False:
self.set_state(True)
elif self.state == True:
if self.has_mixed:
self.set_state('mixed')
else:
self.set_state(False)
elif self.state == 'mixed':
self.set_state(False)
def mouse_event(self, size, event, button, x, y, focus):
"""
Toggle state on button 1 press.
>>> size = (20,)
>>> cb = CheckBox("clickme")
>>> cb.state
False
>>> cb.mouse_event(size, 'mouse press', 1, 2, 0, True)
True
>>> cb.state
True
"""
if button != 1 or not is_mouse_press(event):
return False
self.toggle_state()
return True
class RadioButton(CheckBox):
states = {
True: SelectableIcon("(X)"),
False: SelectableIcon("( )"),
'mixed': SelectableIcon("(#)") }
reserve_columns = 4
def __init__(self, group, label, state="first True",
on_state_change=None, user_data=None):
"""
:param group: list for radio buttons in same group
:param label: markup for radio button label
:param state: False, True, "mixed" or "first True"
:param on_state_change: shorthand for connect_signal()
function call for a single 'change' callback
:param user_data: user_data for on_state_change
This function will append the new radio button to group.
"first True" will set to True if group is empty.
Signals supported: ``'change'``
Register signal handler with::
urwid.connect_signal(radio_button, 'change', callback, user_data)
where callback is callback(radio_button, new_state [,user_data])
Unregister signal handlers with::
urwid.disconnect_signal(radio_button, 'change', callback, user_data)
>>> bgroup = [] # button group
>>> b1 = RadioButton(bgroup, u"Agree")
>>> b2 = RadioButton(bgroup, u"Disagree")
>>> len(bgroup)
2
>>> b1
<RadioButton selectable flow widget 'Agree' state=True>
>>> b2
<RadioButton selectable flow widget 'Disagree' state=False>
>>> b2.render((15,), focus=True).text # ... = b in Python 3
[...'( ) Disagree ']
"""
if state=="first True":
state = not group
self.group = group
self.__super.__init__(label, state, False, on_state_change,
user_data)
group.append(self)
def set_state(self, state, do_callback=True):
"""
Set the RadioButton state.
state -- True, False or "mixed"
do_callback -- False to supress signal from this change
If state is True all other radio buttons in the same button
group will be set to False.
>>> bgroup = [] # button group
>>> b1 = RadioButton(bgroup, u"Agree")
>>> b2 = RadioButton(bgroup, u"Disagree")
>>> b3 = RadioButton(bgroup, u"Unsure")
>>> b1.state, b2.state, b3.state
(True, False, False)
>>> b2.set_state(True)
>>> b1.state, b2.state, b3.state
(False, True, False)
>>> def relabel_button(radio_button, new_state):
... radio_button.set_label(u"Think Harder!")
>>> key = connect_signal(b3, 'change', relabel_button)
>>> b3
<RadioButton selectable flow widget 'Unsure' state=False>
>>> b3.set_state(True) # this will trigger the callback
>>> b3
<RadioButton selectable flow widget 'Think Harder!' state=True>
"""
if self._state == state:
return
self.__super.set_state(state, do_callback)
# if we're clearing the state we don't have to worry about
# other buttons in the button group
if state is not True:
return
# clear the state of each other radio button
for cb in self.group:
if cb is self: continue
if cb._state:
cb.set_state(False)
def toggle_state(self):
"""
Set state to True.
>>> bgroup = [] # button group
>>> b1 = RadioButton(bgroup, "Agree")
>>> b2 = RadioButton(bgroup, "Disagree")
>>> b1.state, b2.state
(True, False)
>>> b2.toggle_state()
>>> b1.state, b2.state
(False, True)
>>> b2.toggle_state()
>>> b1.state, b2.state
(False, True)
"""
self.set_state(True)
class Button(WidgetWrap):
def sizing(self):
return frozenset([FLOW])
button_left = Text("<")
button_right = Text(">")
signals = ["click"]
def __init__(self, label, on_press=None, user_data=None):
"""
:param label: markup for button label
:param on_press: shorthand for connect_signal()
function call for a single callback
:param user_data: user_data for on_press
Signals supported: ``'click'``
Register signal handler with::
urwid.connect_signal(button, 'click', callback, user_data)
where callback is callback(button [,user_data])
Unregister signal handlers with::
urwid.disconnect_signal(button, 'click', callback, user_data)
>>> Button(u"Ok")
<Button selectable flow widget 'Ok'>
>>> b = Button("Cancel")
>>> b.render((15,), focus=True).text # ... = b in Python 3
[...'< Cancel >']
"""
self._label = SelectableIcon("", 0)
cols = Columns([
('fixed', 1, self.button_left),
self._label,
('fixed', 1, self.button_right)],
dividechars=1)
self.__super.__init__(cols)
# The old way of listening for a change was to pass the callback
# in to the constructor. Just convert it to the new way:
if on_press:
connect_signal(self, 'click', on_press, user_data)
self.set_label(label)
def _repr_words(self):
# include button.label in repr(button)
return self.__super._repr_words() + [
python3_repr(self.label)]
def set_label(self, label):
"""
Change the button label.
label -- markup for button label
>>> b = Button("Ok")
>>> b.set_label(u"Yup yup")
>>> b
<Button selectable flow widget 'Yup yup'>
"""
self._label.set_text(label)
def get_label(self):
"""
Return label text.
>>> b = Button(u"Ok")
>>> print b.get_label()
Ok
>>> print b.label
Ok
"""
return self._label.text
label = property(get_label)
def keypress(self, size, key):
"""
Send 'click' signal on 'activate' command.
>>> assert Button._command_map[' '] == 'activate'
>>> assert Button._command_map['enter'] == 'activate'
>>> size = (15,)
>>> b = Button(u"Cancel")
>>> clicked_buttons = []
>>> def handle_click(button):
... clicked_buttons.append(button.label)
>>> key = connect_signal(b, 'click', handle_click)
>>> b.keypress(size, 'enter')
>>> b.keypress(size, ' ')
>>> clicked_buttons # ... = u in Python 2
[...'Cancel', ...'Cancel']
"""
if self._command_map[key] != ACTIVATE:
return key
self._emit('click')
def mouse_event(self, size, event, button, x, y, focus):
"""
Send 'click' signal on button 1 press.
>>> size = (15,)
>>> b = Button(u"Ok")
>>> clicked_buttons = []
>>> def handle_click(button):
... clicked_buttons.append(button.label)
>>> key = connect_signal(b, 'click', handle_click)
>>> b.mouse_event(size, 'mouse press', 1, 4, 0, True)
True
>>> b.mouse_event(size, 'mouse press', 2, 4, 0, True) # ignored
False
>>> clicked_buttons # ... = u in Python 2
[...'Ok']
"""
if button != 1 or not is_mouse_press(event):
return False
self._emit('click')
return True
class PopUpLauncher(delegate_to_widget_mixin('_original_widget'),
WidgetDecoration):
def __init__(self, original_widget):
self.__super.__init__(original_widget)
self._pop_up_widget = None
def create_pop_up(self):
"""
Subclass must override this method and return a widget
to be used for the pop-up. This method is called once each time
the pop-up is opened.
"""
raise NotImplementedError("Subclass must override this method")
def get_pop_up_parameters(self):
"""
Subclass must override this method and have it return a dict, eg:
{'left':0, 'top':1, 'overlay_width':30, 'overlay_height':4}
This method is called each time this widget is rendered.
"""
raise NotImplementedError("Subclass must override this method")
def open_pop_up(self):
self._pop_up_widget = self.create_pop_up()
self._invalidate()
def close_pop_up(self):
self._pop_up_widget = None
self._invalidate()
def render(self, size, focus=False):
canv = self.__super.render(size, focus)
if self._pop_up_widget:
canv = CompositeCanvas(canv)
canv.set_pop_up(self._pop_up_widget, **self.get_pop_up_parameters())
return canv
class PopUpTarget(WidgetDecoration):
# FIXME: this whole class is a terrible hack and must be fixed
# when layout and rendering are separated
_sizing = set([BOX])
_selectable = True
def __init__(self, original_widget):
self.__super.__init__(original_widget)
self._pop_up = None
self._current_widget = self._original_widget
def _update_overlay(self, size, focus):
canv = self._original_widget.render(size, focus=focus)
self._cache_original_canvas = canv # imperfect performance hack
pop_up = canv.get_pop_up()
if pop_up:
left, top, (
w, overlay_width, overlay_height) = pop_up
if self._pop_up != w:
self._pop_up = w
self._current_widget = Overlay(w, self._original_widget,
('fixed left', left), overlay_width,
('fixed top', top), overlay_height)
else:
self._current_widget.set_overlay_parameters(
('fixed left', left), overlay_width,
('fixed top', top), overlay_height)
else:
self._pop_up = None
self._current_widget = self._original_widget
def render(self, size, focus=False):
self._update_overlay(size, focus)
return self._current_widget.render(size, focus=focus)
def get_cursor_coords(self, size):
self._update_overlay(size, True)
return self._current_widget.get_cursor_coords(size)
def get_pref_col(self, size):
self._update_overlay(size, True)
return self._current_widget.get_pref_col(size)
def keypress(self, size, key):
self._update_overlay(size, True)
return self._current_widget.keypress(size, key)
def move_cursor_to_coords(self, size, x, y):
self._update_overlay(size, True)
return self._current_widget.move_cursor_to_coords(size, x, y)
def mouse_event(self, size, event, button, x, y, focus):
self._update_overlay(size, focus)
return self._current_widget.mouse_event(size, event, button, x, y, focus)
def pack(self, size=None, focus=False):
self._update_overlay(size, focus)
return self._current_widget.pack(size)
def _test():
import doctest
doctest.testmod()
if __name__=='__main__':
_test()