diff options
-rw-r--r-- | activity/activity.info | 2 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rw-r--r-- | terminal.py | 472 |
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 @@ -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')) |