86 Commits
2ws ... master

Author SHA1 Message Date
Florian Mounier
da79ffe04b Changelog 2018-09-12 10:23:06 +02:00
Florian Mounier
c348e1f285 Bump 3.2.5 2018-09-12 10:22:02 +02:00
Florian Mounier
91d52ed6ae Fix coffee as per #179 2018-09-12 10:20:10 +02:00
Mounier Florian
06751c68f9 Merge pull request #179 from GrahamDumpleton/uri-root-path-coffee
Apply uri-root-path change to coffee/sass files and regenerate.
2018-09-12 10:16:49 +02:00
Graham Dumpleton
a9854e9136 Apply uri-root-path change to coffee/sass scripts and regenerate. 2018-09-12 11:16:12 +10:00
Florian Mounier
039c730409 Bump 3.2.4 2018-09-03 14:41:39 +02:00
Florian Mounier
82676862ca Fix one-shot auto-open url when uri-root-path is used. 2018-09-03 11:54:38 +02:00
Mounier Florian
5b6b61286d Merge pull request #173 from GrahamDumpleton/uri-root-path
Fix up --uri-root-path so behaves as one would expect for this.
2018-09-03 11:42:36 +02:00
Mounier Florian
f32cb4d358 Merge pull request #172 from ZoomerAnalytics/fix-keepalive-ping
added missing keepalive_timer.start()
2018-09-03 11:27:55 +02:00
Graham Dumpleton
ad155f1f17 Only create default conf file after options are parsed. 2018-08-30 12:30:22 +10:00
Felix Zumstein
9e1045de9b added missing keepalive_timer.start() 2018-08-26 22:54:53 +02:00
Graham Dumpleton
db3d37f6fe Fix up generation of URLs with prefix. 2018-08-23 13:26:16 +10:00
Graham Dumpleton
611f2e30d6 Add uri root path before all routes. 2018-08-23 11:53:38 +10:00
Florian Mounier
1984e4b869 Fix compare tags in Changelog 2018-06-04 11:20:09 +02:00
Florian Mounier
f58ea904b3 Merge branch 'master' of github.com:paradoxxxzero/butterfly 2018-06-04 11:15:47 +02:00
Florian Mounier
af0f4d20fe Update Changelog 2018-06-04 11:15:24 +02:00
Mounier Florian
10b5ce3bcc Merge pull request #161 from k4pu77/master
Updated docker baseimage
2018-06-04 11:12:33 +02:00
Florian Mounier
a0287946d9 Bump 3.2.3 2018-06-04 11:05:07 +02:00
Florian Mounier
fbd71d55ef Fix lint 2018-06-04 11:03:06 +02:00
Peter Cai
0ac8437387 term: fix password input on Chrome for Android
1. Also force focus on inputHelper on keyup on Android
2. Clear the inputHelper immediately upon receiving input
2018-06-03 20:58:39 +08:00
Peter Cai
866b56b682 term: bring back touch simulation of special keys on mobile
and also fixed it. The original version did not work because it tried
to change read-only fields of the event, which is not allowed.

The last commit removed support of touch simulation of Ctrl and Alt
by removing the `virtual_input.coffee` file. This commit brings it back
with a better implementation.
2018-06-03 20:10:24 +08:00
Peter Cai
4d87059872 remove unneeded virtual_input
We have already introduced a virtual textarea for every platform.
This one seems redundant.

However, some features may still not work perfectly on a mobile browser
2018-06-03 18:03:23 +08:00
Peter Cai
5bbe456496 term: remove redundant events of inputHelper and redundant contentEditable
We do not need to listen for keydown and keypress for inputHelper because these events will propagate through the parent.
Listening them will be redundant and will cause some shortcut key combinations to stop working.

