├── .dockerignore ├── .gitignore ├── .gitmodules ├── .isort.cfg ├── CHANGELOG.md ├── Dockerfile ├── Gruntfile.coffee ├── LICENSE ├── Makefile ├── Makefile.config ├── README.md ├── bower.json ├── butterfly.png ├── butterfly.server.py ├── butterfly.service ├── butterfly.socket ├── butterfly ├── __about__.py ├── __init__.py ├── bin │ ├── cat.py │ ├── colors.py │ ├── help.py │ ├── html.py │ ├── open.py │ └── session.py ├── butterfly.conf.default ├── escapes.py ├── pam.py ├── routes.py ├── sass │ ├── _16_colors.sass │ ├── _256_colors.sass │ ├── _all_fx.sass │ ├── _colors.sass │ ├── _cursor.sass │ ├── _font.sass │ ├── _layout.sass │ ├── _light_fx.sass │ ├── _styles.sass │ ├── _term_styles.sass │ ├── _text_fx.sass │ ├── _variables.sass │ └── main.sass ├── static │ ├── config.rb │ ├── ext.js │ ├── ext.min.js │ ├── fonts │ │ ├── SIL Open Font License.txt │ │ ├── SourceCodePro-Black.otf │ │ ├── SourceCodePro-Bold.otf │ │ ├── SourceCodePro-ExtraLight.otf │ │ ├── SourceCodePro-Light.otf │ │ ├── SourceCodePro-Medium.otf │ │ ├── SourceCodePro-Regular.otf │ │ └── SourceCodePro-Semibold.otf │ ├── html-sanitizer.js │ ├── images │ │ └── favicon.png │ ├── main.css │ ├── main.js │ └── main.min.js ├── templates │ ├── index.html │ └── motd ├── terminal.py └── utils.py ├── coffees ├── ext │ ├── alarm.coffee │ ├── clipboard.coffee │ ├── close_confirm.coffee │ ├── expand_extended.coffee │ ├── linkify.coffee │ ├── mobile.coffee │ ├── new_term.coffee │ ├── pack.coffee │ ├── popup.coffee │ ├── selection.coffee │ ├── sessions.coffee │ └── theme.coffee ├── main.coffee └── term.coffee ├── docker └── run.sh ├── package.json ├── requirements.txt ├── scripts ├── b └── butterfly ├── setup.cfg ├── setup.py └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .dockerignore 4 | Dockerfile 5 | README.md 6 | butterfly.png 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.key 3 | *.p12 4 | *.pyc 5 | node_modules/ 6 | *.src.coffee 7 | *.map 8 | sass/scss 9 | *.egg-info/ 10 | build/ 11 | .cache/ 12 | .env* 13 | .pytest_cache 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "butterfly/themes"] 2 | path = butterfly/themes 3 | url = https://github.com/paradoxxxzero/butterfly-themes 4 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output=4 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [3.2.5](https://github.com/paradoxxxzero/butterfly/compare/3.2.4...3.2.5) 2 | ===== 3 | 4 | * Fix #155 again (PR #179) 5 | 6 | 7 | [3.2.4](https://github.com/paradoxxxzero/butterfly/compare/3.2.3...3.2.4) 8 | ===== 9 | 10 | * Fix up --uri-root-path so behaves as one would expect for this. Fix #155 (PR #173 thanks @GrahamDumpleton) 11 | * Fix websocket keepalive. Fix #167 (PR #172 thanks @fzumstein) 12 | 13 | [3.2.3](https://github.com/paradoxxxzero/butterfly/compare/3.2.2...3.2.3) 14 | ===== 15 | 16 | * Complete support for IME & CJK rendering (#168 thanks @PeterCxy) 17 | 18 | 3.2.2 19 | ===== 20 | 21 | * Fix unescaping entities when linkifying 22 | 23 | 3.2.1 24 | ===== 25 | 26 | * Issue correct X.509 v3 certificates (you will need to re-generate your certs) 27 | 28 | 3.1.5 29 | ===== 30 | 31 | * Fix new option in older tornado version. (#146 thanks @warpkwd) 32 | 33 | 3.1.4 34 | ===== 35 | 36 | * Add --i-hereby-declare-i-dont-want-any-security-whatsoever option (#143) 37 | 38 | 3.1.3 39 | ===== 40 | 41 | * Fix lsof parsing crash on python 2 42 | 43 | 3.1.0 44 | ===== 45 | 46 | * Start a changelog 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y -q --no-install-recommends \ 5 | build-essential \ 6 | libffi-dev \ 7 | libssl-dev \ 8 | python-dev \ 9 | python-setuptools \ 10 | ca-certificates \ 11 | && easy_install pip \ 12 | && pip install --upgrade setuptools \ 13 | && apt-get clean \ 14 | && rm -r /var/lib/apt/lists/* 15 | 16 | WORKDIR /opt 17 | ADD . /opt/app 18 | WORKDIR /opt/app 19 | 20 | RUN python setup.py build \ 21 | && python setup.py install 22 | 23 | ADD docker/run.sh /opt/run.sh 24 | 25 | EXPOSE 57575 26 | 27 | CMD ["butterfly.server.py", "--unsecure", "--host=0.0.0.0"] 28 | ENTRYPOINT ["docker/run.sh"] 29 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | grunt.initConfig 4 | pkg: grunt.file.readJSON('package.json') 5 | 6 | uglify: 7 | options: 8 | banner: '/*! <%= pkg.name %> 9 | <%= grunt.template.today("yyyy-mm-dd") %> */\n' 10 | sourceMap: true 11 | 12 | butterfly: 13 | files: 14 | 'butterfly/static/main.min.js': 'butterfly/static/main.js' 15 | 'butterfly/static/ext.min.js': 'butterfly/static/ext.js' 16 | 17 | sass: 18 | options: 19 | includePaths: ['butterfly/sass/'] 20 | 21 | butterfly: 22 | expand: true 23 | cwd: 'butterfly/sass/' 24 | src: '*.sass' 25 | dest: 'butterfly/static/' 26 | ext: '.css' 27 | 28 | coffee: 29 | options: 30 | sourceMap: true 31 | 32 | butterfly: 33 | files: 34 | 'butterfly/static/main.js': 'coffees/*.coffee' 35 | 'butterfly/static/ext.js': 'coffees/ext/*.coffee' 36 | 37 | coffeelint: 38 | butterfly: 39 | 'coffees/**/*.coffee' 40 | 41 | watch: 42 | options: 43 | livereload: true 44 | coffee: 45 | files: [ 46 | 'coffees/ext/*.coffee' 47 | 'coffees/*.coffee' 48 | 'Gruntfile.coffee' 49 | ] 50 | tasks: ['coffeelint', 'coffee'] 51 | 52 | sass: 53 | files: [ 54 | 'butterfly/sass/*.sass' 55 | ] 56 | tasks: ['sass'] 57 | 58 | grunt.loadNpmTasks 'grunt-contrib-coffee' 59 | grunt.loadNpmTasks 'grunt-contrib-watch' 60 | grunt.loadNpmTasks 'grunt-contrib-uglify' 61 | grunt.loadNpmTasks 'grunt-contrib-cssmin' 62 | grunt.loadNpmTasks 'grunt-coffeelint' 63 | grunt.loadNpmTasks 'grunt-sass' 64 | grunt.registerTask 'dev', [ 65 | 'coffeelint', 'coffee', 'sass', 'watch'] 66 | grunt.registerTask 'css', ['sass'] 67 | grunt.registerTask 'default', [ 68 | 'coffeelint', 'coffee', 'sass', 'uglify'] 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | butterfly Copyright(C) 2015-2017 Florian Mounier, Kozea 2 | This program is free software: you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation, either version 3 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | You should have received a copy of the GNU General Public License 13 | along with this program. If not, see . 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.config 2 | -include Makefile.custom.config 3 | 4 | all: install lint check-outdated run-debug 5 | 6 | install: 7 | test -d $(VENV) || virtualenv $(VENV) -p $(PYTHON_VERSION) 8 | $(PIP) install --upgrade --no-cache pip setuptools -e .[lint,themes] devcore 9 | $(NPM) install 10 | 11 | clean: 12 | rm -fr $(NODE_MODULES) 13 | rm -fr $(VENV) 14 | rm -fr *.egg-info 15 | 16 | lint: 17 | $(PYTEST) --flake8 -m flake8 $(PROJECT_NAME) 18 | $(PYTEST) --isort -m isort $(PROJECT_NAME) 19 | 20 | check-outdated: 21 | $(PIP) list --outdated --format=columns 22 | 23 | ARGS ?= --port=1212 --unsecure --debug 24 | run-debug: 25 | $(PYTHON) ./butterfly.server.py $(ARGS) 26 | 27 | build-coffee: 28 | $(NODE_MODULES)/.bin/grunt 29 | 30 | release: build-coffee 31 | git pull 32 | $(eval VERSION := $(shell PROJECT_NAME=$(PROJECT_NAME) $(VENV)/bin/devcore bump $(LEVEL))) 33 | git commit -am "Bump $(VERSION)" 34 | git tag $(VERSION) 35 | $(PYTHON) setup.py sdist bdist_wheel upload 36 | git push 37 | git push --tags 38 | -------------------------------------------------------------------------------- /Makefile.config: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = butterfly 2 | 3 | # Python env 4 | PYTHON_VERSION ?= python 5 | VENV = $(PWD)/.env$(if $(filter $(PYTHON_VERSION),python),,-$(PYTHON_VERSION)) 6 | PIP = $(VENV)/bin/pip 7 | PYTHON = $(VENV)/bin/python 8 | PYTEST = $(VENV)/bin/py.test 9 | NODE_MODULES = $(PWD)/node_modules 10 | NPM = yarn 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ƸӜƷ butterfly 3.0 2 | 3 | ![](http://paradoxxxzero.github.io/assets/butterfly_2.0_1.gif) 4 | 5 | 6 | ## Description 7 | 8 | Butterfly is a xterm compatible terminal that runs in your browser. 9 | 10 | 11 | ## Features 12 | 13 | * xterm compatible (support a lot of unused features!) 14 | * Native browser scroll and search 15 | * Theming in css / sass [(20 preset themes)](https://github.com/paradoxxxzero/butterfly-themes) endless possibilities! 16 | * HTML in your terminal! cat images and use <table> 17 | * Multiple sessions support (à la screen -x) to simultaneously access a terminal from several places on the planet! 18 | * Secure authentication with X509 certificates! 19 | * 16,777,216 colors support! 20 | * Keyboard text selection! 21 | * Desktop notifications on terminal output! 22 | * Geolocation from browser! 23 | * May work on firefox too! 24 | 25 | ## Try it 26 | 27 | ``` bash 28 | $ pip install butterfly 29 | $ pip install butterfly[themes] # If you want to use themes 30 | $ pip install butterfly[systemd] # If you want to use systemd 31 | $ butterfly 32 | ``` 33 | 34 | A new tab should appear in your browser. Then type 35 | 36 | ``` bash 37 | $ butterfly help 38 | ``` 39 | 40 | To get an overview of butterfly features. 41 | 42 | 43 | ## Run it as a server 44 | 45 | ``` bash 46 | $ butterfly.server.py --host=myhost --port=57575 47 | ``` 48 | 49 | Or with login prompt 50 | 51 | ```bash 52 | $ butterfly.server.py --host=myhost --port=57575 --login 53 | ``` 54 | 55 | Or with PAM authentication (ROOT required) 56 | 57 | ```bash 58 | # butterfly.server.py --host=myhost --port=57575 --login --pam_profile=sshd 59 | ``` 60 | 61 | You can change `sshd` to your preferred PAM profile. 62 | 63 | The first time it will ask you to generate the certificates (see: [here](http://paradoxxxzero.github.io/2014/03/21/butterfly-with-ssl-auth.html)) 64 | 65 | 66 | ## Run it with systemd (linux) 67 | 68 | Systemd provides a way to automatically activate daemons when needed (socket activation): 69 | 70 | ``` bash 71 | $ cd /etc/systemd/system 72 | $ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.service 73 | $ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.socket 74 | $ systemctl enable butterfly.socket 75 | $ systemctl start butterfly.socket 76 | ``` 77 | 78 | Don't forget to update the /etc/butterfly/butterfly.conf file with your server options (host, port, shell, ...) and to install butterfly with the [systemd] flag. 79 | 80 | 81 | ## Contribute 82 | 83 | and make the world better (or just butterfly). 84 | 85 | Don't hesitate to fork the repository and start hacking on it, I am very open to pull requests. 86 | 87 | If you don't know what to do go to the github issues and pick one you like. 88 | 89 | If you want to motivate me to continue working on this project you can tip me, see: http://paradoxxxzero.github.io/about/ 90 | 91 | Client side development use [grunt](http://gruntjs.com/) and [bower](http://bower.io/). 92 | 93 | ## Credits 94 | 95 | The js part is based on [term.js](https://github.com/chjj/term.js/) which is based on [jslinux](http://bellard.org/jslinux/). 96 | ## Author 97 | 98 | [Florian Mounier](http://paradoxxxzero.github.io/) 99 | 100 | ## License 101 | 102 | ``` 103 | butterfly Copyright (C) 2015-2017 Florian Mounier 104 | 105 | This program is free software: you can redistribute it and/or modify 106 | it under the terms of the GNU General Public License as published by 107 | the Free Software Foundation, either version 3 of the License, or 108 | (at your option) any later version. 109 | 110 | This program is distributed in the hope that it will be useful, 111 | but WITHOUT ANY WARRANTY; without even the implied warranty of 112 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 113 | GNU General Public License for more details. 114 | 115 | You should have received a copy of the GNU General Public License 116 | along with this program. If not, see . 117 | ``` 118 | 119 | ## Docker 120 | There is a docker repository created for this project that is set to automatically rebuild when there is a push 121 | into this repository: https://registry.hub.docker.com/u/garland/butterfly/ 122 | 123 | ### Example usage 124 | 125 | Starting with login and password 126 | 127 | ``` bash 128 | docker run --env PASSWORD=password -d garland/butterfly --login 129 | ``` 130 | 131 | Starting with no password 132 | 133 | ``` bash 134 | docker run -d -p 57575:57575 garland/butterfly 135 | ``` 136 | 137 | Starting with a different port 138 | 139 | ``` bash 140 | docker run -d -p 12345:12345 garland/butterfly --port=12345 141 | ``` 142 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "butterfly", 3 | "version": "1.0.0", 4 | "authors": [ 5 | "Florian Mounier " 6 | ], 7 | "description": "A sleek web based terminal emulator", 8 | "license": "None", 9 | "private": true, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "google-caja": "*" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /butterfly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paradoxxxzero/butterfly/da79ffe04bfeb210143df0d7374067d4b0f5f6c6/butterfly.png -------------------------------------------------------------------------------- /butterfly.server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # *-* coding: utf-8 *-* 3 | 4 | # This file is part of butterfly 5 | # 6 | # butterfly Copyright(C) 2015-2017 Florian Mounier 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import tornado.options 21 | import tornado.ioloop 22 | import tornado.httpserver 23 | try: 24 | from tornado_systemd import SystemdHTTPServer as HTTPServer 25 | except ImportError: 26 | from tornado.httpserver import HTTPServer 27 | 28 | import logging 29 | import webbrowser 30 | import uuid 31 | import ssl 32 | import getpass 33 | import os 34 | import shutil 35 | import stat 36 | import socket 37 | import sys 38 | 39 | tornado.options.define("debug", default=False, help="Debug mode") 40 | tornado.options.define("more", default=False, 41 | help="Debug mode with more verbosity") 42 | tornado.options.define("unminified", default=False, 43 | help="Use the unminified js (for development only)") 44 | 45 | tornado.options.define("host", default='localhost', help="Server host") 46 | tornado.options.define("port", default=57575, type=int, help="Server port") 47 | tornado.options.define("keepalive_interval", default=30, type=int, 48 | help="Interval between ping packets sent from server " 49 | "to client (in seconds)") 50 | tornado.options.define("one_shot", default=False, 51 | help="Run a one-shot instance. Quit at term close") 52 | tornado.options.define("shell", help="Shell to execute at login") 53 | tornado.options.define("motd", default='motd', help="Path to the motd file.") 54 | tornado.options.define("cmd", 55 | help="Command to run instead of shell, f.i.: 'ls -l'") 56 | tornado.options.define("unsecure", default=False, 57 | help="Don't use ssl not recommended") 58 | tornado.options.define("i_hereby_declare_i_dont_want_any_security_whatsoever", 59 | default=False, 60 | help="Remove all security and warnings. There are some " 61 | "use cases for that. Use this if you really know what " 62 | "you are doing.") 63 | tornado.options.define("login", default=False, 64 | help="Use login screen at start") 65 | tornado.options.define("pam_profile", default="", type=str, 66 | help="When --login=True provided and running as ROOT, " 67 | "use PAM with the specified PAM profile for " 68 | "authentication and then execute the user's default " 69 | "shell. Will override --shell.") 70 | tornado.options.define("force_unicode_width", 71 | default=False, 72 | help="Force all unicode characters to the same width." 73 | "Useful for avoiding layout mess.") 74 | tornado.options.define("ssl_version", default=None, 75 | help="SSL protocol version") 76 | tornado.options.define("generate_certs", default=False, 77 | help="Generate butterfly certificates") 78 | tornado.options.define("generate_current_user_pkcs", default=False, 79 | help="Generate current user pfx for client " 80 | "authentication") 81 | tornado.options.define("generate_user_pkcs", default='', 82 | help="Generate user pfx for client authentication " 83 | "(Must be root to create for another user)") 84 | tornado.options.define("uri_root_path", default='', 85 | help="Sets the servier root path: " 86 | "example.com//static/") 87 | 88 | 89 | if os.getuid() == 0: 90 | ev = os.getenv('XDG_CONFIG_DIRS', '/etc') 91 | else: 92 | ev = os.getenv( 93 | 'XDG_CONFIG_HOME', os.path.join( 94 | os.getenv('HOME', os.path.expanduser('~')), 95 | '.config')) 96 | 97 | butterfly_dir = os.path.join(ev, 'butterfly') 98 | conf_file = os.path.join(butterfly_dir, 'butterfly.conf') 99 | ssl_dir = os.path.join(butterfly_dir, 'ssl') 100 | 101 | tornado.options.define("conf", default=conf_file, 102 | help="Butterfly configuration file. " 103 | "Contains the same options as command line.") 104 | 105 | tornado.options.define("ssl_dir", default=ssl_dir, 106 | help="Force SSL directory location") 107 | 108 | # Do it once to get the conf path 109 | tornado.options.parse_command_line() 110 | 111 | if os.path.exists(tornado.options.options.conf): 112 | tornado.options.parse_config_file(tornado.options.options.conf) 113 | 114 | # Do it again to overwrite conf with args 115 | tornado.options.parse_command_line() 116 | 117 | # For next time, create them a conf file from template. 118 | # Need to do this after parsing options so we do not trigger 119 | # code import for butterfly module, in case that code is 120 | # dependent on the set of parsed options. 121 | if not os.path.exists(conf_file): 122 | try: 123 | import butterfly 124 | shutil.copy( 125 | os.path.join( 126 | os.path.abspath(os.path.dirname(butterfly.__file__)), 127 | 'butterfly.conf.default'), conf_file) 128 | print('butterfly.conf installed in %s' % conf_file) 129 | except: 130 | pass 131 | 132 | options = tornado.options.options 133 | 134 | for logger in ('tornado.access', 'tornado.application', 135 | 'tornado.general', 'butterfly'): 136 | level = logging.WARNING 137 | if options.debug: 138 | level = logging.INFO 139 | if options.more: 140 | level = logging.DEBUG 141 | logging.getLogger(logger).setLevel(level) 142 | 143 | log = logging.getLogger('butterfly') 144 | 145 | host = options.host 146 | port = options.port 147 | 148 | if options.i_hereby_declare_i_dont_want_any_security_whatsoever: 149 | options.unsecure = True 150 | 151 | 152 | if not os.path.exists(options.ssl_dir): 153 | os.makedirs(options.ssl_dir) 154 | 155 | 156 | def to_abs(file): 157 | return os.path.join(options.ssl_dir, file) 158 | 159 | 160 | ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [ 161 | 'butterfly_ca.crt', 'butterfly_ca.key', 162 | 'butterfly_%s.crt', 'butterfly_%s.key', 163 | '%s.p12']) 164 | 165 | 166 | def fill_fields(subject): 167 | subject.C = 'WW' 168 | subject.O = 'Butterfly' 169 | subject.OU = 'Butterfly Terminal' 170 | subject.ST = 'World Wide' 171 | subject.L = 'Terminal' 172 | 173 | 174 | def write(file, content): 175 | with open(file, 'wb') as fd: 176 | fd.write(content) 177 | print('Writing %s' % file) 178 | 179 | 180 | def read(file): 181 | print('Reading %s' % file) 182 | with open(file, 'rb') as fd: 183 | return fd.read() 184 | 185 | def b(s): 186 | return s.encode('utf-8') 187 | 188 | 189 | if options.generate_certs: 190 | from OpenSSL import crypto 191 | print('Generating certificates for %s (change it with --host)\n' % host) 192 | 193 | if not os.path.exists(ca) and not os.path.exists(ca_key): 194 | print('Root certificate not found, generating it') 195 | ca_pk = crypto.PKey() 196 | ca_pk.generate_key(crypto.TYPE_RSA, 2048) 197 | ca_cert = crypto.X509() 198 | ca_cert.set_version(2) 199 | ca_cert.get_subject().CN = 'Butterfly CA on %s' % socket.gethostname() 200 | fill_fields(ca_cert.get_subject()) 201 | ca_cert.set_serial_number(uuid.uuid4().int) 202 | ca_cert.gmtime_adj_notBefore(0) # From now 203 | ca_cert.gmtime_adj_notAfter(315360000) # to 10y 204 | ca_cert.set_issuer(ca_cert.get_subject()) # Self signed 205 | ca_cert.set_pubkey(ca_pk) 206 | ca_cert.add_extensions([ 207 | crypto.X509Extension( 208 | b('basicConstraints'), True, b('CA:TRUE, pathlen:0')), 209 | crypto.X509Extension( 210 | b('keyUsage'), True, b('keyCertSign, cRLSign')), 211 | crypto.X509Extension( 212 | b('subjectKeyIdentifier'), False, b('hash'), subject=ca_cert), 213 | ]) 214 | ca_cert.add_extensions([ 215 | crypto.X509Extension( 216 | b('authorityKeyIdentifier'), False, 217 | b('issuer:always, keyid:always'), 218 | issuer=ca_cert, subject=ca_cert 219 | ) 220 | ]) 221 | ca_cert.sign(ca_pk, 'sha512') 222 | 223 | write(ca, crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert)) 224 | write(ca_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk)) 225 | os.chmod(ca_key, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms 226 | else: 227 | print('Root certificate found, using it') 228 | ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca)) 229 | ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key)) 230 | 231 | server_pk = crypto.PKey() 232 | server_pk.generate_key(crypto.TYPE_RSA, 2048) 233 | server_cert = crypto.X509() 234 | server_cert.set_version(2) 235 | server_cert.get_subject().CN = host 236 | server_cert.add_extensions([ 237 | crypto.X509Extension( 238 | b('basicConstraints'), False, b('CA:FALSE')), 239 | crypto.X509Extension( 240 | b('subjectKeyIdentifier'), False, b('hash'), subject=server_cert), 241 | crypto.X509Extension( 242 | b('subjectAltName'), False, b('DNS:%s' % host)), 243 | ]) 244 | server_cert.add_extensions([ 245 | crypto.X509Extension( 246 | b('authorityKeyIdentifier'), False, 247 | b('issuer:always, keyid:always'), 248 | issuer=ca_cert, subject=ca_cert 249 | ) 250 | ]) 251 | fill_fields(server_cert.get_subject()) 252 | server_cert.set_serial_number(uuid.uuid4().int) 253 | server_cert.gmtime_adj_notBefore(0) # From now 254 | server_cert.gmtime_adj_notAfter(315360000) # to 10y 255 | server_cert.set_issuer(ca_cert.get_subject()) # Signed by ca 256 | server_cert.set_pubkey(server_pk) 257 | server_cert.sign(ca_pk, 'sha512') 258 | 259 | write(cert % host, crypto.dump_certificate( 260 | crypto.FILETYPE_PEM, server_cert)) 261 | write(cert_key % host, crypto.dump_privatekey( 262 | crypto.FILETYPE_PEM, server_pk)) 263 | os.chmod(cert_key % host, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms 264 | 265 | print('\nNow you can run --generate-user-pkcs=user ' 266 | 'to generate user certificate.') 267 | sys.exit(0) 268 | 269 | 270 | if (options.generate_current_user_pkcs or 271 | options.generate_user_pkcs): 272 | from butterfly import utils 273 | try: 274 | current_user = utils.User() 275 | except Exception: 276 | current_user = None 277 | 278 | from OpenSSL import crypto 279 | if not all(map(os.path.exists, [ca, ca_key])): 280 | print('Please generate certificates using --generate-certs before') 281 | sys.exit(1) 282 | 283 | if options.generate_current_user_pkcs: 284 | user = current_user.name 285 | else: 286 | user = options.generate_user_pkcs 287 | 288 | if user != current_user.name and current_user.uid != 0: 289 | print('Cannot create certificate for another user with ' 290 | 'current privileges.') 291 | sys.exit(1) 292 | 293 | ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca)) 294 | ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key)) 295 | 296 | client_pk = crypto.PKey() 297 | client_pk.generate_key(crypto.TYPE_RSA, 2048) 298 | 299 | client_cert = crypto.X509() 300 | client_cert.set_version(2) 301 | client_cert.get_subject().CN = user 302 | fill_fields(client_cert.get_subject()) 303 | client_cert.set_serial_number(uuid.uuid4().int) 304 | client_cert.gmtime_adj_notBefore(0) # From now 305 | client_cert.gmtime_adj_notAfter(315360000) # to 10y 306 | client_cert.set_issuer(ca_cert.get_subject()) # Signed by ca 307 | client_cert.set_pubkey(client_pk) 308 | client_cert.sign(client_pk, 'sha512') 309 | client_cert.sign(ca_pk, 'sha512') 310 | 311 | pfx = crypto.PKCS12() 312 | pfx.set_certificate(client_cert) 313 | pfx.set_privatekey(client_pk) 314 | pfx.set_ca_certificates([ca_cert]) 315 | pfx.set_friendlyname(('%s cert for butterfly' % user).encode('utf-8')) 316 | 317 | while True: 318 | password = getpass.getpass('\nPKCS12 Password (can be blank): ') 319 | password2 = getpass.getpass('Verify Password (can be blank): ') 320 | if password == password2: 321 | break 322 | print('Passwords do not match.') 323 | 324 | print('') 325 | write(pkcs12 % user, pfx.export(password.encode('utf-8'))) 326 | os.chmod(pkcs12 % user, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms 327 | sys.exit(0) 328 | 329 | 330 | if options.unsecure: 331 | ssl_opts = None 332 | else: 333 | if not all(map(os.path.exists, [cert % host, cert_key % host, ca])): 334 | print("Unable to find butterfly certificate for host %s" % host) 335 | print(cert % host) 336 | print(cert_key % host) 337 | print(ca) 338 | print("Can't run butterfly without certificate.\n") 339 | print("Either generate them using --generate-certs --host=host " 340 | "or run as --unsecure (NOT RECOMMENDED)\n") 341 | print("For more information go to http://paradoxxxzero.github.io/" 342 | "2014/03/21/butterfly-with-ssl-auth.html\n") 343 | sys.exit(1) 344 | 345 | ssl_opts = { 346 | 'certfile': cert % host, 347 | 'keyfile': cert_key % host, 348 | 'ca_certs': ca, 349 | 'cert_reqs': ssl.CERT_REQUIRED 350 | } 351 | if options.ssl_version is not None: 352 | if not hasattr( 353 | ssl, 'PROTOCOL_%s' % options.ssl_version): 354 | print( 355 | "Unknown SSL protocol %s" % 356 | options.ssl_version) 357 | sys.exit(1) 358 | ssl_opts['ssl_version'] = getattr( 359 | ssl, 'PROTOCOL_%s' % options.ssl_version) 360 | 361 | from butterfly import application 362 | application.butterfly_dir = butterfly_dir 363 | log.info('Starting server') 364 | http_server = HTTPServer(application, ssl_options=ssl_opts) 365 | http_server.listen(port, address=host) 366 | 367 | if getattr(http_server, 'systemd', False): 368 | os.environ.pop('LISTEN_PID') 369 | os.environ.pop('LISTEN_FDS') 370 | 371 | log.info('Starting loop') 372 | 373 | ioloop = tornado.ioloop.IOLoop.instance() 374 | 375 | if port == 0: 376 | port = list(http_server._sockets.values())[0].getsockname()[1] 377 | 378 | url = "http%s://%s:%d/%s" % ( 379 | "s" if not options.unsecure else "", host, port, 380 | (options.uri_root_path.strip('/') + '/') if options.uri_root_path else '' 381 | ) 382 | 383 | if not options.one_shot or not webbrowser.open(url): 384 | log.warn('Butterfly is ready, open your browser to: %s' % url) 385 | 386 | ioloop.start() 387 | -------------------------------------------------------------------------------- /butterfly.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Butterfly Terminal Server 3 | 4 | [Service] 5 | ExecStart=/usr/bin/butterfly.server.py 6 | -------------------------------------------------------------------------------- /butterfly.socket: -------------------------------------------------------------------------------- 1 | [Socket] 2 | ListenStream=57575 3 | 4 | [Install] 5 | WantedBy=sockets.target 6 | -------------------------------------------------------------------------------- /butterfly/__about__.py: -------------------------------------------------------------------------------- 1 | __title__ = "butterfly" 2 | __version__ = "3.2.5" 3 | 4 | __summary__ = "A sleek web based terminal emulator" 5 | __uri__ = "https://github.com/paradoxxxzero/butterfly" 6 | __author__ = "Florian Mounier" 7 | __email__ = "paradoxxx.zero@gmail.com" 8 | 9 | __license__ = "GPLv3" 10 | __copyright__ = "Copyright 2017 %s" % __author__ 11 | 12 | __all__ = [ 13 | '__title__', '__version__', '__summary__', '__uri__', '__author__', 14 | '__email__', '__license__', '__copyright__' 15 | ] 16 | -------------------------------------------------------------------------------- /butterfly/__init__.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | # This file is part of butterfly 3 | # 4 | # butterfly Copyright(C) 2015-2017 Florian Mounier 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | from .__about__ import * # noqa: F401,F403 18 | 19 | import os 20 | import tornado.web 21 | import tornado.options 22 | import tornado.web 23 | from logging import getLogger 24 | 25 | 26 | log = getLogger('butterfly') 27 | 28 | 29 | class url(object): 30 | def __init__(self, url): 31 | self.url = url 32 | 33 | def __call__(self, cls): 34 | if tornado.options.options.uri_root_path: 35 | url = '/' + tornado.options.options.uri_root_path.strip('/') + self.url 36 | else: 37 | url = self.url 38 | application.add_handlers( 39 | r'.*$', 40 | (tornado.web.url(url, cls, name=cls.__name__),) 41 | ) 42 | 43 | return cls 44 | 45 | 46 | class Route(tornado.web.RequestHandler): 47 | @property 48 | def log(self): 49 | return log 50 | 51 | @property 52 | def builtin_themes_dir(self): 53 | return os.path.join( 54 | os.path.dirname(__file__), 'themes') 55 | 56 | @property 57 | def themes_dir(self): 58 | return os.path.join( 59 | self.application.butterfly_dir, 'themes') 60 | 61 | @property 62 | def local_js_dir(self): 63 | return os.path.join( 64 | self.application.butterfly_dir, 'js') 65 | 66 | def get_theme_dir(self, theme): 67 | if theme.startswith('built-in-'): 68 | return os.path.join( 69 | self.builtin_themes_dir, theme[len('built-in-'):]) 70 | return os.path.join( 71 | self.themes_dir, theme) 72 | 73 | 74 | # Imported from executable 75 | if hasattr(tornado.options.options, 'debug'): 76 | application = tornado.web.Application( 77 | static_path=os.path.join(os.path.dirname(__file__), "static"), 78 | template_path=os.path.join(os.path.dirname(__file__), "templates"), 79 | debug=tornado.options.options.debug, 80 | static_url_prefix='%s/static/' % ( 81 | '/%s' % tornado.options.options.uri_root_path.strip('/') 82 | if tornado.options.options.uri_root_path else '') 83 | ) 84 | 85 | import butterfly.routes # noqa: F401 86 | -------------------------------------------------------------------------------- /butterfly/bin/cat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import base64 4 | import mimetypes 5 | import os 6 | import subprocess 7 | import sys 8 | 9 | from butterfly.escapes import image 10 | 11 | parser = argparse.ArgumentParser(description='Butterfly cat wrapper.') 12 | parser.add_argument('-o', action="store_true", 13 | dest='original', help='Force original cat') 14 | parser.add_argument( 15 | 'files', metavar='FILES', nargs='+', 16 | help='Force original cat') 17 | 18 | args, remaining = parser.parse_known_args() 19 | if args.original: 20 | os.execvp('/usr/bin/cat', remaining + args.files) 21 | 22 | 23 | for file in args.files: 24 | if (not os.path.exists(sys.argv[1])): 25 | print('%s: No such file' % file) 26 | else: 27 | mime = mimetypes.guess_type(file)[0] 28 | if mime and 'image' in mime: 29 | with image(mime): 30 | with open(file, 'rb') as f: 31 | print(base64.b64encode(f.read()).decode('ascii')) 32 | else: 33 | subprocess.call(['cat'] + remaining + [file]) 34 | -------------------------------------------------------------------------------- /butterfly/bin/colors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import sys 4 | 5 | parser = argparse.ArgumentParser( 6 | description='Butterfly terminal color tester.') 7 | parser.add_argument( 8 | '--colors', 9 | default='16', 10 | choices=['8', '16', '256', '16M'], 11 | help='Set the color mode to test') 12 | args = parser.parse_args() 13 | 14 | print() 15 | 16 | 17 | if args.colors in ['8', '16']: 18 | print('Background\n') 19 | for l in range(3): 20 | sys.stdout.write(' ') 21 | for i in range(8): 22 | sys.stdout.write('\x1b[%dm \x1b[m ' % (40 + i)) 23 | sys.stdout.write('\n') 24 | sys.stdout.flush() 25 | 26 | if args.colors == '16': 27 | print() 28 | for l in range(3): 29 | sys.stdout.write(' ') 30 | for i in range(8): 31 | sys.stdout.write('\x1b[%dm \x1b[m ' % (100 + i)) 32 | sys.stdout.write('\n') 33 | sys.stdout.flush() 34 | 35 | print('\nForeground\n') 36 | 37 | for l in range(3): 38 | sys.stdout.write(' ') 39 | for i in range(8): 40 | sys.stdout.write('\x1b[%dm ░▒▓██\x1b[m ' % (30 + i)) 41 | sys.stdout.write('\n') 42 | sys.stdout.flush() 43 | 44 | if args.colors == '16': 45 | print() 46 | for l in range(3): 47 | sys.stdout.write(' ') 48 | for i in range(8): 49 | sys.stdout.write('\x1b[1;%dm ░▒▓██\x1b[m ' % (30 + i)) 50 | sys.stdout.write('\n') 51 | sys.stdout.flush() 52 | 53 | if args.colors == '256': 54 | for i in range(16): 55 | sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (i)) 56 | print() 57 | for i in range(16): 58 | sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (i, i)) 59 | print() 60 | 61 | for j in range(6): 62 | for i in range(36): 63 | sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (16 + j * 36 + i)) 64 | print() 65 | for i in range(36): 66 | sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % ( 67 | 16 + j * 36 + i, 16 + j * 36 + i)) 68 | print() 69 | for i in range(24): 70 | sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (232 + i)) 71 | print() 72 | for i in range(24): 73 | sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (232 + i, 232 + i)) 74 | 75 | if args.colors == '16M': 76 | b = 0 77 | g = 0 78 | for r in range(256): 79 | if r == 128: 80 | print() 81 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 82 | print() 83 | 84 | r = 255 85 | b = 0 86 | for g in range(256): 87 | if g == 128: 88 | print() 89 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 90 | print() 91 | 92 | r = 255 93 | g = 255 94 | for b in range(256): 95 | if b == 128: 96 | print() 97 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 98 | print() 99 | 100 | r = 255 101 | b = 255 102 | for g in reversed(range(256)): 103 | if g == 127: 104 | print() 105 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 106 | print() 107 | 108 | g = 0 109 | b = 255 110 | for r in reversed(range(256)): 111 | if r == 127: 112 | print() 113 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 114 | print() 115 | 116 | r = 0 117 | g = 0 118 | for b in reversed(range(256)): 119 | if b == 127: 120 | print() 121 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 122 | print() 123 | 124 | r = 0 125 | b = 0 126 | for g in range(256): 127 | if g == 128: 128 | print() 129 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 130 | print() 131 | 132 | r = 0 133 | g = 255 134 | for b in range(256): 135 | if b == 128: 136 | print() 137 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 138 | print() 139 | 140 | b = 255 141 | g = 255 142 | for r in range(256): 143 | if r == 128: 144 | print() 145 | sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b)) 146 | print() 147 | -------------------------------------------------------------------------------- /butterfly/bin/help.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import base64 3 | import os 4 | import subprocess 5 | 6 | import butterfly 7 | from butterfly.escapes import image 8 | from butterfly.utils import ansi_colors 9 | 10 | print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset) 11 | path = os.getenv('BUTTERFLY_PATH') 12 | if path: 13 | path = os.path.join(path, '../static/images/favicon.png') 14 | 15 | if path and os.path.exists(path): 16 | with image('image/png'): 17 | with open(path, 'rb') as i: 18 | print(base64.b64encode(i.read()).decode('ascii')) 19 | print(""" 20 | Butterfly is a xterm compliant terminal built with python and javascript. 21 | 22 | {title}Terminal functionalities:{reset} 23 | {strong}[ScrollLock] : {reset}Lock the scrolling to the current position. Press again to release. 24 | {strong}[Ctrl] + [c] <> : {reset}Cut the output when [Ctrl] + [c] is not enough. 25 | {strong}[Ctrl] + [Shift] + [Up] : {reset}Trigger visual selection mode. Hitting [Enter] inserts the selection in the prompt. 26 | {strong}[Alt] + [a] : {reset}Set an alarm which sends a notification when a modification is detected. (Ring on regexp match with [Shift]) 27 | {strong}[Alt] + [s] : {reset}Open theme selection prompt. Use [Alt] + [Shift] + [s] to refresh current theme. 28 | {strong}[Alt] + [e] : {reset}List open user sessions. (Only available in secure mode) 29 | {strong}[Alt] + [o] : {reset}Open new terminal (As a popup) 30 | {strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key. 31 | Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]). 32 | 33 | 34 | {title}Butterfly programs:{reset} 35 | {strong}b : {reset}Alias for {strong}butterfly{reset} executable. Takes a comand in parameter or launch a butterfly server for one shot use (if outside butterfly). 36 | {strong}b cat : {reset}A wrapper around cat allowing to display images as instead of binary. 37 | {strong}b open : {reset}Open a new terminal at specified location. 38 | {strong}b session : {reset}Open or rattach a butterfly session. Multiplexing is supported. 39 | {strong}b colors : {reset}Test the terminal colors (16, 256 and 16777216 colors) 40 | {strong}b html : {reset}Output in html standard input. 41 | 42 | For more butterfly programs check out: https://github.com/paradoxxxzero/butterfly-demos 43 | 44 | 45 | {title}Styling butterfly:{reset} 46 | To style butterfly in sass, you need to have the libsass python library installed. 47 | 48 | Theming is done by overriding the default sass files located in {code}{main}{reset} in your theme directory. 49 | This directory can include images and custom fonts. 50 | Please take a look at official themes here: https://github.com/paradoxxxzero/butterfly-themes 51 | and submit your best themes as pull request! 52 | 53 | \x1b[{rcol}G\x1b[3m{dark}butterfly @ 2015 Mounier Florian{reset}\ 54 | """.format( 55 | title=ansi_colors.light_blue, 56 | dark=ansi_colors.light_black, 57 | strong=ansi_colors.white, 58 | code=ansi_colors.light_yellow, 59 | comment=ansi_colors.light_magenta, 60 | reset=ansi_colors.reset, 61 | rcol=int(subprocess.check_output(['stty', 'size']).split()[1]) - 31, 62 | main=os.path.normpath(os.path.join( 63 | os.path.abspath(os.path.dirname(butterfly.__file__)), 'sass')))) 64 | -------------------------------------------------------------------------------- /butterfly/bin/html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import fileinput 4 | import sys 5 | 6 | from butterfly.escapes import html 7 | 8 | parser = argparse.ArgumentParser( 9 | description="Butterfly html converter.\n\n" 10 | "Output in html standard input.\n" 11 | "Example: $ echo \"Bold\" | b html", 12 | formatter_class=argparse.RawTextHelpFormatter) 13 | 14 | parser.parse_known_args() 15 | 16 | 17 | with html(): 18 | for line in fileinput.input(): 19 | sys.stdout.write(line) 20 | -------------------------------------------------------------------------------- /butterfly/bin/open.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import os 4 | import webbrowser 5 | 6 | try: 7 | from urllib.parse import urlparse, parse_qs, urlencode, urlunparse 8 | except ImportError: 9 | from urlparse import urlparse, parse_qs, urlunparse 10 | from urllib import urlencode 11 | 12 | 13 | parser = argparse.ArgumentParser(description='Butterfly tab opener.') 14 | parser.add_argument( 15 | 'location', 16 | nargs='?', 17 | default=os.getcwd(), 18 | help='Directory to open the new tab in. (Defaults to current)') 19 | args = parser.parse_args() 20 | 21 | url_parts = urlparse(os.getenv('LOCATION', '/')) 22 | query = parse_qs(url_parts.query) 23 | query['path'] = os.path.abspath(args.location) 24 | 25 | url = urlunparse(url_parts._replace(path='')._replace(query=urlencode(query))) 26 | if not webbrowser.open(url): 27 | print('Unable to open browser, please go to %s' % url) 28 | -------------------------------------------------------------------------------- /butterfly/bin/session.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import os 4 | import webbrowser 5 | 6 | parser = argparse.ArgumentParser(description='Butterfly session opener.') 7 | parser.add_argument( 8 | 'session', 9 | help='Open or rattach a butterfly session. ' 10 | '(Only in secure mode or in user unsecure mode (no su login))') 11 | args = parser.parse_args() 12 | 13 | url = '%ssession/%s' % (os.getenv('LOCATION', '/'), args.session) 14 | if not webbrowser.open(url): 15 | print('Unable to open browser, please go to %s' % url) 16 | -------------------------------------------------------------------------------- /butterfly/butterfly.conf.default: -------------------------------------------------------------------------------- 1 | # Butterfly autogenerated config file 2 | 3 | # Activate debug mode 4 | # 5 | #debug=False 6 | 7 | # In debug mode produce more verbose output 8 | # 9 | #more=False 10 | 11 | # Use unminified version of js for development 12 | # 13 | #unminified=False 14 | 15 | # Server host 16 | # Use 'localhost' for local only 17 | # Use your ip to share over your network 18 | # Use '0.0.0.0' to listen to every network 19 | # 20 | #host='localhost' 21 | 22 | # Server port 23 | # 24 | #port=57575 25 | 26 | # Shell to launch at start (defaults to user shell) 27 | # 28 | #shell=None # shell='/bin/bash' for instance 29 | 30 | # Motd, path to custom message of the day file 31 | # 32 | #motd='motd' 33 | 34 | # Command to run instead of shell 35 | # 36 | #cmd=None # cmd='ls -l' 37 | 38 | 39 | # Unsecure mode 40 | # This mode use http without ssl and is therefore NOT RECOMMENDED 41 | # Please generate yourself a certificate using the butterfly.server.py command 42 | # 43 | #unsecure=False 44 | 45 | # Force user login in unsecure mode 46 | # 47 | #login=False 48 | 49 | # Force unicode width 50 | # This mode force every character to be the same width 51 | # Which can be useful in some case 52 | # But this breaks unicode display of varying width character 53 | # 54 | #force_unicode_width=False 55 | 56 | # SSL version defaults to auto 57 | # 58 | #ssl_version=None 59 | -------------------------------------------------------------------------------- /butterfly/escapes.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import termios 3 | import tty 4 | from contextlib import contextmanager 5 | 6 | from butterfly.utils import ansi_colors as colors # noqa: F401 7 | 8 | 9 | @contextmanager 10 | def html(): 11 | sys.stdout.write('\x1bP;HTML|') 12 | yield 13 | sys.stdout.write('\x1bP') 14 | sys.stdout.flush() 15 | 16 | 17 | @contextmanager 18 | def image(mime='image'): 19 | sys.stdout.write('\x1bP;IMAGE|%s;' % mime) 20 | yield 21 | sys.stdout.write('\x1bP\n') 22 | sys.stdout.flush() 23 | 24 | 25 | @contextmanager 26 | def prompt(): 27 | sys.stdout.write('\x1bP;PROMPT|') 28 | yield 29 | sys.stdout.write('\x1bP') 30 | sys.stdout.flush() 31 | 32 | 33 | @contextmanager 34 | def text(): 35 | sys.stdout.write('\x1bP;TEXT|') 36 | yield 37 | sys.stdout.write('\x1bP') 38 | sys.stdout.flush() 39 | 40 | 41 | def geolocation(): 42 | sys.stdout.write('\x1b[?99n') 43 | sys.stdout.flush() 44 | 45 | fd = sys.stdin.fileno() 46 | old_settings = termios.tcgetattr(fd) 47 | try: 48 | tty.setraw(sys.stdin.fileno()) 49 | rv = sys.stdin.read(1) 50 | if rv != '\x1b': 51 | raise 52 | rv = sys.stdin.read(1) 53 | if rv != '[': 54 | raise 55 | rv = sys.stdin.read(1) 56 | if rv != '?': 57 | raise 58 | 59 | loc = '' 60 | while rv != 'R': 61 | rv = sys.stdin.read(1) 62 | if rv != 'R': 63 | loc += rv 64 | except Exception: 65 | return 66 | finally: 67 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 68 | if not loc or ';' not in loc: 69 | return 70 | return tuple(map(float, loc.split(';'))) 71 | -------------------------------------------------------------------------------- /butterfly/pam.py: -------------------------------------------------------------------------------- 1 | # (c) 2007 Chris AtLee 2 | # Licensed under the MIT license: 3 | # http://www.opensource.org/licenses/mit-license.php 4 | # 5 | # Original author: Chris AtLee 6 | # 7 | # Modified by David Ford, 2011-12-6 8 | # added py3 support and encoding 9 | # added pam_end 10 | # added pam_setcred to reset credentials after seeing Leon Walker's remarks 11 | # added byref as well 12 | # use readline to prestuff the getuser input 13 | # Modified by Peter Cai, 2017-02-10 14 | # interactive login for Butterfly 15 | 16 | ''' 17 | PAM module for python 18 | Provides an authenticate function that will allow the caller to authenticate 19 | a user against the Pluggable Authentication Modules (PAM) on the system. 20 | Implemented using ctypes, so no compilation is necessary. 21 | ''' 22 | 23 | import os 24 | import sys 25 | from ctypes import ( 26 | CDLL, CFUNCTYPE, POINTER, Structure, byref, c_char_p, c_int, c_size_t, 27 | c_void_p) 28 | from ctypes.util import find_library 29 | 30 | 31 | class PamHandle(Structure): 32 | """wrapper class for pam_handle_t pointer""" 33 | _fields_ = [("handle", c_void_p)] 34 | 35 | def __init__(self): 36 | Structure.__init__(self) 37 | self.handle = 0 38 | 39 | 40 | class PamMessage(Structure): 41 | """wrapper class for pam_message structure""" 42 | _fields_ = [("msg_style", c_int), ("msg", c_char_p)] 43 | 44 | def __repr__(self): 45 | return "" % (self.msg_style, self.msg) 46 | 47 | 48 | class PamResponse(Structure): 49 | """wrapper class for pam_response structure""" 50 | _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)] 51 | 52 | def __repr__(self): 53 | return "" % (self.resp_retcode, self.resp) 54 | 55 | 56 | conv_func = CFUNCTYPE( 57 | c_int, c_int, POINTER(POINTER(PamMessage)), 58 | POINTER(POINTER(PamResponse)), c_void_p) 59 | 60 | 61 | class PamConv(Structure): 62 | """wrapper class for pam_conv structure""" 63 | _fields_ = [("conv", conv_func), ("appdata_ptr", c_void_p)] 64 | 65 | 66 | # Various constants 67 | PAM_PROMPT_ECHO_OFF = 1 68 | PAM_PROMPT_ECHO_ON = 2 69 | PAM_ERROR_MSG = 3 70 | PAM_TEXT_INFO = 4 71 | PAM_REINITIALIZE_CRED = 8 72 | 73 | libc = CDLL(find_library("c")) 74 | libpam = CDLL(find_library("pam")) 75 | libpam_misc = CDLL(find_library("pam_misc")) 76 | 77 | calloc = libc.calloc 78 | calloc.restype = c_void_p 79 | calloc.argtypes = [c_size_t, c_size_t] 80 | 81 | pam_end = libpam.pam_end 82 | pam_end.restype = c_int 83 | pam_end.argtypes = [PamHandle, c_int] 84 | 85 | pam_start = libpam.pam_start 86 | pam_start.restype = c_int 87 | pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)] 88 | 89 | pam_setcred = libpam.pam_setcred 90 | pam_setcred.restype = c_int 91 | pam_setcred.argtypes = [PamHandle, c_int] 92 | 93 | pam_strerror = libpam.pam_strerror 94 | pam_strerror.restype = c_char_p 95 | pam_strerror.argtypes = [PamHandle, c_int] 96 | 97 | pam_authenticate = libpam.pam_authenticate 98 | pam_authenticate.restype = c_int 99 | pam_authenticate.argtypes = [PamHandle, c_int] 100 | 101 | misc_conv = libpam_misc.misc_conv 102 | 103 | 104 | class PAM(): 105 | code = 0 106 | reason = None 107 | 108 | def __init__(self): 109 | pass 110 | 111 | def authenticate( 112 | self, username, 113 | service='login', encoding='utf-8', resetcreds=True): 114 | """PAM authentication through standard input for the given service. 115 | Returns True for success, or False for failure. 116 | self.code (integer) and self.reason (string) are always stored 117 | and may be referenced for the reason why authentication failed. 118 | 0/'Success' will be stored for success. 119 | Python3 expects bytes() for ctypes inputs. This function will make 120 | necessary conversions using the supplied encoding. 121 | Inputs: 122 | username: username to authenticate 123 | service: PAM service to authenticate against, defaults to 'login' 124 | Returns: 125 | success: True 126 | failure: False 127 | """ 128 | 129 | # python3 ctypes prefers bytes 130 | if sys.version_info >= (3,): 131 | if isinstance(username, str): 132 | username = username.encode(encoding) 133 | if isinstance(service, str): 134 | service = service.encode(encoding) 135 | else: 136 | if isinstance(username, unicode): # noqa: F821 137 | username = username.encode(encoding) 138 | if isinstance(service, unicode): # noqa: F821 139 | service = service.encode(encoding) 140 | 141 | if b'\x00' in username or b'\x00' in service: 142 | self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM 143 | self.reason = 'strings may not contain NUL' 144 | return False 145 | 146 | handle = PamHandle() 147 | conv = PamConv(conv_func(misc_conv), 0) 148 | retval = pam_start(service, username, byref(conv), byref(handle)) 149 | 150 | if retval != 0: 151 | # This is not an authentication error, 152 | # something has gone wrong starting up PAM 153 | self.code = retval 154 | self.reason = "pam_start() failed" 155 | return False 156 | 157 | retval = pam_authenticate(handle, 0) 158 | auth_success = retval == 0 159 | 160 | if auth_success and resetcreds: 161 | retval = pam_setcred(handle, PAM_REINITIALIZE_CRED) 162 | 163 | # store information to inform the caller why we failed 164 | self.code = retval 165 | self.reason = pam_strerror(handle, retval) 166 | if sys.version_info >= (3,): 167 | self.reason = self.reason.decode(encoding) 168 | 169 | pam_end(handle, retval) 170 | 171 | return auth_success 172 | 173 | 174 | def login_prompt(username, profile, env): 175 | pam = PAM() 176 | 177 | success = pam.authenticate(username, profile) 178 | print('{} {}'.format(pam.code, pam.reason)) 179 | 180 | if success: 181 | su = '/usr/bin/su' 182 | if not os.path.exists(su): 183 | su = '/bin/su' 184 | os.execvpe(su, [su, '-l', username], env) 185 | return success 186 | 187 | 188 | if __name__ == "__main__": 189 | if login_prompt(sys.argv[1], sys.argv[2], os.environ): 190 | exit(0) 191 | else: 192 | exit(1) 193 | -------------------------------------------------------------------------------- /butterfly/routes.py: -------------------------------------------------------------------------------- 1 | # *-* coding: utf-8 *-* 2 | # This file is part of butterfly 3 | # 4 | # butterfly Copyright(C) 2015-2017 Florian Mounier 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | import json 20 | import os 21 | import struct 22 | import sys 23 | import time 24 | from collections import defaultdict 25 | from mimetypes import guess_type 26 | from uuid import uuid4 27 | 28 | import tornado.escape 29 | import tornado.options 30 | import tornado.process 31 | import tornado.web 32 | import tornado.websocket 33 | 34 | from butterfly import Route, url, utils 35 | from butterfly.terminal import Terminal 36 | 37 | 38 | def u(s): 39 | if sys.version_info[0] == 2: 40 | return s.decode('utf-8') 41 | return s 42 | 43 | 44 | @url(r'/(?:session/(?P[^/]+)/?)?') 45 | class Index(Route): 46 | def get(self, session): 47 | user = self.request.query_arguments.get( 48 | 'user', [b''])[0].decode('utf-8') 49 | if not tornado.options.options.unsecure and user: 50 | raise tornado.web.HTTPError(400) 51 | return self.render( 52 | 'index.html', session=session or str(uuid4())) 53 | 54 | 55 | @url(r'/theme/([^/]+)/style.css') 56 | class Theme(Route): 57 | 58 | def get(self, theme): 59 | self.log.info('Getting style') 60 | try: 61 | import sass 62 | sass.CompileError 63 | except Exception: 64 | self.log.error( 65 | 'You must install libsass to use sass ' 66 | '(pip install libsass)') 67 | return 68 | base_dir = self.get_theme_dir(theme) 69 | 70 | style = None 71 | for ext in ['css', 'scss', 'sass']: 72 | probable_style = os.path.join(base_dir, 'style.%s' % ext) 73 | if os.path.exists(probable_style): 74 | style = probable_style 75 | 76 | if not style: 77 | raise tornado.web.HTTPError(404) 78 | 79 | sass_path = os.path.join( 80 | os.path.dirname(__file__), 'sass') 81 | 82 | css = None 83 | try: 84 | css = sass.compile(filename=style, include_paths=[ 85 | base_dir, sass_path]) 86 | except sass.CompileError: 87 | self.log.error( 88 | 'Unable to compile style (filename: %s, paths: %r) ' % ( 89 | style, [base_dir, sass_path]), exc_info=True) 90 | if not style: 91 | raise tornado.web.HTTPError(500) 92 | 93 | self.log.debug('Style ok') 94 | self.set_header("Content-Type", "text/css") 95 | self.write(css) 96 | self.finish() 97 | 98 | 99 | @url(r'/theme/([^/]+)/(.+)') 100 | class ThemeStatic(Route): 101 | def get(self, theme, name): 102 | if '..' in name: 103 | raise tornado.web.HTTPError(403) 104 | 105 | base_dir = self.get_theme_dir(theme) 106 | 107 | fn = os.path.normpath(os.path.join(base_dir, name)) 108 | if not fn.startswith(base_dir): 109 | raise tornado.web.HTTPError(403) 110 | 111 | if os.path.exists(fn): 112 | type = guess_type(fn)[0] 113 | if type is None: 114 | # Fallback if there's no mimetypes on the system 115 | type = { 116 | 'png': 'image/png', 117 | 'jpg': 'image/jpeg', 118 | 'jpeg': 'image/jpeg', 119 | 'gif': 'image/gif', 120 | 'woff': 'application/font-woff', 121 | 'ttf': 'application/x-font-ttf' 122 | }.get(fn.split('.')[-1], 'text/plain') 123 | 124 | self.set_header("Content-Type", type) 125 | with open(fn, 'rb') as s: 126 | while True: 127 | data = s.read(16384) 128 | if data: 129 | self.write(data) 130 | else: 131 | break 132 | self.finish() 133 | raise tornado.web.HTTPError(404) 134 | 135 | 136 | class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler): 137 | keepalive_timer = None 138 | 139 | def open(self, *args, **kwargs): 140 | self.keepalive_timer = tornado.ioloop.PeriodicCallback( 141 | self.send_ping, tornado.options.options.keepalive_interval * 1000) 142 | self.keepalive_timer.start() 143 | 144 | def send_ping(self): 145 | t = int(time.time()) 146 | frame = struct.pack('[^/]+)') 159 | class TermCtlWebSocket(Route, KeptAliveWebSocketHandler): 160 | sessions = defaultdict(list) 161 | sessions_secure_users = {} 162 | 163 | def open(self, session): 164 | super(TermCtlWebSocket, self).open(session) 165 | self.session = session 166 | self.closed = False 167 | self.log.info('Websocket /ctl opened %r' % self) 168 | 169 | def create_terminal(self): 170 | socket = utils.Socket(self.ws_connection.stream.socket) 171 | user = self.request.query_arguments.get( 172 | 'user', [b''])[0].decode('utf-8') 173 | path = self.request.query_arguments.get( 174 | 'path', [b''])[0].decode('utf-8') 175 | secure_user = None 176 | 177 | if not tornado.options.options.unsecure: 178 | user = utils.parse_cert( 179 | self.ws_connection.stream.socket.getpeercert()) 180 | assert user, 'No user in certificate' 181 | try: 182 | user = utils.User(name=user) 183 | except LookupError: 184 | raise Exception('Invalid user in certificate') 185 | 186 | # Certificate authed user 187 | secure_user = user 188 | 189 | elif socket.local and socket.user == utils.User() and not user: 190 | # Local to local returning browser user 191 | secure_user = socket.user 192 | elif user: 193 | try: 194 | user = utils.User(name=user) 195 | except LookupError: 196 | raise Exception('Invalid user') 197 | 198 | if secure_user: 199 | user = secure_user 200 | if self.session in self.sessions and self.session in ( 201 | self.sessions_secure_users): 202 | if user.name != self.sessions_secure_users[self.session]: 203 | # Restrict to authorized users 204 | raise tornado.web.HTTPError(403) 205 | else: 206 | self.sessions_secure_users[self.session] = user.name 207 | 208 | self.sessions[self.session].append(self) 209 | 210 | terminal = Terminal.sessions.get(self.session) 211 | # Handling terminal session 212 | if terminal: 213 | TermWebSocket.last.write_message(terminal.history) 214 | # And returning, we don't want another terminal 215 | return 216 | 217 | # New session, opening terminal 218 | terminal = Terminal( 219 | user, path, self.session, socket, 220 | self.request.full_url().replace('/ctl/', '/'), self.render_string, 221 | TermWebSocket.broadcast) 222 | 223 | terminal.pty() 224 | self.log.info('Openning session %s for secure user %r' % ( 225 | self.session, user)) 226 | 227 | @classmethod 228 | def broadcast(cls, session, message, emitter=None): 229 | for wsocket in cls.sessions[session]: 230 | try: 231 | if wsocket != emitter: 232 | wsocket.write_message(message) 233 | except Exception: 234 | wsocket.log.exception('Error on broadcast') 235 | wsocket.close() 236 | 237 | def on_message(self, message): 238 | cmd = json.loads(message) 239 | if cmd['cmd'] == 'open': 240 | self.create_terminal() 241 | else: 242 | try: 243 | Terminal.sessions[self.session].ctl(cmd) 244 | except Exception: 245 | # FF strange bug 246 | pass 247 | self.broadcast(self.session, message, self) 248 | 249 | def on_close(self): 250 | super(TermCtlWebSocket, self).on_close() 251 | if self.closed: 252 | return 253 | self.closed = True 254 | self.log.info('Websocket /ctl closed %r' % self) 255 | if self in self.sessions[self.session]: 256 | self.sessions[self.session].remove(self) 257 | 258 | if tornado.options.options.one_shot or ( 259 | getattr(self.application, 'systemd', False) and 260 | not sum([ 261 | len(wsockets) 262 | for session, wsockets in self.sessions.items()])): 263 | sys.exit(0) 264 | 265 | 266 | @url(r'/ws/session/(?P[^/]+)') 267 | class TermWebSocket(Route, KeptAliveWebSocketHandler): 268 | # List of websockets per session 269 | sessions = defaultdict(list) 270 | 271 | # Last is kept for session shared history send 272 | last = None 273 | 274 | # Session history 275 | history = {} 276 | 277 | def open(self, session): 278 | super(TermWebSocket, self).open(session) 279 | self.set_nodelay(True) 280 | self.session = session 281 | self.closed = False 282 | self.sessions[session].append(self) 283 | self.__class__.last = self 284 | self.log.info('Websocket /ws opened %r' % self) 285 | 286 | @classmethod 287 | def close_session(cls, session): 288 | wsockets = (cls.sessions.get(session, []) + 289 | TermCtlWebSocket.sessions.get(session, [])) 290 | for wsocket in wsockets: 291 | wsocket.on_close() 292 | 293 | wsocket.close() 294 | 295 | if session in cls.sessions: 296 | del cls.sessions[session] 297 | if session in TermCtlWebSocket.sessions_secure_users: 298 | del TermCtlWebSocket.sessions_secure_users[session] 299 | if session in TermCtlWebSocket.sessions: 300 | del TermCtlWebSocket.sessions[session] 301 | 302 | @classmethod 303 | def broadcast(cls, session, message, emitter=None): 304 | if message is None: 305 | cls.close_session(session) 306 | return 307 | 308 | wsockets = cls.sessions.get(session) 309 | for wsocket in wsockets: 310 | try: 311 | if wsocket != emitter: 312 | wsocket.write_message(message) 313 | except Exception: 314 | wsocket.log.exception('Error on broadcast') 315 | wsocket.close() 316 | 317 | def on_message(self, message): 318 | Terminal.sessions[self.session].write(message) 319 | 320 | def on_close(self): 321 | super(TermWebSocket, self).on_close() 322 | if self.closed: 323 | return 324 | self.closed = True 325 | self.log.info('Websocket /ws closed %r' % self) 326 | self.sessions[self.session].remove(self) 327 | 328 | 329 | @url(r'/sessions/list.json') 330 | class SessionsList(Route): 331 | """Get the theme list""" 332 | 333 | def get(self): 334 | if tornado.options.options.unsecure: 335 | raise tornado.web.HTTPError(403) 336 | 337 | cert = self.request.get_ssl_certificate() 338 | user = utils.parse_cert(cert) 339 | 340 | if not user: 341 | raise tornado.web.HTTPError(403) 342 | 343 | self.set_header('Content-Type', 'application/json') 344 | self.write(tornado.escape.json_encode({ 345 | 'sessions': sorted( 346 | TermWebSocket.sessions), 347 | 'user': user 348 | })) 349 | 350 | 351 | @url(r'/themes/list.json') 352 | class ThemesList(Route): 353 | """Get the theme list""" 354 | 355 | def get(self): 356 | 357 | if os.path.exists(self.themes_dir): 358 | themes = [ 359 | theme 360 | for theme in os.listdir(self.themes_dir) 361 | if os.path.isdir(os.path.join(self.themes_dir, theme)) and 362 | not theme.startswith('.')] 363 | else: 364 | themes = [] 365 | 366 | if os.path.exists(self.builtin_themes_dir): 367 | builtin_themes = [ 368 | 'built-in-%s' % theme 369 | for theme in os.listdir(self.builtin_themes_dir) 370 | if os.path.isdir(os.path.join( 371 | self.builtin_themes_dir, theme)) and 372 | not theme.startswith('.')] 373 | else: 374 | builtin_themes = [] 375 | 376 | self.set_header('Content-Type', 'application/json') 377 | self.write(tornado.escape.json_encode({ 378 | 'themes': sorted(themes), 379 | 'builtin_themes': sorted(builtin_themes), 380 | 'dir': self.themes_dir 381 | })) 382 | 383 | 384 | @url('/local.js') 385 | class LocalJsStatic(Route): 386 | def get(self): 387 | self.set_header("Content-Type", 'application/javascript') 388 | if os.path.exists(self.local_js_dir): 389 | for fn in os.listdir(self.local_js_dir): 390 | if not fn.endswith('.js'): 391 | continue 392 | with open(os.path.join(self.local_js_dir, fn), 'rb') as s: 393 | while True: 394 | data = s.read(16384) 395 | if data: 396 | self.write(data) 397 | else: 398 | self.write(';') 399 | break 400 | self.finish() 401 | -------------------------------------------------------------------------------- /butterfly/sass/_16_colors.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | 19 | /* Here are the 16 "normal" colors for theming */ 20 | 21 | +termcolor(0, nth($colors, 1)) 22 | +termcolor(1, nth($colors, 2)) 23 | +termcolor(2, nth($colors, 3)) 24 | +termcolor(3, nth($colors, 4)) 25 | +termcolor(4, nth($colors, 5)) 26 | +termcolor(5, nth($colors, 6)) 27 | +termcolor(6, nth($colors, 7)) 28 | +termcolor(7, nth($colors, 8)) 29 | +termcolor(8, nth($colors, 9)) 30 | +termcolor(9, nth($colors, 10)) 31 | +termcolor(10, nth($colors, 11)) 32 | +termcolor(11, nth($colors, 12)) 33 | +termcolor(12, nth($colors, 13)) 34 | +termcolor(13, nth($colors, 14)) 35 | +termcolor(14, nth($colors, 15)) 36 | +termcolor(15, nth($colors, 16)) 37 | -------------------------------------------------------------------------------- /butterfly/sass/_256_colors.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | /* Here are the 240 xterm colors */ 19 | /* See http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg */ 20 | 21 | $st: 00, 95, 135, 175, 215, 255 22 | 23 | @for $i from 0 through 215 24 | $r: nth($st, 1 + floor(($i / 36) % 6)) 25 | $g: nth($st, 1 + floor(($i / 6) % 6)) 26 | $b: nth($st, 1 + $i % 6) 27 | +termcolor($i + 16, rgb($r, $g, $b)) 28 | 29 | @for $i from 0 through 23 30 | $l: 8 + $i * 10 31 | +termcolor($i + 232, rgb($l, $l, $l)) 32 | 33 | +termcolor(256, $default-bg) 34 | +termcolor(257, $default-fg) 35 | -------------------------------------------------------------------------------- /butterfly/sass/_all_fx.sass: -------------------------------------------------------------------------------- 1 | body 2 | &.copied 3 | transform: scale(1.05) 4 | 5 | &.pasted 6 | transform: scale(.95) 7 | -------------------------------------------------------------------------------- /butterfly/sass/_colors.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | =termcolor($i, $color) 19 | .bg-color-#{$i} 20 | background-color: $color 21 | &.reverse-video 22 | @if $color == transparent 23 | color: $reverse-transparent !important 24 | @else 25 | color: $color !important 26 | 27 | .fg-color-#{$i} 28 | color: $color 29 | &.reverse-video 30 | background-color: $color !important 31 | 32 | @if $shadow != 0 33 | text-shadow: 0 0 $shadow rgba($color, $shadow-alpha) 34 | -------------------------------------------------------------------------------- /butterfly/sass/_cursor.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | .focus .cursor 19 | transition: 300ms 20 | 21 | .cursor.reverse-video 22 | box-shadow: 0 0 $shadow-alpha $fg 23 | -------------------------------------------------------------------------------- /butterfly/sass/_font.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | $weights: (ExtraLight 100) (Light 300) (Regular 400) (Medium 500) (Semibold 600) (Bold 700) (Black 900) 19 | 20 | @if $font-family == "SourceCodePro" 21 | @each $weight in $weights 22 | $weight_name: nth($weight, 1) 23 | 24 | @font-face 25 | font-family: "SourceCodePro" 26 | src: url("fonts/SourceCodePro-#{$weight_name}.otf") format("woff") 27 | font-weight: nth($weight, 2) 28 | 29 | body 30 | font-family: $font-family 31 | font-size: $font-size 32 | line-height: $font-line-height 33 | -------------------------------------------------------------------------------- /butterfly/sass/_layout.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | html, body 19 | margin: 0 20 | padding: 0 21 | background-color: $bg 22 | color: $fg 23 | 24 | body 25 | padding-bottom: .5em 26 | white-space: nowrap 27 | overflow-x: hidden 28 | overflow-y: scroll 29 | a 30 | text-decoration: underline rgba($fg, .2) 31 | transition: text-decoration-color 500ms 32 | &:hover 33 | text-decoration: underline 34 | 35 | .line.active 36 | background-color: $active-bg 37 | 38 | .line.extended 39 | cursor: zoom-in 40 | background-image: linear-gradient(90deg, rgba(darken($bg, 3%), 0), 95%, darken($bg, 3%)) 41 | 42 | .extra 43 | display: none 44 | 45 | &:not(.expanded):hover 46 | background-color: lighten($bg, 2%) 47 | 48 | &.expanded 49 | cursor: zoom-out 50 | background-color: darken($bg, 3%) 51 | 52 | .extra 53 | display: block 54 | white-space: pre-wrap 55 | word-break: break-all 56 | 57 | &::-webkit-scrollbar 58 | background: $scroll-bg 59 | width: $scroll-width 60 | 61 | &::-webkit-scrollbar-thumb 62 | background: $scroll-fg 63 | 64 | &::-webkit-scrollbar-thumb:hover 65 | background: $scroll-fg-hover 66 | 67 | /* Pop ups */ 68 | .hidden 69 | display: none !important 70 | 71 | #popup 72 | position: fixed 73 | display: flex 74 | align-items: center 75 | justify-content: center 76 | width: 100% 77 | height: 100% 78 | 79 | form, > div 80 | padding: 1.5em 81 | background: $popup-bg 82 | color: $popup-fg 83 | font-size: $popup-fs 84 | 85 | h2 86 | margin: 0 .5em .5em .5em 87 | select 88 | min-width: 300px 89 | padding: .5em 90 | width: 100% 91 | label 92 | display: block 93 | padding: .5em 94 | font-size: .75em 95 | 96 | #input-view 97 | position: fixed 98 | z-index: 100 99 | padding: 0 100 | margin: 0 101 | text-decoration: underline 102 | 103 | #input-helper 104 | position: fixed 105 | z-index: -100 106 | opacity: 0 107 | white-space: nowrap 108 | overflow: hidden 109 | resize: none 110 | 111 | .terminal 112 | outline: none 113 | -------------------------------------------------------------------------------- /butterfly/sass/_light_fx.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | body 19 | transition: filter 200ms 20 | transform-origin: bottom 21 | 22 | &.bell 23 | filter: blur(2px) 24 | 25 | &.skip 26 | filter: sepia(1) 27 | 28 | &.selection 29 | filter: saturate(2) 30 | 31 | &.alarm 32 | filter: hue-rotate(150deg) 33 | 34 | &.dead 35 | filter: grayscale(1) 36 | 37 | &:after 38 | content: "CLOSED" 39 | font-size: 15em 40 | display: flex 41 | justify-content: center 42 | align-items: center 43 | position: fixed 44 | top: 0 45 | left: 0 46 | width: 100% 47 | height: 100% 48 | transform: rotate(-45deg) 49 | opacity: .2 50 | font-weight: 900 51 | 52 | &.stopped 53 | filter: brightness(50%) 54 | 55 | &.locked 56 | &::-webkit-scrollbar-thumb 57 | background: rgba(red, .7) 58 | 59 | &::-webkit-scrollbar-thumb:hover 60 | background: rgba(red, .8) 61 | -------------------------------------------------------------------------------- /butterfly/sass/_styles.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | /* Theses are the various imported style files 19 | /* THIS NEEDS the python `libsass` library to be installed. 20 | /* You can copy the imported files in the theme dir, they will be imported prioritarily. 21 | 22 | /* You can change this file to import any webfont: 23 | @import font 24 | 25 | /* You can comment / uncomment the following to enable/disable terminal effects. 26 | @import light_fx 27 | /* Comment this one to remove the blurry text: 28 | @import text_fx 29 | /* @import all_fx 30 | 31 | @import colors 32 | /* The color theme is defined in this one: 33 | @import 16_colors 34 | @import 256_colors 35 | 36 | @import layout 37 | @import cursor 38 | @import term_styles 39 | -------------------------------------------------------------------------------- /butterfly/sass/_term_styles.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | .bold 19 | font-weight: bold 20 | 21 | .underline 22 | text-decoration: underline 23 | 24 | .italic 25 | font-style: italic 26 | 27 | .faint 28 | opacity: .6 29 | 30 | .crossed 31 | text-decoration: line-through 32 | 33 | /* Not supported, emulated 34 | /* .blink 35 | /* text-decoration: blink 36 | .blink 37 | animation: blink 1s ease-in-out infinite 38 | 39 | .blink-fast 40 | animation: blink 250ms ease-in-out infinite 41 | 42 | @keyframes blink 43 | 0% 44 | opacity: 1 45 | 50% 46 | opacity: 0 47 | 100% 48 | opacity: 1 49 | 50 | .invisible 51 | visibility: hidden 52 | 53 | .reverse-video 54 | color: $bg 55 | background-color: $fg 56 | 57 | .blur .cursor 58 | border: 1px solid $fg 59 | background: none 60 | 61 | .nbsp 62 | @extend .underline 63 | @extend .fg-color-1 64 | 65 | .inline-html 66 | overflow: hidden 67 | 68 | .inline-image 69 | max-width: 100% 70 | max-height: 50vh 71 | 72 | a 73 | color: inherit 74 | -------------------------------------------------------------------------------- /butterfly/sass/_text_fx.sass: -------------------------------------------------------------------------------- 1 | $fg: #fff !default 2 | $shadow: 6px !default 3 | $shadow-alpha: .5 !default 4 | 5 | body 6 | text-shadow: 0 0 $shadow rgba($fg, $shadow-alpha) 7 | -------------------------------------------------------------------------------- /butterfly/sass/_variables.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* Variables */ 16 | 17 | /** Font 18 | $font-family: "SourceCodePro" !default 19 | $font-size: 1em !default 20 | $font-line-height: 1.2 !default 21 | 22 | /** Colors */ 23 | /* Foreground */ 24 | $fg: #f4ead5 !default 25 | /* Background */ 26 | $bg: #110f13 !default 27 | 28 | $default-bg: transparent !default 29 | $active-bg: transparent !default 30 | $default-fg: $fg !default 31 | 32 | $reverse-transparent: $bg !default 33 | 34 | /* 16 Colors in this orders: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Bright Black, Bright Red, Bright Green, Bright Yellow, Bright Blue, Bright Magenta, Bright Cyan, Bright White */ 35 | $colors: #2e3436, #cc0000, #4e9a06, #c4a000, #3465a4, #75507b, #06989a, #d3d7cf, #555753, #ef2929, #8ae234, #fce94f, #729fcf, #ad7fa8, #34e2e2, #eeeeec !default 36 | 37 | /** Text effects */ 38 | 39 | /* The shadow is the size of the blur (in px for instance) 40 | $shadow: 0 !default 41 | /* The shadow alpha is the opacity of the shadow 42 | $shadow-alpha: 0 !default 43 | 44 | /** Scroll */ 45 | $scroll-bg: $bg !default 46 | $scroll-fg: rgba($fg, .1) !default 47 | $scroll-fg-hover: rgba($fg, .1) !default 48 | $scroll-width: .75em !default 49 | 50 | /** Popup */ 51 | $popup-bg: rgba(127, 127, 127, .5) !default 52 | $popup-fg: $fg !default 53 | $popup-fs: 1em !default 54 | 55 | -------------------------------------------------------------------------------- /butterfly/sass/main.sass: -------------------------------------------------------------------------------- 1 | /* *-* coding: utf-8 *-* */ 2 | /* This file is part of butterfly */ 3 | 4 | /* butterfly Copyright(C) 2015-2017 Florian Mounier */ 5 | /* This program is free software: you can redistribute it and/or modify */ 6 | /* it under the terms of the GNU General Public License as published by */ 7 | /* the Free Software Foundation, either version 3 of the License, or */ 8 | /* (at your option) any later version. */ 9 | 10 | /* This program is distributed in the hope that it will be useful, */ 11 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 12 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 13 | /* GNU General Public License for more details. */ 14 | 15 | /* You should have received a copy of the GNU General Public License */ 16 | /* along with this program. If not, see . */ 17 | 18 | /* Theses are the various imported style files 19 | /* THIS NEEDS the python `libsass` library to be installed. 20 | /* You can copy the imported files in the theme dir, they will be imported prioritarily. 21 | 22 | /* These a the default variables */ 23 | @import variables 24 | 25 | /* These are all imported files */ 26 | @import styles 27 | -------------------------------------------------------------------------------- /butterfly/static/config.rb: -------------------------------------------------------------------------------- 1 | http_path = "/static/" 2 | css_dir = "stylesheets" 3 | fonts_dir = "fonts" 4 | sass_dir = "sass" 5 | images_dir = "images" 6 | javascripts_dir = "javascripts" 7 | -------------------------------------------------------------------------------- /butterfly/static/ext.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, escape, histSize, linkify, maybePack, nextLeaf, packSize, popup, previousLeaf, selection, setAlarm, tags, tid, walk, 3 | indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 4 | 5 | clean_ansi = function(data) { 6 | var c, i, out, state; 7 | if (data.indexOf('\x1b') < 0) { 8 | return data; 9 | } 10 | i = -1; 11 | out = ''; 12 | state = 'normal'; 13 | while (i < data.length - 1) { 14 | c = data.charAt(++i); 15 | switch (state) { 16 | case 'normal': 17 | if (c === '\x1b') { 18 | state = 'escaped'; 19 | break; 20 | } 21 | out += c; 22 | break; 23 | case 'escaped': 24 | if (c === '[') { 25 | state = 'csi'; 26 | break; 27 | } 28 | if (c === ']') { 29 | state = 'osc'; 30 | break; 31 | } 32 | if ('#()%*+-./'.indexOf(c) >= 0) { 33 | i++; 34 | } 35 | state = 'normal'; 36 | break; 37 | case 'csi': 38 | if ("?>!$\" '".indexOf(c) >= 0) { 39 | break; 40 | } 41 | if (('0' <= c && c <= '9')) { 42 | break; 43 | } 44 | if (c === ';') { 45 | break; 46 | } 47 | state = 'normal'; 48 | break; 49 | case 'osc': 50 | if (c === "\x1b" || c === "\x07") { 51 | if (c === "\x1b") { 52 | i++; 53 | } 54 | state = 'normal'; 55 | } 56 | } 57 | } 58 | return out; 59 | }; 60 | 61 | setAlarm = function(notification, cond) { 62 | var alarm; 63 | alarm = function(data) { 64 | var message, note, notif; 65 | message = clean_ansi(data.data.slice(1)); 66 | if (cond !== null && !cond.test(message)) { 67 | return; 68 | } 69 | butterfly.body.classList.remove('alarm'); 70 | note = "Butterfly [" + butterfly.title + "]"; 71 | if (notification) { 72 | notif = new Notification(note, { 73 | body: message, 74 | icon: '/static/images/favicon.png' 75 | }); 76 | notif.onclick = function() { 77 | window.focus(); 78 | return notif.close(); 79 | }; 80 | } else { 81 | alert(note + '\n' + message); 82 | } 83 | return butterfly.ws.shell.removeEventListener('message', alarm); 84 | }; 85 | butterfly.ws.shell.addEventListener('message', alarm); 86 | return butterfly.body.classList.add('alarm'); 87 | }; 88 | 89 | cancel = function(ev) { 90 | if (ev.preventDefault) { 91 | ev.preventDefault(); 92 | } 93 | if (ev.stopPropagation) { 94 | ev.stopPropagation(); 95 | } 96 | ev.cancelBubble = true; 97 | return false; 98 | }; 99 | 100 | document.addEventListener('keydown', function(e) { 101 | var cond; 102 | if (!(e.altKey && e.keyCode === 65)) { 103 | return true; 104 | } 105 | cond = null; 106 | if (e.shiftKey) { 107 | cond = prompt('Ring alarm when encountering the following text: (can be a regexp)'); 108 | if (!cond) { 109 | return; 110 | } 111 | cond = new RegExp(cond); 112 | } 113 | if (Notification && Notification.permission === 'default') { 114 | Notification.requestPermission(function() { 115 | return setAlarm(Notification.permission === 'granted', cond); 116 | }); 117 | } else { 118 | setAlarm(Notification.permission === 'granted', cond); 119 | } 120 | return cancel(e); 121 | }); 122 | 123 | addEventListener('copy', copy = function(e) { 124 | var data, end, j, len, line, ref, sel; 125 | document.getElementsByTagName('body')[0].contentEditable = false; 126 | butterfly.bell("copied"); 127 | e.clipboardData.clearData(); 128 | sel = getSelection().toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' '); 129 | data = ''; 130 | ref = sel.split('\n'); 131 | for (j = 0, len = ref.length; j < len; j++) { 132 | line = ref[j]; 133 | if (line.slice(-1) === '\u23CE') { 134 | end = ''; 135 | line = line.slice(0, -1); 136 | } else { 137 | end = '\n'; 138 | } 139 | data += line.replace(/\s*$/, '') + end; 140 | } 141 | e.clipboardData.setData('text/plain', data.slice(0, -1)); 142 | return e.preventDefault(); 143 | }); 144 | 145 | addEventListener('paste', function(e) { 146 | var data, send, size; 147 | document.getElementsByTagName('body')[0].contentEditable = false; 148 | butterfly.bell("pasted"); 149 | data = e.clipboardData.getData('text/plain'); 150 | data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r'); 151 | size = 1024; 152 | send = function() { 153 | butterfly.send(data.substring(0, size)); 154 | data = data.substring(size); 155 | if (data.length) { 156 | return setTimeout(send, 25); 157 | } 158 | }; 159 | send(); 160 | return e.preventDefault(); 161 | }); 162 | 163 | addEventListener('beforeunload', function(e) { 164 | if (!(butterfly.body.classList.contains('dead') || location.href.indexOf('session') > -1)) { 165 | return e.returnValue = 'This terminal is active and not in session. Are you sure you want to kill it?'; 166 | } 167 | }); 168 | 169 | Terminal.on('change', function(line) { 170 | if (indexOf.call(line.classList, 'extended') >= 0) { 171 | return line.addEventListener('click', (function(line) { 172 | return function() { 173 | var after, before; 174 | if (indexOf.call(line.classList, 'expanded') >= 0) { 175 | return line.classList.remove('expanded'); 176 | } else { 177 | before = line.getBoundingClientRect().height; 178 | line.classList.add('expanded'); 179 | after = line.getBoundingClientRect().height; 180 | return document.body.scrollTop += after - before; 181 | } 182 | }; 183 | })(line)); 184 | } 185 | }); 186 | 187 | walk = function(node, callback) { 188 | var child, j, len, ref, results; 189 | ref = node.childNodes; 190 | results = []; 191 | for (j = 0, len = ref.length; j < len; j++) { 192 | child = ref[j]; 193 | callback.call(child); 194 | results.push(walk(child, callback)); 195 | } 196 | return results; 197 | }; 198 | 199 | linkify = function(text) { 200 | var emailAddressPattern, pseudoUrlPattern, urlPattern; 201 | urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; 202 | pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim; 203 | emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim; 204 | return text.replace(urlPattern, '$&').replace(pseudoUrlPattern, '$1$2').replace(emailAddressPattern, '$&'); 205 | }; 206 | 207 | tags = { 208 | '&': '&', 209 | '<': '<', 210 | '>': '>' 211 | }; 212 | 213 | escape = function(s) { 214 | return s.replace(/[&<>]/g, function(tag) { 215 | return tags[tag] || tag; 216 | }); 217 | }; 218 | 219 | Terminal.on('change', function(line) { 220 | return walk(line, function() { 221 | var linkified, newNode, val; 222 | if (this.nodeType === 3) { 223 | val = this.nodeValue; 224 | linkified = linkify(escape(val)); 225 | if (linkified !== val) { 226 | newNode = document.createElement('span'); 227 | newNode.innerHTML = linkified; 228 | this.parentElement.replaceChild(newNode, this); 229 | return true; 230 | } 231 | } 232 | }); 233 | }); 234 | 235 | ctrl = false; 236 | 237 | alt = false; 238 | 239 | addEventListener('touchstart', function(e) { 240 | if (e.touches.length === 2) { 241 | return ctrl = true; 242 | } else if (e.touches.length === 3) { 243 | ctrl = false; 244 | return alt = true; 245 | } else if (e.touches.length === 4) { 246 | ctrl = true; 247 | return alt = true; 248 | } 249 | }); 250 | 251 | window.mobileKeydown = function(e) { 252 | var _altKey, _ctrlKey, _keyCode; 253 | if (ctrl || alt) { 254 | _ctrlKey = ctrl; 255 | _altKey = alt; 256 | _keyCode = e.keyCode; 257 | if (e.keyCode >= 97 && e.keyCode <= 122) { 258 | _keyCode -= 32; 259 | } 260 | e = new KeyboardEvent('keydown', { 261 | ctrlKey: _ctrlKey, 262 | altKey: _altKey, 263 | keyCode: _keyCode 264 | }); 265 | ctrl = alt = false; 266 | setTimeout(function() { 267 | return window.dispatchEvent(e); 268 | }, 0); 269 | return true; 270 | } else { 271 | return false; 272 | } 273 | }; 274 | 275 | document.addEventListener('keydown', function(e) { 276 | if (!(e.altKey && e.keyCode === 79)) { 277 | return true; 278 | } 279 | open(location.origin); 280 | return cancel(e); 281 | }); 282 | 283 | tid = null; 284 | 285 | packSize = 1000; 286 | 287 | histSize = 100; 288 | 289 | maybePack = function() { 290 | var hist, i, j, pack, packfrag, ref; 291 | if (!(butterfly.term.childElementCount > packSize + butterfly.rows)) { 292 | return; 293 | } 294 | hist = document.getElementById('packed'); 295 | packfrag = document.createDocumentFragment('fragment'); 296 | for (i = j = 0, ref = packSize; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { 297 | packfrag.appendChild(butterfly.term.firstChild); 298 | } 299 | pack = document.createElement('div'); 300 | pack.classList.add('pack'); 301 | pack.appendChild(packfrag); 302 | hist.appendChild(pack); 303 | if (hist.childElementCount > histSize) { 304 | hist.firstChild.remove(); 305 | } 306 | return tid = setTimeout(maybePack); 307 | }; 308 | 309 | Terminal.on('refresh', function() { 310 | if (tid) { 311 | clearTimeout(tid); 312 | } 313 | return maybePack(); 314 | }); 315 | 316 | Terminal.on('clear', function() { 317 | var hist, newHist; 318 | newHist = document.createElement('div'); 319 | newHist.id = 'packed'; 320 | hist = document.getElementById('packed'); 321 | return butterfly.body.replaceChild(newHist, hist); 322 | }); 323 | 324 | Popup = (function() { 325 | function Popup() { 326 | this.el = document.getElementById('popup'); 327 | this.bound_click_maybe_close = this.click_maybe_close.bind(this); 328 | this.bound_key_maybe_close = this.key_maybe_close.bind(this); 329 | } 330 | 331 | Popup.prototype.open = function(html) { 332 | this.el.innerHTML = html; 333 | this.el.classList.remove('hidden'); 334 | addEventListener('click', this.bound_click_maybe_close); 335 | return addEventListener('keydown', this.bound_key_maybe_close); 336 | }; 337 | 338 | Popup.prototype.close = function() { 339 | removeEventListener('click', this.bound_click_maybe_close); 340 | removeEventListener('keydown', this.bound_key_maybe_close); 341 | this.el.classList.add('hidden'); 342 | return this.el.innerHTML = ''; 343 | }; 344 | 345 | Popup.prototype.click_maybe_close = function(e) { 346 | var t; 347 | t = e.target; 348 | while (t.parentElement) { 349 | if (Array.prototype.slice.call(this.el.children).indexOf(t) > -1) { 350 | return true; 351 | } 352 | t = t.parentElement; 353 | } 354 | this.close(); 355 | return cancel(e); 356 | }; 357 | 358 | Popup.prototype.key_maybe_close = function(e) { 359 | if (e.keyCode !== 27) { 360 | return true; 361 | } 362 | this.close(); 363 | return cancel(e); 364 | }; 365 | 366 | return Popup; 367 | 368 | })(); 369 | 370 | popup = new Popup(); 371 | 372 | selection = null; 373 | 374 | cancel = function(ev) { 375 | if (ev.preventDefault) { 376 | ev.preventDefault(); 377 | } 378 | if (ev.stopPropagation) { 379 | ev.stopPropagation(); 380 | } 381 | ev.cancelBubble = true; 382 | return false; 383 | }; 384 | 385 | previousLeaf = function(node) { 386 | var previous; 387 | previous = node.previousSibling; 388 | if (!previous) { 389 | previous = node.parentNode.previousSibling; 390 | } 391 | if (!previous) { 392 | previous = node.parentNode.parentNode.previousSibling; 393 | } 394 | while (previous.lastChild) { 395 | previous = previous.lastChild; 396 | } 397 | return previous; 398 | }; 399 | 400 | nextLeaf = function(node) { 401 | var next; 402 | next = node.nextSibling; 403 | if (!next) { 404 | next = node.parentNode.nextSibling; 405 | } 406 | if (!next) { 407 | next = node.parentNode.parentNode.nextSibling; 408 | } 409 | while (next != null ? next.firstChild : void 0) { 410 | next = next.firstChild; 411 | } 412 | return next; 413 | }; 414 | 415 | Selection = (function() { 416 | function Selection() { 417 | butterfly.body.classList.add('selection'); 418 | this.selection = getSelection(); 419 | } 420 | 421 | Selection.prototype.reset = function() { 422 | var fakeRange, ref, results; 423 | this.selection = getSelection(); 424 | fakeRange = document.createRange(); 425 | fakeRange.setStart(this.selection.anchorNode, this.selection.anchorOffset); 426 | fakeRange.setEnd(this.selection.focusNode, this.selection.focusOffset); 427 | this.start = { 428 | node: this.selection.anchorNode, 429 | offset: this.selection.anchorOffset 430 | }; 431 | this.end = { 432 | node: this.selection.focusNode, 433 | offset: this.selection.focusOffset 434 | }; 435 | if (fakeRange.collapsed) { 436 | ref = [this.end, this.start], this.start = ref[0], this.end = ref[1]; 437 | } 438 | this.startLine = this.start.node; 439 | while (!this.startLine.classList || indexOf.call(this.startLine.classList, 'line') < 0) { 440 | this.startLine = this.startLine.parentNode; 441 | } 442 | this.endLine = this.end.node; 443 | results = []; 444 | while (!this.endLine.classList || indexOf.call(this.endLine.classList, 'line') < 0) { 445 | results.push(this.endLine = this.endLine.parentNode); 446 | } 447 | return results; 448 | }; 449 | 450 | Selection.prototype.clear = function() { 451 | return this.selection.removeAllRanges(); 452 | }; 453 | 454 | Selection.prototype.destroy = function() { 455 | butterfly.body.classList.remove('selection'); 456 | return this.clear(); 457 | }; 458 | 459 | Selection.prototype.text = function() { 460 | return this.selection.toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' '); 461 | }; 462 | 463 | Selection.prototype.up = function() { 464 | return this.go(-1); 465 | }; 466 | 467 | Selection.prototype.down = function() { 468 | return this.go(+1); 469 | }; 470 | 471 | Selection.prototype.go = function(n) { 472 | var index; 473 | index = Array.prototype.indexOf.call(butterfly.term.childNodes, this.startLine) + n; 474 | if (!((0 <= index && index < butterfly.term.childElementCount))) { 475 | return; 476 | } 477 | while (!butterfly.term.childNodes[index].textContent.match(/\S/)) { 478 | index += n; 479 | if (!((0 <= index && index < butterfly.term.childElementCount))) { 480 | return; 481 | } 482 | } 483 | return this.selectLine(index); 484 | }; 485 | 486 | Selection.prototype.apply = function() { 487 | var range; 488 | this.clear(); 489 | range = document.createRange(); 490 | range.setStart(this.start.node, this.start.offset); 491 | range.setEnd(this.end.node, this.end.offset); 492 | return this.selection.addRange(range); 493 | }; 494 | 495 | Selection.prototype.selectLine = function(index) { 496 | var line, lineEnd, lineStart; 497 | line = butterfly.term.childNodes[index]; 498 | lineStart = { 499 | node: line.firstChild, 500 | offset: 0 501 | }; 502 | lineEnd = { 503 | node: line.lastChild, 504 | offset: line.lastChild.textContent.length 505 | }; 506 | this.start = this.walk(lineStart, /\S/); 507 | return this.end = this.walk(lineEnd, /\S/, true); 508 | }; 509 | 510 | Selection.prototype.collapsed = function(start, end) { 511 | var fakeRange; 512 | fakeRange = document.createRange(); 513 | fakeRange.setStart(start.node, start.offset); 514 | fakeRange.setEnd(end.node, end.offset); 515 | return fakeRange.collapsed; 516 | }; 517 | 518 | Selection.prototype.shrinkRight = function() { 519 | var end, node; 520 | node = this.walk(this.end, /\s/, true); 521 | end = this.walk(node, /\S/, true); 522 | if (!this.collapsed(this.start, end)) { 523 | return this.end = end; 524 | } 525 | }; 526 | 527 | Selection.prototype.shrinkLeft = function() { 528 | var node, start; 529 | node = this.walk(this.start, /\s/); 530 | start = this.walk(node, /\S/); 531 | if (!this.collapsed(start, this.end)) { 532 | return this.start = start; 533 | } 534 | }; 535 | 536 | Selection.prototype.expandRight = function() { 537 | var node; 538 | node = this.walk(this.end, /\S/); 539 | return this.end = this.walk(node, /\s/); 540 | }; 541 | 542 | Selection.prototype.expandLeft = function() { 543 | var node; 544 | node = this.walk(this.start, /\S/, true); 545 | return this.start = this.walk(node, /\s/, true); 546 | }; 547 | 548 | Selection.prototype.walk = function(needle, til, backward) { 549 | var i, node, text; 550 | if (backward == null) { 551 | backward = false; 552 | } 553 | if (needle.node.firstChild) { 554 | node = needle.node.firstChild; 555 | } else { 556 | node = needle.node; 557 | } 558 | text = node != null ? node.textContent : void 0; 559 | i = needle.offset; 560 | if (backward) { 561 | while (node) { 562 | while (i > 0) { 563 | if (text[--i].match(til)) { 564 | return { 565 | node: node, 566 | offset: i + 1 567 | }; 568 | } 569 | } 570 | node = previousLeaf(node); 571 | text = node != null ? node.textContent : void 0; 572 | i = text.length; 573 | } 574 | } else { 575 | while (node) { 576 | while (i < text.length) { 577 | if (text[i++].match(til)) { 578 | return { 579 | node: node, 580 | offset: i - 1 581 | }; 582 | } 583 | } 584 | node = nextLeaf(node); 585 | text = node != null ? node.textContent : void 0; 586 | i = 0; 587 | } 588 | } 589 | return needle; 590 | }; 591 | 592 | return Selection; 593 | 594 | })(); 595 | 596 | document.addEventListener('keydown', function(e) { 597 | var r, ref, ref1; 598 | if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) { 599 | return true; 600 | } 601 | if (e.shiftKey && e.keyCode === 13 && !selection && !getSelection().isCollapsed) { 602 | butterfly.send(getSelection().toString()); 603 | getSelection().removeAllRanges(); 604 | return cancel(e); 605 | } 606 | if (selection) { 607 | selection.reset(); 608 | if (!e.ctrlKey && e.shiftKey && (37 <= (ref1 = e.keyCode) && ref1 <= 40)) { 609 | return true; 610 | } 611 | if (e.shiftKey && e.ctrlKey) { 612 | if (e.keyCode === 38) { 613 | selection.up(); 614 | } else if (e.keyCode === 40) { 615 | selection.down(); 616 | } 617 | } else if (e.keyCode === 39) { 618 | selection.shrinkLeft(); 619 | } else if (e.keyCode === 38) { 620 | selection.expandLeft(); 621 | } else if (e.keyCode === 37) { 622 | selection.shrinkRight(); 623 | } else if (e.keyCode === 40) { 624 | selection.expandRight(); 625 | } else { 626 | return cancel(e); 627 | } 628 | if (selection != null) { 629 | selection.apply(); 630 | } 631 | return cancel(e); 632 | } 633 | if (!selection && e.ctrlKey && e.shiftKey && e.keyCode === 38) { 634 | r = Math.max(butterfly.term.childElementCount - butterfly.rows, 0); 635 | selection = new Selection(); 636 | selection.selectLine(r + butterfly.y - 1); 637 | selection.apply(); 638 | return cancel(e); 639 | } 640 | return true; 641 | }); 642 | 643 | document.addEventListener('keyup', function(e) { 644 | var ref, ref1; 645 | if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) { 646 | return true; 647 | } 648 | if (selection) { 649 | if (e.keyCode === 13) { 650 | butterfly.send(selection.text()); 651 | selection.destroy(); 652 | selection = null; 653 | return cancel(e); 654 | } 655 | if (ref1 = e.keyCode, indexOf.call([37, 38, 39, 40], ref1) < 0) { 656 | selection.destroy(); 657 | selection = null; 658 | return true; 659 | } 660 | } 661 | return true; 662 | }); 663 | 664 | document.addEventListener('dblclick', function(e) { 665 | var anchorNode, anchorOffset, newRange, range, sel; 666 | if (e.ctrlKey || e.altkey) { 667 | return; 668 | } 669 | sel = getSelection(); 670 | if (sel.isCollapsed || sel.toString().match(/\s/)) { 671 | return; 672 | } 673 | range = document.createRange(); 674 | range.setStart(sel.anchorNode, sel.anchorOffset); 675 | range.setEnd(sel.focusNode, sel.focusOffset); 676 | if (range.collapsed) { 677 | sel.removeAllRanges(); 678 | newRange = document.createRange(); 679 | newRange.setStart(sel.focusNode, sel.focusOffset); 680 | newRange.setEnd(sel.anchorNode, sel.anchorOffset); 681 | sel.addRange(newRange); 682 | } 683 | while (!(sel.toString().match(/\s/) || !sel.toString())) { 684 | sel.modify('extend', 'forward', 'character'); 685 | } 686 | sel.modify('extend', 'backward', 'character'); 687 | anchorNode = sel.anchorNode; 688 | anchorOffset = sel.anchorOffset; 689 | sel.collapseToEnd(); 690 | sel.extend(anchorNode, anchorOffset); 691 | while (!(sel.toString().match(/\s/) || !sel.toString())) { 692 | sel.modify('extend', 'backward', 'character'); 693 | } 694 | return sel.modify('extend', 'forward', 'character'); 695 | }); 696 | 697 | document.addEventListener('keydown', function(e) { 698 | var oReq; 699 | if (!(e.altKey && e.keyCode === 69)) { 700 | return true; 701 | } 702 | oReq = new XMLHttpRequest(); 703 | oReq.addEventListener('load', function() { 704 | var j, len, out, ref, response, session; 705 | response = JSON.parse(this.responseText); 706 | out = '
'; 707 | out += '

