Initial bring over of VUI code. (Won't work yet.)

This commit is contained in:
Simon Forman 2019-05-06 13:07:01 -07:00
parent 67f042cc57
commit d4fdde50f9
19 changed files with 2891 additions and 0 deletions

BIN
joy/vui/Iosevka12.BMP Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

163
joy/vui/README.txt Normal file
View File

@ -0,0 +1,163 @@
What is it?
A simple Graphical User Interface for the Joy programming language,
written using Pygame to bypass X11 et. al., modeled on the Oberon OS, and
intended to be just functional enough to support bootstrapping further Joy
development.
It's basic functionality is more-or-less as a crude text editor along with
a simple Joy runtime (interpreter, stack, and dictionary.) It auto- saves
any named files (in a versioned home directory) and you can write new Joy
primitives in Python and Joy definitions and immediately install and use
them, as well as recording them for reuse (after restarts.)
How it works now.
The only dependencies are Pygame and Dulwich (a Python Git library.)
When the main.py script starts it checks for an environment var "JOY_HOME"
which should point to a directory where you want the system to store the
files ("resources") it will edit and save, this directory defaults to
'~/.joypy'. The first time you run it, it will create some default files
as content. Right click on see_resources to open a viewer with the list
of resources (files), copy a name to the stack and right click on
open_resource_at_good_location to open a viewer on that resource.
Right now the screen size defaults to windowed 1024x768, but if you pass
the '-f' option to the main.py script the UI will take up the full screen
at the highest available resolution. The window is divided into two (or
three in fullscreen) vertical "tracks", and the number and width of the
tracks are fixed at start up. (Feel free to edit the values in main.py to
play around with different track configurations.) Each track gets divided
horizontally into zero or more "viewers" (like windows in a windowed GUI,
cf. Chapter 4 of "Project Oberon") for a kind of tiled layout.
Currently, there are only two kinds of (interesting) viewers: TextViewers
and StackViewer. The TextViewers are crude text editors. They provide
just enough functionality to let the user write text and code (Python and
Joy) and execute Joy functions. One important thing they do is
automatically save their content after changes. No more lost work.
The StackViewer is a specialized TextViewer that shows the contents of the
Joy stack one line per stack item. It's a very handy visual aid to keep
track of what's going on. There's also a log.txt file that gets written
to when commands are executed, and so records the log of user actions and
system events. It tends to fill up quickly so there's a reset_log command
that clears it out.
Viewers have "grow" and "close" in ther menu bars. These are buttons.
When you right-click on grow a viewer a copy is created that covers that
viewer's entire track. If you grow a viewer that already takes up its
whole track then a copy is created that takes up an additional track, up
to the whole screen. Closing a viewer just deletes that viewer, and when
a track has no more viewers, it is deleted and that exposes any previous
tracks and viewers that were hidden.
(Note: if you ever close all the viewers and are sitting at a blank screen
with nowhere to type and execute commands, press the Pause/Break key.
This will open a new "trap" viewer which you can then use to recover.)
Copies of a viewer all share the same model and update their display as it
changes. (If you have two viewers open on the same named resource and edit
one you'll see the other update as you type.)
UI Guide
left mouse sets cursor in text, in menu bar resizes viewer interactively
(this is a little buggy in that you can move the mouse quickly and get
outside the menu, leaving the viewer in the "resizing" state. Until I fix
this, the workaround is to just grab the menu bar again and wiggle it a
few pixels and let go. This will reset the machinery.)
Right mouse executes Joy command (functions), and you can drag with the
right button to highlight (well, underline) commands. Words that aren't
names of Joy commands won't be underlined. Release the button to execute
the command.
The middle mouse button (usually a wheel these days) scrolls the text but
you can also click and drag any viewer with it to move that viewer to
another track or to a different location in the same track. There's no
direct visual feedback for this (yet) but that dosen't seem to impair its
usefulness.
F1, F2 - set selection begin and end markers (crude but usable.)
F3 - copy selected text to the top of the stack.
Shift-F3 - as copy then run "parse" command on the string.
F4 - cut selected text to the top of the stack.
Shift-F4 - as cut then run "pop" (delete selection.)
Joy
Pretty much all of the rest of the functionality of the system is provided
by executing Joy commands (aka functions, aka "words" in Forth) by right-
clicking on their names in any text.
To get help on a Joy function select the name of the function in a
TextViewer using F1 and F2, then press shift-F3 to parse the selection.
The function (really its Symbol) will appear on the stack in brackets (a
"quoted program" such as "[pop]".) Then right-click on the word help in
any TextViewer (if it's not already there, just type it in somewhere.)
This will print the docstring or definition of the word (function) to
stdout. At some point I'll write a thing to send that to the log.txt file
instead, but for now look for output in the terminal.
I have pre-defined some system-specific commands, like see_stack to open a
StackViewer, and I should really go and add docstrings to those (so they
work with the help command.)
... inscribe and evaluate for making new Joy and Python, respectively,
commands...
----
Still to do:
* Return key can orphan a line at the bottom of a viewer.
* Calculator buttons on the numpad?
* System query for most recent selection
* Home/End keys
* Vertical scrolling w/ scrollbar?
* Shift-scroll changes viewer height?
* Horizontal scrolling w/ keys
* Horizontal scrolling w/ scrollbar?
* Pgup/down keys?
* Tab key?
* When moving viewers sometimes a command gets executed from the underlying
viewer. This shouldn't happen.
Done:
- Redirect stdout to "print" to the log.
- Initial contents for JOY_HOME.
- Pause/Break to open a trap viewer (in case you close them all.)
- "shutdown" signal to tell PT to commit outstanding changes.
- Local library auto-loaded at start-time
- library.py, primitives in Python
- definitions.txt
- Can name and persist a viewer on an unstored string(list).
- Inscribe function
- Reverse video, well, grey background, menu bars
- PT scans JOY_HOME for resource lists
- Capture and display tracebacks
- StackViewer
- Update log when stack changes
- Open a resource list
- Open a viewer on a (unstored) string
- Selecting text
- Copy and Cut
- Paste
- Menu text, commands and name or title
- "print" to e.g. log
- Command evaluation
- Joy integration
- Persistance of data
- Content change notification
- Vertical scrolling w/ keys
- Vertical scrolling w/ mouse wheel
- Enter/return key
- Arrow keys wrap at line ends
- Backspace/delete wrap at line ends

0
joy/vui/__init__.py Normal file
View File

201
joy/vui/core.py Normal file
View File

@ -0,0 +1,201 @@
from sys import stderr
from traceback import format_exc
import pygame
from joy.joy import run
from joy.utils.stack import stack_to_string
COMMITTER = 'Joy <auto-commit@example.com>'
BLACK = FOREGROUND = 0, 0, 0
GREY = 127, 127, 127
WHITE = BACKGROUND = 255, 255, 255
BLUE = 100, 100, 255
GREEN = 70, 200, 70
MOUSE_EVENTS = frozenset({
pygame.MOUSEMOTION,
pygame.MOUSEBUTTONDOWN,
pygame.MOUSEBUTTONUP
})
ARROW_KEYS = frozenset({
pygame.K_UP,
pygame.K_DOWN,
pygame.K_LEFT,
pygame.K_RIGHT
})
TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS))
AVAILABLE_TASK_EVENTS = set(TASK_EVENTS)
ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN]
ALLOWED_EVENTS.extend(MOUSE_EVENTS)
ALLOWED_EVENTS.extend(TASK_EVENTS)
# Message status codes... dunno if this is a good idea or not...
ERROR = -1
PENDING = 0
SUCCESS = 1
# messaging support
class Message(object):
def __init__(self, sender):
self.sender = sender
class CommandMessage(Message):
def __init__(self, sender, command):
Message.__init__(self, sender)
self.command = command
class ModifyMessage(Message):
def __init__(self, sender, subject, **details):
Message.__init__(self, sender)
self.subject = subject
self.details = details
class OpenMessage(Message):
def __init__(self, sender, name):
Message.__init__(self, sender)
self.name = name
self.content_id = self.thing = None
self.status = PENDING
self.traceback = None
class PersistMessage(Message):
def __init__(self, sender, content_id, **details):
Message.__init__(self, sender)
self.content_id = content_id
self.details = details
class ShutdownMessage(Message): pass
# Joy Interpreter & Context
class World(object):
def __init__(self, stack_id, stack_holder, dictionary, notify, log):
self.stack_holder = stack_holder
self.dictionary = dictionary
self.notify = notify
self.stack_id = stack_id
self.log = log.lines
self.log_id = log.content_id
def handle(self, message):
if (isinstance(message, ModifyMessage)
and message.subject is self.stack_holder
):
self._log_lines('', '%s <-' % self.format_stack())
if not isinstance(message, CommandMessage):
return
c, s, d = message.command, self.stack_holder[0], self.dictionary
self._log_lines('', '-> %s' % (c,))
self.stack_holder[0], _, self.dictionary = run(c, s, d)
mm = ModifyMessage(self, self.stack_holder, content_id=self.stack_id)
self.notify(mm)
def _log_lines(self, *lines):
self.log.extend(lines)
self.notify(ModifyMessage(self, self.log, content_id=self.log_id))
def format_stack(self):
try:
return stack_to_string(self.stack_holder[0])
except:
print >> stderr, format_exc()
return str(self.stack_holder[0])
def push(sender, item, notify, stack_name='stack.pickle'):
om = OpenMessage(sender, stack_name)
notify(om)
if om.status == SUCCESS:
om.thing[0] = item, om.thing[0]
notify(ModifyMessage(sender, om.thing, content_id=om.content_id))
return om.status
def open_viewer_on_string(sender, content, notify):
push(sender, content, notify)
notify(CommandMessage(sender, 'good_viewer_location open_viewer'))
# main loop
class TheLoop(object):
FRAME_RATE = 24
def __init__(self, display, clock):
self.display = display
self.clock = clock
self.tasks = {}
self.running = False
def install_task(self, F, milliseconds):
try:
task_event_id = AVAILABLE_TASK_EVENTS.pop()
except KeyError:
raise RuntimeError('out of task ids')
self.tasks[task_event_id] = F
pygame.time.set_timer(task_event_id, milliseconds)
return task_event_id
def remove_task(self, task_event_id):
assert task_event_id in self.tasks, repr(task_event_id)
pygame.time.set_timer(task_event_id, 0)
del self.tasks[task_event_id]
AVAILABLE_TASK_EVENTS.add(task_event_id)
def __del__(self):
for task_event_id in self.tasks:
pygame.time.set_timer(task_event_id, 0)
def run_task(self, task_event_id):
task = self.tasks[task_event_id]
try:
task()
except:
traceback = format_exc()
self.remove_task(task_event_id)
print >> stderr, traceback
print >> stderr, 'TASK removed due to ERROR', task
open_viewer_on_string(self, traceback, self.display.broadcast)
def loop(self):
self.running = True
while self.running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYUP and event.key == pygame.K_ESCAPE:
self.running = False
elif event.type in self.tasks:
self.run_task(event.type)
else:
self.display.dispatch_event(event)
pygame.display.update()
self.clock.tick(self.FRAME_RATE)
self.display.broadcast(ShutdownMessage(self))

18
joy/vui/debug_main.py Normal file
View File

@ -0,0 +1,18 @@
import sys, traceback
# To enable "hot" reloading in the IDLE shell.
for name in 'core main display viewer text_viewer stack_viewer persist_task'.split():
try:
del sys.modules[name]
except KeyError:
pass
import main
try:
A = A # (screen, clock, pt), three things that we DON'T want to recreate
# each time we restart main().
except NameError:
A = main.init()
d = main.main(*A)

View File

@ -0,0 +1,17 @@
see_stack == good_viewer_location open_stack
see_resources == list_resources good_viewer_location open_viewer
open_resource_at_good_location == good_viewer_location open_resource
see_log == "log.txt" open_resource_at_good_location
see_definitions == "definitions.txt" open_resource_at_good_location
round_to_cents == 100 * ++ floor 100 /
reset_log == "del log.lines[1:] ; log.at_line = 0" evaluate
see_menu == "menu.txt" good_viewer_location open_resource
# Ordered Binary Tree datastructure functions.
BTree-new == swap [[] []] cons cons
_BTree-P == over [popop popop first] nullary
_BTree-T> == [cons cons dipdd] cons cons cons infra
_BTree-T< == [cons cons dipd] cons cons cons infra
_BTree-E == pop swap roll< rest rest cons cons
_BTree-recur == _BTree-P [_BTree-T>] [_BTree-E] [_BTree-T<] cmp
BTree-add == [popop not] [[pop] dipd BTree-new] [] [_BTree-recur] genrec

View File