Since we now have a hidden `textarea`, there is no longer need to set anything to contentEditable
2018-06-03 17:51:16 +08:00
Peter Cai
5b9cc257a8 term: do not re-focus on keyup when on mobile
Doing this will mess up the mobile browsers.
Although we still don't support mobile browsers very well, this can at least make it usable on them.
2018-06-03 12:19:43 +08:00
Peter Cai
34b6287e0c term: complete support for IME & CJK rendering
this fixes #75 and #47, two bugs originated long long ago.

1. Added support for IME events `compositionstart` `compositionupdate` and `compositionend`.
2. Refactored some code to receive input events from a hidden textarea just as how `xterm.js` now does. This removes the need to set `contentEditable` on the body in order to receive IME compistion events, and also guides the IME input box correctly following the cursor.
3. Fixed CJK rendering. Forces "forceWidth" mode with double width on those known CJK ranges in Unicode. Corrected the placeholder logic of the force width mode. Note that some rare halfwidth CJK characters will still not render correctly without `force-unicode-width` enabled. If you see any issue, please enable the `--force-unicode-width` option.
4. Miscallaneous fixes for some problems after introducing the above change

Tested on Firefox Nightly 62 on Linux and Chromium 67 on Linux, with `fcitx` as input method.
2018-06-03 10:27:49 +08:00
Peter Cai
41ee5fb843 update grunt-sass
the old grunt-sass no longer works with newer node.js
2018-06-03 10:12:19 +08:00
Christoph Christen
ae6b36fa89 Updated docker baseimage 2018-01-02 21:05:49 +01:00
Mounier Florian
cfda54a724 Merge pull request #158 from brentley/master
updating setuptools
2017-12-19 18:02:48 +01:00
Brent Langston
033169ab08 updating setuptools 2017-12-19 10:56:53 -06:00
Florian Mounier
920c435b00 Bump 3.2.2 2017-11-23 14:57:08 +01:00
Florian Mounier
27e6aa8a5d Update changelog 2017-11-23 14:56:56 +01:00
Florian Mounier
92633f52ce Fix unescaping entities when linkifying 2017-11-23 14:56:18 +01:00
Florian Mounier
f5f854964b Bump 3.2.1 2017-09-27 11:55:15 +02:00
Florian Mounier
55528fdf91 Issue correct X.509 v3 certificates (you will need to re-generate your certs) 2017-09-27 11:54:58 +02:00
Florian Mounier
9eae13486e Use X509 v4. 2017-09-27 11:34:41 +02:00
Florian Mounier
79bd074dae Bump 3.2.0 2017-09-27 10:59:19 +02:00
Mounier Florian
7b0ba2bfe7 Merge pull request #147 from 3ch01c/master
updated cert generation to v3 to comply with new browser standards
2017-09-21 17:58:55 +02:00
Mounier Florian
db17b9d8ac Merge pull request #152 from f0ma/master
Fix problem with ignoring --shell option in python2
2017-09-21 17:58:11 +02:00
Stanislav Ivanov
b5de82bfcf Fix problem with ignoring --shell option in python2 2017-09-21 18:04:03 +03:00
Jack Miner
13dbe0434c updated cert generation to v3 to comply with new browser standards 2017-07-24 17:52:14 -06:00
Florian Mounier
ef0057c23f Bump 3.1.5 2017-05-29 10:32:28 +02:00
Mounier Florian
6bc8e1438f Merge pull request #146 from warpkwd/fix_i_dont_options
fix i-hereby-...-whatsoever option
2017-05-29 10:21:39 +02:00
Yukihiro KAWADA
8856ea9dc4 fix i-hereby-...-whatsoever option
i-hereby-...-whatsoever revise to i_hereby_whatsoever
2017-05-24 12:59:18 +09:00
Florian Mounier
4edb2d269f Bump 3.1.4 2017-05-15 15:32:50 +02:00
Florian Mounier
272891470c Add --i-hereby-declare-i-dont-want-any-security-whatsoever option. Fix #143 2017-05-15 15:32:39 +02:00
Florian Mounier
574b3dc74b Bump 3.1.3 2017-05-15 11:31:07 +02:00
Florian Mounier
269dd2b618 Fix crash on lsof on python3 2017-05-15 11:30:59 +02:00
Florian Mounier
0625e05cbb Actually fix white-space on folded lines. 2017-05-10 16:01:40 +02:00
Florian Mounier
6b1101bc45 Fix white-space on folded lines. 2017-05-10 15:59:42 +02:00
Florian Mounier
3e6d0b203f Bump 3.1.2 2017-05-03 10:27:40 +02:00
Florian Mounier
8189598dd6 Add __about__ __all__ 2017-05-03 10:27:30 +02:00
Florian Mounier
4a8b5f2147 Add yarn.lock 2017-05-02 18:17:46 +02:00
Florian Mounier
f9a1ff4dea Bump 3.1.1 2017-05-02 18:12:36 +02:00
Florian Mounier
96d88a5e91 Bump 3.1.0 2017-05-02 18:00:24 +02:00
Florian Mounier
bdc1c7a80d Add a Makefile. Lint code. Fix butterfly open. Add a CHANGELOG.md 2017-05-02 17:59:52 +02:00
Florian Mounier
eacfdcd52f Fix huge performance loss on extended lines 2017-04-04 18:14:27 +02:00
Florian Mounier
ed347e2bd0 Use a __about__ file 2017-03-30 10:20:25 +02:00
Florian Mounier
3228e8c204 Integrate new themes 2017-03-30 10:14:03 +02:00
Florian Mounier
b9c991e3b6 Use pkg_resources and bump 3.0.1 2017-03-20 10:43:23 +01:00
Florian Mounier
8ad12c2379 Don't import pam if not necessary + pep8 2017-03-20 10:32:55 +01:00
Florian Mounier
2aa237ef12 Fix certificate generation 2017-03-13 11:59:10 +01:00
Florian Mounier
40496eb9d1 Protect session closing. References #124 2017-02-21 11:14:04 +01:00
Florian Mounier
ffd19b8162 setup.py typo 2017-02-13 15:25:22 +01:00
Florian Mounier
6663568500 Version to beta 2017-02-13 15:23:31 +01:00
Florian Mounier
3a09c47ef0 Fix #132 2017-02-13 15:10:54 +01:00
Florian Mounier
41ab0f36ff Fix user argument 2017-02-13 11:54:40 +01:00
Florian Mounier
70e00ac696 Fix pam condition 2017-02-13 11:36:29 +01:00
Florian Mounier
70369a0b32 Remove systemd direct calls 2017-02-13 11:26:39 +01:00
Florian Mounier
8c20ffb943 Merge remote-tracking branch 'PeterCxy/patch-pam' 2017-02-13 11:18:01 +01:00
Florian Mounier
729c768dc2 Happy new year (a bit late) 2017-02-13 11:16:59 +01:00
Florian Mounier
17f8c1d1c9 Merge branch 'master' of github.com:paradoxxxzero/butterfly 2017-02-13 11:03:20 +01:00
Florian Mounier
964fd07143 uuid4 from Math.random is a security flaw 2017-02-13 10:45:31 +01:00
Florian Mounier
8553bbd0cb Typo 2017-02-13 10:37:04 +01:00
Peter Cai
f494541652 pam: environment should be reinitialized after authentication 2017-02-11 09:00:24 +08:00
Peter Cai
dd6c917462 pam: authenticate in a separate process 2017-02-11 08:56:17 +08:00
Peter Cai
9e03e24764 terminal: support PAM authentication
Fix #129

