diff --git a/joy/vui/core.py b/joy/vui/core.py index ec8aeed..d31a84c 100644 --- a/joy/vui/core.py +++ b/joy/vui/core.py @@ -1,280 +1,281 @@ -# -*- 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 . -# -''' - -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 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 ' - - -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 >> stderr, format_exc() - 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 >> stderr, traceback - print >> stderr, 'TASK removed due to ERROR', task - 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)) +# -*- 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 . +# +''' + +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 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 ' + + +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)) diff --git a/joy/vui/debug_main.py b/joy/vui/debug_main.py index 128a993..aeda7b3 100644 --- a/joy/vui/debug_main.py +++ b/joy/vui/debug_main.py @@ -1,18 +1,19 @@ -import sys, traceback - -# To enable "hot" reloading in the IDLE shell. -for name in 'core main display viewer text_viewer stack_viewer persist_task'.split(): - try: - del sys.modules[name] - except KeyError: - pass - -import main - -try: - A = A # (screen, clock, pt), three things that we DON'T want to recreate - # each time we restart main(). -except NameError: - A = main.init() - -d = main.main(*A) +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) diff --git a/joy/vui/display.py b/joy/vui/display.py index d479e7c..2e94417 100644 --- a/joy/vui/display.py +++ b/joy/vui/display.py @@ -1,504 +1,505 @@ -# -*- 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 . -# -''' - -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 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 = 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 >> stderr, ( - 'received event %s Use pygame.event.set_allowed().' - % pygame.event.event_name(event.type) - ) - # Catch all exceptions and open a viewer. - except: - err = format_exc() - print >> stderr, err # To be safe just print it right away. - open_viewer_on_string(self, err, self.broadcast) - - def _keyboard_event(self, event): - if event.key == pygame.K_PAUSE and event.type == pygame.KEYUP: - # At least on my keyboard the break/pause key sends K_PAUSE. - # The main use of this is to open a TextViewer if you - # accidentally close all the viewers, so you can recover. - raise KeyboardInterrupt('break') - if not self.focused_viewer: - return - if event.type == pygame.KEYUP: - self.focused_viewer.key_up(self, event.key, event.mod) - elif event.type == pygame.KEYDOWN: - self.focused_viewer.key_down( - self, event.unicode, event.key, event.mod) - - def _mouse_event(self, event): - V, x, y = self.at(*event.pos) - - if event.type == pygame.MOUSEMOTION: - if not isinstance(V, Track): - V.mouse_motion(self, x, y, *(event.rel + event.buttons)) - - elif event.type == pygame.MOUSEBUTTONDOWN: - if event.button == 1: - self.focus(V) - V.mouse_down(self, x, y, event.button) - - else: - assert event.type == pygame.MOUSEBUTTONUP - - # Check for moving viewer. - if (event.button == 2 - and self.focused_viewer - and V is not self.focused_viewer - and V.MINIMUM_HEIGHT < y < V.h - self.focused_viewer.MINIMUM_HEIGHT - ): - self._move_viewer(V, y, self.focused_viewer, *event.pos) - - else: - V.mouse_up(self, x, y, event.button) - - def init_text(self, pt, x, y, filename): - ''' - 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() +# -*- 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 . +# +''' + +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 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 = 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) + + 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() diff --git a/joy/vui/font_data.py b/joy/vui/font_data.py index ff84e51..7f2fc48 100644 --- a/joy/vui/font_data.py +++ b/joy/vui/font_data.py @@ -1,186 +1,187 @@ -# -*- 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 . -# -from StringIO import StringIO -import base64, zlib - - -def create(fn='Iosevka12.BMP'): - with open(fn, 'rb') as f: - data = f.read() - return base64.encodestring(zlib.compress(data)) - - -data = StringIO(zlib.decompress(base64.decodestring('''\ -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 -lnalXc/9SsNb2vUirzS8pV0v8gJv/w/2vRht'''))) - - -if __name__ == '__main__': - print create() +# -*- 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 . +# +from __future__ import print_function +from StringIO import StringIO +import base64, zlib + + +def create(fn='Iosevka12.BMP'): + with open(fn, 'rb') as f: + data = f.read() + return base64.encodestring(zlib.compress(data)) + + +data = StringIO(zlib.decompress(base64.decodestring('''\ +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 +lnalXc/9SsNb2vUirzS8pV0v8gJv/w/2vRht'''))) + + +if __name__ == '__main__': + print(create()) diff --git a/joy/vui/init_joy_home.py b/joy/vui/init_joy_home.py index 52ea90f..ec494a8 100644 --- a/joy/vui/init_joy_home.py +++ b/joy/vui/init_joy_home.py @@ -1,275 +1,276 @@ -# -*- 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 . -# -''' -Utility module to help with setting up the initial contents of the -JOY_HOME directory. - -These contents are kept in this Python module as a base64-encoded zip -file, so you can just do, e.g.: - - import init_joy_home - init_joy_home.initialize(JOY_HOME) - -''' -import base64, os, StringIO, zipfile - - -def initialize(joy_home): - Z.extractall(joy_home) - - -def create_data(from_dir='./default_joy_home'): - f = StringIO.StringIO() - z = zipfile.ZipFile(f, mode='w') - for fn in os.listdir(from_dir): - from_fn = os.path.join(from_dir, fn) - z.write(from_fn, fn) - z.close() - return base64.encodestring(f.getvalue()) - - -Z = zipfile.ZipFile(StringIO.StringIO(base64.decodestring('''\ -UEsDBBQAAAAAAORmeE794BlRfgMAAH4DAAAPAAAAZGVmaW5pdGlvbnMudHh0c2VlX3N0YWNrID09 -IGdvb2Rfdmlld2VyX2xvY2F0aW9uIG9wZW5fc3RhY2sNCnNlZV9yZXNvdXJjZXMgPT0gbGlzdF9y -ZXNvdXJjZXMgZ29vZF92aWV3ZXJfbG9jYXRpb24gb3Blbl92aWV3ZXINCm9wZW5fcmVzb3VyY2Vf -YXRfZ29vZF9sb2NhdGlvbiA9PSBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQpz -ZWVfbG9nID09ICJsb2cudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9jYXRpb24NCnNlZV9k -ZWZpbml0aW9ucyA9PSAiZGVmaW5pdGlvbnMudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9j -YXRpb24NCnJvdW5kX3RvX2NlbnRzID09IDEwMCAqICsrIGZsb29yIDEwMCAvDQpyZXNldF9sb2cg -PT0gImRlbCBsb2cubGluZXNbMTpdIDsgbG9nLmF0X2xpbmUgPSAwIiBldmFsdWF0ZQ0Kc2VlX21l -bnUgPT0gIm1lbnUudHh0IiBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQoNCiMg -T3JkZXJlZCBCaW5hcnkgVHJlZSBkYXRhc3RydWN0dXJlIGZ1bmN0aW9ucy4NCkJUcmVlLW5ldyA9 -PSBzd2FwIFtbXSBbXV0gY29ucyBjb25zDQogX0JUcmVlLVAgPT0gb3ZlciBbcG9wb3AgcG9wb3Ag -Zmlyc3RdIG51bGxhcnkNCiBfQlRyZWUtVD4gPT0gW2NvbnMgY29ucyBkaXBkZF0gY29ucyBjb25z -IGNvbnMgaW5mcmENCiBfQlRyZWUtVDwgPT0gW2NvbnMgY29ucyBkaXBkXSBjb25zIGNvbnMgY29u -cyBpbmZyYQ0KIF9CVHJlZS1FID09IHBvcCBzd2FwIHJvbGw8IHJlc3QgcmVzdCBjb25zIGNvbnMN -CiBfQlRyZWUtcmVjdXIgPT0gX0JUcmVlLVAgW19CVHJlZS1UPl0gW19CVHJlZS1FXSBbX0JUcmVl -LVQ8XSBjbXANCkJUcmVlLWFkZCA9PSBbcG9wb3Agbm90XSBbW3BvcF0gZGlwZCBCVHJlZS1uZXdd -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() +# -*- 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 . +# +''' +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 +import base64, os, StringIO, zipfile + + +def initialize(joy_home): + Z.extractall(joy_home) + + +def create_data(from_dir='./default_joy_home'): + f = StringIO.StringIO() + z = zipfile.ZipFile(f, mode='w') + for fn in os.listdir(from_dir): + from_fn = os.path.join(from_dir, fn) + z.write(from_fn, fn) + z.close() + return base64.encodestring(f.getvalue()) + + +Z = zipfile.ZipFile(StringIO.StringIO(base64.decodestring('''\ +UEsDBBQAAAAAAORmeE794BlRfgMAAH4DAAAPAAAAZGVmaW5pdGlvbnMudHh0c2VlX3N0YWNrID09 +IGdvb2Rfdmlld2VyX2xvY2F0aW9uIG9wZW5fc3RhY2sNCnNlZV9yZXNvdXJjZXMgPT0gbGlzdF9y +ZXNvdXJjZXMgZ29vZF92aWV3ZXJfbG9jYXRpb24gb3Blbl92aWV3ZXINCm9wZW5fcmVzb3VyY2Vf +YXRfZ29vZF9sb2NhdGlvbiA9PSBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQpz +ZWVfbG9nID09ICJsb2cudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9jYXRpb24NCnNlZV9k +ZWZpbml0aW9ucyA9PSAiZGVmaW5pdGlvbnMudHh0IiBvcGVuX3Jlc291cmNlX2F0X2dvb2RfbG9j +YXRpb24NCnJvdW5kX3RvX2NlbnRzID09IDEwMCAqICsrIGZsb29yIDEwMCAvDQpyZXNldF9sb2cg +PT0gImRlbCBsb2cubGluZXNbMTpdIDsgbG9nLmF0X2xpbmUgPSAwIiBldmFsdWF0ZQ0Kc2VlX21l +bnUgPT0gIm1lbnUudHh0IiBnb29kX3ZpZXdlcl9sb2NhdGlvbiBvcGVuX3Jlc291cmNlDQoNCiMg +T3JkZXJlZCBCaW5hcnkgVHJlZSBkYXRhc3RydWN0dXJlIGZ1bmN0aW9ucy4NCkJUcmVlLW5ldyA9 +PSBzd2FwIFtbXSBbXV0gY29ucyBjb25zDQogX0JUcmVlLVAgPT0gb3ZlciBbcG9wb3AgcG9wb3Ag +Zmlyc3RdIG51bGxhcnkNCiBfQlRyZWUtVD4gPT0gW2NvbnMgY29ucyBkaXBkZF0gY29ucyBjb25z +IGNvbnMgaW5mcmENCiBfQlRyZWUtVDwgPT0gW2NvbnMgY29ucyBkaXBkXSBjb25zIGNvbnMgY29u +cyBpbmZyYQ0KIF9CVHJlZS1FID09IHBvcCBzd2FwIHJvbGw8IHJlc3QgcmVzdCBjb25zIGNvbnMN +CiBfQlRyZWUtcmVjdXIgPT0gX0JUcmVlLVAgW19CVHJlZS1UPl0gW19CVHJlZS1FXSBbX0JUcmVl +LVQ8XSBjbXANCkJUcmVlLWFkZCA9PSBbcG9wb3Agbm90XSBbW3BvcF0gZGlwZCBCVHJlZS1uZXdd +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()) diff --git a/joy/vui/main.py b/joy/vui/main.py index 399d9d2..8cd5fe3 100644 --- a/joy/vui/main.py +++ b/joy/vui/main.py @@ -1,174 +1,175 @@ -# -*- 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 . -# -''' - -Main Module -====================================== - -Pulls everything together. - -''' -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, d.h / 3, 'menu.txt') - t = d.init_text(pt, d.w / 2, 0, 'scratch.txt') - loop = core.TheLoop(d, clock) - stack_id, stack_holder = pt.open('stack.pickle') - world = core.World(stack_id, stack_holder, D, d.broadcast, log) - loop.install_task(pt.task_run, 10000) # save files every ten seconds - d.handlers.append(pt.handle) - d.handlers.append(world.handle) - load_definitions(pt, D) - return locals() - - -def error_guard(loop, n=10): - ''' - 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 in name_space.copy() - return stack - - name_space['D']['evaluate'] = evaluate - - - sys.stdout, old_stdout = FileFaker(name_space['log']), sys.stdout - try: - error_guard(name_space['loop'].loop) - finally: - sys.stdout = old_stdout - - return name_space['d'] +# -*- 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 . +# +''' + +Main Module +====================================== + +Pulls everything together. + +''' +from __future__ import print_function +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, d.h / 3, 'menu.txt') + t = d.init_text(pt, d.w / 2, 0, 'scratch.txt') + loop = core.TheLoop(d, clock) + stack_id, stack_holder = pt.open('stack.pickle') + world = core.World(stack_id, stack_holder, D, d.broadcast, log) + loop.install_task(pt.task_run, 10000) # save files every ten seconds + d.handlers.append(pt.handle) + d.handlers.append(world.handle) + load_definitions(pt, D) + return locals() + + +def error_guard(loop, n=10): + ''' + 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'] diff --git a/joy/vui/persist_task.py b/joy/vui/persist_task.py index 7d99405..662fc43 100644 --- a/joy/vui/persist_task.py +++ b/joy/vui/persist_task.py @@ -1,272 +1,273 @@ -# -*- 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 . -# -''' - -Persist Task -=========================== - -This module deals with persisting the "resources" (text files and the -stack) to the git repo in the ``JOY_HOME`` directory. - -''' -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, 0700) - return init_repo(repo_dir) - try: - return Repo(repo_dir) - except NotGitRepository: - if initialize: - return init_repo(repo_dir) - raise - - -def init_repo(repo_dir): - ''' - 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 >> f, line - - 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, 0600) - self._to_file(f) - f.flush() - os.fsync(f.fileno()) - # For goodness's sake, write it to the disk already! - repo.stage([self.repo_relative_filename]) - - -class PickledResource(Resource): - ''' - 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) - - -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 +# -*- 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 . +# +''' + +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 +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) + + +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) diff --git a/joy/vui/text_viewer.py b/joy/vui/text_viewer.py index 4b2fa5f..6e1f9b2 100644 --- a/joy/vui/text_viewer.py +++ b/joy/vui/text_viewer.py @@ -1,699 +1,700 @@ -# -*- 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 . -# -''' - -Text Viewer -================= - -''' -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, self.line_h / 2 - 3, - self.char_w - 2, self.line_h / 2) - pygame.draw.rect(surface, FG, r, 1) - else: - iy, ix = divmod(i, 26) - ix *= self.char_w - iy *= self.char_h - area = ix, iy, self.char_w, self.char_h - surface.blit(self.IMAGE, (x, 0), area) - x += self.char_w - return surface - - def __contains__(self, char): - assert len(char) == 1, repr(char) - return char in self.LOOKUP - - -FONT = Font() - - -class TextViewer(MenuViewer): - - MINIMUM_HEIGHT = FONT.line_h + 3 - CLOSE_TEXT = FONT.render('close') - GROW_TEXT = FONT.render('grow') - - class Cursor(object): - - def __init__(self, viewer): - self.v = viewer - self.x = self.y = 0 - self.w, self.h = 2, FONT.line_h - self.mem = pygame.Surface((self.w, self.h)) - self.can_fade = False - - def set_to(self, x, y): - self.fade() - self.x, self.y = x, y - self.draw() - - def draw(self): - r = self.x * FONT.char_w, self.screen_y(), self.w, self.h - self.mem.blit(self.v.body_surface, (0, 0), r) - self.v.body_surface.fill(FG, r) - self.can_fade = True - - def fade(self): - if self.can_fade: - dest = self.x * FONT.char_w, self.screen_y() - self.v.body_surface.blit(self.mem, dest) - self.can_fade = False - - def screen_y(self, row=None): - if row is None: row = self.y - return (row - self.v.at_line) * FONT.line_h - - def up(self, _mod): - if self.y: - self.fade() - self.y -= 1 - self.x = min(self.x, len(self.v.lines[self.y])) - self.draw() - - def down(self, _mod): - if self.y < len(self.v.lines) - 1: - self.fade() - self.y += 1 - self.x = min(self.x, len(self.v.lines[self.y])) - self.draw() - self._check_scroll() - - def left(self, _mod): - if self.x: - self.fade() - self.x -= 1 - self.draw() - elif self.y: - self.fade() - self.y -= 1 - self.x = len(self.v.lines[self.y]) - self.draw() - self._check_scroll() - - def right(self, _mod): - if self.x < len(self.v.lines[self.y]): - self.fade() - self.x += 1 - self.draw() - elif self.y < len(self.v.lines) - 1: - self.fade() - self.y += 1 - self.x = 0 - self.draw() - self._check_scroll() - - def _check_scroll(self): - if self.y < self.v.at_line: - self.v.scroll_down() - elif self.y > self.v.at_line + self.v.h_in_lines: - self.v.scroll_up() - - def __init__(self, surface): - self.cursor = self.Cursor(self) - MenuViewer.__init__(self, surface) - self.lines = [''] - self.content_id = None - self.at_line = 0 - self.bg = BG - self.command = self.command_rect = None - self._sel_start = self._sel_end = None - - def resurface(self, surface): - self.cursor.fade() - MenuViewer.resurface(self, surface) - - w, h = self.CLOSE_TEXT.get_size() - self.close_rect = pygame.rect.Rect(self.w - 2 - w, 1, w, h) - w, h = self.GROW_TEXT.get_size() - self.grow_rect = pygame.rect.Rect(1, 1, w, h) - - self.body_surface = surface.subsurface(self.body_rect) - self.line_w = self.body_rect.w / FONT.char_w + 1 - self.h_in_lines = self.body_rect.h / FONT.line_h - 1 - self.command_rect = self.command = None - self._sel_start = self._sel_end = None - - def handle(self, message): - if super(TextViewer, self).handle(message): - return - if (isinstance(message, ModifyMessage) - and message.subject is self.lines - ): - # TODO: check self.at_line - self.draw_body() - - # Drawing - - def draw_menu(self): - #MenuViewer.draw_menu(self) - self.surface.blit(self.GROW_TEXT, (1, 1)) - self.surface.blit(self.CLOSE_TEXT, - (self.w - 2 - self.close_rect.w, 1)) - if self.content_id: - self.surface.blit(FONT.render('| ' + self.content_id), - (self.grow_rect.w + FONT.char_w + 3, 1)) - self.surface.fill( # light grey background - (196, 196, 196), - (0, 0, self.w - 1, self.MINIMUM_HEIGHT), - pygame.BLEND_MULT - ) - - def draw_body(self): - MenuViewer.draw_body(self) - ys = xrange(0, self.body_rect.height, FONT.line_h) - ls = self.lines[self.at_line:self.at_line + self.h_in_lines + 2] - for y, line in zip(ys, ls): - self.draw_line(y, line) - - def draw_line(self, y, line): - surface = FONT.render(line[:self.line_w]) - self.body_surface.blit(surface, (0, y)) - - def _redraw_line(self, row): - try: line = self.lines[row] - except IndexError: line = ' ' * self.line_w - else: - n = self.line_w - len(line) - if n > 0: line = line + ' ' * n - self.draw_line(self.cursor.screen_y(row), line) - - # General Functionality - - def focus(self, display): - self.cursor.v = self - self.cursor.draw() - - def unfocus(self): - self.cursor.fade() - - def scroll_up(self): - if self.at_line < len(self.lines) - 1: - self._fade_command() - self._deselect() - self._sel_start = self._sel_end = None - self.at_line += 1 - self.body_surface.scroll(0, -FONT.line_h) - row = self.h_in_lines + self.at_line - self._redraw_line(row) - self._redraw_line(row + 1) - self.cursor.draw() - - def scroll_down(self): - if self.at_line: - self._fade_command() - self._deselect() - self._sel_start = self._sel_end = None - self.at_line -= 1 - self.body_surface.scroll(0, FONT.line_h) - self._redraw_line(self.at_line) - self.cursor.draw() - - def command_down(self, display, x, y): - if self.command_rect and self.command_rect.collidepoint(x, y): - return - self._fade_command() - line, column, _row = self.at(x, y) - word_start = line.rfind(' ', 0, column) + 1 - word_end = line.find(' ', column) - if word_end == -1: word_end = len(line) - word = line[word_start:word_end] - if not _is_command(display, word): - return - r = self.command_rect = pygame.Rect( - word_start * FONT.char_w, # x - y / FONT.line_h * FONT.line_h, # y - len(word) * FONT.char_w, # w - FONT.line_h # h - ) - pygame.draw.line(self.body_surface, FG, r.bottomleft, r.bottomright) - self.command = word - - def command_up(self, display): - if self.command: - command = self.command - self._fade_command() - display.broadcast(CommandMessage(self, command)) - - def _fade_command(self): - self.command = None - r, self.command_rect = self.command_rect, None - if r: - pygame.draw.line(self.body_surface, BG, r.bottomleft, r.bottomright) - - def at(self, x, y): - ''' - Given screen coordinates return the line, row, and column of the - character there. - ''' - row = self.at_line + y / FONT.line_h - try: - line = self.lines[row] - except IndexError: - row = len(self.lines) - 1 - line = self.lines[row] - column = len(line) - else: - column = min(x / FONT.char_w, len(line)) - return line, column, row - - # Event Processing - - def body_click(self, display, x, y, button): - if button == 1: - _line, column, row = self.at(x, y) - self.cursor.set_to(column, row) - elif button == 2: - if pygame.KMOD_SHIFT & pygame.key.get_mods(): - self.scroll_up() - else: - self.scroll_down() - elif button == 3: - self.command_down(display, x, y) - elif button == 4: self.scroll_down() - elif button == 5: self.scroll_up() - - def menu_click(self, display, x, y, button): - if MenuViewer.menu_click(self, display, x, y, button): - return True - - def mouse_up(self, display, x, y, button): - if MenuViewer.mouse_up(self, display, x, y, button): - return True - elif button == 3 and self.body_rect.collidepoint(x, y): - self.command_up(display) - - def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2): - if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y, - button0, button1, button2): - return True - if (button0 - and display.focused_viewer is self - and self.body_rect.collidepoint(x, y) - ): - bx, by = self.body_rect.topleft - _line, column, row = self.at(x - bx, y - by) - self.cursor.set_to(column, row) - elif button2 and self.body_rect.collidepoint(x, y): - bx, by = self.body_rect.topleft - self.command_down(display, x - bx, y - by) - - def close(self): - self._sel_start = self._sel_end = None - - def key_down(self, display, uch, key, mod): - - if key in SELECTION_KEYS: - self._selection_key(display, key, mod) - return - if key in STACK_CHATTER_KEYS: - self._stack_chatter_key(display, key, mod) - return - if key in ARROW_KEYS: - self._arrow_key(key, mod) - return - - line, i = self.lines[self.cursor.y], self.cursor.x - modified = () - if key == pygame.K_RETURN: - self._return_key(mod, line, i) - modified = True - elif key == pygame.K_BACKSPACE: - modified = self._backspace_key(mod, line, i) - elif key == pygame.K_DELETE: - modified = self._delete_key(mod, line, i) - elif key == pygame.K_INSERT: - modified = self._insert_key(display, mod, line, i) - elif uch and uch in FONT or uch == ' ': - self._printable_key(uch, mod, line, i) - modified = True - else: - print '%r %i %s' % (uch, key, bin(mod)) - - if modified: - # The selection is fragile. - self._deselect() - self._sel_start = self._sel_end = None - message = ModifyMessage( - self, self.lines, content_id=self.content_id) - display.broadcast(message) - - def _stack_chatter_key(self, display, key, mod): - if key == pygame.K_F5: - if mod & pygame.KMOD_SHIFT: - command = 'roll<' - else: - command = 'swap' - elif key == pygame.K_F6: - if mod & pygame.KMOD_SHIFT: - command = 'roll>' - else: - command = 'dup' - elif key == pygame.K_F7: - if mod & pygame.KMOD_SHIFT: - command = 'tuck' - else: - command = 'over' -## elif key == pygame.K_F8: -## if mod & pygame.KMOD_SHIFT: -## command = '' -## else: -## command = '' - else: - return - display.broadcast(CommandMessage(self, command)) - - # Selection Handling - - def _selection_key(self, display, key, mod): - self.cursor.fade() - self._deselect() - if key == pygame.K_F1: # set sel start - self._sel_start = self.cursor.y, self.cursor.x - self._update_selection() - elif key == pygame.K_F2: # set sel end - self._sel_end = self.cursor.y, self.cursor.x - self._update_selection() - elif key == pygame.K_F3: # copy - if mod & pygame.KMOD_SHIFT: - self._parse_selection(display) - else: - self._copy_selection(display) - self._update_selection() - elif key == pygame.K_F4: # cut or delete - if mod & pygame.KMOD_SHIFT: - self._delete_selection(display) - else: - self._cut_selection(display) - self.cursor.draw() - - def _deselect(self): - if self._has_selection(): - srow, erow = self._sel_start[0], self._sel_end[0] - # Just erase the whole selection. - for r in range(min(srow, erow), max(srow, erow) + 1): - self._redraw_line(r) - - def _copy_selection(self, display): - if push(self, self._get_selection(), display.broadcast) == SUCCESS: - return True -## om = OpenMessage(self, 'stack.pickle') -## display.broadcast(om) -## if om.status == SUCCESS: -## selection = self._get_selection() -## om.thing[0] = selection, om.thing[0] -## display.broadcast(ModifyMessage( -## self, om.thing, content_id=om.content_id)) - - def _parse_selection(self, display): - if self._has_selection(): - if self._copy_selection(display): - display.broadcast(CommandMessage(self, 'parse')) - - def _cut_selection(self, display): - if self._has_selection(): - if self._copy_selection(display): - self._delete_selection(display) - - def _delete_selection(self, display): - if not self._has_selection(): - return - self.cursor.fade() - srow, scolumn, erow, ecolumn = self._selection_coords() - if srow == erow: - line = self.lines[srow] - self.lines[srow] = line[:scolumn] + line[ecolumn:] - else: - left = self.lines[srow][:scolumn] - right = self.lines[erow][ecolumn:] - self.lines[srow:erow + 1] = [left + right] - self.draw_body() - self.cursor.set_to(srow, scolumn) - display.broadcast(ModifyMessage( - self, self.lines, content_id=self.content_id)) - - def _has_selection(self): - return (self._sel_start - and self._sel_end - and self._sel_start != self._sel_end) - - def _get_selection(self): - '''Return the current selection if any as a single string.''' - if not self._has_selection(): - return '' - srow, scolumn, erow, ecolumn = self._selection_coords() - if srow == erow: - return str(self.lines[srow][scolumn:ecolumn]) - lines = [] - assert srow < erow - while srow <= erow: - line = self.lines[srow] - e = ecolumn if srow == erow else len(line) - lines.append(line[scolumn:e]) - scolumn = 0 - srow += 1 - return str('\n'.join(lines)) - - def _selection_coords(self): - (srow, scolumn), (erow, ecolumn) = ( - min(self._sel_start, self._sel_end), - max(self._sel_start, self._sel_end) - ) - return srow, scolumn, erow, ecolumn - - def _update_selection(self): - if self._sel_start is None and self._sel_end: - self._sel_start = self._sel_end - elif self._sel_end is None and self._sel_start: - self._sel_end = self._sel_start - assert self._sel_start and self._sel_end - if self._sel_start != self._sel_end: - for rect in self._iter_selection_rectangles(): - self.body_surface.fill( - SELECTION_COLOR, - rect, - pygame.BLEND_RGBA_MULT - ) - - def _iter_selection_rectangles(self, ): - srow, scolumn, erow, ecolumn = self._selection_coords() - if srow == erow: - yield ( - scolumn * FONT.char_w, - self.cursor.screen_y(srow), - (ecolumn - scolumn) * FONT.char_w, - FONT.line_h - ) - return - lines = self.lines[srow:erow + 1] - assert len(lines) >= 2 - first_line = lines[0] - yield ( - scolumn * FONT.char_w, - self.cursor.screen_y(srow), - (len(first_line) - scolumn) * FONT.char_w, - FONT.line_h - ) - yield ( - 0, - self.cursor.screen_y(erow), - ecolumn * FONT.char_w, - FONT.line_h - ) - if len(lines) > 2: - for line in lines[1:-1]: - srow += 1 - yield ( - 0, - self.cursor.screen_y(srow), - len(line) * FONT.char_w, - FONT.line_h - ) - - # Key Handlers - - def _printable_key(self, uch, _mod, line, i): - line = line[:i] + uch + line[i:] - self.lines[self.cursor.y] = line - self.cursor.fade() - self.cursor.x += 1 - self.draw_line(self.cursor.screen_y(), line) - self.cursor.draw() - - def _backspace_key(self, _mod, line, i): - res = False - if i: - line = line[:i - 1] + line[i:] - self.lines[self.cursor.y] = line - self.cursor.fade() - self.cursor.x -= 1 - self.draw_line(self.cursor.screen_y(), line + ' ') - self.cursor.draw() - res = True - elif self.cursor.y: - y = self.cursor.y - left, right = self.lines[y - 1:y + 1] - self.lines[y - 1:y + 1] = [left + right] - self.cursor.x = len(left) - self.cursor.y -= 1 - self.draw_body() - self.cursor.draw() - res = True - return res - - def _delete_key(self, _mod, line, i): - res = False - if i < len(line): - line = line[:i] + line[i + 1:] - self.lines[self.cursor.y] = line - self.cursor.fade() - self.draw_line(self.cursor.screen_y(), line + ' ') - self.cursor.draw() - res = True - elif self.cursor.y < len(self.lines) - 1: - y = self.cursor.y - left, right = self.lines[y:y + 2] - self.lines[y:y + 2] = [left + right] - self.draw_body() - self.cursor.draw() - res = True - return res - - def _arrow_key(self, key, mod): - if key == pygame.K_UP: self.cursor.up(mod) - elif key == pygame.K_DOWN: self.cursor.down(mod) - elif key == pygame.K_LEFT: self.cursor.left(mod) - elif key == pygame.K_RIGHT: self.cursor.right(mod) - - def _return_key(self, _mod, line, i): - self.cursor.fade() - # Ignore the mods for now. - n = self.cursor.y - self.lines[n:n + 1] = [line[:i], line[i:]] - self.cursor.y += 1 - self.cursor.x = 0 - if self.cursor.y > self.at_line + self.h_in_lines: - self.scroll_up() - else: - self.draw_body() - self.cursor.draw() - - def _insert_key(self, display, mod, _line, _i): - om = OpenMessage(self, 'stack.pickle') - display.broadcast(om) - if om.status != SUCCESS: - return - stack = om.thing[0] - if stack: - content = format_stack_item(stack[0]) - if self.insert(content): - if mod & pygame.KMOD_SHIFT: - display.broadcast(CommandMessage(self, 'pop')) - return True - - def insert(self, content): - assert isinstance(content, basestring), repr(content) - if content: - self.cursor.fade() - row, column = self.cursor.y, self.cursor.x - line = self.lines[row] - lines = (line[:column] + content + line[column:]).splitlines() - self.lines[row:row + 1] = lines - self.draw_body() - self.cursor.y = row + len(lines) - 1 - self.cursor.x = len(lines[-1]) - len(line) + column - self.cursor.draw() - return True - - def append(self, content): - self.cursor.fade() - self.cursor.y = len(self.lines) - 1 - self.cursor.x = len(self.lines[self.cursor.y]) - self.insert(content) +# -*- 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 . +# +''' + +Text Viewer +================= + +''' +from __future__ import print_function +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, self.line_h / 2 - 3, + self.char_w - 2, self.line_h / 2) + pygame.draw.rect(surface, FG, r, 1) + else: + iy, ix = divmod(i, 26) + ix *= self.char_w + iy *= self.char_h + area = ix, iy, self.char_w, self.char_h + surface.blit(self.IMAGE, (x, 0), area) + x += self.char_w + return surface + + def __contains__(self, char): + assert len(char) == 1, repr(char) + return char in self.LOOKUP + + +FONT = Font() + + +class TextViewer(MenuViewer): + + MINIMUM_HEIGHT = FONT.line_h + 3 + CLOSE_TEXT = FONT.render('close') + GROW_TEXT = FONT.render('grow') + + class Cursor(object): + + def __init__(self, viewer): + self.v = viewer + self.x = self.y = 0 + self.w, self.h = 2, FONT.line_h + self.mem = pygame.Surface((self.w, self.h)) + self.can_fade = False + + def set_to(self, x, y): + self.fade() + self.x, self.y = x, y + self.draw() + + def draw(self): + r = self.x * FONT.char_w, self.screen_y(), self.w, self.h + self.mem.blit(self.v.body_surface, (0, 0), r) + self.v.body_surface.fill(FG, r) + self.can_fade = True + + def fade(self): + if self.can_fade: + dest = self.x * FONT.char_w, self.screen_y() + self.v.body_surface.blit(self.mem, dest) + self.can_fade = False + + def screen_y(self, row=None): + if row is None: row = self.y + return (row - self.v.at_line) * FONT.line_h + + def up(self, _mod): + if self.y: + self.fade() + self.y -= 1 + self.x = min(self.x, len(self.v.lines[self.y])) + self.draw() + + def down(self, _mod): + if self.y < len(self.v.lines) - 1: + self.fade() + self.y += 1 + self.x = min(self.x, len(self.v.lines[self.y])) + self.draw() + self._check_scroll() + + def left(self, _mod): + if self.x: + self.fade() + self.x -= 1 + self.draw() + elif self.y: + self.fade() + self.y -= 1 + self.x = len(self.v.lines[self.y]) + self.draw() + self._check_scroll() + + def right(self, _mod): + if self.x < len(self.v.lines[self.y]): + self.fade() + self.x += 1 + self.draw() + elif self.y < len(self.v.lines) - 1: + self.fade() + self.y += 1 + self.x = 0 + self.draw() + self._check_scroll() + + def _check_scroll(self): + if self.y < self.v.at_line: + self.v.scroll_down() + elif self.y > self.v.at_line + self.v.h_in_lines: + self.v.scroll_up() + + def __init__(self, surface): + self.cursor = self.Cursor(self) + MenuViewer.__init__(self, surface) + self.lines = [''] + self.content_id = None + self.at_line = 0 + self.bg = BG + self.command = self.command_rect = None + self._sel_start = self._sel_end = None + + def resurface(self, surface): + self.cursor.fade() + MenuViewer.resurface(self, surface) + + w, h = self.CLOSE_TEXT.get_size() + self.close_rect = pygame.rect.Rect(self.w - 2 - w, 1, w, h) + w, h = self.GROW_TEXT.get_size() + self.grow_rect = pygame.rect.Rect(1, 1, w, h) + + self.body_surface = surface.subsurface(self.body_rect) + self.line_w = self.body_rect.w / FONT.char_w + 1 + self.h_in_lines = self.body_rect.h / FONT.line_h - 1 + self.command_rect = self.command = None + self._sel_start = self._sel_end = None + + def handle(self, message): + if super(TextViewer, self).handle(message): + return + if (isinstance(message, ModifyMessage) + and message.subject is self.lines + ): + # TODO: check self.at_line + self.draw_body() + + # Drawing + + def draw_menu(self): + #MenuViewer.draw_menu(self) + self.surface.blit(self.GROW_TEXT, (1, 1)) + self.surface.blit(self.CLOSE_TEXT, + (self.w - 2 - self.close_rect.w, 1)) + if self.content_id: + self.surface.blit(FONT.render('| ' + self.content_id), + (self.grow_rect.w + FONT.char_w + 3, 1)) + self.surface.fill( # light grey background + (196, 196, 196), + (0, 0, self.w - 1, self.MINIMUM_HEIGHT), + pygame.BLEND_MULT + ) + + def draw_body(self): + MenuViewer.draw_body(self) + ys = xrange(0, self.body_rect.height, FONT.line_h) + ls = self.lines[self.at_line:self.at_line + self.h_in_lines + 2] + for y, line in zip(ys, ls): + self.draw_line(y, line) + + def draw_line(self, y, line): + surface = FONT.render(line[:self.line_w]) + self.body_surface.blit(surface, (0, y)) + + def _redraw_line(self, row): + try: line = self.lines[row] + except IndexError: line = ' ' * self.line_w + else: + n = self.line_w - len(line) + if n > 0: line = line + ' ' * n + self.draw_line(self.cursor.screen_y(row), line) + + # General Functionality + + def focus(self, display): + self.cursor.v = self + self.cursor.draw() + + def unfocus(self): + self.cursor.fade() + + def scroll_up(self): + if self.at_line < len(self.lines) - 1: + self._fade_command() + self._deselect() + self._sel_start = self._sel_end = None + self.at_line += 1 + self.body_surface.scroll(0, -FONT.line_h) + row = self.h_in_lines + self.at_line + self._redraw_line(row) + self._redraw_line(row + 1) + self.cursor.draw() + + def scroll_down(self): + if self.at_line: + self._fade_command() + self._deselect() + self._sel_start = self._sel_end = None + self.at_line -= 1 + self.body_surface.scroll(0, FONT.line_h) + self._redraw_line(self.at_line) + self.cursor.draw() + + def command_down(self, display, x, y): + if self.command_rect and self.command_rect.collidepoint(x, y): + return + self._fade_command() + line, column, _row = self.at(x, y) + word_start = line.rfind(' ', 0, column) + 1 + word_end = line.find(' ', column) + if word_end == -1: word_end = len(line) + word = line[word_start:word_end] + if not _is_command(display, word): + return + r = self.command_rect = pygame.Rect( + word_start * FONT.char_w, # x + y / FONT.line_h * FONT.line_h, # y + len(word) * FONT.char_w, # w + FONT.line_h # h + ) + pygame.draw.line(self.body_surface, FG, r.bottomleft, r.bottomright) + self.command = word + + def command_up(self, display): + if self.command: + command = self.command + self._fade_command() + display.broadcast(CommandMessage(self, command)) + + def _fade_command(self): + self.command = None + r, self.command_rect = self.command_rect, None + if r: + pygame.draw.line(self.body_surface, BG, r.bottomleft, r.bottomright) + + def at(self, x, y): + ''' + Given screen coordinates return the line, row, and column of the + character there. + ''' + row = self.at_line + y / FONT.line_h + try: + line = self.lines[row] + except IndexError: + row = len(self.lines) - 1 + line = self.lines[row] + column = len(line) + else: + column = min(x / FONT.char_w, len(line)) + return line, column, row + + # Event Processing + + def body_click(self, display, x, y, button): + if button == 1: + _line, column, row = self.at(x, y) + self.cursor.set_to(column, row) + elif button == 2: + if pygame.KMOD_SHIFT & pygame.key.get_mods(): + self.scroll_up() + else: + self.scroll_down() + elif button == 3: + self.command_down(display, x, y) + elif button == 4: self.scroll_down() + elif button == 5: self.scroll_up() + + def menu_click(self, display, x, y, button): + if MenuViewer.menu_click(self, display, x, y, button): + return True + + def mouse_up(self, display, x, y, button): + if MenuViewer.mouse_up(self, display, x, y, button): + return True + elif button == 3 and self.body_rect.collidepoint(x, y): + self.command_up(display) + + def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2): + if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y, + button0, button1, button2): + return True + if (button0 + and display.focused_viewer is self + and self.body_rect.collidepoint(x, y) + ): + bx, by = self.body_rect.topleft + _line, column, row = self.at(x - bx, y - by) + self.cursor.set_to(column, row) + elif button2 and self.body_rect.collidepoint(x, y): + bx, by = self.body_rect.topleft + self.command_down(display, x - bx, y - by) + + def close(self): + self._sel_start = self._sel_end = None + + def key_down(self, display, uch, key, mod): + + if key in SELECTION_KEYS: + self._selection_key(display, key, mod) + return + if key in STACK_CHATTER_KEYS: + self._stack_chatter_key(display, key, mod) + return + if key in ARROW_KEYS: + self._arrow_key(key, mod) + return + + line, i = self.lines[self.cursor.y], self.cursor.x + modified = () + if key == pygame.K_RETURN: + self._return_key(mod, line, i) + modified = True + elif key == pygame.K_BACKSPACE: + modified = self._backspace_key(mod, line, i) + elif key == pygame.K_DELETE: + modified = self._delete_key(mod, line, i) + elif key == pygame.K_INSERT: + modified = self._insert_key(display, mod, line, i) + elif uch and uch in FONT or uch == ' ': + self._printable_key(uch, mod, line, i) + modified = True + else: + print('%r %i %s' % (uch, key, bin(mod))) + + if modified: + # The selection is fragile. + self._deselect() + self._sel_start = self._sel_end = None + message = ModifyMessage( + self, self.lines, content_id=self.content_id) + display.broadcast(message) + + def _stack_chatter_key(self, display, key, mod): + if key == pygame.K_F5: + if mod & pygame.KMOD_SHIFT: + command = 'roll<' + else: + command = 'swap' + elif key == pygame.K_F6: + if mod & pygame.KMOD_SHIFT: + command = 'roll>' + else: + command = 'dup' + elif key == pygame.K_F7: + if mod & pygame.KMOD_SHIFT: + command = 'tuck' + else: + command = 'over' +## elif key == pygame.K_F8: +## if mod & pygame.KMOD_SHIFT: +## command = '' +## else: +## command = '' + else: + return + display.broadcast(CommandMessage(self, command)) + + # Selection Handling + + def _selection_key(self, display, key, mod): + self.cursor.fade() + self._deselect() + if key == pygame.K_F1: # set sel start + self._sel_start = self.cursor.y, self.cursor.x + self._update_selection() + elif key == pygame.K_F2: # set sel end + self._sel_end = self.cursor.y, self.cursor.x + self._update_selection() + elif key == pygame.K_F3: # copy + if mod & pygame.KMOD_SHIFT: + self._parse_selection(display) + else: + self._copy_selection(display) + self._update_selection() + elif key == pygame.K_F4: # cut or delete + if mod & pygame.KMOD_SHIFT: + self._delete_selection(display) + else: + self._cut_selection(display) + self.cursor.draw() + + def _deselect(self): + if self._has_selection(): + srow, erow = self._sel_start[0], self._sel_end[0] + # Just erase the whole selection. + for r in range(min(srow, erow), max(srow, erow) + 1): + self._redraw_line(r) + + def _copy_selection(self, display): + if push(self, self._get_selection(), display.broadcast) == SUCCESS: + return True +## om = OpenMessage(self, 'stack.pickle') +## display.broadcast(om) +## if om.status == SUCCESS: +## selection = self._get_selection() +## om.thing[0] = selection, om.thing[0] +## display.broadcast(ModifyMessage( +## self, om.thing, content_id=om.content_id)) + + def _parse_selection(self, display): + if self._has_selection(): + if self._copy_selection(display): + display.broadcast(CommandMessage(self, 'parse')) + + def _cut_selection(self, display): + if self._has_selection(): + if self._copy_selection(display): + self._delete_selection(display) + + def _delete_selection(self, display): + if not self._has_selection(): + return + self.cursor.fade() + srow, scolumn, erow, ecolumn = self._selection_coords() + if srow == erow: + line = self.lines[srow] + self.lines[srow] = line[:scolumn] + line[ecolumn:] + else: + left = self.lines[srow][:scolumn] + right = self.lines[erow][ecolumn:] + self.lines[srow:erow + 1] = [left + right] + self.draw_body() + self.cursor.set_to(srow, scolumn) + display.broadcast(ModifyMessage( + self, self.lines, content_id=self.content_id)) + + def _has_selection(self): + return (self._sel_start + and self._sel_end + and self._sel_start != self._sel_end) + + def _get_selection(self): + '''Return the current selection if any as a single string.''' + if not self._has_selection(): + return '' + srow, scolumn, erow, ecolumn = self._selection_coords() + if srow == erow: + return str(self.lines[srow][scolumn:ecolumn]) + lines = [] + assert srow < erow + while srow <= erow: + line = self.lines[srow] + e = ecolumn if srow == erow else len(line) + lines.append(line[scolumn:e]) + scolumn = 0 + srow += 1 + return str('\n'.join(lines)) + + def _selection_coords(self): + (srow, scolumn), (erow, ecolumn) = ( + min(self._sel_start, self._sel_end), + max(self._sel_start, self._sel_end) + ) + return srow, scolumn, erow, ecolumn + + def _update_selection(self): + if self._sel_start is None and self._sel_end: + self._sel_start = self._sel_end + elif self._sel_end is None and self._sel_start: + self._sel_end = self._sel_start + assert self._sel_start and self._sel_end + if self._sel_start != self._sel_end: + for rect in self._iter_selection_rectangles(): + self.body_surface.fill( + SELECTION_COLOR, + rect, + pygame.BLEND_RGBA_MULT + ) + + def _iter_selection_rectangles(self, ): + srow, scolumn, erow, ecolumn = self._selection_coords() + if srow == erow: + yield ( + scolumn * FONT.char_w, + self.cursor.screen_y(srow), + (ecolumn - scolumn) * FONT.char_w, + FONT.line_h + ) + return + lines = self.lines[srow:erow + 1] + assert len(lines) >= 2 + first_line = lines[0] + yield ( + scolumn * FONT.char_w, + self.cursor.screen_y(srow), + (len(first_line) - scolumn) * FONT.char_w, + FONT.line_h + ) + yield ( + 0, + self.cursor.screen_y(erow), + ecolumn * FONT.char_w, + FONT.line_h + ) + if len(lines) > 2: + for line in lines[1:-1]: + srow += 1 + yield ( + 0, + self.cursor.screen_y(srow), + len(line) * FONT.char_w, + FONT.line_h + ) + + # Key Handlers + + def _printable_key(self, uch, _mod, line, i): + line = line[:i] + uch + line[i:] + self.lines[self.cursor.y] = line + self.cursor.fade() + self.cursor.x += 1 + self.draw_line(self.cursor.screen_y(), line) + self.cursor.draw() + + def _backspace_key(self, _mod, line, i): + res = False + if i: + line = line[:i - 1] + line[i:] + self.lines[self.cursor.y] = line + self.cursor.fade() + self.cursor.x -= 1 + self.draw_line(self.cursor.screen_y(), line + ' ') + self.cursor.draw() + res = True + elif self.cursor.y: + y = self.cursor.y + left, right = self.lines[y - 1:y + 1] + self.lines[y - 1:y + 1] = [left + right] + self.cursor.x = len(left) + self.cursor.y -= 1 + self.draw_body() + self.cursor.draw() + res = True + return res + + def _delete_key(self, _mod, line, i): + res = False + if i < len(line): + line = line[:i] + line[i + 1:] + self.lines[self.cursor.y] = line + self.cursor.fade() + self.draw_line(self.cursor.screen_y(), line + ' ') + self.cursor.draw() + res = True + elif self.cursor.y < len(self.lines) - 1: + y = self.cursor.y + left, right = self.lines[y:y + 2] + self.lines[y:y + 2] = [left + right] + self.draw_body() + self.cursor.draw() + res = True + return res + + def _arrow_key(self, key, mod): + if key == pygame.K_UP: self.cursor.up(mod) + elif key == pygame.K_DOWN: self.cursor.down(mod) + elif key == pygame.K_LEFT: self.cursor.left(mod) + elif key == pygame.K_RIGHT: self.cursor.right(mod) + + def _return_key(self, _mod, line, i): + self.cursor.fade() + # Ignore the mods for now. + n = self.cursor.y + self.lines[n:n + 1] = [line[:i], line[i:]] + self.cursor.y += 1 + self.cursor.x = 0 + if self.cursor.y > self.at_line + self.h_in_lines: + self.scroll_up() + else: + self.draw_body() + self.cursor.draw() + + def _insert_key(self, display, mod, _line, _i): + om = OpenMessage(self, 'stack.pickle') + display.broadcast(om) + if om.status != SUCCESS: + return + stack = om.thing[0] + if stack: + content = format_stack_item(stack[0]) + if self.insert(content): + if mod & pygame.KMOD_SHIFT: + display.broadcast(CommandMessage(self, 'pop')) + return True + + def insert(self, content): + assert isinstance(content, basestring), repr(content) + if content: + self.cursor.fade() + row, column = self.cursor.y, self.cursor.x + line = self.lines[row] + lines = (line[:column] + content + line[column:]).splitlines() + self.lines[row:row + 1] = lines + self.draw_body() + self.cursor.y = row + len(lines) - 1 + self.cursor.x = len(lines[-1]) - len(line) + column + self.cursor.draw() + return True + + def append(self, content): + self.cursor.fade() + self.cursor.y = len(self.lines) - 1 + self.cursor.x = len(self.lines[self.cursor.y]) + self.insert(content) diff --git a/joy/vui/viewer.py b/joy/vui/viewer.py index 6fa61a0..1d7d81b 100644 --- a/joy/vui/viewer.py +++ b/joy/vui/viewer.py @@ -1,245 +1,246 @@ -# -*- 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 . -# -''' - -Viewer -================= - -''' -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), - except ValueError: - pass - - -# Note that Oberon book says that if you split at the exact top of a viewer -# it should close, and I think this implies the new viewer gets the old -# viewer's whole height. I haven't implemented that yet, so the edge-case -# in the code is broken by "intent" for now.. - - -def draw_a(surface, color=FOREGROUND, blend=False): - w, h = surface.get_width() - 2, surface.get_height() - 2 - pygame.draw.aalines(surface, color, False, ( - (1, h), (w / 2, 1), (w, h), (1, h / 2) - ), blend) +# -*- 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 . +# +''' + +Viewer +================= + +''' +from __future__ import print_function +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), (w / 2, 1), (w, h), (1, h / 2) + ), blend)