@ -0,0 +1,206 @@
'''
This file is execfile()'d with a namespace containing:
D - the Joy dictionary
d - the Display object
pt - the PersistTask object
log - the log.txt viewer
loop - the TheLoop main loop object
stack_holder - the Python list object that holds the Joy stack tuple
world - the Joy environment
'''
from joy.library import (
DefinitionWrapper,
FunctionWrapper,
SimpleFunctionWrapper,
)
from joy.utils.stack import list_to_stack, concat
from vui import core, text_viewer, stack_viewer
def install(command): D[command.name] = command
@install
@SimpleFunctionWrapper
def list_resources(stack):
'''
Put a string on the stack with the names of all the known resources
one-per-line.
'''
return '\n'.join(pt.scan()), stack
@install
@SimpleFunctionWrapper
def open_stack(stack):
'''
Given a coordinate pair [x y] (in pixels) open a StackViewer there.
'''
(x, (y, _)), stack = stack
V = d.open_viewer(x, y, stack_viewer.StackViewer)
V.draw()
return stack
@install
@SimpleFunctionWrapper
def open_resource(stack):
'''
Given a coordinate pair [x y] (in pixels) and the name of a resource
(from list_resources command) open a viewer on that resource at that
location.
'''
((x, (y, _)), (name, stack)) = stack
om = core.OpenMessage(world, name)
d.broadcast(om)
if om.status == core.SUCCESS:
V = d.open_viewer(x, y, text_viewer.TextViewer)
V.content_id, V.lines = om.content_id, om.thing
V.draw()
return stack
@install
@SimpleFunctionWrapper
def name_viewer(stack):
'''
Given a string name on the stack, if the currently focused viewer is
anonymous, name the viewer and persist it in the resource store under
that name.
'''
name, stack = stack
assert isinstance(name, str), repr(name)
if d.focused_viewer and not d.focused_viewer.content_id:
d.focused_viewer.content_id = name
pm = core.PersistMessage(world, name, thing=d.focused_viewer.lines)
d.broadcast(pm)
d.focused_viewer.draw_menu()
return stack
##@install
##@SimpleFunctionWrapper
##def persist_viewer(stack):
## if self.focused_viewer:
##
## self.focused_viewer.content_id = name
## self.focused_viewer.draw_menu()
## return stack
@install
@SimpleFunctionWrapper
def inscribe(stack):
'''
Create a new Joy function definition in the Joy dictionary. A
definition is given as a string with a name followed by a double
equal sign then one or more Joy functions, the body. for example:
sqr == dup mul
If you want the definition to persist over restarts, enter it into
the definitions.txt resource.
'''
definition, stack = stack
DefinitionWrapper.add_def(definition, D)
return stack
@install
@SimpleFunctionWrapper
def open_viewer(stack):
'''
Given a coordinate pair [x y] (in pixels) and a string, open a new
unnamed viewer on that string at that location.
'''
((x, (y, _)), (content, stack)) = stack
V = d.open_viewer(x, y, text_viewer.TextViewer)
V.lines = content.splitlines()
V.draw()
return stack
@install
@SimpleFunctionWrapper
def good_viewer_location(stack):
'''
Leave a coordinate pair [x y] (in pixels) on the stack that would
be a good location at which to open a new viewer. (The heuristic
employed is to take up the bottom half of the currently open viewer
with the greatest area.)
'''
viewers = list(d.iter_viewers())
if viewers:
viewers.sort(key=lambda (V, x, y): V.w * V.h)
V, x, y = viewers[-1]
coords = (x + 1, (y + V.h / 2, ()))
else:
coords = (0, (0, ()))
return coords, stack
@install
@FunctionWrapper
def cmp_(stack, expression, dictionary):
'''
The cmp combinator takes two values and three quoted programs on the
stack and runs one of the three depending on the results of comparing
the two values:
a b [G] [E] [L] cmp
------------------------- a > b
G
a b [G] [E] [L] cmp
------------------------- a = b
E
a b [G] [E] [L] cmp
------------------------- a < b
L
'''
L, (E, (G, (b, (a, stack)))) = stack
expression = concat(G if a > b else L if a < b else E, expression)
return stack, expression, dictionary
@install
@SimpleFunctionWrapper
def list_viewers(stack):
'''
Put a string on the stack with some information about the currently
open viewers, one-per-line. This is kind of a demo function, rather
than something really useful.
'''
lines = []
for x, T in d.tracks:
#lines.append('x: %i, w: %i, %r' % (x, T.w, T))
for y, V in T.viewers:
lines.append('x: %i y: %i h: %i %r %r' % (x, y, V.h, V.content_id, V))
return '\n'.join(lines), stack
@install
@SimpleFunctionWrapper
def splitlines(stack):
'''
Given a string on the stack replace it with a list of the lines in
the string.
'''
text, stack = stack
assert isinstance(text, str), repr(text)
return list_to_stack(text.splitlines()), stack
@install
@SimpleFunctionWrapper
def hiya(stack):
'''
Demo function to insert "Hi World!" into the current viewer, if any.
'''
if d.focused_viewer:
d.focused_viewer.insert('Hi World!')
return stack

View File

@ -0,0 +1 @@
Joypy log

View File

@ -0,0 +1,51 @@
name_viewer
list_resources
open_resource_at_good_location
good_viewer_location
open_viewer
see_stack
see_resources
see_definitions
see_log
reset_log
inscribe
evaluate
pop clear dup swap
add sub mul div truediv modulus divmod
pm ++ -- sum product pow sqr sqrt
< <= = >= > <>
& << >>
i dupdip
!= % & * *fraction *fraction0 + ++ - -- / < << <= <> = > >= >> ? ^
abs add anamorphism and app1 app2 app3 at average
b binary branch
choice clear cleave concat cons
dinfrirst dip dipd dipdd disenstacken div divmod down_to_zero drop
dudipd dup dupd dupdip
enstacken eq
first flatten floor floordiv
gcd ge genrec getitem grand_reset gt
help
i id ifte infra inscribe
key_bindings
le least_fraction loop lshift lt
map max min mod modulus mouse_bindings mul
ne neg not nullary
of or over
pam parse pick pm pop popd popdd popop pow pred primrec product
quoted
range range_to_zero rem remainder remove reset_log rest reverse
roll< roll> rolldown rollup rshift run
second select sharing show_log shunt size sort sqr sqrt stack step
step_zero sub succ sum swaack swap swoncat swons
take ternary third times truediv truthy tuck
unary uncons unique unit unquoted unstack
void
warranty while words
x xor
zip

View File

