silk / silk.py

nakamura b438c01 







nakamura 0856dff 
nakamura b438c01 

nakamura af3009f 
nakamura b438c01 






































nakamura fc2c232 





nakamura b438c01 

nakamura 068b005 
nakamura b438c01 























nakamura 0856dff 

nakamura b438c01 






nakamura 25a0bcf 
nakamura b438c01 
nakamura 25a0bcf 

nakamura b438c01 



nakamura 25a0bcf 
nakamura 0c97531 

nakamura b438c01 
nakamura 0856dff 
nakamura af3009f 
nakamura b438c01 















nakamura 25a0bcf 

nakamura 0c97531 
nakamura 25a0bcf 
nakamura 0856dff 
















nakamura af3009f 








nakamura 7ce0011 


nakamura b438c01 















nakamura f1301de 
nakamura b438c01 

nakamura 34aefe1 
nakamura b438c01 

nakamura 34aefe1 


nakamura f1301de 

nakamura b438c01 































nakamura 068b005 
nakamura b438c01 











nakamura 7ce0011 

nakamura b438c01 















nakamura fc2c232 

nakamura b438c01 
nakamura fc2c232 

nakamura b438c01 

nakamura 7ce0011 













nakamura b438c01 




























nakamura 34aefe1 
nakamura b438c01 


#!/usr/bin/python
"""
silk - terminal emulator to control multiple commands at once

silk open multiple virtual terminals for each command
and send keyboard input for all/some/one terminal.
"""

import gobject
import os

from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import Vte


class Terminal(Vte.Terminal):
    term_name = 'xterm'
    _size = (80, 25)

    def __init__(self, command, size=None, *args, **kwargs):
        super(Terminal, self).__init__(*args, **kwargs)

        if size is not None:
            self._size = size

        self.resize_terminal()
        self.run_command(command)

        # TODO: right click context menu

    @property
    def width(self):
        # FIXME: how to get xpad?
        xpad = 5
        x, _ = self._size
        return self.get_char_width() * x + xpad

    @property
    def height(self):
        # FIXME: how to get ypad?
        ypad = 5
        _, y = self._size
        return self.get_char_height() * y + ypad

    def resize_terminal(self, size=None):
        if size is not None:
            self._size = size

        self.set_size_request(self.width, self.height)

    @property
    def child_exited(self):
        # if child exited, Terminal.get_pty_object() return None
        # XXX: realy?
        return self.get_pty_object() is None

    def run_command(self, command=None):
        if command is None:
            command = Vte.get_user_shell()

        # cwd
        working_directory = ''

        env = os.environ.copy()
        env['TERM'] = self.term_name
        envv = ['%s=%s' % kv for kv in env.iteritems()]

        if isinstance(command, (tuple, list)):
            argv = command
        else:
            argv = ['/bin/sh', '-c', command]

        self.fork_command_full(Vte.PtyFlags.DEFAULT,
                               working_directory,
                               argv, envv, 0,
                               None, None)


class TerminalFrame(Gtk.VBox):
    def __init__(self, command, terminal_size=None):
        super(TerminalFrame, self).__init__()
        self.set_border_width(1)

        self._command = command

        self._hbox = Gtk.HBox()
        self.pack_start(self._hbox, False, False, 0)

        self._sync_check = Gtk.CheckButton()
        self._sync_check.set_active(True)
        self._hbox.pack_start(self._sync_check, False, False, 0)

        self._eb = Gtk.EventBox()
        self._label = Gtk.Label(command)
        self._hbox.pack_start(self._eb, True, True, 0)
        self._eb.add(self._label)

        self.terminal = Terminal(command, terminal_size)
        self.pack_end(self.terminal, True, True, 0)

        self._eb.connect('button-release-event', self.do_eb_button_release)
        self._sync_check.connect('button-release-event',
                                 lambda *_: self.terminal.grab_focus())

        self.terminal.connect('child-exited', self.do_terminal_child_exited)
        self.terminal.connect('key-press-event', self.do_terminal_key_press)

    @property
    def width(self):
        xpad = 2 # border_width * 2
        return self.terminal.width + self.get_border_width() + xpad

    @property
    def height(self):
        # FIXME: how to get ypad?
        ypad = 19
        return self.terminal.height + self.get_border_width() + ypad

    @property
    def is_synced(self):
        return self._sync_check.get_active()

    def do_eb_button_release(self, *args):
        self._sync_check.set_active(not self._sync_check.get_active())
        self.terminal.grab_focus()

    def do_terminal_child_exited(self, *args):
        self._sync_check.set_active(False)
        msg = '\n[Press Enter to restart command]'
        self.terminal.feed(msg, len(msg))
        self.terminal.connect('commit', self.do_terminal_commit)

    def do_terminal_commit(self, term, text, length):
        if text in ('\n', '\r', '\r\n'):
            self.terminal.disconnect_by_func(self.do_terminal_commit)
            self.terminal.feed('\r\n', 2)

            # to avoid this input being feeded to this command,
            # call run_command after this callback
            gobject.timeout_add(0, self.terminal.run_command, self._command)

            return True

    def do_terminal_key_press(self, term, event):
        keyname = Gdk.keyval_name(event.keyval)

        if event.state == Gdk.ModifierType.CONTROL_MASK|Gdk.ModifierType.MOD1_MASK:
            # Ctrl + Alt + ...
            if keyname == 's':
                self._sync_check.set_active(not self._sync_check.get_active())
                return True

    def set_sync(self, is_synced):
        self._sync_check.set_active(is_synced)


