Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activity/activity.info2
-rwxr-xr-xsetup.py2
-rw-r--r--terminal.py472
3 files changed, 312 insertions, 164 deletions
diff --git a/activity/activity.info b/activity/activity.info
index 3a90d0e..a8242fd 100644
--- a/activity/activity.info
+++ b/activity/activity.info
@@ -1,6 +1,6 @@
[Activity]
name = Terminal
-activity_version = 23
+activity_version = 24
service_name = org.laptop.Terminal
exec = sugar-activity terminal.TerminalActivity
icon = activity-terminal
diff --git a/setup.py b/setup.py
index 297d112..876cd3f 100755
--- a/setup.py
+++ b/setup.py
@@ -18,5 +18,5 @@
from sugar.activity import bundlebuilder
-bundlebuilder.start('Terminal')
+bundlebuilder.start()
diff --git a/terminal.py b/terminal.py
index fc7afe3..cdaee02 100644
--- a/terminal.py
+++ b/terminal.py
@@ -15,130 +15,335 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-import os
+import os, os.path, simplejson, ConfigParser
-import logging
from gettext import gettext as _
-import gtk
-import dbus
-
-from sugar.activity import activity
-from sugar import env
-from sugar.graphics.toolbutton import ToolButton
-from sugar.graphics.palette import Palette
-import ConfigParser
-import os.path
+# Initialize logging.
+import logging
+log = logging.getLogger('Terminal')
+log.setLevel(logging.DEBUG)
+logging.basicConfig()
+import gtk
import vte
import pango
-class TerminalActivity(activity.Activity):
+import sugar.graphics.toolbutton
+import sugar.activity.activity
+import sugar.env
+
+MASKED_ENVIRONMENT = [
+ 'DBUS_SESSION_BUS_ADDRESS',
+ 'PPID'
+]
+
+class TerminalActivity(sugar.activity.activity.Activity):
def __init__(self, handle):
- activity.Activity.__init__(self, handle)
+ sugar.activity.activity.Activity.__init__(self, handle)
- logging.debug('Starting the Terminal activity')
+ self.data_file = None
self.set_title(_('Terminal Activity'))
- self.connect('key-press-event', self.__key_press_cb)
+
+ # Non-working attempt to hide the Escape key from Sugar.
+ #self.connect('key-press-event', self._key_press_cb)
- toolbox = activity.ActivityToolbox(self)
+ toolbox = sugar.activity.activity.ActivityToolbox(self)
- self._edit_toolbar = activity.EditToolbar()
- toolbox.add_toolbar(_('Edit'), self._edit_toolbar)
- self._edit_toolbar.show()
- self._edit_toolbar.undo.props.visible = False
- self._edit_toolbar.redo.props.visible = False
- self._edit_toolbar.separator.props.visible = False
- self._edit_toolbar.copy.connect('clicked', self._copy_cb)
- self._edit_toolbar.paste.connect('clicked', self._paste_cb)
+ editbar = sugar.activity.activity.EditToolbar()
+ toolbox.add_toolbar(_('Edit'), editbar)
+ editbar.show()
+ editbar.undo.props.visible = False
+ editbar.redo.props.visible = False
+ editbar.separator.props.visible = False
+ editbar.copy.connect('clicked', self._copy_cb)
+ editbar.copy.props.accelerator = '<Ctrl><Shift>C'
+ editbar.paste.connect('clicked', self._paste_cb)
+ editbar.paste.props.accelerator = '<Ctrl><Shift>V'
- activity_toolbar = toolbox.get_activity_toolbar()
- # free up keyboard accelerators per #4646
- activity_toolbar.stop.props.accelerator = None
+ newtabbtn = sugar.graphics.toolbutton.ToolButton('list-add')
+ newtabbtn.set_tooltip(_("Open New Tab"))
+ newtabbtn.props.accelerator = '<Ctrl><Shift>T'
+ newtabbtn.connect('clicked', self._open_tab_cb)
- # unneeded buttons (also frees up keyboard accelerators per #4646)
- activity_toolbar.remove(activity_toolbar.share)
- activity_toolbar.share = None
- activity_toolbar.remove(activity_toolbar.keep)
- activity_toolbar.keep = None
+ deltabbtn = sugar.graphics.toolbutton.ToolButton('list-remove')
+ deltabbtn.set_tooltip(_("Close Tab"))
+ deltabbtn.props.accelerator = '<Ctrl><Shift>X'
+ deltabbtn.connect('clicked', self._close_tab_cb)
+
+ tabsep = gtk.SeparatorToolItem()
+ tabsep.set_expand(True)
+ tabsep.set_draw(False)
# Add a button that will be used to become root easily.
- activity_toolbar.become_root = ToolButton('activity-become-root')
- activity_toolbar.become_root.set_tooltip(_('Become root'))
- activity_toolbar.become_root.connect('clicked',
- self._become_root_cb)
- activity_toolbar.insert(activity_toolbar.become_root, 2)
- activity_toolbar.become_root.show()
+ rootbtn = sugar.graphics.toolbutton.ToolButton('activity-become-root')
+ rootbtn.set_tooltip(_('Become root'))
+ rootbtn.connect('clicked', self._become_root_cb)
+
+ prevtabbtn = sugar.graphics.toolbutton.ToolButton('go-previous')
+ prevtabbtn.set_tooltip(_("Previous Tab"))
+ prevtabbtn.props.accelerator = '<Ctrl><Shift>Left'
+ prevtabbtn.connect('clicked', self._prev_tab_cb)
+
+ nexttabbtn = sugar.graphics.toolbutton.ToolButton('go-next')
+ nexttabbtn.set_tooltip(_("Next Tab"))
+ nexttabbtn.props.accelerator = '<Ctrl><Shift>Right'
+ nexttabbtn.connect('clicked', self._next_tab_cb)
+ tabbar = gtk.Toolbar()
+ tabbar.insert(newtabbtn, -1)
+ tabbar.insert(deltabbtn, -1)
+ tabbar.insert(tabsep, -1)
+ tabbar.insert(rootbtn, -1)
+ tabbar.insert(prevtabbtn, -1)
+ tabbar.insert(nexttabbtn, -1)
+ tabbar.show_all()
+
+ toolbox.add_toolbar(_('Tab'), tabbar)
+
+ activity_toolbar = toolbox.get_activity_toolbar()
+ activity_toolbar.share.props.visible = False
+ activity_toolbar.keep.props.visible = False
+
+ fullscreenbtn = sugar.graphics.toolbutton.ToolButton('view-fullscreen')
+ fullscreenbtn.set_tooltip(_("Fullscreen"))
+ fullscreenbtn.props.accelerator = '<Alt>Enter'
+ fullscreenbtn.connect('clicked', self._fullscreen_cb)
+ activity_toolbar.insert(fullscreenbtn, 2)
+ fullscreenbtn.show()
+
self.set_toolbox(toolbox)
toolbox.show()
- box = gtk.HBox(False, 4)
+ self.notebook = gtk.Notebook()
+ self.notebook.set_property("tab-pos", gtk.POS_BOTTOM)
+ self.notebook.set_scrollable(True)
+ self.notebook.show()
- self._vte = VTE()
- self._vte.set_scroll_on_keystroke(True)
- self._vte.connect("child-exited", lambda term: self.close())
- self._vte.show()
+ self.set_canvas(self.notebook)
- scrollbar = gtk.VScrollbar(self._vte.get_adjustment())
- scrollbar.show()
+ self._create_tab(None)
+
+ def _open_tab_cb(self, btn):
+ index = self._create_tab(None)
+ self.notebook.page = index
+
+ def _close_tab_cb(self, btn):
+ self._close_tab(self.notebook.props.page)
+
+ def _prev_tab_cb(self, btn):
+ if self.notebook.props.page == 0:
+ self.notebook.props.page = self.notebook.get_n_pages() - 1
+ else:
+ self.notebook.props.page = self.notebook.props.page - 1
+ vt = self.notebook.get_nth_page(self.notebook.get_current_page()).vt
+ vt.grab_focus()
+
+ def _next_tab_cb(self, btn):
+ if self.notebook.props.page == self.notebook.get_n_pages() - 1:
+ self.notebook.props.page = 0
+ else:
+ self.notebook.props.page = self.notebook.props.page + 1
+ vt = self.notebook.get_nth_page(self.notebook.get_current_page()).vt
+ vt.grab_focus()
- box.pack_start(self._vte)
- box.pack_start(scrollbar, False, False, 0)
+ def _close_tab(self, index):
+ self.notebook.remove_page(index)
+ if self.notebook.get_n_pages() == 0:
+ self.close()
+
+ def _tab_child_exited_cb(self, vt):
+ for i in range(self.notebook.get_n_pages()):
+ if self.notebook.get_nth_page(i).vt == vt:
+ self._close_tab(i)
+ return
+
+ def _tab_title_changed_cb(self, vt):
+ for i in range(self.notebook.get_n_pages()):
+ if self.notebook.get_nth_page(i).vt == vt:
+ label = self.notebook.get_nth_page(i).label
+ label.set_text(vt.get_window_title())
+ return
+
+ def _drag_data_received_cb(self, widget, context, x, y, selection, target, time):
+ widget.feed_child(selection.data)
+ context.finish(True, False, time)
+ return True
+
+ def _create_tab(self, tab_state):
+ vt = vte.Terminal()
+ vt.connect("child-exited", self._tab_child_exited_cb)
+ vt.connect("window-title-changed", self._tab_title_changed_cb)
+
+ vt.drag_dest_set(gtk.DEST_DEFAULT_MOTION|gtk.DEST_DEFAULT_DROP,
+ [('text/plain', 0, 0), ('STRING', 0, 1)],
+ gtk.gdk.ACTION_DEFAULT|
+ gtk.gdk.ACTION_COPY)
+ vt.connect('drag_data_received', self._drag_data_received_cb)
- self.set_canvas(box)
- box.show()
+ self._configure_vt(vt)
+
+ vt.show()
+
+ label = gtk.Label()
+
+ scrollbar = gtk.VScrollbar(vt.get_adjustment())
+ scrollbar.show()
+
+ box = gtk.HBox()
+ box.pack_start(vt)
+ box.pack_start(scrollbar)
+
+ box.vt = vt
+ box.label = label
- self._vte.grab_focus()
+ index = self.notebook.append_page(box, label)
+ self.notebook.show_all()
+
+ # Uncomment this to only show the tab bar when there is at least one tab.
+ # I think it's useful to always see it, since it displays the 'window title'.
+ #self.notebook.props.show_tabs = self.notebook.get_n_pages() > 1
+
+ # Launch the default shell in the HOME directory.
+ os.chdir(os.environ["HOME"])
+
+ if tab_state:
+ # Restore the environment.
+ # This is currently not enabled.
+ env = tab_state['env']
+
+ filtered_env = []
+ for e in env:
+ var, sep, value = e.partition('=')
+ if var not in MASKED_ENVIRONMENT:
+ filtered_env.append(var + sep + value)
+
+ # TODO: Make the shell restore these environment variables, then clear out TERMINAL_ENV.
+ #os.environ['TERMINAL_ENV'] = '\n'.join(filtered_env)
+
+ # Restore the working directory.
+ if tab_state.has_key('cwd'):
+ os.chdir(tab_state['cwd'])
+
+ # Restore the scrollback buffer.
+ for l in tab_state['scrollback']:
+ vt.feed(l + '\r\n')
+
+ box.pid = vt.fork_command()
+
+ self.notebook.props.page = index
+ vt.grab_focus()
+
+ return index
def _copy_cb(self, button):
- if self._vte.get_has_selection():
- self._vte.copy_clipboard()
+ vt = self.notebook.get_nth_page(self.notebook.get_current_page()).vt
+ if vt.get_has_selection():
+ vt.copy_clipboard()
def _paste_cb(self, button):
- self._vte.paste_clipboard()
+ vt = self.notebook.get_nth_page(self.notebook.get_current_page()).vt
+ vt.paste_clipboard()
def _become_root_cb(self, button):
- self._vte.fork_command("/bin/su", ('/bin/su', '-'))
+ vt = self.notebook.get_nth_page(self.notebook.get_current_page()).vt
+ vt.feed('\r\n')
+ vt.fork_command("/bin/su", ('/bin/su', '-'))
+
+ def _fullscreen_cb(self, btn):
+ self.fullscreen()
+
+ def _key_press_cb(self, window, event):
+ # Escape keypresses are routed directly to the vte and then dropped.
+ # This hack prevents Sugar from hijacking them and canceling fullscreen mode.
+ if gtk.gdk.keyval_name(event.keyval) == 'Escape':
+ vt = self.notebook.get_nth_page(self.notebook.get_current_page()).vt
+ vt.event(event)
+ return True
- def __key_press_cb(self, window, event):
- if event.state & gtk.gdk.CONTROL_MASK and event.state & gtk.gdk.SHIFT_MASK:
-
- if gtk.gdk.keyval_name(event.keyval) == "C":
- if self._vte.get_has_selection():
- self._vte.copy_clipboard()
- return True
- elif gtk.gdk.keyval_name(event.keyval) == "V":
- self._vte.paste_clipboard()
- return True
-
return False
-class VTE(vte.Terminal):
- def __init__(self):
- vte.Terminal.__init__(self)
- self._configure_vte()
- self.drag_dest_set(gtk.DEST_DEFAULT_MOTION|
- gtk.DEST_DEFAULT_DROP,
- [('text/plain', 0, 0),
- ('STRING', 0, 1)],
- gtk.gdk.ACTION_DEFAULT|
- gtk.gdk.ACTION_COPY)
- self.connect('drag_data_received', self.data_cb)
-
- os.chdir(os.environ["HOME"])
- self.fork_command()
-
- def data_cb(self, widget, context, x, y, selection, target, time):
- self.feed_child(selection.data)
- context.finish(True, False, time)
- return True
+ def read_file(self, file_path):
+ if self.metadata['mime_type'] != 'text/plain':
+ return
+
+ fd = open(file_path, 'r')
+ text = fd.read()
+ data = simplejson.loads(text)
+ fd.close()
+
+ data_file = file_path
+
+ # Clean out any existing tabs.
+ while self.notebook.get_n_pages():
+ self.notebook.remove_page(0)
+
+ # Create new tabs from saved state.
+ for tab_state in data['tabs']:
+ self._create_tab(tab_state)
+
+ # Restore active tab.
+ self.notebook.props.page = data['current-tab']
+
+ # Create a blank one if this state had no terminals.
+ if self.notebook.get_n_pages() == 0:
+ self._create_tab(None)
+
+ def write_file(self, file_path):
+ if not self.metadata['mime_type']:
+ self.metadata['mime_type'] = 'text/plain'
+
+ data = {}
+ data['current-tab'] = self.notebook.get_current_page()
+ data['tabs'] = []
+
+ for i in range(self.notebook.get_n_pages()):
+ page = self.notebook.get_nth_page(i)
+
+ def selected_cb(terminal, c, row, cb_data):
+ return 1
+ (scrollback_text, attrs) = page.vt.get_text(selected_cb, 1)
+
+ scrollback_lines = scrollback_text.split('\n')
+
+ # Note- this currently gets the child's initial environment rather than the current
+ # environment, making it not very useful.
+ environment = open('/proc/%d/environ' % page.pid, 'r').read().split('\0')
+
+ cwd = os.readlink('/proc/%d/cwd' % page.pid)
+
+ tab_state = { 'env': environment, 'cwd': cwd, 'scrollback': scrollback_lines }
+
+ data['tabs'].append(tab_state)
+
+ fd = open(file_path, 'w')
+ text = simplejson.dumps(data)
+ fd.write(text)
+ fd.close()
- def _configure_vte(self):
+ def _get_conf(self, conf, var, default):
+ if conf.has_option('terminal', var):
+ if isinstance(default, bool):
+ return conf.getboolean('terminal', var)
+ elif isinstance(default, int):
+ return conf.getint('terminal', var)
+ else:
+ return conf.get('terminal', var)
+ else:
+ if isinstance(default, bool):
+ conf.setboolean('terminal', var, default)
+ elif isinstance(default, int):
+ conf.setint('terminal', var, default)
+ else:
+ conf.set('terminal', var, default)
+
+ return default
+
+ def _configure_vt(self, vt):
conf = ConfigParser.ConfigParser()
- conf_file = os.path.join(env.get_profile_path(), 'terminalrc')
+ conf_file = os.path.join(sugar.env.get_profile_path(), 'terminalrc')
if os.path.isfile(conf_file):
f = open(conf_file, 'r')
@@ -146,88 +351,31 @@ class VTE(vte.Terminal):
f.close()
else:
conf.add_section('terminal')
+
+ font = self._get_conf(conf, 'font', 'Monospace')
+ vt.set_font(pango.FontDescription(font))
- if conf.has_option('terminal', 'font'):
- font = conf.get('terminal', 'font')
- else:
- font = 'Monospace 10'
- conf.set('terminal', 'font', font)
- self.set_font(pango.FontDescription(font))
+ blink = self._get_conf(conf, 'cursor_blink', False)
+ vt.set_cursor_blinks(blink)
- if conf.has_option('terminal', 'fg_color'):
- fg_color = conf.get('terminal', 'fg_color')
- else:
- fg_color = '#000000'
- conf.set('terminal', 'fg_color', fg_color)
- if conf.has_option('terminal', 'bg_color'):
- bg_color = conf.get('terminal', 'bg_color')
- else:
- bg_color = '#FFFFFF'
- conf.set('terminal', 'bg_color', bg_color)
- self.set_colors(gtk.gdk.color_parse (fg_color),
- gtk.gdk.color_parse (bg_color),
- [])
-
- if conf.has_option('terminal', 'cursor_blink'):
- blink = conf.getboolean('terminal', 'cursor_blink')
- else:
- blink = False
- conf.set('terminal', 'cursor_blink', blink)
+ bell = self._get_conf(conf, 'bell', False)
+ vt.set_audible_bell(bell)
- self.set_cursor_blinks(blink)
+ scrollback_lines = self._get_conf(conf, 'scrollback_lines', 1000)
+ vt.set_scrollback_lines(scrollback_lines)
- if conf.has_option('terminal', 'bell'):
- bell = conf.getboolean('terminal', 'bell')
- else:
- bell = False
- conf.set('terminal', 'bell', bell)
- self.set_audible_bell(bell)
+ vt.set_allow_bold(True)
- if conf.has_option('terminal', 'scrollback_lines'):
- scrollback_lines = conf.getint('terminal', 'scrollback_lines')
- else:
- scrollback_lines = 1000
- conf.set('terminal', 'scrollback_lines', scrollback_lines)
-
- self.set_scrollback_lines(scrollback_lines)
- self.set_allow_bold(True)
-
- if conf.has_option('terminal', 'scroll_on_keystroke'):
- scroll_key = conf.getboolean('terminal', 'scroll_on_keystroke')
- else:
- scroll_key = False
- conf.set('terminal', 'scroll_on_keystroke', scroll_key)
- self.set_scroll_on_keystroke(scroll_key)
+ scroll_key = self._get_conf(conf, 'scroll_on_keystroke', True)
+ vt.set_scroll_on_keystroke(scroll_key)
- if conf.has_option('terminal', 'scroll_on_output'):
- scroll_output = conf.getboolean('terminal', 'scroll_on_output')
- else:
- scroll_output = False
- conf.set('terminal', 'scroll_on_output', scroll_output)
- self.set_scroll_on_output(scroll_output)
+ scroll_output = self._get_conf(conf, 'scroll_on_output', False)
+ vt.set_scroll_on_output(scroll_output)
- if conf.has_option('terminal', 'emulation'):
- emulation = conf.get('terminal', 'emulation')
- else:
- emulation = 'xterm'
- conf.set('terminal', 'emulation', emulation)
- self.set_emulation(emulation)
+ emulation = self._get_conf(conf, 'emulation', 'xterm')
+ vt.set_emulation(emulation)
- if conf.has_option('terminal', 'visible_bell'):
- visible_bell = conf.getboolean('terminal', 'visible_bell')
- else:
- visible_bell = False
- conf.set('terminal', 'visible_bell', visible_bell)
- self.set_visible_bell(visible_bell)
- conf.write(open(conf_file, 'w'))
-
- def on_gconf_notification(self, client, cnxn_id, entry, what):
- self.reconfigure_vte()
+ visible_bell = self._get_conf(conf, 'visible_bell', False)
+ vt.set_visible_bell(visible_bell)
- def on_vte_button_press(self, term, event):
- if event.button == 3:
- self.do_popup(event)
- return True
-
- def on_vte_popup_menu(self, term):
- pass
+ conf.write(open(conf_file, 'w'))