@ -0,0 +1,85 @@
What is it?
A simple Graphical User Interface for the Joy programming language,
written using Pygame to bypass X11 et. al., modeled on the Oberon OS, and
intended to be just functional enough to support bootstrapping further Joy
development.
It's basic functionality is more-or-less as a crude text editor along with
a simple Joy runtime (interpreter, stack, and dictionary.) It auto- saves
any named files (in a versioned home directory) and you can write new Joy
primitives in Python and Joy definitions and immediately install and use
them, as well as recording them for reuse (after restarts.)
Currently, there are only two kinds of (interesting) viewers: TextViewers
and StackViewer. The TextViewers are crude text editors. They provide
just enough functionality to let the user write text and code (Python and
Joy) and execute Joy functions. One important thing they do is
automatically save their content after changes. No more lost work.
The StackViewer is a specialized TextViewer that shows the contents of the
Joy stack one line per stack item. It's a very handy visual aid to keep
track of what's going on. There's also a log.txt file that gets written
to when commands are executed, and so records the log of user actions and
system events. It tends to fill up quickly so there's a reset_log command
that clears it out.
Viewers have "grow" and "close" in their menu bars. These are buttons.
When you right-click on grow a viewer a copy is created that covers that
viewer's entire track. If you grow a viewer that already takes up its
whole track then a copy is created that takes up an additional track, up
to the whole screen. Closing a viewer just deletes that viewer, and when
a track has no more viewers, it is deleted and that exposes any previous
tracks and viewers that were hidden.
(Note: if you ever close all the viewers and are sitting at a blank screen
with nowhere to type and execute commands, press the Pause/Break key.
This will open a new "trap" viewer which you can then use to recover.)
Copies of a viewer all share the same model and update their display as it
changes. (If you have two viewers open on the same named resource and edit
one you'll see the other update as you type.)
UI Guide
left mouse sets cursor in text, in menu bar resizes viewer interactively
(this is a little buggy in that you can move the mouse quickly and get
outside the menu, leaving the viewer in the "resizing" state. Until I fix
this, the workaround is to just grab the menu bar again and wiggle it a
few pixels and let go. This will reset the machinery.)
Right mouse executes Joy command (functions), and you can drag with the
right button to highlight (well, underline) commands. Words that aren't
names of Joy commands won't be underlined. Release the button to execute
the command.
The middle mouse button (usually a wheel these days) scrolls the text but
you can also click and drag any viewer with it to move that viewer to
another track or to a different location in the same track. There's no
direct visual feedback for this (yet) but that dosen't seem to impair its
usefulness.
F1, F2 - set selection begin and end markers (crude but usable.)
F3 - copy selected text to the top of the stack.
Shift-F3 - as copy then run "parse" command on the string.
F4 - cut selected text to the top of the stack.
Shift-F4 - as cut then run "pop" (delete selection.)
Joy
Pretty much all of the rest of the functionality of the system is provided
by executing Joy commands (aka functions, aka "words" in Forth) by right-
clicking on their names in any text.
To get help on a Joy function select the name of the function in a
TextViewer using F1 and F2, then press shift-F3 to parse the selection.
The function (really its Symbol) will appear on the stack in brackets (a
"quoted program" such as "[pop]".) Then right-click on the word help in
any TextViewer (if it's not already there, just type it in somewhere.)
This will print the docstring or definition of the word (function) to
stdout. At some point I'll write a thing to send that to the log.txt file
instead, but for now look for output in the terminal.

View File

@ -0,0 +1 @@
(t.

471
joy/vui/display.py Normal file
View File

@ -0,0 +1,471 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2018 Simon Forman
#
# This file is part of joy.py
#
# joy.py is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# joy.py 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with joy.py. If not see <http://www.gnu.org/licenses/>.
#
'''
This module implements a simple visual display system modeled on Oberon.
Refer to Chapter 4 of the Project Oberon book for more information.
There is a Display object that manages a pygame surface and N vertical
tracks each of which manages zero or more viewers.
'''
from copy import copy
from sys import stderr
from traceback import format_exc
import pygame
from core import (
open_viewer_on_string,
GREY,
MOUSE_EVENTS,
)
from viewer import Viewer
import text_viewer
class Display(object):
'''
Manage tracks and viewers on a screen (Pygame surface.)
The size and number of tracks are defined by passing in at least two
ratios, e.g. Display(screen, 1, 4, 4) would create three tracks, one
small one on the left and two larger ones of the same size, each four
times wider than the left one.
All tracks take up the whole height of the display screen. Tracks
manage zero or more Viewers. When you "grow" a viewer a new track is
created that overlays or hides one or two existing tracks, and when
the last viewer in an overlay track is closed the track closes too
and reveals the hidden tracks (and their viewers, if any.)
In order to facilitate command underlining while mouse dragging the
lookup parameter must be a function that accepts a string and returns
a Boolean indicating whether that string is a valid Joy function name.
Typically you pass in the __contains__ method of the Joy dict. This
is a case of breaking "loose coupling" to gain efficiency, as otherwise
we would have to e.g. send some sort of lookup message to the
World context object, going through the whole Display.broadcast()
machinery, etc. Not something you want to do on each MOUSEMOTION
event.
'''
def __init__(self, screen, lookup, *track_ratios):
self.screen = screen
self.w, self.h = screen.get_width(), screen.get_height()
self.lookup = lookup
self.focused_viewer = None
self.tracks = [] # (x, track)
self.handlers = [] # Non-viewers that should receive messages.
# Create the tracks.
if not track_ratios: track_ratios = 1, 4
x, total = 0, sum(track_ratios)
for ratio in track_ratios[:-1]:
track_width = self.w * ratio / total
assert track_width >= 10 # minimum width 10 pixels
self._open_track(x, track_width)
x += track_width
self._open_track(x, self.w - x)
def _open_track(self, x, w):
'''Helper function to create the pygame surface and Track.'''
track_surface = self.screen.subsurface((x, 0, w, self.h))
self.tracks.append((x, Track(track_surface)))
def open_viewer(self, x, y, class_):
'''
Open a viewer of class_ at the x, y location on the display,
return the viewer.
'''
track = self._track_at(x)[0]
V = track.open_viewer(y, class_)
V.focus(self)
return V
def close_viewer(self, viewer):
'''Close the viewer.'''
for x, track in self.tracks:
if track.close_viewer(viewer):
if not track.viewers and track.hiding:
i = self.tracks.index((x, track))
self.tracks[i:i + 1] = track.hiding
assert sorted(self.tracks) == self.tracks
for _, exposed_track in track.hiding:
exposed_track.redraw()
if viewer is self.focused_viewer:
self.focused_viewer = None
break
def change_viewer(self, viewer, y, relative=False):
'''
Adjust the top of the viewer to a new y within the boundaries of
its neighbors.
If relative is False new_y should be in screen coords, else new_y
should be relative to the top of the viewer.
'''
for _, track in self.tracks:
if track.change_viewer(viewer, y, relative):
break
def grow_viewer(self, viewer):
'''
Cause the viewer to take up its whole track or, if it does
already, take up another track, up to the whole screen.
This is the inverse of closing a viewer. "Growing" a viewer
actually creates a new copy and a new track to hold it. The old
tracks and viewers are retained, and they get restored when the
covering track closes, which happens automatically when the last
viewer in the covering track is closed.
'''
for x, track in self.tracks:
for _, V in track.viewers:
if V is viewer:
return self._grow_viewer(x, track, viewer)
def _grow_viewer(self, x, track, viewer):
'''Helper function to "grow" a viewer.'''
new_viewer = None
if viewer.h < self.h:
# replace the track with a new track that contains
# a copy of the viewer at full height.
new_track = Track(track.surface) # Reuse it, why not?
new_viewer = copy(viewer)
new_track._grow_by(new_viewer, 0, self.h - viewer.h)
new_track.viewers.append((0, new_viewer))
new_track.hiding = [(x, track)]
self.tracks[self.tracks.index((x, track))] = x, new_track
elif viewer.w < self.w:
# replace two tracks
i = self.tracks.index((x, track))
try: # prefer the one on the right
xx, xtrack = self.tracks[i + 1]
except IndexError:
i -= 1 # okay, the one on the left
xx, xtrack = self.tracks[i]
hiding = [(xx, xtrack), (x, track)]
else:
hiding = [(x, track), (xx, xtrack)]
# We know there has to be at least one other track because it
# there weren't then that implies that the one track takes up
# the whole display screen (the only way you can get just one
# track is by growing a viewer to cover the whole screen.)
# Ergo, viewer.w == self.w, so this branch doesn't run.
new_x = min(x, xx)
new_w = track.w + xtrack.w
r = new_x, 0, new_w, self.h
new_track = Track(self.screen.subsurface(r))
new_viewer = copy(viewer)
r = 0, 0, new_w, self.h
new_viewer.resurface(new_track.surface.subsurface(r))
new_track.viewers.append((0, new_viewer))
new_track.hiding = hiding
self.tracks[i:i + 2] = [(new_x, new_track)]
new_viewer.draw()
return new_viewer
def _move_viewer(self, to, rel_y, viewer, _x, y):
'''
Helper function to move (really copy) a viewer to a new location.
'''
h = to.split(rel_y)
new_viewer = copy(viewer)
if not isinstance(to, Track):
to = next(T for _, T in self.tracks
for _, V in T.viewers
if V is to)
new_viewer.resurface(to.surface.subsurface((0, y, to.w, h)))
to.viewers.append((y, new_viewer))
to.viewers.sort() # bisect.insort() would be overkill here.
new_viewer.draw()
self.close_viewer(viewer)
def _track_at(self, x):
'''
Return the track at x along with the track-relative x coordinate,
raise ValueError if x is off-screen.
'''
for track_x, track in self.tracks:
if x < track_x + track.w:
return track, x - track_x
raise ValueError('x outside display: %r' % (x,))
def at(self, x, y):
'''
Return the viewer (which can be a Track) at the x, y location,
along with the relative-to-viewer-surface x and y coordinates.
If there is no viewer at the location the Track will be returned
instead.
'''
track, x = self._track_at(x)
viewer, y = track.viewer_at(y)
return viewer, x, y
def iter_viewers(self):
for x, T in self.tracks:
for y, V in T.viewers:
yield V, x, y
def done_resizing(self):
for _, track in self.tracks: # This should be done by a Message?
if track.resizing_viewer:
track.resizing_viewer.draw()
track.resizing_viewer = None
break
def broadcast(self, message):
for _, track in self.tracks:
track.broadcast(message)
for handler in self.handlers:
handler(message)
def redraw(self):
for _, track in self.tracks:
track.redraw()
def focus(self, viewer):
if isinstance(viewer, Track):
if self.focused_viewer: self.focused_viewer.unfocus()
self.focused_viewer = None
elif viewer is not self.focused_viewer:
if self.focused_viewer: self.focused_viewer.unfocus()
self.focused_viewer = viewer
viewer.focus(self)
def dispatch_event(self, event):
'''
Display event handling.
'''
try:
if event.type in {pygame.KEYUP, pygame.KEYDOWN}:
self._keyboard_event(event)
elif event.type in MOUSE_EVENTS:
self._mouse_event(event)
else:
print >> stderr, (
'received event %s Use pygame.event.set_allowed().'
% pygame.event.event_name(event.type)
)
# Catch all exceptions and open a viewer.
except:
err = format_exc()
print >> stderr, err # To be safe just print it right away.
open_viewer_on_string(self, err, self.broadcast)
def _keyboard_event(self, event):
if event.key == pygame.K_PAUSE and event.type == pygame.KEYUP:
# At least on my keyboard the break/pause key sends K_PAUSE.
# The main use of this is to open a TextViewer if you
# accidentally close all the viewers, so you can recover.
raise KeyboardInterrupt('break')
if not self.focused_viewer:
return
if event.type == pygame.KEYUP:
self.focused_viewer.key_up(self, event.key, event.mod)
elif event.type == pygame.KEYDOWN:
self.focused_viewer.key_down(
self, event.unicode, event.key, event.mod)
def _mouse_event(self, event):
V, x, y = self.at(*event.pos)
if event.type == pygame.MOUSEMOTION:
if not isinstance(V, Track):
V.mouse_motion(self, x, y, *(event.rel + event.buttons))
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
self.focus(V)
V.mouse_down(self, x, y, event.button)
else:
assert event.type == pygame.MOUSEBUTTONUP
# Check for moving viewer.
if (event.button == 2
and self.focused_viewer
and V is not self.focused_viewer
and V.MINIMUM_HEIGHT < y < V.h - self.focused_viewer.MINIMUM_HEIGHT
):
self._move_viewer(V, y, self.focused_viewer, *event.pos)
else:
V.mouse_up(self, x, y, event.button)
def init_text(self, pt, x, y, filename):
viewer = self.open_viewer(x, y, text_viewer.TextViewer)
viewer.content_id, viewer.lines = pt.open(filename)
viewer.draw()
return viewer
class Track(Viewer):
def __init__(self, surface):
Viewer.__init__(self, surface)
self.viewers = [] # (y, viewer)
self.hiding = None
self.resizing_viewer = None
self.draw()
def split(self, y):
'''
Split the Track at the y coordinate and return the height
available for a new viewer. Tracks manage a vertical strip of
the display screen so they don't resize their surface when split.
'''
h = self.viewers[0][0] if self.viewers else self.h
assert h > y
return h - y
def draw(self, rect=None):
'''Draw the track onto its surface, clearing all content.
If rect is passed only draw to that area. This supports e.g.
closing a viewer that then exposes part of the track.
'''
self.surface.fill(GREY, rect=rect)
def viewer_at(self, y):
'''
Return the viewer at y along with the viewer-relative y coordinate,
if there's no viewer at y return this track and y.
'''
for viewer_y, viewer in self.viewers:
if viewer_y < y <= viewer_y + viewer.h:
return viewer, y - viewer_y
return self, y
def open_viewer(self, y, class_):
'''Open and return a viewer of class_ at y.'''
# Todo: if y coincides with some other viewer's y replace it.
viewer, viewer_y = self.viewer_at(y)
h = viewer.split(viewer_y)
new_viewer = class_(self.surface.subsurface((0, y, self.w, h)))
new_viewer.draw()
self.viewers.append((y, new_viewer))
self.viewers.sort() # Could use bisect module but how many
# viewers will you ever have?
return new_viewer
def close_viewer(self, viewer):
'''Close the viewer, reuse the freed space.'''
for y, V in self.viewers:
if V is viewer:
self._close_viewer(y, V)
return True
return False
def _close_viewer(self, y, viewer):
'''Helper function to do the actual closing.'''
i = self.viewers.index((y, viewer))
del self.viewers[i]
if i: # The previous viewer gets the space.
previous_y, previous_viewer = self.viewers[i - 1]
self._grow_by(previous_viewer, previous_y, viewer.h)
else: # This track gets the space.
self.draw((0, y, self.w, viewer.surface.get_height()))
viewer.close()
def _grow_by(self, viewer, y, h):
'''Grow a viewer (located at y) by height h.
This might seem like it should be a method of the viewer, but
the viewer knows nothing of its own y location on the screen nor
the parent track's surface (to make a new subsurface) so it has
to be a method of the track, which has both.
'''
h = viewer.surface.get_height() + h
try:
surface = self.surface.subsurface((0, y, self.w, h))
except ValueError: # subsurface rectangle outside surface area
pass
else:
viewer.resurface(surface)
if h <= viewer.last_touch[1]: viewer.last_touch = 0, 0
viewer.draw()
def change_viewer(self, viewer, new_y, relative=False):
'''
Adjust the top of the viewer to a new y within the boundaries of
its neighbors.
If relative is False new_y should be in screen coords, else new_y
should be relative to the top of the viewer.
'''
for old_y, V in self.viewers:
if V is viewer:
if relative: new_y += old_y
if new_y != old_y: self._change_viewer(new_y, old_y, V)
return True
return False
def _change_viewer(self, new_y, old_y, viewer):
new_y = max(0, min(self.h, new_y))
i = self.viewers.index((old_y, viewer))
if new_y < old_y: # Enlarge self, shrink upper neighbor.
if i:
previous_y, previous_viewer = self.viewers[i - 1]
if new_y - previous_y < self.MINIMUM_HEIGHT:
return
previous_viewer.resizing = 1
h = previous_viewer.split(new_y - previous_y)
previous_viewer.resizing = 0
self.resizing_viewer = previous_viewer
else:
h = old_y - new_y
self._grow_by(viewer, new_y, h)
else: # Shink self, enlarge upper neighbor.
# Enforce invariant.
try:
h, _ = self.viewers[i + 1]
except IndexError: # No next viewer.
h = self.h
if h - new_y < self.MINIMUM_HEIGHT:
return
# Change the viewer and adjust the upper viewer or track.
h = new_y - old_y
self._grow_by(viewer, new_y, -h) # grow by negative height!
if i:
previous_y, previous_viewer = self.viewers[i - 1]
previous_viewer.resizing = 1
self._grow_by(previous_viewer, previous_y, h)
previous_viewer.resizing = 0
self.resizing_viewer = previous_viewer
else:
self.draw((0, old_y, self.w, h))
self.viewers[i] = new_y, viewer
# self.viewers.sort() # Not necessary, invariant holds.
assert sorted(self.viewers) == self.viewers
def broadcast(self, message):
for _, viewer in self.viewers:
if viewer is not message.sender:
viewer.handle(message)
def redraw(self):
'''Redraw the track and all of its viewers.'''
self.draw()
for _, viewer in self.viewers:
viewer.draw()

167
joy/vui/font_data.py Normal file
View File

@ -0,0 +1,167 @@
from StringIO import StringIO
import base64, zlib
def create(fn='Iosevka12.BMP'):
with open(fn, 'rb') as f:
data = f.read()
return base64.encodestring(zlib.compress(data))
data = StringIO(zlib.decompress(base64.decodestring('''\
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
lnalXc/9SsNb2vUirzS8pV0v8gJv/w/2vRht''')))
if __name__ == '__main__':
print create()

256
joy/vui/init_joy_home.py Normal file
View File

@ -0,0 +1,256 @@
'''
Utility module to help with setting up the initial contents of the
JOY_HOME directory.
These contents are kept in this Python module as a base64-encoded zip
file, so you can just do, e.g.:
import init_joy_home
init_joy_home.initialize(JOY_HOME)
'''
import base64, os, StringIO, zipfile
def initialize(joy_home):
Z.extractall(joy_home)
def create_data(from_dir='./default_joy_home'):
f = StringIO.StringIO()
z = zipfile.ZipFile(f, mode='w')
for fn in os.listdir(from_dir):
from_fn = os.path.join(from_dir, fn)
z.write(from_fn, fn)
z.close()
return base64.encodestring(f.getvalue())
Z = zipfile.ZipFile(StringIO.StringIO(base64.decodestring('''\
UEsDBBQAAAAAAORmeE794BlRfgMAAH4DAAAPAAAAZGVmaW5pdGlvbnMudHh0c2VlX3N0YWNrID09
IGdvb2Rfdmlld2VyX2xvY2F0aW9uIG9wZW5fc3RhY2sNCnNlZV9yZXNvdXJjZXMgPT0gbGlzdF9y
ZXNvdXJjZXMgZ29vZF92aWV3ZXJfbG9jYXRpb24gb3Blbl92aWV3ZXINCm9wZW5fcmVzb3VyY2Vf
YXRfZ29vZF9sb2NhdGlvbiA9PSBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQpz
ZWVfbG9nID09ICJsb2cudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9jYXRpb24NCnNlZV9k
ZWZpbml0aW9ucyA9PSAiZGVmaW5pdGlvbnMudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9j
YXRpb24NCnJvdW5kX3RvX2NlbnRzID09IDEwMCAqICsrIGZsb29yIDEwMCAvDQpyZXNldF9sb2cg
PT0gImRlbCBsb2cubGluZXNbMTpdIDsgbG9nLmF0X2xpbmUgPSAwIiBldmFsdWF0ZQ0Kc2VlX21l
bnUgPT0gIm1lbnUudHh0IiBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQoNCiMg
T3JkZXJlZCBCaW5hcnkgVHJlZSBkYXRhc3RydWN0dXJlIGZ1bmN0aW9ucy4NCkJUcmVlLW5ldyA9
PSBzd2FwIFtbXSBbXV0gY29ucyBjb25zDQogX0JUcmVlLVAgPT0gb3ZlciBbcG9wb3AgcG9wb3Ag
Zmlyc3RdIG51bGxhcnkNCiBfQlRyZWUtVD4gPT0gW2NvbnMgY29ucyBkaXBkZF0gY29ucyBjb25z
IGNvbnMgaW5mcmENCiBfQlRyZWUtVDwgPT0gW2NvbnMgY29ucyBkaXBkXSBjb25zIGNvbnMgY29u
cyBpbmZyYQ0KIF9CVHJlZS1FID09IHBvcCBzd2FwIHJvbGw8IHJlc3QgcmVzdCBjb25zIGNvbnMN
CiBfQlRyZWUtcmVjdXIgPT0gX0JUcmVlLVAgW19CVHJlZS1UPl0gW19CVHJlZS1FXSBbX0JUcmVl
LVQ8XSBjbXANCkJUcmVlLWFkZCA9PSBbcG9wb3Agbm90XSBbW3BvcF0gZGlwZCBCVHJlZS1uZXdd
IFtdIFtfQlRyZWUtcmVjdXJdIGdlbnJlYw0KUEsDBBQAAAAAAAVjpk4zTskZFBYAABQWAAAKAAAA
bGlicmFyeS5weScnJw0KVGhpcyBmaWxlIGlzIGV4ZWNmaWxlKCknZCB3aXRoIGEgbmFtZXNwYWNl
IGNvbnRhaW5pbmc6DQoNCiAgRCAtIHRoZSBKb3kgZGljdGlvbmFyeQ0KICBkIC0gdGhlIERpc3Bs
YXkgb2JqZWN0DQogIHB0IC0gdGhlIFBlcnNpc3RUYXNrIG9iamVjdA0KICBsb2cgLSB0aGUgbG9n
LnR4dCB2aWV3ZXINCiAgbG9vcCAtIHRoZSBUaGVMb29wIG1haW4gbG9vcCBvYmplY3QNCiAgc3Rh
Y2tfaG9sZGVyIC0gdGhlIFB5dGhvbiBsaXN0IG9iamVjdCB0aGF0IGhvbGRzIHRoZSBKb3kgc3Rh
Y2sgdHVwbGUNCiAgd29ybGQgLSB0aGUgSm95IGVudmlyb25tZW50DQoNCicnJw0KZnJvbSBqb3ku
bGlicmFyeSBpbXBvcnQgKA0KICAgIERlZmluaXRpb25XcmFwcGVyLA0KICAgIEZ1bmN0aW9uV3Jh
cHBlciwNCiAgICBTaW1wbGVGdW5jdGlvbldyYXBwZXIsDQogICAgKQ0KZnJvbSBqb3kudXRpbHMu
c3RhY2sgaW1wb3J0IGxpc3RfdG9fc3RhY2ssIGNvbmNhdA0KZnJvbSB2dWkgaW1wb3J0IGNvcmUs
IHRleHRfdmlld2VyLCBzdGFja192aWV3ZXINCg0KDQpkZWYgaW5zdGFsbChjb21tYW5kKTogRFtj
b21tYW5kLm5hbWVdID0gY29tbWFuZA0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFw
cGVyDQpkZWYgbGlzdF9yZXNvdXJjZXMoc3RhY2spOg0KICAgICcnJw0KICAgIFB1dCBhIHN0cmlu
ZyBvbiB0aGUgc3RhY2sgd2l0aCB0aGUgbmFtZXMgb2YgYWxsIHRoZSBrbm93biByZXNvdXJjZXMN
CiAgICBvbmUtcGVyLWxpbmUuDQogICAgJycnDQogICAgcmV0dXJuICdcbicuam9pbihwdC5zY2Fu
KCkpLCBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgb3Bl
bl9zdGFjayhzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3gg
eV0gKGluIHBpeGVscykgb3BlbiBhIFN0YWNrVmlld2VyIHRoZXJlLg0KICAgICcnJw0KICAgICh4
LCAoeSwgXykpLCBzdGFjayA9IHN0YWNrDQogICAgViA9IGQub3Blbl92aWV3ZXIoeCwgeSwgc3Rh
Y2tfdmlld2VyLlN0YWNrVmlld2VyKQ0KICAgIFYuZHJhdygpDQogICAgcmV0dXJuIHN0YWNrDQoN
Cg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBvcGVuX3Jlc291cmNlKHN0
YWNrKToNCiAgICAnJycNCiAgICBHaXZlbiBhIGNvb3JkaW5hdGUgcGFpciBbeCB5XSAoaW4gcGl4
ZWxzKSBhbmQgdGhlIG5hbWUgb2YgYSByZXNvdXJjZQ0KICAgIChmcm9tIGxpc3RfcmVzb3VyY2Vz
IGNvbW1hbmQpIG9wZW4gYSB2aWV3ZXIgb24gdGhhdCByZXNvdXJjZSBhdCB0aGF0DQogICAgbG9j
YXRpb24uDQogICAgJycnDQogICAgKCh4LCAoeSwgXykpLCAobmFtZSwgc3RhY2spKSA9IHN0YWNr
DQogICAgb20gPSBjb3JlLk9wZW5NZXNzYWdlKHdvcmxkLCBuYW1lKQ0KICAgIGQuYnJvYWRjYXN0
KG9tKQ0KICAgIGlmIG9tLnN0YXR1cyA9PSBjb3JlLlNVQ0NFU1M6DQogICAgICAgIFYgPSBkLm9w
ZW5fdmlld2VyKHgsIHksIHRleHRfdmlld2VyLlRleHRWaWV3ZXIpDQogICAgICAgIFYuY29udGVu
dF9pZCwgVi5saW5lcyA9IG9tLmNvbnRlbnRfaWQsIG9tLnRoaW5nDQogICAgICAgIFYuZHJhdygp
DQogICAgcmV0dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXIN
CmRlZiBuYW1lX3ZpZXdlcihzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBzdHJpbmcgbmFt
ZSBvbiB0aGUgc3RhY2ssIGlmIHRoZSBjdXJyZW50bHkgZm9jdXNlZCB2aWV3ZXIgaXMNCiAgICBh
bm9ueW1vdXMsIG5hbWUgdGhlIHZpZXdlciBhbmQgcGVyc2lzdCBpdCBpbiB0aGUgcmVzb3VyY2Ug
c3RvcmUgdW5kZXINCiAgICB0aGF0IG5hbWUuDQogICAgJycnDQogICAgbmFtZSwgc3RhY2sgPSBz
dGFjaw0KICAgIGFzc2VydCBpc2luc3RhbmNlKG5hbWUsIHN0ciksIHJlcHIobmFtZSkNCiAgICBp
ZiBkLmZvY3VzZWRfdmlld2VyIGFuZCBub3QgZC5mb2N1c2VkX3ZpZXdlci5jb250ZW50X2lkOg0K
ICAgICAgICBkLmZvY3VzZWRfdmlld2VyLmNvbnRlbnRfaWQgPSBuYW1lDQogICAgICAgIHBtID0g
Y29yZS5QZXJzaXN0TWVzc2FnZSh3b3JsZCwgbmFtZSwgdGhpbmc9ZC5mb2N1c2VkX3ZpZXdlci5s
aW5lcykNCiAgICAgICAgZC5icm9hZGNhc3QocG0pDQogICAgICAgIGQuZm9jdXNlZF92aWV3ZXIu
ZHJhd19tZW51KCkNCiAgICByZXR1cm4gc3RhY2sNCg0KDQojI0BpbnN0YWxsDQojI0BTaW1wbGVG
dW5jdGlvbldyYXBwZXINCiMjZGVmIHBlcnNpc3Rfdmlld2VyKHN0YWNrKToNCiMjICAgIGlmIHNl
bGYuZm9jdXNlZF92aWV3ZXI6DQojIyAgICAgICAgDQojIyAgICAgICAgc2VsZi5mb2N1c2VkX3Zp
ZXdlci5jb250ZW50X2lkID0gbmFtZQ0KIyMgICAgICAgIHNlbGYuZm9jdXNlZF92aWV3ZXIuZHJh
d19tZW51KCkNCiMjICAgIHJldHVybiBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rp
b25XcmFwcGVyDQpkZWYgaW5zY3JpYmUoc3RhY2spOg0KICAgICcnJw0KICAgIENyZWF0ZSBhIG5l
dyBKb3kgZnVuY3Rpb24gZGVmaW5pdGlvbiBpbiB0aGUgSm95IGRpY3Rpb25hcnkuICBBDQogICAg
ZGVmaW5pdGlvbiBpcyBnaXZlbiBhcyBhIHN0cmluZyB3aXRoIGEgbmFtZSBmb2xsb3dlZCBieSBh
IGRvdWJsZQ0KICAgIGVxdWFsIHNpZ24gdGhlbiBvbmUgb3IgbW9yZSBKb3kgZnVuY3Rpb25zLCB0
aGUgYm9keS4gZm9yIGV4YW1wbGU6DQoNCiAgICAgICAgc3FyID09IGR1cCBtdWwNCg0KICAgIElm
IHlvdSB3YW50IHRoZSBkZWZpbml0aW9uIHRvIHBlcnNpc3Qgb3ZlciByZXN0YXJ0cywgZW50ZXIg
aXQgaW50bw0KICAgIHRoZSBkZWZpbml0aW9ucy50eHQgcmVzb3VyY2UuDQogICAgJycnDQogICAg
ZGVmaW5pdGlvbiwgc3RhY2sgPSBzdGFjaw0KICAgIERlZmluaXRpb25XcmFwcGVyLmFkZF9kZWYo
ZGVmaW5pdGlvbiwgRCkNCiAgICByZXR1cm4gc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1
bmN0aW9uV3JhcHBlcg0KZGVmIG9wZW5fdmlld2VyKHN0YWNrKToNCiAgICAnJycNCiAgICBHaXZl
biBhIGNvb3JkaW5hdGUgcGFpciBbeCB5XSAoaW4gcGl4ZWxzKSBhbmQgYSBzdHJpbmcsIG9wZW4g
YSBuZXcNCiAgICB1bm5hbWVkIHZpZXdlciBvbiB0aGF0IHN0cmluZyBhdCB0aGF0IGxvY2F0aW9u
Lg0KICAgICcnJw0KICAgICgoeCwgKHksIF8pKSwgKGNvbnRlbnQsIHN0YWNrKSkgPSBzdGFjaw0K
ICAgIFYgPSBkLm9wZW5fdmlld2VyKHgsIHksIHRleHRfdmlld2VyLlRleHRWaWV3ZXIpDQogICAg
Vi5saW5lcyA9IGNvbnRlbnQuc3BsaXRsaW5lcygpDQogICAgVi5kcmF3KCkNCiAgICByZXR1cm4g
c3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVmIGdvb2Rfdmll
d2VyX2xvY2F0aW9uKHN0YWNrKToNCiAgICAnJycNCiAgICBMZWF2ZSBhIGNvb3JkaW5hdGUgcGFp
ciBbeCB5XSAoaW4gcGl4ZWxzKSBvbiB0aGUgc3RhY2sgdGhhdCB3b3VsZA0KICAgIGJlIGEgZ29v
ZCBsb2NhdGlvbiBhdCB3aGljaCB0byBvcGVuIGEgbmV3IHZpZXdlci4gIChUaGUgaGV1cmlzdGlj
DQogICAgZW1wbG95ZWQgaXMgdG8gdGFrZSB1cCB0aGUgYm90dG9tIGhhbGYgb2YgdGhlIGN1cnJl
bnRseSBvcGVuIHZpZXdlcg0KICAgIHdpdGggdGhlIGdyZWF0ZXN0IGFyZWEuKQ0KICAgICcnJw0K
ICAgIHZpZXdlcnMgPSBsaXN0KGQuaXRlcl92aWV3ZXJzKCkpDQogICAgaWYgdmlld2VyczoNCiAg
ICAgICAgdmlld2Vycy5zb3J0KGtleT1sYW1iZGEgKFYsIHgsIHkpOiBWLncgKiBWLmgpDQogICAg
ICAgIFYsIHgsIHkgPSB2aWV3ZXJzWy0xXQ0KICAgICAgICBjb29yZHMgPSAoeCArIDEsICh5ICsg
Vi5oIC8gMiwgKCkpKQ0KICAgIGVsc2U6DQogICAgICAgIGNvb3JkcyA9ICgwLCAoMCwgKCkpKQ0K
ICAgIHJldHVybiBjb29yZHMsIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBGdW5jdGlvbldyYXBwZXIN
CmRlZiBjbXBfKHN0YWNrLCBleHByZXNzaW9uLCBkaWN0aW9uYXJ5KToNCiAgICAnJycNCiAgICBU
aGUgY21wIGNvbWJpbmF0b3IgdGFrZXMgdHdvIHZhbHVlcyBhbmQgdGhyZWUgcXVvdGVkIHByb2dy
YW1zIG9uIHRoZQ0KICAgIHN0YWNrIGFuZCBydW5zIG9uZSBvZiB0aGUgdGhyZWUgZGVwZW5kaW5n
IG9uIHRoZSByZXN1bHRzIG9mIGNvbXBhcmluZw0KICAgIHRoZSB0d28gdmFsdWVzOg0KDQogICAg
ICAgICAgIGEgYiBbR10gW0VdIFtMXSBjbXANCiAgICAgICAgLS0tLS0tLS0tLS0tLS0tLS0tLS0t
LS0tLSBhID4gYg0KICAgICAgICAgICAgICAgIEcNCg0KICAgICAgICAgICBhIGIgW0ddIFtFXSBb
TF0gY21wDQogICAgICAgIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gYSA9IGINCiAgICAgICAg
ICAgICAgICAgICAgRQ0KDQogICAgICAgICAgIGEgYiBbR10gW0VdIFtMXSBjbXANCiAgICAgICAg
LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSBhIDwgYg0KICAgICAgICAgICAgICAgICAgICAgICAg
TA0KDQogICAgJycnDQogICAgTCwgKEUsIChHLCAoYiwgKGEsIHN0YWNrKSkpKSA9IHN0YWNrDQog
ICAgZXhwcmVzc2lvbiA9IGNvbmNhdChHIGlmIGEgPiBiIGVsc2UgTCBpZiBhIDwgYiBlbHNlIEUs
IGV4cHJlc3Npb24pDQogICAgcmV0dXJuIHN0YWNrLCBleHByZXNzaW9uLCBkaWN0aW9uYXJ5DQoN
Cg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBsaXN0X3ZpZXdlcnMoc3Rh
Y2spOg0KICAgICcnJw0KICAgIFB1dCBhIHN0cmluZyBvbiB0aGUgc3RhY2sgd2l0aCBzb21lIGlu
Zm9ybWF0aW9uIGFib3V0IHRoZSBjdXJyZW50bHkNCiAgICBvcGVuIHZpZXdlcnMsIG9uZS1wZXIt
bGluZS4gIFRoaXMgaXMga2luZCBvZiBhIGRlbW8gZnVuY3Rpb24sIHJhdGhlcg0KICAgIHRoYW4g
c29tZXRoaW5nIHJlYWxseSB1c2VmdWwuDQogICAgJycnDQogICAgbGluZXMgPSBbXQ0KICAgIGZv
ciB4LCBUIGluIGQudHJhY2tzOg0KICAgICAgICAjbGluZXMuYXBwZW5kKCd4OiAlaSwgdzogJWks
ICVyJyAlICh4LCBULncsIFQpKQ0KICAgICAgICBmb3IgeSwgViBpbiBULnZpZXdlcnM6DQogICAg
ICAgICAgICBsaW5lcy5hcHBlbmQoJ3g6ICVpIHk6ICVpIGg6ICVpICVyICVyJyAlICh4LCB5LCBW
LmgsIFYuY29udGVudF9pZCwgVikpDQogICAgcmV0dXJuICdcbicuam9pbihsaW5lcyksIHN0YWNr
DQoNCg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBzcGxpdGxpbmVzKHN0
YWNrKToNCiAgICAnJycNCiAgICBHaXZlbiBhIHN0cmluZyBvbiB0aGUgc3RhY2sgcmVwbGFjZSBp
dCB3aXRoIGEgbGlzdCBvZiB0aGUgbGluZXMgaW4NCiAgICB0aGUgc3RyaW5nLg0KICAgICcnJw0K
ICAgIHRleHQsIHN0YWNrID0gc3RhY2sNCiAgICBhc3NlcnQgaXNpbnN0YW5jZSh0ZXh0LCBzdHIp
LCByZXByKHRleHQpDQogICAgcmV0dXJuIGxpc3RfdG9fc3RhY2sodGV4dC5zcGxpdGxpbmVzKCkp
LCBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgaGl5YShz
dGFjayk6DQogICAgJycnDQogICAgRGVtbyBmdW5jdGlvbiB0byBpbnNlcnQgIkhpIFdvcmxkISIg
aW50byB0aGUgY3VycmVudCB2aWV3ZXIsIGlmIGFueS4NCiAgICAnJycNCiAgICBpZiBkLmZvY3Vz
ZWRfdmlld2VyOg0KICAgICAgICBkLmZvY3VzZWRfdmlld2VyLmluc2VydCgnSGkgV29ybGQhJykN
CiAgICByZXR1cm4gc3RhY2sNClBLAwQUAAAAAADkZnhORezk1gsAAAALAAAABwAAAGxvZy50eHRK
b3lweSBsb2cNClBLAwQUAAAAAADkZnhOZ/a7zQIFAAACBQAACAAAAG1lbnUudHh0ICBuYW1lX3Zp
ZXdlcg0KICBsaXN0X3Jlc291cmNlcw0KICBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9jYXRpb24N
CiAgZ29vZF92aWV3ZXJfbG9jYXRpb24NCiAgb3Blbl92aWV3ZXINCiAgc2VlX3N0YWNrDQogIHNl
ZV9yZXNvdXJjZXMNCiAgc2VlX2RlZmluaXRpb25zDQogIHNlZV9sb2cNCiAgcmVzZXRfbG9nDQoN
CiAgaW5zY3JpYmUNCiAgZXZhbHVhdGUNCg0KICBwb3AgY2xlYXIgICAgZHVwIHN3YXANCg0KICBh
ZGQgc3ViIG11bCBkaXYgdHJ1ZWRpdiBtb2R1bHVzIGRpdm1vZA0KICBwbSArKyAtLSBzdW0gcHJv
ZHVjdCBwb3cgc3FyIHNxcnQNCiAgPCA8PSA9ID49ID4gPD4NCiAgJiA8PCA+Pg0KDQogIGkgZHVw
ZGlwDQoNCiE9ICUgJiAqICpmcmFjdGlvbiAqZnJhY3Rpb24wICsgKysgLSAtLSAvIDwgPDwgPD0g
PD4gPSA+ID49ID4+ID8gXg0KYWJzIGFkZCBhbmFtb3JwaGlzbSBhbmQgYXBwMSBhcHAyIGFwcDMg
YXQgYXZlcmFnZQ0KYiBiaW5hcnkgYnJhbmNoDQpjaG9pY2UgY2xlYXIgY2xlYXZlIGNvbmNhdCBj
b25zDQpkaW5mcmlyc3QgZGlwIGRpcGQgZGlwZGQgZGlzZW5zdGFja2VuIGRpdiBkaXZtb2QgZG93
bl90b196ZXJvIGRyb3ANCmR1ZGlwZCBkdXAgZHVwZCBkdXBkaXANCmVuc3RhY2tlbiBlcQ0KZmly
c3QgZmxhdHRlbiBmbG9vciBmbG9vcmRpdg0KZ2NkIGdlIGdlbnJlYyBnZXRpdGVtIGdyYW5kX3Jl
c2V0IGd0DQpoZWxwDQppIGlkIGlmdGUgaW5mcmEgaW5zY3JpYmUNCmtleV9iaW5kaW5ncw0KbGUg
bGVhc3RfZnJhY3Rpb24gbG9vcCBsc2hpZnQgbHQNCm1hcCBtYXggbWluIG1vZCBtb2R1bHVzIG1v
dXNlX2JpbmRpbmdzIG11bA0KbmUgbmVnIG5vdCBudWxsYXJ5DQpvZiBvciBvdmVyDQpwYW0gcGFy
c2UgcGljayBwbSBwb3AgcG9wZCBwb3BkZCBwb3BvcCBwb3cgcHJlZCBwcmltcmVjIHByb2R1Y3QN
CnF1b3RlZA0KcmFuZ2UgcmFuZ2VfdG9femVybyByZW0gcmVtYWluZGVyIHJlbW92ZSByZXNldF9s
b2cgcmVzdCByZXZlcnNlDQpyb2xsPCByb2xsPiByb2xsZG93biByb2xsdXAgcnNoaWZ0IHJ1bg0K
c2Vjb25kIHNlbGVjdCBzaGFyaW5nIHNob3dfbG9nIHNodW50IHNpemUgc29ydCBzcXIgc3FydCBz
dGFjayBzdGVwDQpzdGVwX3plcm8gc3ViIHN1Y2Mgc3VtIHN3YWFjayBzd2FwIHN3b25jYXQgc3dv
bnMNCnRha2UgdGVybmFyeSB0aGlyZCB0aW1lcyB0cnVlZGl2IHRydXRoeSB0dWNrDQp1bmFyeSB1
bmNvbnMgdW5pcXVlIHVuaXQgdW5xdW90ZWQgdW5zdGFjaw0Kdm9pZA0Kd2FycmFudHkgd2hpbGUg
d29yZHMNCnggeG9yDQp6aXANClBLAwQUAAAAAADkZnhOAKs9xo4QAACOEAAACwAAAHNjcmF0Y2gu
dHh0V2hhdCBpcyBpdD8NCg0KQSBzaW1wbGUgR3JhcGhpY2FsIFVzZXIgSW50ZXJmYWNlIGZvciB0
aGUgSm95IHByb2dyYW1taW5nIGxhbmd1YWdlLA0Kd3JpdHRlbiB1c2luZyBQeWdhbWUgdG8gYnlw
YXNzIFgxMSBldC4gYWwuLCBtb2RlbGVkIG9uIHRoZSBPYmVyb24gT1MsIGFuZA0KaW50ZW5kZWQg
dG8gYmUganVzdCBmdW5jdGlvbmFsIGVub3VnaCB0byBzdXBwb3J0IGJvb3RzdHJhcHBpbmcgZnVy
dGhlciBKb3kNCmRldmVsb3BtZW50Lg0KDQpJdCdzIGJhc2ljIGZ1bmN0aW9uYWxpdHkgaXMgbW9y
ZS1vci1sZXNzIGFzIGEgY3J1ZGUgdGV4dCBlZGl0b3IgYWxvbmcgd2l0aA0KYSBzaW1wbGUgSm95
IHJ1bnRpbWUgKGludGVycHJldGVyLCBzdGFjaywgYW5kIGRpY3Rpb25hcnkuKSAgSXQgYXV0by0g
c2F2ZXMNCmFueSBuYW1lZCBmaWxlcyAoaW4gYSB2ZXJzaW9uZWQgaG9tZSBkaXJlY3RvcnkpIGFu
ZCB5b3UgY2FuIHdyaXRlIG5ldyBKb3kNCnByaW1pdGl2ZXMgaW4gUHl0aG9uIGFuZCBKb3kgZGVm
aW5pdGlvbnMgYW5kIGltbWVkaWF0ZWx5IGluc3RhbGwgYW5kIHVzZQ0KdGhlbSwgYXMgd2VsbCBh
cyByZWNvcmRpbmcgdGhlbSBmb3IgcmV1c2UgKGFmdGVyIHJlc3RhcnRzLikNCg0KQ3VycmVudGx5
LCB0aGVyZSBhcmUgb25seSB0d28ga2luZHMgb2YgKGludGVyZXN0aW5nKSB2aWV3ZXJzOiBUZXh0
Vmlld2Vycw0KYW5kIFN0YWNrVmlld2VyLiBUaGUgVGV4dFZpZXdlcnMgYXJlIGNydWRlIHRleHQg
ZWRpdG9ycy4gIFRoZXkgcHJvdmlkZQ0KanVzdCBlbm91Z2ggZnVuY3Rpb25hbGl0eSB0byBsZXQg
dGhlIHVzZXIgd3JpdGUgdGV4dCBhbmQgY29kZSAoUHl0aG9uIGFuZA0KSm95KSBhbmQgZXhlY3V0
ZSBKb3kgZnVuY3Rpb25zLiAgT25lIGltcG9ydGFudCB0aGluZyB0aGV5IGRvIGlzDQphdXRvbWF0
aWNhbGx5IHNhdmUgdGhlaXIgY29udGVudCBhZnRlciBjaGFuZ2VzLiAgTm8gbW9yZSBsb3N0IHdv
cmsuDQoNClRoZSBTdGFja1ZpZXdlciBpcyBhIHNwZWNpYWxpemVkIFRleHRWaWV3ZXIgdGhhdCBz
aG93cyB0aGUgY29udGVudHMgb2YgdGhlDQpKb3kgc3RhY2sgb25lIGxpbmUgcGVyIHN0YWNrIGl0
ZW0uICBJdCdzIGEgdmVyeSBoYW5keSB2aXN1YWwgYWlkIHRvIGtlZXANCnRyYWNrIG9mIHdoYXQn
cyBnb2luZyBvbi4gIFRoZXJlJ3MgYWxzbyBhIGxvZy50eHQgZmlsZSB0aGF0IGdldHMgd3JpdHRl
bg0KdG8gd2hlbiBjb21tYW5kcyBhcmUgZXhlY3V0ZWQsIGFuZCBzbyByZWNvcmRzIHRoZSBsb2cg
b2YgdXNlciBhY3Rpb25zIGFuZA0Kc3lzdGVtIGV2ZW50cy4gIEl0IHRlbmRzIHRvIGZpbGwgdXAg
cXVpY2tseSBzbyB0aGVyZSdzIGEgcmVzZXRfbG9nIGNvbW1hbmQNCnRoYXQgY2xlYXJzIGl0IG91
dC4NCg0KVmlld2VycyBoYXZlICJncm93IiBhbmQgImNsb3NlIiBpbiB0aGVpciBtZW51IGJhcnMu
ICBUaGVzZSBhcmUgYnV0dG9ucy4NCldoZW4geW91IHJpZ2h0LWNsaWNrIG9uIGdyb3cgYSB2aWV3
ZXIgYSBjb3B5IGlzIGNyZWF0ZWQgdGhhdCBjb3ZlcnMgdGhhdA0Kdmlld2VyJ3MgZW50aXJlIHRy
YWNrLiAgSWYgeW91IGdyb3cgYSB2aWV3ZXIgdGhhdCBhbHJlYWR5IHRha2VzIHVwIGl0cw0Kd2hv
bGUgdHJhY2sgdGhlbiBhIGNvcHkgaXMgY3JlYXRlZCB0aGF0IHRha2VzIHVwIGFuIGFkZGl0aW9u
YWwgdHJhY2ssIHVwDQp0byB0aGUgd2hvbGUgc2NyZWVuLiAgQ2xvc2luZyBhIHZpZXdlciBqdXN0
IGRlbGV0ZXMgdGhhdCB2aWV3ZXIsIGFuZCB3aGVuDQphIHRyYWNrIGhhcyBubyBtb3JlIHZpZXdl
cnMsIGl0IGlzIGRlbGV0ZWQgYW5kIHRoYXQgZXhwb3NlcyBhbnkgcHJldmlvdXMNCnRyYWNrcyBh
bmQgdmlld2VycyB0aGF0IHdlcmUgaGlkZGVuLg0KDQooTm90ZTogaWYgeW91IGV2ZXIgY2xvc2Ug
YWxsIHRoZSB2aWV3ZXJzIGFuZCBhcmUgc2l0dGluZyBhdCBhIGJsYW5rIHNjcmVlbg0Kd2l0aCAg
bm93aGVyZSB0byB0eXBlIGFuZCBleGVjdXRlIGNvbW1hbmRzLCBwcmVzcyB0aGUgUGF1c2UvQnJl
YWsga2V5Lg0KVGhpcyB3aWxsIG9wZW4gYSBuZXcgInRyYXAiIHZpZXdlciB3aGljaCB5b3UgY2Fu
IHRoZW4gdXNlIHRvIHJlY292ZXIuKQ0KDQpDb3BpZXMgb2YgYSB2aWV3ZXIgYWxsIHNoYXJlIHRo
ZSBzYW1lIG1vZGVsIGFuZCB1cGRhdGUgdGhlaXIgZGlzcGxheSBhcyBpdA0KY2hhbmdlcy4gKElm
IHlvdSBoYXZlIHR3byB2aWV3ZXJzIG9wZW4gb24gdGhlIHNhbWUgbmFtZWQgcmVzb3VyY2UgYW5k
IGVkaXQNCm9uZSB5b3UnbGwgc2VlIHRoZSBvdGhlciB1cGRhdGUgYXMgeW91IHR5cGUuKQ0KDQpV
SSBHdWlkZQ0KDQpsZWZ0IG1vdXNlIHNldHMgY3Vyc29yIGluIHRleHQsIGluIG1lbnUgYmFyIHJl
c2l6ZXMgdmlld2VyIGludGVyYWN0aXZlbHkNCih0aGlzIGlzIGEgbGl0dGxlIGJ1Z2d5IGluIHRo
YXQgeW91IGNhbiBtb3ZlIHRoZSBtb3VzZSBxdWlja2x5IGFuZCBnZXQNCm91dHNpZGUgdGhlIG1l
bnUsIGxlYXZpbmcgdGhlIHZpZXdlciBpbiB0aGUgInJlc2l6aW5nIiBzdGF0ZS4gVW50aWwgSSBm
aXgNCnRoaXMsIHRoZSB3b3JrYXJvdW5kIGlzIHRvIGp1c3QgZ3JhYiB0aGUgbWVudSBiYXIgYWdh
aW4gYW5kIHdpZ2dsZSBpdCBhDQpmZXcgcGl4ZWxzIGFuZCBsZXQgZ28uICBUaGlzIHdpbGwgcmVz
ZXQgdGhlIG1hY2hpbmVyeS4pDQoNClJpZ2h0IG1vdXNlIGV4ZWN1dGVzIEpveSBjb21tYW5kIChm
dW5jdGlvbnMpLCBhbmQgeW91IGNhbiBkcmFnIHdpdGggdGhlDQpyaWdodCBidXR0b24gdG8gaGln
aGxpZ2h0ICh3ZWxsLCB1bmRlcmxpbmUpIGNvbW1hbmRzLiAgV29yZHMgdGhhdCBhcmVuJ3QNCm5h
bWVzIG9mIEpveSBjb21tYW5kcyB3b24ndCBiZSB1bmRlcmxpbmVkLiAgUmVsZWFzZSB0aGUgYnV0
dG9uIHRvIGV4ZWN1dGUNCnRoZSBjb21tYW5kLg0KDQpUaGUgbWlkZGxlIG1vdXNlIGJ1dHRvbiAo
dXN1YWxseSBhIHdoZWVsIHRoZXNlIGRheXMpIHNjcm9sbHMgdGhlIHRleHQgYnV0DQp5b3UgY2Fu
IGFsc28gY2xpY2sgYW5kIGRyYWcgYW55IHZpZXdlciB3aXRoIGl0IHRvIG1vdmUgdGhhdCB2aWV3
ZXIgdG8NCmFub3RoZXIgdHJhY2sgb3IgdG8gYSBkaWZmZXJlbnQgbG9jYXRpb24gaW4gdGhlIHNh
bWUgdHJhY2suICBUaGVyZSdzIG5vDQpkaXJlY3QgdmlzdWFsIGZlZWRiYWNrIGZvciB0aGlzICh5
ZXQpIGJ1dCB0aGF0IGRvc2VuJ3Qgc2VlbSB0byBpbXBhaXIgaXRzDQp1c2VmdWxuZXNzLg0KDQpG
MSwgRjIgLSBzZXQgc2VsZWN0aW9uIGJlZ2luIGFuZCBlbmQgbWFya2VycyAoY3J1ZGUgYnV0IHVz
YWJsZS4pDQoNCkYzIC0gY29weSBzZWxlY3RlZCB0ZXh0IHRvIHRoZSB0b3Agb2YgdGhlIHN0YWNr
Lg0KDQpTaGlmdC1GMyAtIGFzIGNvcHkgdGhlbiBydW4gInBhcnNlIiBjb21tYW5kIG9uIHRoZSBz
dHJpbmcuDQoNCkY0IC0gY3V0IHNlbGVjdGVkIHRleHQgdG8gdGhlIHRvcCBvZiB0aGUgc3RhY2su
DQoNClNoaWZ0LUY0IC0gYXMgY3V0IHRoZW4gcnVuICJwb3AiIChkZWxldGUgc2VsZWN0aW9uLikN
Cg0KSm95DQoNClByZXR0eSBtdWNoIGFsbCBvZiB0aGUgcmVzdCBvZiB0aGUgZnVuY3Rpb25hbGl0
eSBvZiB0aGUgc3lzdGVtIGlzIHByb3ZpZGVkDQpieSBleGVjdXRpbmcgSm95IGNvbW1hbmRzIChh
a2EgZnVuY3Rpb25zLCBha2EgIndvcmRzIiBpbiBGb3J0aCkgYnkgcmlnaHQtDQpjbGlja2luZyBv
biB0aGVpciBuYW1lcyBpbiBhbnkgdGV4dC4NCg0KVG8gZ2V0IGhlbHAgb24gYSBKb3kgZnVuY3Rp
b24gc2VsZWN0IHRoZSBuYW1lIG9mIHRoZSBmdW5jdGlvbiBpbiBhDQpUZXh0Vmlld2VyIHVzaW5n
IEYxIGFuZCBGMiwgdGhlbiBwcmVzcyBzaGlmdC1GMyB0byBwYXJzZSB0aGUgc2VsZWN0aW9uLg0K
VGhlIGZ1bmN0aW9uIChyZWFsbHkgaXRzIFN5bWJvbCkgd2lsbCBhcHBlYXIgb24gdGhlIHN0YWNr
IGluIGJyYWNrZXRzIChhDQoicXVvdGVkIHByb2dyYW0iIHN1Y2ggYXMgIltwb3BdIi4pICBUaGVu
IHJpZ2h0LWNsaWNrIG9uIHRoZSB3b3JkIGhlbHAgaW4NCmFueSBUZXh0Vmlld2VyIChpZiBpdCdz
IG5vdCBhbHJlYWR5IHRoZXJlLCBqdXN0IHR5cGUgaXQgaW4gc29tZXdoZXJlLikNClRoaXMgd2ls
bCBwcmludCB0aGUgZG9jc3RyaW5nIG9yIGRlZmluaXRpb24gb2YgdGhlIHdvcmQgKGZ1bmN0aW9u
KSB0bw0Kc3Rkb3V0LiAgQXQgc29tZSBwb2ludCBJJ2xsIHdyaXRlIGEgdGhpbmcgdG8gc2VuZCB0
aGF0IHRvIHRoZSBsb2cudHh0IGZpbGUNCmluc3RlYWQsIGJ1dCBmb3Igbm93IGxvb2sgZm9yIG91
dHB1dCBpbiB0aGUgdGVybWluYWwuDQpQSwMEFAAAAAAA5GZ4Tnd/ml4DAAAAAwAAAAwAAABzdGFj
ay5waWNrbGUodC5QSwECFAAUAAAAAADkZnhO/eAZUX4DAAB+AwAADwAAAAAAAAAAAAAAtoEAAAAA
ZGVmaW5pdGlvbnMudHh0UEsBAhQAFAAAAAAABWOmTjNOyRkUFgAAFBYAAAoAAAAAAAAAAAAAALaB
qwMAAGxpYnJhcnkucHlQSwECFAAUAAAAAADkZnhORezk1gsAAAALAAAABwAAAAAAAAAAAAAAtoHn
GQAAbG9nLnR4dFBLAQIUABQAAAAAAORmeE5n9rvNAgUAAAIFAAAIAAAAAAAAAAAAAAC2gRcaAABt
ZW51LnR4dFBLAQIUABQAAAAAAORmeE4Aqz3GjhAAAI4QAAALAAAAAAAAAAAAAAC2gT8fAABzY3Jh
dGNoLnR4dFBLAQIUABQAAAAAAORmeE53f5peAwAAAAMAAAAMAAAAAAAAAAAAAAC2gfYvAABzdGFj
ay5waWNrbGVQSwUGAAAAAAYABgBTAQAAIzAAAAAA''')))
if __name__ == '__main__':
print create_data()

113
joy/vui/main.py Normal file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env python
import os, sys, traceback
import pygame
from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper
import core, display, persist_task
FULLSCREEN = '-f' in sys.argv
JOY_HOME = os.environ.get('JOY_HOME')
if JOY_HOME is None:
JOY_HOME = os.path.expanduser('~/.joypy')
if not os.path.isabs(JOY_HOME):
raise ValueError('what directory?')
def load_definitions(pt, dictionary):
lines = pt.open('definitions.txt')[1]
for line in lines:
if '==' in line:
DefinitionWrapper.add_def(line, dictionary)
def load_primitives(home, name_space):
fn = os.path.join(home, 'library.py')
if os.path.exists(fn):
execfile(fn, name_space)
def init():
print 'Initializing Pygame...'
pygame.init()
print 'Creating window...'
if FULLSCREEN:
screen = pygame.display.set_mode()
else:
screen = pygame.display.set_mode((1024, 768))
clock = pygame.time.Clock()
pygame.event.set_allowed(None)
pygame.event.set_allowed(core.ALLOWED_EVENTS)
pt = persist_task.PersistTask(JOY_HOME)
return screen, clock, pt
def init_context(screen, clock, pt):
D = initialize()
d = display.Display(
screen,
D.__contains__,
*((144 - 89, 144, 89) if FULLSCREEN else (89, 144))
)
log = d.init_text(pt, 0, 0, 'log.txt')
tho = d.init_text(pt, 0, d.h / 3, 'menu.txt')
t = d.init_text(pt, d.w / 2, 0, 'scratch.txt')
loop = core.TheLoop(d, clock)
stack_id, stack_holder = pt.open('stack.pickle')
world = core.World(stack_id, stack_holder, D, d.broadcast, log)
loop.install_task(pt.task_run, 10000) # save files every ten seconds
d.handlers.append(pt.handle)
d.handlers.append(world.handle)
load_definitions(pt, D)
return locals()
def error_guard(loop, n=10):
error_count = 0
while error_count < n:
try:
loop()
break
except:
traceback.print_exc(file=sys.stderr)
error_count += 1
class FileFaker(object):
def __init__(self, log):
self.log = log
def write(self, text):
self.log.append(text)
def flush(self):
pass
def main(screen, clock, pt):
name_space = init_context(screen, clock, pt)
load_primitives(pt.home, name_space.copy())
@SimpleFunctionWrapper
def evaluate(stack):
'''Evaluate the Python code text on the top of the stack.'''
code, stack = stack
exec code in name_space.copy()
return stack
name_space['D']['evaluate'] = evaluate
sys.stdout, old_stdout = FileFaker(name_space['log']), sys.stdout
try:
error_guard(name_space['loop'].loop)
finally:
sys.stdout = old_stdout
return name_space['d']
if __name__ == '__main__':
main(*init())

185
joy/vui/persist_task.py Normal file
View File

@ -0,0 +1,185 @@
import os, pickle, traceback
from collections import Counter
from dulwich.errors import NotGitRepository
from dulwich.repo import Repo
import core, init_joy_home
def open_repo(repo_dir=None, initialize=False):
if not os.path.exists(repo_dir):
os.makedirs(repo_dir, 0700)
return init_repo(repo_dir)
try:
return Repo(repo_dir)
except NotGitRepository:
if initialize:
return init_repo(repo_dir)
raise
def init_repo(repo_dir):
repo = Repo.init(repo_dir)
init_joy_home.initialize(repo_dir)
repo.stage([
fn
for fn in os.listdir(repo_dir)
if os.path.isfile(os.path.join(repo_dir, fn))
])
repo.do_commit('Initial commit.', committer=core.COMMITTER)
return repo
def make_repo_relative_path_maker(repo):
c = repo.controldir()
def repo_relative_path(path):
return os.path.relpath(path, os.path.commonprefix((c, path)))
return repo_relative_path
class Resource(object):
def __init__(self, filename, repo_relative_filename, thing=None):
self.filename = filename
self.repo_relative_filename = repo_relative_filename
self.thing = thing or self._from_file(open(filename))
def _from_file(self, f):
return f.read().splitlines()
def _to_file(self, f):
for line in self.thing:
print >> f, line
def persist(self, repo):
with open(self.filename, 'w') as f:
os.chmod(self.filename, 0600)
self._to_file(f)
f.flush()
os.fsync(f.fileno())
# For goodness's sake, write it to the disk already!
repo.stage([self.repo_relative_filename])
class PickledResource(Resource):
def _from_file(self, f):
return [pickle.load(f)]
def _to_file(self, f):
pickle.dump(self.thing[0], f)
class PersistTask(object):
LIMIT = 10
MAX_SAVE = 10
def __init__(self, home):
self.home = home
self.repo = open_repo(home)
self._r = make_repo_relative_path_maker(self.repo)
self.counter = Counter()
self.store = {}
def open(self, name):
# look up the file in home and get its data
fn = os.path.join(self.home, name)
content_id = name # hash(fn)
try:
resource = self.store[content_id]
except KeyError:
R = PickledResource if name.endswith('.pickle') else Resource
resource = self.store[content_id] = R(fn, self._r(fn))
return content_id, resource.thing
def handle(self, message):
if isinstance(message, core.OpenMessage):
self.handle_open(message)
elif isinstance(message, core.ModifyMessage):
self.handle_modify(message)
elif isinstance(message, core.PersistMessage):
self.handle_persist(message)
elif isinstance(message, core.ShutdownMessage):
for content_id in self.counter:
self.store[content_id].persist(self.repo)
self.commit('shutdown')
def handle_open(self, message):
try:
message.content_id, message.thing = self.open(message.name)
except:
message.traceback = traceback.format_exc()
message.status = core.ERROR
else:
message.status = core.SUCCESS
def handle_modify(self, message):
try:
content_id = message.details['content_id']
except KeyError:
return
if not content_id:
return
self.counter[content_id] += 1
if self.counter[content_id] > self.LIMIT:
self.persist(content_id)
self.commit('due to activity')
def handle_persist(self, message):
try:
resource = self.store[message.content_id]
except KeyError:
resource = self.handle_persist_new(message)
resource.persist(self.repo)
self.commit('by request from %r' % (message.sender,))
def handle_persist_new(self, message):
name = message.content_id
check_filename(name)
fn = os.path.join(self.home, name)
thing = message.details['thing']
R = PickledResource if name.endswith('.pickle') else Resource # !!! refactor!
resource = self.store[name] = R(fn, self._r(fn), thing)
return resource
def persist(self, content_id):
del self.counter[content_id]
self.store[content_id].persist(self.repo)
def task_run(self):
if not self.counter:
return
for content_id, _ in self.counter.most_common(self.MAX_SAVE):
self.persist(content_id)
self.commit()
def commit(self, message='auto-commit'):
return self.repo.do_commit(message, committer=core.COMMITTER)
def scan(self):
return sorted([
fn
for fn in os.listdir(self.home)
if os.path.isfile(os.path.join(self.home, fn))
])
def check_filename(name):
# TODO: improve this...
if len(name) > 64:
raise ValueError('bad name %r' % (name,))
left, dot, right = name.partition('.')
if not left.isalnum() or dot and not right.isalnum():
raise ValueError('bad name %r' % (name,))
if __name__ == '__main__':
JOY_HOME = os.path.expanduser('~/.joypy')
pt = PersistTask(JOY_HOME)
content_id, thing = pt.open('stack.pickle')
pt.persist(content_id)
print pt.counter
mm = core.ModifyMessage(None, None, content_id=content_id)
pt.handle(mm)
print pt.counter

50
joy/vui/stack_viewer.py Normal file
View File

@ -0,0 +1,50 @@
from joy.utils.stack import expression_to_string, iter_stack
import core, text_viewer
MAX_WIDTH = 64
def fsi(item):
'''Format Stack Item'''
if isinstance(item, tuple):
res = '[%s]' % expression_to_string(item)
elif isinstance(item, str):
res = '"%s"' % item
else:
assert not isinstance(item, unicode), repr(item)
res = str(item)
if len(res) > MAX_WIDTH:
return res[:MAX_WIDTH - 3] + '...'
return res
class StackViewer(text_viewer.TextViewer):
def __init__(self, surface):
super(StackViewer, self).__init__(surface)
self.stack_holder = None
self.content_id = 'stack viewer'
def _attach(self, display):
if self.stack_holder:
return
om = core.OpenMessage(self, 'stack.pickle')
display.broadcast(om)
if om.status != core.SUCCESS:
raise RuntimeError('stack unavailable')
self.stack_holder = om.thing
def _update(self):
self.lines[:] = map(fsi, iter_stack(self.stack_holder[0])) or ['']
def focus(self, display):
self._attach(display)
super(StackViewer, self).focus(display)
def handle(self, message):
if (isinstance(message, core.ModifyMessage)
and message.subject is self.stack_holder
):
self._update()
self.draw_body()

674
joy/vui/text_viewer.py Normal file
View File

@ -0,0 +1,674 @@
import string
import pygame
from joy.utils.stack import expression_to_string
from core import (
ARROW_KEYS,
BACKGROUND as BG,
FOREGROUND as FG,
CommandMessage,
ModifyMessage,
OpenMessage,
SUCCESS,
push,
)
import viewer, font_data
reload(viewer)
MenuViewer = viewer.MenuViewer
SELECTION_COLOR = 235, 255, 0, 32
SELECTION_KEYS = {
pygame.K_F1,
pygame.K_F2,
pygame.K_F3,
pygame.K_F4,
}
STACK_CHATTER_KEYS = {
pygame.K_F5,
pygame.K_F6,
pygame.K_F7,
pygame.K_F8,
}
def _is_command(display, word):
return display.lookup(word) or word.isdigit() or all(
not s or s.isdigit() for s in word.split('.', 1)
) and len(word) > 1
def format_stack_item(content):
if isinstance(content, tuple):
return '[%s]' % expression_to_string(content)
return str(content)
class Font(object):
IMAGE = pygame.image.load(font_data.data, 'Iosevka12.BMP')
LOOKUP = (string.ascii_letters +
string.digits +
'''@#$&_~|`'"%^=-+*/\\<>[]{}(),.;:!?''')
def __init__(self, char_w=8, char_h=19, line_h=19):
self.char_w = char_w
self.char_h = char_h
self.line_h = line_h
def size(self, text):
return self.char_w * len(text), self.line_h
def render(self, text):
surface = pygame.Surface(self.size(text))
surface.fill(BG)
x = 0
for ch in text:
if not ch.isspace():
try:
i = self.LOOKUP.index(ch)
except ValueError:
# render a lil box...
r = (x + 1, self.line_h / 2 - 3,
self.char_w - 2, self.line_h / 2)
pygame.draw.rect(surface, FG, r, 1)
else:
iy, ix = divmod(i, 26)
ix *= self.char_w
iy *= self.char_h
area = ix, iy, self.char_w, self.char_h
surface.blit(self.IMAGE, (x, 0), area)
x += self.char_w
return surface
def __contains__(self, char):
assert len(char) == 1, repr(char)
return char in self.LOOKUP
FONT = Font()
class TextViewer(MenuViewer):
MINIMUM_HEIGHT = FONT.line_h + 3
CLOSE_TEXT = FONT.render('close')
GROW_TEXT = FONT.render('grow')
class Cursor(object):
def __init__(self, viewer):
self.v = viewer
self.x = self.y = 0
self.w, self.h = 2, FONT.line_h
self.mem = pygame.Surface((self.w, self.h))
self.can_fade = False
def set_to(self, x, y):
self.fade()
self.x, self.y = x, y
self.draw()
def draw(self):
r = self.x * FONT.char_w, self.screen_y(), self.w, self.h
self.mem.blit(self.v.body_surface, (0, 0), r)
self.v.body_surface.fill(FG, r)
self.can_fade = True
def fade(self):
if self.can_fade:
dest = self.x * FONT.char_w, self.screen_y()
self.v.body_surface.blit(self.mem, dest)
self.can_fade = False
def screen_y(self, row=None):
if row is None: row = self.y
return (row - self.v.at_line) * FONT.line_h
def up(self, _mod):
if self.y:
self.fade()
self.y -= 1
self.x = min(self.x, len(self.v.lines[self.y]))
self.draw()
def down(self, _mod):
if self.y < len(self.v.lines) - 1:
self.fade()
self.y += 1
self.x = min(self.x, len(self.v.lines[self.y]))
self.draw()
self._check_scroll()
def left(self, _mod):
if self.x:
self.fade()
self.x -= 1
self.draw()
elif self.y:
self.fade()
self.y -= 1
self.x = len(self.v.lines[self.y])
self.draw()
self._check_scroll()
def right(self, _mod):
if self.x < len(self.v.lines[self.y]):
self.fade()
self.x += 1
self.draw()
elif self.y < len(self.v.lines) - 1:
self.fade()
self.y += 1
self.x = 0
self.draw()
self._check_scroll()
def _check_scroll(self):
if self.y < self.v.at_line:
self.v.scroll_down()
elif self.y > self.v.at_line + self.v.h_in_lines:
self.v.scroll_up()
def __init__(self, surface):
self.cursor = self.Cursor(self)
MenuViewer.__init__(self, surface)
self.lines = ['']
self.content_id = None
self.at_line = 0
self.bg = BG
self.command = self.command_rect = None
self._sel_start = self._sel_end = None
def resurface(self, surface):
self.cursor.fade()
MenuViewer.resurface(self, surface)
w, h = self.CLOSE_TEXT.get_size()
self.close_rect = pygame.rect.Rect(self.w - 2 - w, 1, w, h)
w, h = self.GROW_TEXT.get_size()
self.grow_rect = pygame.rect.Rect(1, 1, w, h)
self.body_surface = surface.subsurface(self.body_rect)
self.line_w = self.body_rect.w / FONT.char_w + 1
self.h_in_lines = self.body_rect.h / FONT.line_h - 1
self.command_rect = self.command = None
self._sel_start = self._sel_end = None
def handle(self, message):
if super(TextViewer, self).handle(message):
return
if (isinstance(message, ModifyMessage)
and message.subject is self.lines
):
# TODO: check self.at_line
self.draw_body()
# Drawing
def draw_menu(self):
#MenuViewer.draw_menu(self)
self.surface.blit(self.GROW_TEXT, (1, 1))
self.surface.blit(self.CLOSE_TEXT,
(self.w - 2 - self.close_rect.w, 1))
if self.content_id:
self.surface.blit(FONT.render('| ' + self.content_id),
(self.grow_rect.w + FONT.char_w + 3, 1))
self.surface.fill( # light grey background
(196, 196, 196),
(0, 0, self.w - 1, self.MINIMUM_HEIGHT),
pygame.BLEND_MULT
)
def draw_body(self):
MenuViewer.draw_body(self)
ys = xrange(0, self.body_rect.height, FONT.line_h)
ls = self.lines[self.at_line:self.at_line + self.h_in_lines + 2]
for y, line in zip(ys, ls):
self.draw_line(y, line)
def draw_line(self, y, line):
surface = FONT.render(line[:self.line_w])
self.body_surface.blit(surface, (0, y))
def _redraw_line(self, row):
try: line = self.lines[row]
except IndexError: line = ' ' * self.line_w
else:
n = self.line_w - len(line)
if n > 0: line = line + ' ' * n
self.draw_line(self.cursor.screen_y(row), line)
# General Functionality
def focus(self, display):
self.cursor.v = self
self.cursor.draw()
def unfocus(self):
self.cursor.fade()
def scroll_up(self):
if self.at_line < len(self.lines) - 1:
self._fade_command()
self._deselect()
self._sel_start = self._sel_end = None
self.at_line += 1
self.body_surface.scroll(0, -FONT.line_h)
row = self.h_in_lines + self.at_line
self._redraw_line(row)
self._redraw_line(row + 1)
self.cursor.draw()
def scroll_down(self):
if self.at_line:
self._fade_command()
self._deselect()
self._sel_start = self._sel_end = None
self.at_line -= 1
self.body_surface.scroll(0, FONT.line_h)
self._redraw_line(self.at_line)
self.cursor.draw()
def command_down(self, display, x, y):
if self.command_rect and self.command_rect.collidepoint(x, y):
return
self._fade_command()
line, column, _row = self.at(x, y)
word_start = line.rfind(' ', 0, column) + 1
word_end = line.find(' ', column)
if word_end == -1: word_end = len(line)
word = line[word_start:word_end]
if not _is_command(display, word):
return
r = self.command_rect = pygame.Rect(
word_start * FONT.char_w, # x
y / FONT.line_h * FONT.line_h, # y
len(word) * FONT.char_w, # w
FONT.line_h # h
)
pygame.draw.line(self.body_surface, FG, r.bottomleft, r.bottomright)
self.command = word
def command_up(self, display):
if self.command:
command = self.command
self._fade_command()
display.broadcast(CommandMessage(self, command))
def _fade_command(self):
self.command = None
r, self.command_rect = self.command_rect, None
if r:
pygame.draw.line(self.body_surface, BG, r.bottomleft, r.bottomright)
def at(self, x, y):
'''
Given screen coordinates return the line, row, and column of the
character there.
'''
row = self.at_line + y / FONT.line_h
try:
line = self.lines[row]
except IndexError:
row = len(self.lines) - 1
line = self.lines[row]
column = len(line)
else:
column = min(x / FONT.char_w, len(line))
return line, column, row
# Event Processing
def body_click(self, display, x, y, button):
if button == 1:
_line, column, row = self.at(x, y)
self.cursor.set_to(column, row)
elif button == 2:
if pygame.KMOD_SHIFT & pygame.key.get_mods():
self.scroll_up()
else:
self.scroll_down()
elif button == 3:
self.command_down(display, x, y)
elif button == 4: self.scroll_down()
elif button == 5: self.scroll_up()
def menu_click(self, display, x, y, button):
if MenuViewer.menu_click(self, display, x, y, button):
return True
def mouse_up(self, display, x, y, button):
if MenuViewer.mouse_up(self, display, x, y, button):
return True
elif button == 3 and self.body_rect.collidepoint(x, y):
self.command_up(display)
def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
button0, button1, button2):
return True
if (button0
and display.focused_viewer is self
and self.body_rect.collidepoint(x, y)
):
bx, by = self.body_rect.topleft
_line, column, row = self.at(x - bx, y - by)
self.cursor.set_to(column, row)
elif button2 and self.body_rect.collidepoint(x, y):
bx, by = self.body_rect.topleft
self.command_down(display, x - bx, y - by)
def close(self):
self._sel_start = self._sel_end = None
def key_down(self, display, uch, key, mod):
if key in SELECTION_KEYS:
self._selection_key(display, key, mod)
return
if key in STACK_CHATTER_KEYS:
self._stack_chatter_key(display, key, mod)
return
if key in ARROW_KEYS:
self._arrow_key(key, mod)
return
line, i = self.lines[self.cursor.y], self.cursor.x
modified = ()
if key == pygame.K_RETURN:
self._return_key(mod, line, i)
modified = True
elif key == pygame.K_BACKSPACE:
modified = self._backspace_key(mod, line, i)
elif key == pygame.K_DELETE:
modified = self._delete_key(mod, line, i)
elif key == pygame.K_INSERT:
modified = self._insert_key(display, mod, line, i)
elif uch and uch in FONT or uch == ' ':
self._printable_key(uch, mod, line, i)
modified = True
else:
print '%r %i %s' % (uch, key, bin(mod))
if modified:
# The selection is fragile.
self._deselect()
self._sel_start = self._sel_end = None
message = ModifyMessage(
self, self.lines, content_id=self.content_id)
display.broadcast(message)
def _stack_chatter_key(self, display, key, mod):
if key == pygame.K_F5:
if mod & pygame.KMOD_SHIFT:
command = 'roll<'
else:
command = 'swap'
elif key == pygame.K_F6:
if mod & pygame.KMOD_SHIFT:
command = 'roll>'
else:
command = 'dup'
elif key == pygame.K_F7:
if mod & pygame.KMOD_SHIFT:
command = 'tuck'
else:
command = 'over'
## elif key == pygame.K_F8:
## if mod & pygame.KMOD_SHIFT:
## command = ''
## else:
## command = ''
else:
return
display.broadcast(CommandMessage(self, command))
# Selection Handling
def _selection_key(self, display, key, mod):
self.cursor.fade()
self._deselect()
if key == pygame.K_F1: # set sel start
self._sel_start = self.cursor.y, self.cursor.x
self._update_selection()
elif key == pygame.K_F2: # set sel end
self._sel_end = self.cursor.y, self.cursor.x
self._update_selection()
elif key == pygame.K_F3: # copy
if mod & pygame.KMOD_SHIFT:
self._parse_selection(display)
else:
self._copy_selection(display)
self._update_selection()
elif key == pygame.K_F4: # cut or delete
if mod & pygame.KMOD_SHIFT:
self._delete_selection(display)
else:
self._cut_selection(display)
self.cursor.draw()
def _deselect(self):
if self._has_selection():
srow, erow = self._sel_start[0], self._sel_end[0]
# Just erase the whole selection.
for r in range(min(srow, erow), max(srow, erow) + 1):
self._redraw_line(r)
def _copy_selection(self, display):
if push(self, self._get_selection(), display.broadcast) == SUCCESS:
return True
## om = OpenMessage(self, 'stack.pickle')
## display.broadcast(om)
## if om.status == SUCCESS:
## selection = self._get_selection()
## om.thing[0] = selection, om.thing[0]
## display.broadcast(ModifyMessage(
## self, om.thing, content_id=om.content_id))
def _parse_selection(self, display):
if self._has_selection():
if self._copy_selection(display):
display.broadcast(CommandMessage(self, 'parse'))
def _cut_selection(self, display):
if self._has_selection():
if self._copy_selection(display):
self._delete_selection(display)
def _delete_selection(self, display):
if not self._has_selection():
return
self.cursor.fade()
srow, scolumn, erow, ecolumn = self._selection_coords()
if srow == erow:
line = self.lines[srow]
self.lines[srow] = line[:scolumn] + line[ecolumn:]
else:
left = self.lines[srow][:scolumn]
right = self.lines[erow][ecolumn:]
self.lines[srow:erow + 1] = [left + right]
self.draw_body()
self.cursor.set_to(srow, scolumn)
display.broadcast(ModifyMessage(
self, self.lines, content_id=self.content_id))
def _has_selection(self):
return (self._sel_start
and self._sel_end
and self._sel_start != self._sel_end)
def _get_selection(self):
'''Return the current selection if any as a single string.'''
if not self._has_selection():
return ''
srow, scolumn, erow, ecolumn = self._selection_coords()
if srow == erow:
return str(self.lines[srow][scolumn:ecolumn])
lines = []
assert srow < erow
while srow <= erow:
line = self.lines[srow]
e = ecolumn if srow == erow else len(line)
lines.append(line[scolumn:e])
scolumn = 0
srow += 1
return str('\n'.join(lines))
def _selection_coords(self):
(srow, scolumn), (erow, ecolumn) = (
min(self._sel_start, self._sel_end),
max(self._sel_start, self._sel_end)
)
return srow, scolumn, erow, ecolumn
def _update_selection(self):
if self._sel_start is None and self._sel_end:
self._sel_start = self._sel_end
elif self._sel_end is None and self._sel_start:
self._sel_end = self._sel_start
assert self._sel_start and self._sel_end
if self._sel_start != self._sel_end:
for rect in self._iter_selection_rectangles():
self.body_surface.fill(
SELECTION_COLOR,
rect,
pygame.BLEND_RGBA_MULT
)
def _iter_selection_rectangles(self, ):
srow, scolumn, erow, ecolumn = self._selection_coords()
if srow == erow:
yield (
scolumn * FONT.char_w,
self.cursor.screen_y(srow),
(ecolumn - scolumn) * FONT.char_w,
FONT.line_h
)
return
lines = self.lines[srow:erow + 1]
assert len(lines) >= 2
first_line = lines[0]
yield (
scolumn * FONT.char_w,
self.cursor.screen_y(srow),
(len(first_line) - scolumn) * FONT.char_w,
FONT.line_h
)
yield (
0,
self.cursor.screen_y(erow),
ecolumn * FONT.char_w,
FONT.line_h
)
if len(lines) > 2:
for line in lines[1:-1]:
srow += 1
yield (
0,
self.cursor.screen_y(srow),
len(line) * FONT.char_w,
FONT.line_h
)
# Key Handlers
def _printable_key(self, uch, _mod, line, i):
line = line[:i] + uch + line[i:]
self.lines[self.cursor.y] = line
self.cursor.fade()
self.cursor.x += 1
self.draw_line(self.cursor.screen_y(), line)
self.cursor.draw()
def _backspace_key(self, _mod, line, i):
res = False
if i:
line = line[:i - 1] + line[i:]
self.lines[self.cursor.y] = line
self.cursor.fade()
self.cursor.x -= 1
self.draw_line(self.cursor.screen_y(), line + ' ')
self.cursor.draw()
res = True
elif self.cursor.y:
y = self.cursor.y
left, right = self.lines[y - 1:y + 1]
self.lines[y - 1:y + 1] = [left + right]
self.cursor.x = len(left)
self.cursor.y -= 1
self.draw_body()
self.cursor.draw()
res = True
return res
def _delete_key(self, _mod, line, i):
res = False
if i < len(line):
line = line[:i] + line[i + 1:]
self.lines[self.cursor.y] = line
self.cursor.fade()
self.draw_line(self.cursor.screen_y(), line + ' ')
self.cursor.draw()
res = True
elif self.cursor.y < len(self.lines) - 1:
y = self.cursor.y
left, right = self.lines[y:y + 2]
self.lines[y:y + 2] = [left + right]
self.draw_body()
self.cursor.draw()
res = True
return res
def _arrow_key(self, key, mod):
if key == pygame.K_UP: self.cursor.up(mod)
elif key == pygame.K_DOWN: self.cursor.down(mod)
elif key == pygame.K_LEFT: self.cursor.left(mod)
elif key == pygame.K_RIGHT: self.cursor.right(mod)
def _return_key(self, _mod, line, i):
self.cursor.fade()
# Ignore the mods for now.
n = self.cursor.y
self.lines[n:n + 1] = [line[:i], line[i:]]
self.cursor.y += 1
self.cursor.x = 0
if self.cursor.y > self.at_line + self.h_in_lines:
self.scroll_up()
else:
self.draw_body()
self.cursor.draw()
def _insert_key(self, display, mod, _line, _i):
om = OpenMessage(self, 'stack.pickle')
display.broadcast(om)
if om.status != SUCCESS:
return
stack = om.thing[0]
if stack:
content = format_stack_item(stack[0])
if self.insert(content):
if mod & pygame.KMOD_SHIFT:
display.broadcast(CommandMessage(self, 'pop'))
return True
def insert(self, content):
assert isinstance(content, basestring), repr(content)
if content:
self.cursor.fade()
row, column = self.cursor.y, self.cursor.x
line = self.lines[row]
lines = (line[:column] + content + line[column:]).splitlines()
self.lines[row:row + 1] = lines
self.draw_body()
self.cursor.y = row + len(lines) - 1
self.cursor.x = len(lines[-1]) - len(line) + column
self.cursor.draw()
return True
def append(self, content):
self.cursor.fade()
self.cursor.y = len(self.lines) - 1
self.cursor.x = len(self.lines[self.cursor.y])
self.insert(content)

232
joy/vui/viewer.py Normal file
View File

@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2018 Simon Forman
#
# This file is part of joy.py
#
# joy.py is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# joy.py 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with joy.py. If not see <http://www.gnu.org/licenses/>.
#
import pygame
from core import BACKGROUND, FOREGROUND
class Viewer(object):
MINIMUM_HEIGHT = 11
def __init__(self, surface):
self.resurface(surface)
self.last_touch = 0, 0
def resurface(self, surface):
self.w, self.h = surface.get_width(), surface.get_height()
self.surface = surface
def split(self, y):
'''
Split the viewer at the y coordinate (which is relative to the
viewer's surface and must be inside it somewhere) and return the
remaining height. The upper part of the viewer remains (and gets
redrawn on a new surface) and the lower space is now available
for e.g. a new viewer.
'''
assert y >= self.MINIMUM_HEIGHT
new_viewer_h = self.h - y
self.resurface(self.surface.subsurface((0, 0, self.w, y)))
if y <= self.last_touch[1]: self.last_touch = 0, 0
self.draw()
return new_viewer_h
def handle(self, message):
assert self is not message.sender
pass
def draw(self):
'''Draw the viewer onto its surface.'''
self.surface.fill(BACKGROUND)
x, y, h = self.w - 1, self.MINIMUM_HEIGHT, self.h - 1
# Right-hand side.
pygame.draw.line(self.surface, FOREGROUND, (x, 0), (x, h))
# Between header and body.
pygame.draw.line(self.surface, FOREGROUND, (0, y), (x, y))
# Bottom.
pygame.draw.line(self.surface, FOREGROUND, (0, h), (x, h))
def close(self):
'''Close the viewer and release any resources, etc...'''
def focus(self, display):
pass
def unfocus(self):
pass
# Event handling.
def mouse_down(self, display, x, y, button):
self.last_touch = x, y
def mouse_up(self, display, x, y, button):
pass
def mouse_motion(self, display, x, y, dx, dy, button0, button1, button2):
pass
def key_up(self, display, key, mod):
if key == pygame.K_q and mod & pygame.KMOD_CTRL: # Ctrl-q
display.close_viewer(self)
return True
if key == pygame.K_g and mod & pygame.KMOD_CTRL: # Ctrl-g
display.grow_viewer(self)
return True
def key_down(self, display, uch, key, mod):
pass
class MenuViewer(Viewer):
MINIMUM_HEIGHT = 26
def __init__(self, surface):
Viewer.__init__(self, surface)
self.resizing = 0
self.bg = 100, 150, 100
def resurface(self, surface):
Viewer.resurface(self, surface)
n = self.MINIMUM_HEIGHT - 2
self.close_rect = pygame.rect.Rect(self.w - 2 - n, 1, n, n)
self.grow_rect = pygame.rect.Rect(1, 1, n, n)
self.body_rect = pygame.rect.Rect(
0, self.MINIMUM_HEIGHT + 1,
self.w - 1, self.h - self.MINIMUM_HEIGHT - 2)
def draw(self):
'''Draw the viewer onto its surface.'''
Viewer.draw(self)
if not self.resizing:
self.draw_menu()
self.draw_body()
def draw_menu(self):
# menu buttons
pygame.draw.rect(self.surface, FOREGROUND, self.close_rect, 1)
pygame.draw.rect(self.surface, FOREGROUND, self.grow_rect, 1)
def draw_body(self):
self.surface.fill(self.bg, self.body_rect)
def mouse_down(self, display, x, y, button):
Viewer.mouse_down(self, display, x, y, button)
if y <= self.MINIMUM_HEIGHT:
self.menu_click(display, x, y, button)
else:
bx, by = self.body_rect.topleft
self.body_click(display, x - bx, y - by, button)
def body_click(self, display, x, y, button):
if button == 1:
self.draw_an_a(x, y)
def menu_click(self, display, x, y, button):
if button == 1:
self.resizing = 1
elif button == 3:
if self.close_rect.collidepoint(x, y):
display.close_viewer(self)
return True
elif self.grow_rect.collidepoint(x, y):
display.grow_viewer(self)
return True
def mouse_up(self, display, x, y, button):
if button == 1 and self.resizing:
if self.resizing == 2:
self.resizing = 0
self.draw()
display.done_resizing()
self.resizing = 0
return True
def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
if self.resizing and button0:
self.resizing = 2
display.change_viewer(self, rel_y, relative=True)
return True
else:
self.resizing = 0
#self.draw_an_a(x, y)
def key_up(self, display, key, mod):
if Viewer.key_up(self, display, key, mod):
return True
def draw_an_a(self, x, y):
# Draw a crude letter A.
lw, lh = 10, 14
try: surface = self.surface.subsurface((x - lw, y - lh, lw, lh))
except ValueError: return
draw_a(surface, blend=1)
class SomeViewer(MenuViewer):
def __init__(self, surface):
MenuViewer.__init__(self, surface)
def resurface(self, surface):
MenuViewer.resurface(self, surface)
def draw_menu(self):
MenuViewer.draw_menu(self)
def draw_body(self):
pass
def body_click(self, display, x, y, button):
pass
def menu_click(self, display, x, y, button):
if MenuViewer.menu_click(self, display, x, y, button):
return True
def mouse_up(self, display, x, y, button):
if MenuViewer.mouse_up(self, display, x, y, button):
return True
def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
button0, button1, button2):
return True
def key_down(self, display, uch, key, mod):
try:
print chr(key),
except ValueError:
pass
# Note that Oberon book says that if you split at the exact top of a viewer
# it should close, and I think this implies the new viewer gets the old
# viewer's whole height. I haven't implemented that yet, so the edge-case
# in the code is broken by "intent" for now..
def draw_a(surface, color=FOREGROUND, blend=False):
w, h = surface.get_width() - 2, surface.get_height() - 2
pygame.draw.aalines(surface, color, False, (
(1, h), (w / 2, 1), (w, h), (1, h / 2)
), blend)