Remove the types stuff et. al.

This commit is contained in:
Simon Forman
2020-05-19 13:01:37 -07:00
parent 95cd1e6cc6
commit 8bd0e7ce0e
163 changed files with 90 additions and 33382 deletions
View File
-24
View File
@@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2018 Simon Forman
#
# This file is part of Joypy.
#
# Joypy 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.
#
# Joypy 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 Joypy. If not see <http://www.gnu.org/licenses/>.
#
import sys
import joy.gui.main
sys.exit(joy.gui.main.main())
-156
View File
@@ -1,156 +0,0 @@
'''
Copyright (C) 2004 - 2008 Simon Forman
This file is part of Xerblin.
Xerblin 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 2 of the License, or
(at your option) any later version.
This program 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 this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
'''
from future import standard_library
standard_library.install_aliases()
from builtins import map, object, str
from tkinter import Listbox, SINGLE
from tkinter.dnd import dnd_start
from joy.utils.stack import iter_stack, list_to_stack, expression_to_string
class SourceWrapper(object):
'''
Helper object for drag and drop.
'''
def __init__(self, source, widget, index=None):
'''
source is the object being dragged, widget is the container that's
initialing the drag operation, and index s thu index of the item
in the widget's model object (which presumably is a ListModel
containing the source object.)
'''
self.source = source
self.widget = widget
self.index = index
def dnd_end(self, target, event):
try:
self.widget.clear()
except AttributeError:
pass
class DraggyListbox(Listbox):
def __init__(self, master=None, **kw):
# Get our stack.
self.stack = kw.pop('items')
# Override any passed in selectmode.
kw['selectmode'] = SINGLE
Listbox.__init__(self, master, **kw)
self.bind('<Button-1>', self.startDrag)
self.bind('<ButtonRelease-1>', self.clear)
def clear(self, event=None):
i = self.curselection()
if i:
i = int(i[0])
self.selection_clear(i)
def startDrag(self, event):
i = self.nearest(event.y)
if i >= 0:
self.selection_set(i)
source = self.stack[i]
source = SourceWrapper(source, self, i)
event.num = 1 # Don't ask. (See Tkdnd.py)
dnd_start(source, event)
return "break"
class ControllerListbox(DraggyListbox):
def __init__(self, master=None, **kw):
DraggyListbox.__init__(self, master, **kw)
self._dragIndex = -1
def dnd_accept(self, source, event):
self.focus_force()
return self
def dnd_enter(self, source, event):
pass
def dnd_motion(self, source, event):
I = self.nearest(event.y_root - self.winfo_rooty())
if self._dragIndex >= 0:
self.delete(self._dragIndex)
self._dragIndex = I
self.insert(I, '---')
def dnd_leave(self, source, event):
if self._dragIndex >= 0:
self.delete(self._dragIndex)
self._dragIndex = -1
def dnd_commit(self, source, event):
i = self._dragIndex
if i >= 0:
self.delete(i)
self._dragIndex = -1
try:
if self is source.widget:
# Don't duplicate something by dropping it on itself.
if i == source.index:
return
# Instead, move it by removing it before the pending append.
del self.stack[source.index]
if i > source.index:
i -= 1
self.stack.insert(i, source.source)
finally:
self.clear()
class StackListbox(ControllerListbox):
def __init__(self, world, master=None, **kw):
ControllerListbox.__init__(self, master, **kw)
self.world = world
def _update(self):
self.delete(0, 'end')
self.insert(0, *map(self.format, self.stack))
def update_stack(self, stack):
self.stack = list(iter_stack(stack))
self._update()
def dnd_commit(self, source, event):
ControllerListbox.dnd_commit(self, source, event)
self._update()
self.world.stack = list_to_stack(self.stack)
@staticmethod
def format(item):
if isinstance(item, tuple):
return '[%s]' % expression_to_string(item)
return str(item)
-49
View File
@@ -1,49 +0,0 @@
round_to_cents == 100 * ++ floor 100 /
Ordered Binary Tree datastructure functions.
fourth == rest_two rest first
?fourth == [] [fourth] [] ifte
first_two == uncons uncons pop
ccons == cons cons
cinf == cons infra
rest_two == rest rest
_Tree_T> == [dipd] cinf
_Tree_T< == [dipdd] cinf
_Tree_add_P == over [popop popop first] nullary
_Tree_add_T> == ccons _Tree_T<
_Tree_add_T< == ccons _Tree_T>
_Tree_add_Ee = = pop swap roll< rest_two ccons
_Tree_add_R == _Tree_add_P [_Tree_add_T>] [_Tree_add_Ee] [_Tree_add_T<] cmp
_Tree_add_E == [pop] dipd Tree-new
_Tree_iter_order_left == [cons dip] dupdip
_Tree_iter_order_current == [[F] dupdip] dip
_Tree_iter_order_right == [fourth] dip i
_Tree_iter_order_R == _Tree_iter_order_left _Tree_iter_order_current _Tree_iter_order_right
_Tree_get_P == over [pop popop first] nullary
_Tree_get_T> == [fourth] dipd i
_Tree_get_T< == [third] dipd i
_Tree_get_E = = popop second
_Tree_get_R == _Tree_get_P [_Tree_get_T>] [_Tree_get_E] [_Tree_get_T<] cmp
_Tree_delete_rightmost == [?fourth] [fourth] while
_Tree_delete_clear_stuff = = roll> popop rest
_Tree_delete_del == dip cons dipd swap
_Tree_delete_W == dup _Tree_delete_rightmost first_two over
_Tree_delete_E.0 == _Tree_delete_clear_stuff [_Tree_delete_W] _Tree_delete_del
_Tree_delete_E == [[[pop third not] pop fourth] [[pop fourth not] pop third] [[_Tree_delete_E.0] cinf]] cond
_Tree_delete_R0 = = over first swap dup
_Tree_delete_R1 == cons roll> [_Tree_T>] [_Tree_delete_E] [_Tree_T<] cmp
Tree-new == swap [[] []] ccons
Tree-add == [popop not] [_Tree_add_E] [] [_Tree_add_R] genrec
Tree-iter == [not] [pop] roll< [dupdip rest_two] cons [step] genrec
Tree-iter-order == [not] [pop] [dup third] [_Tree_iter_order_R] genrec
Tree-get == [pop not] swap [] [_Tree_get_R] genrec
Tree-delete == [pop not] [pop] [_Tree_delete_R0] [_Tree_delete_R1] genrec
-4
View File
@@ -1,4 +0,0 @@
Joypy - Copyright © 2018 Simon Forman
This program comes with ABSOLUTELY NO WARRANTY; for details right-click "warranty". This is free software, and you are welcome to redistribute it under certain conditions; right-click "sharing" for details. Right-click on these commands to see docs on UI commands: key_bindings mouse_bindings
<-
-59
View File
@@ -1,59 +0,0 @@
reset_log words mouse_bindings key_bindings
Stack Chatter
dup dupd dupdd over tuck
pop popd popdd popop popopd popopdd
swap roll< roll> rolldown rollup
unit clear
Math
add + sub - mul * truediv / mod %
div divmod floor pm
abs sqr sqrt neg pow
max min sum average product
pred -- succ ++ lshift << rshift >>
Logic
ge gt eq le lt ne
< <= = >= > != <>
and & or not xor ^
bool truthy ?
Combinators
i x b infra dip dipd dipdd dupdip dupdipd
cleave fork app1 app2 app3 map pam
nullary unary binary ternary
Control Flow
branch cond ifte choice
loop while genrec primrec
make_generator
List Manipulation
enstacken disenstacken stack unstack
first first_two second third fourth rest rrest
flatten drop take reverse select zip
size sort shunt getitem
step step_zero times
cons ccons uncons swons unswons
concat unique
remove
at of pick
unquoted quoted
Misc
down_to_zero cmp gcd help id
least_fraction parse quoted
range range_to_zero
reset_log show_log
run
stuncons stununcons
swaack
void
-1
View File
@@ -1 +0,0 @@
(t.
-15
View File
@@ -1,15 +0,0 @@
[key bindings]
<F5> = swap
<F6> = dup
<Shift-F5> = roll<
<Shift-F6> = roll>
<F7> = over
<Shift-F7> = tuck
<F8> = parse
<F12> = words
<F1> = reset_log show_log
<Escape> = clear reset_log show_log
<Control-Delete> = pop
<Control-i> = i
-106
View File
@@ -1,106 +0,0 @@
'''
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)
'''
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
import base64, os, io, zipfile
def initialize(joy_home):
Z.extractall(joy_home)
def create_data(from_dir='./default_joy_home'):
f = io.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(io.BytesIO(base64.decodestring(b'''\
UEsDBBQAAAAAAJKh9Uw/yHAgFQQAABUEAAALAAAAc2NyYXRjaC50eHRyZXNldF9sb2cgd29yZHMg
bW91c2VfYmluZGluZ3Mga2V5X2JpbmRpbmdzCgpTdGFjayBDaGF0dGVyCgogZHVwIGR1cGQgZHVw
ZGQgb3ZlciB0dWNrCiBwb3AgcG9wZCBwb3BkZCBwb3BvcCBwb3BvcGQgcG9wb3BkZAogc3dhcCBy
b2xsPCByb2xsPiByb2xsZG93biByb2xsdXAgCiB1bml0IGNsZWFyCgpNYXRoCgogYWRkICsgc3Vi
IC0gbXVsICogdHJ1ZWRpdiAvIG1vZCAlCiBkaXYgZGl2bW9kIGZsb29yIHBtCiBhYnMgc3FyIHNx
cnQgbmVnIHBvdwogbWF4IG1pbiBzdW0gYXZlcmFnZSBwcm9kdWN0CiBwcmVkIC0tIHN1Y2MgKysg
bHNoaWZ0IDw8IHJzaGlmdCA+PgoKTG9naWMKCiBnZSBndCBlcSBsZSBsdCBuZQogIDwgPD0gPSAg
Pj0gPiAgIT0gPD4KIGFuZCAmIG9yIG5vdCB4b3IgXgogYm9vbCB0cnV0aHkgPwoKQ29tYmluYXRv
cnMKCiBpIHggYiBpbmZyYSBkaXAgZGlwZCBkaXBkZCBkdXBkaXAgZHVwZGlwZAogY2xlYXZlIGZv
cmsgYXBwMSBhcHAyIGFwcDMgbWFwIHBhbQogbnVsbGFyeSB1bmFyeSBiaW5hcnkgdGVybmFyeSAK
CkNvbnRyb2wgRmxvdwoKIGJyYW5jaCBjb25kIGlmdGUgY2hvaWNlCiBsb29wIHdoaWxlIGdlbnJl
YyBwcmltcmVjCiBtYWtlX2dlbmVyYXRvcgoKTGlzdCBNYW5pcHVsYXRpb24KCiBlbnN0YWNrZW4g
ZGlzZW5zdGFja2VuIHN0YWNrIHVuc3RhY2sKIGZpcnN0IGZpcnN0X3R3byBzZWNvbmQgdGhpcmQg
Zm91cnRoIHJlc3QgcnJlc3QKIGZsYXR0ZW4gZHJvcCB0YWtlIHJldmVyc2Ugc2VsZWN0IHppcAog
c2l6ZSBzb3J0IHNodW50IGdldGl0ZW0KIHN0ZXAgc3RlcF96ZXJvIHRpbWVzIAogY29ucyBjY29u
cyB1bmNvbnMgc3dvbnMgdW5zd29ucwogY29uY2F0IHVuaXF1ZQogcmVtb3ZlCiBhdCBvZiBwaWNr
CiB1bnF1b3RlZCBxdW90ZWQKCk1pc2MKCiBkb3duX3RvX3plcm8gY21wIGdjZCBoZWxwIGlkIAog
bGVhc3RfZnJhY3Rpb24gcGFyc2UgcXVvdGVkCiByYW5nZSByYW5nZV90b196ZXJvCiByZXNldF9s
b2cgIHNob3dfbG9nCiBydW4gCiBzdHVuY29ucyBzdHVudW5jb25zCiBzd2FhY2sgCiB2b2lkICAg
ICAKUEsDBBQAAAAAAEBB9Uzn5GRHUQEAAFEBAAAHAAAAbG9nLnR4dEpveXB5IC0gQ29weXJpZ2h0
IMKpIDIwMTggU2ltb24gRm9ybWFuClRoaXMgcHJvZ3JhbSBjb21lcyB3aXRoIEFCU09MVVRFTFkg
Tk8gV0FSUkFOVFk7IGZvciBkZXRhaWxzIHJpZ2h0LWNsaWNrICJ3YXJyYW50eSIuIFRoaXMgaXMg
ZnJlZSBzb2Z0d2FyZSwgYW5kIHlvdSBhcmUgd2VsY29tZSB0byByZWRpc3RyaWJ1dGUgaXQgdW5k
ZXIgY2VydGFpbiBjb25kaXRpb25zOyByaWdodC1jbGljayAic2hhcmluZyIgZm9yIGRldGFpbHMu
IFJpZ2h0LWNsaWNrIG9uIHRoZXNlIGNvbW1hbmRzIHRvIHNlZSBkb2NzIG9uIFVJIGNvbW1hbmRz
OiBrZXlfYmluZGluZ3MgbW91c2VfYmluZGluZ3MKCiA8LQpQSwMEFAAAAAAAQUH1THd/ml4DAAAA
AwAAAAwAAABzdGFjay5waWNrbGUodC5QSwMEFAAAAAAApqH1TNL4a8/sAAAA7AAAAAsAAAB0aHVu
LmNvbmZpZwpba2V5IGJpbmRpbmdzXQo8RjU+ID0gc3dhcAo8RjY+ID0gZHVwCjxTaGlmdC1GNT4g
PSByb2xsPAo8U2hpZnQtRjY+ID0gcm9sbD4KPEY3PiA9IG92ZXIKPFNoaWZ0LUY3PiA9IHR1Y2sK
PEY4PiA9IHBhcnNlCjxGMTI+ID0gd29yZHMKPEYxPiA9IHJlc2V0X2xvZyBzaG93X2xvZwo8RXNj
YXBlPiA9IGNsZWFyIHJlc2V0X2xvZyBzaG93X2xvZwo8Q29udHJvbC1EZWxldGU+ID0gcG9wCjxD
b250cm9sLWk+ID0gaQoKUEsDBBQAAAAAAHOh9UyQUJ4KOgcAADoHAAAPAAAAZGVmaW5pdGlvbnMu
dHh0CnJvdW5kX3RvX2NlbnRzID09IDEwMCAqICsrIGZsb29yIDEwMCAvCgoKT3JkZXJlZCBCaW5h
cnkgVHJlZSBkYXRhc3RydWN0dXJlIGZ1bmN0aW9ucy4KCmZvdXJ0aCA9PSByZXN0X3R3byByZXN0
IGZpcnN0Cj9mb3VydGggPT0gW10gW2ZvdXJ0aF0gW10gaWZ0ZQpmaXJzdF90d28gPT0gdW5jb25z
IHVuY29ucyBwb3AKY2NvbnMgPT0gY29ucyBjb25zCmNpbmYgPT0gY29ucyBpbmZyYQpyZXN0X3R3
byA9PSByZXN0IHJlc3QKCl9UcmVlX1Q+ID09IFtkaXBkXSBjaW5mCl9UcmVlX1Q8ID09IFtkaXBk
ZF0gY2luZgoKX1RyZWVfYWRkX1AgPT0gb3ZlciBbcG9wb3AgcG9wb3AgZmlyc3RdIG51bGxhcnkK
X1RyZWVfYWRkX1Q+ID09IGNjb25zIF9UcmVlX1Q8Cl9UcmVlX2FkZF9UPCA9PSBjY29ucyBfVHJl
ZV9UPgpfVHJlZV9hZGRfRWUgPSA9IHBvcCBzd2FwIHJvbGw8IHJlc3RfdHdvIGNjb25zCl9UcmVl
X2FkZF9SID09IF9UcmVlX2FkZF9QIFtfVHJlZV9hZGRfVD5dIFtfVHJlZV9hZGRfRWVdIFtfVHJl
ZV9hZGRfVDxdIGNtcApfVHJlZV9hZGRfRSA9PSBbcG9wXSBkaXBkIFRyZWUtbmV3CgpfVHJlZV9p
dGVyX29yZGVyX2xlZnQgPT0gW2NvbnMgZGlwXSBkdXBkaXAKX1RyZWVfaXRlcl9vcmRlcl9jdXJy
ZW50ID09IFtbRl0gZHVwZGlwXSBkaXAKX1RyZWVfaXRlcl9vcmRlcl9yaWdodCA9PSBbZm91cnRo
XSBkaXAgaQpfVHJlZV9pdGVyX29yZGVyX1IgPT0gX1RyZWVfaXRlcl9vcmRlcl9sZWZ0IF9UcmVl
X2l0ZXJfb3JkZXJfY3VycmVudCBfVHJlZV9pdGVyX29yZGVyX3JpZ2h0CgpfVHJlZV9nZXRfUCA9
PSBvdmVyIFtwb3AgcG9wb3AgZmlyc3RdIG51bGxhcnkKX1RyZWVfZ2V0X1Q+ID09IFtmb3VydGhd
IGRpcGQgaQpfVHJlZV9nZXRfVDwgPT0gW3RoaXJkXSBkaXBkIGkKX1RyZWVfZ2V0X0UgPSA9IHBv
cG9wIHNlY29uZApfVHJlZV9nZXRfUiA9PSBfVHJlZV9nZXRfUCBbX1RyZWVfZ2V0X1Q+XSBbX1Ry
ZWVfZ2V0X0VdIFtfVHJlZV9nZXRfVDxdIGNtcAoKX1RyZWVfZGVsZXRlX3JpZ2h0bW9zdCA9PSBb
P2ZvdXJ0aF0gW2ZvdXJ0aF0gd2hpbGUKX1RyZWVfZGVsZXRlX2NsZWFyX3N0dWZmID0gPSByb2xs
PiBwb3BvcCByZXN0Cl9UcmVlX2RlbGV0ZV9kZWwgPT0gZGlwIGNvbnMgZGlwZCBzd2FwCl9UcmVl
X2RlbGV0ZV9XID09IGR1cCBfVHJlZV9kZWxldGVfcmlnaHRtb3N0IGZpcnN0X3R3byBvdmVyCl9U
cmVlX2RlbGV0ZV9FLjAgPT0gX1RyZWVfZGVsZXRlX2NsZWFyX3N0dWZmIFtfVHJlZV9kZWxldGVf
V10gX1RyZWVfZGVsZXRlX2RlbApfVHJlZV9kZWxldGVfRSA9PSBbW1twb3AgdGhpcmQgbm90XSBw
b3AgZm91cnRoXSBbW3BvcCBmb3VydGggbm90XSBwb3AgdGhpcmRdIFtbX1RyZWVfZGVsZXRlX0Uu
MF0gY2luZl1dIGNvbmQKX1RyZWVfZGVsZXRlX1IwID0gPSBvdmVyIGZpcnN0IHN3YXAgZHVwCl9U
cmVlX2RlbGV0ZV9SMSA9PSBjb25zIHJvbGw+IFtfVHJlZV9UPl0gW19UcmVlX2RlbGV0ZV9FXSBb
X1RyZWVfVDxdIGNtcAoKVHJlZS1uZXcgPT0gc3dhcCBbW10gW11dIGNjb25zClRyZWUtYWRkID09
IFtwb3BvcCBub3RdIFtfVHJlZV9hZGRfRV0gW10gW19UcmVlX2FkZF9SXSBnZW5yZWMKVHJlZS1p
dGVyID09IFtub3RdIFtwb3BdIHJvbGw8IFtkdXBkaXAgcmVzdF90d29dIGNvbnMgW3N0ZXBdIGdl
bnJlYwpUcmVlLWl0ZXItb3JkZXIgPT0gW25vdF0gW3BvcF0gW2R1cCB0aGlyZF0gW19UcmVlX2l0
ZXJfb3JkZXJfUl0gZ2VucmVjClRyZWUtZ2V0ID09IFtwb3Agbm90XSBzd2FwIFtdIFtfVHJlZV9n
ZXRfUl0gZ2VucmVjClRyZWUtZGVsZXRlID09IFtwb3Agbm90XSBbcG9wXSBbX1RyZWVfZGVsZXRl
X1IwXSBbX1RyZWVfZGVsZXRlX1IxXSBnZW5yZWNQSwECFAMUAAAAAACSofVMP8hwIBUEAAAVBAAA
CwAAAAAAAAAAAAAAgIEAAAAAc2NyYXRjaC50eHRQSwECFAMUAAAAAABAQfVM5+RkR1EBAABRAQAA
BwAAAAAAAAAAAAAAgIE+BAAAbG9nLnR4dFBLAQIUAxQAAAAAAEFB9Ux3f5peAwAAAAMAAAAMAAAA
AAAAAAAAAACAgbQFAABzdGFjay5waWNrbGVQSwECFAMUAAAAAACmofVM0vhrz+wAAADsAAAACwAA
AAAAAAAAAAAAtIHhBQAAdGh1bi5jb25maWdQSwECFAMUAAAAAABzofVMkFCeCjoHAAA6BwAADwAA
AAAAAAAAAAAAtIH2BgAAZGVmaW5pdGlvbnMudHh0UEsFBgAAAAAFAAUAHgEAAF0OAAAAAA==''')))
if __name__ == '__main__':
print(create_data())
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This is a script, the module namespace is used as a kind of singleton
# for organizing the moving parts of the system. I forget why I didn't
# use a more typical class.
#
# This docstring doubles as the log header that the system prints when
# the log is reset.
('''\
Joypy - Copyright © 2018 Simon Forman
'''
'This program comes with ABSOLUTELY NO WARRANTY; for details right-click "warranty".'
' This is free software, and you are welcome to redistribute it under certain conditions;'
' right-click "sharing" for details.'
' Right-click on these commands to see docs on UI commands: key_bindings mouse_bindings')
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
import logging, os, pickle, sys
from datetime import datetime
from textwrap import dedent
from configparser import RawConfigParser
from joy.gui.utils import init_home, argparser, FileFaker
DATETIME_FORMAT = "Thun • %B %d %a%I:%M %p"
VIEWER_DEFAULTS = dict(width=80, height=25)
args = argparser.parse_args()
JOY_HOME = args.joy_home
repo = init_home(JOY_HOME)
homed = lambda fn: os.path.join(JOY_HOME, fn)
# Set up logging before doing anything else.
_log = logging.getLogger(__name__)
logging.basicConfig(
format='%(asctime)-15s %(levelname)s %(name)s %(message)s',
filename=os.path.join(JOY_HOME, 'thun.log'),
level=logging.INFO,
)
_log.info('Starting with JOY_HOME=%s', JOY_HOME)
# Now that logging is set up, continue loading the system.
from joy.gui.textwidget import TextViewerWidget, tk, get_font
from joy.gui.world import StackWorld
from joy.gui.controllerlistbox import StackListbox
from joy.library import initialize, DefinitionWrapper
from joy.utils.stack import stack_to_string
cp = RawConfigParser()
# Don't mess with uppercase. We need it for Tk event binding.
cp.optionxform = str
with open(os.path.join(args.joy_home, 'thun.config')) as f:
cp.readfp(f)
GLOBAL_COMMANDS = dict(cp.items('key bindings'))
def repo_relative_path(path):
return os.path.relpath(
path,
os.path.commonprefix((repo.controldir(), path))
)
def commands():
'''
We define a bunch of meta-interpreter command functions here and
return them in a dictionary. They have all the contents of this
module in their scope so they can e.g. modify the log viewer window.
'''
# pylint: disable=unused-variable
def key_bindings(*args):
commands = [ # These are bound in the TextViewerWidget.
'Control-Enter - Run the selection as Joy code, or if there\'s no selection the line containing the cursor.',
'F3 - Copy selection to stack.',
'Shift-F3 - Cut selection to stack.',
'F4 - Paste item on top of stack to insertion cursor.',
'Shift-F4 - Pop and paste top of stack to insertion cursor.',
]
for key, command in GLOBAL_COMMANDS.items():
commands.append('%s - %s' % (key.lstrip('<').rstrip('>'), command))
print('\n'.join([''] + sorted(commands)))
return args
def mouse_bindings(*args):
print(dedent('''
Mouse button chords (to cancel a chord, click the third mouse button.)
Left - Point, sweep selection
Left-Middle - Copy the selection, place text on stack
Left-Right - Run the selection as Joy code
Middle - Paste selection (bypass stack); click and drag to scroll.
Middle-Left - Paste from top of stack, preserve
Middle-Right - Paste from top of stack, pop
Right - Execute command word under mouse cursor
Right-Left - Print docs of command word under mouse cursor
Right-Middle - Lookup word (kinda useless now)
'''))
return args
def reset_log(*args):
log.delete('0.0', tk.END)
print(datetime.now().strftime(DATETIME_FORMAT))
return args
def Thun(*args):
print(__doc__)
return args
def show_log(*args):
log_window.wm_deiconify()
log_window.update()
return args
def show_stack(*args):
stack_window.wm_deiconify()
stack_window.update()
return args
def grand_reset(s, e, d):
stack = world.load_stack() or ()
log.reset()
t.reset()
return stack, e, d
return locals()
# Identify the system core files.
DEFS_FN = homed('definitions.txt')
JOY_FN = homed('scratch.txt')
LOG_FN = homed('log.txt')
STACK_FN = homed('stack.pickle')
REL_STACK_FN = repo_relative_path(STACK_FN)
# Initialize the Joy dictionary.
D = initialize()
D.update(commands())
DefinitionWrapper.load_definitions(DEFS_FN, D)
world = StackWorld(repo, STACK_FN, REL_STACK_FN, dictionary=D)
t = TextViewerWidget(world, **VIEWER_DEFAULTS)
log_window = tk.Toplevel()
# Make it so that you can't actually close the log window, if you try it
# will just "withdraw" (which is like minifying but without a entry in
# the taskbar or icon or whatever.)
log_window.protocol("WM_DELETE_WINDOW", log_window.withdraw)
log = TextViewerWidget(world, log_window, **VIEWER_DEFAULTS)
FONT = get_font('Iosevka', size=14) # Requires Tk root already set up.
stack_window = tk.Toplevel()
stack_window.title("Stack")
stack_window.protocol("WM_DELETE_WINDOW", log_window.withdraw)
stack_viewer = StackListbox(world, stack_window, items=[], font=FONT)
stack_viewer.pack(expand=True, fill=tk.BOTH)
world.set_viewer(stack_viewer)
log.init('Log', LOG_FN, repo_relative_path(LOG_FN), repo, FONT)
t.init('Joy - ' + JOY_HOME, JOY_FN, repo_relative_path(JOY_FN), repo, FONT)
for event, command in GLOBAL_COMMANDS.items():
callback = lambda _, _command=command: world.interpret(_command)
t.bind_all(event, callback)
def main():
sys.stdout, old_stdout = FileFaker(log), sys.stdout
try:
t.mainloop()
finally:
sys.stdout = old_stdout
return 0
if __name__ == '__main__':
main()
-211
View File
@@ -1,211 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2014, 2015 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/>.
#
from builtins import object
#Do-nothing event handler.
nothing = lambda event: None
class MouseBindingsMixin(object):
"""TextViewerWidget mixin class to provide mouse bindings."""
def __init__(self):
#Remember our mouse button state
self.B1_DOWN = False
self.B2_DOWN = False
self.B3_DOWN = False
#Remember our pending action.
self.dothis = nothing
#We'll need to remember whether or not we've been moving B2.
self.beenMovingB2 = False
#Unbind the events we're interested in.
for sequence in (
"<Button-1>", "<B1-Motion>", "<ButtonRelease-1>",
"<Button-2>", "<B2-Motion>", "<ButtonRelease-2>",
"<Button-3>", "<B3-Motion>", "<ButtonRelease-3>",
"<B1-Leave>", "<B2-Leave>", "<B3-Leave>", "<Any-Leave>", "<Leave>"
):
self.unbind(sequence)
self.unbind_all(sequence)
self.event_delete('<<PasteSelection>>') #I forgot what this was for! :-P D'oh!
#Bind our event handlers to their events.
self.bind("<Button-1>", self.B1d)
self.bind("<B1-Motion>", self.B1m)
self.bind("<ButtonRelease-1>", self.B1r)
self.bind("<Button-2>", self.B2d)
self.bind("<B2-Motion>", self.B2m)
self.bind("<ButtonRelease-2>", self.B2r)
self.bind("<Button-3>", self.B3d)
self.bind("<B3-Motion>", self.B3m)
self.bind("<ButtonRelease-3>", self.B3r)
self.bind("<Any-Leave>", self.leave)
self.bind("<Motion>", self.scan_command)
def B1d(self, event):
'''button one pressed'''
self.B1_DOWN = True
if self.B2_DOWN:
self.unhighlight_command()
if self.B3_DOWN :
self.dothis = self.cancel
else:
#copy TOS to the mouse (instead of system selection.)
self.dothis = self.copyto #middle-left-interclick
elif self.B3_DOWN :
self.unhighlight_command()
self.dothis = self.opendoc #right-left-interclick
else:
##button 1 down, set insertion and begin selection.
##Actually, do nothing. Tk Text widget defaults take care of it.
self.dothis = nothing
return
#Prevent further event handling by returning "break".
return "break"
def B2d(self, event):
'''button two pressed'''
self.B2_DOWN = 1
if self.B1_DOWN :
if self.B3_DOWN :
self.dothis = self.cancel
else:
#left-middle-interclick - copy selection to stack
self.dothis = self.copy_selection_to_stack
elif self.B3_DOWN :
self.unhighlight_command()
self.dothis = self.lookup #right-middle-interclick - lookup
else:
#middle-click - paste X selection to mouse pointer
self.set_insertion_point(event)
self.dothis = self.paste_X_selection_to_mouse_pointer
return
return "break"
def B3d(self, event):
'''button three pressed'''
self.B3_DOWN = 1
if self.B1_DOWN :
if self.B2_DOWN :
self.dothis = self.cancel
else:
#left-right-interclick - run selection
self.dothis = self.run_selection
elif self.B2_DOWN :
#middle-right-interclick - Pop/Cut from TOS to insertion cursor
self.unhighlight_command()
self.dothis = self.pastecut
else:
#right-click
self.CommandFirstDown(event)
return "break"
def B1m(self, event):
'''button one moved'''
if self.B2_DOWN or self.B3_DOWN:
return "break"
def B2m(self, event):
'''button two moved'''
if self.dothis == self.paste_X_selection_to_mouse_pointer and \
not (self.B1_DOWN or self.B3_DOWN):
self.beenMovingB2 = True
return
return "break"
def B3m(self, event):
'''button three moved'''
if self.dothis == self.do_command and \
not (self.B1_DOWN or self.B2_DOWN):
self.update_command_word(event)
return "break"
def scan_command(self, event):
self.update_command_word(event)
def B1r(self, event):
'''button one released'''
self.B1_DOWN = False
if not (self.B2_DOWN or self.B3_DOWN):
self.dothis(event)
return "break"
def B2r(self, event):
'''button two released'''
self.B2_DOWN = False
if not (self.B1_DOWN or self.B3_DOWN or self.beenMovingB2):
self.dothis(event)
self.beenMovingB2 = False
return "break"
def B3r(self, event):
'''button three released'''
self.B3_DOWN = False
if not (self.B1_DOWN or self.B2_DOWN) :
self.dothis(event)
return "break"
def InsertFirstDown(self, event):
self.focus()
self.dothis = nothing
self.set_insertion_point(event)
def CommandFirstDown(self, event):
self.dothis = self.do_command
self.update_command_word(event)
-474
View File
@@ -1,474 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2014, 2015, 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/>.
#
'''
A Graphical User Interface for a dialect of Joy in Python.
The GUI
History
Structure
Commands
Mouse Chords
Keyboard
Output from Joy
'''
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
from builtins import str, map, object
from past.builtins import basestring
try:
import tkinter as tk
from tkinter.font import families, Font
except ImportError:
import Tkinter as tk
from tkFont import families, Font
from re import compile as regular_expression
from traceback import format_exc
import os, sys
from joy.utils.stack import stack_to_string
from .mousebindings import MouseBindingsMixin
from .utils import is_numerical
from .world import World
def make_gui(dictionary):
t = TextViewerWidget(World(dictionary=dictionary))
t['font'] = get_font()
t._root().title('Joy')
t.pack(expand=True, fill=tk.BOTH)
return t
def get_font(family='EB Garamond', size=14):
if family not in families():
family = 'Times'
return Font(family=family, size=size)
#: Define mapping between Tkinter events and functions or methods. The
#: keys are string Tk "event sequences" and the values are callables that
#: get passed the TextViewer instance (so you can bind to methods) and
#: must return the actual callable to which to bind the event sequence.
TEXT_BINDINGS = {
#I want to ensure that these keyboard shortcuts work.
'<Control-Return>': lambda tv: tv._control_enter,
'<Control-v>': lambda tv: tv._paste,
'<Control-V>': lambda tv: tv._paste,
'<F3>': lambda tv: tv.copy_selection_to_stack,
'<F4>': lambda tv: tv.copyto,
'<Shift-F3>': lambda tv: tv.cut,
'<Shift-F4>': lambda tv: tv.pastecut,
'<Shift-Insert>': lambda tv: tv._paste,
}
class SavingMixin(object):
def __init__(self, saver=None, filename=None, save_delay=2000):
self.saver = self._saver if saver is None else saver
self.filename = filename
self._save_delay = save_delay
self.tk.call(self._w, 'edit', 'modified', 0)
self.bind('<<Modified>>', self._beenModified)
self._resetting_modified_flag = False
self._save = None
def save(self):
'''
Call _saveFunc() after a certain amount of idle time.
Called by _beenModified().
'''
self._cancelSave()
if self.saver:
self._saveAfter(self._save_delay)
def _saveAfter(self, delay):
'''
Trigger a cancel-able call to _saveFunc() after delay milliseconds.
'''
self._save = self.after(delay, self._saveFunc)
def _saveFunc(self):
self._save = None
self.saver(self._get_contents())
def _saver(self, text):
if not self.filename:
return
with open(self.filename, 'wb') as f:
os.chmod(self.filename, 0o600)
f.write(text.encode('UTF_8'))
f.flush()
os.fsync(f.fileno())
if hasattr(self, 'repo'):
self.repo.stage([self.repo_relative_filename])
self.world.save()
def _cancelSave(self):
if self._save is not None:
self.after_cancel(self._save)
self._save = None
def _get_contents(self):
self['state'] = 'disabled'
try:
return self.get('0.0', 'end')[:-1]
finally:
self['state'] = 'normal'
def _beenModified(self, event):
if self._resetting_modified_flag:
return
self._clearModifiedFlag()
self.save()
def _clearModifiedFlag(self):
self._resetting_modified_flag = True
try:
self.tk.call(self._w, 'edit', 'modified', 0)
finally:
self._resetting_modified_flag = False
## tags = self._saveTags()
## chunks = self.DUMP()
## print chunks
class TextViewerWidget(tk.Text, MouseBindingsMixin, SavingMixin):
"""
This class is a Tkinter Text with special mousebindings to make
it act as a Xerblin Text Viewer.
"""
#This is a regular expression for finding commands in the text.
command_re = regular_expression(r'[-a-zA-Z0-9_\\~/.:!@#$%&*?=+<>]+')
#These are the config tags for command text when it's highlighted.
command_tags = dict(
#underline = 1,
#bgstipple = "gray50",
borderwidth = 2,
relief=tk.RIDGE,
foreground = "green"
)
def __init__(self, world, master=None, **kw):
self.world = world
if self.world.text_widget is None:
self.world.text_widget = self
#Turn on undo, but don't override a passed-in setting.
kw.setdefault('undo', True)
# kw.setdefault('bg', 'white')
kw.setdefault('wrap', 'word')
kw.setdefault('font', 'arial 12')
text_bindings = kw.pop('text_bindings', TEXT_BINDINGS)
#Create ourselves as a Tkinter Text
tk.Text.__init__(self, master, **kw)
#Initialize our mouse mixin.
MouseBindingsMixin.__init__(self)
#Initialize our saver mixin.
SavingMixin.__init__(self)
#Add tag config for command highlighting.
self.tag_config('command', **self.command_tags)
self.tag_config('bzzt', foreground = "orange")
self.tag_config('huh', foreground = "grey")
self.tag_config('number', foreground = "blue")
#Create us a command instance variable
self.command = ''
#Activate event bindings. Modify text_bindings in your config
#file to affect the key bindings and whatnot here.
for event_sequence, callback_finder in text_bindings.items():
callback = callback_finder(self)
self.bind(event_sequence, callback)
## T.protocol("WM_DELETE_WINDOW", self.on_close)
def find_command_in_line(self, line, index):
'''
Return the command at index in line and its begin and end indices.
find_command_in_line(line, index) => command, begin, end
'''
for match in self.command_re.finditer(line):
b, e = match.span()
if b <= index <= e:
return match.group(), b, e
def paste_X_selection_to_mouse_pointer(self, event):
'''Paste the X selection to the mouse pointer.'''
try:
text = self.selection_get()
except tk.TclError:
return 'break'
self.insert_it(text)
def update_command_word(self, event):
'''Highlight the command under the mouse.'''
self.unhighlight_command()
self.command = ''
index = '@%d,%d' % (event.x, event.y)
linestart = self.index(index + 'linestart')
lineend = self.index(index + 'lineend')
line = self.get(linestart, lineend)
row, offset = self._get_index(index)
if offset >= len(line) or line[offset].isspace():
# The mouse is off the end of the line or on a space so there's no
# command, we're done.
return
cmd = self.find_command_in_line(line, offset)
if cmd is None:
return
cmd, b, e = cmd
if is_numerical(cmd):
extra_tags = 'number',
elif self.world.has(cmd):
check = self.world.check(cmd)
if check: extra_tags = ()
elif check is None: extra_tags = 'huh',
else: extra_tags = 'bzzt',
else:
return
self.command = cmd
self.highlight_command(
'%d.%d' % (row, b),
'%d.%d' % (row, e),
*extra_tags)
def highlight_command(self, from_, to, *extra_tags):
'''Apply command style from from_ to to.'''
cmdstart = self.index(from_)
cmdend = self.index(to)
self.tag_add('command', cmdstart, cmdend)
for tag in extra_tags:
self.tag_add(tag, cmdstart, cmdend)
def do_command(self, event):
'''Do the currently highlighted command.'''
self.unhighlight_command()
if self.command:
self.run_command(self.command)
def _control_enter(self, event):
select_indices = self.tag_ranges(tk.SEL)
if select_indices:
command = self.get(select_indices[0], select_indices[1])
else:
linestart = self.index(tk.INSERT + ' linestart')
lineend = self.index(tk.INSERT + ' lineend')
command = self.get(linestart, lineend)
if command and not command.isspace():
self.run_command(command)
return 'break'
def run_command(self, command):
'''Given a string run it on the stack, report errors.'''
try:
self.world.interpret(command)
except SystemExit:
raise
except:
self.popupTB(format_exc().rstrip())
def unhighlight_command(self):
'''Remove any command highlighting.'''
self.tag_remove('number', 1.0, tk.END)
self.tag_remove('huh', 1.0, tk.END)
self.tag_remove('bzzt', 1.0, tk.END)
self.tag_remove('command', 1.0, tk.END)
def set_insertion_point(self, event):
'''Set the insertion cursor to the current mouse location.'''
self.focus()
self.mark_set(tk.INSERT, '@%d,%d' % (event.x, event.y))
def copy_selection_to_stack(self, event):
'''Copy selection to stack.'''
select_indices = self.tag_ranges(tk.SEL)
if select_indices:
s = self.get(select_indices[0], select_indices[1])
self.world.push(s)
def cut(self, event):
'''Cut selection to stack.'''
self.copy_selection_to_stack(event)
# Let the pre-existing machinery take care of cutting the selection.
self.event_generate("<<Cut>>")
def copyto(self, event):
'''Actually "paste" from TOS'''
s = self.world.peek()
if s is not None:
self.insert_it(s)
def insert_it(self, s):
if not isinstance(s, basestring):
s = stack_to_string(s)
# When pasting from the mouse we have to remove the current selection
# to prevent destroying it by the paste operation.
select_indices = self.tag_ranges(tk.SEL)
if select_indices:
# Set two marks to remember the selection.
self.mark_set('_sel_start', select_indices[0])
self.mark_set('_sel_end', select_indices[1])
self.tag_remove(tk.SEL, 1.0, tk.END)
self.insert(tk.INSERT, s)
if select_indices:
self.tag_add(tk.SEL, '_sel_start', '_sel_end')
self.mark_unset('_sel_start')
self.mark_unset('_sel_end')
def run_selection(self, event):
'''Run the current selection if any on the stack.'''
select_indices = self.tag_ranges(tk.SEL)
if select_indices:
selection = self.get(select_indices[0], select_indices[1])
self.tag_remove(tk.SEL, 1.0, tk.END)
self.run_command(selection)
def pastecut(self, event):
'''Cut the TOS item to the mouse.'''
self.copyto(event)
self.world.pop()
def opendoc(self, event):
'''OpenDoc the current command.'''
if self.command:
self.world.do_opendoc(self.command)
def lookup(self, event):
'''Look up the current command.'''
if self.command:
self.world.do_lookup(self.command)
def cancel(self, event):
'''Cancel whatever we're doing.'''
self.leave(None)
self.tag_remove(tk.SEL, 1.0, tk.END)
self._sel_anchor = '0.0'
self.mark_unset(tk.INSERT)
def leave(self, event):
'''Called when mouse leaves the Text window.'''
self.unhighlight_command()
self.command = ''
def _get_index(self, index):
'''Get the index in (int, int) form of index.'''
return tuple(map(int, self.index(index).split('.')))
def _paste(self, event):
'''Paste the system selection to the current selection, replacing it.'''
# If we're "key" pasting, we have to move the insertion point
# to the selection so the pasted text gets inserted at the
# location of the deleted selection.
select_indices = self.tag_ranges(tk.SEL)
if select_indices:
# Mark the location of the current insertion cursor
self.mark_set('tmark', tk.INSERT)
# Put the insertion cursor at the selection
self.mark_set(tk.INSERT, select_indices[1])
# Paste to the current selection, or if none, to the insertion cursor.
self.event_generate("<<Paste>>")
# If we mess with the insertion cursor above, fix it now.
if select_indices:
# Put the insertion cursor back where it was.
self.mark_set(tk.INSERT, 'tmark')
# And get rid of our unneeded mark.
self.mark_unset('tmark')
return 'break'
def init(self, title, filename, repo_relative_filename, repo, font):
self.set_window_title(title)
if os.path.exists(filename):
with open(filename) as f:
data = f.read()
self.insert(tk.END, data)
# Prevent this from triggering a git commit.
self.update()
self._cancelSave()
self.pack(expand=True, fill=tk.BOTH)
self.filename = filename
self.repo_relative_filename = repo_relative_filename
self.repo = repo
self['font'] = font # See below.
def set_window_title(self, title):
self.winfo_toplevel().title(title)
def reset(self):
if os.path.exists(self.filename):
with open(self.filename) as f:
data = f.read()
if data:
self.delete('0.0', tk.END)
self.insert(tk.END, data)
def popupTB(self, tb):
top = tk.Toplevel()
T = TextViewerWidget(
self.world,
top,
width=max(len(s) for s in tb.splitlines()) + 3,
)
T['background'] = 'darkgrey'
T['foreground'] = 'darkblue'
T.tag_config('err', foreground='yellow')
T.insert(tk.END, tb)
last_line = str(int(T.index(tk.END).split('.')[0]) - 1) + '.0'
T.tag_add('err', last_line, tk.END)
T['state'] = tk.DISABLED
top.title(T.get(last_line, tk.END).strip())
T.pack(expand=1, fill=tk.BOTH)
T.see(tk.END)
-98
View File
@@ -1,98 +0,0 @@
from __future__ import print_function
from builtins import object
import argparse, os, sys
from os import listdir, mkdir
from os.path import abspath, exists, expanduser, isfile, join
from dulwich.errors import NotGitRepository
from dulwich.repo import Repo
COMMITTER = b'Joy <auto-commit@example.com>'
DEFAULT_JOY_HOME = expanduser(join('~', '.joypy'))
def is_numerical(s):
try:
float(s)
except ValueError:
return False
return True
def home_dir(path):
'''Return the absolute path of an existing directory.'''
fullpath = expanduser(path) if path.startswith('~') else abspath(path)
if not exists(fullpath):
if path == DEFAULT_JOY_HOME:
print('Creating JOY_HOME', repr(fullpath))
mkdir(fullpath, 0o700)
else:
print(repr(fullpath), "doesn't exist.", file=sys.stderr)
raise ValueError(path)
return fullpath
def init_home(fullpath):
'''
Open or create the Repo.
If there are contents in the dir but it's not a git repo, quit.
'''
try:
repo = Repo(fullpath)
except NotGitRepository:
print(repr(fullpath), "no repository", file=sys.stderr)
if listdir(fullpath):
print(repr(fullpath), "has contents\nQUIT.", file=sys.stderr)
sys.exit(2)
print('Initializing repository in', fullpath)
repo = init_repo(fullpath)
print('Using repository in', fullpath)
return repo
def init_repo(repo_dir):
'''
Create a repo, load the initial content, and make the first commit.
Return the Repo object.
'''
repo = Repo.init(repo_dir)
import joy.gui.init_joy_home
joy.gui.init_joy_home.initialize(repo_dir)
repo.stage([fn for fn in listdir(repo_dir) if isfile(join(repo_dir, fn))])
repo.do_commit(b'Initial commit.', committer=COMMITTER)
return repo
argparser = argparse.ArgumentParser(
prog='joy.gui',
description='Experimental Brutalist UI for Joy.',
)
argparser.add_argument(
'-j', '--joy-home',
help='Use a directory other than %s as JOY_HOME' % DEFAULT_JOY_HOME,
default=DEFAULT_JOY_HOME,
dest='joy_home',
type=home_dir,
)
class FileFaker(object):
def __init__(self, T):
self.T = T
def write(self, text):
self.T.insert('end', text)
self.T.see('end')
def flush(self):
pass
-173
View File
@@ -1,173 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2014, 2015 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/>.
#
from __future__ import print_function
from builtins import object
from logging import getLogger
_log = getLogger(__name__)
import os, pickle, sys
from inspect import getdoc
from joy.joy import run
from joy.library import HELP_TEMPLATE
from joy.parser import Symbol
from joy.utils.stack import stack_to_string
from joy.utils.types import type_check
from .utils import is_numerical
class World(object):
def __init__(self, stack=(), dictionary=None, text_widget=None):
self.stack = stack
self.dictionary = dictionary or {}
self.text_widget = text_widget
self.check_cache = {}
def check(self, name):
try:
res = self.check_cache[name]
except KeyError:
res = self.check_cache[name] = type_check(name, self.stack)
return res
def do_lookup(self, name):
if name in self.dictionary:
self.stack = (Symbol(name), ()), self.stack
self.print_stack()
self.check_cache.clear()
else:
assert is_numerical(name)
self.interpret(name)
def do_opendoc(self, name):
if is_numerical(name):
doc = 'The number ' + str(name)
else:
try:
word = self.dictionary[name]
except KeyError:
doc = 'Unknown: ' + repr(name)
else:
doc = getdoc(word)
print(HELP_TEMPLATE % (name, doc, name))
self.print_stack()
def pop(self):
if self.stack:
self.stack = self.stack[1]
self.print_stack()
self.check_cache.clear()
def push(self, it):
it = it.encode('utf8')
self.stack = it, self.stack
self.print_stack()
self.check_cache.clear()
def peek(self):
if self.stack:
return self.stack[0]
def interpret(self, command):
if self.has(command) and self.check(command) == False: # not in {True, None}:
return
old_stack = self.stack
try:
self.stack, _, self.dictionary = run(
command,
self.stack,
self.dictionary,
)
finally:
self.print_stack()
if old_stack != self.stack:
self.check_cache.clear()
def has(self, name):
return name in self.dictionary
def save(self):
pass
def print_stack(self):
stack_out_index = self.text_widget.search('<' 'STACK', 1.0)
if stack_out_index:
self.text_widget.see(stack_out_index)
s = stack_to_string(self.stack) + '\n'
self.text_widget.insert(stack_out_index, s)
class StackDisplayWorld(World):
def __init__(self, repo, filename, rel_filename, dictionary=None, text_widget=None):
self.filename = filename
stack = self.load_stack() or ()
World.__init__(self, stack, dictionary, text_widget)
self.repo = repo
self.relative_STACK_FN = rel_filename
def interpret(self, command):
command = command.strip()
if self.has(command) and self.check(command) == False: # not in {True, None}:
return
# print('\njoy?', command)
self.print_command(command)
super(StackDisplayWorld, self).interpret(command)
def print_command(self, command):
print(command)
def print_stack(self):
print('\n%s <-' % stack_to_string(self.stack))
def save(self):
with open(self.filename, 'wb') as f:
os.chmod(self.filename, 0o600)
pickle.dump(self.stack, f, protocol=2)
f.flush()
os.fsync(f.fileno())
self.repo.stage([self.relative_STACK_FN])
commit_id = self.repo.do_commit(
b'auto-save',
committer=b'thun-auto-save <nobody@example.com>',
)
_log.info('commit %s', commit_id)
def load_stack(self):
if os.path.exists(self.filename):
with open(self.filename, 'rb') as f:
return pickle.load(f)
class StackWorld(StackDisplayWorld):
viewer = None
def set_viewer(self, viewer):
self.viewer = viewer
self.viewer.update_stack(self.stack)
def print_stack(self):
print(stack_to_string(self.stack), '', end=' ')
if self.viewer:
self.viewer.update_stack(self.stack)
-96
View File
@@ -1,96 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2018 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
I really want tracebacks to show which function was being executed when
an error in the wrapper function happens. In order to do that, you have
to do this (the function in this module.)
Here's what it looks like when you pass too few arguments to e.g. "mul".
>>> from joy.library import _dictionary
>>> m = _dictionary['*']
>>> m((), (), {})
Traceback (most recent call last):
File "<pyshell#49>", line 1, in <module>
m((), (), {})
File "joy/library.py", line 185, in mul:inner
(a, (b, stack)) = stack
ValueError: need more than 0 values to unpack
>>>
Notice that line 185 in the library.py file is (as of this writing) in
the BinaryBuiltinWrapper's inner() function, but this hacky code has
managed to insert the name of the wrapped function ("mul") along with a
colon into the wrapper function's reported name.
Normally I would frown on this sort of mad hackery, but... this is in
the service of ease-of-debugging! Very valuable. And note that all the
hideous patching is finished in the module-load-stage, it shouldn't cause
issues of its own at runtime.
The main problem I see with this is that people coming to this code later
might be mystified if they just see a traceback with a ':' in the
function name! Hopefully they will discover this documentation.
'''
def rename_code_object(new_name):
'''
If you want to wrap a function in another function and have the wrapped
function's name show up in the traceback when an exception occurs in
the wrapper function, you must do this brutal hackery to change the
func.__code__.co_name attribute. Just functools.wraps() is not enough.
See:
https://stackoverflow.com/questions/29919804/function-decorated-using-functools-wraps-raises-typeerror-with-the-name-of-the-w
https://stackoverflow.com/questions/29488327/changing-the-name-of-a-generator/29488561#29488561
I'm just glad it's possible.
'''
def inner(func):
name = new_name + ':' + func.__name__
code_object = func.__code__
return type(func)(
type(code_object)(
code_object.co_argcount,
code_object.co_nlocals,
code_object.co_stacksize,
code_object.co_flags,
code_object.co_code,
code_object.co_consts,
code_object.co_names,
code_object.co_varnames,
code_object.co_filename,
name,
code_object.co_firstlineno,
code_object.co_lnotab,
code_object.co_freevars,
code_object.co_cellvars
),
func.__globals__,
name,
func.__defaults__,
func.__closure__
)
return inner
-241
View File
@@ -1,241 +0,0 @@
'''
A crude compiler for a subset of Joy functions.
I think I'm going about this the wrong way.
The inference algorithm can "collapse" Yin function sequences into
single stack effects which can then be written out as Python functions.
Why not keep track of the new variables introduced as results of Yang
functions, during inference? Could I write out better code that way?
In any event, I am proceeding with this sort of ad hoc way for now.
'''
from __future__ import print_function
from builtins import next
from builtins import str
from builtins import object
from joy.parser import text_to_expression, Symbol
from joy.utils.stack import concat, iter_stack, list_to_stack
from joy.library import SimpleFunctionWrapper, YIN_STACK_EFFECTS
from functools import reduce
def import_yin():
from joy.utils.generated_library import *
return locals()
class InfiniteStack(tuple):
def _names():
n = 0
while True:
m = yield Symbol('a' + str(n))
n = n + 1 if m is None else m
_NAMES = _names()
next(_NAMES)
names = lambda: next(_NAMES)
reset = lambda _self, _n=_NAMES: _n.send(-1)
def __init__(self, code):
self.reset()
self.code = code
def __iter__(self):
if not self:
new_var = self.names()
self.code.append(('pop', new_var))
return iter((new_var, self))
def I(expression):
code = []
stack = InfiniteStack(code)
while expression:
term, expression = expression
if isinstance(term, Symbol):
func = D[term]
stack, expression, _ = func(stack, expression, code)
else:
stack = term, stack
code.append(tuple(['ret'] + list(iter_stack(stack))))
return code
strtup = lambda a, b: '(%s, %s)' % (b, a)
strstk = lambda rest: reduce(strtup, rest, 'stack')
def code_gen(code):
#for p in code: print p
coalesce_pops(code)
lines = []
emit = lines.append
for t in code:
tag, rest = t[0], t[1:]
if tag == 'pop': emit(strstk(rest) + ' = stack')
elif tag == 'call': emit('%s = %s%s' % rest)
elif tag == 'ret': emit('return ' + strstk(rest[::-1]))
else:
raise ValueError(tag)
return '\n'.join(' ' + line for line in lines)
def coalesce_pops(code):
code.sort(key=lambda p: p[0] != 'pop') # All pops to the front.
try: index = next((i for i, t in enumerate(code) if t[0] != 'pop'))
except StopIteration: return
code[:index] = [tuple(['pop'] + [t for _, t in code[:index][::-1]])]
def compile_yinyang(name, text):
return '''
def %s(stack):
%s
''' % (name, code_gen(I(text_to_expression(text))))
def q():
memo = {}
def bar(type_var):
try:
res = memo[type_var]
except KeyError:
res = memo[type_var] = InfiniteStack.names()
return res
return bar
def type_vars_to_labels(thing, map_):
if not thing:
return thing
if not isinstance(thing, tuple):
return map_(thing)
return tuple(type_vars_to_labels(inner, map_) for inner in thing)
def remap_inputs(in_, stack, code):
map_ = q()
while in_:
term, in_ = in_
arg0, stack = stack
term = type_vars_to_labels(term, map_)
code.append(('call', term, '', arg0))
return stack, map_
class BinaryBuiltin(object):
def __init__(self, name):
self.name = name
def __call__(self, stack, expression, code):
in1, (in0, stack) = stack
out = InfiniteStack.names()
code.append(('call', out, self.name, (in0, in1)))
return (out, stack), expression, code
YIN = import_yin()
D = {
name: SimpleFunctionWrapper(YIN[name])
for name in '''
ccons
cons
dup
dupd
dupdd
over
pop
popd
popdd
popop
popopd
popopdd
rolldown
rollup
swap
swons
tuck
unit
'''.split()
}
for name in '''
first
first_two
fourth
rest
rrest
second
third
uncons
unswons
'''.split():
def foo(stack, expression, code, name=name):
in_, out = YIN_STACK_EFFECTS[name]
stack, map_ = remap_inputs(in_, stack, code)
out = type_vars_to_labels(out, map_)
return concat(out, stack), expression, code
foo.__name__ = name
D[name] = foo
for name in '''
eq
ge
gt
le
lt
ne
xor
lshift
rshift
and_
or_
add
floordiv
mod
mul
pow
sub
truediv
'''.split():
D[name.rstrip('-')] = BinaryBuiltin(name)
'''
stack
stuncons
stununcons
swaack
'''
for name in sorted(D):
print(name, end=' ')
## print compile_yinyang(name, name)
print('-' * 100)
print(compile_yinyang('mul_', 'mul'))
print(compile_yinyang('pop', 'pop'))
print(compile_yinyang('ppm', 'popop mul'))
print(compile_yinyang('sqr', 'dup mul'))
print(compile_yinyang('foo', 'dup 23 sub mul'))
print(compile_yinyang('four_mul', 'mul mul mul mul'))
print(compile_yinyang('baz', 'mul dup sub dup'))
print(compile_yinyang('to_the_fifth_power', 'dup dup mul dup mul mul'))
print(compile_yinyang('dup3', 'dup dup dup'))
print(compile_yinyang('df2m', 'dup first_two mul'))
print(compile_yinyang('sqr_first', 'uncons swap dup mul swons'))
print(compile_yinyang('0BAD', 'uncons dup mul'))
print(compile_yinyang('uncons', 'uncons'))
+2
View File
@@ -1,4 +1,6 @@
# GENERATED FILE. DO NOT EDIT.
# The code that generated these functions is in the repo history
# at the v0.4.0 tag.
def _Tree_add_Ee(stack):
-31
View File
@@ -1,31 +0,0 @@
from builtins import str
from joy.parser import Symbol
def _names():
n = 0
while True:
yield Symbol('a' + str(n))
n += 1
class InfiniteStack(tuple):
names = lambda n=_names(): next(n)
def __iter__(self):
if not self:
return iter((self.names(), self))
i = InfiniteStack()
a, b = i
lambda u: (lambda fu, u: fu * fu * u)(
(lambda u: (lambda fu, u: fu * fu)(
(lambda u: (lambda fu, u: fu * fu * u)(
(lambda u: 1)(u), u))(u), u))(u),
u)
lambda u: (lambda fu, u: fu * fu * u)((lambda u: (lambda fu, u: fu * fu)((lambda u: (lambda fu, u: fu * fu * u)((lambda u: 1)(u), u))(u), u))(u), u)
-756
View File
@@ -1,756 +0,0 @@
# -*- coding: utf_8
#
# Copyright © 2018 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
from __future__ import print_function
from builtins import str
from builtins import map
from past.builtins import basestring
from builtins import object
from logging import getLogger, addLevelName
from functools import reduce
_log = getLogger(__name__)
addLevelName(15, 'hmm')
from collections import Counter
from itertools import chain, product
from inspect import stack as inspect_stack
from joy.utils.stack import (
concat,
expression_to_string,
list_to_stack,
stack_to_string,
)
from joy.parser import Symbol, text_to_expression
class AnyJoyType(object):
'''
Joy type variable. Represents any Joy value.
'''
accept = tuple, int, float, int, complex, str, bool, Symbol
prefix = 'a'
def __init__(self, number):
self.number = number
def __repr__(self):
return self.prefix + str(self.number)
def __eq__(self, other):
return (
isinstance(other, self.__class__)
and other.prefix == self.prefix
and other.number == self.number
)
def __ge__(self, other):
return (
issubclass(other.__class__, self.__class__)
or isinstance(other, self.accept)
)
def __le__(self, other):
# 'a string' >= AnyJoyType() should be False.
return issubclass(self.__class__, other.__class__)
def __add__(self, other):
return self.__class__(self.number + other)
__radd__ = __add__
def __hash__(self):
return hash(repr(self))
class BooleanJoyType(AnyJoyType):
accept = bool
prefix = 'b'
class NumberJoyType(AnyJoyType):
accept = bool, int, float, complex
prefix = 'n'
class FloatJoyType(NumberJoyType):
accept = float
prefix = 'f'
class IntJoyType(FloatJoyType):
accept = int
prefix = 'i'
class TextJoyType(AnyJoyType):
accept = basestring
prefix = 't'
class StackJoyType(AnyJoyType):
accept = tuple
prefix = 's'
def __bool__(self):
# Imitate () at the end of cons list.
return False
class KleeneStar(object):
u'''
A sequence of zero or more `AnyJoyType` variables would be:
A*
The `A*` works by splitting the universe into two alternate histories:
A* → ∅
A* → A A*
The Kleene star variable disappears in one universe, and in the other
it turns into an `AnyJoyType` variable followed by itself again.
We have to return all universes (represented by their substitution
dicts, the "unifiers") that don't lead to type conflicts.
'''
kind = AnyJoyType
def __init__(self, number):
assert number
self.number = number
self.count = 0
self.prefix = repr(self)
def __repr__(self):
return '%s%i*' % (self.kind.prefix, self.number)
def another(self):
self.count += 1
return self.kind(10000 * self.number + self.count)
def __eq__(self, other):
return (
isinstance(other, self.__class__)
and other.number == self.number
)
def __ge__(self, other):
return self.kind >= other.kind
def __add__(self, other):
return self.__class__(self.number + other)
__radd__ = __add__
def __hash__(self):
return hash(repr(self))
class AnyStarJoyType(KleeneStar): kind = AnyJoyType
class NumberStarJoyType(KleeneStar): kind = NumberJoyType
class FloatStarJoyType(KleeneStar): kind = FloatJoyType
class IntStarJoyType(KleeneStar): kind = IntJoyType
class StackStarJoyType(KleeneStar): kind = StackJoyType
class TextStarJoyType(KleeneStar): kind = TextJoyType
class FunctionJoyType(AnyJoyType):
def __init__(self, name, sec, number):
self.name = name
self.stack_effects = sec
self.number = number
def __add__(self, other):
return self
__radd__ = __add__
def __repr__(self):
return self.name
class SymbolJoyType(FunctionJoyType):
'''
Represent non-combinator functions.
These type variables carry the stack effect comments and can
appear in expressions (as in quoted programs.)
'''
prefix = 'F'
class CombinatorJoyType(FunctionJoyType):
'''
Represent combinators.
These type variables carry Joy functions that implement the
behaviour of Joy combinators and they can appear in expressions.
For simple combinators the implementation functions can be the
combinators themselves.
These types can also specify a stack effect (input side only) to
guard against being used on invalid types.
'''
prefix = 'C'
def __init__(self, name, sec, number, expect=None):
super(CombinatorJoyType, self).__init__(name, sec, number)
self.expect = expect
def enter_guard(self, f):
if self.expect is None:
return f
g = self.expect, self.expect
new_f = list(poly_compose(f, g, ()))
assert len(new_f) == 1, repr(new_f)
return new_f[0][1]
class JoyTypeError(Exception): pass
def reify(meaning, name, seen=None):
'''
Apply substitution dict to term, returning new term.
'''
if isinstance(name, tuple):
return tuple(reify(meaning, inner) for inner in name)
safety = 101
while name in meaning and safety:
safety -= 1
name = meaning[name]
if not safety:
raise ValueError('Cycle in substitution dict: %s' % (meaning,))
return name
def relabel(left, right):
'''
Re-number type variables to avoid collisions between stack effects.
'''
return left, _1000(right)
def _1000(right):
if isinstance(right, Symbol):
return right
if not isinstance(right, tuple):
return 1000 + right
return tuple(_1000(n) for n in right)
def delabel(f, seen=None, c=None):
'''
Fix up type variable numbers after relabel().
'''
if seen is None:
assert c is None
seen, c = {}, Counter()
try:
return seen[f]
except KeyError:
pass
if not isinstance(f, tuple):
try:
seen[f] = f.__class__(c[f.prefix] + 1)
except (TypeError, # FunctionJoyTypes break this.
AttributeError): # Symbol
seen[f] = f
else:
c[f.prefix] += 1
return seen[f]
return tuple(delabel(inner, seen, c) for inner in f)
def uni_unify(u, v, s=None):
'''
Return a substitution dict representing a unifier for u and v.
'''
if s is None:
s = {}
elif s:
u = reify(s, u)
v = reify(s, v)
if isinstance(u, AnyJoyType) and isinstance(v, AnyJoyType):
if u >= v:
s[u] = v
elif v >= u:
s[v] = u
else:
raise JoyTypeError('Cannot unify %r and %r.' % (u, v))
elif isinstance(u, tuple) and isinstance(v, tuple):
if len(u) != len(v) != 2:
raise ValueError(repr((u, v))) # Bad input.
(a, b), (c, d) = u, v
s = uni_unify(b, d, uni_unify(a, c, s))
elif isinstance(v, tuple):
if not _stacky(u):
raise JoyTypeError('Cannot unify %r and %r.' % (u, v))
s[u] = v
elif isinstance(u, tuple):
if not _stacky(v):
raise JoyTypeError('Cannot unify %r and %r.' % (v, u))
s[v] = u
else:
raise JoyTypeError('Cannot unify %r and %r.' % (u, v))
return s
def _log_uni(U):
def inner(u, v, s=None):
_log.debug(
'%3i %s U %s w/ %s',
len(inspect_stack()), u, v, s,
)
res = U(u, v, s)
_log.debug(
'%3i %s U %s w/ %s => %s',
len(inspect_stack()), u, v, s, res,
)
return res
return inner
@_log_uni
def unify(u, v, s=None):
'''
Return a tuple of substitution dicts representing unifiers for u and v.
'''
if s is None:
s = {}
elif s:
u = reify(s, u)
v = reify(s, v)
if u == v:
res = s,
elif isinstance(u, tuple) and isinstance(v, tuple):
if len(u) != 2 or len(v) != 2:
if _that_one_special_case(u, v):
return s,
raise ValueError(repr((u, v))) # Bad input.
(a, b), (c, d) = v, u
if isinstance(a, KleeneStar):
if isinstance(c, KleeneStar):
s = _lil_uni(a, c, s) # Attempt to unify the two K-stars.
res = unify(d, b, s[0])
else:
# Two universes, in one the Kleene star disappears and
# unification continues without it...
s0 = unify(u, b)
# In the other it spawns a new variable.
s1 = unify(u, (a.another(), v))
res = s0 + s1
for sn in res:
sn.update(s)
elif isinstance(c, KleeneStar):
res = unify(v, d) + unify(v, (c.another(), u))
for sn in res:
sn.update(s)
else:
res = tuple(flatten(unify(d, b, sn) for sn in unify(c, a, s)))
elif isinstance(v, tuple):
if not _stacky(u):
raise JoyTypeError('Cannot unify %r and %r.' % (u, v))
s[u] = v
res = s,
elif isinstance(u, tuple):
if not _stacky(v):
raise JoyTypeError('Cannot unify %r and %r.' % (v, u))
s[v] = u
res = s,
else:
res = _lil_uni(u, v, s)
return res
def _that_one_special_case(u, v):
'''
Handle e.g. ((), (n1*, s1)) when type-checking sum, product, etc...
'''
return (
u == ()
and len(v) == 2
and isinstance(v[0], KleeneStar)
and isinstance(v[1], StackJoyType)
)
def flatten(g):
return list(chain.from_iterable(g))
def _lil_uni(u, v, s):
if u >= v:
s[u] = v
return s,
if v >= u:
s[v] = u
return s,
raise JoyTypeError('Cannot unify %r and %r.' % (u, v))
def _stacky(thing):
return thing.__class__ in {AnyJoyType, StackJoyType}
def _compose(f, g):
'''
Return the stack effect of the composition of two stack effects.
'''
# Relabel, unify, update, delabel.
(f_in, f_out), (g_in, g_out) = relabel(f, g)
fg = reify(uni_unify(g_in, f_out), (f_in, g_out))
return delabel(fg)
def compose(*functions):
'''
Return the stack effect of the composition of some of stack effects.
'''
return reduce(_compose, functions)
def compilable(f):
'''
Return True if a stack effect represents a function that can be
automatically compiled (to Python), False otherwise.
'''
return isinstance(f, tuple) and all(map(compilable, f)) or _stacky(f)
def doc_from_stack_effect(inputs, outputs=('??', ())):
'''
Return a crude string representation of a stack effect.
'''
switch = [False] # Do we need to display the '...' for the rest of the main stack?
i, o = _f(inputs, switch), _f(outputs, switch)
if switch[0]:
i.append('...')
o.append('...')
return '(%s--%s)' % (
' '.join(reversed([''] + i)),
' '.join(reversed(o + [''])),
)
def _f(term, switch):
a = []
while term and isinstance(term, tuple):
item, term = term
a.append(item)
assert isinstance(term, (tuple, StackJoyType)), repr(term)
a = [_to_str(i, term, switch) for i in a]
return a
def _to_str(term, stack, switch):
if not isinstance(term, tuple):
if term == stack:
switch[0] = True
return '[...]'
return (
'[...%i]' % term.number
if isinstance(term, StackJoyType)
else str(term)
)
a = []
while term and isinstance(term, tuple):
item, term = term
a.append(_to_str(item, stack, switch))
assert isinstance(term, (tuple, StackJoyType)), repr(term)
if term == stack:
switch[0] = True
end = '' if term == () else '...'
#end = '...'
else:
end = '' if term == () else '...%i' % term.number
a.append(end)
return '[%s]' % ' '.join(a)
def compile_(name, f, doc=None):
'''
Return a string of Python code implementing the function described
by the stack effect. If no doc string is passed doc_from_stack_effect()
is used to generate one.
'''
i, o = f
if doc is None:
doc = doc_from_stack_effect(i, o)
return '''def %s(stack):
"""
::
%s
"""
%s = stack
return %s''' % (name, doc, i, o)
def _poly_compose(f, g, e):
(f_in, f_out), (g_in, g_out) = f, g
for s in unify(g_in, f_out):
yield reify(s, (e, (f_in, g_out)))
def poly_compose(f, g, e):
'''
Yield the stack effects of the composition of two stack effects. An
expression is carried along and updated and yielded.
'''
f, g = relabel(f, g)
for fg in _poly_compose(f, g, e):
yield delabel(fg)
def _meta_compose(F, G, e):
for f, g in product(F, G):
try:
for result in poly_compose(f, g, e): yield result
except JoyTypeError:
pass
def meta_compose(F, G, e):
'''
Yield the stack effects of the composition of two lists of stack
effects. An expression is carried along and updated and yielded.
'''
res = sorted(set(_meta_compose(F, G, e)))
if not res:
raise JoyTypeError('Cannot unify %r and %r.' % (F, G))
return res
_S0 = StackJoyType(0)
ID = _S0, _S0 # Identity function.
def _infer(e, F=ID):
if __debug__:
_log_it(e, F)
if not e:
return [F]
n, e = e
if isinstance(n, SymbolJoyType):
eFG = meta_compose([F], n.stack_effects, e)
res = flatten(_infer(e, Fn) for e, Fn in eFG)
elif isinstance(n, CombinatorJoyType):
fi, fo = n.enter_guard(F)
res = flatten(_interpret(f, fi, fo, e) for f in n.stack_effects)
elif isinstance(n, Symbol):
if n in FUNCTIONS:
res =_infer((FUNCTIONS[n], e), F)
else:
raise JoyTypeError(n)
# print n
# func = joy.library._dictionary[n]
# res = _interpret(func, F[0], F[1], e)
else:
fi, fo = F
res = _infer(e, (fi, (n, fo)))
return res
def _interpret(f, fi, fo, e):
new_fo, ee, _ = f(fo, e, {})
ee = reify(FUNCTIONS, ee) # Fix Symbols.
new_F = fi, new_fo
return _infer(ee, new_F)
def _log_it(e, F):
_log.log(
15,
u'%3i %s%s',
len(inspect_stack()),
doc_from_stack_effect(*F),
expression_to_string(e),
)
def infer(*expression):
'''
Return a list of stack effects for a Joy expression.
For example::
h = infer(pop, swap, rolldown, rest, rest, cons, cons)
for fi, fo in h:
print doc_from_stack_effect(fi, fo)
Prints::
([a4 a5 ...1] a3 a2 a1 -- [a2 a3 ...1])
'''
return sorted(set(_infer(list_to_stack(expression))))
def infer_string(string):
e = reify(FUNCTIONS, text_to_expression(string)) # Fix Symbols.
return sorted(set(_infer(e)))
def infer_expression(expression):
e = reify(FUNCTIONS, expression) # Fix Symbols.
return sorted(set(_infer(e)))
def type_check(name, stack):
'''
Trinary predicate. True if named function type-checks, False if it
fails, None if it's indeterminate (because I haven't entered it into
the FUNCTIONS dict yet.)
'''
try:
func = FUNCTIONS[name]
except KeyError:
return # None, indicating unknown
if isinstance(func, SymbolJoyType):
secs = func.stack_effects
elif isinstance(func, CombinatorJoyType):
if func.expect is None:
return # None, indicating unknown
secs = [(func.expect, ())]
else:
raise TypeError(repr(func)) # wtf?
for fi, fo in secs:
try:
unify(fi, stack)
except (JoyTypeError, ValueError):
continue
except:
_log.exception(
'Type-checking %s %s against %s',
name,
doc_from_stack_effect(fi, fo),
stack_to_string(stack),
)
continue
return True
return False
FUNCTIONS = {} # Polytypes (lists of stack effects.)
_functions = {} # plain ol' stack effects.
def __(*seq):
stack = StackJoyType(23)
for item in seq: stack = item, stack
return stack
def stack_effect(*inputs):
def _stack_effect(*outputs):
def _apply_to(function):
i, o = _functions[function.name] = __(*inputs), __(*outputs)
d = doc_from_stack_effect(i, o)
function.__doc__ += (
'\nStack effect::\n\n ' # '::' for Sphinx docs.
+ d
)
_log.info('Setting stack effect for %s := %s', function.name, d)
return function
return _apply_to
return _stack_effect
def ef(*inputs):
def _ef(*outputs):
return __(*inputs), __(*outputs)
return _ef
def combinator_effect(number, *expect):
def _combinator_effect(c):
e = __(*expect) if expect else None
FUNCTIONS[c.name] = CombinatorJoyType(c.name, [c], number, e)
if e:
sec = doc_from_stack_effect(e)
_log.info('Setting stack EXPECT for combinator %s := %s', c.name, sec)
return c
return _combinator_effect
def show(DEFS):
for name, stack_effect_comment in sorted(DEFS.items()):
t = ' *'[compilable(stack_effect_comment)]
print(name, '=', doc_from_stack_effect(*stack_effect_comment), t)
def generate_library_code(DEFS, f=None):
if f is None:
import sys
f = sys.stdout
print('# GENERATED FILE. DO NOT EDIT.\n', file=f)
for name, stack_effect_comment in sorted(DEFS.items()):
if not compilable(stack_effect_comment):
continue
print(file=f)
print(compile_(name, stack_effect_comment), file=f)
print(file=f)
def poly_combinator_effect(number, effect_funcs, *expect):
def _poly_combinator_effect(c):
e = __(*expect) if expect else None
FUNCTIONS[c.name] = CombinatorJoyType(c.name, effect_funcs, number, e)
if e:
_log.info('Setting stack EXPECT for combinator %s := %s', c.name, e)
return c
return _poly_combinator_effect
#FUNCTIONS['branch'].expect = s7, (s6, (b1, s5))
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

-163
View File
@@ -1,163 +0,0 @@
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 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.
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
View File
-24
View File
@@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
import joy.vui.main
joy.vui.main.main(*joy.vui.main.init())
-282
View File
@@ -1,282 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
Core
=====================
The core module defines a bunch of system-wide "constants" (some colors
and PyGame event groups), the message classes for Oberon-style message
passing, a "world" class that holds the main context for the system, and
a mainloop class that manages the, uh, main loop (the PyGame event queue.)
'''
from __future__ import print_function
from builtins import object, str, range
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
})
'PyGame mouse events.'
ARROW_KEYS = frozenset({
pygame.K_UP,
pygame.K_DOWN,
pygame.K_LEFT,
pygame.K_RIGHT
})
'PyGame arrow key events.'
TASK_EVENTS = tuple(range(pygame.USEREVENT, pygame.NUMEVENTS))
'Keep track of all possible task events.'
AVAILABLE_TASK_EVENTS = set(TASK_EVENTS)
'Task IDs that have not been assigned to a task.'
ALLOWED_EVENTS = [pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN]
ALLOWED_EVENTS.extend(MOUSE_EVENTS)
ALLOWED_EVENTS.extend(TASK_EVENTS)
'Event "mask" for PyGame event queue, we are only interested in these event types.'
ERROR = -1
PENDING = 0
SUCCESS = 1
# 'Message status codes... dunno if this is a good idea or not...
class Message(object):
'''Message base class. Contains ``sender`` field.'''
def __init__(self, sender):
self.sender = sender
class CommandMessage(Message):
'''For commands, adds ``command`` field.'''
def __init__(self, sender, command):
Message.__init__(self, sender)
self.command = command
class ModifyMessage(Message):
'''
For when resources are modified, adds ``subject`` and ``details``
fields.
'''
def __init__(self, sender, subject, **details):
Message.__init__(self, sender)
self.subject = subject
self.details = details
class OpenMessage(Message):
'''
For when resources are modified, adds ``name``, content_id``,
``status``, and ``traceback`` fields.
'''
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):
'''
For when resources are modified, adds ``content_id`` and ``details``
fields.
'''
def __init__(self, sender, content_id, **details):
Message.__init__(self, sender)
self.content_id = content_id
self.details = details
class ShutdownMessage(Message):
'''Signals that the system is shutting down.'''
# Joy Interpreter & Context
class World(object):
'''
This object contains the system context, the stack, dictionary, a
reference to the display broadcast method, and the log.
'''
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):
'''
Deal with updates to the stack and commands.
'''
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(format_exc(), file=stderr)
return str(self.stack_holder[0])
def push(sender, item, notify, stack_name='stack.pickle'):
'''
Helper function to push an item onto the system stack with message.
'''
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):
'''
Helper function to open a text viewer on a string.
Typically used to show tracebacks.
'''
push(sender, content, notify)
notify(CommandMessage(sender, 'good_viewer_location open_viewer'))
# main loop
class TheLoop(object):
'''
The main loop manages tasks and the PyGame event queue
and framerate clock.
'''
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):
'''
Install a task to run every so many 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):
'''
Remove an installed task.
'''
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):
# Best effort to cancel all running tasks.
for task_event_id in self.tasks:
pygame.time.set_timer(task_event_id, 0)
def run_task(self, task_event_id):
'''
Give a task its time to shine.
'''
task = self.tasks[task_event_id]
try:
task()
except:
traceback = format_exc()
self.remove_task(task_event_id)
print(traceback, file=stderr)
print('TASK removed due to ERROR', task, file=stderr)
open_viewer_on_string(self, traceback, self.display.broadcast)
def loop(self):
'''
The actual main loop machinery.
Maintain a ``running`` flag, pump the PyGame event queue and
handle the events (dispatching to the display), tick the clock.
When the loop is exited (by clicking the window close button or
pressing the ``escape`` key) it broadcasts a ``ShutdownMessage``.
'''
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))
-19
View File
@@ -1,19 +0,0 @@
from __future__ import absolute_import
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
from . 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)
-17
View File
@@ -1,17 +0,0 @@
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
-206
View File
@@ -1,206 +0,0 @@
'''
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 joy.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
-1
View File
@@ -1 +0,0 @@
Joypy log
-51
View File
@@ -1,51 +0,0 @@
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
-85
View File
@@ -1,85 +0,0 @@
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.
-1
View File
@@ -1 +0,0 @@
(t.
-510
View File
@@ -1,510 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
Display
=================
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 __future__ import print_function
from __future__ import division
from builtins import next, object
from past.utils import old_div
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
from joy.vui 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 = old_div(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):
'''
Iterate through all viewers yielding (viewer, x, y) three-tuples.
The x and y coordinates are screen pixels of the top-left corner
of the viewer.
'''
for x, T in self.tracks:
for y, V in T.viewers:
yield V, x, y
def done_resizing(self):
'''
Helper method called directly by ``MenuViewer.mouse_up()`` to (hackily)
update the display when done resizing a viewer.
'''
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):
'''
Broadcast a message to all viewers (except the sender) and all
registered handlers.
'''
for _, track in self.tracks:
track.broadcast(message)
for handler in self.handlers:
handler(message)
def redraw(self):
'''
Redraw all tracks (which will redraw all viewers.)
'''
for _, track in self.tracks:
track.redraw()
def focus(self, viewer):
'''
Set system focus to a given viewer (or no viewer if a track.)
'''
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((
'received event %s Use pygame.event.set_allowed().'
% pygame.event.event_name(event.type)
), file=stderr)
# Catch all exceptions and open a viewer.
except:
err = format_exc()
print(err, file=stderr) # 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)
# This is not UnicodeType. TODO does this need to be fixed?
# self, event.str, 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):
'''
Open and return a ``TextViewer`` on a given file (which must be present
in the ``JOYHOME`` directory.)
'''
viewer = self.open_viewer(x, y, text_viewer.TextViewer)
viewer.content_id, viewer.lines = pt.open(filename)
viewer.draw()
return viewer
class Track(Viewer):
'''
Manage a vertical strip of the display, and the viewers on it.
'''
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):
'''
Broadcast a message to all viewers on this track (except the sender.)
'''
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()
-189
View File
@@ -1,189 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
from io 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())
-278
View File
@@ -1,278 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
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)
'''
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
import base64, os, io, zipfile
def initialize(joy_home):
Z.extractall(joy_home)
def create_data(from_dir='./default_joy_home'):
f = io.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(io.StringIO(base64.decodestring('''\
UEsDBBQAAAAAAORmeE794BlRfgMAAH4DAAAPAAAAZGVmaW5pdGlvbnMudHh0c2VlX3N0YWNrID09
IGdvb2Rfdmlld2VyX2xvY2F0aW9uIG9wZW5fc3RhY2sNCnNlZV9yZXNvdXJjZXMgPT0gbGlzdF9y
ZXNvdXJjZXMgZ29vZF92aWV3ZXJfbG9jYXRpb24gb3Blbl92aWV3ZXINCm9wZW5fcmVzb3VyY2Vf
YXRfZ29vZF9sb2NhdGlvbiA9PSBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQpz
ZWVfbG9nID09ICJsb2cudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9jYXRpb24NCnNlZV9k
ZWZpbml0aW9ucyA9PSAiZGVmaW5pdGlvbnMudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9j
YXRpb24NCnJvdW5kX3RvX2NlbnRzID09IDEwMCAqICsrIGZsb29yIDEwMCAvDQpyZXNldF9sb2cg
PT0gImRlbCBsb2cubGluZXNbMTpdIDsgbG9nLmF0X2xpbmUgPSAwIiBldmFsdWF0ZQ0Kc2VlX21l
bnUgPT0gIm1lbnUudHh0IiBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQoNCiMg
T3JkZXJlZCBCaW5hcnkgVHJlZSBkYXRhc3RydWN0dXJlIGZ1bmN0aW9ucy4NCkJUcmVlLW5ldyA9
PSBzd2FwIFtbXSBbXV0gY29ucyBjb25zDQogX0JUcmVlLVAgPT0gb3ZlciBbcG9wb3AgcG9wb3Ag
Zmlyc3RdIG51bGxhcnkNCiBfQlRyZWUtVD4gPT0gW2NvbnMgY29ucyBkaXBkZF0gY29ucyBjb25z
IGNvbnMgaW5mcmENCiBfQlRyZWUtVDwgPT0gW2NvbnMgY29ucyBkaXBkXSBjb25zIGNvbnMgY29u
cyBpbmZyYQ0KIF9CVHJlZS1FID09IHBvcCBzd2FwIHJvbGw8IHJlc3QgcmVzdCBjb25zIGNvbnMN
CiBfQlRyZWUtcmVjdXIgPT0gX0JUcmVlLVAgW19CVHJlZS1UPl0gW19CVHJlZS1FXSBbX0JUcmVl
LVQ8XSBjbXANCkJUcmVlLWFkZCA9PSBbcG9wb3Agbm90XSBbW3BvcF0gZGlwZCBCVHJlZS1uZXdd
IFtdIFtfQlRyZWUtcmVjdXJdIGdlbnJlYw0KUEsDBBQAAAAAACFrpk7/HHjxGBYAABgWAAAKAAAA
bGlicmFyeS5weScnJw0KVGhpcyBmaWxlIGlzIGV4ZWNmaWxlKCknZCB3aXRoIGEgbmFtZXNwYWNl
IGNvbnRhaW5pbmc6DQoNCiAgRCAtIHRoZSBKb3kgZGljdGlvbmFyeQ0KICBkIC0gdGhlIERpc3Bs
YXkgb2JqZWN0DQogIHB0IC0gdGhlIFBlcnNpc3RUYXNrIG9iamVjdA0KICBsb2cgLSB0aGUgbG9n
LnR4dCB2aWV3ZXINCiAgbG9vcCAtIHRoZSBUaGVMb29wIG1haW4gbG9vcCBvYmplY3QNCiAgc3Rh
Y2tfaG9sZGVyIC0gdGhlIFB5dGhvbiBsaXN0IG9iamVjdCB0aGF0IGhvbGRzIHRoZSBKb3kgc3Rh
Y2sgdHVwbGUNCiAgd29ybGQgLSB0aGUgSm95IGVudmlyb25tZW50DQoNCicnJw0KZnJvbSBqb3ku
bGlicmFyeSBpbXBvcnQgKA0KICAgIERlZmluaXRpb25XcmFwcGVyLA0KICAgIEZ1bmN0aW9uV3Jh
cHBlciwNCiAgICBTaW1wbGVGdW5jdGlvbldyYXBwZXIsDQogICAgKQ0KZnJvbSBqb3kudXRpbHMu
c3RhY2sgaW1wb3J0IGxpc3RfdG9fc3RhY2ssIGNvbmNhdA0KZnJvbSBqb3kudnVpIGltcG9ydCBj
b3JlLCB0ZXh0X3ZpZXdlciwgc3RhY2tfdmlld2VyDQoNCg0KZGVmIGluc3RhbGwoY29tbWFuZCk6
IERbY29tbWFuZC5uYW1lXSA9IGNvbW1hbmQNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9u
V3JhcHBlcg0KZGVmIGxpc3RfcmVzb3VyY2VzKHN0YWNrKToNCiAgICAnJycNCiAgICBQdXQgYSBz
dHJpbmcgb24gdGhlIHN0YWNrIHdpdGggdGhlIG5hbWVzIG9mIGFsbCB0aGUga25vd24gcmVzb3Vy
Y2VzDQogICAgb25lLXBlci1saW5lLg0KICAgICcnJw0KICAgIHJldHVybiAnXG4nLmpvaW4ocHQu
c2NhbigpKSwgc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVm
IG9wZW5fc3RhY2soc3RhY2spOg0KICAgICcnJw0KICAgIEdpdmVuIGEgY29vcmRpbmF0ZSBwYWly
IFt4IHldIChpbiBwaXhlbHMpIG9wZW4gYSBTdGFja1ZpZXdlciB0aGVyZS4NCiAgICAnJycNCiAg
ICAoeCwgKHksIF8pKSwgc3RhY2sgPSBzdGFjaw0KICAgIFYgPSBkLm9wZW5fdmlld2VyKHgsIHks
IHN0YWNrX3ZpZXdlci5TdGFja1ZpZXdlcikNCiAgICBWLmRyYXcoKQ0KICAgIHJldHVybiBzdGFj
aw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgb3Blbl9yZXNvdXJj
ZShzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3ggeV0gKGlu
IHBpeGVscykgYW5kIHRoZSBuYW1lIG9mIGEgcmVzb3VyY2UNCiAgICAoZnJvbSBsaXN0X3Jlc291
cmNlcyBjb21tYW5kKSBvcGVuIGEgdmlld2VyIG9uIHRoYXQgcmVzb3VyY2UgYXQgdGhhdA0KICAg
IGxvY2F0aW9uLg0KICAgICcnJw0KICAgICgoeCwgKHksIF8pKSwgKG5hbWUsIHN0YWNrKSkgPSBz
dGFjaw0KICAgIG9tID0gY29yZS5PcGVuTWVzc2FnZSh3b3JsZCwgbmFtZSkNCiAgICBkLmJyb2Fk
Y2FzdChvbSkNCiAgICBpZiBvbS5zdGF0dXMgPT0gY29yZS5TVUNDRVNTOg0KICAgICAgICBWID0g
ZC5vcGVuX3ZpZXdlcih4LCB5LCB0ZXh0X3ZpZXdlci5UZXh0Vmlld2VyKQ0KICAgICAgICBWLmNv
bnRlbnRfaWQsIFYubGluZXMgPSBvbS5jb250ZW50X2lkLCBvbS50aGluZw0KICAgICAgICBWLmRy
YXcoKQ0KICAgIHJldHVybiBzdGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFw
cGVyDQpkZWYgbmFtZV92aWV3ZXIoc3RhY2spOg0KICAgICcnJw0KICAgIEdpdmVuIGEgc3RyaW5n
IG5hbWUgb24gdGhlIHN0YWNrLCBpZiB0aGUgY3VycmVudGx5IGZvY3VzZWQgdmlld2VyIGlzDQog
ICAgYW5vbnltb3VzLCBuYW1lIHRoZSB2aWV3ZXIgYW5kIHBlcnNpc3QgaXQgaW4gdGhlIHJlc291
cmNlIHN0b3JlIHVuZGVyDQogICAgdGhhdCBuYW1lLg0KICAgICcnJw0KICAgIG5hbWUsIHN0YWNr
ID0gc3RhY2sNCiAgICBhc3NlcnQgaXNpbnN0YW5jZShuYW1lLCBzdHIpLCByZXByKG5hbWUpDQog
ICAgaWYgZC5mb2N1c2VkX3ZpZXdlciBhbmQgbm90IGQuZm9jdXNlZF92aWV3ZXIuY29udGVudF9p
ZDoNCiAgICAgICAgZC5mb2N1c2VkX3ZpZXdlci5jb250ZW50X2lkID0gbmFtZQ0KICAgICAgICBw
bSA9IGNvcmUuUGVyc2lzdE1lc3NhZ2Uod29ybGQsIG5hbWUsIHRoaW5nPWQuZm9jdXNlZF92aWV3
ZXIubGluZXMpDQogICAgICAgIGQuYnJvYWRjYXN0KHBtKQ0KICAgICAgICBkLmZvY3VzZWRfdmll
d2VyLmRyYXdfbWVudSgpDQogICAgcmV0dXJuIHN0YWNrDQoNCg0KIyNAaW5zdGFsbA0KIyNAU2lt
cGxlRnVuY3Rpb25XcmFwcGVyDQojI2RlZiBwZXJzaXN0X3ZpZXdlcihzdGFjayk6DQojIyAgICBp
ZiBzZWxmLmZvY3VzZWRfdmlld2VyOg0KIyMgICAgICAgIA0KIyMgICAgICAgIHNlbGYuZm9jdXNl
ZF92aWV3ZXIuY29udGVudF9pZCA9IG5hbWUNCiMjICAgICAgICBzZWxmLmZvY3VzZWRfdmlld2Vy
LmRyYXdfbWVudSgpDQojIyAgICByZXR1cm4gc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1
bmN0aW9uV3JhcHBlcg0KZGVmIGluc2NyaWJlKHN0YWNrKToNCiAgICAnJycNCiAgICBDcmVhdGUg
YSBuZXcgSm95IGZ1bmN0aW9uIGRlZmluaXRpb24gaW4gdGhlIEpveSBkaWN0aW9uYXJ5LiAgQQ0K
ICAgIGRlZmluaXRpb24gaXMgZ2l2ZW4gYXMgYSBzdHJpbmcgd2l0aCBhIG5hbWUgZm9sbG93ZWQg
YnkgYSBkb3VibGUNCiAgICBlcXVhbCBzaWduIHRoZW4gb25lIG9yIG1vcmUgSm95IGZ1bmN0aW9u
cywgdGhlIGJvZHkuIGZvciBleGFtcGxlOg0KDQogICAgICAgIHNxciA9PSBkdXAgbXVsDQoNCiAg
ICBJZiB5b3Ugd2FudCB0aGUgZGVmaW5pdGlvbiB0byBwZXJzaXN0IG92ZXIgcmVzdGFydHMsIGVu
dGVyIGl0IGludG8NCiAgICB0aGUgZGVmaW5pdGlvbnMudHh0IHJlc291cmNlLg0KICAgICcnJw0K
ICAgIGRlZmluaXRpb24sIHN0YWNrID0gc3RhY2sNCiAgICBEZWZpbml0aW9uV3JhcHBlci5hZGRf
ZGVmKGRlZmluaXRpb24sIEQpDQogICAgcmV0dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1w
bGVGdW5jdGlvbldyYXBwZXINCmRlZiBvcGVuX3ZpZXdlcihzdGFjayk6DQogICAgJycnDQogICAg
R2l2ZW4gYSBjb29yZGluYXRlIHBhaXIgW3ggeV0gKGluIHBpeGVscykgYW5kIGEgc3RyaW5nLCBv
cGVuIGEgbmV3DQogICAgdW5uYW1lZCB2aWV3ZXIgb24gdGhhdCBzdHJpbmcgYXQgdGhhdCBsb2Nh
dGlvbi4NCiAgICAnJycNCiAgICAoKHgsICh5LCBfKSksIChjb250ZW50LCBzdGFjaykpID0gc3Rh
Y2sNCiAgICBWID0gZC5vcGVuX3ZpZXdlcih4LCB5LCB0ZXh0X3ZpZXdlci5UZXh0Vmlld2VyKQ0K
ICAgIFYubGluZXMgPSBjb250ZW50LnNwbGl0bGluZXMoKQ0KICAgIFYuZHJhdygpDQogICAgcmV0
dXJuIHN0YWNrDQoNCg0KQGluc3RhbGwNCkBTaW1wbGVGdW5jdGlvbldyYXBwZXINCmRlZiBnb29k
X3ZpZXdlcl9sb2NhdGlvbihzdGFjayk6DQogICAgJycnDQogICAgTGVhdmUgYSBjb29yZGluYXRl
IHBhaXIgW3ggeV0gKGluIHBpeGVscykgb24gdGhlIHN0YWNrIHRoYXQgd291bGQNCiAgICBiZSBh
IGdvb2QgbG9jYXRpb24gYXQgd2hpY2ggdG8gb3BlbiBhIG5ldyB2aWV3ZXIuICAoVGhlIGhldXJp
c3RpYw0KICAgIGVtcGxveWVkIGlzIHRvIHRha2UgdXAgdGhlIGJvdHRvbSBoYWxmIG9mIHRoZSBj
dXJyZW50bHkgb3BlbiB2aWV3ZXINCiAgICB3aXRoIHRoZSBncmVhdGVzdCBhcmVhLikNCiAgICAn
JycNCiAgICB2aWV3ZXJzID0gbGlzdChkLml0ZXJfdmlld2VycygpKQ0KICAgIGlmIHZpZXdlcnM6
DQogICAgICAgIHZpZXdlcnMuc29ydChrZXk9bGFtYmRhIChWLCB4LCB5KTogVi53ICogVi5oKQ0K
ICAgICAgICBWLCB4LCB5ID0gdmlld2Vyc1stMV0NCiAgICAgICAgY29vcmRzID0gKHggKyAxLCAo
eSArIFYuaCAvIDIsICgpKSkNCiAgICBlbHNlOg0KICAgICAgICBjb29yZHMgPSAoMCwgKDAsICgp
KSkNCiAgICByZXR1cm4gY29vcmRzLCBzdGFjaw0KDQoNCkBpbnN0YWxsDQpARnVuY3Rpb25XcmFw
cGVyDQpkZWYgY21wXyhzdGFjaywgZXhwcmVzc2lvbiwgZGljdGlvbmFyeSk6DQogICAgJycnDQog
ICAgVGhlIGNtcCBjb21iaW5hdG9yIHRha2VzIHR3byB2YWx1ZXMgYW5kIHRocmVlIHF1b3RlZCBw
cm9ncmFtcyBvbiB0aGUNCiAgICBzdGFjayBhbmQgcnVucyBvbmUgb2YgdGhlIHRocmVlIGRlcGVu
ZGluZyBvbiB0aGUgcmVzdWx0cyBvZiBjb21wYXJpbmcNCiAgICB0aGUgdHdvIHZhbHVlczoNCg0K
ICAgICAgICAgICBhIGIgW0ddIFtFXSBbTF0gY21wDQogICAgICAgIC0tLS0tLS0tLS0tLS0tLS0t
LS0tLS0tLS0gYSA+IGINCiAgICAgICAgICAgICAgICBHDQoNCiAgICAgICAgICAgYSBiIFtHXSBb
RV0gW0xdIGNtcA0KICAgICAgICAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIGEgPSBiDQogICAg
ICAgICAgICAgICAgICAgIEUNCg0KICAgICAgICAgICBhIGIgW0ddIFtFXSBbTF0gY21wDQogICAg
ICAgIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gYSA8IGINCiAgICAgICAgICAgICAgICAgICAg
ICAgIEwNCg0KICAgICcnJw0KICAgIEwsIChFLCAoRywgKGIsIChhLCBzdGFjaykpKSkgPSBzdGFj
aw0KICAgIGV4cHJlc3Npb24gPSBjb25jYXQoRyBpZiBhID4gYiBlbHNlIEwgaWYgYSA8IGIgZWxz
ZSBFLCBleHByZXNzaW9uKQ0KICAgIHJldHVybiBzdGFjaywgZXhwcmVzc2lvbiwgZGljdGlvbmFy
eQ0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgbGlzdF92aWV3ZXJz
KHN0YWNrKToNCiAgICAnJycNCiAgICBQdXQgYSBzdHJpbmcgb24gdGhlIHN0YWNrIHdpdGggc29t
ZSBpbmZvcm1hdGlvbiBhYm91dCB0aGUgY3VycmVudGx5DQogICAgb3BlbiB2aWV3ZXJzLCBvbmUt
cGVyLWxpbmUuICBUaGlzIGlzIGtpbmQgb2YgYSBkZW1vIGZ1bmN0aW9uLCByYXRoZXINCiAgICB0
aGFuIHNvbWV0aGluZyByZWFsbHkgdXNlZnVsLg0KICAgICcnJw0KICAgIGxpbmVzID0gW10NCiAg
ICBmb3IgeCwgVCBpbiBkLnRyYWNrczoNCiAgICAgICAgI2xpbmVzLmFwcGVuZCgneDogJWksIHc6
ICVpLCAlcicgJSAoeCwgVC53LCBUKSkNCiAgICAgICAgZm9yIHksIFYgaW4gVC52aWV3ZXJzOg0K
ICAgICAgICAgICAgbGluZXMuYXBwZW5kKCd4OiAlaSB5OiAlaSBoOiAlaSAlciAlcicgJSAoeCwg
eSwgVi5oLCBWLmNvbnRlbnRfaWQsIFYpKQ0KICAgIHJldHVybiAnXG4nLmpvaW4obGluZXMpLCBz
dGFjaw0KDQoNCkBpbnN0YWxsDQpAU2ltcGxlRnVuY3Rpb25XcmFwcGVyDQpkZWYgc3BsaXRsaW5l
cyhzdGFjayk6DQogICAgJycnDQogICAgR2l2ZW4gYSBzdHJpbmcgb24gdGhlIHN0YWNrIHJlcGxh
Y2UgaXQgd2l0aCBhIGxpc3Qgb2YgdGhlIGxpbmVzIGluDQogICAgdGhlIHN0cmluZy4NCiAgICAn
JycNCiAgICB0ZXh0LCBzdGFjayA9IHN0YWNrDQogICAgYXNzZXJ0IGlzaW5zdGFuY2UodGV4dCwg
c3RyKSwgcmVwcih0ZXh0KQ0KICAgIHJldHVybiBsaXN0X3RvX3N0YWNrKHRleHQuc3BsaXRsaW5l
cygpKSwgc3RhY2sNCg0KDQpAaW5zdGFsbA0KQFNpbXBsZUZ1bmN0aW9uV3JhcHBlcg0KZGVmIGhp
eWEoc3RhY2spOg0KICAgICcnJw0KICAgIERlbW8gZnVuY3Rpb24gdG8gaW5zZXJ0ICJIaSBXb3Js
ZCEiIGludG8gdGhlIGN1cnJlbnQgdmlld2VyLCBpZiBhbnkuDQogICAgJycnDQogICAgaWYgZC5m
b2N1c2VkX3ZpZXdlcjoNCiAgICAgICAgZC5mb2N1c2VkX3ZpZXdlci5pbnNlcnQoJ0hpIFdvcmxk
IScpDQogICAgcmV0dXJuIHN0YWNrDQpQSwMEFAAAAAAA5GZ4TkXs5NYLAAAACwAAAAcAAABsb2cu
dHh0Sm95cHkgbG9nDQpQSwMEFAAAAAAA5GZ4Tmf2u80CBQAAAgUAAAgAAABtZW51LnR4dCAgbmFt
ZV92aWV3ZXINCiAgbGlzdF9yZXNvdXJjZXMNCiAgb3Blbl9yZXNvdXJjZV9hdF9nb29kX2xvY2F0
aW9uDQogIGdvb2Rfdmlld2VyX2xvY2F0aW9uDQogIG9wZW5fdmlld2VyDQogIHNlZV9zdGFjaw0K
ICBzZWVfcmVzb3VyY2VzDQogIHNlZV9kZWZpbml0aW9ucw0KICBzZWVfbG9nDQogIHJlc2V0X2xv
Zw0KDQogIGluc2NyaWJlDQogIGV2YWx1YXRlDQoNCiAgcG9wIGNsZWFyICAgIGR1cCBzd2FwDQoN
CiAgYWRkIHN1YiBtdWwgZGl2IHRydWVkaXYgbW9kdWx1cyBkaXZtb2QNCiAgcG0gKysgLS0gc3Vt
IHByb2R1Y3QgcG93IHNxciBzcXJ0DQogIDwgPD0gPSA+PSA+IDw+DQogICYgPDwgPj4NCg0KICBp
IGR1cGRpcA0KDQohPSAlICYgKiAqZnJhY3Rpb24gKmZyYWN0aW9uMCArICsrIC0gLS0gLyA8IDw8
IDw9IDw+ID0gPiA+PSA+PiA/IF4NCmFicyBhZGQgYW5hbW9ycGhpc20gYW5kIGFwcDEgYXBwMiBh
cHAzIGF0IGF2ZXJhZ2UNCmIgYmluYXJ5IGJyYW5jaA0KY2hvaWNlIGNsZWFyIGNsZWF2ZSBjb25j
YXQgY29ucw0KZGluZnJpcnN0IGRpcCBkaXBkIGRpcGRkIGRpc2Vuc3RhY2tlbiBkaXYgZGl2bW9k
IGRvd25fdG9femVybyBkcm9wDQpkdWRpcGQgZHVwIGR1cGQgZHVwZGlwDQplbnN0YWNrZW4gZXEN
CmZpcnN0IGZsYXR0ZW4gZmxvb3IgZmxvb3JkaXYNCmdjZCBnZSBnZW5yZWMgZ2V0aXRlbSBncmFu
ZF9yZXNldCBndA0KaGVscA0KaSBpZCBpZnRlIGluZnJhIGluc2NyaWJlDQprZXlfYmluZGluZ3MN
CmxlIGxlYXN0X2ZyYWN0aW9uIGxvb3AgbHNoaWZ0IGx0DQptYXAgbWF4IG1pbiBtb2QgbW9kdWx1
cyBtb3VzZV9iaW5kaW5ncyBtdWwNCm5lIG5lZyBub3QgbnVsbGFyeQ0Kb2Ygb3Igb3Zlcg0KcGFt
IHBhcnNlIHBpY2sgcG0gcG9wIHBvcGQgcG9wZGQgcG9wb3AgcG93IHByZWQgcHJpbXJlYyBwcm9k
dWN0DQpxdW90ZWQNCnJhbmdlIHJhbmdlX3RvX3plcm8gcmVtIHJlbWFpbmRlciByZW1vdmUgcmVz
ZXRfbG9nIHJlc3QgcmV2ZXJzZQ0Kcm9sbDwgcm9sbD4gcm9sbGRvd24gcm9sbHVwIHJzaGlmdCBy
dW4NCnNlY29uZCBzZWxlY3Qgc2hhcmluZyBzaG93X2xvZyBzaHVudCBzaXplIHNvcnQgc3FyIHNx
cnQgc3RhY2sgc3RlcA0Kc3RlcF96ZXJvIHN1YiBzdWNjIHN1bSBzd2FhY2sgc3dhcCBzd29uY2F0
IHN3b25zDQp0YWtlIHRlcm5hcnkgdGhpcmQgdGltZXMgdHJ1ZWRpdiB0cnV0aHkgdHVjaw0KdW5h
cnkgdW5jb25zIHVuaXF1ZSB1bml0IHVucXVvdGVkIHVuc3RhY2sNCnZvaWQNCndhcnJhbnR5IHdo
aWxlIHdvcmRzDQp4IHhvcg0KemlwDQpQSwMEFAAAAAAA5GZ4TgCrPcaOEAAAjhAAAAsAAABzY3Jh
dGNoLnR4dFdoYXQgaXMgaXQ/DQoNCkEgc2ltcGxlIEdyYXBoaWNhbCBVc2VyIEludGVyZmFjZSBm
b3IgdGhlIEpveSBwcm9ncmFtbWluZyBsYW5ndWFnZSwNCndyaXR0ZW4gdXNpbmcgUHlnYW1lIHRv
IGJ5cGFzcyBYMTEgZXQuIGFsLiwgbW9kZWxlZCBvbiB0aGUgT2Jlcm9uIE9TLCBhbmQNCmludGVu
ZGVkIHRvIGJlIGp1c3QgZnVuY3Rpb25hbCBlbm91Z2ggdG8gc3VwcG9ydCBib290c3RyYXBwaW5n
IGZ1cnRoZXIgSm95DQpkZXZlbG9wbWVudC4NCg0KSXQncyBiYXNpYyBmdW5jdGlvbmFsaXR5IGlz
IG1vcmUtb3ItbGVzcyBhcyBhIGNydWRlIHRleHQgZWRpdG9yIGFsb25nIHdpdGgNCmEgc2ltcGxl
IEpveSBydW50aW1lIChpbnRlcnByZXRlciwgc3RhY2ssIGFuZCBkaWN0aW9uYXJ5LikgIEl0IGF1
dG8tIHNhdmVzDQphbnkgbmFtZWQgZmlsZXMgKGluIGEgdmVyc2lvbmVkIGhvbWUgZGlyZWN0b3J5
KSBhbmQgeW91IGNhbiB3cml0ZSBuZXcgSm95DQpwcmltaXRpdmVzIGluIFB5dGhvbiBhbmQgSm95
IGRlZmluaXRpb25zIGFuZCBpbW1lZGlhdGVseSBpbnN0YWxsIGFuZCB1c2UNCnRoZW0sIGFzIHdl
bGwgYXMgcmVjb3JkaW5nIHRoZW0gZm9yIHJldXNlIChhZnRlciByZXN0YXJ0cy4pDQoNCkN1cnJl
bnRseSwgdGhlcmUgYXJlIG9ubHkgdHdvIGtpbmRzIG9mIChpbnRlcmVzdGluZykgdmlld2Vyczog
VGV4dFZpZXdlcnMNCmFuZCBTdGFja1ZpZXdlci4gVGhlIFRleHRWaWV3ZXJzIGFyZSBjcnVkZSB0
ZXh0IGVkaXRvcnMuICBUaGV5IHByb3ZpZGUNCmp1c3QgZW5vdWdoIGZ1bmN0aW9uYWxpdHkgdG8g
bGV0IHRoZSB1c2VyIHdyaXRlIHRleHQgYW5kIGNvZGUgKFB5dGhvbiBhbmQNCkpveSkgYW5kIGV4
ZWN1dGUgSm95IGZ1bmN0aW9ucy4gIE9uZSBpbXBvcnRhbnQgdGhpbmcgdGhleSBkbyBpcw0KYXV0
b21hdGljYWxseSBzYXZlIHRoZWlyIGNvbnRlbnQgYWZ0ZXIgY2hhbmdlcy4gIE5vIG1vcmUgbG9z
dCB3b3JrLg0KDQpUaGUgU3RhY2tWaWV3ZXIgaXMgYSBzcGVjaWFsaXplZCBUZXh0Vmlld2VyIHRo
YXQgc2hvd3MgdGhlIGNvbnRlbnRzIG9mIHRoZQ0KSm95IHN0YWNrIG9uZSBsaW5lIHBlciBzdGFj
ayBpdGVtLiAgSXQncyBhIHZlcnkgaGFuZHkgdmlzdWFsIGFpZCB0byBrZWVwDQp0cmFjayBvZiB3
aGF0J3MgZ29pbmcgb24uICBUaGVyZSdzIGFsc28gYSBsb2cudHh0IGZpbGUgdGhhdCBnZXRzIHdy
aXR0ZW4NCnRvIHdoZW4gY29tbWFuZHMgYXJlIGV4ZWN1dGVkLCBhbmQgc28gcmVjb3JkcyB0aGUg
bG9nIG9mIHVzZXIgYWN0aW9ucyBhbmQNCnN5c3RlbSBldmVudHMuICBJdCB0ZW5kcyB0byBmaWxs
IHVwIHF1aWNrbHkgc28gdGhlcmUncyBhIHJlc2V0X2xvZyBjb21tYW5kDQp0aGF0IGNsZWFycyBp
dCBvdXQuDQoNClZpZXdlcnMgaGF2ZSAiZ3JvdyIgYW5kICJjbG9zZSIgaW4gdGhlaXIgbWVudSBi
YXJzLiAgVGhlc2UgYXJlIGJ1dHRvbnMuDQpXaGVuIHlvdSByaWdodC1jbGljayBvbiBncm93IGEg
dmlld2VyIGEgY29weSBpcyBjcmVhdGVkIHRoYXQgY292ZXJzIHRoYXQNCnZpZXdlcidzIGVudGly
ZSB0cmFjay4gIElmIHlvdSBncm93IGEgdmlld2VyIHRoYXQgYWxyZWFkeSB0YWtlcyB1cCBpdHMN
Cndob2xlIHRyYWNrIHRoZW4gYSBjb3B5IGlzIGNyZWF0ZWQgdGhhdCB0YWtlcyB1cCBhbiBhZGRp
dGlvbmFsIHRyYWNrLCB1cA0KdG8gdGhlIHdob2xlIHNjcmVlbi4gIENsb3NpbmcgYSB2aWV3ZXIg
anVzdCBkZWxldGVzIHRoYXQgdmlld2VyLCBhbmQgd2hlbg0KYSB0cmFjayBoYXMgbm8gbW9yZSB2
aWV3ZXJzLCBpdCBpcyBkZWxldGVkIGFuZCB0aGF0IGV4cG9zZXMgYW55IHByZXZpb3VzDQp0cmFj
a3MgYW5kIHZpZXdlcnMgdGhhdCB3ZXJlIGhpZGRlbi4NCg0KKE5vdGU6IGlmIHlvdSBldmVyIGNs
b3NlIGFsbCB0aGUgdmlld2VycyBhbmQgYXJlIHNpdHRpbmcgYXQgYSBibGFuayBzY3JlZW4NCndp
dGggIG5vd2hlcmUgdG8gdHlwZSBhbmQgZXhlY3V0ZSBjb21tYW5kcywgcHJlc3MgdGhlIFBhdXNl
L0JyZWFrIGtleS4NClRoaXMgd2lsbCBvcGVuIGEgbmV3ICJ0cmFwIiB2aWV3ZXIgd2hpY2ggeW91
IGNhbiB0aGVuIHVzZSB0byByZWNvdmVyLikNCg0KQ29waWVzIG9mIGEgdmlld2VyIGFsbCBzaGFy
ZSB0aGUgc2FtZSBtb2RlbCBhbmQgdXBkYXRlIHRoZWlyIGRpc3BsYXkgYXMgaXQNCmNoYW5nZXMu
IChJZiB5b3UgaGF2ZSB0d28gdmlld2VycyBvcGVuIG9uIHRoZSBzYW1lIG5hbWVkIHJlc291cmNl
IGFuZCBlZGl0DQpvbmUgeW91J2xsIHNlZSB0aGUgb3RoZXIgdXBkYXRlIGFzIHlvdSB0eXBlLikN
Cg0KVUkgR3VpZGUNCg0KbGVmdCBtb3VzZSBzZXRzIGN1cnNvciBpbiB0ZXh0LCBpbiBtZW51IGJh
ciByZXNpemVzIHZpZXdlciBpbnRlcmFjdGl2ZWx5DQoodGhpcyBpcyBhIGxpdHRsZSBidWdneSBp
biB0aGF0IHlvdSBjYW4gbW92ZSB0aGUgbW91c2UgcXVpY2tseSBhbmQgZ2V0DQpvdXRzaWRlIHRo
ZSBtZW51LCBsZWF2aW5nIHRoZSB2aWV3ZXIgaW4gdGhlICJyZXNpemluZyIgc3RhdGUuIFVudGls
IEkgZml4DQp0aGlzLCB0aGUgd29ya2Fyb3VuZCBpcyB0byBqdXN0IGdyYWIgdGhlIG1lbnUgYmFy
IGFnYWluIGFuZCB3aWdnbGUgaXQgYQ0KZmV3IHBpeGVscyBhbmQgbGV0IGdvLiAgVGhpcyB3aWxs
IHJlc2V0IHRoZSBtYWNoaW5lcnkuKQ0KDQpSaWdodCBtb3VzZSBleGVjdXRlcyBKb3kgY29tbWFu
ZCAoZnVuY3Rpb25zKSwgYW5kIHlvdSBjYW4gZHJhZyB3aXRoIHRoZQ0KcmlnaHQgYnV0dG9uIHRv
IGhpZ2hsaWdodCAod2VsbCwgdW5kZXJsaW5lKSBjb21tYW5kcy4gIFdvcmRzIHRoYXQgYXJlbid0
DQpuYW1lcyBvZiBKb3kgY29tbWFuZHMgd29uJ3QgYmUgdW5kZXJsaW5lZC4gIFJlbGVhc2UgdGhl
IGJ1dHRvbiB0byBleGVjdXRlDQp0aGUgY29tbWFuZC4NCg0KVGhlIG1pZGRsZSBtb3VzZSBidXR0
b24gKHVzdWFsbHkgYSB3aGVlbCB0aGVzZSBkYXlzKSBzY3JvbGxzIHRoZSB0ZXh0IGJ1dA0KeW91
IGNhbiBhbHNvIGNsaWNrIGFuZCBkcmFnIGFueSB2aWV3ZXIgd2l0aCBpdCB0byBtb3ZlIHRoYXQg
dmlld2VyIHRvDQphbm90aGVyIHRyYWNrIG9yIHRvIGEgZGlmZmVyZW50IGxvY2F0aW9uIGluIHRo
ZSBzYW1lIHRyYWNrLiAgVGhlcmUncyBubw0KZGlyZWN0IHZpc3VhbCBmZWVkYmFjayBmb3IgdGhp
cyAoeWV0KSBidXQgdGhhdCBkb3Nlbid0IHNlZW0gdG8gaW1wYWlyIGl0cw0KdXNlZnVsbmVzcy4N
Cg0KRjEsIEYyIC0gc2V0IHNlbGVjdGlvbiBiZWdpbiBhbmQgZW5kIG1hcmtlcnMgKGNydWRlIGJ1
dCB1c2FibGUuKQ0KDQpGMyAtIGNvcHkgc2VsZWN0ZWQgdGV4dCB0byB0aGUgdG9wIG9mIHRoZSBz
dGFjay4NCg0KU2hpZnQtRjMgLSBhcyBjb3B5IHRoZW4gcnVuICJwYXJzZSIgY29tbWFuZCBvbiB0
aGUgc3RyaW5nLg0KDQpGNCAtIGN1dCBzZWxlY3RlZCB0ZXh0IHRvIHRoZSB0b3Agb2YgdGhlIHN0
YWNrLg0KDQpTaGlmdC1GNCAtIGFzIGN1dCB0aGVuIHJ1biAicG9wIiAoZGVsZXRlIHNlbGVjdGlv
bi4pDQoNCkpveQ0KDQpQcmV0dHkgbXVjaCBhbGwgb2YgdGhlIHJlc3Qgb2YgdGhlIGZ1bmN0aW9u
YWxpdHkgb2YgdGhlIHN5c3RlbSBpcyBwcm92aWRlZA0KYnkgZXhlY3V0aW5nIEpveSBjb21tYW5k
cyAoYWthIGZ1bmN0aW9ucywgYWthICJ3b3JkcyIgaW4gRm9ydGgpIGJ5IHJpZ2h0LQ0KY2xpY2tp
bmcgb24gdGhlaXIgbmFtZXMgaW4gYW55IHRleHQuDQoNClRvIGdldCBoZWxwIG9uIGEgSm95IGZ1
bmN0aW9uIHNlbGVjdCB0aGUgbmFtZSBvZiB0aGUgZnVuY3Rpb24gaW4gYQ0KVGV4dFZpZXdlciB1
c2luZyBGMSBhbmQgRjIsIHRoZW4gcHJlc3Mgc2hpZnQtRjMgdG8gcGFyc2UgdGhlIHNlbGVjdGlv
bi4NClRoZSBmdW5jdGlvbiAocmVhbGx5IGl0cyBTeW1ib2wpIHdpbGwgYXBwZWFyIG9uIHRoZSBz
dGFjayBpbiBicmFja2V0cyAoYQ0KInF1b3RlZCBwcm9ncmFtIiBzdWNoIGFzICJbcG9wXSIuKSAg
VGhlbiByaWdodC1jbGljayBvbiB0aGUgd29yZCBoZWxwIGluDQphbnkgVGV4dFZpZXdlciAoaWYg
aXQncyBub3QgYWxyZWFkeSB0aGVyZSwganVzdCB0eXBlIGl0IGluIHNvbWV3aGVyZS4pDQpUaGlz
IHdpbGwgcHJpbnQgdGhlIGRvY3N0cmluZyBvciBkZWZpbml0aW9uIG9mIHRoZSB3b3JkIChmdW5j
dGlvbikgdG8NCnN0ZG91dC4gIEF0IHNvbWUgcG9pbnQgSSdsbCB3cml0ZSBhIHRoaW5nIHRvIHNl
bmQgdGhhdCB0byB0aGUgbG9nLnR4dCBmaWxlDQppbnN0ZWFkLCBidXQgZm9yIG5vdyBsb29rIGZv
ciBvdXRwdXQgaW4gdGhlIHRlcm1pbmFsLg0KUEsDBBQAAAAAAORmeE53f5peAwAAAAMAAAAMAAAA
c3RhY2sucGlja2xlKHQuUEsBAhQAFAAAAAAA5GZ4Tv3gGVF+AwAAfgMAAA8AAAAAAAAAAAAAALaB
AAAAAGRlZmluaXRpb25zLnR4dFBLAQIUABQAAAAAACFrpk7/HHjxGBYAABgWAAAKAAAAAAAAAAAA
AAC2gasDAABsaWJyYXJ5LnB5UEsBAhQAFAAAAAAA5GZ4TkXs5NYLAAAACwAAAAcAAAAAAAAAAAAA
ALaB6xkAAGxvZy50eHRQSwECFAAUAAAAAADkZnhOZ/a7zQIFAAACBQAACAAAAAAAAAAAAAAAtoEb
GgAAbWVudS50eHRQSwECFAAUAAAAAADkZnhOAKs9xo4QAACOEAAACwAAAAAAAAAAAAAAtoFDHwAA
c2NyYXRjaC50eHRQSwECFAAUAAAAAADkZnhOd3+aXgMAAAADAAAADAAAAAAAAAAAAAAAtoH6LwAA
c3RhY2sucGlja2xlUEsFBgAAAAAGAAYAUwEAACcwAAAAAA==''')))
if __name__ == '__main__':
print(create_data())
-179
View File
@@ -1,179 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
Main Module
======================================
Pulls everything together.
'''
from __future__ import print_function
from __future__ import division
from past.builtins import execfile
from builtins import object
from past.utils import old_div
import os, sys, traceback
import pygame
from joy.library import initialize, DefinitionWrapper, SimpleFunctionWrapper
from joy.vui 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('~/.thun')
if not os.path.isabs(JOY_HOME):
raise ValueError('what directory?')
def load_definitions(pt, dictionary):
'''Load definitions from ``definitions.txt``.'''
lines = pt.open('definitions.txt')[1]
for line in lines:
if '==' in line:
DefinitionWrapper.add_def(line, dictionary)
def load_primitives(home, name_space):
'''Load primitives from ``library.py``.'''
fn = os.path.join(home, 'library.py')
if os.path.exists(fn):
execfile(fn, name_space)
def init():
'''
Initialize the system.
* Init PyGame
* Create main window
* Start the PyGame clock
* Set the event mask
* Create the PersistTask
'''
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):
'''
More initialization
* Create the Joy dictionary
* Create the Display
* Open the log, menu, and scratch text viewers, and the stack pickle
* Start the main loop
* Create the World object
* Register PersistTask and World message handlers with the Display
* Load user function definitions.
'''
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, old_div(d.h, 3), 'menu.txt')
t = d.init_text(pt, old_div(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):
'''
Run a loop function, retry for ``n`` exceptions.
Prints tracebacks on ``sys.stderr``.
'''
error_count = 0
while error_count < n:
try:
loop()
break
except:
traceback.print_exc(file=sys.stderr)
error_count += 1
class FileFaker(object):
'''Pretends to be a file object but writes to log instead.'''
def __init__(self, log):
self.log = log
def write(self, text):
'''Write text to log.'''
self.log.append(text)
def flush(self):
pass
def main(screen, clock, pt):
'''
Main function.
* Call ``init_context()``
* Load primitives
* Create an ``evaluate`` function that lets you just eval some Python code
* Redirect ``stdout`` to the log using a ``FileFaker`` object, and...
* Start the main loop.
'''
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, 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']
-274
View File
@@ -1,274 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
Persist Task
===========================
This module deals with persisting the "resources" (text files and the
stack) to the git repo in the ``JOY_HOME`` directory.
'''
from __future__ import print_function
from builtins import object
import os, pickle, traceback
from collections import Counter
from dulwich.errors import NotGitRepository
from dulwich.repo import Repo
from joy.vui import core, init_joy_home
def open_repo(repo_dir=None, initialize=False):
'''
Open, or create, and return a Dulwich git repo object for the given
directory. If the dir path doesn't exist it will be created. If it
does exist but isn't a repo the result depends on the ``initialize``
argument. If it is ``False`` (the default) a ``NotGitRepository``
exception is raised, otherwise ``git init`` is effected in the dir.
'''
if not os.path.exists(repo_dir):
os.makedirs(repo_dir, 0o700)
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):
'''
Initialize a git repository in the directory. Stage and commit all
files (toplevel, not those in subdirectories if any) in the 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):
'''
Helper function to return a function that returns a path given a path,
that's relative to the repository.
'''
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):
'''
Handle the content of a text files as a list of lines, deal with
saving it and staging the changes to a repo.
'''
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(line, file=f)
def persist(self, repo):
'''
Save the lines to the file and stage the file in the repo.
'''
with open(self.filename, 'w') as f:
os.chmod(self.filename, 0o600)
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):
'''
A ``Resource`` subclass that uses ``pickle`` on its file/thing.
'''
def _from_file(self, f):
return [pickle.load(f)]
def _to_file(self, f):
pickle.dump(self.thing[0], f, protocol=2)
class PersistTask(object):
'''
This class deals with saving changes to the git repo.
'''
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 named file in home and return its content_id and 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):
'''
Handle messages, dispatch to ``handle_FOO()`` methods.
'''
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):
'''
Foo.
'''
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):
'''
Foo.
'''
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):
'''
Foo.
'''
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):
'''
Foo.
'''
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):
'''
Persist a resource.
'''
del self.counter[content_id]
self.store[content_id].persist(self.repo)
def task_run(self):
'''
Stage any outstanding changes.
'''
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'):
'''
Commit.
'''
return self.repo.do_commit(message, committer=core.COMMITTER)
def scan(self):
'''
Return a sorted list of all the files in the home dir.
'''
return sorted([
fn
for fn in os.listdir(self.home)
if os.path.isfile(os.path.join(self.home, fn))
])
def check_filename(name):
'''
Sanity checks for filename.
'''
# 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('~/.thun')
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)
-75
View File
@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
Stack Viewer
=================
'''
from builtins import map, str
from joy.utils.stack import expression_to_string, iter_stack
from joy.vui 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:
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[:] = list(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()
-704
View File
@@ -1,704 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 Simon Forman
#
# This file is part of Thun
#
# Thun 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.
#
# Thun 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 Thun. If not see <http://www.gnu.org/licenses/>.
#
'''
Text Viewer
=================
'''
from __future__ import print_function
from __future__ import division
from builtins import object, range, str, zip
from past.builtins import basestring
from past.utils import old_div
import string
import pygame
from joy.utils.stack import expression_to_string
from joy.vui.core import (
ARROW_KEYS,
BACKGROUND as BG,
FOREGROUND as FG,
CommandMessage,
ModifyMessage,
OpenMessage,
SUCCESS,
push,
)
from joy.vui 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, old_div(self.line_h, 2) - 3,
self.char_w - 2, old_div(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 = old_div(self.body_rect.w, FONT.char_w) + 1
self.h_in_lines = old_div(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 = range(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
old_div(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 + old_div(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(old_div(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)
-249
View File
@@ -1,249 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2019 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/>.
#
'''
Viewer
=================
'''
from __future__ import print_function
from __future__ import division
from builtins import chr, object
from past.utils import old_div
import pygame
from joy.vui.core import BACKGROUND, FOREGROUND
class Viewer(object):
'''
Base Viewer class
'''
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):
'''
MenuViewer class
'''
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), end=' ')
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), (old_div(w, 2), 1), (w, h), (1, old_div(h, 2))
), blend)