class Config(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

    def update(self, *args, **kwargs):
        return self.__dict__.update(*args, **kwargs)

    def get(self, *args, **kwargs):
        return self.__dict__.get(*args, **kwargs)


class MainWindow(Gtk.Window):
    config = Config(
        columns = 2,
        terminal_size = (80, 25),
        window_size = (800, 640),
    )

    def __init__(self, config=None, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        if config is not None:
            self.config.update(config)

        self.set_default_size(*self.config.window_size)

        self._scrolled = Gtk.ScrolledWindow()
        hadj = self._scrolled.get_hadjustment()
        vadj = self._scrolled.get_vadjustment()
        self._layout = Gtk.Layout.new(hadj, vadj)
        self._grid = Gtk.Grid()

        self.add(self._scrolled)
        self._scrolled.add(self._layout)
        self._layout.put(self._grid, 0, 0)

        self.connect('destroy', lambda *_: Gtk.main_quit())

        # TODO: provide keyboard shortcut to move focus
        # TODO: highlights dffis of terminals
        # TODO: blur terminals that keyboard input will not send to
        # TODO: provide way to open new terminal

    def open(self, *targets):
        for t in targets:
            commands = self.config.get(t, t)
            if not isinstance(commands, (tuple, list)):
                commands = [commands]

            for c in commands:
                self.open_terminal(c)

    @property
    def terminals(self):
        return self._grid.get_children()

    def open_terminal(self, command=None):
        if command is None:
            command = Vte.get_user_shell()

        t = TerminalFrame(command, self.config.terminal_size)
        self.add_terminal(t)

    def add_terminal(self, term_frame):
        row, col = divmod(len(self.terminals), self.config.columns)
        self._grid.attach(term_frame, col, row, 1, 1)

        self.resize_layout()

        term_frame.terminal.connect_object('commit',
                                           self.do_term_commit, term_frame)
        term_frame.terminal.connect_object('key-press-event',
                                           self.do_term_key_press, term_frame)

        term_frame.show_all()

    def resize_layout(self):
        w = 0
        h = 0

        for row in (self.terminals[n:n+self.config.columns]
                    for n in xrange(0, len(self.terminals), self.config.columns)):
            w = max(w, sum(t.width for t in row))
            h += max(t.height for t in row)

        self._layout.set_size(w, h)
        self._grid.set_size_request(w, h)

    def do_term_commit(self, term_frame, text, length):
        if not term_frame.terminal.child_exited and \
           term_frame.terminal.is_focus() and term_frame.is_synced:
            for t in self.terminals:
                if t is not term_frame and \
                   not t.terminal.child_exited and t.is_synced:
                    t.terminal.feed_child(text, length)

    def do_term_key_press(self, term_frame, event):
        keyname = Gdk.keyval_name(event.keyval)

        # FIXME: wrap 80 chars
        if event.state == Gdk.ModifierType.CONTROL_MASK|Gdk.ModifierType.MOD1_MASK|Gdk.ModifierType.SHIFT_MASK:
            # Ctrl + Alt + Shift + ...
            if keyname == 'S':
                self.sync_all(not term_frame.is_synced)
                return True

    def sync_all(self, is_synced):
        for t in self.terminals:
            t.set_sync(is_synced)


# for develop/debug
def dump_props(widgets):
    print '------------------'
    print widgets
    for p in dir(widgets.props):
        if p.startswith('_'):
            continue
        try:
            print p, getattr(widgets.props, p)
        except:
            pass
    print '------------------'


if __name__ == '__main__':
    from optparse import OptionParser
    op = OptionParser(usage='USAGE: %prog [options] [command, command, ...]')

    op.add_option('-c', '--config', dest='config_file',
                  default=os.path.expanduser('~/.silkrc'),
                  help='(default: ~/.silkrc)')

    opt, args = op.parse_args()

    user_config = {}
    if os.path.exists(opt.config_file):
        execfile(opt.config_file, user_config)

    w = MainWindow(config=user_config)
    w.show_all()
    w.open(*args)

    Gtk.main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.