Actually, we are reinventing the wheel... But after all, it is not
possible to change the profile name of `su`, so we just pull in the PAM
bindings for Python and use it for PAM authentication.

A new option `--pam_profile` has been added for users to specify their
preferred PAM profile. Note that Butterfly should be started as ROOT or
it will not be possible to authenticate via PAM.
2017-02-10 20:13:09 +08:00
Mounier Florian
6b5f3ac76f Merge pull request #131 from PeterCxy/patch-keepalive
Send ping packets to keep connection alive
2017-02-10 10:07:03 +01:00
Peter Cai
a36579bb12 Send ping packets to keep connection alive
Fix #126

Idle WebSocket connections tend to be closed after some period of time.
This commit enables the Butterfly server to send ping packets
periodically in order to keep the connection alive.

A new option `keepalive_interval` is also introduced for users to
specify the interval to send `ping` packets. By default it is 30
seconds.
2017-02-10 15:11:51 +08:00
Florian Mounier
e4ce69a967 Fix keyboard selection 2016-11-28 14:38:49 +01:00
Florian Mounier
b0e1f37cac Interesting Fixes 2016-10-13 17:45:12 +02:00
Florian Mounier
da659b7526 Fix selection 2016-10-13 16:25:18 +02:00
Florian Mounier
08ecb4d0d2 Add a bottom margin 2016-10-13 16:19:33 +02:00
Florian Mounier
3624962d3c Historize by ext 2016-10-13 16:11:44 +02:00
Florian Mounier
b9f1727f1e Finally a near perfect resize 2016-10-13 15:24:57 +02:00
Florian Mounier
5a7c4da0b1 Unoptimized but working dom creation 2016-10-13 12:41:31 +02:00
Florian Mounier
fa2b9d2bee Rework refresh 2016-10-13 10:03:15 +02:00
56 changed files with 3480 additions and 1017 deletions