Session list

'; 708 | if (response.sessions.length === 0) { 709 | out += "No current session for user " + response.user; 710 | } else { 711 | out += '
    '; 712 | ref = response.sessions; 713 | for (j = 0, len = ref.length; j < len; j++) { 714 | session = ref[j]; 715 | out += "
  • " + session + "
  • "; 716 | } 717 | out += '
'; 718 | } 719 | out += '
'; 720 | return popup.open(out); 721 | }); 722 | oReq.open("GET", "/sessions/list.json"); 723 | oReq.send(); 724 | return cancel(e); 725 | }); 726 | 727 | _set_theme_href = function(href) { 728 | var img; 729 | document.getElementById('style').setAttribute('href', href); 730 | img = document.createElement('img'); 731 | img.onerror = function() { 732 | return setTimeout((function() { 733 | return typeof butterfly !== "undefined" && butterfly !== null ? butterfly.resize() : void 0; 734 | }), 250); 735 | }; 736 | return img.src = href; 737 | }; 738 | 739 | _theme = typeof localStorage !== "undefined" && localStorage !== null ? localStorage.getItem('theme') : void 0; 740 | 741 | if (_theme) { 742 | _set_theme_href(_theme); 743 | } 744 | 745 | this.set_theme = function(theme) { 746 | _theme = theme; 747 | if (typeof localStorage !== "undefined" && localStorage !== null) { 748 | localStorage.setItem('theme', theme); 749 | } 750 | if (theme) { 751 | return _set_theme_href(theme); 752 | } 753 | }; 754 | 755 | document.addEventListener('keydown', function(e) { 756 | var oReq, style; 757 | if (!(e.altKey && e.keyCode === 83)) { 758 | return true; 759 | } 760 | if (e.shiftKey) { 761 | style = document.getElementById('style').getAttribute('href'); 762 | style = style.split('?')[0]; 763 | _set_theme_href(style + '?' + (new Date().getTime())); 764 | return cancel(e); 765 | } 766 | oReq = new XMLHttpRequest(); 767 | oReq.addEventListener('load', function() { 768 | var builtin_themes, inner, j, k, len, len1, option, response, theme, theme_list, themes, url; 769 | response = JSON.parse(this.responseText); 770 | builtin_themes = response.builtin_themes; 771 | themes = response.themes; 772 | inner = "
\n

