silk / silk.py

#!/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 os

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)

    def run_command(self, command=None):
        if command is None:
            # TODO: use USER's login shell
            command = '/bin/bash'

        # 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._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._label = Gtk.Label(command)
        self._hbox.pack_start(self._label, True, True, 0)

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

        # FIXME: why isn't this work?
        #self._label.connect('button-release-event',
        #                    lambda *_: self._sync_check.toggled())

        # TODO: "press Enter to restert command"
        # TODO: provide keyboard shortcut to toggle sync check button

    @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()


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),
    )

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

        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:
            # TODO: use login shell
            command = '/bin/bash'

        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.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 term_frame.terminal.is_focus() and term_frame.is_synced:
            for t in self.terminals:
                if t is not term_frame and t.is_synced:
                    t.terminal.feed_child(text, length)


# 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()
    w.config.update(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.