3
.gitignore vendored
View File

@@ -8,3 +8,6 @@ node_modules/
sass/scss
*.egg-info/
build/
.cache/
.env*
.pytest_cache

2
.isort.cfg Normal file
View File

@@ -0,0 +1,2 @@
[settings]
multi_line_output=4

46
CHANGELOG.md Normal file
View 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

View File

@@ -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/*

View File

@@ -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
View 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
View 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

View File

@@ -1,4 +1,4 @@
# ƸӜƷ butterfly 2.0
# ƸӜƷ butterfly 3.0
![](http://paradoxxxzero.github.io/assets/butterfly_2.0_1.gif)
@@ -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

View File

@@ -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
View 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__'
]

View File

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

View File

@@ -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",

View File

@@ -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',

View File

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

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 */

View File

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

View File

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

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
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

File diff suppressed because one or more lines are too long

View File

@@ -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 */

View File

@@ -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 += "&amp;";
break;
case "<":
char += "&lt;";
break;
case ">":
char += "&gt;";
break;
case " ":
char += '<span class="nbsp">\u2007</span>';
break;
default:
if (ch <= " ") {
char += "&nbsp;";
} 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 += "&amp;";
break;
case "<":
out += "&lt;";
break;
case ">":
out += "&gt;";
break;
default:
if (ch === " ") {
out += '<span class="nbsp">\u2007</span>';
} else if (ch <= " ") {
out += "&nbsp;";
} 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) {

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -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 }}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 =
'&': '&amp;'
'<': '&lt;'
'>': '&gt;'
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
View 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

View File

@@ -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
View 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

View File

@@ -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 = ''

View File

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

View File

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

View File

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

View File

@@ -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 += "&amp;"
when "<"
char += "&lt;"
when ">"
char += "&gt;"
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 += "&nbsp;"
# CJK characters should always be forced to be fullwidth
else unless @forceWidth or @isCJK ch
char += ch
else
switch ch
when "&"
out += "&amp;"
when "<"
out += "&lt;"
when ">"
out += "&gt;"
else
if ch == " "
out += '<span class="nbsp">\u2007</span>'
else if ch <= " "
out += "&nbsp;"
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]

View File

@@ -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"
}
}

View File

@@ -1,2 +1,7 @@
[bdist_wheel]
universal = 1
[tool:pytest]
flake8-ignore =
*.py E731 E402
butterfly/bin/help.py E501

View File

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

1782
yarn.lock Normal file

File diff suppressed because it is too large Load Diff