Pick a theme:

\n \n \n
"; 800 | popup.open(inner); 801 | theme_list = document.getElementById('theme_list'); 802 | return theme_list.addEventListener('change', function() { 803 | return set_theme(theme_list.value); 804 | }); 805 | }); 806 | oReq.open("GET", "/themes/list.json"); 807 | oReq.send(); 808 | return cancel(e); 809 | }); 810 | 811 | }).call(this); 812 | 813 | //# sourceMappingURL=ext.js.map 814 | -------------------------------------------------------------------------------- /butterfly/static/ext.min.js: -------------------------------------------------------------------------------- 1 | /*! butterfly 2018-09-12 */ 2 | 3 | (function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w=[].indexOf||function(a){for(var b=0,c=this.length;b=0&&c++,e="normal";break;case"csi":if("?>!$\" '".indexOf(b)>=0)break;if("0"<=b&&b<="9")break;if(";"===b)break;e="normal";break;case"osc":""!==b&&""!==b||(""===b&&c++,e="normal")}return d},s=function(a,b){var c;return c=function(d){var e,f,h;if(e=g(d.data.slice(1)),null===b||b.test(e))return butterfly.body.classList.remove("alarm"),f="Butterfly ["+butterfly.title+"]",a?(h=new Notification(f,{body:e,icon:"/static/images/favicon.png"}),h.onclick=function(){return window.focus(),h.close()}):alert(f+"\n"+e),butterfly.ws.shell.removeEventListener("message",c)},butterfly.ws.shell.addEventListener("message",c),butterfly.body.classList.add("alarm")},f=function(a){return a.preventDefault&&a.preventDefault(),a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0,!1},document.addEventListener("keydown",function(a){var b;if(!a.altKey||65!==a.keyCode)return!0;if(b=null,a.shiftKey){if(b=prompt("Ring alarm when encountering the following text: (can be a regexp)"),!b)return;b=new RegExp(b)}return Notification&&"default"===Notification.permission?Notification.requestPermission(function(){return s("granted"===Notification.permission,b)}):s("granted"===Notification.permission,b),f(a)}),addEventListener("copy",h=function(a){var b,c,d,e,f,g,h;for(document.getElementsByTagName("body")[0].contentEditable=!1,butterfly.bell("copied"),a.clipboardData.clearData(),h=getSelection().toString().replace(/\u00A0/g," ").replace(/\u2007/g," "),b="",g=h.split("\n"),d=0,e=g.length;d-1))return a.returnValue="This terminal is active and not in session. Are you sure you want to kill it?"}),Terminal.on("change",function(a){if(w.call(a.classList,"extended")>=0)return a.addEventListener("click",function(a){return function(){var b,c;return w.call(a.classList,"expanded")>=0?a.classList.remove("expanded"):(c=a.getBoundingClientRect().height,a.classList.add("expanded"),b=a.getBoundingClientRect().height,document.body.scrollTop+=b-c)}}(a))}),v=function(a,b){var c,d,e,f,g;for(f=a.childNodes,g=[],d=0,e=f.length;d$&').replace(c,'$1$2').replace(b,'$&')},t={"&":"&","<":"<",">":">"},j=function(a){return a.replace(/[&<>]/g,function(a){return t[a]||a})},Terminal.on("change",function(a){return v(a,function(){var a,b,c;if(3===this.nodeType&&(c=this.nodeValue,a=l(j(c)),a!==c))return b=document.createElement("span"),b.innerHTML=a,this.parentElement.replaceChild(b,this),!0})}),i=!1,e=!1,addEventListener("touchstart",function(a){return 2===a.touches.length?i=!0:3===a.touches.length?(i=!1,e=!0):4===a.touches.length?(i=!0,e=!0):void 0}),window.mobileKeydown=function(a){var b,c,d;return!(!i&&!e)&&(c=i,b=e,d=a.keyCode,a.keyCode>=97&&a.keyCode<=122&&(d-=32),a=new KeyboardEvent("keydown",{ctrlKey:c,altKey:b,keyCode:d}),i=e=!1,setTimeout(function(){return window.dispatchEvent(a)},0),!0)},document.addEventListener("keydown",function(a){return!a.altKey||79!==a.keyCode||(open(location.origin),f(a))}),u=null,o=1e3,k=100,m=function(){var a,b,c,d,e,f;if(butterfly.term.childElementCount>o+butterfly.rows){for(a=document.getElementById("packed"),e=document.createDocumentFragment("fragment"),b=c=0,f=o;0<=f?c<=f:c>=f;b=0<=f?++c:--c)e.appendChild(butterfly.term.firstChild);return d=document.createElement("div"),d.classList.add("pack"),d.appendChild(e),a.appendChild(d),a.childElementCount>k&&a.firstChild.remove(),u=setTimeout(m)}},Terminal.on("refresh",function(){return u&&clearTimeout(u),m()}),Terminal.on("clear",function(){var a,b;return b=document.createElement("div"),b.id="packed",a=document.getElementById("packed"),butterfly.body.replaceChild(b,a)}),a=function(){function a(){this.el=document.getElementById("popup"),this.bound_click_maybe_close=this.click_maybe_close.bind(this),this.bound_key_maybe_close=this.key_maybe_close.bind(this)}return a.prototype.open=function(a){return this.el.innerHTML=a,this.el.classList.remove("hidden"),addEventListener("click",this.bound_click_maybe_close),addEventListener("keydown",this.bound_key_maybe_close)},a.prototype.close=function(){return removeEventListener("click",this.bound_click_maybe_close),removeEventListener("keydown",this.bound_key_maybe_close),this.el.classList.add("hidden"),this.el.innerHTML=""},a.prototype.click_maybe_close=function(a){var b;for(b=a.target;b.parentElement;){if(Array.prototype.slice.call(this.el.children).indexOf(b)>-1)return!0;b=b.parentElement}return this.close(),f(a)},a.prototype.key_maybe_close=function(a){return 27!==a.keyCode||(this.close(),f(a))},a}(),p=new a,r=null,f=function(a){return a.preventDefault&&a.preventDefault(),a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0,!1},q=function(a){var b;for(b=a.previousSibling,b||(b=a.parentNode.previousSibling),b||(b=a.parentNode.parentNode.previousSibling);b.lastChild;)b=b.lastChild;return b},n=function(a){var b;for(b=a.nextSibling,b||(b=a.parentNode.nextSibling),b||(b=a.parentNode.parentNode.nextSibling);null!=b?b.firstChild:void 0;)b=b.firstChild;return b},b=function(){function a(){butterfly.body.classList.add("selection"),this.selection=getSelection()}return a.prototype.reset=function(){var a,b,c;for(this.selection=getSelection(),a=document.createRange(),a.setStart(this.selection.anchorNode,this.selection.anchorOffset),a.setEnd(this.selection.focusNode,this.selection.focusOffset),this.start={node:this.selection.anchorNode,offset:this.selection.anchorOffset},this.end={node:this.selection.focusNode,offset:this.selection.focusOffset},a.collapsed&&(b=[this.end,this.start],this.start=b[0],this.end=b[1]),this.startLine=this.start.node;!this.startLine.classList||w.call(this.startLine.classList,"line")<0;)this.startLine=this.startLine.parentNode;for(this.endLine=this.end.node,c=[];!this.endLine.classList||w.call(this.endLine.classList,"line")<0;)c.push(this.endLine=this.endLine.parentNode);return c},a.prototype.clear=function(){return this.selection.removeAllRanges()},a.prototype.destroy=function(){return butterfly.body.classList.remove("selection"),this.clear()},a.prototype.text=function(){return this.selection.toString().replace(/\u00A0/g," ").replace(/\u2007/g," ")},a.prototype.up=function(){return this.go(-1)},a.prototype.down=function(){return this.go(1)},a.prototype.go=function(a){var b;if(b=Array.prototype.indexOf.call(butterfly.term.childNodes,this.startLine)+a,0<=b&&b0;)if(f[--d].match(b))return{node:e,offset:d+1};e=q(e),f=null!=e?e.textContent:void 0,d=f.length}else for(;e;){for(;d=0)return!0;if(a.shiftKey&&13===a.keyCode&&!r&&!getSelection().isCollapsed)return butterfly.send(getSelection().toString()),getSelection().removeAllRanges(),f(a);if(r){if(r.reset(),!a.ctrlKey&&a.shiftKey&&37<=(e=a.keyCode)&&e<=40)return!0;if(a.shiftKey&&a.ctrlKey)38===a.keyCode?r.up():40===a.keyCode&&r.down();else if(39===a.keyCode)r.shrinkLeft();else if(38===a.keyCode)r.expandLeft();else if(37===a.keyCode)r.shrinkRight();else{if(40!==a.keyCode)return f(a);r.expandRight()}return null!=r&&r.apply(),f(a)}return!(!r&&a.ctrlKey&&a.shiftKey&&38===a.keyCode)||(c=Math.max(butterfly.term.childElementCount-butterfly.rows,0),r=new b,r.selectLine(c+butterfly.y-1),r.apply(),f(a))}),document.addEventListener("keyup",function(a){var b,c;if(b=a.keyCode,w.call([16,17,18,19],b)>=0)return!0;if(r){if(13===a.keyCode)return butterfly.send(r.text()),r.destroy(),r=null,f(a);if(c=a.keyCode,w.call([37,38,39,40],c)<0)return r.destroy(),r=null,!0}return!0}),document.addEventListener("dblclick",function(a){var b,c,d,e,f;if(!(a.ctrlKey||a.altkey||(f=getSelection(),f.isCollapsed||f.toString().match(/\s/)))){for(e=document.createRange(),e.setStart(f.anchorNode,f.anchorOffset),e.setEnd(f.focusNode,f.focusOffset),e.collapsed&&(f.removeAllRanges(),d=document.createRange(),d.setStart(f.focusNode,f.focusOffset),d.setEnd(f.anchorNode,f.anchorOffset),f.addRange(d));!f.toString().match(/\s/)&&f.toString();)f.modify("extend","forward","character");for(f.modify("extend","backward","character"),b=f.anchorNode,c=f.anchorOffset,f.collapseToEnd(),f.extend(b,c);!f.toString().match(/\s/)&&f.toString();)f.modify("extend","backward","character");return f.modify("extend","forward","character")}}),document.addEventListener("keydown",function(a){var b;return!a.altKey||69!==a.keyCode||(b=new XMLHttpRequest,b.addEventListener("load",function(){var a,b,c,d,e,f;if(e=JSON.parse(this.responseText),c="
",c+="

Session list

",0===e.sessions.length)c+="No current session for user "+e.user;else{for(c+="
    ",d=e.sessions,a=0,b=d.length;a'+f+"";c+="
"}return c+="
",p.open(c)}),b.open("GET","/sessions/list.json"),b.send(),f(a))}),c=function(a){var b;return document.getElementById("style").setAttribute("href",a),b=document.createElement("img"),b.onerror=function(){return setTimeout(function(){return"undefined"!=typeof butterfly&&null!==butterfly?butterfly.resize():void 0},250)},b.src=a},d="undefined"!=typeof localStorage&&null!==localStorage?localStorage.getItem("theme"):void 0,d&&c(d),this.set_theme=function(a){if(d=a,"undefined"!=typeof localStorage&&null!==localStorage&&localStorage.setItem("theme",a),a)return c(a)},document.addEventListener("keydown",function(a){var b,e;return!a.altKey||83!==a.keyCode||(a.shiftKey?(e=document.getElementById("style").getAttribute("href"),e=e.split("?")[0],c(e+"?"+(new Date).getTime()),f(a)):(b=new XMLHttpRequest,b.addEventListener("load",function(){var a,b,c,e,f,g,h,i,j,k,l,m;if(i=JSON.parse(this.responseText),a=i.builtin_themes,l=i.themes,b='
\n

Pick a theme:

\n