From ee6d1e00d55dc3b2bc0b373c1fd3b2d87cafb35b Mon Sep 17 00:00:00 2001 From: Wade Brainerd Date: Wed, 04 Mar 2009 15:01:57 +0000 Subject: Added support for multiple tabs. Reworked toolbox a bit. WIP on save/resume shell state from Journal. --- diff --git a/terminal.py b/terminal.py index 6c88a9d..2d027c0 100644 --- a/terminal.py +++ b/terminal.py @@ -17,12 +17,18 @@ import os -import logging from gettext import gettext as _ +# Initialize logging. +import logging +log = logging.getLogger('Terminal') +log.setLevel(logging.DEBUG) +logging.basicConfig() + import gtk import dbus +import sugar.graphics.toolbutton from sugar.activity import activity from sugar import env from sugar.graphics.toolbutton import ToolButton @@ -33,201 +39,262 @@ import os.path import vte import pango +import simplejson + class TerminalActivity(activity.Activity): def __init__(self, handle): 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) + self.connect('key-press-event', self._key_press_cb) toolbox = 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 = 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 = 'C' + editbar.paste.connect('clicked', self._paste_cb) + editbar.paste.props.accelerator = '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 = 'T' + newtabbtn.connect('clicked', self._open_tab_cb) + + deltabbtn = sugar.graphics.toolbutton.ToolButton('list-remove') + deltabbtn.set_tooltip(_("Close Tab")) + deltabbtn.props.accelerator = 'X' + deltabbtn.connect('clicked', self._close_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 + 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 = 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 = 'Left' + prevtabbtn.connect('clicked', self._prev_tab_cb) + + nexttabbtn = sugar.graphics.toolbutton.ToolButton('go-next') + nexttabbtn.set_tooltip(_("Next Tab")) + nexttabbtn.props.accelerator = '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 = '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.set_canvas(self.notebook) + + 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() + + def _restore_shell(self, vt, tab_idx, data, data_file): + tab_state = data['tabs'][tab_idx] + + # Restore the scrollback buffer. + for l in tab_state['scrollback']: + vt.feed(l + '\r\n') + + # Launch the shell using the wrapper script to restore the environment. + bundle_path = activity.get_bundle_path() + os.chdir(bundle_path) + pid = vt.fork_command(bundle_path + '/shell.py', \ + '--state', data_file, + '--tab', tab_idx) + + # TODO: Figure out how to grab the environment from the PID when closing. + log.debug(pid) + + def _launch_shell(self, vt, tab_state): + # Launch the default shell in the HOME directory. + if tab_state: + # TODO: Need to save cwd on exit. + #os.chdir(tab_data['cwd']) + os.chdir(os.environ["HOME"]) + + # Restore the scrollback buffer. + for l in tab_state['scrollback']: + vt.feed(l + '\r\n') + + else: + os.chdir(os.environ["HOME"]) + + vt.fork_command() + + 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 _create_tab(self, tab_state): + vt = vte.Terminal() + vt.set_scroll_on_keystroke(True) + vt.connect("child-exited", self._tab_child_exited_cb) + vt.connect("window-title-changed", self._tab_title_changed_cb) + vt.show() - self._vte = VTE() - self._vte.set_scroll_on_keystroke(True) - self._vte.connect("child-exited", lambda term: self.close()) - self._vte.show() + label = gtk.Label() - scrollbar = gtk.VScrollbar(self._vte.get_adjustment()) + scrollbar = gtk.VScrollbar(vt.get_adjustment()) scrollbar.show() - box.pack_start(self._vte) - box.pack_start(scrollbar, False, False, 0) - - self.set_canvas(box) - box.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() + self.notebook.props.show_tabs = self.notebook.get_n_pages() > 1 + + self._launch_shell(vt, tab_state) + + 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 __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 + def _fullscreen_cb(self, btn): + self.fullscreen() + + def _key_press_cb(self, window, event): + if gtk.gdk.keyval_name(event.keyval) == 'Escape': + 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 _configure_vte(self): - conf = ConfigParser.ConfigParser() - conf_file = os.path.join(env.get_profile_path(), 'terminalrc') - - if os.path.isfile(conf_file): - f = open(conf_file, 'r') - conf.readfp(f) - f.close() - else: - conf.add_section('terminal') + def read_file(self, file_path): + if self.metadata['mime_type'] != 'text/plain': + return - if conf.has_option('terminal', 'font'): - font = conf.get('terminal', 'font') - else: - font = 'Monospace 8' - conf.set('terminal', 'font', font) - self.set_font(pango.FontDescription(font)) + fd = open(file_path, 'r') + text = fd.read() + data = simplejson.loads(text) + fd.close() - 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) - - self.set_cursor_blinks(blink) + data_file = file_path - if conf.has_option('terminal', 'bell'): - bell = conf.getboolean('terminal', 'bell') - else: - bell = False - conf.set('terminal', 'bell', bell) - self.set_audible_bell(bell) - - 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) + while self.notebook.get_n_pages(): + self.notebook.remove_page(0) - 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) - - if conf.has_option('terminal', 'emulation'): - emulation = conf.get('terminal', 'emulation') - else: - emulation = 'xterm' - conf.set('terminal', 'emulation', emulation) - self.set_emulation(emulation) + for tab in data['tabs']: + index = self._create_tab(tab) - 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')) + self.notebook.props.page = data['current-tab'] - def on_gconf_notification(self, client, cnxn_id, entry, what): - self.reconfigure_vte() + def write_file(self, file_path): + if not self.metadata['mime_type']: + self.metadata['mime_type'] = 'text/plain' - def on_vte_button_press(self, term, event): - if event.button == 3: - self.do_popup(event) - return True + 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') + + tab = { 'env': {}, 'cwd': '', 'scrollback': scrollback_lines } + data['tabs'].append(tab) + + fd = open(file_path, 'w') + text = simplejson.dumps(data) + fd.write(text) + fd.close() - def on_vte_popup_menu(self, term): - pass -- cgit v0.9.1