mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-06-10 06:14:39 +00:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da79ffe04b | ||
|
|
c348e1f285 | ||
|
|
91d52ed6ae | ||
|
|
06751c68f9 | ||
|
|
a9854e9136 | ||
|
|
039c730409 | ||
|
|
82676862ca | ||
|
|
5b6b61286d | ||
|
|
f32cb4d358 | ||
|
|
ad155f1f17 | ||
|
|
9e1045de9b | ||
|
|
db3d37f6fe | ||
|
|
611f2e30d6 | ||
|
|
1984e4b869 | ||
|
|
f58ea904b3 | ||
|
|
af0f4d20fe | ||
|
|
10b5ce3bcc | ||
|
|
a0287946d9 | ||
|
|
fbd71d55ef | ||
|
|
0ac8437387 | ||
|
|
866b56b682 | ||
|
|
4d87059872 | ||
|
|
5bbe456496 | ||
|
|
5b9cc257a8 | ||
|
|
34b6287e0c | ||
|
|
41ee5fb843 | ||
|
|
ae6b36fa89 | ||
|
|
cfda54a724 | ||
|
|
033169ab08 | ||
|
|
920c435b00 | ||
|
|
27e6aa8a5d | ||
|
|
92633f52ce | ||
|
|
f5f854964b | ||
|
|
55528fdf91 | ||
|
|
9eae13486e | ||
|
|
79bd074dae | ||
|
|
7b0ba2bfe7 | ||
|
|
db17b9d8ac | ||
|
|
b5de82bfcf | ||
|
|
13dbe0434c | ||
|
|
ef0057c23f | ||
|
|
6bc8e1438f | ||
|
|
8856ea9dc4 | ||
|
|
4edb2d269f | ||
|
|
272891470c | ||
|
|
574b3dc74b | ||
|
|
269dd2b618 | ||
|
|
0625e05cbb | ||
|
|
6b1101bc45 | ||
|
|
3e6d0b203f | ||
|
|
8189598dd6 | ||
|
|
4a8b5f2147 | ||
|
|
f9a1ff4dea | ||
|
|
96d88a5e91 | ||
|
|
bdc1c7a80d | ||
|
|
eacfdcd52f | ||
|
|
ed347e2bd0 | ||
|
|
3228e8c204 | ||
|
|
b9c991e3b6 | ||
|
|
8ad12c2379 | ||
|
|
2aa237ef12 | ||
|
|
40496eb9d1 | ||
|
|
ffd19b8162 | ||
|
|
6663568500 | ||
|
|
3a09c47ef0 | ||
|
|
41ab0f36ff | ||
|
|
70e00ac696 | ||
|
|
70369a0b32 | ||
|
|
8c20ffb943 | ||
|
|
729c768dc2 | ||
|
|
17f8c1d1c9 | ||
|
|
964fd07143 | ||
|
|
8553bbd0cb | ||
|
|
f494541652 | ||
|
|
dd6c917462 | ||
|
|
9e03e24764 | ||
|
|
6b5f3ac76f | ||
|
|
a36579bb12 | ||
|
|
e4ce69a967 | ||
|
|
b0e1f37cac | ||
|
|
da659b7526 | ||
|
|
08ecb4d0d2 | ||
|
|
3624962d3c | ||
|
|
b9f1727f1e | ||
|
|
5a7c4da0b1 | ||
|
|
fa2b9d2bee |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,3 +8,6 @@ node_modules/
|
||||
sass/scss
|
||||
*.egg-info/
|
||||
build/
|
||||
.cache/
|
||||
.env*
|
||||
.pytest_cache
|
||||
|
||||
2
.isort.cfg
Normal file
2
.isort.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
multi_line_output=4
|
||||
46
CHANGELOG.md
Normal file
46
CHANGELOG.md
Normal file
@@ -0,0 +1,46 @@
|
||||
[3.2.5](https://github.com/paradoxxxzero/butterfly/compare/3.2.4...3.2.5)
|
||||
=====
|
||||
|
||||
* Fix #155 again (PR #179)
|
||||
|
||||
|
||||
[3.2.4](https://github.com/paradoxxxzero/butterfly/compare/3.2.3...3.2.4)
|
||||
=====
|
||||
|
||||
* Fix up --uri-root-path so behaves as one would expect for this. Fix #155 (PR #173 thanks @GrahamDumpleton)
|
||||
* Fix websocket keepalive. Fix #167 (PR #172 thanks @fzumstein)
|
||||
|
||||
[3.2.3](https://github.com/paradoxxxzero/butterfly/compare/3.2.2...3.2.3)
|
||||
=====
|
||||
|
||||
* Complete support for IME & CJK rendering (#168 thanks @PeterCxy)
|
||||
|
||||
3.2.2
|
||||
=====
|
||||
|
||||
* Fix unescaping entities when linkifying
|
||||
|
||||
3.2.1
|
||||
=====
|
||||
|
||||
* Issue correct X.509 v3 certificates (you will need to re-generate your certs)
|
||||
|
||||
3.1.5
|
||||
=====
|
||||
|
||||
* Fix new option in older tornado version. (#146 thanks @warpkwd)
|
||||
|
||||
3.1.4
|
||||
=====
|
||||
|
||||
* Add --i-hereby-declare-i-dont-want-any-security-whatsoever option (#143)
|
||||
|
||||
3.1.3
|
||||
=====
|
||||
|
||||
* Fix lsof parsing crash on python 2
|
||||
|
||||
3.1.0
|
||||
=====
|
||||
|
||||
* Start a changelog
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:14.04
|
||||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y -q --no-install-recommends \
|
||||
@@ -7,6 +7,9 @@ RUN apt-get update \
|
||||
libssl-dev \
|
||||
python-dev \
|
||||
python-setuptools \
|
||||
ca-certificates \
|
||||
&& easy_install pip \
|
||||
&& pip install --upgrade setuptools \
|
||||
&& apt-get clean \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
butterfly Copyright (C) 2015 Florian Mounier, Kozea
|
||||
butterfly Copyright(C) 2015-2017 Florian Mounier, Kozea
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
|
||||
37
Makefile
Normal file
37
Makefile
Normal file
@@ -0,0 +1,37 @@
|
||||
include Makefile.config
|
||||
-include Makefile.custom.config
|
||||
|
||||
all: install lint check-outdated run-debug
|
||||
|
||||
install:
|
||||
test -d $(VENV) || virtualenv $(VENV) -p $(PYTHON_VERSION)
|
||||
$(PIP) install --upgrade --no-cache pip setuptools -e .[lint,themes] devcore
|
||||
$(NPM) install
|
||||
|
||||
clean:
|
||||
rm -fr $(NODE_MODULES)
|
||||
rm -fr $(VENV)
|
||||
rm -fr *.egg-info
|
||||
|
||||
lint:
|
||||
$(PYTEST) --flake8 -m flake8 $(PROJECT_NAME)
|
||||
$(PYTEST) --isort -m isort $(PROJECT_NAME)
|
||||
|
||||
check-outdated:
|
||||
$(PIP) list --outdated --format=columns
|
||||
|
||||
ARGS ?= --port=1212 --unsecure --debug
|
||||
run-debug:
|
||||
$(PYTHON) ./butterfly.server.py $(ARGS)
|
||||
|
||||
build-coffee:
|
||||
$(NODE_MODULES)/.bin/grunt
|
||||
|
||||
release: build-coffee
|
||||
git pull
|
||||
$(eval VERSION := $(shell PROJECT_NAME=$(PROJECT_NAME) $(VENV)/bin/devcore bump $(LEVEL)))
|
||||
git commit -am "Bump $(VERSION)"
|
||||
git tag $(VERSION)
|
||||
$(PYTHON) setup.py sdist bdist_wheel upload
|
||||
git push
|
||||
git push --tags
|
||||
10
Makefile.config
Normal file
10
Makefile.config
Normal file
@@ -0,0 +1,10 @@
|
||||
PROJECT_NAME = butterfly
|
||||
|
||||
# Python env
|
||||
PYTHON_VERSION ?= python
|
||||
VENV = $(PWD)/.env$(if $(filter $(PYTHON_VERSION),python),,-$(PYTHON_VERSION))
|
||||
PIP = $(VENV)/bin/pip
|
||||
PYTHON = $(VENV)/bin/python
|
||||
PYTEST = $(VENV)/bin/py.test
|
||||
NODE_MODULES = $(PWD)/node_modules
|
||||
NPM = yarn
|
||||
26
README.md
26
README.md
@@ -1,4 +1,4 @@
|
||||
# ƸӜƷ butterfly 2.0
|
||||
# ƸӜƷ butterfly 3.0
|
||||
|
||||

|
||||
|
||||
@@ -12,7 +12,7 @@ Butterfly is a xterm compatible terminal that runs in your browser.
|
||||
|
||||
* xterm compatible (support a lot of unused features!)
|
||||
* Native browser scroll and search
|
||||
* Theming in css / sass [(18 preset themes)](https://github.com/paradoxxxzero/butterfly-themes) endless possibilities!
|
||||
* Theming in css / sass [(20 preset themes)](https://github.com/paradoxxxzero/butterfly-themes) endless possibilities!
|
||||
* HTML in your terminal! cat images and use <table>
|
||||
* Multiple sessions support (à la screen -x) to simultaneously access a terminal from several places on the planet!
|
||||
* Secure authentication with X509 certificates!
|
||||
@@ -26,7 +26,8 @@ Butterfly is a xterm compatible terminal that runs in your browser.
|
||||
|
||||
``` bash
|
||||
$ pip install butterfly
|
||||
$ pip install libsass # If you want to use themes
|
||||
$ pip install butterfly[themes] # If you want to use themes
|
||||
$ pip install butterfly[systemd] # If you want to use systemd
|
||||
$ butterfly
|
||||
```
|
||||
|
||||
@@ -45,6 +46,20 @@ To get an overview of butterfly features.
|
||||
$ butterfly.server.py --host=myhost --port=57575
|
||||
```
|
||||
|
||||
Or with login prompt
|
||||
|
||||
```bash
|
||||
$ butterfly.server.py --host=myhost --port=57575 --login
|
||||
```
|
||||
|
||||
Or with PAM authentication (ROOT required)
|
||||
|
||||
```bash
|
||||
# butterfly.server.py --host=myhost --port=57575 --login --pam_profile=sshd
|
||||
```
|
||||
|
||||
You can change `sshd` to your preferred PAM profile.
|
||||
|
||||
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))
|
||||
|
||||
|
||||
@@ -60,7 +75,8 @@ $ systemctl enable butterfly.socket
|
||||
$ systemctl start butterfly.socket
|
||||
```
|
||||
|
||||
Don't forget to update the /etc/butterfly/butterfly.conf file with your server options (host, port, shell, ...)
|
||||
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.
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -84,7 +100,7 @@ The js part is based on [term.js](https://github.com/chjj/term.js/) which is bas
|
||||
## License
|
||||
|
||||
```
|
||||
butterfly Copyright (C) 2015 Florian Mounier
|
||||
butterfly Copyright (C) 2015-2017 Florian Mounier
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -20,7 +20,11 @@
|
||||
import tornado.options
|
||||
import tornado.ioloop
|
||||
import tornado.httpserver
|
||||
import tornado_systemd
|
||||
try:
|
||||
from tornado_systemd import SystemdHTTPServer as HTTPServer
|
||||
except ImportError:
|
||||
from tornado.httpserver import HTTPServer
|
||||
|
||||
import logging
|
||||
import webbrowser
|
||||
import uuid
|
||||
@@ -40,6 +44,9 @@ tornado.options.define("unminified", default=False,
|
||||
|
||||
tornado.options.define("host", default='localhost', help="Server host")
|
||||
tornado.options.define("port", default=57575, type=int, help="Server port")
|
||||
tornado.options.define("keepalive_interval", default=30, type=int,
|
||||
help="Interval between ping packets sent from server "
|
||||
"to client (in seconds)")
|
||||
tornado.options.define("one_shot", default=False,
|
||||
help="Run a one-shot instance. Quit at term close")
|
||||
tornado.options.define("shell", help="Shell to execute at login")
|
||||
@@ -48,8 +55,18 @@ tornado.options.define("cmd",
|
||||
help="Command to run instead of shell, f.i.: 'ls -l'")
|
||||
tornado.options.define("unsecure", default=False,
|
||||
help="Don't use ssl not recommended")
|
||||
tornado.options.define("i_hereby_declare_i_dont_want_any_security_whatsoever",
|
||||
default=False,
|
||||
help="Remove all security and warnings. There are some "
|
||||
"use cases for that. Use this if you really know what "
|
||||
"you are doing.")
|
||||
tornado.options.define("login", default=False,
|
||||
help="Use login screen at start")
|
||||
tornado.options.define("pam_profile", default="", type=str,
|
||||
help="When --login=True provided and running as ROOT, "
|
||||
"use PAM with the specified PAM profile for "
|
||||
"authentication and then execute the user's default "
|
||||
"shell. Will override --shell.")
|
||||
tornado.options.define("force_unicode_width",
|
||||
default=False,
|
||||
help="Force all unicode characters to the same width."
|
||||
@@ -68,6 +85,7 @@ tornado.options.define("uri_root_path", default='',
|
||||
help="Sets the servier root path: "
|
||||
"example.com/<uri_root_path>/static/")
|
||||
|
||||
|
||||
if os.getuid() == 0:
|
||||
ev = os.getenv('XDG_CONFIG_DIRS', '/etc')
|
||||
else:
|
||||
@@ -80,17 +98,6 @@ butterfly_dir = os.path.join(ev, 'butterfly')
|
||||
conf_file = os.path.join(butterfly_dir, 'butterfly.conf')
|
||||
ssl_dir = os.path.join(butterfly_dir, 'ssl')
|
||||
|
||||
if not os.path.exists(conf_file):
|
||||
try:
|
||||
import butterfly
|
||||
shutil.copy(
|
||||
os.path.join(
|
||||
os.path.abspath(os.path.dirname(butterfly.__file__)),
|
||||
'butterfly.conf.default'), conf_file)
|
||||
print('butterfly.conf installed in %s' % conf_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
tornado.options.define("conf", default=conf_file,
|
||||
help="Butterfly configuration file. "
|
||||
"Contains the same options as command line.")
|
||||
@@ -107,6 +114,21 @@ if os.path.exists(tornado.options.options.conf):
|
||||
# Do it again to overwrite conf with args
|
||||
tornado.options.parse_command_line()
|
||||
|
||||
# For next time, create them a conf file from template.
|
||||
# Need to do this after parsing options so we do not trigger
|
||||
# code import for butterfly module, in case that code is
|
||||
# dependent on the set of parsed options.
|
||||
if not os.path.exists(conf_file):
|
||||
try:
|
||||
import butterfly
|
||||
shutil.copy(
|
||||
os.path.join(
|
||||
os.path.abspath(os.path.dirname(butterfly.__file__)),
|
||||
'butterfly.conf.default'), conf_file)
|
||||
print('butterfly.conf installed in %s' % conf_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
options = tornado.options.options
|
||||
|
||||
for logger in ('tornado.access', 'tornado.application',
|
||||
@@ -123,6 +145,9 @@ log = logging.getLogger('butterfly')
|
||||
host = options.host
|
||||
port = options.port
|
||||
|
||||
if options.i_hereby_declare_i_dont_want_any_security_whatsoever:
|
||||
options.unsecure = True
|
||||
|
||||
|
||||
if not os.path.exists(options.ssl_dir):
|
||||
os.makedirs(options.ssl_dir)
|
||||
@@ -131,6 +156,7 @@ if not os.path.exists(options.ssl_dir):
|
||||
def to_abs(file):
|
||||
return os.path.join(options.ssl_dir, file)
|
||||
|
||||
|
||||
ca, ca_key, cert, cert_key, pkcs12 = map(to_abs, [
|
||||
'butterfly_ca.crt', 'butterfly_ca.key',
|
||||
'butterfly_%s.crt', 'butterfly_%s.key',
|
||||
@@ -156,6 +182,10 @@ def read(file):
|
||||
with open(file, 'rb') as fd:
|
||||
return fd.read()
|
||||
|
||||
def b(s):
|
||||
return s.encode('utf-8')
|
||||
|
||||
|
||||
if options.generate_certs:
|
||||
from OpenSSL import crypto
|
||||
print('Generating certificates for %s (change it with --host)\n' % host)
|
||||
@@ -165,6 +195,7 @@ if options.generate_certs:
|
||||
ca_pk = crypto.PKey()
|
||||
ca_pk.generate_key(crypto.TYPE_RSA, 2048)
|
||||
ca_cert = crypto.X509()
|
||||
ca_cert.set_version(2)
|
||||
ca_cert.get_subject().CN = 'Butterfly CA on %s' % socket.gethostname()
|
||||
fill_fields(ca_cert.get_subject())
|
||||
ca_cert.set_serial_number(uuid.uuid4().int)
|
||||
@@ -172,6 +203,21 @@ if options.generate_certs:
|
||||
ca_cert.gmtime_adj_notAfter(315360000) # to 10y
|
||||
ca_cert.set_issuer(ca_cert.get_subject()) # Self signed
|
||||
ca_cert.set_pubkey(ca_pk)
|
||||
ca_cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b('basicConstraints'), True, b('CA:TRUE, pathlen:0')),
|
||||
crypto.X509Extension(
|
||||
b('keyUsage'), True, b('keyCertSign, cRLSign')),
|
||||
crypto.X509Extension(
|
||||
b('subjectKeyIdentifier'), False, b('hash'), subject=ca_cert),
|
||||
])
|
||||
ca_cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b('authorityKeyIdentifier'), False,
|
||||
b('issuer:always, keyid:always'),
|
||||
issuer=ca_cert, subject=ca_cert
|
||||
)
|
||||
])
|
||||
ca_cert.sign(ca_pk, 'sha512')
|
||||
|
||||
write(ca, crypto.dump_certificate(crypto.FILETYPE_PEM, ca_cert))
|
||||
@@ -185,7 +231,23 @@ if options.generate_certs:
|
||||
server_pk = crypto.PKey()
|
||||
server_pk.generate_key(crypto.TYPE_RSA, 2048)
|
||||
server_cert = crypto.X509()
|
||||
server_cert.set_version(2)
|
||||
server_cert.get_subject().CN = host
|
||||
server_cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b('basicConstraints'), False, b('CA:FALSE')),
|
||||
crypto.X509Extension(
|
||||
b('subjectKeyIdentifier'), False, b('hash'), subject=server_cert),
|
||||
crypto.X509Extension(
|
||||
b('subjectAltName'), False, b('DNS:%s' % host)),
|
||||
])
|
||||
server_cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b('authorityKeyIdentifier'), False,
|
||||
b('issuer:always, keyid:always'),
|
||||
issuer=ca_cert, subject=ca_cert
|
||||
)
|
||||
])
|
||||
fill_fields(server_cert.get_subject())
|
||||
server_cert.set_serial_number(uuid.uuid4().int)
|
||||
server_cert.gmtime_adj_notBefore(0) # From now
|
||||
@@ -235,6 +297,7 @@ if (options.generate_current_user_pkcs or
|
||||
client_pk.generate_key(crypto.TYPE_RSA, 2048)
|
||||
|
||||
client_cert = crypto.X509()
|
||||
client_cert.set_version(2)
|
||||
client_cert.get_subject().CN = user
|
||||
fill_fields(client_cert.get_subject())
|
||||
client_cert.set_serial_number(uuid.uuid4().int)
|
||||
@@ -298,12 +361,10 @@ else:
|
||||
from butterfly import application
|
||||
application.butterfly_dir = butterfly_dir
|
||||
log.info('Starting server')
|
||||
http_server = tornado_systemd.SystemdHTTPServer(
|
||||
application,
|
||||
ssl_options=ssl_opts)
|
||||
http_server = HTTPServer(application, ssl_options=ssl_opts)
|
||||
http_server.listen(port, address=host)
|
||||
|
||||
if http_server.systemd:
|
||||
if getattr(http_server, 'systemd', False):
|
||||
os.environ.pop('LISTEN_PID')
|
||||
os.environ.pop('LISTEN_FDS')
|
||||
|
||||
@@ -314,8 +375,10 @@ ioloop = tornado.ioloop.IOLoop.instance()
|
||||
if port == 0:
|
||||
port = list(http_server._sockets.values())[0].getsockname()[1]
|
||||
|
||||
url = "http%s://%s:%d/" % (
|
||||
"s" if not options.unsecure else "", host, port)
|
||||
url = "http%s://%s:%d/%s" % (
|
||||
"s" if not options.unsecure else "", host, port,
|
||||
(options.uri_root_path.strip('/') + '/') if options.uri_root_path else ''
|
||||
)
|
||||
|
||||
if not options.one_shot or not webbrowser.open(url):
|
||||
log.warn('Butterfly is ready, open your browser to: %s' % url)
|
||||
|
||||
15
butterfly/__about__.py
Normal file
15
butterfly/__about__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
__title__ = "butterfly"
|
||||
__version__ = "3.2.5"
|
||||
|
||||
__summary__ = "A sleek web based terminal emulator"
|
||||
__uri__ = "https://github.com/paradoxxxzero/butterfly"
|
||||
__author__ = "Florian Mounier"
|
||||
__email__ = "paradoxxx.zero@gmail.com"
|
||||
|
||||
__license__ = "GPLv3"
|
||||
__copyright__ = "Copyright 2017 %s" % __author__
|
||||
|
||||
__all__ = [
|
||||
'__title__', '__version__', '__summary__', '__uri__', '__author__',
|
||||
'__email__', '__license__', '__copyright__'
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -14,8 +14,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
__version__ = '3.0.0-alpha'
|
||||
|
||||
from .__about__ import * # noqa: F401,F403
|
||||
|
||||
import os
|
||||
import tornado.web
|
||||
@@ -23,6 +22,7 @@ import tornado.options
|
||||
import tornado.web
|
||||
from logging import getLogger
|
||||
|
||||
|
||||
log = getLogger('butterfly')
|
||||
|
||||
|
||||
@@ -31,10 +31,15 @@ class url(object):
|
||||
self.url = url
|
||||
|
||||
def __call__(self, cls):
|
||||
if tornado.options.options.uri_root_path:
|
||||
url = '/' + tornado.options.options.uri_root_path.strip('/') + self.url
|
||||
else:
|
||||
url = self.url
|
||||
application.add_handlers(
|
||||
r'.*$',
|
||||
(tornado.web.url(self.url, cls, name=cls.__name__),)
|
||||
(tornado.web.url(url, cls, name=cls.__name__),)
|
||||
)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
@@ -73,8 +78,8 @@ if hasattr(tornado.options.options, 'debug'):
|
||||
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
||||
debug=tornado.options.options.debug,
|
||||
static_url_prefix='%s/static/' % (
|
||||
'/%s' % tornado.options.options.uri_root_path
|
||||
'/%s' % tornado.options.options.uri_root_path.strip('/')
|
||||
if tornado.options.options.uri_root_path else '')
|
||||
)
|
||||
|
||||
import butterfly.routes
|
||||
import butterfly.routes # noqa: F401
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from butterfly.escapes import image
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly cat wrapper.')
|
||||
parser.add_argument('-o', action="store_true",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly terminal color tester.')
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Butterfly terminal color tester.')
|
||||
parser.add_argument(
|
||||
'--colors',
|
||||
default='16',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import butterfly
|
||||
from butterfly.escapes import image
|
||||
from butterfly.utils import ansi_colors
|
||||
import os
|
||||
import butterfly
|
||||
import base64
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
|
||||
path = os.getenv('BUTTERFLY_PATH')
|
||||
@@ -58,7 +58,6 @@ Butterfly is a xterm compliant terminal built with python and javascript.
|
||||
code=ansi_colors.light_yellow,
|
||||
comment=ansi_colors.light_magenta,
|
||||
reset=ansi_colors.reset,
|
||||
rcol=int(subprocess.check_output(['stty','size']).split()[1]) - 31,
|
||||
rcol=int(subprocess.check_output(['stty', 'size']).split()[1]) - 31,
|
||||
main=os.path.normpath(os.path.join(
|
||||
os.path.abspath(os.path.dirname(butterfly.__file__)),
|
||||
'sass'))))
|
||||
os.path.abspath(os.path.dirname(butterfly.__file__)), 'sass'))))
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
from butterfly.escapes import html
|
||||
import argparse
|
||||
import fileinput
|
||||
import sys
|
||||
|
||||
from butterfly.escapes import html
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Butterfly html converter.\n\n"
|
||||
"Output in html standard input.\n"
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import os
|
||||
import webbrowser
|
||||
import argparse
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||
except ImportError:
|
||||
from urlparse import urlparse, parse_qs, urlunparse
|
||||
from urllib import urlencode
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly tab opener.')
|
||||
parser.add_argument(
|
||||
@@ -11,6 +18,10 @@ parser.add_argument(
|
||||
help='Directory to open the new tab in. (Defaults to current)')
|
||||
args = parser.parse_args()
|
||||
|
||||
url = '%swd%s' % (os.getenv('LOCATION', '/'), os.path.abspath(args.location))
|
||||
url_parts = urlparse(os.getenv('LOCATION', '/'))
|
||||
query = parse_qs(url_parts.query)
|
||||
query['path'] = os.path.abspath(args.location)
|
||||
|
||||
url = urlunparse(url_parts._replace(path='')._replace(query=urlencode(query)))
|
||||
if not webbrowser.open(url):
|
||||
print('Unable to open browser, please go to %s' % url)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import os
|
||||
import webbrowser
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly session opener.')
|
||||
parser.add_argument(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from contextlib import contextmanager
|
||||
from butterfly.utils import ansi_colors as colors
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from contextlib import contextmanager
|
||||
|
||||
from butterfly.utils import ansi_colors as colors # noqa: F401
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -60,7 +61,7 @@ def geolocation():
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != 'R':
|
||||
loc += rv
|
||||
except:
|
||||
except Exception:
|
||||
return
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
|
||||
192
butterfly/pam.py
Normal file
192
butterfly/pam.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# (c) 2007 Chris AtLee <chris@atlee.ca>
|
||||
# Licensed under the MIT license:
|
||||
# http://www.opensource.org/licenses/mit-license.php
|
||||
#
|
||||
# Original author: Chris AtLee
|
||||
#
|
||||
# Modified by David Ford, 2011-12-6
|
||||
# added py3 support and encoding
|
||||
# added pam_end
|
||||
# added pam_setcred to reset credentials after seeing Leon Walker's remarks
|
||||
# added byref as well
|
||||
# use readline to prestuff the getuser input
|
||||
# Modified by Peter Cai, 2017-02-10
|
||||
# interactive login for Butterfly
|
||||
|
||||
'''
|
||||
PAM module for python
|
||||
Provides an authenticate function that will allow the caller to authenticate
|
||||
a user against the Pluggable Authentication Modules (PAM) on the system.
|
||||
Implemented using ctypes, so no compilation is necessary.
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
from ctypes import (
|
||||
CDLL, CFUNCTYPE, POINTER, Structure, byref, c_char_p, c_int, c_size_t,
|
||||
c_void_p)
|
||||
from ctypes.util import find_library
|
||||
|
||||
|
||||
class PamHandle(Structure):
|
||||
"""wrapper class for pam_handle_t pointer"""
|
||||
_fields_ = [("handle", c_void_p)]
|
||||
|
||||
def __init__(self):
|
||||
Structure.__init__(self)
|
||||
self.handle = 0
|
||||
|
||||
|
||||
class PamMessage(Structure):
|
||||
"""wrapper class for pam_message structure"""
|
||||
_fields_ = [("msg_style", c_int), ("msg", c_char_p)]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
|
||||
|
||||
|
||||
class PamResponse(Structure):
|
||||
"""wrapper class for pam_response structure"""
|
||||
_fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
|
||||
|
||||
|
||||
conv_func = CFUNCTYPE(
|
||||
c_int, c_int, POINTER(POINTER(PamMessage)),
|
||||
POINTER(POINTER(PamResponse)), c_void_p)
|
||||
|
||||
|
||||
class PamConv(Structure):
|
||||
"""wrapper class for pam_conv structure"""
|
||||
_fields_ = [("conv", conv_func), ("appdata_ptr", c_void_p)]
|
||||
|
||||
|
||||
# Various constants
|
||||
PAM_PROMPT_ECHO_OFF = 1
|
||||
PAM_PROMPT_ECHO_ON = 2
|
||||
PAM_ERROR_MSG = 3
|
||||
PAM_TEXT_INFO = 4
|
||||
PAM_REINITIALIZE_CRED = 8
|
||||
|
||||
libc = CDLL(find_library("c"))
|
||||
libpam = CDLL(find_library("pam"))
|
||||
libpam_misc = CDLL(find_library("pam_misc"))
|
||||
|
||||
calloc = libc.calloc
|
||||
calloc.restype = c_void_p
|
||||
calloc.argtypes = [c_size_t, c_size_t]
|
||||
|
||||
pam_end = libpam.pam_end
|
||||
pam_end.restype = c_int
|
||||
pam_end.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_start = libpam.pam_start
|
||||
pam_start.restype = c_int
|
||||
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
|
||||
|
||||
pam_setcred = libpam.pam_setcred
|
||||
pam_setcred.restype = c_int
|
||||
pam_setcred.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_strerror = libpam.pam_strerror
|
||||
pam_strerror.restype = c_char_p
|
||||
pam_strerror.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_authenticate = libpam.pam_authenticate
|
||||
pam_authenticate.restype = c_int
|
||||
pam_authenticate.argtypes = [PamHandle, c_int]
|
||||
|
||||
misc_conv = libpam_misc.misc_conv
|
||||
|
||||
|
||||
class PAM():
|
||||
code = 0
|
||||
reason = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def authenticate(
|
||||
self, username,
|
||||
service='login', encoding='utf-8', resetcreds=True):
|
||||
"""PAM authentication through standard input for the given service.
|
||||
Returns True for success, or False for failure.
|
||||
self.code (integer) and self.reason (string) are always stored
|
||||
and may be referenced for the reason why authentication failed.
|
||||
0/'Success' will be stored for success.
|
||||
Python3 expects bytes() for ctypes inputs. This function will make
|
||||
necessary conversions using the supplied encoding.
|
||||
Inputs:
|
||||
username: username to authenticate
|
||||
service: PAM service to authenticate against, defaults to 'login'
|
||||
Returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
# python3 ctypes prefers bytes
|
||||
if sys.version_info >= (3,):
|
||||
if isinstance(username, str):
|
||||
username = username.encode(encoding)
|
||||
if isinstance(service, str):
|
||||
service = service.encode(encoding)
|
||||
else:
|
||||
if isinstance(username, unicode): # noqa: F821
|
||||
username = username.encode(encoding)
|
||||
if isinstance(service, unicode): # noqa: F821
|
||||
service = service.encode(encoding)
|
||||
|
||||
if b'\x00' in username or b'\x00' in service:
|
||||
self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM
|
||||
self.reason = 'strings may not contain NUL'
|
||||
return False
|
||||
|
||||
handle = PamHandle()
|
||||
conv = PamConv(conv_func(misc_conv), 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
if retval != 0:
|
||||
# This is not an authentication error,
|
||||
# something has gone wrong starting up PAM
|
||||
self.code = retval
|
||||
self.reason = "pam_start() failed"
|
||||
return False
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
auth_success = retval == 0
|
||||
|
||||
if auth_success and resetcreds:
|
||||
retval = pam_setcred(handle, PAM_REINITIALIZE_CRED)
|
||||
|
||||
# store information to inform the caller why we failed
|
||||
self.code = retval
|
||||
self.reason = pam_strerror(handle, retval)
|
||||
if sys.version_info >= (3,):
|
||||
self.reason = self.reason.decode(encoding)
|
||||
|
||||
pam_end(handle, retval)
|
||||
|
||||
return auth_success
|
||||
|
||||
|
||||
def login_prompt(username, profile, env):
|
||||
pam = PAM()
|
||||
|
||||
success = pam.authenticate(username, profile)
|
||||
print('{} {}'.format(pam.code, pam.reason))
|
||||
|
||||
if success:
|
||||
su = '/usr/bin/su'
|
||||
if not os.path.exists(su):
|
||||
su = '/bin/su'
|
||||
os.execvpe(su, [su, '-l', username], env)
|
||||
return success
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if login_prompt(sys.argv[1], sys.argv[2], os.environ):
|
||||
exit(0)
|
||||
else:
|
||||
exit(1)
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -18,15 +18,20 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from mimetypes import guess_type
|
||||
from uuid import uuid4
|
||||
|
||||
import tornado.escape
|
||||
import tornado.options
|
||||
import tornado.process
|
||||
import tornado.escape
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
from mimetypes import guess_type
|
||||
from collections import defaultdict
|
||||
from butterfly import url, Route, utils, __version__
|
||||
|
||||
from butterfly import Route, url, utils
|
||||
from butterfly.terminal import Terminal
|
||||
|
||||
|
||||
@@ -36,12 +41,15 @@ def u(s):
|
||||
return s
|
||||
|
||||
|
||||
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?/?(?:session/(.+))?')
|
||||
@url(r'/(?:session/(?P<session>[^/]+)/?)?')
|
||||
class Index(Route):
|
||||
def get(self, user, path, session):
|
||||
def get(self, session):
|
||||
user = self.request.query_arguments.get(
|
||||
'user', [b''])[0].decode('utf-8')
|
||||
if not tornado.options.options.unsecure and user:
|
||||
raise tornado.web.HTTPError(400)
|
||||
return self.render('index.html')
|
||||
return self.render(
|
||||
'index.html', session=session or str(uuid4()))
|
||||
|
||||
|
||||
@url(r'/theme/([^/]+)/style.css')
|
||||
@@ -101,7 +109,19 @@ class ThemeStatic(Route):
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
if os.path.exists(fn):
|
||||
self.set_header("Content-Type", guess_type(fn)[0])
|
||||
type = guess_type(fn)[0]
|
||||
if type is None:
|
||||
# Fallback if there's no mimetypes on the system
|
||||
type = {
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'woff': 'application/font-woff',
|
||||
'ttf': 'application/x-font-ttf'
|
||||
}.get(fn.split('.')[-1], 'text/plain')
|
||||
|
||||
self.set_header("Content-Type", type)
|
||||
with open(fn, 'rb') as s:
|
||||
while True:
|
||||
data = s.read(16384)
|
||||
@@ -113,26 +133,48 @@ class ThemeStatic(Route):
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
|
||||
class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
keepalive_timer = None
|
||||
|
||||
def open(self, *args, **kwargs):
|
||||
self.keepalive_timer = tornado.ioloop.PeriodicCallback(
|
||||
self.send_ping, tornado.options.options.keepalive_interval * 1000)
|
||||
self.keepalive_timer.start()
|
||||
|
||||
def send_ping(self):
|
||||
t = int(time.time())
|
||||
frame = struct.pack('<I', t) # A ping frame based on time
|
||||
self.log.info("Sending ping frame %s" % t)
|
||||
try:
|
||||
self.ping(frame)
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
self.keepalive_timer.stop()
|
||||
|
||||
def on_close(self):
|
||||
if self.keepalive_timer is not None:
|
||||
self.keepalive_timer.stop()
|
||||
|
||||
|
||||
@url(r'/ctl/session/(?P<session>[^/]+)')
|
||||
class TermCtlWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
class TermCtlWebSocket(Route, KeptAliveWebSocketHandler):
|
||||
sessions = defaultdict(list)
|
||||
sessions_secure_users = {}
|
||||
|
||||
def open(self, session):
|
||||
super(TermCtlWebSocket, self).open(session)
|
||||
self.session = session
|
||||
self.closed = False
|
||||
self.log.info('Websocket /ctl opened %r' % self)
|
||||
|
||||
def create_terminal(self):
|
||||
socket = utils.Socket(self.ws_connection.stream.socket)
|
||||
opts = tornado.options.options
|
||||
user = self.request.query_arguments.get(
|
||||
'user', [b''])[0].decode('utf-8')
|
||||
path = self.request.query_arguments.get(
|
||||
'path', [b''])[0].decode('utf-8')
|
||||
secure_user = None
|
||||
|
||||
if not opts.unsecure:
|
||||
if not tornado.options.options.unsecure:
|
||||
user = utils.parse_cert(
|
||||
self.ws_connection.stream.socket.getpeercert())
|
||||
assert user, 'No user in certificate'
|
||||
@@ -205,6 +247,7 @@ class TermCtlWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
self.broadcast(self.session, message, self)
|
||||
|
||||
def on_close(self):
|
||||
super(TermCtlWebSocket, self).on_close()
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
@@ -212,9 +255,8 @@ class TermCtlWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
if self in self.sessions[self.session]:
|
||||
self.sessions[self.session].remove(self)
|
||||
|
||||
opts = tornado.options.options
|
||||
if opts.one_shot or (
|
||||
self.application.systemd and
|
||||
if tornado.options.options.one_shot or (
|
||||
getattr(self.application, 'systemd', False) and
|
||||
not sum([
|
||||
len(wsockets)
|
||||
for session, wsockets in self.sessions.items()])):
|
||||
@@ -222,7 +264,7 @@ class TermCtlWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
|
||||
|
||||
@url(r'/ws/session/(?P<session>[^/]+)')
|
||||
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
class TermWebSocket(Route, KeptAliveWebSocketHandler):
|
||||
# List of websockets per session
|
||||
sessions = defaultdict(list)
|
||||
|
||||
@@ -233,6 +275,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
history = {}
|
||||
|
||||
def open(self, session):
|
||||
super(TermWebSocket, self).open(session)
|
||||
self.set_nodelay(True)
|
||||
self.session = session
|
||||
self.closed = False
|
||||
@@ -242,14 +285,19 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
|
||||
@classmethod
|
||||
def close_session(cls, session):
|
||||
wsockets = (cls.sessions.get(session) +
|
||||
TermCtlWebSocket.sessions.get(session))
|
||||
wsockets = (cls.sessions.get(session, []) +
|
||||
TermCtlWebSocket.sessions.get(session, []))
|
||||
for wsocket in wsockets:
|
||||
wsocket.on_close()
|
||||
|
||||
wsocket.close()
|
||||
del cls.sessions[session]
|
||||
del TermCtlWebSocket.sessions_secure_users[session]
|
||||
del TermCtlWebSocket.sessions[session]
|
||||
|
||||
if session in cls.sessions:
|
||||
del cls.sessions[session]
|
||||
if session in TermCtlWebSocket.sessions_secure_users:
|
||||
del TermCtlWebSocket.sessions_secure_users[session]
|
||||
if session in TermCtlWebSocket.sessions:
|
||||
del TermCtlWebSocket.sessions[session]
|
||||
|
||||
@classmethod
|
||||
def broadcast(cls, session, message, emitter=None):
|
||||
@@ -270,6 +318,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
Terminal.sessions[self.session].write(message)
|
||||
|
||||
def on_close(self):
|
||||
super(TermWebSocket, self).on_close()
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -23,7 +23,7 @@ $weights: (ExtraLight 100) (Light 300) (Regular 400) (Medium 500) (Semibold 600)
|
||||
|
||||
@font-face
|
||||
font-family: "SourceCodePro"
|
||||
src: url("/static/fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
|
||||
src: url("fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
|
||||
font-weight: nth($weight, 2)
|
||||
|
||||
body
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -22,6 +22,7 @@ html, body
|
||||
color: $fg
|
||||
|
||||
body
|
||||
padding-bottom: .5em
|
||||
white-space: nowrap
|
||||
overflow-x: hidden
|
||||
overflow-y: scroll
|
||||
@@ -35,11 +36,12 @@ body
|
||||
background-color: $active-bg
|
||||
|
||||
.line.extended
|
||||
overflow-x: auto
|
||||
overflow-x: overlay
|
||||
cursor: zoom-in
|
||||
background-image: linear-gradient(90deg, rgba(darken($bg, 3%), 0), 95%, darken($bg, 3%))
|
||||
|
||||
.extra
|
||||
display: none
|
||||
|
||||
&:not(.expanded):hover
|
||||
background-color: lighten($bg, 2%)
|
||||
|
||||
@@ -49,19 +51,9 @@ body
|
||||
|
||||
.extra
|
||||
display: block
|
||||
white-space: pre-line
|
||||
white-space: pre-wrap
|
||||
word-break: break-all
|
||||
|
||||
&::-webkit-scrollbar
|
||||
background: rgba($scroll-bg, .1)
|
||||
height: 0
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: rgba($scroll-fg, .1)
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover
|
||||
background: rgba($scroll-fg-hover, .1)
|
||||
|
||||
&::-webkit-scrollbar
|
||||
background: $scroll-bg
|
||||
width: $scroll-width
|
||||
@@ -101,5 +93,20 @@ body
|
||||
padding: .5em
|
||||
font-size: .75em
|
||||
|
||||
#input-view
|
||||
position: fixed
|
||||
z-index: 100
|
||||
padding: 0
|
||||
margin: 0
|
||||
text-decoration: underline
|
||||
|
||||
#input-helper
|
||||
position: fixed
|
||||
z-index: -100
|
||||
opacity: 0
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
resize: none
|
||||
|
||||
.terminal
|
||||
outline: none
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
(function() {
|
||||
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, first, linkify, nextLeaf, popup, previousLeaf, selection, setAlarm, virtualInput, walk,
|
||||
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,
|
||||
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; };
|
||||
|
||||
clean_ansi = function(data) {
|
||||
@@ -121,13 +121,14 @@
|
||||
});
|
||||
|
||||
addEventListener('copy', copy = function(e) {
|
||||
var data, end, j, len1, line, ref, sel;
|
||||
var data, end, j, len, line, ref, sel;
|
||||
document.getElementsByTagName('body')[0].contentEditable = false;
|
||||
butterfly.bell("copied");
|
||||
e.clipboardData.clearData();
|
||||
sel = getSelection().toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ');
|
||||
data = '';
|
||||
ref = sel.split('\n');
|
||||
for (j = 0, len1 = ref.length; j < len1; j++) {
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
line = ref[j];
|
||||
if (line.slice(-1) === '\u23CE') {
|
||||
end = '';
|
||||
@@ -143,6 +144,7 @@
|
||||
|
||||
addEventListener('paste', function(e) {
|
||||
var data, send, size;
|
||||
document.getElementsByTagName('body')[0].contentEditable = false;
|
||||
butterfly.bell("pasted");
|
||||
data = e.clipboardData.getData('text/plain');
|
||||
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r');
|
||||
@@ -164,37 +166,29 @@
|
||||
}
|
||||
});
|
||||
|
||||
Terminal.on('change', function(lines) {
|
||||
var j, len1, line, results;
|
||||
results = [];
|
||||
for (j = 0, len1 = lines.length; j < len1; j++) {
|
||||
line = lines[j];
|
||||
if (indexOf.call(line.classList, 'extended') >= 0) {
|
||||
results.push(line.addEventListener('click', (function(line) {
|
||||
return function() {
|
||||
var after, before;
|
||||
if (indexOf.call(line.classList, 'expanded') >= 0) {
|
||||
return line.classList.remove('expanded');
|
||||
} else {
|
||||
before = line.getBoundingClientRect().height;
|
||||
line.classList.add('expanded');
|
||||
after = line.getBoundingClientRect().height;
|
||||
return document.body.scrollTop += after - before;
|
||||
}
|
||||
};
|
||||
})(line)));
|
||||
} else {
|
||||
results.push(void 0);
|
||||
}
|
||||
Terminal.on('change', function(line) {
|
||||
if (indexOf.call(line.classList, 'extended') >= 0) {
|
||||
return line.addEventListener('click', (function(line) {
|
||||
return function() {
|
||||
var after, before;
|
||||
if (indexOf.call(line.classList, 'expanded') >= 0) {
|
||||
return line.classList.remove('expanded');
|
||||
} else {
|
||||
before = line.getBoundingClientRect().height;
|
||||
line.classList.add('expanded');
|
||||
after = line.getBoundingClientRect().height;
|
||||
return document.body.scrollTop += after - before;
|
||||
}
|
||||
};
|
||||
})(line));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
walk = function(node, callback) {
|
||||
var child, j, len1, ref, results;
|
||||
var child, j, len, ref, results;
|
||||
ref = node.childNodes;
|
||||
results = [];
|
||||
for (j = 0, len1 = ref.length; j < len1; j++) {
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
child = ref[j];
|
||||
callback.call(child);
|
||||
results.push(walk(child, callback));
|
||||
@@ -210,35 +204,123 @@
|
||||
return text.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>');
|
||||
};
|
||||
|
||||
Terminal.on('change', function(lines) {
|
||||
var j, len1, line, results;
|
||||
results = [];
|
||||
for (j = 0, len1 = lines.length; j < len1; j++) {
|
||||
line = lines[j];
|
||||
results.push(walk(line, function() {
|
||||
var linkified, newNode;
|
||||
if (this.nodeType === 3) {
|
||||
linkified = linkify(this.nodeValue);
|
||||
if (linkified !== this.nodeValue) {
|
||||
newNode = document.createElement('span');
|
||||
newNode.innerHTML = linkified;
|
||||
this.parentElement.replaceChild(newNode, this);
|
||||
return true;
|
||||
}
|
||||
tags = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
escape = function(s) {
|
||||
return s.replace(/[&<>]/g, function(tag) {
|
||||
return tags[tag] || tag;
|
||||
});
|
||||
};
|
||||
|
||||
Terminal.on('change', function(line) {
|
||||
return walk(line, function() {
|
||||
var linkified, newNode, val;
|
||||
if (this.nodeType === 3) {
|
||||
val = this.nodeValue;
|
||||
linkified = linkify(escape(val));
|
||||
if (linkified !== val) {
|
||||
newNode = document.createElement('span');
|
||||
newNode.innerHTML = linkified;
|
||||
this.parentElement.replaceChild(newNode, this);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ctrl = false;
|
||||
|
||||
alt = false;
|
||||
|
||||
addEventListener('touchstart', function(e) {
|
||||
if (e.touches.length === 2) {
|
||||
return ctrl = true;
|
||||
} else if (e.touches.length === 3) {
|
||||
ctrl = false;
|
||||
return alt = true;
|
||||
} else if (e.touches.length === 4) {
|
||||
ctrl = true;
|
||||
return alt = true;
|
||||
}
|
||||
});
|
||||
|
||||
window.mobileKeydown = function(e) {
|
||||
var _altKey, _ctrlKey, _keyCode;
|
||||
if (ctrl || alt) {
|
||||
_ctrlKey = ctrl;
|
||||
_altKey = alt;
|
||||
_keyCode = e.keyCode;
|
||||
if (e.keyCode >= 97 && e.keyCode <= 122) {
|
||||
_keyCode -= 32;
|
||||
}
|
||||
e = new KeyboardEvent('keydown', {
|
||||
ctrlKey: _ctrlKey,
|
||||
altKey: _altKey,
|
||||
keyCode: _keyCode
|
||||
});
|
||||
ctrl = alt = false;
|
||||
setTimeout(function() {
|
||||
return window.dispatchEvent(e);
|
||||
}, 0);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!(e.altKey && e.keyCode === 79)) {
|
||||
return true;
|
||||
}
|
||||
open(location.href);
|
||||
open(location.origin);
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
tid = null;
|
||||
|
||||
packSize = 1000;
|
||||
|
||||
histSize = 100;
|
||||
|
||||
maybePack = function() {
|
||||
var hist, i, j, pack, packfrag, ref;
|
||||
if (!(butterfly.term.childElementCount > packSize + butterfly.rows)) {
|
||||
return;
|
||||
}
|
||||
hist = document.getElementById('packed');
|
||||
packfrag = document.createDocumentFragment('fragment');
|
||||
for (i = j = 0, ref = packSize; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
|
||||
packfrag.appendChild(butterfly.term.firstChild);
|
||||
}
|
||||
pack = document.createElement('div');
|
||||
pack.classList.add('pack');
|
||||
pack.appendChild(packfrag);
|
||||
hist.appendChild(pack);
|
||||
if (hist.childElementCount > histSize) {
|
||||
hist.firstChild.remove();
|
||||
}
|
||||
return tid = setTimeout(maybePack);
|
||||
};
|
||||
|
||||
Terminal.on('refresh', function() {
|
||||
if (tid) {
|
||||
clearTimeout(tid);
|
||||
}
|
||||
return maybePack();
|
||||
});
|
||||
|
||||
Terminal.on('clear', function() {
|
||||
var hist, newHist;
|
||||
newHist = document.createElement('div');
|
||||
newHist.id = 'packed';
|
||||
hist = document.getElementById('packed');
|
||||
return butterfly.body.replaceChild(newHist, hist);
|
||||
});
|
||||
|
||||
Popup = (function() {
|
||||
function Popup() {
|
||||
this.el = document.getElementById('popup');
|
||||
@@ -249,9 +331,6 @@
|
||||
Popup.prototype.open = function(html) {
|
||||
this.el.innerHTML = html;
|
||||
this.el.classList.remove('hidden');
|
||||
if (typeof InstallTrigger !== "undefined") {
|
||||
document.body.contentEditable = 'false';
|
||||
}
|
||||
addEventListener('click', this.bound_click_maybe_close);
|
||||
return addEventListener('keydown', this.bound_key_maybe_close);
|
||||
};
|
||||
@@ -259,9 +338,6 @@
|
||||
Popup.prototype.close = function() {
|
||||
removeEventListener('click', this.bound_click_maybe_close);
|
||||
removeEventListener('keydown', this.bound_key_maybe_close);
|
||||
if (typeof InstallTrigger !== "undefined") {
|
||||
document.body.contentEditable = 'true';
|
||||
}
|
||||
this.el.classList.add('hidden');
|
||||
return this.el.innerHTML = '';
|
||||
};
|
||||
@@ -394,13 +470,13 @@
|
||||
|
||||
Selection.prototype.go = function(n) {
|
||||
var index;
|
||||
index = butterfly.children.indexOf(this.startLine) + n;
|
||||
if (!((0 <= index && index < butterfly.children.length))) {
|
||||
index = Array.prototype.indexOf.call(butterfly.term.childNodes, this.startLine) + n;
|
||||
if (!((0 <= index && index < butterfly.term.childElementCount))) {
|
||||
return;
|
||||
}
|
||||
while (!butterfly.children[index].textContent.match(/\S/)) {
|
||||
while (!butterfly.term.childNodes[index].textContent.match(/\S/)) {
|
||||
index += n;
|
||||
if (!((0 <= index && index < butterfly.children.length))) {
|
||||
if (!((0 <= index && index < butterfly.term.childElementCount))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -418,7 +494,7 @@
|
||||
|
||||
Selection.prototype.selectLine = function(index) {
|
||||
var line, lineEnd, lineStart;
|
||||
line = butterfly.children[index];
|
||||
line = butterfly.term.childNodes[index];
|
||||
lineStart = {
|
||||
node: line.firstChild,
|
||||
offset: 0
|
||||
@@ -518,7 +594,7 @@
|
||||
})();
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var ref, ref1;
|
||||
var r, ref, ref1;
|
||||
if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -555,8 +631,9 @@
|
||||
return cancel(e);
|
||||
}
|
||||
if (!selection && e.ctrlKey && e.shiftKey && e.keyCode === 38) {
|
||||
r = Math.max(butterfly.term.childElementCount - butterfly.rows, 0);
|
||||
selection = new Selection();
|
||||
selection.selectLine(butterfly.y - 1);
|
||||
selection.selectLine(r + butterfly.y - 1);
|
||||
selection.apply();
|
||||
return cancel(e);
|
||||
}
|
||||
@@ -624,7 +701,7 @@
|
||||
}
|
||||
oReq = new XMLHttpRequest();
|
||||
oReq.addEventListener('load', function() {
|
||||
var j, len1, out, ref, response, session;
|
||||
var j, len, out, ref, response, session;
|
||||
response = JSON.parse(this.responseText);
|
||||
out = '<div>';
|
||||
out += '<h2>Session list</h2>';
|
||||
@@ -633,7 +710,7 @@
|
||||
} else {
|
||||
out += '<ul>';
|
||||
ref = response.sessions;
|
||||
for (j = 0, len1 = ref.length; j < len1; j++) {
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
session = ref[j];
|
||||
out += "<li><a href=\"/session/" + session + "\">" + session + "</a></li>";
|
||||
}
|
||||
@@ -688,7 +765,7 @@
|
||||
}
|
||||
oReq = new XMLHttpRequest();
|
||||
oReq.addEventListener('load', function() {
|
||||
var builtin_themes, inner, j, k, len1, len2, option, response, theme, theme_list, themes, url;
|
||||
var builtin_themes, inner, j, k, len, len1, option, response, theme, theme_list, themes, url;
|
||||
response = JSON.parse(this.responseText);
|
||||
builtin_themes = response.builtin_themes;
|
||||
themes = response.themes;
|
||||
@@ -705,7 +782,7 @@
|
||||
option("/static/main.css", 'default');
|
||||
if (themes.length) {
|
||||
inner += '<optgroup label="Local themes">';
|
||||
for (j = 0, len1 = themes.length; j < len1; j++) {
|
||||
for (j = 0, len = themes.length; j < len; j++) {
|
||||
theme = themes[j];
|
||||
url = "/theme/" + theme + "/style.css";
|
||||
option(url, theme);
|
||||
@@ -713,7 +790,7 @@
|
||||
inner += '</optgroup>';
|
||||
}
|
||||
inner += '<optgroup label="Built-in themes">';
|
||||
for (k = 0, len2 = builtin_themes.length; k < len2; k++) {
|
||||
for (k = 0, len1 = builtin_themes.length; k < len1; k++) {
|
||||
theme = builtin_themes[k];
|
||||
url = "/theme/" + theme + "/style.css";
|
||||
option(url, theme.slice('built-in-'.length));
|
||||
@@ -731,74 +808,6 @@
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
|
||||
ctrl = false;
|
||||
alt = false;
|
||||
first = true;
|
||||
virtualInput = document.createElement('input');
|
||||
virtualInput.type = 'password';
|
||||
virtualInput.style.position = 'fixed';
|
||||
virtualInput.style.top = 0;
|
||||
virtualInput.style.left = 0;
|
||||
virtualInput.style.border = 'none';
|
||||
virtualInput.style.outline = 'none';
|
||||
virtualInput.style.opacity = 0;
|
||||
virtualInput.value = '0';
|
||||
document.body.appendChild(virtualInput);
|
||||
virtualInput.addEventListener('blur', function() {
|
||||
return setTimeout(((function(_this) {
|
||||
return function() {
|
||||
return _this.focus();
|
||||
};
|
||||
})(this)), 10);
|
||||
});
|
||||
addEventListener('click', function() {
|
||||
return virtualInput.focus();
|
||||
});
|
||||
addEventListener('touchstart', function(e) {
|
||||
if (e.touches.length === 2) {
|
||||
return ctrl = true;
|
||||
} else if (e.touches.length === 3) {
|
||||
ctrl = false;
|
||||
return alt = true;
|
||||
} else if (e.touches.length === 4) {
|
||||
ctrl = true;
|
||||
return alt = true;
|
||||
}
|
||||
});
|
||||
virtualInput.addEventListener('keydown', function(e) {
|
||||
butterfly.keyDown(e);
|
||||
return true;
|
||||
});
|
||||
virtualInput.addEventListener('input', function(e) {
|
||||
var len;
|
||||
len = this.value.length;
|
||||
if (len === 0) {
|
||||
e.keyCode = 8;
|
||||
butterfly.keyDown(e);
|
||||
this.value = '0';
|
||||
return true;
|
||||
}
|
||||
e.keyCode = this.value.charAt(1).charCodeAt(0);
|
||||
if ((ctrl || alt) && !first) {
|
||||
e.keyCode = this.value.charAt(1).charCodeAt(0);
|
||||
e.ctrlKey = ctrl;
|
||||
e.altKey = alt;
|
||||
if (e.keyCode >= 97 && e.keyCode <= 122) {
|
||||
e.keyCode -= 32;
|
||||
}
|
||||
butterfly.keyDown(e);
|
||||
this.value = '0';
|
||||
ctrl = alt = false;
|
||||
return true;
|
||||
}
|
||||
butterfly.keyPress(e);
|
||||
first = false;
|
||||
this.value = '0';
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
}).call(this);
|
||||
|
||||
//# sourceMappingURL=ext.js.map
|
||||
|
||||
4
butterfly/static/ext.min.js
vendored
4
butterfly/static/ext.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -17,7 +17,7 @@
|
||||
/* These a the default variables */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -40,7 +40,7 @@
|
||||
/* These are all imported files */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -57,7 +57,7 @@
|
||||
/* You can change this file to import any webfont: */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -70,37 +70,37 @@
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
@font-face {
|
||||
font-family: "SourceCodePro";
|
||||
src: url("/static/fonts/SourceCodePro-ExtraLight.otf") format("woff");
|
||||
src: url("fonts/SourceCodePro-ExtraLight.otf") format("woff");
|
||||
font-weight: 100; }
|
||||
|
||||
@font-face {
|
||||
font-family: "SourceCodePro";
|
||||
src: url("/static/fonts/SourceCodePro-Light.otf") format("woff");
|
||||
src: url("fonts/SourceCodePro-Light.otf") format("woff");
|
||||
font-weight: 300; }
|
||||
|
||||
@font-face {
|
||||
font-family: "SourceCodePro";
|
||||
src: url("/static/fonts/SourceCodePro-Regular.otf") format("woff");
|
||||
src: url("fonts/SourceCodePro-Regular.otf") format("woff");
|
||||
font-weight: 400; }
|
||||
|
||||
@font-face {
|
||||
font-family: "SourceCodePro";
|
||||
src: url("/static/fonts/SourceCodePro-Medium.otf") format("woff");
|
||||
src: url("fonts/SourceCodePro-Medium.otf") format("woff");
|
||||
font-weight: 500; }
|
||||
|
||||
@font-face {
|
||||
font-family: "SourceCodePro";
|
||||
src: url("/static/fonts/SourceCodePro-Semibold.otf") format("woff");
|
||||
src: url("fonts/SourceCodePro-Semibold.otf") format("woff");
|
||||
font-weight: 600; }
|
||||
|
||||
@font-face {
|
||||
font-family: "SourceCodePro";
|
||||
src: url("/static/fonts/SourceCodePro-Bold.otf") format("woff");
|
||||
src: url("fonts/SourceCodePro-Bold.otf") format("woff");
|
||||
font-weight: 700; }
|
||||
|
||||
@font-face {
|
||||
font-family: "SourceCodePro";
|
||||
src: url("/static/fonts/SourceCodePro-Black.otf") format("woff");
|
||||
src: url("fonts/SourceCodePro-Black.otf") format("woff");
|
||||
font-weight: 900; }
|
||||
|
||||
body {
|
||||
@@ -111,7 +111,7 @@ body {
|
||||
/* You can comment / uncomment the following to enable/disable terminal effects. */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -163,7 +163,7 @@ body {
|
||||
/* @import all_fx */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -177,7 +177,7 @@ body {
|
||||
/* The color theme is defined in this one: */
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -351,7 +351,7 @@ body {
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -2786,7 +2786,7 @@ body {
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -2804,6 +2804,7 @@ html, body {
|
||||
color: #f4ead5; }
|
||||
|
||||
body {
|
||||
padding-bottom: .5em;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
@@ -2816,10 +2817,10 @@ body {
|
||||
body .line.active {
|
||||
background-color: transparent; }
|
||||
body .line.extended {
|
||||
overflow-x: auto;
|
||||
overflow-x: overlay;
|
||||
cursor: zoom-in;
|
||||
background-image: linear-gradient(90deg, rgba(9, 8, 10, 0), 95%, #09080a); }
|
||||
body .line.extended .extra {
|
||||
display: none; }
|
||||
body .line.extended:not(.expanded):hover {
|
||||
background-color: #161419; }
|
||||
body .line.extended.expanded {
|
||||
@@ -2827,15 +2828,8 @@ body {
|
||||
background-color: #09080a; }
|
||||
body .line.extended.expanded .extra {
|
||||
display: block;
|
||||
white-space: pre-line;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all; }
|
||||
body .line.extended::-webkit-scrollbar {
|
||||
background: rgba(17, 15, 19, 0.1);
|
||||
height: 0; }
|
||||
body .line.extended::-webkit-scrollbar-thumb {
|
||||
background: rgba(244, 234, 213, 0.1); }
|
||||
body .line.extended::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(244, 234, 213, 0.1); }
|
||||
body::-webkit-scrollbar {
|
||||
background: #110f13;
|
||||
width: 0.75em; }
|
||||
@@ -2867,13 +2861,26 @@ body {
|
||||
display: block;
|
||||
padding: .5em;
|
||||
font-size: .75em; }
|
||||
body #input-view {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-decoration: underline; }
|
||||
body #input-helper {
|
||||
position: fixed;
|
||||
z-index: -100;
|
||||
opacity: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none; }
|
||||
|
||||
.terminal {
|
||||
outline: none; }
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
@@ -2892,7 +2899,7 @@ body {
|
||||
|
||||
/* *-* coding: utf-8 *-* */
|
||||
/* This file is part of butterfly */
|
||||
/* butterfly Copyright (C) 2015 Florian Mounier */
|
||||
/* butterfly Copyright(C) 2015-2017 Florian Mounier */
|
||||
/* This program is free software: you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation, either version 3 of the License, or */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
(function() {
|
||||
var $, State, Terminal, cancel, cols, openTs, quit, rows, s, uuid, ws,
|
||||
slice = [].slice,
|
||||
var $, State, Terminal, cancel, cols, isMobile, openTs, quit, rows, s, ws,
|
||||
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; };
|
||||
|
||||
cols = rows = null;
|
||||
@@ -16,15 +15,6 @@
|
||||
|
||||
$ = document.querySelectorAll.bind(document);
|
||||
|
||||
uuid = function() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r, v;
|
||||
r = Math.random() * 16 | 0;
|
||||
v = c === 'x' ? r : r & 0x3 | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var close, ctl, error, init_ctl_ws, init_shell_ws, open, path, reopenOnClose, rootPath, term, write, write_request, wsUrl;
|
||||
term = null;
|
||||
@@ -34,13 +24,14 @@
|
||||
wsUrl = 'ws://';
|
||||
}
|
||||
rootPath = document.body.getAttribute('data-root-path');
|
||||
rootPath = rootPath.replace(/^\/+|\/+$/g, '');
|
||||
if (rootPath.length) {
|
||||
rootPath = "/" + rootPath;
|
||||
}
|
||||
wsUrl += document.location.host + rootPath;
|
||||
path = location.pathname;
|
||||
path = '/';
|
||||
if (path.indexOf('/session') < 0) {
|
||||
path += "session/" + (uuid());
|
||||
path += "session/" + (document.body.getAttribute('data-session-token'));
|
||||
}
|
||||
path += location.search;
|
||||
ws.shell = new WebSocket(wsUrl + '/ws' + path);
|
||||
@@ -141,6 +132,10 @@
|
||||
return false;
|
||||
};
|
||||
|
||||
isMobile = function() {
|
||||
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
};
|
||||
|
||||
s = 0;
|
||||
|
||||
State = {
|
||||
@@ -170,40 +165,57 @@
|
||||
return Terminal.hooks[hook].pop(fun);
|
||||
};
|
||||
|
||||
function Terminal(parent, out1, ctl1) {
|
||||
var div, px;
|
||||
function Terminal(parent, out, ctl1) {
|
||||
var div;
|
||||
this.parent = parent;
|
||||
this.out = out1;
|
||||
this.out = out;
|
||||
this.ctl = ctl1 != null ? ctl1 : function() {};
|
||||
this.document = this.parent.ownerDocument;
|
||||
this.html = this.document.getElementsByTagName('html')[0];
|
||||
this.body = this.document.getElementsByTagName('body')[0];
|
||||
this.term = this.document.getElementById('term');
|
||||
this.forceWidth = this.body.getAttribute('data-force-unicode-width') === 'yes';
|
||||
this.inputHelper = this.document.getElementById('input-helper');
|
||||
this.inputView = this.document.getElementById('input-view');
|
||||
this.body.className = 'terminal focus';
|
||||
this.body.style.outline = 'none';
|
||||
this.body.setAttribute('tabindex', 0);
|
||||
this.body.setAttribute('spellcheck', 'false');
|
||||
this.inputHelper.setAttribute('tabindex', 0);
|
||||
this.inputHelper.setAttribute('spellcheck', 'false');
|
||||
div = this.document.createElement('div');
|
||||
div.className = 'line';
|
||||
this.body.appendChild(div);
|
||||
this.children = [div];
|
||||
this.term.appendChild(div);
|
||||
this.computeCharSize();
|
||||
this.cols = Math.floor(this.body.clientWidth / this.charSize.width);
|
||||
this.rows = Math.floor(window.innerHeight / this.charSize.height);
|
||||
px = window.innerHeight % this.charSize.height;
|
||||
this.body.style['padding-bottom'] = px + "px";
|
||||
this.scrollback = 1000000;
|
||||
this.buffSize = 100000;
|
||||
this.visualBell = 100;
|
||||
this.convertEol = false;
|
||||
this.termName = 'xterm';
|
||||
this.cursorBlink = true;
|
||||
this.cursorState = 0;
|
||||
this.inComposition = false;
|
||||
this.compositionText = "";
|
||||
this.resetVars();
|
||||
this.focus();
|
||||
this.startBlink();
|
||||
this.inputHelper.addEventListener('compositionstart', this.compositionStart.bind(this));
|
||||
this.inputHelper.addEventListener('compositionupdate', this.compositionUpdate.bind(this));
|
||||
this.inputHelper.addEventListener('compositionend', this.compositionEnd.bind(this));
|
||||
addEventListener('keydown', this.keyDown.bind(this));
|
||||
addEventListener('keypress', this.keyPress.bind(this));
|
||||
addEventListener('keyup', (function(_this) {
|
||||
return function() {
|
||||
return _this.inputHelper.focus();
|
||||
};
|
||||
})(this));
|
||||
if (isMobile()) {
|
||||
addEventListener('click', (function(_this) {
|
||||
return function() {
|
||||
return _this.inputHelper.focus();
|
||||
};
|
||||
})(this));
|
||||
}
|
||||
addEventListener('focus', this.focus.bind(this));
|
||||
addEventListener('blur', this.blur.bind(this));
|
||||
addEventListener('resize', (function(_this) {
|
||||
@@ -216,9 +228,6 @@
|
||||
return _this.nativeScrollTo();
|
||||
};
|
||||
})(this), true);
|
||||
if (typeof InstallTrigger !== "undefined") {
|
||||
this.body.contentEditable = 'true';
|
||||
}
|
||||
this.initmouse();
|
||||
addEventListener('load', (function(_this) {
|
||||
return function() {
|
||||
@@ -226,11 +235,11 @@
|
||||
};
|
||||
})(this));
|
||||
this.emit('load');
|
||||
this.active = null;
|
||||
}
|
||||
|
||||
Terminal.prototype.emit = function() {
|
||||
var args, fun, hook, k, len, ref, results;
|
||||
hook = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
|
||||
Terminal.prototype.emit = function(hook, obj) {
|
||||
var fun, k, len, ref, results;
|
||||
if (Terminal.hooks[hook] == null) {
|
||||
Terminal.hooks[hook] = [];
|
||||
}
|
||||
@@ -238,7 +247,11 @@
|
||||
results = [];
|
||||
for (k = 0, len = ref.length; k < len; k++) {
|
||||
fun = ref[k];
|
||||
results.push(fun.apply(this, args));
|
||||
results.push(setTimeout((function(f) {
|
||||
return function() {
|
||||
return f.call(this, obj);
|
||||
};
|
||||
})(fun), 10));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
@@ -258,7 +271,8 @@
|
||||
invisible: a.invisible,
|
||||
italic: a.italic,
|
||||
faint: a.faint,
|
||||
crossed: a.crossed
|
||||
crossed: a.crossed,
|
||||
placeholder: false
|
||||
};
|
||||
};
|
||||
|
||||
@@ -266,18 +280,24 @@
|
||||
return a.bg === b.bg && a.fg === b.fg && a.bold === b.bold && a.underline === b.underline && a.blink === b.blink && a.inverse === b.inverse && a.invisible === b.invisible && a.italic === b.italic && a.faint === b.faint && a.crossed === b.crossed;
|
||||
};
|
||||
|
||||
Terminal.prototype.putChar = function(c) {
|
||||
Terminal.prototype.putChar = function(c, placeholder) {
|
||||
var newChar;
|
||||
if (placeholder == null) {
|
||||
placeholder = false;
|
||||
}
|
||||
newChar = this.cloneAttr(this.curAttr, c);
|
||||
newChar.placeholder = placeholder;
|
||||
if (this.insertMode) {
|
||||
this.screen[this.y + this.shift].chars.splice(this.x, 0, this.cloneAttr(this.curAttr, c));
|
||||
this.screen[this.y + this.shift].chars.splice(this.x, 0, newChar);
|
||||
this.screen[this.y + this.shift].chars.pop();
|
||||
} else {
|
||||
this.screen[this.y + this.shift].chars[this.x] = this.cloneAttr(this.curAttr, c);
|
||||
this.screen[this.y + this.shift].chars[this.x] = newChar;
|
||||
}
|
||||
return this.screen[this.y + this.shift].dirty = true;
|
||||
};
|
||||
|
||||
Terminal.prototype.resetVars = function() {
|
||||
var i;
|
||||
var k, ref, row;
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.cursorHidden = false;
|
||||
@@ -307,16 +327,16 @@
|
||||
invisible: false,
|
||||
italic: false,
|
||||
faint: false,
|
||||
crossed: false
|
||||
crossed: false,
|
||||
placeholder: false
|
||||
};
|
||||
this.curAttr = this.cloneAttr(this.defAttr);
|
||||
this.params = [];
|
||||
this.currentParam = 0;
|
||||
this.prefix = "";
|
||||
this.screen = [];
|
||||
i = this.rows;
|
||||
this.shift = 0;
|
||||
while (i--) {
|
||||
for (row = k = 0, ref = this.rows - 1; 0 <= ref ? k <= ref : k >= ref; row = 0 <= ref ? ++k : --k) {
|
||||
this.screen.push(this.blankLine(false, false));
|
||||
}
|
||||
this.setupStops();
|
||||
@@ -324,15 +344,16 @@
|
||||
};
|
||||
|
||||
Terminal.prototype.computeCharSize = function() {
|
||||
var testSpan;
|
||||
var line, testSpan;
|
||||
testSpan = document.createElement('span');
|
||||
testSpan.textContent = '0123456789';
|
||||
this.children[0].appendChild(testSpan);
|
||||
line = this.term.firstChild;
|
||||
line.appendChild(testSpan);
|
||||
this.charSize = {
|
||||
width: testSpan.getBoundingClientRect().width / 10,
|
||||
height: this.children[0].getBoundingClientRect().height
|
||||
height: line.getBoundingClientRect().height
|
||||
};
|
||||
return this.children[0].removeChild(testSpan);
|
||||
return line.removeChild(testSpan);
|
||||
};
|
||||
|
||||
Terminal.prototype.eraseAttr = function() {
|
||||
@@ -352,6 +373,7 @@
|
||||
this.showCursor();
|
||||
this.body.classList.add('focus');
|
||||
this.body.classList.remove('blur');
|
||||
this.inputHelper.focus();
|
||||
this.resize();
|
||||
return this.scrollLock = old_sl;
|
||||
};
|
||||
@@ -543,213 +565,226 @@
|
||||
})(this));
|
||||
};
|
||||
|
||||
Terminal.prototype.getClasses = function(data) {
|
||||
var classes, fg, styles;
|
||||
classes = [];
|
||||
styles = [];
|
||||
if (data.bold) {
|
||||
classes.push("bold");
|
||||
}
|
||||
if (data.underline) {
|
||||
classes.push("underline");
|
||||
}
|
||||
if (data.blink === 1) {
|
||||
classes.push("blink");
|
||||
}
|
||||
if (data.blink === 2) {
|
||||
classes.push("blink-fast");
|
||||
}
|
||||
if (data.inverse) {
|
||||
classes.push("reverse-video");
|
||||
}
|
||||
if (data.invisible) {
|
||||
classes.push("invisible");
|
||||
}
|
||||
if (data.italic) {
|
||||
classes.push("italic");
|
||||
}
|
||||
if (data.faint) {
|
||||
classes.push("faint");
|
||||
}
|
||||
if (data.crossed) {
|
||||
classes.push("crossed");
|
||||
}
|
||||
if (typeof data.fg === 'number') {
|
||||
fg = data.fg;
|
||||
if (data.bold && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
classes.push("fg-color-" + fg);
|
||||
} else if (typeof data.fg === 'string') {
|
||||
styles.push("color: " + data.fg);
|
||||
}
|
||||
if (typeof data.bg === 'number') {
|
||||
classes.push("bg-color-" + data.bg);
|
||||
} else if (typeof data.bg === 'string') {
|
||||
styles.push("background-color: " + data.bg);
|
||||
}
|
||||
return [classes, styles];
|
||||
};
|
||||
|
||||
Terminal.prototype.isCJK = function(ch) {
|
||||
return ("\u4e00" <= ch && ch <= "\u9fff") || ("\u3040" <= ch && ch <= "\u30ff") || ("\u31f0" <= ch && ch <= "\u31ff") || ("\u3190" <= ch && ch <= "\u319f") || ("\u3301" <= ch && ch <= "\u3356") || ("\uac00" <= ch && ch <= "\ud7ff") || ("\u3000" <= ch && ch <= "\u303f") || ("\uff00" <= ch && ch <= "\uff60") || ("\uffe0" <= ch && ch <= "\uffe6");
|
||||
};
|
||||
|
||||
Terminal.prototype.charToDom = function(data, attr, cursor) {
|
||||
var ch, char, classes, ref, styles;
|
||||
if (data.placeholder) {
|
||||
return;
|
||||
}
|
||||
if (data.html) {
|
||||
return data.html;
|
||||
}
|
||||
attr = attr || this.cloneAttr(this.defAttr);
|
||||
ch = data.ch;
|
||||
char = '';
|
||||
if (!this.equalAttr(data, attr)) {
|
||||
if (!this.equalAttr(attr, this.defAttr)) {
|
||||
char += "</span>";
|
||||
}
|
||||
if (!this.equalAttr(data, this.defAttr)) {
|
||||
ref = this.getClasses(data), classes = ref[0], styles = ref[1];
|
||||
char += "<span class=\"" + (classes.join(" ")) + "\"";
|
||||
if (styles.length) {
|
||||
char += " style=\"" + styles.join("; ") + "\"";
|
||||
}
|
||||
char += ">";
|
||||
}
|
||||
}
|
||||
if (cursor) {
|
||||
char += "<span class=\"" + (this.cursorState ? "reverse-video " : "") + "cursor\">";
|
||||
}
|
||||
switch (ch) {
|
||||
case "&":
|
||||
char += "&";
|
||||
break;
|
||||
case "<":
|
||||
char += "<";
|
||||
break;
|
||||
case ">":
|
||||
char += ">";
|
||||
break;
|
||||
case " ":
|
||||
char += '<span class="nbsp">\u2007</span>';
|
||||
break;
|
||||
default:
|
||||
if (ch <= " ") {
|
||||
char += " ";
|
||||
} else if (!(this.forceWidth || this.isCJK(ch))) {
|
||||
char += ch;
|
||||
} else {
|
||||
if (ch <= "~") {
|
||||
char += ch;
|
||||
} else if (this.isCJK(ch)) {
|
||||
char += "<span style=\"display: inline-block; width: " + (2 * this.charSize.width) + "px\">" + ch + "</span>";
|
||||
} else {
|
||||
char += "<span style=\"display: inline-block; width: " + this.charSize.width + "px\">" + ch + "</span>";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cursor) {
|
||||
char += "</span>";
|
||||
}
|
||||
return char;
|
||||
};
|
||||
|
||||
Terminal.prototype.lineToDom = function(y, line, active) {
|
||||
var cursorX, eol, k, ref, results, x;
|
||||
if (active) {
|
||||
cursorX = this.x;
|
||||
}
|
||||
results = [];
|
||||
for (x = k = 0, ref = this.cols; 0 <= ref ? k <= ref : k >= ref; x = 0 <= ref ? ++k : --k) {
|
||||
if (x !== this.cols) {
|
||||
results.push(this.charToDom(line.chars[x], line.chars[x - 1], x === cursorX));
|
||||
} else {
|
||||
eol = '';
|
||||
if (!this.equalAttr(line.chars[x - 1], this.defAttr)) {
|
||||
eol += '</span>';
|
||||
}
|
||||
if (line.wrap) {
|
||||
eol += '\u23CE';
|
||||
}
|
||||
if (line.extra) {
|
||||
results.push(eol += "<span class=\"extra\">" + line.extra + "</span>");
|
||||
} else {
|
||||
results.push(void 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
Terminal.prototype.screenToDom = function(force) {
|
||||
var active, div, k, len, line, ref, results, y;
|
||||
ref = this.screen;
|
||||
results = [];
|
||||
for (y = k = 0, len = ref.length; k < len; y = ++k) {
|
||||
line = ref[y];
|
||||
if (line.dirty || force) {
|
||||
active = y === this.y + this.shift && !this.cursorHidden;
|
||||
div = document.createElement('div');
|
||||
div.classList.add('line');
|
||||
if (active) {
|
||||
div.classList.add('active');
|
||||
}
|
||||
if (line.extra) {
|
||||
div.classList.add('extended');
|
||||
}
|
||||
div.innerHTML = (this.lineToDom(y, line, active)).join('');
|
||||
if (active) {
|
||||
this.active = div;
|
||||
this.cursor = div.querySelectorAll('.cursor')[0];
|
||||
}
|
||||
results.push(div);
|
||||
} else {
|
||||
results.push(void 0);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
Terminal.prototype.writeDom = function(dom) {
|
||||
var frag, k, len, line, r, y;
|
||||
r = Math.max(this.term.childElementCount - this.rows, 0);
|
||||
for (y = k = 0, len = dom.length; k < len; y = ++k) {
|
||||
line = dom[y];
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
this.screen[y].dirty = false;
|
||||
if (y < this.rows && y < this.term.childElementCount) {
|
||||
this.term.replaceChild(line, this.term.childNodes[r + y]);
|
||||
} else {
|
||||
frag = frag || document.createDocumentFragment('fragment');
|
||||
frag.appendChild(line);
|
||||
}
|
||||
this.emit('change', line);
|
||||
}
|
||||
frag && this.term.appendChild(frag);
|
||||
this.shift = 0;
|
||||
return this.screen = this.screen.slice(-this.rows);
|
||||
};
|
||||
|
||||
Terminal.prototype.refresh = function(force) {
|
||||
var active, attr, ch, classes, cls, cursor, data, fg, group, i, j, k, len, len1, len2, len3, len4, line, lines, m, modified, n, newOut, o, out, q, ref, ref1, ref2, ref3, ref4, ref5, skipnext, styles, u, x;
|
||||
var dom, ref;
|
||||
if (force == null) {
|
||||
force = false;
|
||||
}
|
||||
ref = this.body.querySelectorAll(".cursor");
|
||||
for (k = 0, len = ref.length; k < len; k++) {
|
||||
cursor = ref[k];
|
||||
cursor.parentNode.replaceChild(this.document.createTextNode(cursor.textContent), cursor);
|
||||
if (this.active != null) {
|
||||
this.active.classList.remove('active');
|
||||
}
|
||||
ref1 = this.body.querySelectorAll(".line.active");
|
||||
for (m = 0, len1 = ref1.length; m < len1; m++) {
|
||||
active = ref1[m];
|
||||
active.classList.remove('active');
|
||||
}
|
||||
newOut = '';
|
||||
modified = [];
|
||||
ref2 = this.screen;
|
||||
for (j = n = 0, len2 = ref2.length; n < len2; j = ++n) {
|
||||
line = ref2[j];
|
||||
if (!(line.dirty || force)) {
|
||||
continue;
|
||||
}
|
||||
out = "";
|
||||
if (j === this.y + this.shift && !this.cursorHidden) {
|
||||
x = this.x;
|
||||
} else {
|
||||
x = -Infinity;
|
||||
}
|
||||
attr = this.cloneAttr(this.defAttr);
|
||||
skipnext = false;
|
||||
for (i = o = 0, ref3 = this.cols - 1; 0 <= ref3 ? o <= ref3 : o >= ref3; i = 0 <= ref3 ? ++o : --o) {
|
||||
data = line.chars[i];
|
||||
if (data.html) {
|
||||
out += data.html;
|
||||
break;
|
||||
}
|
||||
if (skipnext) {
|
||||
skipnext = false;
|
||||
continue;
|
||||
}
|
||||
ch = data.ch;
|
||||
if (!this.equalAttr(data, attr)) {
|
||||
if (!this.equalAttr(attr, this.defAttr)) {
|
||||
out += "</span>";
|
||||
}
|
||||
if (!this.equalAttr(data, this.defAttr)) {
|
||||
classes = [];
|
||||
styles = [];
|
||||
out += "<span ";
|
||||
if (data.bold) {
|
||||
classes.push("bold");
|
||||
}
|
||||
if (data.underline) {
|
||||
classes.push("underline");
|
||||
}
|
||||
if (data.blink === 1) {
|
||||
classes.push("blink");
|
||||
}
|
||||
if (data.blink === 2) {
|
||||
classes.push("blink-fast");
|
||||
}
|
||||
if (data.inverse) {
|
||||
classes.push("reverse-video");
|
||||
}
|
||||
if (data.invisible) {
|
||||
classes.push("invisible");
|
||||
}
|
||||
if (data.italic) {
|
||||
classes.push("italic");
|
||||
}
|
||||
if (data.faint) {
|
||||
classes.push("faint");
|
||||
}
|
||||
if (data.crossed) {
|
||||
classes.push("crossed");
|
||||
}
|
||||
if (typeof data.fg === 'number') {
|
||||
fg = data.fg;
|
||||
if (data.bold && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
classes.push("fg-color-" + fg);
|
||||
}
|
||||
if (typeof data.fg === 'string') {
|
||||
styles.push("color: " + data.fg);
|
||||
}
|
||||
if (typeof data.bg === 'number') {
|
||||
classes.push("bg-color-" + data.bg);
|
||||
}
|
||||
if (typeof data.bg === 'string') {
|
||||
styles.push("background-color: " + data.bg);
|
||||
}
|
||||
out += "class=\"";
|
||||
out += classes.join(" ");
|
||||
out += "\"";
|
||||
if (styles.length) {
|
||||
out += " style=\"" + styles.join("; ") + "\"";
|
||||
}
|
||||
out += ">";
|
||||
}
|
||||
}
|
||||
if (i === x) {
|
||||
out += "<span class=\"" + (this.cursorState ? "reverse-video " : "") + "cursor\">";
|
||||
}
|
||||
if (ch.length > 1) {
|
||||
out += ch;
|
||||
} else {
|
||||
switch (ch) {
|
||||
case "&":
|
||||
out += "&";
|
||||
break;
|
||||
case "<":
|
||||
out += "<";
|
||||
break;
|
||||
case ">":
|
||||
out += ">";
|
||||
break;
|
||||
default:
|
||||
if (ch === " ") {
|
||||
out += '<span class="nbsp">\u2007</span>';
|
||||
} else if (ch <= " ") {
|
||||
out += " ";
|
||||
} else if (!this.forceWidth || ch <= "~") {
|
||||
out += ch;
|
||||
} else if (("\uff00" < ch && ch < "\uffef")) {
|
||||
skipnext = true;
|
||||
out += "<span style=\"display: inline-block; width: " + (2 * this.charSize.width) + "px\">" + ch + "</span>";
|
||||
} else {
|
||||
out += "<span style=\"display: inline-block; width: " + this.charSize.width + "px\">" + ch + "</span>";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i === x) {
|
||||
out += "</span>";
|
||||
}
|
||||
attr = data;
|
||||
}
|
||||
if (!this.equalAttr(attr, this.defAttr)) {
|
||||
out += "</span>";
|
||||
}
|
||||
if (line.wrap) {
|
||||
out += '\u23CE';
|
||||
}
|
||||
if (line.extra) {
|
||||
out += '<span class="extra">' + line.extra + '</span>';
|
||||
}
|
||||
if (this.children[j]) {
|
||||
this.children[j].innerHTML = out;
|
||||
modified.push(this.children[j]);
|
||||
if (x !== -Infinity) {
|
||||
this.children[j].classList.add('active');
|
||||
}
|
||||
if (line.extra) {
|
||||
this.children[j].classList.add('extended');
|
||||
}
|
||||
} else {
|
||||
cls = ['line'];
|
||||
if (x !== -Infinity) {
|
||||
cls.push('active');
|
||||
}
|
||||
if (line.extra) {
|
||||
cls.push('extended');
|
||||
}
|
||||
newOut += "<div class=\"" + (cls.join(' ')) + "\">" + out + "</div>";
|
||||
}
|
||||
this.screen[j].dirty = false;
|
||||
}
|
||||
if (newOut !== '') {
|
||||
group = this.document.createElement('div');
|
||||
group.className = 'group';
|
||||
group.innerHTML = newOut;
|
||||
modified.push(group);
|
||||
this.body.appendChild(group);
|
||||
this.screen = this.screen.slice(-this.rows);
|
||||
this.shift = 0;
|
||||
lines = document.querySelectorAll('.line');
|
||||
if (lines.length > this.scrollback) {
|
||||
ref4 = Array.prototype.slice.call(lines, 0, lines.length - this.scrollback);
|
||||
for (q = 0, len3 = ref4.length; q < len3; q++) {
|
||||
line = ref4[q];
|
||||
line.remove();
|
||||
}
|
||||
ref5 = document.querySelectorAll('.group:empty');
|
||||
for (u = 0, len4 = ref5.length; u < len4; u++) {
|
||||
group = ref5[u];
|
||||
group.remove();
|
||||
}
|
||||
lines = document.querySelectorAll('.line');
|
||||
}
|
||||
this.children = Array.prototype.slice.call(lines, -this.rows);
|
||||
if (this.cursor) {
|
||||
if ((ref = this.cursor.parentNode) != null) {
|
||||
ref.replaceChild(this.document.createTextNode(this.cursor.textContent), this.cursor);
|
||||
}
|
||||
}
|
||||
dom = this.screenToDom(force);
|
||||
this.writeDom(dom);
|
||||
this.nativeScrollTo();
|
||||
return this.emit('change', modified);
|
||||
this.updateInputViews();
|
||||
return this.emit('refresh');
|
||||
};
|
||||
|
||||
Terminal.prototype._cursorBlink = function() {
|
||||
var cursor;
|
||||
this.cursorState ^= 1;
|
||||
cursor = this.body.querySelector(".cursor");
|
||||
if (!cursor) {
|
||||
if (!this.cursor) {
|
||||
return;
|
||||
}
|
||||
if (cursor.classList.contains("reverse-video")) {
|
||||
return cursor.classList.remove("reverse-video");
|
||||
if (this.cursor.classList.contains("reverse-video")) {
|
||||
return this.cursor.classList.remove("reverse-video");
|
||||
} else {
|
||||
return cursor.classList.add("reverse-video");
|
||||
return this.cursor.classList.add("reverse-video");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -917,12 +952,8 @@
|
||||
}
|
||||
this.putChar(ch);
|
||||
this.x++;
|
||||
if (this.forceWidth && ("\uff00" < ch && ch < "\uffef")) {
|
||||
if (this.cols < 2 || this.x >= this.cols) {
|
||||
this.putChar(" ");
|
||||
break;
|
||||
}
|
||||
this.putChar(" ");
|
||||
if (this.isCJK(ch)) {
|
||||
this.putChar(" ", true);
|
||||
this.x++;
|
||||
}
|
||||
}
|
||||
@@ -1414,11 +1445,93 @@
|
||||
return this.write(data + "\r\n");
|
||||
};
|
||||
|
||||
Terminal.prototype.updateInputViews = function() {
|
||||
var cursorPos;
|
||||
cursorPos = this.cursor.getBoundingClientRect();
|
||||
this.inputView.style['left'] = cursorPos.left + "px";
|
||||
this.inputView.style['top'] = cursorPos.top + "px";
|
||||
this.inputHelper.style['left'] = cursorPos.left + "px";
|
||||
this.inputHelper.style['top'] = cursorPos.top + "px";
|
||||
return this.inputHelper.value = "";
|
||||
};
|
||||
|
||||
Terminal.prototype.compositionStart = function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.updateInputViews();
|
||||
this.inputView.className = "";
|
||||
this.inputView.innerText = "";
|
||||
this.cursor.style['visibility'] = "hidden";
|
||||
this.inComposition = true;
|
||||
this.compositionText = "";
|
||||
return false;
|
||||
};
|
||||
|
||||
Terminal.prototype.compositionUpdate = function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.compositionText = ev.data;
|
||||
this.inputView.innerText = this.compositionText;
|
||||
return false;
|
||||
};
|
||||
|
||||
Terminal.prototype.compositionEnd = function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.finishComposition();
|
||||
return false;
|
||||
};
|
||||
|
||||
Terminal.prototype.finishComposition = function() {
|
||||
this.inComposition = false;
|
||||
this.showCursor();
|
||||
this.inputHelper.value = "";
|
||||
this.inputView.className = "hidden";
|
||||
this.send(this.compositionText);
|
||||
this.compositionText = "";
|
||||
return this.inputHelper.focus();
|
||||
};
|
||||
|
||||
Terminal.prototype.keyDown = function(ev) {
|
||||
var key, ref;
|
||||
if (this.inComposition) {
|
||||
if (ev.keyCode === 229) {
|
||||
return false;
|
||||
} else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
|
||||
return false;
|
||||
}
|
||||
this.finishComposition();
|
||||
}
|
||||
if (ev.keyCode === 229) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setTimeout((function(_this) {
|
||||
return function() {
|
||||
var char, e, val;
|
||||
if (!(_this.inComposition || _this.inputHelper.value.length > 1)) {
|
||||
val = _this.inputHelper.value;
|
||||
_this.inputHelper.value = "";
|
||||
char = val.toUpperCase().charCodeAt(0);
|
||||
if ((65 <= char && char <= 90)) {
|
||||
e = new KeyboardEvent('keydown', {
|
||||
keyCode: char
|
||||
});
|
||||
if (window.mobileKeydown(e)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return _this.send(val);
|
||||
}
|
||||
};
|
||||
})(this), 0);
|
||||
return false;
|
||||
}
|
||||
if (ev.keyCode > 15 && ev.keyCode < 19) {
|
||||
return true;
|
||||
}
|
||||
if (window.mobileKeydown(ev)) {
|
||||
return true;
|
||||
}
|
||||
if (ev.keyCode === 19) {
|
||||
this.body.classList.add('stopped');
|
||||
this.out('\x03');
|
||||
@@ -1429,6 +1542,7 @@
|
||||
return true;
|
||||
}
|
||||
if ((ev.shiftKey && ev.ctrlKey) && ((ref = ev.keyCode) === 67 || ref === 86)) {
|
||||
this.body.contentEditable = true;
|
||||
return true;
|
||||
}
|
||||
if (ev.altKey && ev.keyCode === 90 && !this.skipNextKey) {
|
||||
@@ -1695,7 +1809,7 @@
|
||||
};
|
||||
|
||||
Terminal.prototype.resize = function(x, y, notif) {
|
||||
var el, h, i, j, line, oldCols, oldRows, px, w;
|
||||
var h, insert, k, len, len1, len2, len3, line, m, n, o, oldCols, oldRows, ref, ref1, ref2, ref3, w;
|
||||
if (x == null) {
|
||||
x = null;
|
||||
}
|
||||
@@ -1709,11 +1823,13 @@
|
||||
oldRows = this.rows;
|
||||
this.computeCharSize();
|
||||
w = this.body.clientWidth;
|
||||
h = this.html.clientHeight - (this.html.offsetHeight - this.html.scrollHeight);
|
||||
h = this.html.clientHeight;
|
||||
if (this.charSize.width === 0 || this.charSize.height === 0) {
|
||||
console.error('Null size in refresh');
|
||||
return;
|
||||
}
|
||||
this.cols = x || Math.floor(w / this.charSize.width);
|
||||
this.rows = y || Math.floor(h / this.charSize.height);
|
||||
px = h % this.charSize.height;
|
||||
this.body.style['padding-bottom'] = px + "px";
|
||||
this.cols = Math.max(1, this.cols);
|
||||
this.rows = Math.max(1, this.rows);
|
||||
this.nativeScrollTo();
|
||||
@@ -1727,91 +1843,76 @@
|
||||
rows: this.rows
|
||||
}));
|
||||
}
|
||||
if (oldCols < this.cols) {
|
||||
i = this.screen.length;
|
||||
while (i--) {
|
||||
while (this.screen[i].chars.length < this.cols) {
|
||||
this.screen[i].chars.push(this.defAttr);
|
||||
if (this.cols > oldCols) {
|
||||
ref = this.screen;
|
||||
for (k = 0, len = ref.length; k < len; k++) {
|
||||
line = ref[k];
|
||||
while (line.chars.length < this.cols) {
|
||||
line.chars.push(this.defAttr);
|
||||
}
|
||||
this.screen[i].wrap = false;
|
||||
line.wrap = false;
|
||||
}
|
||||
} else if (oldCols > this.cols) {
|
||||
i = this.screen.length;
|
||||
while (i--) {
|
||||
while (this.screen[i].chars.length > this.cols) {
|
||||
this.screen[i].chars.pop();
|
||||
if (this.normal) {
|
||||
ref1 = this.normal.screen;
|
||||
for (m = 0, len1 = ref1.length; m < len1; m++) {
|
||||
line = ref1[m];
|
||||
while (line.chars.length < this.cols) {
|
||||
line.chars.push(this.defAttr);
|
||||
}
|
||||
line.wrap = false;
|
||||
}
|
||||
}
|
||||
} else if (this.cols < oldCols) {
|
||||
ref2 = this.screen;
|
||||
for (n = 0, len2 = ref2.length; n < len2; n++) {
|
||||
line = ref2[n];
|
||||
while (line.chars.length > this.cols) {
|
||||
line.chars.pop();
|
||||
}
|
||||
}
|
||||
if (this.normal) {
|
||||
ref3 = this.normal.screen;
|
||||
for (o = 0, len3 = ref3.length; o < len3; o++) {
|
||||
line = ref3[o];
|
||||
while (line.chars.length > this.cols) {
|
||||
line.chars.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setupStops(oldCols);
|
||||
j = oldRows;
|
||||
if (j < this.rows) {
|
||||
el = this.body;
|
||||
while (j++ < this.rows) {
|
||||
if (this.screen.length < this.rows) {
|
||||
this.screen.push(this.blankLine());
|
||||
}
|
||||
if (this.children.length < this.rows) {
|
||||
line = this.document.createElement("div");
|
||||
line.className = 'line';
|
||||
el.appendChild(line);
|
||||
this.children.push(line);
|
||||
}
|
||||
}
|
||||
} else if (j > this.rows) {
|
||||
while (j-- > this.rows) {
|
||||
if (this.screen.length > this.rows) {
|
||||
this.screen.pop();
|
||||
}
|
||||
if (this.children.length > this.rows) {
|
||||
el = this.children.pop();
|
||||
if (el != null) {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.term.childElementCount >= this.rows) {
|
||||
this.y += this.rows - oldRows;
|
||||
insert = 'unshift';
|
||||
} else {
|
||||
insert = 'push';
|
||||
}
|
||||
while (this.screen.length > this.rows) {
|
||||
this.screen.shift();
|
||||
}
|
||||
while (this.screen.length < this.rows) {
|
||||
this.screen[insert](this.blankLine(false, false));
|
||||
}
|
||||
if (this.normal) {
|
||||
if (oldCols < this.cols) {
|
||||
i = this.normal.screen.length;
|
||||
while (i--) {
|
||||
while (this.normal.screen[i].chars.length < this.cols) {
|
||||
this.normal.screen[i].chars.push(this.defAttr);
|
||||
}
|
||||
this.normal.screen[i].wrap = false;
|
||||
}
|
||||
} else if (oldCols > this.cols) {
|
||||
i = this.normal.screen.length;
|
||||
while (i--) {
|
||||
while (this.normal.screen[i].chars.length > this.cols) {
|
||||
this.normal.screen[i].chars.pop();
|
||||
}
|
||||
}
|
||||
while (this.normal.screen.length > this.rows) {
|
||||
this.normal.screen.shift();
|
||||
}
|
||||
j = oldRows;
|
||||
if (j < this.rows) {
|
||||
while (j++ < this.rows) {
|
||||
if (this.normal.screen.length < this.rows) {
|
||||
this.normal.screen.push(this.blankLine());
|
||||
}
|
||||
}
|
||||
} else if (j > this.rows) {
|
||||
while (j-- > this.rows) {
|
||||
if (this.normal.screen.length > this.rows) {
|
||||
this.normal.screen.pop();
|
||||
}
|
||||
}
|
||||
while (this.normal.screen.length < this.rows) {
|
||||
this.normal.screen[insert](this.blankLine(false, false));
|
||||
}
|
||||
}
|
||||
if (this.y >= this.rows) {
|
||||
this.y = this.rows - 1;
|
||||
}
|
||||
if (this.y < 0) {
|
||||
this.y = 0;
|
||||
}
|
||||
if (this.x >= this.cols) {
|
||||
this.x = this.cols - 1;
|
||||
}
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = this.rows - 1;
|
||||
this.refresh(true);
|
||||
this.refresh();
|
||||
if (!notif && (x || y)) {
|
||||
return this.reset();
|
||||
}
|
||||
@@ -1965,22 +2066,10 @@
|
||||
};
|
||||
|
||||
Terminal.prototype.clearScrollback = function() {
|
||||
var group, k, len, len1, line, lines, m, ref, ref1;
|
||||
lines = document.querySelectorAll('.line');
|
||||
if (lines.length > this.rows) {
|
||||
ref = Array.prototype.slice.call(lines, 0, lines.length - this.rows);
|
||||
for (k = 0, len = ref.length; k < len; k++) {
|
||||
line = ref[k];
|
||||
line.remove();
|
||||
}
|
||||
ref1 = document.querySelectorAll('.group:empty');
|
||||
for (m = 0, len1 = ref1.length; m < len1; m++) {
|
||||
group = ref1[m];
|
||||
group.remove();
|
||||
}
|
||||
lines = document.querySelectorAll('.line');
|
||||
while (this.term.childElementCount > this.rows) {
|
||||
this.term.firstChild.remove();
|
||||
}
|
||||
return this.children = Array.prototype.slice.call(lines, -this.rows);
|
||||
return this.emit('clear');
|
||||
};
|
||||
|
||||
Terminal.prototype.tabSet = function() {
|
||||
@@ -2297,7 +2386,7 @@
|
||||
};
|
||||
|
||||
Terminal.prototype.deleteLines = function(params) {
|
||||
var i, k, param, ref, ref1, results;
|
||||
var i, k, node, param, ref, ref1, results;
|
||||
param = params[0];
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
@@ -2306,8 +2395,8 @@
|
||||
this.screen.splice(this.scrollBottom + this.shift, 0, this.blankLine(true));
|
||||
this.screen.splice(this.y + this.shift, 1);
|
||||
if (!(this.normal || this.scrollTop !== 0 || this.scrollBottom !== this.rows - 1)) {
|
||||
this.children[this.y + this.shift].remove();
|
||||
this.children.splice(this.y + this.shift, 1);
|
||||
node = this.term.childElementCount - this.rows + this.y + this.shift;
|
||||
this.term.childNodes[node].remove();
|
||||
}
|
||||
}
|
||||
if (this.normal || this.scrollTop !== 0 || this.scrollBottom !== this.rows - 1) {
|
||||
|
||||
6
butterfly/static/main.min.js
vendored
6
butterfly/static/main.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
{% from tornado.options import options %}
|
||||
{% from uuid import uuid4 %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -15,7 +16,12 @@
|
||||
|
||||
<body spellcheck="false"
|
||||
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}"
|
||||
data-root-path="{{ options.uri_root_path }}">
|
||||
data-root-path="{{ options.uri_root_path }}"
|
||||
data-session-token={{ session }}>
|
||||
<textarea id="input-helper">
|
||||
</textarea>
|
||||
<div id="input-view" class="hidden">
|
||||
</div>
|
||||
<div id="popup" class="hidden">
|
||||
</div>
|
||||
<script src="{{ static_url('html-sanitizer.js') }}"></script>
|
||||
@@ -24,5 +30,7 @@
|
||||
<script src="{{ static_url('ext.%sjs' % (
|
||||
'' if options.unminified else 'min.')) }}"></script>
|
||||
<script src="{{ reverse_url('LocalJsStatic') }}"></script>
|
||||
<div id="packed"></div>
|
||||
<div id="term"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
! ! {{ colors.red if opts.unsecure else colors.green }}{{ butterfly.socket.remote_addr }}:{{ butterfly.socket.remote_port }}{{ colors.reset }}
|
||||
|
||||
For more information type: {{ colors.white }}$ {{ colors.green }}butterfly help{{ colors.reset }}
|
||||
{% if opts.unsecure %}{{ colors.light_red + '\x1b[5m' }}/!\{{ colors.reset }} {{ colors.red }}This session is UNSECURE everyone can access you terminal at:
|
||||
{% if opts.unsecure and not opts.i_hereby_declare_i_dont_want_any_security_whatsoever %}{{ colors.light_red + '\x1b[5m' }}/!\{{ colors.reset }} {{ colors.red }}This session is UNSECURE everyone can access you terminal at:
|
||||
{{ uri }}
|
||||
{% else %}You can share your session with the following uri:
|
||||
{{ uri }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -25,13 +25,15 @@ import string
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
from logging import getLogger
|
||||
|
||||
import tornado.ioloop
|
||||
import tornado.options
|
||||
import tornado.process
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
from logging import getLogger
|
||||
from butterfly import utils, __version__
|
||||
|
||||
from butterfly import __version__, utils
|
||||
|
||||
log = getLogger('butterfly')
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
@@ -95,10 +97,10 @@ class Terminal(object):
|
||||
version=__version__,
|
||||
opts=tornado.options.options,
|
||||
uri=self.uri,
|
||||
colors=utils.ansi_colors)
|
||||
.decode('utf-8')
|
||||
.replace('\r', '')
|
||||
.replace('\n', '\r\n'))
|
||||
colors=utils.ansi_colors
|
||||
).decode('utf-8')
|
||||
.replace('\r', '')
|
||||
.replace('\n', '\r\n'))
|
||||
self.send(motd)
|
||||
|
||||
log.info('Forking pty for user %r' % self.user)
|
||||
@@ -194,19 +196,19 @@ class Terminal(object):
|
||||
tty, os.getpid(),
|
||||
self.callee.name, self.uri)
|
||||
|
||||
if not tornado.options.options.unsecure or (
|
||||
self.socket.local and
|
||||
self.caller == self.callee and
|
||||
server == self.callee
|
||||
) and not tornado.options.options.login:
|
||||
local_login = (
|
||||
self.socket.local and self.caller == self.callee and
|
||||
server == self.callee)
|
||||
secure = not tornado.options.options.unsecure
|
||||
force_login = tornado.options.options.login
|
||||
ignore_security = (
|
||||
tornado.options.options.
|
||||
i_hereby_declare_i_dont_want_any_security_whatsoever)
|
||||
|
||||
if not force_login and (ignore_security or secure or local_login):
|
||||
# User has been auth with ssl or is the same user as server
|
||||
# or login is explicitly turned off
|
||||
if (
|
||||
not tornado.options.options.unsecure and not (
|
||||
self.socket.local and
|
||||
self.caller == self.callee and
|
||||
server == self.callee
|
||||
)):
|
||||
if secure and not local_login:
|
||||
# User is authed by ssl, setting groups
|
||||
try:
|
||||
os.initgroups(self.callee.name, self.callee.gid)
|
||||
@@ -232,6 +234,16 @@ class Terminal(object):
|
||||
os.execvpe(args[0], args, env)
|
||||
# This process has been replaced
|
||||
|
||||
if tornado.options.options.pam_profile:
|
||||
if not server.root:
|
||||
print('You must be root to use pam_profile option.')
|
||||
sys.exit(3)
|
||||
pam_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'pam.py')
|
||||
os.execvpe(sys.executable, [
|
||||
sys.executable, pam_path, self.callee.name,
|
||||
tornado.options.options.pam_profile], env)
|
||||
|
||||
# Unsecure connection with su
|
||||
if server.root:
|
||||
if self.socket.local:
|
||||
@@ -252,7 +264,7 @@ class Terminal(object):
|
||||
args = ['/bin/su']
|
||||
|
||||
args.append('-l')
|
||||
if sys.platform == 'linux' and tornado.options.options.shell:
|
||||
if sys.platform.startswith('linux') and tornado.options.options.shell:
|
||||
args.append('-s')
|
||||
args.append(tornado.options.options.shell)
|
||||
args.append(self.callee.name)
|
||||
|
||||
Submodule butterfly/themes updated: 4d352b32d6...d640d1ec1c
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -18,13 +18,13 @@
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import time
|
||||
import sys
|
||||
import struct
|
||||
from logging import getLogger
|
||||
from collections import namedtuple
|
||||
import subprocess
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
|
||||
log = getLogger('butterfly')
|
||||
|
||||
@@ -168,7 +168,7 @@ def get_lsof_socket_line(addr, port):
|
||||
# May want to make this into a dictionary in the future...
|
||||
regex = "\w+\s+(?P<pid>\d+)\s+(?P<user>\w+).*\s" \
|
||||
"(?P<laddr>.*?):(?P<lport>\d+)->(?P<raddr>.*?):(?P<rport>\d+)"
|
||||
output = subprocess.check_output(['lsof', '-Pni'])
|
||||
output = subprocess.check_output(['lsof', '-Pni']).decode('utf-8')
|
||||
lines = output.split('\n')
|
||||
for line in lines:
|
||||
# Look for local address with peer port
|
||||
@@ -294,11 +294,12 @@ def get_wtmp_file():
|
||||
if os.path.exists(file):
|
||||
return file
|
||||
|
||||
|
||||
UTmp = namedtuple(
|
||||
'UTmp',
|
||||
['type', 'pid', 'line', 'id', 'user', 'host',
|
||||
'exit0', 'exit1', 'session',
|
||||
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
|
||||
'UTmp',
|
||||
['type', 'pid', 'line', 'id', 'user', 'host',
|
||||
'exit0', 'exit1', 'session',
|
||||
'sec', 'usec', 'addr0', 'addr1', 'addr2', 'addr3', 'unused'])
|
||||
|
||||
|
||||
def utmp_line(id, type, pid, fd, user, host, ts):
|
||||
@@ -408,4 +409,5 @@ class AnsiColors(object):
|
||||
return '\x1b[0m'
|
||||
return ''
|
||||
|
||||
|
||||
ansi_colors = AnsiColors()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -16,6 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
addEventListener 'copy', copy = (e) ->
|
||||
document.getElementsByTagName('body')[0].contentEditable = false
|
||||
butterfly.bell "copied"
|
||||
e.clipboardData.clearData()
|
||||
sel = getSelection().toString().replace(
|
||||
@@ -35,6 +36,7 @@ addEventListener 'copy', copy = (e) ->
|
||||
|
||||
|
||||
addEventListener 'paste', (e) ->
|
||||
document.getElementsByTagName('body')[0].contentEditable = false
|
||||
butterfly.bell "pasted"
|
||||
data = e.clipboardData.getData 'text/plain'
|
||||
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r')
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
Terminal.on 'change', (lines) ->
|
||||
for line in lines
|
||||
if 'extended' in line.classList
|
||||
line.addEventListener 'click', do (line) -> ->
|
||||
if 'expanded' in line.classList
|
||||
line.classList.remove 'expanded'
|
||||
else
|
||||
before = line.getBoundingClientRect().height
|
||||
line.classList.add 'expanded'
|
||||
after = line.getBoundingClientRect().height
|
||||
document.body.scrollTop += after - before
|
||||
Terminal.on 'change', (line) ->
|
||||
if 'extended' in line.classList
|
||||
line.addEventListener 'click', do (line) -> ->
|
||||
if 'expanded' in line.classList
|
||||
line.classList.remove 'expanded'
|
||||
else
|
||||
before = line.getBoundingClientRect().height
|
||||
line.classList.add 'expanded'
|
||||
after = line.getBoundingClientRect().height
|
||||
document.body.scrollTop += after - before
|
||||
|
||||
@@ -14,13 +14,20 @@ linkify = (text) ->
|
||||
.replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>')
|
||||
.replace(emailAddressPattern, '<a href="mailto:$&">$&</a>')
|
||||
|
||||
Terminal.on 'change', (lines) ->
|
||||
for line in lines
|
||||
walk line, ->
|
||||
if @nodeType is 3
|
||||
linkified = linkify @nodeValue
|
||||
if linkified isnt @nodeValue
|
||||
newNode = document.createElement('span')
|
||||
newNode.innerHTML = linkified
|
||||
@parentElement.replaceChild newNode, @
|
||||
true
|
||||
tags =
|
||||
'&': '&'
|
||||
'<': '<'
|
||||
'>': '>'
|
||||
|
||||
escape = (s) -> s.replace(/[&<>]/g, (tag) -> tags[tag] or tag)
|
||||
|
||||
Terminal.on 'change', (line) ->
|
||||
walk line, ->
|
||||
if @nodeType is 3
|
||||
val = @nodeValue
|
||||
linkified = linkify escape(val)
|
||||
if linkified isnt val
|
||||
newNode = document.createElement('span')
|
||||
newNode.innerHTML = linkified
|
||||
@parentElement.replaceChild newNode, @
|
||||
true
|
||||
|
||||
52
coffees/ext/mobile.coffee
Normal file
52
coffees/ext/mobile.coffee
Normal file
@@ -0,0 +1,52 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ctrl = false
|
||||
alt = false
|
||||
|
||||
addEventListener 'touchstart', (e) ->
|
||||
if e.touches.length == 2
|
||||
ctrl = true
|
||||
else if e.touches.length == 3
|
||||
ctrl = false
|
||||
alt = true
|
||||
else if e.touches.length == 4
|
||||
ctrl = true
|
||||
alt = true
|
||||
|
||||
# Dispatch a new event if the current event need to
|
||||
# be modified with ctrlKey and altKey from touch events
|
||||
# If so, this function will return true and dispatch the new event.
|
||||
# The caller should return immediately upon receiving true.
|
||||
window.mobileKeydown = (e) ->
|
||||
if ctrl or alt
|
||||
_ctrlKey = ctrl
|
||||
_altKey = alt
|
||||
_keyCode = e.keyCode
|
||||
if e.keyCode >= 97 && e.keyCode <= 122
|
||||
_keyCode -= 32
|
||||
e = new KeyboardEvent 'keydown',
|
||||
ctrlKey: _ctrlKey,
|
||||
altKey: _altKey,
|
||||
keyCode: _keyCode
|
||||
ctrl = alt = false
|
||||
setTimeout ->
|
||||
window.dispatchEvent e
|
||||
, 0
|
||||
return true
|
||||
else
|
||||
return false
|
||||
@@ -1,4 +1,4 @@
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 79
|
||||
open(location.href)
|
||||
open(location.origin)
|
||||
cancel e
|
||||
|
||||
29
coffees/ext/pack.coffee
Normal file
29
coffees/ext/pack.coffee
Normal file
@@ -0,0 +1,29 @@
|
||||
tid = null
|
||||
packSize = 1000
|
||||
histSize = 100
|
||||
|
||||
maybePack = ->
|
||||
return unless butterfly.term.childElementCount > packSize + butterfly.rows
|
||||
hist = document.getElementById 'packed'
|
||||
packfrag = document.createDocumentFragment 'fragment'
|
||||
for i in [0..packSize]
|
||||
packfrag.appendChild butterfly.term.firstChild
|
||||
pack = document.createElement 'div'
|
||||
pack.classList.add 'pack'
|
||||
pack.appendChild packfrag
|
||||
hist.appendChild pack
|
||||
|
||||
hist.firstChild.remove() if hist.childElementCount > histSize
|
||||
|
||||
tid = setTimeout maybePack
|
||||
|
||||
|
||||
Terminal.on 'refresh', ->
|
||||
clearTimeout tid if tid
|
||||
maybePack()
|
||||
|
||||
Terminal.on 'clear', ->
|
||||
newHist = document.createElement 'div'
|
||||
newHist.id = 'packed'
|
||||
hist = document.getElementById 'packed'
|
||||
butterfly.body.replaceChild newHist, hist
|
||||
@@ -9,10 +9,6 @@ class Popup
|
||||
@el.innerHTML = html
|
||||
@el.classList.remove 'hidden'
|
||||
|
||||
# ff glorious hack
|
||||
if typeof InstallTrigger isnt "undefined"
|
||||
document.body.contentEditable = 'false'
|
||||
|
||||
addEventListener 'click', @bound_click_maybe_close
|
||||
addEventListener 'keydown', @bound_key_maybe_close
|
||||
|
||||
@@ -20,10 +16,6 @@ class Popup
|
||||
removeEventListener 'click', @bound_click_maybe_close
|
||||
removeEventListener 'keydown', @bound_key_maybe_close
|
||||
|
||||
# ff glorious hack
|
||||
if typeof InstallTrigger isnt "undefined"
|
||||
document.body.contentEditable = 'true'
|
||||
|
||||
@el.classList.add 'hidden'
|
||||
@el.innerHTML = ''
|
||||
|
||||
|
||||
@@ -87,12 +87,13 @@ class Selection
|
||||
@go +1
|
||||
|
||||
go: (n) ->
|
||||
index = butterfly.children.indexOf(@startLine) + n
|
||||
return unless 0 <= index < butterfly.children.length
|
||||
index = Array.prototype.indexOf.call(
|
||||
butterfly.term.childNodes, @startLine) + n
|
||||
return unless 0 <= index < butterfly.term.childElementCount
|
||||
|
||||
until butterfly.children[index].textContent.match /\S/
|
||||
until butterfly.term.childNodes[index].textContent.match /\S/
|
||||
index += n
|
||||
return unless 0 <= index < butterfly.children.length
|
||||
return unless 0 <= index < butterfly.term.childElementCount
|
||||
|
||||
@selectLine index
|
||||
|
||||
@@ -104,7 +105,7 @@ class Selection
|
||||
@selection.addRange range
|
||||
|
||||
selectLine: (index) ->
|
||||
line = butterfly.children[index]
|
||||
line = butterfly.term.childNodes[index]
|
||||
lineStart =
|
||||
node: line.firstChild
|
||||
offset: 0
|
||||
@@ -204,8 +205,9 @@ document.addEventListener 'keydown', (e) ->
|
||||
|
||||
# Start selection mode with shift up
|
||||
if not selection and e.ctrlKey and e.shiftKey and e.keyCode == 38
|
||||
r = Math.max butterfly.term.childElementCount - butterfly.rows, 0
|
||||
selection = new Selection()
|
||||
selection.selectLine butterfly.y - 1
|
||||
selection.selectLine r + butterfly.y - 1
|
||||
selection.apply()
|
||||
return cancel e
|
||||
true
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
if /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
|
||||
.test navigator.userAgent
|
||||
ctrl = false
|
||||
alt = false
|
||||
first = true
|
||||
virtualInput = document.createElement 'input'
|
||||
virtualInput.type = 'password'
|
||||
virtualInput.style.position = 'fixed'
|
||||
virtualInput.style.top = 0
|
||||
virtualInput.style.left = 0
|
||||
virtualInput.style.border = 'none'
|
||||
virtualInput.style.outline = 'none'
|
||||
virtualInput.style.opacity = 0
|
||||
virtualInput.value = '0'
|
||||
document.body.appendChild virtualInput
|
||||
|
||||
virtualInput.addEventListener 'blur', ->
|
||||
setTimeout((=> @focus()), 10)
|
||||
|
||||
addEventListener 'click', ->
|
||||
virtualInput.focus()
|
||||
|
||||
addEventListener 'touchstart', (e) ->
|
||||
if e.touches.length == 2
|
||||
ctrl = true
|
||||
else if e.touches.length == 3
|
||||
ctrl = false
|
||||
alt = true
|
||||
else if e.touches.length == 4
|
||||
ctrl = true
|
||||
alt = true
|
||||
|
||||
virtualInput.addEventListener 'keydown', (e) ->
|
||||
butterfly.keyDown(e)
|
||||
return true
|
||||
|
||||
virtualInput.addEventListener 'input', (e) ->
|
||||
len = @value.length
|
||||
|
||||
if len == 0
|
||||
e.keyCode = 8
|
||||
butterfly.keyDown e
|
||||
@value = '0'
|
||||
return true
|
||||
|
||||
e.keyCode = @value.charAt(1).charCodeAt(0)
|
||||
|
||||
if (ctrl or alt) and not first
|
||||
e.keyCode = @value.charAt(1).charCodeAt(0)
|
||||
e.ctrlKey = ctrl
|
||||
e.altKey = alt
|
||||
if e.keyCode >= 97 && e.keyCode <= 122
|
||||
e.keyCode -= 32
|
||||
butterfly.keyDown e
|
||||
@value = '0'
|
||||
ctrl = alt = false
|
||||
return true
|
||||
|
||||
butterfly.keyPress e
|
||||
first = false
|
||||
@value = '0'
|
||||
true
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -10,7 +10,7 @@
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.s
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
@@ -25,12 +25,6 @@ ws =
|
||||
|
||||
$ = document.querySelectorAll.bind(document)
|
||||
|
||||
uuid = ->
|
||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) ->
|
||||
r = Math.random() * 16 | 0
|
||||
v = if c is 'x' then r else (r & 0x3|0x8)
|
||||
v.toString(16)
|
||||
|
||||
document.addEventListener 'DOMContentLoaded', ->
|
||||
term = null
|
||||
|
||||
@@ -40,13 +34,14 @@ document.addEventListener 'DOMContentLoaded', ->
|
||||
wsUrl = 'ws://'
|
||||
|
||||
rootPath = document.body.getAttribute('data-root-path')
|
||||
rootPath = rootPath.replace(/^\/+|\/+$/g, '')
|
||||
if rootPath.length
|
||||
rootPath = "/#{rootPath}"
|
||||
|
||||
wsUrl += document.location.host + rootPath
|
||||
path = location.pathname
|
||||
path = '/'
|
||||
if path.indexOf('/session') < 0
|
||||
path += "session/#{uuid()}"
|
||||
path += "session/#{document.body.getAttribute('data-session-token')}"
|
||||
|
||||
path += location.search
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2015 Florian Mounier
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
@@ -34,6 +34,9 @@ cancel = (ev) ->
|
||||
ev.cancelBubble = true
|
||||
false
|
||||
|
||||
isMobile = ->
|
||||
/iPhone|iPad|iPod|Android/i.test navigator.userAgent
|
||||
|
||||
s = 0
|
||||
State =
|
||||
normal: s++
|
||||
@@ -63,43 +66,65 @@ class Terminal
|
||||
@document = @parent.ownerDocument
|
||||
@html = @document.getElementsByTagName('html')[0]
|
||||
@body = @document.getElementsByTagName('body')[0]
|
||||
@term = @document.getElementById('term')
|
||||
@forceWidth = @body.getAttribute(
|
||||
'data-force-unicode-width') is 'yes'
|
||||
|
||||
# A hidden textarea to capture all input events
|
||||
# This allows the body to receive IME composition events
|
||||
# without being `contentEditable`, which will mess up
|
||||
# the layout
|
||||
@inputHelper = @document.getElementById('input-helper')
|
||||
|
||||
# A simple div to take place of the IME input preview
|
||||
# which is now hidden due to the textarea
|
||||
@inputView = @document.getElementById('input-view')
|
||||
|
||||
# Main terminal element
|
||||
@body.className = 'terminal focus'
|
||||
@body.style.outline = 'none'
|
||||
@body.setAttribute 'tabindex', 0
|
||||
@body.setAttribute 'spellcheck', 'false'
|
||||
@inputHelper.setAttribute 'tabindex', 0
|
||||
@inputHelper.setAttribute 'spellcheck', 'false'
|
||||
|
||||
# Adding one line to compute char size
|
||||
div = @document.createElement('div')
|
||||
div.className = 'line'
|
||||
@body.appendChild(div)
|
||||
@children = [div]
|
||||
@term.appendChild(div)
|
||||
|
||||
@computeCharSize()
|
||||
@cols = Math.floor(@body.clientWidth / @charSize.width)
|
||||
@rows = Math.floor(window.innerHeight / @charSize.height)
|
||||
px = window.innerHeight % @charSize.height
|
||||
@body.style['padding-bottom'] = "#{px}px"
|
||||
|
||||
@scrollback = 1000000
|
||||
@buffSize = 100000
|
||||
|
||||
@visualBell = 100
|
||||
@convertEol = false
|
||||
@termName = 'xterm'
|
||||
@cursorBlink = true
|
||||
@cursorState = 0
|
||||
@inComposition = false
|
||||
@compositionText = ""
|
||||
|
||||
@resetVars()
|
||||
|
||||
@focus()
|
||||
|
||||
@startBlink()
|
||||
# IME Events should be registered to the textarea
|
||||
# The textarea helps guiding the IME to pop up
|
||||
# at the correct position
|
||||
@inputHelper.addEventListener 'compositionstart',
|
||||
@compositionStart.bind(@)
|
||||
@inputHelper.addEventListener 'compositionupdate',
|
||||
@compositionUpdate.bind(@)
|
||||
@inputHelper.addEventListener 'compositionend',
|
||||
@compositionEnd.bind(@)
|
||||
addEventListener 'keydown', @keyDown.bind(@)
|
||||
addEventListener 'keypress', @keyPress.bind(@)
|
||||
# Always focus on the inputHelper textarea
|
||||
addEventListener 'keyup', => @inputHelper.focus()
|
||||
if isMobile()
|
||||
addEventListener 'click', => @inputHelper.focus()
|
||||
addEventListener 'focus', @focus.bind(@)
|
||||
addEventListener 'blur', @blur.bind(@)
|
||||
addEventListener 'resize', => @resize()
|
||||
@@ -107,19 +132,18 @@ class Terminal
|
||||
@nativeScrollTo()
|
||||
, true
|
||||
|
||||
# # Horrible Firefox paste workaround
|
||||
if typeof InstallTrigger isnt "undefined"
|
||||
@body.contentEditable = 'true'
|
||||
|
||||
@initmouse()
|
||||
addEventListener 'load', => @resize()
|
||||
@emit 'load'
|
||||
@active = null
|
||||
|
||||
emit: (hook, args...) ->
|
||||
emit: (hook, obj) ->
|
||||
unless Terminal.hooks[hook]?
|
||||
Terminal.hooks[hook] = []
|
||||
for fun in Terminal.hooks[hook]
|
||||
fun.apply(@, args)
|
||||
# fun.call(@, obj)
|
||||
setTimeout ((f) -> ->
|
||||
f.call(@, obj))(fun), 10
|
||||
|
||||
cloneAttr: (a, char=null) ->
|
||||
bg: a.bg
|
||||
@@ -133,6 +157,7 @@ class Terminal
|
||||
italic: a.italic
|
||||
faint: a.faint
|
||||
crossed: a.crossed
|
||||
placeholder: false
|
||||
|
||||
equalAttr: (a, b) ->
|
||||
# Not testing char
|
||||
@@ -142,12 +167,14 @@ class Terminal
|
||||
a.italic is b.italic and a.faint is b.faint and
|
||||
a.crossed is b.crossed)
|
||||
|
||||
putChar: (c) ->
|
||||
putChar: (c, placeholder = false) ->
|
||||
newChar = @cloneAttr @curAttr, c
|
||||
newChar.placeholder = placeholder
|
||||
if @insertMode
|
||||
@screen[@y + @shift].chars.splice(@x, 0, @cloneAttr @curAttr, c)
|
||||
@screen[@y + @shift].chars.splice(@x, 0, newChar)
|
||||
@screen[@y + @shift].chars.pop()
|
||||
else
|
||||
@screen[@y + @shift].chars[@x] = @cloneAttr @curAttr, c
|
||||
@screen[@y + @shift].chars[@x] = newChar
|
||||
|
||||
@screen[@y + @shift].dirty = true
|
||||
|
||||
@@ -189,26 +216,28 @@ class Terminal
|
||||
italic: false
|
||||
faint: false
|
||||
crossed: false
|
||||
placeholder: false
|
||||
|
||||
@curAttr = @cloneAttr @defAttr
|
||||
@params = []
|
||||
@currentParam = 0
|
||||
@prefix = ""
|
||||
@screen = []
|
||||
i = @rows
|
||||
@shift = 0
|
||||
@screen.push @blankLine(false, false) while i--
|
||||
for row in [0..@rows - 1]
|
||||
@screen.push @blankLine(false, false)
|
||||
@setupStops()
|
||||
@skipNextKey = null
|
||||
|
||||
computeCharSize: ->
|
||||
testSpan = document.createElement('span')
|
||||
testSpan.textContent = '0123456789'
|
||||
@children[0].appendChild(testSpan)
|
||||
line = @term.firstChild
|
||||
line.appendChild(testSpan)
|
||||
@charSize =
|
||||
width: testSpan.getBoundingClientRect().width / 10
|
||||
height: @children[0].getBoundingClientRect().height
|
||||
@children[0].removeChild(testSpan)
|
||||
height: line.getBoundingClientRect().height
|
||||
line.removeChild(testSpan)
|
||||
|
||||
eraseAttr: ->
|
||||
erased = @cloneAttr @defAttr
|
||||
@@ -223,6 +252,7 @@ class Terminal
|
||||
@showCursor()
|
||||
@body.classList.add('focus')
|
||||
@body.classList.remove('blur')
|
||||
@inputHelper.focus() # Always focus on the textarea
|
||||
@resize()
|
||||
|
||||
@scrollLock = old_sl
|
||||
@@ -415,167 +445,161 @@ class Terminal
|
||||
sendButton ev
|
||||
cancel ev
|
||||
|
||||
refresh: (force=false) ->
|
||||
for cursor in @body.querySelectorAll(".cursor")
|
||||
cursor.parentNode.replaceChild(
|
||||
@document.createTextNode(cursor.textContent), cursor)
|
||||
for active in @body.querySelectorAll(".line.active")
|
||||
active.classList.remove('active')
|
||||
getClasses: (data) ->
|
||||
classes = []
|
||||
styles = []
|
||||
# bold
|
||||
classes.push "bold" if data.bold
|
||||
# underline
|
||||
classes.push "underline" if data.underline
|
||||
# blink
|
||||
classes.push "blink" if data.blink is 1
|
||||
classes.push "blink-fast" if data.blink is 2
|
||||
# inverse
|
||||
classes.push "reverse-video" if data.inverse
|
||||
# invisible
|
||||
classes.push "invisible" if data.invisible
|
||||
# italic
|
||||
classes.push "italic" if data.italic
|
||||
# faint
|
||||
classes.push "faint" if data.faint
|
||||
# crossed
|
||||
classes.push "crossed" if data.crossed
|
||||
|
||||
newOut = ''
|
||||
modified = []
|
||||
for line, j in @screen
|
||||
continue unless line.dirty or force
|
||||
out = ""
|
||||
if typeof data.fg is 'number'
|
||||
fg = data.fg
|
||||
if data.bold and fg < 8
|
||||
fg += 8
|
||||
classes.push "fg-color-" + fg
|
||||
else if typeof data.fg is 'string'
|
||||
styles.push "color: " + data.fg
|
||||
|
||||
if j is @y + @shift and not @cursorHidden
|
||||
x = @x
|
||||
if typeof data.bg is 'number'
|
||||
classes.push "bg-color-" + data.bg
|
||||
else if typeof data.bg is 'string'
|
||||
styles.push "background-color: " + data.bg
|
||||
|
||||
[classes, styles]
|
||||
|
||||
# Fullwidth (CJK) character ranges
|
||||
isCJK: (ch) ->
|
||||
"\u4e00" <= ch <= "\u9fff" or # CJK Unified Ideographs
|
||||
"\u3040" <= ch <= "\u30ff" or # Japanese Hiragana and Katakana
|
||||
"\u31f0" <= ch <= "\u31ff" or # Japanese Katakana Phonetic Extensions
|
||||
"\u3190" <= ch <= "\u319f" or # Japanese Kanbun symbols
|
||||
"\u3301" <= ch <= "\u3356" or # Japanese compound characters Kumimoji (組文字)
|
||||
"\uac00" <= ch <= "\ud7ff" or # Hangul precomposed syllables
|
||||
"\u3000" <= ch <= "\u303f" or # CJK Punctuations and Symbols
|
||||
"\uff00" <= ch <= "\uff60" or # Fullwidth forms
|
||||
"\uffe0" <= ch <= "\uffe6" # Fullwidth forms
|
||||
|
||||
charToDom: (data, attr, cursor) ->
|
||||
# Just do not render if we see any placeholder characters
|
||||
return if data.placeholder
|
||||
return data.html if data.html
|
||||
attr = attr or @cloneAttr @defAttr
|
||||
ch = data.ch
|
||||
char = ''
|
||||
unless @equalAttr data, attr
|
||||
char += "</span>" unless @equalAttr attr, @defAttr
|
||||
unless @equalAttr data, @defAttr
|
||||
[classes, styles] = @getClasses data
|
||||
char += "<span class=\"#{classes.join(" ")}\""
|
||||
char += " style=\"" + styles.join("; ") + "\"" if styles.length
|
||||
char += ">"
|
||||
|
||||
char += "<span class=\"#{
|
||||
if @cursorState then "reverse-video " else ""}cursor\">" if cursor
|
||||
|
||||
switch ch
|
||||
when "&"
|
||||
char += "&"
|
||||
when "<"
|
||||
char += "<"
|
||||
when ">"
|
||||
char += ">"
|
||||
when " "
|
||||
char += '<span class="nbsp">\u2007</span>'
|
||||
else
|
||||
x = -Infinity
|
||||
|
||||
attr = @cloneAttr @defAttr
|
||||
skipnext = false
|
||||
for i in [0..@cols - 1]
|
||||
data = line.chars[i]
|
||||
if data.html
|
||||
out += data.html
|
||||
break
|
||||
if skipnext
|
||||
skipnext = false
|
||||
continue
|
||||
|
||||
ch = data.ch
|
||||
unless @equalAttr data, attr
|
||||
out += "</span>" unless @equalAttr attr, @defAttr
|
||||
unless @equalAttr data, @defAttr
|
||||
classes = []
|
||||
styles = []
|
||||
out += "<span "
|
||||
|
||||
# bold
|
||||
classes.push "bold" if data.bold
|
||||
# underline
|
||||
classes.push "underline" if data.underline
|
||||
# blink
|
||||
classes.push "blink" if data.blink is 1
|
||||
classes.push "blink-fast" if data.blink is 2
|
||||
# inverse
|
||||
classes.push "reverse-video" if data.inverse
|
||||
# invisible
|
||||
classes.push "invisible" if data.invisible
|
||||
# italic
|
||||
classes.push "italic" if data.italic
|
||||
# faint
|
||||
classes.push "faint" if data.faint
|
||||
# crossed
|
||||
classes.push "crossed" if data.crossed
|
||||
|
||||
if typeof data.fg is 'number'
|
||||
fg = data.fg
|
||||
if data.bold and fg < 8
|
||||
fg += 8
|
||||
classes.push "fg-color-" + fg
|
||||
|
||||
if typeof data.fg is 'string'
|
||||
styles.push "color: " + data.fg
|
||||
|
||||
if typeof data.bg is 'number'
|
||||
classes.push "bg-color-" + data.bg
|
||||
|
||||
if typeof data.bg is 'string'
|
||||
styles.push "background-color: " + data.bg
|
||||
|
||||
out += "class=\""
|
||||
out += classes.join(" ")
|
||||
out += "\""
|
||||
if styles.length
|
||||
out += " style=\"" + styles.join("; ") + "\""
|
||||
out += ">"
|
||||
|
||||
out += "<span class=\"" + (
|
||||
if @cursorState then "reverse-video " else ""
|
||||
) + "cursor\">" if i is x
|
||||
|
||||
# This is a temporary dirty hack for raw html insertion
|
||||
if ch.length > 1
|
||||
out += ch
|
||||
if ch <= " "
|
||||
char += " "
|
||||
# CJK characters should always be forced to be fullwidth
|
||||
else unless @forceWidth or @isCJK ch
|
||||
char += ch
|
||||
else
|
||||
switch ch
|
||||
when "&"
|
||||
out += "&"
|
||||
when "<"
|
||||
out += "<"
|
||||
when ">"
|
||||
out += ">"
|
||||
else
|
||||
if ch == " "
|
||||
out += '<span class="nbsp">\u2007</span>'
|
||||
else if ch <= " "
|
||||
out += " "
|
||||
else if not @forceWidth or ch <= "~" # Ascii chars
|
||||
out += ch
|
||||
else if "\uff00" < ch < "\uffef"
|
||||
skipnext = true
|
||||
out += "<span style=\"display: inline-block;
|
||||
width: #{2 * @charSize.width}px\">#{ch}</span>"
|
||||
else
|
||||
out += "<span style=\"display: inline-block;
|
||||
width: #{@charSize.width}px\">#{ch}</span>"
|
||||
if ch <= "~" # Ascii chars
|
||||
char += ch
|
||||
else if @isCJK ch # CJK always fullwidth
|
||||
char += "<span style=\"display: inline-block; width: #{
|
||||
2 * @charSize.width}px\">#{ch}</span>"
|
||||
else
|
||||
char += "<span style=\"display: inline-block; width: #{
|
||||
@charSize.width}px\">#{ch}</span>"
|
||||
|
||||
out += "</span>" if i is x
|
||||
attr = data
|
||||
out += "</span>" unless @equalAttr attr, @defAttr
|
||||
out += '\u23CE' if line.wrap
|
||||
if line.extra
|
||||
out += '<span class="extra">' + line.extra + '</span>'
|
||||
if @children[j]
|
||||
@children[j].innerHTML = out
|
||||
modified.push @children[j]
|
||||
if x isnt -Infinity
|
||||
@children[j].classList.add 'active'
|
||||
if line.extra
|
||||
@children[j].classList.add 'extended'
|
||||
char += "</span>" if cursor
|
||||
char
|
||||
|
||||
lineToDom: (y, line, active) ->
|
||||
cursorX = @x if active
|
||||
for x in [0..@cols]
|
||||
unless x is @cols
|
||||
@charToDom line.chars[x], line.chars[x - 1], x is cursorX
|
||||
else
|
||||
cls = ['line']
|
||||
if x isnt -Infinity
|
||||
cls.push 'active'
|
||||
if line.extra
|
||||
cls.push 'extended'
|
||||
newOut += "<div class=\"#{cls.join(' ')}\">#{out}</div>"
|
||||
@screen[j].dirty = false
|
||||
eol = ''
|
||||
eol += '</span>' unless @equalAttr line.chars[x - 1], @defAttr
|
||||
eol += '\u23CE' if line.wrap
|
||||
eol += "<span class=\"extra\">#{line.extra}</span>" if line.extra
|
||||
|
||||
if newOut isnt ''
|
||||
group = @document.createElement('div')
|
||||
group.className = 'group'
|
||||
group.innerHTML = newOut
|
||||
modified.push group
|
||||
@body.appendChild group
|
||||
@screen = @screen.slice(-@rows)
|
||||
@shift = 0
|
||||
screenToDom: (force) ->
|
||||
for line, y in @screen
|
||||
if line.dirty or force
|
||||
active = y is @y + @shift and not @cursorHidden
|
||||
div = document.createElement 'div'
|
||||
div.classList.add 'line'
|
||||
div.classList.add 'active' if active
|
||||
div.classList.add 'extended' if line.extra
|
||||
div.innerHTML = (@lineToDom y, line, active).join('')
|
||||
if active
|
||||
@active = div
|
||||
@cursor = div.querySelectorAll('.cursor')[0]
|
||||
div
|
||||
|
||||
lines = document.querySelectorAll('.line')
|
||||
if lines.length > @scrollback
|
||||
for line in Array.prototype.slice.call(
|
||||
lines, 0, lines.length - @scrollback)
|
||||
line.remove()
|
||||
for group in document.querySelectorAll('.group:empty')
|
||||
group.remove()
|
||||
lines = document.querySelectorAll('.line')
|
||||
@children = Array.prototype.slice.call(
|
||||
lines, -@rows)
|
||||
writeDom: (dom) ->
|
||||
r = Math.max @term.childElementCount - @rows, 0
|
||||
for line, y in dom
|
||||
continue unless line
|
||||
@screen[y].dirty = false
|
||||
if y < @rows and y < @term.childElementCount
|
||||
@term.replaceChild(line, @term.childNodes[r + y])
|
||||
else
|
||||
frag = frag or document.createDocumentFragment('fragment')
|
||||
frag.appendChild line
|
||||
@emit 'change', line
|
||||
|
||||
frag and @term.appendChild frag
|
||||
|
||||
@shift = 0
|
||||
@screen = @screen.slice -@rows
|
||||
|
||||
refresh: (force=false) ->
|
||||
if @active?
|
||||
@active.classList.remove('active')
|
||||
if @cursor
|
||||
@cursor.parentNode?.replaceChild(
|
||||
@document.createTextNode(@cursor.textContent), @cursor)
|
||||
dom = @screenToDom(force)
|
||||
@writeDom dom
|
||||
@nativeScrollTo()
|
||||
@emit 'change', modified
|
||||
@updateInputViews()
|
||||
@emit 'refresh'
|
||||
|
||||
_cursorBlink: ->
|
||||
@cursorState ^= 1
|
||||
cursor = @body.querySelector(".cursor")
|
||||
return unless cursor
|
||||
if cursor.classList.contains("reverse-video")
|
||||
cursor.classList.remove "reverse-video"
|
||||
return unless @cursor
|
||||
if @cursor.classList.contains("reverse-video")
|
||||
@cursor.classList.remove "reverse-video"
|
||||
else
|
||||
cursor.classList.add "reverse-video"
|
||||
|
||||
@cursor.classList.add "reverse-video"
|
||||
|
||||
showCursor: ->
|
||||
unless @cursorState
|
||||
@@ -583,19 +607,16 @@ class Terminal
|
||||
@screen[@y + @shift].dirty = true
|
||||
@refresh()
|
||||
|
||||
|
||||
startBlink: ->
|
||||
return unless @cursorBlink
|
||||
@_blinker = => @_cursorBlink()
|
||||
@t_blink = setInterval(@_blinker, 500)
|
||||
|
||||
|
||||
refreshBlink: ->
|
||||
return unless @cursorBlink
|
||||
clearInterval @t_blink
|
||||
@t_blink = setInterval(@_blinker, 500)
|
||||
|
||||
|
||||
scroll: ->
|
||||
# Use emulated scroll in alternate buffer or when scroll region is defined
|
||||
if @normal or @scrollTop isnt 0 or @scrollBottom isnt @rows - 1
|
||||
@@ -718,12 +739,15 @@ class Terminal
|
||||
@x = 0
|
||||
@putChar ch
|
||||
@x++
|
||||
if @forceWidth and "\uff00" < ch < "\uffef"
|
||||
if @cols < 2 or @x >= @cols
|
||||
@putChar " "
|
||||
break
|
||||
|
||||
@putChar " "
|
||||
if @isCJK ch
|
||||
# Add a dummy, placeholder character
|
||||
# for double-width, CJK characters
|
||||
# In order to fix counting of characters
|
||||
# when calculating for remaining cols
|
||||
# They are always considered to be
|
||||
# @forceWidth because otherwise they
|
||||
# do not render properly at all
|
||||
@putChar " ", true
|
||||
@x++
|
||||
|
||||
when State.escaped
|
||||
@@ -1277,11 +1301,96 @@ class Terminal
|
||||
writeln: (data) ->
|
||||
@write "#{data}\r\n"
|
||||
|
||||
updateInputViews: ->
|
||||
# Re-position the textarea and the preview box
|
||||
# to the current position of the cursor
|
||||
cursorPos = @cursor.getBoundingClientRect()
|
||||
@inputView.style['left'] = cursorPos.left + "px"
|
||||
@inputView.style['top'] = cursorPos.top + "px"
|
||||
@inputHelper.style['left'] = cursorPos.left + "px"
|
||||
@inputHelper.style['top'] = cursorPos.top + "px"
|
||||
# Clear the textarea as often as possible
|
||||
@inputHelper.value = ""
|
||||
|
||||
compositionStart: (ev) ->
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
@updateInputViews()
|
||||
|
||||
# Show the preview box
|
||||
@inputView.className = ""
|
||||
@inputView.innerText = ""
|
||||
|
||||
# Hide the blinking cursor
|
||||
@cursor.style['visibility'] = "hidden"
|
||||
|
||||
@inComposition = true
|
||||
@compositionText = ""
|
||||
return false
|
||||
|
||||
compositionUpdate: (ev) ->
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
# Update the composition text
|
||||
@compositionText = ev.data
|
||||
@inputView.innerText = @compositionText
|
||||
return false
|
||||
|
||||
compositionEnd: (ev) ->
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
@finishComposition()
|
||||
return false
|
||||
|
||||
finishComposition: ->
|
||||
@inComposition = false
|
||||
@showCursor()
|
||||
@inputHelper.value = ""
|
||||
@inputView.className = "hidden"
|
||||
@send @compositionText
|
||||
@compositionText = ""
|
||||
# Force focus on the inputHelper
|
||||
@inputHelper.focus()
|
||||
|
||||
keyDown: (ev) ->
|
||||
if @inComposition
|
||||
# Continue IME composition if the character is
|
||||
# composition key or modifier key
|
||||
if ev.keyCode is 229
|
||||
return false
|
||||
else if ev.keyCode is 16 || ev.keyCode is 17 || ev.keyCode is 18
|
||||
return false
|
||||
# Otherwise, if we receive a keyDown, abort the composition
|
||||
@finishComposition()
|
||||
|
||||
if ev.keyCode is 229
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
# If the composition key is sent while IME not active
|
||||
# it means that some character have been input while IME
|
||||
# enabled, i.e. punctuations
|
||||
# in which case we just fetch it from the text area
|
||||
setTimeout =>
|
||||
unless @inComposition || @inputHelper.value.length > 1
|
||||
val = @inputHelper.value
|
||||
@inputHelper.value = "" # Clear the value immediately
|
||||
char = val.toUpperCase().charCodeAt(0)
|
||||
if 65 <= char <= 90
|
||||
# If the character sent here is a letter
|
||||
# allow it to be overridden on mobile
|
||||
# Combinations like "Ctrl+C" won't work without this
|
||||
# on Chrome for Android
|
||||
e = new KeyboardEvent 'keydown', keyCode: char
|
||||
return if window.mobileKeydown e
|
||||
@send val
|
||||
, 0
|
||||
return false
|
||||
|
||||
# Key Resources:
|
||||
# https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
||||
# Don't handle modifiers alone
|
||||
return true if ev.keyCode > 15 and ev.keyCode < 19
|
||||
return true if window.mobileKeydown ev
|
||||
|
||||
if ev.keyCode is 19 # Pause break
|
||||
@body.classList.add 'stopped'
|
||||
@@ -1294,7 +1403,11 @@ class Terminal
|
||||
return true if (ev.shiftKey or ev.ctrlKey) and ev.keyCode is 45
|
||||
|
||||
# Let the ctrl+shift+c, ctrl+shift+v go through to handle native copy paste
|
||||
return true if (ev.shiftKey and ev.ctrlKey) and ev.keyCode in [67, 86]
|
||||
if (ev.shiftKey and ev.ctrlKey) and ev.keyCode in [67, 86]
|
||||
# Make the content temporarily ediatble, to allow the paste event
|
||||
# to propagate (this does not work for the textarea if not set like this)
|
||||
@body.contentEditable = true
|
||||
return true
|
||||
|
||||
# Alt-z works as an escape to relay the following keys to the browser.
|
||||
# usefull to trigger browser shortcuts, i.e.: Alt+Z F5 to reload
|
||||
@@ -1572,11 +1685,13 @@ class Terminal
|
||||
oldRows = @rows
|
||||
@computeCharSize()
|
||||
w = @body.clientWidth
|
||||
h = @html.clientHeight - (@html.offsetHeight - @html.scrollHeight)
|
||||
h = @html.clientHeight #- (@html.offsetHeight - @html.scrollHeight)
|
||||
|
||||
if @charSize.width is 0 or @charSize.height is 0
|
||||
console.error 'Null size in refresh'
|
||||
return
|
||||
@cols = x or Math.floor(w / @charSize.width)
|
||||
@rows = y or Math.floor(h / @charSize.height)
|
||||
px = h % @charSize.height
|
||||
@body.style['padding-bottom'] = "#{px}px"
|
||||
|
||||
@cols = Math.max 1, @cols
|
||||
@rows = Math.max 1, @rows
|
||||
@@ -1589,71 +1704,51 @@ class Terminal
|
||||
cmd: 'size', cols: @cols, rows: @rows)) unless notif
|
||||
|
||||
# resize cols
|
||||
if oldCols < @cols
|
||||
if @cols > oldCols
|
||||
# does xterm use the default attr?
|
||||
i = @screen.length
|
||||
while i--
|
||||
@screen[i].chars.push @defAttr while @screen[i].chars.length < @cols
|
||||
@screen[i].wrap = false
|
||||
for line in @screen
|
||||
line.chars.push @defAttr while line.chars.length < @cols
|
||||
line.wrap = false
|
||||
if @normal
|
||||
for line in @normal.screen
|
||||
line.chars.push @defAttr while line.chars.length < @cols
|
||||
line.wrap = false
|
||||
|
||||
else if oldCols > @cols
|
||||
i = @screen.length
|
||||
while i--
|
||||
@screen[i].chars.pop() while @screen[i].chars.length > @cols
|
||||
else if @cols < oldCols
|
||||
for line in @screen
|
||||
line.chars.pop() while line.chars.length > @cols
|
||||
if @normal
|
||||
for line in @normal.screen
|
||||
line.chars.pop() while line.chars.length > @cols
|
||||
|
||||
@setupStops oldCols
|
||||
|
||||
if @term.childElementCount >= @rows
|
||||
@y += @rows - oldRows
|
||||
insert = 'unshift'
|
||||
else
|
||||
insert = 'push'
|
||||
|
||||
# resize rows
|
||||
j = oldRows
|
||||
if j < @rows
|
||||
el = @body
|
||||
while j++ < @rows
|
||||
@screen.push @blankLine() if @screen.length < @rows
|
||||
if @children.length < @rows
|
||||
line = @document.createElement("div")
|
||||
line.className = 'line'
|
||||
el.appendChild line
|
||||
@children.push line
|
||||
else if j > @rows
|
||||
while j-- > @rows
|
||||
@screen.pop() if @screen.length > @rows
|
||||
if @children.length > @rows
|
||||
el = @children.pop()
|
||||
el?.parentNode.removeChild el
|
||||
while @screen.length > @rows
|
||||
@screen.shift()
|
||||
while @screen.length < @rows
|
||||
@screen[insert] @blankLine(false, false)
|
||||
|
||||
if @normal
|
||||
# resize cols
|
||||
if oldCols < @cols
|
||||
# does xterm use the default attr?
|
||||
i = @normal.screen.length
|
||||
while i--
|
||||
while @normal.screen[i].chars.length < @cols
|
||||
@normal.screen[i].chars.push @defAttr
|
||||
@normal.screen[i].wrap = false
|
||||
|
||||
else if oldCols > @cols
|
||||
i = @normal.screen.length
|
||||
while i--
|
||||
while @normal.screen[i].chars.length > @cols
|
||||
@normal.screen[i].chars.pop()
|
||||
|
||||
# resize rows
|
||||
j = oldRows
|
||||
if j < @rows
|
||||
while j++ < @rows
|
||||
@normal.screen.push @blankLine() if @normal.screen.length < @rows
|
||||
else if j > @rows
|
||||
while j-- > @rows
|
||||
@normal.screen.pop() if @normal.screen.length > @rows
|
||||
while @normal.screen.length > @rows
|
||||
@normal.screen.shift()
|
||||
while @normal.screen.length < @rows
|
||||
@normal.screen[insert] @blankLine(false, false)
|
||||
|
||||
# make sure the cursor stays on screen
|
||||
@y = @rows - 1 if @y >= @rows
|
||||
@y = 0 if @y < 0
|
||||
@x = @cols - 1 if @x >= @cols
|
||||
|
||||
@scrollTop = 0
|
||||
@scrollBottom = @rows - 1
|
||||
|
||||
@refresh(true)
|
||||
@refresh()
|
||||
@reset() if not notif and (x or y)
|
||||
|
||||
resizeWindowPlease: (cols) ->
|
||||
@@ -1753,17 +1848,9 @@ class Terminal
|
||||
clearScrollback: ->
|
||||
# In case of real hard reset
|
||||
# Drop DOM history
|
||||
lines = document.querySelectorAll('.line')
|
||||
if lines.length > @rows
|
||||
for line in Array.prototype.slice.call(
|
||||
lines, 0, lines.length - @rows)
|
||||
line.remove()
|
||||
for group in document.querySelectorAll('.group:empty')
|
||||
group.remove()
|
||||
lines = document.querySelectorAll('.line')
|
||||
@children = Array.prototype.slice.call(
|
||||
lines, -@rows)
|
||||
|
||||
while @term.childElementCount > @rows
|
||||
@term.firstChild.remove()
|
||||
@emit 'clear'
|
||||
|
||||
# ESC H Tab Set (HTS is 0x88).
|
||||
tabSet: ->
|
||||
@@ -2184,8 +2271,8 @@ class Terminal
|
||||
@screen.splice @scrollBottom + @shift, 0, @blankLine(true)
|
||||
@screen.splice @y + @shift, 1
|
||||
unless @normal or @scrollTop isnt 0 or @scrollBottom isnt @rows - 1
|
||||
@children[@y + @shift].remove()
|
||||
@children.splice @y + @shift, 1
|
||||
node = @term.childElementCount - @rows + @y + @shift
|
||||
@term.childNodes[node].remove()
|
||||
|
||||
if @normal or @scrollTop isnt 0 or @scrollBottom isnt @rows - 1
|
||||
for i in [@y + @shift..@screen.length - 1]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "butterfly",
|
||||
"version": "2.0.2",
|
||||
"version": "3.0.0",
|
||||
"description": "A sleek web based terminal emulator",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -20,6 +20,6 @@
|
||||
"grunt-contrib-cssmin": "^1.0.1",
|
||||
"grunt-contrib-uglify": "^1.0.1",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-sass": "^1.2.0"
|
||||
"grunt-sass": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[tool:pytest]
|
||||
flake8-ignore =
|
||||
*.py E731 E402
|
||||
butterfly/bin/help.py E501
|
||||
|
||||
38
setup.py
38
setup.py
@@ -5,27 +5,31 @@
|
||||
Butterfly - A sleek web based terminal emulator
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
ROOT = os.path.dirname(__file__)
|
||||
with open(os.path.join(ROOT, 'butterfly', '__init__.py')) as fd:
|
||||
__version__ = re.search("__version__ = '([^']+)'", fd.read()).group(1)
|
||||
about = {}
|
||||
with open(os.path.join(
|
||||
os.path.dirname(__file__), "butterfly", "__about__.py")) as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
options = dict(
|
||||
name="butterfly",
|
||||
version=__version__,
|
||||
description="A sleek web based terminal emulator",
|
||||
long_description="See http://github.com/paradoxxxzero/butterfly",
|
||||
author="Florian Mounier",
|
||||
author_email="paradoxxx.zero@gmail.com",
|
||||
url="http://github.com/paradoxxxzero/butterfly",
|
||||
license="GPLv3",
|
||||
setup(
|
||||
name=about['__title__'],
|
||||
version=about['__version__'],
|
||||
description=about['__summary__'],
|
||||
url=about['__uri__'],
|
||||
author=about['__author__'],
|
||||
author_email=about['__email__'],
|
||||
license=about['__license__'],
|
||||
platforms="Any",
|
||||
scripts=['butterfly.server.py', 'scripts/butterfly', 'scripts/b'],
|
||||
packages=['butterfly'],
|
||||
install_requires=["tornado>=3.2", "pyOpenSSL", 'tornado_systemd'],
|
||||
extras_requires=["libsass"],
|
||||
install_requires=["tornado>=3.2", "pyOpenSSL"],
|
||||
extras_require={
|
||||
'themes': ["libsass"],
|
||||
'systemd': ['tornado_systemd'],
|
||||
'lint': ['pytest', 'pytest-flake8', 'pytest-isort']
|
||||
},
|
||||
package_data={
|
||||
'butterfly': [
|
||||
'sass/*.sass',
|
||||
@@ -44,12 +48,10 @@ options = dict(
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Terminals"])
|
||||
|
||||
setup(**options)
|
||||
|
||||
Reference in New Issue
Block a user