Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55970ab144 | ||
|
|
65bd30d026 | ||
|
|
dac83e00a1 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,12 +1,10 @@
|
||||
*.swp
|
||||
*.pyc
|
||||
|
||||
__pycache__
|
||||
|
||||
tags
|
||||
|
||||
venv
|
||||
|
||||
.state
|
||||
.oauth
|
||||
secrets.py
|
||||
|
||||
urwid-src
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "python-asana"]
|
||||
path = python-asana
|
||||
url = git@github.com:Asana/python-asana.git
|
||||
18
LICENSE
18
LICENSE
@@ -1,18 +0,0 @@
|
||||
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
2
Makefile
@@ -1,2 +0,0 @@
|
||||
tags: ui/*.py models/*.py main.py auth.py asana_service.py
|
||||
ctags -R ui models *.py
|
||||
33
README.md
33
README.md
@@ -1,38 +1,15 @@
|
||||
# cmdasana
|
||||
A curses CLI for Asana, using the Asana API.
|
||||
|
||||
## Requirments
|
||||
* python 3
|
||||
Requirments:
|
||||
* [python-asana](https://github.com/Asana/python-asana)
|
||||
* [urwid](http://urwid.org)
|
||||
* [python-dateutil](https://github.com/dateutil/dateutil/)
|
||||
* [urwid (included)](http://urwid.org)
|
||||
* python 2
|
||||
|
||||
## 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='...'
|
||||
Usage:
|
||||
```
|
||||
|
||||
### Install dependencies
|
||||
Using `pip`:
|
||||
```
|
||||
pip3 install asana urwid python-dateutil
|
||||
```
|
||||
|
||||
## Usage
|
||||
```
|
||||
./main.py
|
||||
./cmdasana.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,84 +0,0 @@
|
||||
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
61
auth.py
@@ -1,61 +0,0 @@
|
||||
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
|
||||
388
cmdasana.py
Executable file
388
cmdasana.py
Executable file
@@ -0,0 +1,388 @@
|
||||
#!/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
131
main.py
@@ -1,131 +0,0 @@
|
||||
#!/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
228
models/models.py
@@ -1,228 +0,0 @@
|
||||
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']]
|
||||
1
python-asana
Submodule
1
python-asana
Submodule
Submodule python-asana added at 66c31afc60
@@ -1,3 +0,0 @@
|
||||
asana==0.8.2
|
||||
python-dateutil==2.8.0
|
||||
urwid==2.0.1
|
||||
391
ui.py
Normal file
391
ui.py
Normal file
@@ -0,0 +1,391 @@
|
||||
# -*- 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 ProjectTypeAhead(urwid.Pile):
|
||||
def __init__(self, task):
|
||||
body = [('pack', urwid.AttrMap(ProjectIcon(project, self.loadProject),
|
||||
'project'))
|
||||
for project in task['projects']]
|
||||
|
||||
|
||||
self.edit = urwid.Edit('Add Project: ')
|
||||
urwid.connect_signal(self.edit, 'change', self.typeAhead)
|
||||
|
||||
body.append(('pack', self.edit))
|
||||
|
||||
super(ProjecTypeAhead).__init__(body)
|
||||
|
||||
def typeAhead(self, widget, text):
|
||||
urwid.emit_signal(self, 'projecttypeahead', text, self.updateTypeAhead)
|
||||
|
||||
def updateTypeAhead(self, projects):
|
||||
projects = [(TypeAheadButton(p, self.add), ('pack', None))
|
||||
for p in projects]
|
||||
|
||||
body = [('pack', urwid.AttrMap(ProjectIcon(project, self.loadProject),
|
||||
'project'))
|
||||
for project in self.task['projects']]
|
||||
|
||||
body.append(('pack', self.edit))
|
||||
|
||||
self.contents = body + projects
|
||||
|
||||
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)
|
||||
|
||||
containers = []
|
||||
|
||||
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)
|
||||
containers.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 = containers + \
|
||||
[
|
||||
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)
|
||||
|
||||
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)
|
||||
39
ui/auth.py
39
ui/auth.py
@@ -1,39 +0,0 @@
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
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']
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
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
|
||||
@@ -1,60 +0,0 @@
|
||||
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
73
ui/ui.py
@@ -1,73 +0,0 @@
|
||||
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'
|
||||
)
|
||||
78
urwid/__init__.py
Normal file
78
urwid/__init__.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/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
|
||||
1317
urwid/canvas.py
Normal file
1317
urwid/canvas.py
Normal file
File diff suppressed because it is too large
Load Diff
104
urwid/command_map.py
Normal file
104
urwid/command_map.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/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
|
||||
48
urwid/compat.py
Normal file
48
urwid/compat.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/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])
|
||||
|
||||
|
||||
2303
urwid/container.py
Executable file
2303
urwid/container.py
Executable file
File diff suppressed because it is too large
Load Diff
619
urwid/curses_display.py
Executable file
619
urwid/curses_display.py
Executable file
@@ -0,0 +1,619 @@
|
||||
#!/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()
|
||||
1170
urwid/decoration.py
Executable file
1170
urwid/decoration.py
Executable file
File diff suppressed because it is too large
Load Diff
894
urwid/display_common.py
Executable file
894
urwid/display_common.py
Executable file
@@ -0,0 +1,894 @@
|
||||
#!/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()
|
||||
441
urwid/escape.py
Normal file
441
urwid/escape.py
Normal file
@@ -0,0 +1,441 @@
|
||||
#!/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"
|
||||
450
urwid/font.py
Executable file
450
urwid/font.py
Executable file
@@ -0,0 +1,450 @@
|
||||
#!/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
|
||||
911
urwid/graphics.py
Executable file
911
urwid/graphics.py
Executable file
@@ -0,0 +1,911 @@
|
||||
#!/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
|
||||
245
urwid/html_fragment.py
Executable file
245
urwid/html_fragment.py
Executable file
@@ -0,0 +1,245 @@
|
||||
#!/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('&','&')
|
||||
text = text.replace('<','<')
|
||||
text = text.replace('>','>')
|
||||
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
|
||||
|
||||
|
||||
485
urwid/lcd_display.py
Normal file
485
urwid/lcd_display.py
Normal file
@@ -0,0 +1,485 @@
|
||||
#!/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 byte’s 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 one’s
|
||||
# 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))
|
||||
|
||||
1668
urwid/listbox.py
Normal file
1668
urwid/listbox.py
Normal file
File diff suppressed because it is too large
Load Diff
1375
urwid/main_loop.py
Executable file
1375
urwid/main_loop.py
Executable file
File diff suppressed because it is too large
Load Diff
496
urwid/monitored_list.py
Executable file
496
urwid/monitored_list.py
Executable file
@@ -0,0 +1,496 @@
|
||||
#!/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()
|
||||
|
||||
368
urwid/old_str_util.py
Executable file
368
urwid/old_str_util.py
Executable file
@@ -0,0 +1,368 @@
|
||||
#!/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()
|
||||
|
||||
1030
urwid/raw_display.py
Normal file
1030
urwid/raw_display.py
Normal file
File diff suppressed because it is too large
Load Diff
302
urwid/signals.py
Normal file
302
urwid/signals.py
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/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
|
||||
|
||||
149
urwid/split_repr.py
Executable file
149
urwid/split_repr.py
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/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()
|
||||
391
urwid/tests/test_canvas.py
Normal file
391
urwid/tests/test_canvas.py
Normal file
@@ -0,0 +1,391 @@
|
||||
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"))]])
|
||||
638
urwid/tests/test_container.py
Normal file
638
urwid/tests/test_container.py
Normal file
@@ -0,0 +1,638 @@
|
||||
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])
|
||||
149
urwid/tests/test_decoration.py
Normal file
149
urwid/tests/test_decoration.py
Normal file
@@ -0,0 +1,149 @@
|
||||
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')))
|
||||
22
urwid/tests/test_doctests.py
Normal file
22
urwid/tests/test_doctests.py
Normal file
@@ -0,0 +1,22 @@
|
||||
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
|
||||
147
urwid/tests/test_event_loops.py
Normal file
147
urwid/tests/test_event_loops.py
Normal file
@@ -0,0 +1,147 @@
|
||||
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
|
||||
97
urwid/tests/test_graphics.py
Normal file
97
urwid/tests/test_graphics.py
Normal file
@@ -0,0 +1,97 @@
|
||||
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)]) ] )
|
||||
804
urwid/tests/test_listbox.py
Normal file
804
urwid/tests/test_listbox.py
Normal file
@@ -0,0 +1,804 @@
|
||||
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)
|
||||
|
||||
37
urwid/tests/test_str_util.py
Normal file
37
urwid/tests/test_str_util.py
Normal file
@@ -0,0 +1,37 @@
|
||||
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)
|
||||
342
urwid/tests/test_text_layout.py
Normal file
342
urwid/tests/test_text_layout.py
Normal file
@@ -0,0 +1,342 @@
|
||||
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)
|
||||
178
urwid/tests/test_util.py
Normal file
178
urwid/tests/test_util.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# -*- 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))
|
||||
334
urwid/tests/test_vterm.py
Normal file
334
urwid/tests/test_vterm.py
Normal file
@@ -0,0 +1,334 @@
|
||||
# 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')
|
||||
153
urwid/tests/test_widget.py
Normal file
153
urwid/tests/test_widget.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# -*- 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))
|
||||
8
urwid/tests/util.py
Normal file
8
urwid/tests/util.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import urwid
|
||||
|
||||
class SelectableText(urwid.Text):
|
||||
def selectable(self):
|
||||
return 1
|
||||
|
||||
def keypress(self, size, key):
|
||||
return key
|
||||
506
urwid/text_layout.py
Normal file
506
urwid/text_layout.py
Normal file
@@ -0,0 +1,506 @@
|
||||
#!/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
|
||||
486
urwid/treetools.py
Normal file
486
urwid/treetools.py
Normal file
@@ -0,0 +1,486 @@
|
||||
#!/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)
|
||||
|
||||
474
urwid/util.py
Normal file
474
urwid/util.py
Normal file
@@ -0,0 +1,474 @@
|
||||
#!/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()
|
||||
5
urwid/version.py
Normal file
5
urwid/version.py
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
VERSION = (1, 3, 1, 'dev')
|
||||
__version__ = ''.join(['-.'[type(x) == int]+str(x) for x in VERSION])[1:]
|
||||
|
||||
|
||||
1626
urwid/vterm.py
Normal file
1626
urwid/vterm.py
Normal file
File diff suppressed because it is too large
Load Diff
1092
urwid/web_display.py
Executable file
1092
urwid/web_display.py
Executable file
File diff suppressed because it is too large
Load Diff
1825
urwid/widget.py
Normal file
1825
urwid/widget.py
Normal file
File diff suppressed because it is too large
Load Diff
664
urwid/wimp.py
Executable file
664
urwid/wimp.py
Executable file
@@ -0,0 +1,664 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user