#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008,2009 Chris Ball, based on Collabora's
# "hellomesh" demo.
#
# Copyright (C) 2013,14 Walter Bender
# Copyright (C) 2013,14 Ignacio Rodriguez
# Copyright (C) 2013 Jorge Gomez
# Copyright (C) 2013,14 Sai Vineet
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""Pippy Activity: A simple Python programming activity ."""
import re
import os
import subprocess
from random import uniform
import locale
import json
import sys
from shutil import copy2
from signal import SIGTERM
from gettext import gettext as _
import uuid
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi import require_version
require_version('Gdk', '3.0')
require_version('Gtk', '3.0')
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Pango
try:
require_version('Vte', '2.91')
except:
require_version('Vte', '2.90')
from gi.repository import Vte
from gi.repository import GObject
DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
from sugar3.datastore import datastore
from sugar3.activity import activity as activity
from sugar3.activity.widgets import EditToolbar
from sugar3.activity.widgets import StopButton
from sugar3.activity.activity import get_bundle_path
from sugar3.graphics.alert import Alert
from sugar3.graphics.alert import ConfirmationAlert
from sugar3.graphics.alert import NotifyAlert
from sugar3.graphics.icon import Icon
from sugar3.graphics.objectchooser import ObjectChooser
from sugar3.graphics.toggletoolbutton import ToggleToolButton
from sugar3.graphics.toolbarbox import ToolbarButton
from sugar3.graphics.toolbutton import ToolButton
from sugar3.graphics.toolbarbox import ToolbarBox
from sugar3.activity.widgets import ActivityToolbarButton
from jarabe.view.customizebundle import generate_unique_id
from activity import ViewSourceActivity
from activity import TARGET_TYPE_TEXT
from collabwrapper import CollabWrapper
from filedialog import FileDialog
from icondialog import IconDialog
from notebook import SourceNotebook, tab_object
from toolbars import DevelopViewToolbar
import sound_check
import logging
text_buffer = None
# magic prefix to use utf-8 source encoding
PYTHON_PREFIX = '''#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''
# Force category names into Pootle
DEFAULT_CATEGORIES = [_('graphics'), _('math'), _('python'), _('sound'),
_('string'), _('tutorials')]
_logger = logging.getLogger('pippy-activity')
DISTUTILS_SETUP_SCRIPT = """#!/usr/bin/python3
# -*- coding: utf-8 -*-
from distutils.core import setup
setup(name='{modulename}',
version='1.0',
py_modules=[
{filenames}
],
)
""" # This is .format()'ed with the list of the file names.
DISTUTILS_SETUP_SCRIPT = """#!/usr/bin/python3
# -*- coding: utf-8 -*-
from distutils.core import setup
setup(name='{modulename}',
version='1.0',
py_modules=[
{filenames}
],
)
""" # This is .format()'ed with the list of the file names.
def _has_new_vte_api():
try:
return (Vte.MAJOR_VERSION >= 0 and
Vte.MINOR_VERSION >= 38)
except:
# Really old versions of Vte don't have VERSION
return False
def _find_object_id(activity_id, mimetype='text/x-python'):
''' Round-about way of accessing self._jobject.object_id '''
dsobjects, nobjects = datastore.find({'mime_type': [mimetype]})
for dsobject in dsobjects:
if 'activity_id' in dsobject.metadata and \
dsobject.metadata['activity_id'] == activity_id:
return dsobject.object_id
return None
class PippyActivity(ViewSourceActivity):
'''Pippy Activity as specified in activity.info'''
def __init__(self, handle):
self._pippy_instance = self
self.session_data = [] # Used to manage saving
self._loaded_session = [] # Used to manage tabs
self._py_file_loaded_from_journal = False
self._py_object_id = None
self._dialog = None
sys.path.append(os.path.join(self.get_activity_root(), 'Library'))
ViewSourceActivity.__init__(self, handle)
self._collab = CollabWrapper(self)
self._collab.message.connect(self.__message_cb)
self.set_canvas(self.initialize_display())
self.after_init()
self.connect("notify::active", self.__active_cb)
self._collab.setup()
def focus():
""" Enforce focus for the text view once. """
widget = self.get_toplevel().get_focus()
textview = self._source_tabs.get_text_view()
if widget is None and textview is not None:
textview.grab_focus()
return True
return False
GLib.timeout_add(100, focus)
def initialize_display(self):
'''Build activity toolbar with title input, share button and export
buttons
'''
toolbar_box = ToolbarBox()
activity_button = ActivityToolbarButton(self)
toolbar_box.toolbar.insert(activity_button, 0)
self.set_toolbar_box(toolbar_box)
activity_button.show()
toolbar_box.show()
activity_toolbar = activity_button.page
separator = Gtk.SeparatorToolItem()
activity_toolbar.insert(separator, -1)
separator.show()
button = ToolButton('pippy-import-doc')
button.set_tooltip(_('Import Python file to new tab'))
button.connect('clicked', self._import_py_cb)
activity_toolbar.insert(button, -1)
button.show()
button = ToolButton('pippy-export-doc')
button.set_tooltip(_('Export as Pippy document'))
button.connect('clicked', self._export_document_cb)
activity_toolbar.insert(button, -1)
button.show()
button = ToolButton('pippy-export-library')
button.set_tooltip(_('Save this file to the Pippy library'))
button.connect('clicked', self._save_as_library)
activity_toolbar.insert(button, -1)
if not self._library_writable():
button.set_sensitive(False)
button.show()
button = ToolButton('pippy-export-example')
button.set_tooltip(_('Export as new Pippy example'))
button.connect('clicked', self._export_example_cb)
activity_toolbar.insert(button, -1)
button.show()
button = ToolButton('pippy-create-bundle')
button.set_tooltip(_('Create a Sugar activity bundle'))
button.connect('clicked', self._create_bundle_cb)
activity_toolbar.insert(button, -1)
button.show()
button = ToolButton('pippy-create-distutils')
# TRANS: A distutils package is used to distribute Python modules
button.set_tooltip(_('Export as a distutils package'))
button.connect('clicked', self._export_distutils_cb)
activity_toolbar.insert(button, -1)
button.show()
self._edit_toolbar = EditToolbar()
button = ToolbarButton()
button.set_page(self._edit_toolbar)
button.props.icon_name = 'toolbar-edit'
button.props.label = _('Edit')
self.get_toolbar_box().toolbar.insert(button, -1)
button.show()
self._edit_toolbar.show()
self._edit_toolbar.undo.connect('clicked', self.__undobutton_cb)
self._edit_toolbar.redo.connect('clicked', self.__redobutton_cb)
self._edit_toolbar.copy.connect('clicked', self.__copybutton_cb)
self._edit_toolbar.paste.connect('clicked', self.__pastebutton_cb)
view_btn = ToolbarButton()
view_toolbar = DevelopViewToolbar(self)
view_btn.props.page = view_toolbar
view_btn.props.icon_name = 'toolbar-view'
view_btn.props.label = _('View')
view_toolbar.connect('font-size-changed',
self._font_size_changed_cb)
self.get_toolbar_box().toolbar.insert(view_btn, -1)
self.view_toolbar = view_toolbar
view_toolbar.show()
actions_toolbar = self.get_toolbar_box().toolbar
self._toggle_output = ToggleToolButton('tray-show')
self._toggle_output.set_tooltip(_('Show output panel'))
self._toggle_output.connect('toggled', self._toggle_output_cb)
actions_toolbar.insert(self._toggle_output, -1)
self._toggle_output.show()
self._inverted_colors = ToggleToolButton(icon_name='dark-theme')
self._inverted_colors.set_tooltip(_('Inverted Colors'))
self._inverted_colors.set_accelerator('<Ctrl><Shift>I')
self._inverted_colors.connect(
'toggled', self.__inverted_colors_toggled_cb)
actions_toolbar.insert(self._inverted_colors, -1)
self._inverted_colors.show()
icons_path = os.path.join(get_bundle_path(), 'icons')
icon_bw = Gtk.Image()
icon_bw.set_from_file(os.path.join(icons_path, 'run_bw.svg'))
icon_bw.show()
icon_color = Gtk.Image()
icon_color.set_from_file(os.path.join(icons_path, 'run_color.svg'))
icon_color.show()
button = ToolButton(label=_('Run!'))
button.props.accelerator = _('<alt>r')
button.set_icon_widget(icon_bw)
button.set_tooltip(_('Run!'))
button.connect('clicked', self._flash_cb,
dict({'bw': icon_bw, 'color': icon_color}))
button.connect('clicked', self._go_button_cb)
actions_toolbar.insert(button, -1)
button.show()
icon_bw = Gtk.Image()
icon_bw.set_from_file(os.path.join(icons_path, 'stopit_bw.svg'))
icon_bw.show()
icon_color = Gtk.Image()
icon_color.set_from_file(os.path.join(icons_path, 'stopit_color.svg'))
icon_color.show()
button = ToolButton(label=_('Stop'))
button.props.accelerator = _('<alt>s')
button.set_icon_widget(icon_bw)
button.connect('clicked', self._flash_cb,
dict({'bw': icon_bw, 'color': icon_color}))
button.connect('clicked', self._stop_button_cb)
button.set_tooltip(_('Stop'))
actions_toolbar.insert(button, -1)
button.show()
icon_bw = Gtk.Image()
icon_bw.set_from_file(os.path.join(icons_path, 'eraser_bw.svg'))
icon_bw.show()
icon_color = Gtk.Image()
icon_color.set_from_file(os.path.join(icons_path, 'eraser_color.svg'))
icon_color.show()
button = ToolButton(label=_('Clear output panel'))
button.props.accelerator = _('<alt>c')
button.set_icon_widget(icon_bw)
button.connect('clicked', self._clear_button_cb)
button.connect('clicked', self._flash_cb,
dict({'bw': icon_bw, 'color': icon_color}))
button.set_tooltip(_('Clear output panel'))
actions_toolbar.insert(button, -1)
button.show()
activity_toolbar.show()
separator = Gtk.SeparatorToolItem()
self.get_toolbar_box().toolbar.insert(separator, -1)
separator.show()
button = ToolButton('pippy-openoff')
button.set_tooltip(_('Open an example'))
button.connect('clicked', self._load_example_cb)
self.get_toolbar_box().toolbar.insert(button, -1)
button.show()
separator = Gtk.SeparatorToolItem()
separator.props.draw = False
separator.set_expand(True)
self.get_toolbar_box().toolbar.insert(separator, -1)
separator.show()
stop = StopButton(self)
self.get_toolbar_box().toolbar.insert(stop, -1)
stop.show()
vpane = Gtk.Paned.new(orientation=Gtk.Orientation.VERTICAL)
vpane.set_position(400) # setting initial position
self.paths = []
try:
if sound_check.finddir():
TAMTAM_AVAILABLE = True
else:
TAMTAM_AVAILABLE = False
except sound_check.SoundLibraryNotFoundError:
TAMTAM_AVAILABLE = False
data_path = os.path.join(get_bundle_path(), 'data')
# get default language from locale
locale_lang = locale.getdefaultlocale()[0]
if locale_lang is None:
lang = 'en'
else:
lang = locale_lang.split('_')[0]
_logger.debug(locale.getdefaultlocale())
_logger.debug(lang)
# construct the path for both
lang_path = os.path.join(data_path, lang)
en_lang_path = os.path.join(data_path, 'en')
# get all folders in lang examples
all_folders = []
if os.path.exists(lang_path):
for d in sorted(os.listdir(lang_path)):
all_folders.append(d)
# get all folders in English examples
for d in sorted(os.listdir(en_lang_path)):
# check if folder isn't already in list
if d not in all_folders:
all_folders.append(d)
for folder in all_folders:
# Skip sound folders if TAMTAM is not installed
if folder == 'sound' and not TAMTAM_AVAILABLE:
continue
direntry = {}
# check if dir exists in pref language, if exists, add it
if os.path.exists(os.path.join(lang_path, folder)):
direntry = {
'name': _(folder.capitalize()),
'path': os.path.join(lang_path, folder) + '/'}
# if not try to see if it's in default English path
elif os.path.exists(os.path.join(en_lang_path, folder)):
direntry = {
'name': _(folder.capitalize()),
'path': os.path.join(en_lang_path, folder) + '/'}
self.paths.append([direntry['name'], direntry['path']])
# Adding local examples
data_path = os.path.join(get_bundle_path(), 'data')
self.paths.append([_('My examples'), data_path])
self._source_tabs = SourceNotebook(self, self._collab)
self._source_tabs.connect('tab-added', self._add_source_cb)
self._source_tabs.connect('tab-renamed', self._rename_source_cb)
self._source_tabs.connect('tab-closed', self._close_source_cb)
if self._loaded_session:
for name, content, path in self._loaded_session:
self._source_tabs.add_tab(name, content, path)
else:
self.session_data.append(None)
self._source_tabs.add_tab() # New instance, ergo empty tab
vpane.add1(self._source_tabs)
self._source_tabs.show()
self._outbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self._vte = Vte.Terminal()
self._vte.set_encoding('utf-8')
self._vte.set_size(30, 5)
self._vte.set_scrollback_lines(-1)
self._vte_set_colors('#000000', '#E7E7E7')
self._child_exited_handler = None
self._vte.connect('child_exited', self._child_exited_cb)
self._vte.connect('drag_data_received', self._vte_drop_cb)
self._outbox.pack_start(self._vte, True, True, 0)
outsb = Gtk.Scrollbar(orientation=Gtk.Orientation.VERTICAL)
outsb.set_adjustment(self._vte.get_vadjustment())
outsb.show()
self._outbox.pack_start(outsb, False, False, 0)
self._load_config()
vpane.add2(self._outbox)
self._outbox.show()
vpane.show()
return vpane
def _vte_set_colors(self, bg, fg):
# XXX support both Vte APIs
if _has_new_vte_api():
foreground = Gdk.RGBA()
foreground.parse(bg)
background = Gdk.RGBA()
background.parse(fg)
else:
foreground = Gdk.color_parse(bg)
background = Gdk.color_parse(fg)
self._vte.set_colors(foreground, background, [])
def after_init(self):
self._outbox.hide()
def _font_size_changed_cb(self, widget, size):
self._source_tabs.set_font_size(size)
self._vte.set_font(
Pango.FontDescription('Monospace {}'.format(size)))
def _store_config(self):
font_size = self._source_tabs.get_font_size()
_config_file_path = os.path.join(
activity.get_activity_root(), 'data',
'config.json')
with open(_config_file_path, "w") as f:
f.write(json.dumps(font_size))
def _load_config(self):
_config_file_path = os.path.join(
activity.get_activity_root(), 'data',
'config.json')
if not os.path.isfile(_config_file_path):
return
with open(_config_file_path, "r") as f:
font_size = json.loads(f.read())
self.view_toolbar.set_font_size(font_size)
self._vte.set_font(
Pango.FontDescription('Monospace {}'.format(font_size)))
def __active_cb(self, widget, event):
_logger.debug('__active_cb %r', self.props.active)
if self.props.active:
self.resume()
else:
self.pause()
def do_visibility_notify_event(self, event):
_logger.debug('do_visibility_notify_event %r', event.get_state())
if event.get_state() == Gdk.VisibilityState.FULLY_OBSCURED:
self.pause()
else:
self.resume()
def pause(self):
# FIXME: We had resume, but no pause?
pass
def resume(self):
if self._dialog is not None:
self._dialog.set_keep_above(True)
def _toggle_output_cb(self, button):
shown = button.get_active()
if shown:
self._outbox.show_all()
self._toggle_output.set_tooltip(_('Hide output panel'))
self._toggle_output.set_icon_name('tray-hide')
else:
self._outbox.hide()
self._toggle_output.set_tooltip(_('Show output panel'))
self._toggle_output.set_icon_name('tray-show')
def __inverted_colors_toggled_cb(self, button):
if button.props.active:
self._vte_set_colors('#E7E7E7', '#000000')
self._source_tabs.set_dark()
button.set_icon_name('light-theme')
button.set_tooltip(_('Normal Colors'))
else:
self._vte_set_colors('#000000', '#E7E7E7')
self._source_tabs.set_light()
button.set_icon_name('dark-theme')
button.set_tooltip(_('Inverted Colors'))
def _load_example_cb(self, widget):
widget.set_icon_name('pippy-openon')
self._dialog = FileDialog(self.paths, self, widget)
self._dialog.show()
self._dialog.run()
path = self._dialog.get_path()
if path:
self._select_func_cb(path)
def _add_source_cb(self, button, force=False, editor_id=None):
if self._collab._leader or force:
if editor_id is None:
editor_id = str(uuid.uuid1())
self._source_tabs.add_tab(editor_id=editor_id)
self.session_data.append(None)
self._source_tabs.get_nth_page(-1).show_all()
self._source_tabs.get_text_view().grab_focus()
if self._collab._leader:
self._collab.post(dict(
action='add-source',
editor_id=editor_id))
else:
# The leader must do it first so that they can set
# up the text buffer
self._collab.post(dict(action='add-source-request'))
def _rename_source_cb(self, notebook, page, name):
_logger.debug('_rename_source_cb %r %r' % (page, name))
self._collab.post(dict(action='rename-source', page=page, name=name))
def _close_source_cb(self, notebook, page):
_logger.debug('_close_source_cb %r' % (page))
self._collab.post(dict(action='close-source', page=page))
def __message_cb(self, collab, buddy, msg):
action = msg.get('action')
if action == 'add-source-request' and self._collab._leader:
self._add_source_cb(None, force=True)
elif action == 'add-source':
self._add_source_cb(
None, force=True, editor_id=msg.get('editor_id'))
elif action == 'rename-source':
page = msg.get('page')
name = msg.get('name')
_logger.debug('__message_cb rename-source %r %r' % (page, name))
self._source_tabs.rename_tab(page, name)
elif action == 'close-source':
page = msg.get('page')
_logger.debug('__message_cb close-source %r' % (page))
self._source_tabs.close_tab(page)
def _vte_drop_cb(self, widget, context, x, y, selection, targetType, time):
if targetType == TARGET_TYPE_TEXT:
self._vte.feed_child(selection.data)
def get_data(self):
return self._source_tabs.get_all_data()
def set_data(self, data):
# Remove initial new/blank thing
self.session_data = []
self._loaded_session = []
try:
self._source_tabs.remove_page(0)
tab_object.pop(0)
self._source_tabs.last_tab = 0
except IndexError:
pass
list_ = list(zip(*data))
for name, code, path, modified, editor_id in list_:
self._source_tabs.add_tab(
label=name, editor_id=editor_id)
self.session_data.append(None) # maybe?
def _selection_cb(self, value):
self.save()
_logger.debug('clicked! %s' % value['path'])
_file = open(value['path'], 'r')
lines = _file.readlines()
self._add_source_cb(None)
text_buffer = self._source_tabs.get_text_buffer()
text_buffer.set_text(''.join(lines))
text_buffer.set_modified(False)
self._pippy_instance.metadata['title'] = value['name']
self._stop_button_cb(None)
self._reset_vte()
self._source_tabs.set_current_label(value['name'])
self._source_tabs.set_current_path(value['path'])
self._source_tabs.get_text_view().grab_focus()
def _select_func_cb(self, path):
values = {}
values['name'] = os.path.basename(path)
values['path'] = path
self._selection_cb(values)
def _timer_cb(self, button, icons):
button.set_icon_widget(icons['bw'])
button.show_all()
return False
def _flash_cb(self, button, icons):
button.set_icon_widget(icons['color'])
button.show_all()
GObject.timeout_add(400, self._timer_cb, button, icons)
def _clear_button_cb(self, button):
self.save()
self._stop_button_cb(None)
self._reset_vte()
self._source_tabs.get_text_view().grab_focus()
def _write_all_buffers(self, tmp_dir):
data = self._source_tabs.get_all_data()
zipdata = list(zip(data[0], data[1]))
for name, content in zipdata:
name = self._source_tabs.purify_name(name)
with open(os.path.join(tmp_dir, name), 'w') as f:
# Write utf-8 coding prefix if there's not one already
if re.match(r'coding[:=]\s*([-\w.]+)',
'\n'.join(content.splitlines()[:2])) is None:
f.write(PYTHON_PREFIX)
f.write(content)
def _reset_vte(self):
self._vte.grab_focus()
self._vte.feed(b'\x1B[H\x1B[J\x1B[0;39m')
def __undobutton_cb(self, butston):
text_buffer = self._source_tabs.get_text_buffer()
if text_buffer.can_undo():
text_buffer.undo()
def __redobutton_cb(self, button):
text_buffer = self._source_tabs.get_text_buffer()
if text_buffer.can_redo():
text_buffer.redo()
def __copybutton_cb(self, button):
text_buffer = self._source_tabs.get_text_buffer()
if self._vte.get_has_selection():
self._vte.copy_clipboard()
elif text_buffer.get_has_selection():
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
text_buffer.copy_clipboard(clipboard)
def __pastebutton_cb(self, button):
text_buffer = self._source_tabs.get_text_buffer()
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
text_buffer.paste_clipboard(clipboard, None, True)
def _go_button_cb(self, button):
self._stop_button_cb(button) # Try stopping old code first.
self._reset_vte()
# FIXME: We're losing an odd race here
# Gtk.main_iteration(block=False)
if self._toggle_output.get_active() is False:
self._outbox.show_all()
self._toggle_output.set_active(True)
pippy_tmp_dir = '%s/tmp/' % self.get_activity_root()
self._write_all_buffers(pippy_tmp_dir)
current_file = os.path.join(
pippy_tmp_dir,
self._source_tabs.get_current_file_name())
# Write activity.py here too, to support pippy-based activities.
copy2('%s/activity.py' % get_bundle_path(),
'%s/tmp/activity.py' % self.get_activity_root())
# XXX Support both Vte APIs
if _has_new_vte_api():
vte_run = self._vte.spawn_sync
else:
vte_run = self._vte.fork_command_full
self._pid = vte_run(
Vte.PtyFlags.DEFAULT,
get_bundle_path(),
['/bin/sh', '-c', 'python3 %s; sleep 1' % current_file,
'PYTHONPATH=%s/library:%s' % (get_bundle_path(),
os.getenv('PYTHONPATH', ''))],
['PYTHONPATH=%s/library:%s' % (get_bundle_path(),
os.getenv('PYTHONPATH', ''))],
GLib.SpawnFlags.DO_NOT_REAP_CHILD,
None,
None,)
def _stop_button_cb(self, button):
try:
if self._pid is not None:
os.kill(self._pid[1], SIGTERM)
except:
pass # Process must already be dead.
def _library_writable(self):
return os.access(os.path.join(get_bundle_path(), 'library'), os.W_OK)
def _save_as_library(self, button):
library_dir = os.path.join(get_bundle_path(), 'library')
file_name = self._source_tabs.get_current_file_name()
text_buffer = self._source_tabs.get_text_buffer()
content = text_buffer.get_text(
*text_buffer.get_bounds(),
include_hidden_chars=True)
if not os.path.isdir(library_dir):
os.mkdir(library_dir)
with open(os.path.join(library_dir, file_name), 'w') as f:
f.write(content)
success = True
if success:
alert = NotifyAlert(5)
alert.props.title = _('Python File added to Library')
IMPORT_MESSAGE = _('The file you selected has been added'
' to the library. Use "import {importname}"'
' to import the library for using.')
alert.props.msg = IMPORT_MESSAGE.format(importname=file_name[:-3])
alert.connect('response', self._remove_alert_cb)
self.add_alert(alert)
def _export_document_cb(self, __):
self.copy()
alert = NotifyAlert()
alert.props.title = _('Saved')
alert.props.msg = _('The document has been saved to journal.')
alert.connect('response', lambda x, i: self.remove_alert(x))
self.add_alert(alert)
def _remove_alert_cb(self, alert, response_id):
self.remove_alert(alert)
def _import_py_cb(self, button):
chooser = ObjectChooser()
result = chooser.run()
if result is Gtk.ResponseType.ACCEPT:
dsitem = chooser.get_selected_object()
if dsitem.metadata['mime_type'] != 'text/x-python':
alert = NotifyAlert(5)
alert.props.title = _('Error importing Python file')
alert.props.msg = _('The file you selected is not a '
'Python file.')
alert.connect('response', self._remove_alert_cb)
self.add_alert(alert)
elif dsitem.object_id in self.session_data:
alert = NotifyAlert(5)
alert.props.title = _('Error importing Python file')
alert.props.msg = _('The file you selected is already '
'open')
alert.connect('response', self._remove_alert_cb)
self.add_alert(alert)
else:
name = dsitem.metadata['title']
file_path = dsitem.get_file_path()
content = open(file_path, 'r').read()
self._source_tabs.add_tab(name, content, None)
self._source_tabs.set_current_label(name)
self.session_data.append(dsitem.object_id)
_logger.debug('after import py: %r' % self.session_data)
chooser.destroy()
def _create_bundle_cb(self, button):
from shutil import rmtree
from tempfile import mkdtemp
# Get the name of this pippy program.
title = self._pippy_instance.metadata['title'].replace('.py', '')
title = title.replace('-', '')
if title == 'Pippy Activity':
alert = Alert()
alert.props.title = _('Save as Activity Error')
alert.props.msg = _('Please give your activity a meaningful name '
'before attempting to save it as an activity.')
ok_icon = Icon(icon_name='dialog-ok')
alert.add_button(Gtk.ResponseType.OK, _('Ok'), ok_icon)
alert.connect('response', self._dismiss_alert_cb)
self.add_alert(alert)
return
alert_icon = Alert()
ok_icon = Icon(icon_name='dialog-ok')
alert_icon.add_button(Gtk.ResponseType.OK, _('Ok'), ok_icon)
alert_icon.props.title = _('Activity icon')
alert_icon.props.msg = _('Please select an activity icon.')
self._stop_button_cb(None) # try stopping old code first.
self._reset_vte()
self._outbox.show_all()
self._vte.feed(_("Creating activity bundle...").encode())
self._vte.feed(b'\r\n')
TMPDIR = 'instance'
app_temp = mkdtemp('.activity', 'Pippy',
os.path.join(self.get_activity_root(), TMPDIR))
sourcefile = os.path.join(app_temp, 'xyzzy.py')
# invoke ourself to build the activity bundle.
_logger.debug('writing out source file: %s' % sourcefile)
def internal_callback(window=None, event=None):
icon = '%s/activity/activity-default.svg' % (get_bundle_path())
if window:
icon = window.get_icon()
self._stop_button_cb(None) # Try stopping old code first.
self._reset_vte()
self._vte.feed(_('Creating activity bundle...').encode())
self._vte.feed(b'\r\n')
TMPDIR = 'instance'
app_temp = mkdtemp('.activity', 'Pippy',
os.path.join(self.get_activity_root(), TMPDIR))
sourcefile = os.path.join(app_temp, 'xyzzy.py')
# Invoke ourself to build the activity bundle.
_logger.debug('writing out source file: %s' % sourcefile)
# Write out application code
self._write_text_buffer(sourcefile)
try:
# FIXME: vte invocation was raising errors.
# Switched to subprocss
output = subprocess.check_output(
['/usr/bin/python3',
'%s/pippy_app.py' % get_bundle_path(),
'-p', '%s/library' % get_bundle_path(),
'-d', app_temp, title, sourcefile, icon])
self._vte.feed(output)
self._vte.feed(b'\r\n')
self._bundle_cb(title, app_temp)
except subprocess.CalledProcessError:
rmtree(app_temp, ignore_errors=True) # clean up!
self._vte.feed(_('Save as Activity Error').encode())
self._vte.feed(b'\r\n')
raise
def _alert_response(alert, response_id):
self.remove_alert(alert)
def _dialog():
dialog = IconDialog()
dialog.connect('destroy', internal_callback)
GObject.idle_add(_dialog)
alert_icon.connect('response', _alert_response)
self.add_alert(alert_icon)
def _write_text_buffer(self, filename):
text_buffer = self._source_tabs.get_text_buffer()
start, end = text_buffer.get_bounds()
text = text_buffer.get_text(start, end, True)
with open(filename, 'w') as f:
# Write utf-8 coding prefix if there's not one already
if re.match(r'coding[:=]\s*([-\w.]+)',
'\n'.join(text.splitlines()[:2])) is None:
f.write(PYTHON_PREFIX)
for line in text:
f.write(line)
def _export_distutils_cb(self, button):
app_temp = os.path.join(self.get_activity_root(), 'instance')
data = self._source_tabs.get_all_data()
for filename, content in zip(data[0], data[1]):
fileobj = open(os.path.join(app_temp, filename), 'w')
fileobj.write(content)
fileobj.close()
filenames = ','.join([("'" + name[:-3] + "'") for name in data[0]])
title = self._pippy_instance.metadata['title']
if title is _('Pippy Activity'):
alert = Alert()
alert.props.title = _('Save as distutils package error')
alert.props.msg = _('Please give your activity a meaningful '
'name before attempting to save it '
'as an distutils package.')
ok_icon = Icon(icon_name='dialog-ok')
alert.add_button(Gtk.ResponseType.OK, _('Ok'), ok_icon)
alert.connect('response', self._dismiss_alert_cb)
self.add_alert(alert)
return
setup_script = DISTUTILS_SETUP_SCRIPT.format(modulename=title,
filenames=filenames)
setupfile = open(os.path.join(app_temp, 'setup.py'), 'w')
setupfile.write(setup_script)
setupfile.close()
os.chdir(app_temp)
subprocess.check_output(
['/usr/bin/python3', os.path.join(app_temp, 'setup.py'), 'sdist',
'-v'])
# Hand off to journal
os.chmod(app_temp, 0o777)
jobject = datastore.create()
metadata = {
'title': '%s distutils bundle' % title,
'title_set_by_user': '1',
'mime_type': 'application/x-gzip',
}
for k, v in list(metadata.items()):
# The dict.update method is missing =(
jobject.metadata[k] = v
tarname = 'dist/{modulename}-1.0.tar.gz'.format(modulename=title)
jobject.file_path = os.path.join(app_temp, tarname)
datastore.write(jobject)
def _export_example_cb(self, button):
# Get the name of this pippy program.
title = self._pippy_instance.metadata['title']
if title == _('Pippy Activity'):
alert = Alert()
alert.props.title = _('Save as Example Error')
alert.props.msg = \
_('Please give your activity a meaningful '
'name before attempting to save it as an example.')
ok_icon = Icon(icon_name='dialog-ok')
alert.add_button(Gtk.ResponseType.OK, _('Ok'), ok_icon)
alert.connect('response', self._dismiss_alert_cb)
self.add_alert(alert)
return
self._stop_button_cb(None) # Try stopping old code first.
self._reset_vte()
self._vte.feed(_('Creating example...').encode())
self._vte.feed(b'\r\n')
local_data = os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'], 'data')
local_file = os.path.join(local_data, title)
if os.path.exists(local_file):
alert = ConfirmationAlert()
alert.props.title = _('Save as Example Warning')
alert.props.msg = _('This example already exists. '
'Do you want to overwrite it?')
alert.connect('response', self._confirmation_alert_cb, local_file)
self.add_alert(alert)
else:
self.write_file(local_file)
self._reset_vte()
self._vte.feed(_('Saved as example.').encode())
self._vte.feed(b'\r\n')
self._add_to_example_list(local_file)
def _child_exited_cb(self, *args):
'''Called whenever a child exits. If there's a handler, run it.'''
h, self._child_exited_handler = self._child_exited_handler, None
if h is not None:
h()
def _bundle_cb(self, title, app_temp):
'''Called when we're done building a bundle for a source file.'''
from sugar3 import profile
from shutil import rmtree
try:
# Find the .xo file: were we successful?
bundle_file = [f for f in os.listdir(app_temp)
if f.endswith('.xo')]
if len(bundle_file) != 1:
_logger.debug("Couldn't find bundle: %s" %
str(bundle_file))
self._vte.feed(b'\r\n')
self._vte.feed(_('Error saving activity to journal.').encode())
self._vte.feed(b'\r\n')
return # Something went wrong.
# Hand off to journal
os.chmod(app_temp, 0o755)
jobject = datastore.create()
metadata = {
'title': '%s Bundle' % title,
'title_set_by_user': '1',
'buddies': '',
'preview': '',
'icon-color': profile.get_color().to_string(),
'mime_type': 'application/vnd.olpc-sugar',
}
for k, v in list(metadata.items()):
# The dict.update method is missing =(
jobject.metadata[k] = v
jobject.file_path = os.path.join(app_temp, bundle_file[0])
datastore.write(jobject)
self._vte.feed(b'\r\n')
self._vte.feed(_('Activity saved to journal.').encode())
self._vte.feed(b'\r\n')
self.journal_show_object(jobject.object_id)
jobject.destroy()
finally:
rmtree(app_temp, ignore_errors=True) # clean up!
def _dismiss_alert_cb(self, alert, response_id):
self.remove_alert(alert)
def _confirmation_alert_cb(self, alert, response_id, local_file):
# Callback for conf alert
self.remove_alert(alert)
if response_id is Gtk.ResponseType.OK:
self.write_file(local_file)
self._reset_vte()
self._vte.feed(_('Saved as example.').encode())
self._vte.feed(b'\r\n')
else:
self._reset_vte()
def _add_to_example_list(self, local_file):
entry = {'name': _(os.path.basename(local_file)),
'path': local_file}
_iter = self.model.insert_before(self.example_iter, None)
self.model.set_value(_iter, 0, entry)
self.model.set_value(_iter, 1, entry['name'])
def is_example(self, path):
if path is None:
return False
for name in self.paths:
if path.startswith(name[1]):
return True
return False
def _get_pippy_object_id(self):
''' We need the object_id of this pippy instance to save in the .py
file metadata'''
if self._pippy_instance == self:
return _find_object_id(self.metadata['activity_id'],
mimetype='application/json')
else:
return self._pippy_instance.get_object_id()
def write_file(self, file_path):
pippy_id = self._get_pippy_object_id()
data = self._source_tabs.get_all_data()
zipped_data = list(zip(*data))
session_list = []
app_temp = os.path.join(self.get_activity_root(), 'instance')
tmpfile = os.path.join(app_temp, 'pippy-tempfile-storing.py')
for zipdata, content in zip(zipped_data, self.session_data):
_logger.debug('Session data %r', content)
name, python_code, path, modified, editor_id = zipdata
if content is not None and content == self._py_object_id:
_logger.debug('saving to self')
self.metadata['title'] = name
self.metadata['mime_type'] = 'text/x-python'
if pippy_id is not None:
self.metadata['pippy_instance'] = pippy_id
__file = open(file_path, 'w')
__file.write(python_code)
__file.close()
session_list.append([name, content])
elif content is not None and content[0] != '/':
_logger.debug('Saving an existing dsobject')
dsobject = datastore.get(content)
dsobject.metadata['title'] = name
dsobject.metadata['mime_type'] = 'text/x-python'
if pippy_id is not None:
dsobject.metadata['pippy_instance'] = pippy_id
__file = open(tmpfile, 'w')
__file.write(python_code)
__file.close()
dsobject.set_file_path(tmpfile)
datastore.write(dsobject)
session_list.append([name, dsobject.object_id])
elif modified:
_logger.debug('Creating new dsobj for modified code')
if len(python_code) > 0:
dsobject = datastore.create()
dsobject.metadata['title'] = name
dsobject.metadata['mime_type'] = 'text/x-python'
if pippy_id is not None:
dsobject.metadata['pippy_instance'] = pippy_id
__file = open(tmpfile, 'w')
__file.write(python_code)
__file.close()
dsobject.set_file_path(tmpfile)
datastore.write(dsobject)
session_list.append([name, dsobject.object_id])
# If there are multiple Nones, we need to find
# the correct one.
if content is None and \
self.session_data.count(None) > 1:
i = zipped_data.index(zipdata)
else:
i = self.session_data.index(content)
self.session_data[i] = dsobject.object_id
elif content is not None or path is not None:
_logger.debug('Saving reference to sample file')
if path is None: # Should not happen, but just in case...
_logger.error('path is None.')
session_list.append([name, content])
else:
session_list.append([name, path])
else: # Should not happen, but just in case...
_logger.debug('Nothing to save in tab? %s %s %s %s' %
(str(name), str(python_code), str(path),
str(content)))
self._pippy_instance.metadata['mime_type'] = 'application/json'
pippy_data = json.dumps(session_list)
# Override file path if we created a new Pippy instance
if self._py_file_loaded_from_journal:
file_path = os.path.join(app_temp, 'pippy-temp-instance-data')
_file = open(file_path, 'w')
_file.write(pippy_data)
_file.close()
if self._py_file_loaded_from_journal:
_logger.debug('setting pippy instance file_path to %s' %
file_path)
self._pippy_instance.set_file_path(file_path)
datastore.write(self._pippy_instance)
self._store_config()
def read_file(self, file_path):
# Either we are opening Python code or a list of objects
# stored (json-encoded) in a Pippy instance, or a shared
# session.
# Remove initial new/blank thing
self.session_data = []
self._loaded_session = []
try:
self._source_tabs.remove_page(0)
tab_object.pop(0)
self._source_tabs.last_tab = 0
except IndexError:
pass
if self.metadata['mime_type'] == 'text/x-python':
_logger.debug('Loading Python code')
# Opening some Python code directly
try:
text = open(file_path).read()
except:
alert = NotifyAlert(10)
alert.props.title = _('Error')
alert.props.msg = _('Error reading data.')
def _remove_alert(alert, response_id):
self.remove_alert(alert)
alert.connect("response", _remove_alert)
self.add_alert(alert)
return
self._py_file_loaded_from_journal = True
# Discard the '#!/usr/bin/python3' and 'coding: utf-8' lines,
# if present
python_code = re.sub(r'^' + re.escape(PYTHON_PREFIX), '', text)
name = self.metadata['title']
self._loaded_session.append([name, python_code, None])
# Since we loaded Python code, we need to create (or
# restore) a Pippy instance
if 'pippy_instance' in self.metadata:
_logger.debug('found a pippy instance: %s' %
self.metadata['pippy_instance'])
try:
self._pippy_instance = datastore.get(
self.metadata['pippy_instance'])
except:
_logger.debug('Cannot find old Pippy instance: %s')
self._pippy_instance = None
if self._pippy_instance in [self, None]:
self._pippy_instance = datastore.create()
self._pippy_instance.metadata['title'] = self.metadata['title']
self._pippy_instance.metadata['mime_type'] = 'application/json'
self._pippy_instance.metadata['activity'] = 'org.laptop.Pippy'
datastore.write(self._pippy_instance)
self.metadata['pippy_instance'] = \
self._pippy_instance.get_object_id()
_logger.debug('get_object_id %s' %
self.metadata['pippy_instance'])
# We need the Pippy file path so we can read the session data
file_path = self._pippy_instance.get_file_path()
# Finally, add this Python object to the session data
self._py_object_id = _find_object_id(self.metadata['activity_id'])
self.session_data.append(self._py_object_id)
_logger.debug('session_data: %s' % self.session_data)
if self.metadata['mime_type'] == 'application/json' or \
self._pippy_instance != self:
# Reading file list from Pippy instance
_logger.debug('Loading Pippy instance')
if len(file_path) == 0:
return
data = json.loads(open(file_path).read())
for name, content in data:
# content is either a datastore id or the path to some
# sample code
if content is not None and content[0] == '/': # a path
try:
python_code = open(content).read()
except:
_logger.error('Could not open %s; skipping' % content)
path = content
elif content != self._py_object_id:
try:
dsobject = datastore.get(content)
if 'mime_type' not in dsobject.metadata:
_logger.error(
'Warning: %s missing mime_type' % content)
elif dsobject.metadata['mime_type'] != 'text/x-python':
_logger.error(
'Warning: %s has unexpected mime_type %s' %
(content, dsobject.metadata['mime_type']))
except:
# Could be that the item has subsequently been
# deleted from the datastore, so we skip it.
_logger.error('Could not open %s; skipping' % content)
continue
try:
python_code = open(dsobject.get_file_path()).read()
except:
# Malformed bundle?
_logger.error('Could not open %s; skipping' %
dsobject.get_file_path())
continue
path = None
# Queue up the creation of the tabs...
# And add this content to the session data
if content not in self.session_data:
self.session_data.append(content)
self._loaded_session.append([name, python_code, path])
for name, content, path in self._loaded_session:
self._source_tabs.add_tab(name, content, path)
# TEMPLATES AND INLINE FILES
ACTIVITY_INFO_TEMPLATE = '''
[Activity]
name = %(title)s
bundle_id = %(bundle_id)s
exec = sugar-activity3 %(class)s
icon = activity-icon
activity_version = %(version)d
mime_types = %(mime_types)s
show_launcher = yes
%(extra_info)s
'''
PIPPY_ICON = """<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG
1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
<!ENTITY stroke_color "#010101">
<!ENTITY fill_color "#FFFFFF">
]>
<svg enable-background="new 0 0 55 55" height="55px" version="1.1"
viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block"
id="activity-pippy">
<path d="M28.497,48.507
c5.988,0,14.88-2.838,14.88-11.185
c0-9.285-7.743-10.143-10.954-11.083
c-3.549-0.799-5.913-1.914-6.055-3.455
c-0.243-2.642,1.158-3.671,3.946-3.671
c0,0,6.632,3.664,12.266,0.74
c1.588-0.823,4.432-4.668,4.432-7.32
c0-2.653-9.181-5.719-11.967-5.719
c-2.788,0-5.159,3.847-5.159,3.847
c-5.574,0-11.149,5.306-11.149,10.612
c0,5.305,5.333,9.455,11.707,10.612
c2.963,0.469,5.441,2.22,4.878,5.438
c-0.457,2.613-2.995,5.306-8.361,5.306
c-4.252,0-13.3-0.219-14.745-4.079
c-0.929-2.486,0.168-5.205,1.562-5.205l-0.027-0.16
c-1.42-0.158-5.548,0.16-5.548,5.465
C8.202,45.452,17.347,48.507,28.497,48.507z"
fill="&fill_color;" stroke="&stroke_color;"
stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
<path d="M42.579,19.854c-2.623-0.287-6.611-2-7.467-5.022" fill="none"
stroke="&stroke_color;" stroke-linecap="round" stroke-width="3"/>
<circle cx="35.805" cy="10.96" fill="&stroke_color;" r="1.676"/>
</g></svg><!-- " -->
"""
# ACTIVITY META-INFORMATION
# this is used by Pippy to generate a bundle for itself.
def pippy_activity_version():
'''Returns the version number of the generated activity bundle.'''
return 39
def pippy_activity_extra_files():
'''Returns a map of 'extra' files which should be included in the
generated activity bundle.'''
# Cheat here and generate the map from the fs contents.
extra = {}
bp = get_bundle_path()
for d in ['po', 'data', 'post']: # everybody gets library
for root, dirs, files in os.walk(os.path.join(bp, d)):
for name in files:
fn = os.path.join(root, name).replace(bp + '/', '')
extra[fn] = open(os.path.join(root, name), 'r').read()
return extra
def pippy_activity_news():
'''Return the NEWS file for this activity.'''
# Cheat again.
return open(os.path.join(get_bundle_path(), 'NEWS')).read()
def pippy_activity_icon():
'''Return an SVG document specifying the icon for this activity.'''
return PIPPY_ICON
def pippy_activity_class():
'''Return the class which should be started to run this activity.'''
return 'pippy_app.PippyActivity'
def pippy_activity_bundle_id():
'''Return the bundle_id for the generated activity.'''
return 'org.laptop.Pippy'
def pippy_activity_mime_types():
'''Return the mime types handled by the generated activity, as a list.'''
return ['text/x-python']
def pippy_activity_extra_info():
return '''
license = GPLv2+
update_url = http://activities.sugarlabs.org '''
# ACTIVITY BUNDLER
def main():
'''Create a bundle from a pippy-style source file'''
from optparse import OptionParser
from pyclbr import readmodule_ex
from tempfile import mkdtemp
from shutil import copytree, copy2, rmtree
from sugar3.activity import bundlebuilder
parser = OptionParser(usage='%prog [options] [title] [sourcefile] [icon]')
parser.add_option('-d', '--dir', dest='dir', default='.', metavar='DIR',
help='Put generated bundle in the specified directory.')
parser.add_option('-p', '--pythonpath', dest='path', action='append',
default=[], metavar='DIR',
help='Append directory to python search path.')
(options, args) = parser.parse_args()
if len(args) < 3:
parser.error('The title, sourcefile and icon arguments are required.')
title = args[0]
sourcefile = args[1]
icon_path = args[2]
pytitle = re.sub(r'[^A-Za-z0-9_]', '', title)
if re.match(r'[0-9]', pytitle) is not None:
pytitle = '_' + pytitle # first character cannot be numeric
# First take a gander at the source file and see if it's got extra info
# for us.
sourcedir, basename = os.path.split(sourcefile)
if not sourcedir:
sourcedir = '.'
module, ext = os.path.splitext(basename)
f = open(icon_path, 'r')
icon = f.read()
f.close()
# Things we look for:
bundle_info = {
'version': 1,
'extra_files': {},
'news': 'No news.',
'icon': icon,
'class': 'activity.VteActivity',
'bundle_id': ('org.sugarlabs.pippy.%s%d' %
(generate_unique_id(),
int(round(uniform(1000, 9999), 0)))),
'mime_types': '',
'extra_info': '',
}
# Are any of these things in the module?
try_import = False
info = readmodule_ex(module, [sourcedir] + options.path)
for func in list(bundle_info.keys()):
p_a_func = 'pippy_activity_%s' % func
if p_a_func in info:
try_import = True
if try_import:
# Yes, let's try to execute them to get better info about our bundle
oldpath = list(sys.path)
sys.path[0:0] = [sourcedir] + options.path
modobj = __import__(module)
for func in list(bundle_info.keys()):
p_a_func = 'pippy_activity_%s' % func
if p_a_func in modobj.__dict__:
bundle_info[func] = modobj.__dict__[p_a_func]()
sys.path = oldpath
# Okay! We've done the hard part. Now let's build a bundle.
# Create a new temp dir in which to create the bundle.
app_temp = mkdtemp('.activity', 'Pippy') # Hope TMPDIR is set correctly!
bundle = get_bundle_path()
try:
copytree('%s/library' % bundle, '%s/library' % app_temp)
copy2('%s/activity.py' % bundle, '%s/activity.py' % app_temp)
# create activity.info file.
bundle_info['title'] = title
bundle_info['pytitle'] = pytitle
# put 'extra' files in place.
extra_files = {
'activity/activity.info': ACTIVITY_INFO_TEMPLATE % bundle_info,
'activity/activity-icon.svg': bundle_info['icon'],
'NEWS': bundle_info['news'],
}
extra_files.update(bundle_info['extra_files'])
for path, contents in list(extra_files.items()):
# safety first!
assert '..' not in path
dirname, filename = os.path.split(path)
dirname = os.path.join(app_temp, dirname)
if not os.path.exists(dirname):
os.makedirs(dirname)
with open(os.path.join(dirname, filename), 'w') as f:
f.write(contents)
# Put script into $app_temp/pippy_app.py
copy2(sourcefile, '%s/pippy_app.py' % app_temp)
# Invoke bundle builder
olddir = os.getcwd()
oldargv = sys.argv
os.chdir(app_temp)
sys.argv = ['setup.py', 'dist_xo']
print('\r\nStarting bundlebuilder\r\n')
bundlebuilder.start()
sys.argv = oldargv
os.chdir(olddir)
# Move to destination directory.
src = '%s/dist/%s-%d.xo' % (app_temp, pytitle, bundle_info['version'])
dst = '%s/%s-%d.xo' % (options.dir, pytitle, bundle_info['version'])
if not os.path.exists(src):
print('Cannot find %s\r\n' % (src))
else:
copy2(src, dst)
finally:
rmtree(app_temp, ignore_errors=True)
print('Finally\r\n')
if __name__ == '__main__':
from gettext import gettext as _
if False: # Change this to True to test within Pippy
sys.argv = sys.argv + ['-d', '/tmp', 'Pippy',
'/home/olpc/pippy_app.py']
print(_('Working...'))
sys.stdout.flush()
main()
print(_('done!'))
sys.exit(0)