mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-06-10 06:14:39 +00:00
Compare commits
341 Commits
webworkers
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da79ffe04b | ||
|
|
c348e1f285 | ||
|
|
91d52ed6ae | ||
|
|
06751c68f9 | ||
|
|
a9854e9136 | ||
|
|
039c730409 | ||
|
|
82676862ca | ||
|
|
5b6b61286d | ||
|
|
f32cb4d358 | ||
|
|
ad155f1f17 | ||
|
|
9e1045de9b | ||
|
|
db3d37f6fe | ||
|
|
611f2e30d6 | ||
|
|
1984e4b869 | ||
|
|
f58ea904b3 | ||
|
|
af0f4d20fe | ||
|
|
10b5ce3bcc | ||
|
|
a0287946d9 | ||
|
|
fbd71d55ef | ||
|
|
0ac8437387 | ||
|
|
866b56b682 | ||
|
|
4d87059872 | ||
|
|
5bbe456496 | ||
|
|
5b9cc257a8 | ||
|
|
34b6287e0c | ||
|
|
41ee5fb843 | ||
|
|
ae6b36fa89 | ||
|
|
cfda54a724 | ||
|
|
033169ab08 | ||
|
|
920c435b00 | ||
|
|
27e6aa8a5d | ||
|
|
92633f52ce | ||
|
|
f5f854964b | ||
|
|
55528fdf91 | ||
|
|
9eae13486e | ||
|
|
79bd074dae | ||
|
|
7b0ba2bfe7 | ||
|
|
db17b9d8ac | ||
|
|
b5de82bfcf | ||
|
|
13dbe0434c | ||
|
|
ef0057c23f | ||
|
|
6bc8e1438f | ||
|
|
8856ea9dc4 | ||
|
|
4edb2d269f | ||
|
|
272891470c | ||
|
|
574b3dc74b | ||
|
|
269dd2b618 | ||
|
|
0625e05cbb | ||
|
|
6b1101bc45 | ||
|
|
3e6d0b203f | ||
|
|
8189598dd6 | ||
|
|
4a8b5f2147 | ||
|
|
f9a1ff4dea | ||
|
|
96d88a5e91 | ||
|
|
bdc1c7a80d | ||
|
|
eacfdcd52f | ||
|
|
ed347e2bd0 | ||
|
|
3228e8c204 | ||
|
|
b9c991e3b6 | ||
|
|
8ad12c2379 | ||
|
|
2aa237ef12 | ||
|
|
40496eb9d1 | ||
|
|
ffd19b8162 | ||
|
|
6663568500 | ||
|
|
3a09c47ef0 | ||
|
|
41ab0f36ff | ||
|
|
70e00ac696 | ||
|
|
70369a0b32 | ||
|
|
8c20ffb943 | ||
|
|
729c768dc2 | ||
|
|
17f8c1d1c9 | ||
|
|
964fd07143 | ||
|
|
8553bbd0cb | ||
|
|
f494541652 | ||
|
|
dd6c917462 | ||
|
|
9e03e24764 | ||
|
|
6b5f3ac76f | ||
|
|
a36579bb12 | ||
|
|
e4ce69a967 | ||
|
|
b0e1f37cac | ||
|
|
da659b7526 | ||
|
|
08ecb4d0d2 | ||
|
|
3624962d3c | ||
|
|
b9f1727f1e | ||
|
|
5a7c4da0b1 | ||
|
|
fa2b9d2bee | ||
|
|
3bb6da1eae | ||
|
|
6c827206f7 | ||
|
|
fdeba5a5d4 | ||
|
|
d0eb37765a | ||
|
|
8dffb02980 | ||
|
|
15ebdf6907 | ||
|
|
6e29c702e3 | ||
|
|
c3ad2f342a | ||
|
|
7d7f05e164 | ||
|
|
64a8480938 | ||
|
|
0142ec0a16 | ||
|
|
97d435ce18 | ||
|
|
4b3a5e1ae6 | ||
|
|
9fcc156257 | ||
|
|
2887f6e25a | ||
|
|
ffe8945c09 | ||
|
|
a3e78112a6 | ||
|
|
e5eb7050e8 | ||
|
|
b72da2e4ef | ||
|
|
2d3bed2fef | ||
|
|
cc510500a5 | ||
|
|
1ec50810f9 | ||
|
|
524e578fca | ||
|
|
bce9f99b0b | ||
|
|
9bcc989149 | ||
|
|
1d324ed243 | ||
|
|
3c2bf35b09 | ||
|
|
fe258f44f8 | ||
|
|
1f9d263ad7 | ||
|
|
fe01ffb2b4 | ||
|
|
ac7e9bef8e | ||
|
|
503de38429 | ||
|
|
7ebb122221 | ||
|
|
ec25edb657 | ||
|
|
52714d81ab | ||
|
|
c048f1a4e6 | ||
|
|
c0e2d8959b | ||
|
|
5c054ca290 | ||
|
|
9168878d92 | ||
|
|
056fbc02b1 | ||
|
|
571f07946d | ||
|
|
e09bab810c | ||
|
|
efb019ed00 | ||
|
|
34d2711aa1 | ||
|
|
115190446b | ||
|
|
c8931c6135 | ||
|
|
ab7880779d | ||
|
|
f5724cc39d | ||
|
|
33d4051fca | ||
|
|
28ebf9d8a2 | ||
|
|
5714b97c77 | ||
|
|
a9c35d91f1 | ||
|
|
573b4f1c1b | ||
|
|
856aac2bcb | ||
|
|
e789622b7e | ||
|
|
84c4ff9414 | ||
|
|
4a3bd7f906 | ||
|
|
fb3ec14b43 | ||
|
|
4e4d54de1f | ||
|
|
e6f618ef52 | ||
|
|
0337663059 | ||
|
|
7b37716177 | ||
|
|
2d554483e1 | ||
|
|
fc5879f2d4 | ||
|
|
7371b8b4e1 | ||
|
|
71820849eb | ||
|
|
7b5a4ee244 | ||
|
|
e8512fc2b8 | ||
|
|
9c36b0c8c1 | ||
|
|
ab8e65924d | ||
|
|
ca26454aa0 | ||
|
|
0f36db5264 | ||
|
|
834200256c | ||
|
|
96606d2b0b | ||
|
|
7501aab797 | ||
|
|
38a4c4083d | ||
|
|
c937a8753d | ||
|
|
7916014854 | ||
|
|
93ff8a3969 | ||
|
|
0f7a51d451 | ||
|
|
f67054f9ff | ||
|
|
ced1148275 | ||
|
|
140b0902fc | ||
|
|
909bcfa9f4 | ||
|
|
cf5051d414 | ||
|
|
811620557a | ||
|
|
0e97cc8362 | ||
|
|
6d346af6f4 | ||
|
|
893ec72270 | ||
|
|
6d98a5c5ac | ||
|
|
6c5cbeaca5 | ||
|
|
5aa697381a | ||
|
|
ed03f94c84 | ||
|
|
97de0cc46c | ||
|
|
6e5edde6dc | ||
|
|
81c61ec466 | ||
|
|
95ded4370d | ||
|
|
4e66196d65 | ||
|
|
8c3c780b12 | ||
|
|
d99cffabc4 | ||
|
|
a08cff653d | ||
|
|
354280bbda | ||
|
|
89a7e05f55 | ||
|
|
7d8f3b2845 | ||
|
|
060a2666b5 | ||
|
|
81d9fea01a | ||
|
|
3fa14e9718 | ||
|
|
78e3050387 | ||
|
|
324b6aa020 | ||
|
|
326a42b7f1 | ||
|
|
5fea2d9294 | ||
|
|
807d40bf9f | ||
|
|
5aac9886c5 | ||
|
|
1fafe99fd1 | ||
|
|
cee1983ca7 | ||
|
|
38980afe50 | ||
|
|
04ff7aea62 | ||
|
|
da3e9237be | ||
|
|
c345367d6b | ||
|
|
923e49565b | ||
|
|
f9b4a7a8eb | ||
|
|
3d14bce231 | ||
|
|
2b5ea8dc9a | ||
|
|
5bdcc8dd71 | ||
|
|
e48f029c68 | ||
|
|
10f364f693 | ||
|
|
4492b59e99 | ||
|
|
790a4b8072 | ||
|
|
13cc6b52e7 | ||
|
|
9a58059c1d | ||
|
|
23c905e05e | ||
|
|
809136c7d7 | ||
|
|
a2ab676451 | ||
|
|
5822ba2114 | ||
|
|
e86ff5a93a | ||
|
|
562e345c80 | ||
|
|
5be66f7728 | ||
|
|
23ffeb764e | ||
|
|
b03a3c836a | ||
|
|
909bc5ab44 | ||
|
|
53c880964e | ||
|
|
ad15c05cf0 | ||
|
|
03bb84fcbe | ||
|
|
165b17da4e | ||
|
|
ca454b4149 | ||
|
|
3afebe4be0 | ||
|
|
6e7e222370 | ||
|
|
2eae240842 | ||
|
|
000754d5c8 | ||
|
|
030cf6e70f | ||
|
|
74193530df | ||
|
|
287cbf4e82 | ||
|
|
1df84b3ee6 | ||
|
|
7d0c69ea4d | ||
|
|
c2bde7b3b6 | ||
|
|
683d325522 | ||
|
|
87d76ba36d | ||
|
|
16a1dae39c | ||
|
|
41616f8163 | ||
|
|
8754085deb | ||
|
|
e1eb02ef80 | ||
|
|
7ec715cc38 | ||
|
|
366ba5a67b | ||
|
|
f7a4346aa0 | ||
|
|
6b2517ff93 | ||
|
|
66846b50a2 | ||
|
|
7e0e7f60d9 | ||
|
|
728912299c | ||
|
|
e70d7e0317 | ||
|
|
80dd8c1029 | ||
|
|
7c948fc23a | ||
|
|
1cf98b18c6 | ||
|
|
7953e0647d | ||
|
|
dc5860e16d | ||
|
|
764a9b7884 | ||
|
|
beb28d7a61 | ||
|
|
488e03246c | ||
|
|
0a23565302 | ||
|
|
c0943dfde9 | ||
|
|
2db77cc250 | ||
|
|
3b6578658b | ||
|
|
ea072ea24d | ||
|
|
75cd2a267a | ||
|
|
985d8b86e6 | ||
|
|
585c1b876f | ||
|
|
cbaa83e722 | ||
|
|
7f20325b3a | ||
|
|
e80b5f192d | ||
|
|
80cfc39f07 | ||
|
|
d7298f6229 | ||
|
|
f9c9700062 | ||
|
|
eee9fdfba2 | ||
|
|
eb869a58d2 | ||
|
|
bbd216fe3f | ||
|
|
34c6718d8c | ||
|
|
3772754c2f | ||
|
|
253dd61e38 | ||
|
|
5330429b7a | ||
|
|
8cf1f75224 | ||
|
|
84f5cce7ea | ||
|
|
6a0bdf2147 | ||
|
|
cd6b7aadff | ||
|
|
ed02414cbc | ||
|
|
3ac94a9695 | ||
|
|
e106231613 | ||
|
|
311e7b9524 | ||
|
|
4afcc99fe5 | ||
|
|
a4d59a90f7 | ||
|
|
74700f5046 | ||
|
|
884eeb169a | ||
|
|
515a2c6b46 | ||
|
|
861c28d056 | ||
|
|
ceb50a8a8e | ||
|
|
eb4c84285c | ||
|
|
c995d6e277 | ||
|
|
ee545f2002 | ||
|
|
8e07b75a00 | ||
|
|
6d4dda0aef | ||
|
|
3150a116cf | ||
|
|
2dc13e96f1 | ||
|
|
7e9f5e79f0 | ||
|
|
30108a5ff3 | ||
|
|
7d34dc6ba1 | ||
|
|
8b66e95006 | ||
|
|
1b25dce8be | ||
|
|
713cf483ca | ||
|
|
fe11f8a25a | ||
|
|
6b8758dc3e | ||
|
|
ba1d48fc5f | ||
|
|
5e03b5340a | ||
|
|
1ed9b9eeeb | ||
|
|
78cf01c1fd | ||
|
|
4a34543e0b | ||
|
|
470f235815 | ||
|
|
6190f93c00 | ||
|
|
d79335b032 | ||
|
|
1d7e6323ea | ||
|
|
38cface138 | ||
|
|
425594d633 | ||
|
|
f507ed5be3 | ||
|
|
0cbb4b2afc | ||
|
|
9351fdceec | ||
|
|
14caa88d19 | ||
|
|
3d6677643d | ||
|
|
a5f68cc970 | ||
|
|
4dc55630a3 | ||
|
|
e4d51415ca | ||
|
|
779711f077 | ||
|
|
89d11c9189 | ||
|
|
459b256ddc | ||
|
|
4aa5f75e4f | ||
|
|
6d10994eb7 | ||
|
|
1db6c4eb5c | ||
|
|
cd44280877 | ||
|
|
35838a600b |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
README.md
|
||||
butterfly.png
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
*.crt
|
||||
*.key
|
||||
*.p12
|
||||
*.pyc
|
||||
node_modules/
|
||||
*.src.coffee
|
||||
*.map
|
||||
sass/scss
|
||||
*.egg-info/
|
||||
build/
|
||||
.cache/
|
||||
.env*
|
||||
.pytest_cache
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -0,0 +1,3 @@
|
||||
[submodule "butterfly/themes"]
|
||||
path = butterfly/themes
|
||||
url = https://github.com/paradoxxxzero/butterfly-themes
|
||||
|
||||
2
.isort.cfg
Normal file
2
.isort.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
multi_line_output=4
|
||||
46
CHANGELOG.md
Normal file
46
CHANGELOG.md
Normal file
@@ -0,0 +1,46 @@
|
||||
[3.2.5](https://github.com/paradoxxxzero/butterfly/compare/3.2.4...3.2.5)
|
||||
=====
|
||||
|
||||
* Fix #155 again (PR #179)
|
||||
|
||||
|
||||
[3.2.4](https://github.com/paradoxxxzero/butterfly/compare/3.2.3...3.2.4)
|
||||
=====
|
||||
|
||||
* Fix up --uri-root-path so behaves as one would expect for this. Fix #155 (PR #173 thanks @GrahamDumpleton)
|
||||
* Fix websocket keepalive. Fix #167 (PR #172 thanks @fzumstein)
|
||||
|
||||
[3.2.3](https://github.com/paradoxxxzero/butterfly/compare/3.2.2...3.2.3)
|
||||
=====
|
||||
|
||||
* Complete support for IME & CJK rendering (#168 thanks @PeterCxy)
|
||||
|
||||
3.2.2
|
||||
=====
|
||||
|
||||
* Fix unescaping entities when linkifying
|
||||
|
||||
3.2.1
|
||||
=====
|
||||
|
||||
* Issue correct X.509 v3 certificates (you will need to re-generate your certs)
|
||||
|
||||
3.1.5
|
||||
=====
|
||||
|
||||
* Fix new option in older tornado version. (#146 thanks @warpkwd)
|
||||
|
||||
3.1.4
|
||||
=====
|
||||
|
||||
* Add --i-hereby-declare-i-dont-want-any-security-whatsoever option (#143)
|
||||
|
||||
3.1.3
|
||||
=====
|
||||
|
||||
* Fix lsof parsing crash on python 2
|
||||
|
||||
3.1.0
|
||||
=====
|
||||
|
||||
* Start a changelog
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y -q --no-install-recommends \
|
||||
build-essential \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
python-dev \
|
||||
python-setuptools \
|
||||
ca-certificates \
|
||||
&& easy_install pip \
|
||||
&& pip install --upgrade setuptools \
|
||||
&& apt-get clean \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt
|
||||
ADD . /opt/app
|
||||
WORKDIR /opt/app
|
||||
|
||||
RUN python setup.py build \
|
||||
&& python setup.py install
|
||||
|
||||
ADD docker/run.sh /opt/run.sh
|
||||
|
||||
EXPOSE 57575
|
||||
|
||||
CMD ["butterfly.server.py", "--unsecure", "--host=0.0.0.0"]
|
||||
ENTRYPOINT ["docker/run.sh"]
|
||||
68
Gruntfile.coffee
Normal file
68
Gruntfile.coffee
Normal file
@@ -0,0 +1,68 @@
|
||||
module.exports = (grunt) ->
|
||||
|
||||
grunt.initConfig
|
||||
pkg: grunt.file.readJSON('package.json')
|
||||
|
||||
uglify:
|
||||
options:
|
||||
banner: '/*! <%= pkg.name %>
|
||||
<%= grunt.template.today("yyyy-mm-dd") %> */\n'
|
||||
sourceMap: true
|
||||
|
||||
butterfly:
|
||||
files:
|
||||
'butterfly/static/main.min.js': 'butterfly/static/main.js'
|
||||
'butterfly/static/ext.min.js': 'butterfly/static/ext.js'
|
||||
|
||||
sass:
|
||||
options:
|
||||
includePaths: ['butterfly/sass/']
|
||||
|
||||
butterfly:
|
||||
expand: true
|
||||
cwd: 'butterfly/sass/'
|
||||
src: '*.sass'
|
||||
dest: 'butterfly/static/'
|
||||
ext: '.css'
|
||||
|
||||
coffee:
|
||||
options:
|
||||
sourceMap: true
|
||||
|
||||
butterfly:
|
||||
files:
|
||||
'butterfly/static/main.js': 'coffees/*.coffee'
|
||||
'butterfly/static/ext.js': 'coffees/ext/*.coffee'
|
||||
|
||||
coffeelint:
|
||||
butterfly:
|
||||
'coffees/**/*.coffee'
|
||||
|
||||
watch:
|
||||
options:
|
||||
livereload: true
|
||||
coffee:
|
||||
files: [
|
||||
'coffees/ext/*.coffee'
|
||||
'coffees/*.coffee'
|
||||
'Gruntfile.coffee'
|
||||
]
|
||||
tasks: ['coffeelint', 'coffee']
|
||||
|
||||
sass:
|
||||
files: [
|
||||
'butterfly/sass/*.sass'
|
||||
]
|
||||
tasks: ['sass']
|
||||
|
||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||
grunt.loadNpmTasks 'grunt-contrib-uglify'
|
||||
grunt.loadNpmTasks 'grunt-contrib-cssmin'
|
||||
grunt.loadNpmTasks 'grunt-coffeelint'
|
||||
grunt.loadNpmTasks 'grunt-sass'
|
||||
grunt.registerTask 'dev', [
|
||||
'coffeelint', 'coffee', 'sass', 'watch']
|
||||
grunt.registerTask 'css', ['sass']
|
||||
grunt.registerTask 'default', [
|
||||
'coffeelint', 'coffee', 'sass', 'uglify']
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
butterfly Copyright (C) 2014 Florian Mounier, Kozea
|
||||
butterfly Copyright(C) 2015-2017 Florian Mounier, Kozea
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
|
||||
37
Makefile
Normal file
37
Makefile
Normal file
@@ -0,0 +1,37 @@
|
||||
include Makefile.config
|
||||
-include Makefile.custom.config
|
||||
|
||||
all: install lint check-outdated run-debug
|
||||
|
||||
install:
|
||||
test -d $(VENV) || virtualenv $(VENV) -p $(PYTHON_VERSION)
|
||||
$(PIP) install --upgrade --no-cache pip setuptools -e .[lint,themes] devcore
|
||||
$(NPM) install
|
||||
|
||||
clean:
|
||||
rm -fr $(NODE_MODULES)
|
||||
rm -fr $(VENV)
|
||||
rm -fr *.egg-info
|
||||
|
||||
lint:
|
||||
$(PYTEST) --flake8 -m flake8 $(PROJECT_NAME)
|
||||
$(PYTEST) --isort -m isort $(PROJECT_NAME)
|
||||
|
||||
check-outdated:
|
||||
$(PIP) list --outdated --format=columns
|
||||
|
||||
ARGS ?= --port=1212 --unsecure --debug
|
||||
run-debug:
|
||||
$(PYTHON) ./butterfly.server.py $(ARGS)
|
||||
|
||||
build-coffee:
|
||||
$(NODE_MODULES)/.bin/grunt
|
||||
|
||||
release: build-coffee
|
||||
git pull
|
||||
$(eval VERSION := $(shell PROJECT_NAME=$(PROJECT_NAME) $(VENV)/bin/devcore bump $(LEVEL)))
|
||||
git commit -am "Bump $(VERSION)"
|
||||
git tag $(VERSION)
|
||||
$(PYTHON) setup.py sdist bdist_wheel upload
|
||||
git push
|
||||
git push --tags
|
||||
10
Makefile.config
Normal file
10
Makefile.config
Normal file
@@ -0,0 +1,10 @@
|
||||
PROJECT_NAME = butterfly
|
||||
|
||||
# Python env
|
||||
PYTHON_VERSION ?= python
|
||||
VENV = $(PWD)/.env$(if $(filter $(PYTHON_VERSION),python),,-$(PYTHON_VERSION))
|
||||
PIP = $(VENV)/bin/pip
|
||||
PYTHON = $(VENV)/bin/python
|
||||
PYTEST = $(VENV)/bin/py.test
|
||||
NODE_MODULES = $(PWD)/node_modules
|
||||
NPM = yarn
|
||||
138
README.md
138
README.md
@@ -1,45 +1,141 @@
|
||||
# ƸӜƷ butterfly
|
||||
# ƸӜƷ butterfly 3.0
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Description
|
||||
|
||||
Butterfly is a tornado web server written in python which powers a full featured web terminal.
|
||||
Butterfly is a xterm compatible terminal that runs in your browser.
|
||||
|
||||
The js part is heavily based on [term.js](https://github.com/chjj/term.js/) which is heavily based on [jslinux](http://bellard.org/jslinux/).
|
||||
|
||||
## Features
|
||||
|
||||
* xterm compatible (support a lot of unused features!)
|
||||
* Native browser scroll and search
|
||||
* 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!
|
||||
* 16,777,216 colors support!
|
||||
* Keyboard text selection!
|
||||
* Desktop notifications on terminal output!
|
||||
* Geolocation from browser!
|
||||
* May work on firefox too!
|
||||
|
||||
## Try it
|
||||
|
||||
```bash
|
||||
$ pip install butterfly
|
||||
$ butterfly.server.py
|
||||
``` bash
|
||||
$ pip install butterfly
|
||||
$ pip install butterfly[themes] # If you want to use themes
|
||||
$ pip install butterfly[systemd] # If you want to use systemd
|
||||
$ butterfly
|
||||
```
|
||||
|
||||
Then open [localhost:57575](http://localhost:57575) in your favorite browser and done.
|
||||
A new tab should appear in your browser. Then type
|
||||
|
||||
``` bash
|
||||
$ butterfly help
|
||||
```
|
||||
|
||||
To get an overview of butterfly features.
|
||||
|
||||
|
||||
## Run it as a server
|
||||
|
||||
``` bash
|
||||
$ 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))
|
||||
|
||||
|
||||
## Run it with systemd (linux)
|
||||
|
||||
Systemd provides a way to automatically activate daemons when needed (socket activation):
|
||||
|
||||
``` bash
|
||||
$ cd /etc/systemd/system
|
||||
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.service
|
||||
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.socket
|
||||
$ 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, ...) and to install butterfly with the [systemd] flag.
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
and make the world better (or just butterfly).
|
||||
|
||||
Don't hesitate to fork the repository and start hacking on it, I am very open to pull requests.
|
||||
|
||||
If you don't know what to do go to the github issues and pick one you like.
|
||||
|
||||
If you want to motivate me to continue working on this project you can tip me, see: http://paradoxxxzero.github.io/about/
|
||||
|
||||
Client side development use [grunt](http://gruntjs.com/) and [bower](http://bower.io/).
|
||||
|
||||
## Credits
|
||||
|
||||
The js part is based on [term.js](https://github.com/chjj/term.js/) which is based on [jslinux](http://bellard.org/jslinux/).
|
||||
## Author
|
||||
|
||||
[Florian Mounier](http://github.com/paradoxxxzero)
|
||||
|
||||
[Florian Mounier](http://paradoxxxzero.github.io/)
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
butterfly Copyright (C) 2014 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
|
||||
(at your option) any later version.
|
||||
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.
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
```
|
||||
|
||||
## Docker
|
||||
There is a docker repository created for this project that is set to automatically rebuild when there is a push
|
||||
into this repository: https://registry.hub.docker.com/u/garland/butterfly/
|
||||
|
||||
### Example usage
|
||||
|
||||
Starting with login and password
|
||||
|
||||
``` bash
|
||||
docker run --env PASSWORD=password -d garland/butterfly --login
|
||||
```
|
||||
|
||||
Starting with no password
|
||||
|
||||
``` bash
|
||||
docker run -d -p 57575:57575 garland/butterfly
|
||||
```
|
||||
|
||||
Starting with a different port
|
||||
|
||||
``` bash
|
||||
docker run -d -p 12345:12345 garland/butterfly --port=12345
|
||||
```
|
||||
|
||||
31
bin/ils
31
bin/ils
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#Depends: pillow
|
||||
#Broken: Too slow !
|
||||
from PIL import Image
|
||||
import os
|
||||
import mimetypes
|
||||
import base64
|
||||
import io
|
||||
print('\x1b]99;')
|
||||
|
||||
out = ''
|
||||
|
||||
for f in os.listdir(os.getcwd()):
|
||||
mime = mimetypes.guess_type(f)[0]
|
||||
if 'image' in (mime or ''):
|
||||
try:
|
||||
im = Image.open(f)
|
||||
im.thumbnail((100, 100), Image.ANTIALIAS)
|
||||
buf = io.BytesIO()
|
||||
im.save(buf, im.format)
|
||||
buf.seek(0)
|
||||
out += '<img src="data:%s;base64,%s" alt="%s" />' % (
|
||||
mime,
|
||||
base64.b64encode(buf.read()).decode('ascii'),
|
||||
f)
|
||||
except:
|
||||
pass
|
||||
|
||||
print(out)
|
||||
|
||||
print('\x07')
|
||||
13
bin/month
13
bin/month
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from calendar import LocaleHTMLCalendar
|
||||
from datetime import datetime
|
||||
import locale
|
||||
now = datetime.now()
|
||||
calendar = LocaleHTMLCalendar(locale=locale.getlocale())
|
||||
calendar_table = calendar.formatmonth(now.year, now.month)
|
||||
calendar_table = calendar_table.replace('border="0"', 'border="1"')
|
||||
|
||||
print('\x1b]99;')
|
||||
print(calendar_table)
|
||||
print('\x07')
|
||||
4
bin/nt
4
bin/nt
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import webbrowser
|
||||
webbrowser.open('%swd%s' % (os.getenv('LOCATION'), os.getcwd()))
|
||||
20
bower.json
Normal file
20
bower.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "butterfly",
|
||||
"version": "1.0.0",
|
||||
"authors": [
|
||||
"Florian Mounier <florian.mounier@kozea.fr>"
|
||||
],
|
||||
"description": "A sleek web based terminal emulator",
|
||||
"license": "None",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"google-caja": "*"
|
||||
}
|
||||
}
|
||||
380
butterfly.server.py
Normal file → Executable file
380
butterfly.server.py
Normal file → Executable file
@@ -3,7 +3,7 @@
|
||||
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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
|
||||
@@ -19,46 +19,368 @@
|
||||
|
||||
import tornado.options
|
||||
import tornado.ioloop
|
||||
|
||||
tornado.options.define("secret", default='secret', help="Secret")
|
||||
tornado.options.define("debug", default=False, help="Debug mode")
|
||||
tornado.options.define("host", default='127.0.0.1', help="Server host")
|
||||
tornado.options.define("port", default=57575, type=int, help="Server port")
|
||||
tornado.options.define("shell", help="Shell to execute at login")
|
||||
|
||||
tornado.options.parse_command_line()
|
||||
|
||||
import tornado.httpserver
|
||||
try:
|
||||
from tornado_systemd import SystemdHTTPServer as HTTPServer
|
||||
except ImportError:
|
||||
from tornado.httpserver import HTTPServer
|
||||
|
||||
import logging
|
||||
import webbrowser
|
||||
import uuid
|
||||
import ssl
|
||||
import getpass
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import socket
|
||||
import sys
|
||||
|
||||
tornado.options.define("debug", default=False, help="Debug mode")
|
||||
tornado.options.define("more", default=False,
|
||||
help="Debug mode with more verbosity")
|
||||
tornado.options.define("unminified", default=False,
|
||||
help="Use the unminified js (for development only)")
|
||||
|
||||
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")
|
||||
tornado.options.define("motd", default='motd', help="Path to the motd file.")
|
||||
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."
|
||||
"Useful for avoiding layout mess.")
|
||||
tornado.options.define("ssl_version", default=None,
|
||||
help="SSL protocol version")
|
||||
tornado.options.define("generate_certs", default=False,
|
||||
help="Generate butterfly certificates")
|
||||
tornado.options.define("generate_current_user_pkcs", default=False,
|
||||
help="Generate current user pfx for client "
|
||||
"authentication")
|
||||
tornado.options.define("generate_user_pkcs", default='',
|
||||
help="Generate user pfx for client authentication "
|
||||
"(Must be root to create for another user)")
|
||||
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:
|
||||
ev = os.getenv(
|
||||
'XDG_CONFIG_HOME', os.path.join(
|
||||
os.getenv('HOME', os.path.expanduser('~')),
|
||||
'.config'))
|
||||
|
||||
butterfly_dir = os.path.join(ev, 'butterfly')
|
||||
conf_file = os.path.join(butterfly_dir, 'butterfly.conf')
|
||||
ssl_dir = os.path.join(butterfly_dir, 'ssl')
|
||||
|
||||
tornado.options.define("conf", default=conf_file,
|
||||
help="Butterfly configuration file. "
|
||||
"Contains the same options as command line.")
|
||||
|
||||
tornado.options.define("ssl_dir", default=ssl_dir,
|
||||
help="Force SSL directory location")
|
||||
|
||||
# Do it once to get the conf path
|
||||
tornado.options.parse_command_line()
|
||||
|
||||
if os.path.exists(tornado.options.options.conf):
|
||||
tornado.options.parse_config_file(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',
|
||||
'tornado.general', 'butterfly'):
|
||||
logging.getLogger(logger).setLevel(
|
||||
logging.DEBUG if tornado.options.options.debug else logging.WARNING)
|
||||
level = logging.WARNING
|
||||
if options.debug:
|
||||
level = logging.INFO
|
||||
if options.more:
|
||||
level = logging.DEBUG
|
||||
logging.getLogger(logger).setLevel(level)
|
||||
|
||||
log = logging.getLogger('butterfly')
|
||||
log.debug('Starting server')
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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',
|
||||
'%s.p12'])
|
||||
|
||||
|
||||
def fill_fields(subject):
|
||||
subject.C = 'WW'
|
||||
subject.O = 'Butterfly'
|
||||
subject.OU = 'Butterfly Terminal'
|
||||
subject.ST = 'World Wide'
|
||||
subject.L = 'Terminal'
|
||||
|
||||
|
||||
def write(file, content):
|
||||
with open(file, 'wb') as fd:
|
||||
fd.write(content)
|
||||
print('Writing %s' % file)
|
||||
|
||||
|
||||
def read(file):
|
||||
print('Reading %s' % 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)
|
||||
|
||||
if not os.path.exists(ca) and not os.path.exists(ca_key):
|
||||
print('Root certificate not found, generating it')
|
||||
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)
|
||||
ca_cert.gmtime_adj_notBefore(0) # From now
|
||||
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))
|
||||
write(ca_key, crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_pk))
|
||||
os.chmod(ca_key, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms
|
||||
else:
|
||||
print('Root certificate found, using it')
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca))
|
||||
ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key))
|
||||
|
||||
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
|
||||
server_cert.gmtime_adj_notAfter(315360000) # to 10y
|
||||
server_cert.set_issuer(ca_cert.get_subject()) # Signed by ca
|
||||
server_cert.set_pubkey(server_pk)
|
||||
server_cert.sign(ca_pk, 'sha512')
|
||||
|
||||
write(cert % host, crypto.dump_certificate(
|
||||
crypto.FILETYPE_PEM, server_cert))
|
||||
write(cert_key % host, crypto.dump_privatekey(
|
||||
crypto.FILETYPE_PEM, server_pk))
|
||||
os.chmod(cert_key % host, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms
|
||||
|
||||
print('\nNow you can run --generate-user-pkcs=user '
|
||||
'to generate user certificate.')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if (options.generate_current_user_pkcs or
|
||||
options.generate_user_pkcs):
|
||||
from butterfly import utils
|
||||
try:
|
||||
current_user = utils.User()
|
||||
except Exception:
|
||||
current_user = None
|
||||
|
||||
from OpenSSL import crypto
|
||||
if not all(map(os.path.exists, [ca, ca_key])):
|
||||
print('Please generate certificates using --generate-certs before')
|
||||
sys.exit(1)
|
||||
|
||||
if options.generate_current_user_pkcs:
|
||||
user = current_user.name
|
||||
else:
|
||||
user = options.generate_user_pkcs
|
||||
|
||||
if user != current_user.name and current_user.uid != 0:
|
||||
print('Cannot create certificate for another user with '
|
||||
'current privileges.')
|
||||
sys.exit(1)
|
||||
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, read(ca))
|
||||
ca_pk = crypto.load_privatekey(crypto.FILETYPE_PEM, read(ca_key))
|
||||
|
||||
client_pk = crypto.PKey()
|
||||
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)
|
||||
client_cert.gmtime_adj_notBefore(0) # From now
|
||||
client_cert.gmtime_adj_notAfter(315360000) # to 10y
|
||||
client_cert.set_issuer(ca_cert.get_subject()) # Signed by ca
|
||||
client_cert.set_pubkey(client_pk)
|
||||
client_cert.sign(client_pk, 'sha512')
|
||||
client_cert.sign(ca_pk, 'sha512')
|
||||
|
||||
pfx = crypto.PKCS12()
|
||||
pfx.set_certificate(client_cert)
|
||||
pfx.set_privatekey(client_pk)
|
||||
pfx.set_ca_certificates([ca_cert])
|
||||
pfx.set_friendlyname(('%s cert for butterfly' % user).encode('utf-8'))
|
||||
|
||||
while True:
|
||||
password = getpass.getpass('\nPKCS12 Password (can be blank): ')
|
||||
password2 = getpass.getpass('Verify Password (can be blank): ')
|
||||
if password == password2:
|
||||
break
|
||||
print('Passwords do not match.')
|
||||
|
||||
print('')
|
||||
write(pkcs12 % user, pfx.export(password.encode('utf-8')))
|
||||
os.chmod(pkcs12 % user, stat.S_IRUSR | stat.S_IWUSR) # 0o600 perms
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if options.unsecure:
|
||||
ssl_opts = None
|
||||
else:
|
||||
if not all(map(os.path.exists, [cert % host, cert_key % host, ca])):
|
||||
print("Unable to find butterfly certificate for host %s" % host)
|
||||
print(cert % host)
|
||||
print(cert_key % host)
|
||||
print(ca)
|
||||
print("Can't run butterfly without certificate.\n")
|
||||
print("Either generate them using --generate-certs --host=host "
|
||||
"or run as --unsecure (NOT RECOMMENDED)\n")
|
||||
print("For more information go to http://paradoxxxzero.github.io/"
|
||||
"2014/03/21/butterfly-with-ssl-auth.html\n")
|
||||
sys.exit(1)
|
||||
|
||||
ssl_opts = {
|
||||
'certfile': cert % host,
|
||||
'keyfile': cert_key % host,
|
||||
'ca_certs': ca,
|
||||
'cert_reqs': ssl.CERT_REQUIRED
|
||||
}
|
||||
if options.ssl_version is not None:
|
||||
if not hasattr(
|
||||
ssl, 'PROTOCOL_%s' % options.ssl_version):
|
||||
print(
|
||||
"Unknown SSL protocol %s" %
|
||||
options.ssl_version)
|
||||
sys.exit(1)
|
||||
ssl_opts['ssl_version'] = getattr(
|
||||
ssl, 'PROTOCOL_%s' % options.ssl_version)
|
||||
|
||||
from butterfly import application
|
||||
application.listen(tornado.options.options.port)
|
||||
application.butterfly_dir = butterfly_dir
|
||||
log.info('Starting server')
|
||||
http_server = HTTPServer(application, ssl_options=ssl_opts)
|
||||
http_server.listen(port, address=host)
|
||||
|
||||
if getattr(http_server, 'systemd', False):
|
||||
os.environ.pop('LISTEN_PID')
|
||||
os.environ.pop('LISTEN_FDS')
|
||||
|
||||
url = "http://%s:%d/*" % (
|
||||
tornado.options.options.host, tornado.options.options.port)
|
||||
log.info('Starting loop')
|
||||
|
||||
# This is for debugging purpose
|
||||
try:
|
||||
from wsreload.client import sporadic_reload, watch
|
||||
except ImportError:
|
||||
log.debug('wsreload not found')
|
||||
else:
|
||||
sporadic_reload({'url': url})
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
|
||||
files = ['butterfly/static/javascripts/',
|
||||
'butterfly/static/stylesheets/',
|
||||
'butterfly/templates/']
|
||||
watch({'url': url}, files, unwatch_at_exit=True)
|
||||
if port == 0:
|
||||
port = list(http_server._sockets.values())[0].getsockname()[1]
|
||||
|
||||
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)
|
||||
|
||||
log.debug('Starting loop')
|
||||
ioloop.start()
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
[Unit]
|
||||
Description=Butterfly Terminal Server
|
||||
After=syslog.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/butterfly.server.py
|
||||
Restart=on-abort
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
5
butterfly.socket
Normal file
5
butterfly.socket
Normal file
@@ -0,0 +1,5 @@
|
||||
[Socket]
|
||||
ListenStream=57575
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
15
butterfly/__about__.py
Normal file
15
butterfly/__about__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
__title__ = "butterfly"
|
||||
__version__ = "3.2.5"
|
||||
|
||||
__summary__ = "A sleek web based terminal emulator"
|
||||
__uri__ = "https://github.com/paradoxxxzero/butterfly"
|
||||
__author__ = "Florian Mounier"
|
||||
__email__ = "paradoxxx.zero@gmail.com"
|
||||
|
||||
__license__ = "GPLv3"
|
||||
__copyright__ = "Copyright 2017 %s" % __author__
|
||||
|
||||
__all__ = [
|
||||
'__title__', '__version__', '__summary__', '__uri__', '__author__',
|
||||
'__email__', '__license__', '__copyright__'
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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__ = '1.1.2'
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -43,19 +48,38 @@ class Route(tornado.web.RequestHandler):
|
||||
def log(self):
|
||||
return log
|
||||
|
||||
@property
|
||||
def builtin_themes_dir(self):
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__), 'themes')
|
||||
|
||||
@property
|
||||
def themes_dir(self):
|
||||
return os.path.join(
|
||||
self.application.butterfly_dir, 'themes')
|
||||
|
||||
@property
|
||||
def local_js_dir(self):
|
||||
return os.path.join(
|
||||
self.application.butterfly_dir, 'js')
|
||||
|
||||
def get_theme_dir(self, theme):
|
||||
if theme.startswith('built-in-'):
|
||||
return os.path.join(
|
||||
self.builtin_themes_dir, theme[len('built-in-'):])
|
||||
return os.path.join(
|
||||
self.themes_dir, theme)
|
||||
|
||||
|
||||
# Imported from executable
|
||||
if hasattr(tornado.options.options, 'debug'):
|
||||
opts = dict(
|
||||
application = tornado.web.Application(
|
||||
static_path=os.path.join(os.path.dirname(__file__), "static"),
|
||||
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
||||
debug=tornado.options.options.debug,
|
||||
cookie_secret=tornado.options.options.secret)
|
||||
else:
|
||||
opts = {}
|
||||
static_url_prefix='%s/static/' % (
|
||||
'/%s' % tornado.options.options.uri_root_path.strip('/')
|
||||
if tornado.options.options.uri_root_path else '')
|
||||
)
|
||||
|
||||
|
||||
application = tornado.web.Application(
|
||||
static_path=os.path.join(os.path.dirname(__file__), "static"),
|
||||
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
||||
**opts
|
||||
)
|
||||
|
||||
|
||||
import butterfly.routes
|
||||
import butterfly.routes # noqa: F401
|
||||
|
||||
33
butterfly/bin/cat.py
Normal file
33
butterfly/bin/cat.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from butterfly.escapes import image
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly cat wrapper.')
|
||||
parser.add_argument('-o', action="store_true",
|
||||
dest='original', help='Force original cat')
|
||||
parser.add_argument(
|
||||
'files', metavar='FILES', nargs='+',
|
||||
help='Force original cat')
|
||||
|
||||
args, remaining = parser.parse_known_args()
|
||||
if args.original:
|
||||
os.execvp('/usr/bin/cat', remaining + args.files)
|
||||
|
||||
|
||||
for file in args.files:
|
||||
if (not os.path.exists(sys.argv[1])):
|
||||
print('%s: No such file' % file)
|
||||
else:
|
||||
mime = mimetypes.guess_type(file)[0]
|
||||
if mime and 'image' in mime:
|
||||
with image(mime):
|
||||
with open(file, 'rb') as f:
|
||||
print(base64.b64encode(f.read()).decode('ascii'))
|
||||
else:
|
||||
subprocess.call(['cat'] + remaining + [file])
|
||||
146
butterfly/bin/colors.py
Normal file
146
butterfly/bin/colors.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Butterfly terminal color tester.')
|
||||
parser.add_argument(
|
||||
'--colors',
|
||||
default='16',
|
||||
choices=['8', '16', '256', '16M'],
|
||||
help='Set the color mode to test')
|
||||
args = parser.parse_args()
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if args.colors in ['8', '16']:
|
||||
print('Background\n')
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[%dm \x1b[m ' % (40 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
if args.colors == '16':
|
||||
print()
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[%dm \x1b[m ' % (100 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
print('\nForeground\n')
|
||||
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[%dm ░▒▓██\x1b[m ' % (30 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
if args.colors == '16':
|
||||
print()
|
||||
for l in range(3):
|
||||
sys.stdout.write(' ')
|
||||
for i in range(8):
|
||||
sys.stdout.write('\x1b[1;%dm ░▒▓██\x1b[m ' % (30 + i))
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
if args.colors == '256':
|
||||
for i in range(16):
|
||||
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (i))
|
||||
print()
|
||||
for i in range(16):
|
||||
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (i, i))
|
||||
print()
|
||||
|
||||
for j in range(6):
|
||||
for i in range(36):
|
||||
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (16 + j * 36 + i))
|
||||
print()
|
||||
for i in range(36):
|
||||
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (
|
||||
16 + j * 36 + i, 16 + j * 36 + i))
|
||||
print()
|
||||
for i in range(24):
|
||||
sys.stdout.write('\x1b[48;5;%dm \x1b[m' % (232 + i))
|
||||
print()
|
||||
for i in range(24):
|
||||
sys.stdout.write('\x1b[48;5;%dm %03d\x1b[m' % (232 + i, 232 + i))
|
||||
|
||||
if args.colors == '16M':
|
||||
b = 0
|
||||
g = 0
|
||||
for r in range(256):
|
||||
if r == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 255
|
||||
b = 0
|
||||
for g in range(256):
|
||||
if g == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 255
|
||||
g = 255
|
||||
for b in range(256):
|
||||
if b == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 255
|
||||
b = 255
|
||||
for g in reversed(range(256)):
|
||||
if g == 127:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
g = 0
|
||||
b = 255
|
||||
for r in reversed(range(256)):
|
||||
if r == 127:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 0
|
||||
g = 0
|
||||
for b in reversed(range(256)):
|
||||
if b == 127:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 0
|
||||
b = 0
|
||||
for g in range(256):
|
||||
if g == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
r = 0
|
||||
g = 255
|
||||
for b in range(256):
|
||||
if b == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
|
||||
b = 255
|
||||
g = 255
|
||||
for r in range(256):
|
||||
if r == 128:
|
||||
print()
|
||||
sys.stdout.write('\x1b[48;2;%d;%d;%dm \x1b[m' % (r, g, b))
|
||||
print()
|
||||
63
butterfly/bin/help.py
Normal file
63
butterfly/bin/help.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import butterfly
|
||||
from butterfly.escapes import image
|
||||
from butterfly.utils import ansi_colors
|
||||
|
||||
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
|
||||
path = os.getenv('BUTTERFLY_PATH')
|
||||
if path:
|
||||
path = os.path.join(path, '../static/images/favicon.png')
|
||||
|
||||
if path and os.path.exists(path):
|
||||
with image('image/png'):
|
||||
with open(path, 'rb') as i:
|
||||
print(base64.b64encode(i.read()).decode('ascii'))
|
||||
print("""
|
||||
Butterfly is a xterm compliant terminal built with python and javascript.
|
||||
|
||||
{title}Terminal functionalities:{reset}
|
||||
{strong}[ScrollLock] : {reset}Lock the scrolling to the current position. Press again to release.
|
||||
{strong}[Ctrl] + [c] <<hold>> : {reset}Cut the output when [Ctrl] + [c] is not enough.
|
||||
{strong}[Ctrl] + [Shift] + [Up] : {reset}Trigger visual selection mode. Hitting [Enter] inserts the selection in the prompt.
|
||||
{strong}[Alt] + [a] : {reset}Set an alarm which sends a notification when a modification is detected. (Ring on regexp match with [Shift])
|
||||
{strong}[Alt] + [s] : {reset}Open theme selection prompt. Use [Alt] + [Shift] + [s] to refresh current theme.
|
||||
{strong}[Alt] + [e] : {reset}List open user sessions. (Only available in secure mode)
|
||||
{strong}[Alt] + [o] : {reset}Open new terminal (As a popup)
|
||||
{strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key.
|
||||
Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]).
|
||||
|
||||
|
||||
{title}Butterfly programs:{reset}
|
||||
{strong}b : {reset}Alias for {strong}butterfly{reset} executable. Takes a comand in parameter or launch a butterfly server for one shot use (if outside butterfly).
|
||||
{strong}b cat : {reset}A wrapper around cat allowing to display images as <img> instead of binary.
|
||||
{strong}b open : {reset}Open a new terminal at specified location.
|
||||
{strong}b session : {reset}Open or rattach a butterfly session. Multiplexing is supported.
|
||||
{strong}b colors : {reset}Test the terminal colors (16, 256 and 16777216 colors)
|
||||
{strong}b html : {reset}Output in html standard input.
|
||||
|
||||
For more butterfly programs check out: https://github.com/paradoxxxzero/butterfly-demos
|
||||
|
||||
|
||||
{title}Styling butterfly:{reset}
|
||||
To style butterfly in sass, you need to have the libsass python library installed.
|
||||
|
||||
Theming is done by overriding the default sass files located in {code}{main}{reset} in your theme directory.
|
||||
This directory can include images and custom fonts.
|
||||
Please take a look at official themes here: https://github.com/paradoxxxzero/butterfly-themes
|
||||
and submit your best themes as pull request!
|
||||
|
||||
\x1b[{rcol}G\x1b[3m{dark}butterfly @ 2015 Mounier Florian{reset}\
|
||||
""".format(
|
||||
title=ansi_colors.light_blue,
|
||||
dark=ansi_colors.light_black,
|
||||
strong=ansi_colors.white,
|
||||
code=ansi_colors.light_yellow,
|
||||
comment=ansi_colors.light_magenta,
|
||||
reset=ansi_colors.reset,
|
||||
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'))))
|
||||
19
butterfly/bin/html.py
Normal file
19
butterfly/bin/html.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
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"
|
||||
"Example: $ echo \"<b>Bold</b>\" | b html",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
|
||||
parser.parse_known_args()
|
||||
|
||||
|
||||
with html():
|
||||
for line in fileinput.input():
|
||||
sys.stdout.write(line)
|
||||
27
butterfly/bin/open.py
Normal file
27
butterfly/bin/open.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
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(
|
||||
'location',
|
||||
nargs='?',
|
||||
default=os.getcwd(),
|
||||
help='Directory to open the new tab in. (Defaults to current)')
|
||||
args = parser.parse_args()
|
||||
|
||||
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)
|
||||
15
butterfly/bin/session.py
Normal file
15
butterfly/bin/session.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
parser = argparse.ArgumentParser(description='Butterfly session opener.')
|
||||
parser.add_argument(
|
||||
'session',
|
||||
help='Open or rattach a butterfly session. '
|
||||
'(Only in secure mode or in user unsecure mode (no su login))')
|
||||
args = parser.parse_args()
|
||||
|
||||
url = '%ssession/%s' % (os.getenv('LOCATION', '/'), args.session)
|
||||
if not webbrowser.open(url):
|
||||
print('Unable to open browser, please go to %s' % url)
|
||||
58
butterfly/butterfly.conf.default
Normal file
58
butterfly/butterfly.conf.default
Normal file
@@ -0,0 +1,58 @@
|
||||
# Butterfly autogenerated config file
|
||||
|
||||
# Activate debug mode
|
||||
#
|
||||
#debug=False
|
||||
|
||||
# In debug mode produce more verbose output
|
||||
#
|
||||
#more=False
|
||||
|
||||
# Use unminified version of js for development
|
||||
#
|
||||
#unminified=False
|
||||
|
||||
# Server host
|
||||
# Use 'localhost' for local only
|
||||
# Use your ip to share over your network
|
||||
# Use '0.0.0.0' to listen to every network
|
||||
#
|
||||
#host='localhost'
|
||||
|
||||
# Server port
|
||||
#
|
||||
#port=57575
|
||||
|
||||
# Shell to launch at start (defaults to user shell)
|
||||
#
|
||||
#shell=None # shell='/bin/bash' for instance
|
||||
|
||||
# Motd, path to custom message of the day file
|
||||
#
|
||||
#motd='motd'
|
||||
|
||||
# Command to run instead of shell
|
||||
#
|
||||
#cmd=None # cmd='ls -l'
|
||||
|
||||
|
||||
# Unsecure mode
|
||||
# This mode use http without ssl and is therefore NOT RECOMMENDED
|
||||
# Please generate yourself a certificate using the butterfly.server.py command
|
||||
#
|
||||
#unsecure=False
|
||||
|
||||
# Force user login in unsecure mode
|
||||
#
|
||||
#login=False
|
||||
|
||||
# Force unicode width
|
||||
# This mode force every character to be the same width
|
||||
# Which can be useful in some case
|
||||
# But this breaks unicode display of varying width character
|
||||
#
|
||||
#force_unicode_width=False
|
||||
|
||||
# SSL version defaults to auto
|
||||
#
|
||||
#ssl_version=None
|
||||
70
butterfly/escapes.py
Normal file
70
butterfly/escapes.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from contextlib import contextmanager
|
||||
|
||||
from butterfly.utils import ansi_colors as colors # noqa: F401
|
||||
|
||||
|
||||
@contextmanager
|
||||
def html():
|
||||
sys.stdout.write('\x1bP;HTML|')
|
||||
yield
|
||||
sys.stdout.write('\x1bP')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def image(mime='image'):
|
||||
sys.stdout.write('\x1bP;IMAGE|%s;' % mime)
|
||||
yield
|
||||
sys.stdout.write('\x1bP\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def prompt():
|
||||
sys.stdout.write('\x1bP;PROMPT|')
|
||||
yield
|
||||
sys.stdout.write('\x1bP')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def text():
|
||||
sys.stdout.write('\x1bP;TEXT|')
|
||||
yield
|
||||
sys.stdout.write('\x1bP')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def geolocation():
|
||||
sys.stdout.write('\x1b[?99n')
|
||||
sys.stdout.flush()
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != '\x1b':
|
||||
raise
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != '[':
|
||||
raise
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != '?':
|
||||
raise
|
||||
|
||||
loc = ''
|
||||
while rv != 'R':
|
||||
rv = sys.stdin.read(1)
|
||||
if rv != 'R':
|
||||
loc += rv
|
||||
except Exception:
|
||||
return
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
if not loc or ';' not in loc:
|
||||
return
|
||||
return tuple(map(float, loc.split(';')))
|
||||
192
butterfly/pam.py
Normal file
192
butterfly/pam.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# (c) 2007 Chris AtLee <chris@atlee.ca>
|
||||
# Licensed under the MIT license:
|
||||
# http://www.opensource.org/licenses/mit-license.php
|
||||
#
|
||||
# Original author: Chris AtLee
|
||||
#
|
||||
# Modified by David Ford, 2011-12-6
|
||||
# added py3 support and encoding
|
||||
# added pam_end
|
||||
# added pam_setcred to reset credentials after seeing Leon Walker's remarks
|
||||
# added byref as well
|
||||
# use readline to prestuff the getuser input
|
||||
# Modified by Peter Cai, 2017-02-10
|
||||
# interactive login for Butterfly
|
||||
|
||||
'''
|
||||
PAM module for python
|
||||
Provides an authenticate function that will allow the caller to authenticate
|
||||
a user against the Pluggable Authentication Modules (PAM) on the system.
|
||||
Implemented using ctypes, so no compilation is necessary.
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
from ctypes import (
|
||||
CDLL, CFUNCTYPE, POINTER, Structure, byref, c_char_p, c_int, c_size_t,
|
||||
c_void_p)
|
||||
from ctypes.util import find_library
|
||||
|
||||
|
||||
class PamHandle(Structure):
|
||||
"""wrapper class for pam_handle_t pointer"""
|
||||
_fields_ = [("handle", c_void_p)]
|
||||
|
||||
def __init__(self):
|
||||
Structure.__init__(self)
|
||||
self.handle = 0
|
||||
|
||||
|
||||
class PamMessage(Structure):
|
||||
"""wrapper class for pam_message structure"""
|
||||
_fields_ = [("msg_style", c_int), ("msg", c_char_p)]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
|
||||
|
||||
|
||||
class PamResponse(Structure):
|
||||
"""wrapper class for pam_response structure"""
|
||||
_fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
|
||||
|
||||
|
||||
conv_func = CFUNCTYPE(
|
||||
c_int, c_int, POINTER(POINTER(PamMessage)),
|
||||
POINTER(POINTER(PamResponse)), c_void_p)
|
||||
|
||||
|
||||
class PamConv(Structure):
|
||||
"""wrapper class for pam_conv structure"""
|
||||
_fields_ = [("conv", conv_func), ("appdata_ptr", c_void_p)]
|
||||
|
||||
|
||||
# Various constants
|
||||
PAM_PROMPT_ECHO_OFF = 1
|
||||
PAM_PROMPT_ECHO_ON = 2
|
||||
PAM_ERROR_MSG = 3
|
||||
PAM_TEXT_INFO = 4
|
||||
PAM_REINITIALIZE_CRED = 8
|
||||
|
||||
libc = CDLL(find_library("c"))
|
||||
libpam = CDLL(find_library("pam"))
|
||||
libpam_misc = CDLL(find_library("pam_misc"))
|
||||
|
||||
calloc = libc.calloc
|
||||
calloc.restype = c_void_p
|
||||
calloc.argtypes = [c_size_t, c_size_t]
|
||||
|
||||
pam_end = libpam.pam_end
|
||||
pam_end.restype = c_int
|
||||
pam_end.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_start = libpam.pam_start
|
||||
pam_start.restype = c_int
|
||||
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
|
||||
|
||||
pam_setcred = libpam.pam_setcred
|
||||
pam_setcred.restype = c_int
|
||||
pam_setcred.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_strerror = libpam.pam_strerror
|
||||
pam_strerror.restype = c_char_p
|
||||
pam_strerror.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_authenticate = libpam.pam_authenticate
|
||||
pam_authenticate.restype = c_int
|
||||
pam_authenticate.argtypes = [PamHandle, c_int]
|
||||
|
||||
misc_conv = libpam_misc.misc_conv
|
||||
|
||||
|
||||
class PAM():
|
||||
code = 0
|
||||
reason = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def authenticate(
|
||||
self, username,
|
||||
service='login', encoding='utf-8', resetcreds=True):
|
||||
"""PAM authentication through standard input for the given service.
|
||||
Returns True for success, or False for failure.
|
||||
self.code (integer) and self.reason (string) are always stored
|
||||
and may be referenced for the reason why authentication failed.
|
||||
0/'Success' will be stored for success.
|
||||
Python3 expects bytes() for ctypes inputs. This function will make
|
||||
necessary conversions using the supplied encoding.
|
||||
Inputs:
|
||||
username: username to authenticate
|
||||
service: PAM service to authenticate against, defaults to 'login'
|
||||
Returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
# python3 ctypes prefers bytes
|
||||
if sys.version_info >= (3,):
|
||||
if isinstance(username, str):
|
||||
username = username.encode(encoding)
|
||||
if isinstance(service, str):
|
||||
service = service.encode(encoding)
|
||||
else:
|
||||
if isinstance(username, unicode): # noqa: F821
|
||||
username = username.encode(encoding)
|
||||
if isinstance(service, unicode): # noqa: F821
|
||||
service = service.encode(encoding)
|
||||
|
||||
if b'\x00' in username or b'\x00' in service:
|
||||
self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM
|
||||
self.reason = 'strings may not contain NUL'
|
||||
return False
|
||||
|
||||
handle = PamHandle()
|
||||
conv = PamConv(conv_func(misc_conv), 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
if retval != 0:
|
||||
# This is not an authentication error,
|
||||
# something has gone wrong starting up PAM
|
||||
self.code = retval
|
||||
self.reason = "pam_start() failed"
|
||||
return False
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
auth_success = retval == 0
|
||||
|
||||
if auth_success and resetcreds:
|
||||
retval = pam_setcred(handle, PAM_REINITIALIZE_CRED)
|
||||
|
||||
# store information to inform the caller why we failed
|
||||
self.code = retval
|
||||
self.reason = pam_strerror(handle, retval)
|
||||
if sys.version_info >= (3,):
|
||||
self.reason = self.reason.decode(encoding)
|
||||
|
||||
pam_end(handle, retval)
|
||||
|
||||
return auth_success
|
||||
|
||||
|
||||
def login_prompt(username, profile, env):
|
||||
pam = PAM()
|
||||
|
||||
success = pam.authenticate(username, profile)
|
||||
print('{} {}'.format(pam.code, pam.reason))
|
||||
|
||||
if success:
|
||||
su = '/usr/bin/su'
|
||||
if not os.path.exists(su):
|
||||
su = '/bin/su'
|
||||
os.execvpe(su, [su, '-l', username], env)
|
||||
return success
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if login_prompt(sys.argv[1], sys.argv[2], os.environ):
|
||||
exit(0)
|
||||
else:
|
||||
exit(1)
|
||||
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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,234 +16,385 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import pty
|
||||
import json
|
||||
import os
|
||||
import io
|
||||
import struct
|
||||
import fcntl
|
||||
import termios
|
||||
import tornado.websocket
|
||||
import tornado.process
|
||||
import tornado.ioloop
|
||||
import tornado.options
|
||||
import sys
|
||||
from butterfly import url, Route, utils
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from mimetypes import guess_type
|
||||
from uuid import uuid4
|
||||
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
import tornado.escape
|
||||
import tornado.options
|
||||
import tornado.process
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
|
||||
server = utils.User()
|
||||
daemon = utils.User(name='daemon')
|
||||
from butterfly import Route, url, utils
|
||||
from butterfly.terminal import Terminal
|
||||
|
||||
|
||||
def motd(socket, caller, callee):
|
||||
return (
|
||||
'''
|
||||
B ` '
|
||||
;,,, ` ' ,,,;
|
||||
`Y888888bo. : : .od888888Y'
|
||||
8888888888b. : : .d8888888888 AWelcome to RbutterflyB
|
||||
88888Y' `Y8b. ` ' .d8Y' `Y88888
|
||||
j88888 R.db.B Yb. ' ' .dY R.db.B 88888k AServer running as G%rB
|
||||
`888 RY88YB `b ( ) d' RY88YB 888'
|
||||
888b R'"B ,', R"'B d888 AConnecting to:B
|
||||
j888888bd8gf"' ':' `"?g8bd888888k AHost: G%sB
|
||||
R'Y'B .8' d' 'b '8. R'Y'X AUser: G%rB
|
||||
R!B .8' RdbB d'; ;`b RdbB '8. R!B
|
||||
d88 R`'B 8 ; ; 8 R`'B 88b AFrom:B
|
||||
d888b .g8 ',' 8g. d888b AHost: G%sB
|
||||
:888888888Y' 'Y888888888: AUser: G%rB
|
||||
'! 8888888' `8888888 !'
|
||||
'8Y R`Y Y'B Y8'
|
||||
R Y Y
|
||||
! !X
|
||||
|
||||
'''
|
||||
.replace('B', '\x1b[34;1m')
|
||||
.replace('G', '\x1b[32;1m')
|
||||
.replace('R', '\x1b[37;1m')
|
||||
.replace('A', '\x1b[37;0m')
|
||||
.replace('X', '\x1b[0m')
|
||||
.replace('\n', '\r\n')
|
||||
% (
|
||||
server,
|
||||
'%s:%d' % (socket.remote_addr, socket.remote_port),
|
||||
callee,
|
||||
'%s:%d' % (socket.local_addr, socket.local_port),
|
||||
caller or '?'))
|
||||
def u(s):
|
||||
if sys.version_info[0] == 2:
|
||||
return s.decode('utf-8')
|
||||
return s
|
||||
|
||||
|
||||
@url(r'/(?:user/(.+))?/?(?:wd/(.+))?')
|
||||
@url(r'/(?:session/(?P<session>[^/]+)/?)?')
|
||||
class Index(Route):
|
||||
def get(self, user, path):
|
||||
return self.render('index.html')
|
||||
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', session=session or str(uuid4()))
|
||||
|
||||
|
||||
@url(r'/ws(?:/user/([^/]+))?/?(?:/wd/(.+))?')
|
||||
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||
|
||||
def pty(self):
|
||||
self.pid, self.fd = pty.fork()
|
||||
if self.pid == 0:
|
||||
try:
|
||||
os.closerange(3, 256)
|
||||
except:
|
||||
pass
|
||||
self.shell()
|
||||
else:
|
||||
self.communicate()
|
||||
|
||||
def shell(self):
|
||||
while self.callee is None:
|
||||
user = input('login: ')
|
||||
try:
|
||||
self.callee = utils.User(name=user)
|
||||
except:
|
||||
print('User %s not found' % user)
|
||||
@url(r'/theme/([^/]+)/style.css')
|
||||
class Theme(Route):
|
||||
|
||||
def get(self, theme):
|
||||
self.log.info('Getting style')
|
||||
try:
|
||||
os.chdir(self.path or self.callee.dir)
|
||||
except:
|
||||
pass
|
||||
import sass
|
||||
sass.CompileError
|
||||
except Exception:
|
||||
self.log.error(
|
||||
'You must install libsass to use sass '
|
||||
'(pip install libsass)')
|
||||
return
|
||||
base_dir = self.get_theme_dir(theme)
|
||||
|
||||
env = os.environ
|
||||
env.update(self.socket.env)
|
||||
style = None
|
||||
for ext in ['css', 'scss', 'sass']:
|
||||
probable_style = os.path.join(base_dir, 'style.%s' % ext)
|
||||
if os.path.exists(probable_style):
|
||||
style = probable_style
|
||||
|
||||
env["TERM"] = "xterm-256color"
|
||||
env["COLORTERM"] = "butterfly"
|
||||
env["HOME"] = self.callee.dir
|
||||
env["SHELL"] = self.callee.shell
|
||||
env["LOCATION"] = "http://%s:%d/" % (
|
||||
tornado.options.options.host, tornado.options.options.port)
|
||||
env["PATH"] = '%s:%s' % (os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', 'bin')), env.get("PATH"))
|
||||
args = ['butterfly']
|
||||
if not style:
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
if self.socket.local:
|
||||
# All users are the same -> launch shell
|
||||
if self.caller == self.callee and server == self.callee:
|
||||
args.append('-i')
|
||||
os.execvpe(
|
||||
tornado.options.options.shell or self.callee.shell,
|
||||
args, env)
|
||||
# This process has been replaced
|
||||
return
|
||||
sass_path = os.path.join(
|
||||
os.path.dirname(__file__), 'sass')
|
||||
|
||||
if server.root:
|
||||
if self.callee != self.caller:
|
||||
# Force password prompt by dropping rights
|
||||
# to the daemon user
|
||||
os.setuid(daemon.uid)
|
||||
else:
|
||||
# We are not local so we should always get a password prompt
|
||||
if server.root:
|
||||
if self.callee == daemon:
|
||||
# No logging from daemon
|
||||
sys.exit(1)
|
||||
os.setuid(daemon.uid)
|
||||
css = None
|
||||
try:
|
||||
css = sass.compile(filename=style, include_paths=[
|
||||
base_dir, sass_path])
|
||||
except sass.CompileError:
|
||||
self.log.error(
|
||||
'Unable to compile style (filename: %s, paths: %r) ' % (
|
||||
style, [base_dir, sass_path]), exc_info=True)
|
||||
if not style:
|
||||
raise tornado.web.HTTPError(500)
|
||||
|
||||
args.append('-p')
|
||||
if tornado.options.options.shell:
|
||||
args.append('-s')
|
||||
args.append(tornado.options.options.shell)
|
||||
args.append(self.callee.name)
|
||||
os.execvpe('/bin/su', args, env)
|
||||
self.log.debug('Style ok')
|
||||
self.set_header("Content-Type", "text/css")
|
||||
self.write(css)
|
||||
self.finish()
|
||||
|
||||
def communicate(self):
|
||||
self.log.debug('Adding handler')
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
|
||||
def utf8_error(e):
|
||||
self.log.error(e)
|
||||
@url(r'/theme/([^/]+)/(.+)')
|
||||
class ThemeStatic(Route):
|
||||
def get(self, theme, name):
|
||||
if '..' in name:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
self.reader = io.open(
|
||||
self.fd,
|
||||
'rb',
|
||||
buffering=0,
|
||||
closefd=False
|
||||
)
|
||||
self.writer = io.open(
|
||||
self.fd,
|
||||
'wt',
|
||||
encoding='utf-8',
|
||||
closefd=False
|
||||
)
|
||||
ioloop.add_handler(
|
||||
self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR)
|
||||
base_dir = self.get_theme_dir(theme)
|
||||
|
||||
def open(self, user, path):
|
||||
self.socket = utils.Socket(self.ws_connection.stream.socket)
|
||||
self.set_nodelay(True)
|
||||
self.log.info('Websocket opened %r' % self.socket)
|
||||
self.path = path
|
||||
self.user = user.decode('utf-8') if user else None
|
||||
self.caller = self.callee = None
|
||||
if self.socket.local:
|
||||
self.caller = utils.User(uid=self.socket.uid)
|
||||
else:
|
||||
# We don't know uid is on the other machine
|
||||
pass
|
||||
fn = os.path.normpath(os.path.join(base_dir, name))
|
||||
if not fn.startswith(base_dir):
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
if self.user:
|
||||
try:
|
||||
self.callee = utils.User(name=self.user)
|
||||
except LookupError:
|
||||
print('User %s not found' % self.user)
|
||||
self.callee = None
|
||||
if os.path.exists(fn):
|
||||
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')
|
||||
|
||||
# If no user where given and we are local, keep the same user
|
||||
# as the one who opened the socket
|
||||
# ie: the one openning a terminal in borwser
|
||||
if not self.callee and not self.user and self.socket.local:
|
||||
self.callee = self.caller
|
||||
self.write_message(motd(self.socket, self.caller, self.callee))
|
||||
self.pty()
|
||||
self.set_header("Content-Type", type)
|
||||
with open(fn, 'rb') as s:
|
||||
while True:
|
||||
data = s.read(16384)
|
||||
if data:
|
||||
self.write(data)
|
||||
else:
|
||||
break
|
||||
self.finish()
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
def on_message(self, message):
|
||||
if message[0] == 'R':
|
||||
cols, rows = map(int, message[1:].split(','))
|
||||
s = struct.pack("HHHH", rows, cols, 0, 0)
|
||||
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
|
||||
self.log.info('SIZE (%d, %d)' % (cols, rows))
|
||||
elif message[0] == 'S':
|
||||
self.log.info('WRIT<%r' % message)
|
||||
self.writer.write(message[1:])
|
||||
self.writer.flush()
|
||||
|
||||
def shell_handler(self, fd, events):
|
||||
if events & ioloop.READ:
|
||||
try:
|
||||
read = self.reader.read()
|
||||
except IOError:
|
||||
self.log.info('READ>%r' % read)
|
||||
self.write_message('DIE')
|
||||
return
|
||||
class KeptAliveWebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
keepalive_timer = None
|
||||
|
||||
self.log.info('READ>%r' % read)
|
||||
self.write_message(read.decode('utf-8', 'replace'))
|
||||
def open(self, *args, **kwargs):
|
||||
self.keepalive_timer = tornado.ioloop.PeriodicCallback(
|
||||
self.send_ping, tornado.options.options.keepalive_interval * 1000)
|
||||
self.keepalive_timer.start()
|
||||
|
||||
if events & ioloop.ERROR:
|
||||
self.log.info('Closing due to ioloop fd handler error')
|
||||
ioloop.remove_handler(self.fd)
|
||||
|
||||
# Terminated
|
||||
self.on_close()
|
||||
self.close()
|
||||
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.pid == 0:
|
||||
self.log.warning('pid is 0')
|
||||
if self.keepalive_timer is not None:
|
||||
self.keepalive_timer.stop()
|
||||
|
||||
|
||||
@url(r'/ctl/session/(?P<session>[^/]+)')
|
||||
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)
|
||||
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 tornado.options.options.unsecure:
|
||||
user = utils.parse_cert(
|
||||
self.ws_connection.stream.socket.getpeercert())
|
||||
assert user, 'No user in certificate'
|
||||
try:
|
||||
user = utils.User(name=user)
|
||||
except LookupError:
|
||||
raise Exception('Invalid user in certificate')
|
||||
|
||||
# Certificate authed user
|
||||
secure_user = user
|
||||
|
||||
elif socket.local and socket.user == utils.User() and not user:
|
||||
# Local to local returning browser user
|
||||
secure_user = socket.user
|
||||
elif user:
|
||||
try:
|
||||
user = utils.User(name=user)
|
||||
except LookupError:
|
||||
raise Exception('Invalid user')
|
||||
|
||||
if secure_user:
|
||||
user = secure_user
|
||||
if self.session in self.sessions and self.session in (
|
||||
self.sessions_secure_users):
|
||||
if user.name != self.sessions_secure_users[self.session]:
|
||||
# Restrict to authorized users
|
||||
raise tornado.web.HTTPError(403)
|
||||
else:
|
||||
self.sessions_secure_users[self.session] = user.name
|
||||
|
||||
self.sessions[self.session].append(self)
|
||||
|
||||
terminal = Terminal.sessions.get(self.session)
|
||||
# Handling terminal session
|
||||
if terminal:
|
||||
TermWebSocket.last.write_message(terminal.history)
|
||||
# And returning, we don't want another terminal
|
||||
return
|
||||
try:
|
||||
self.writer.write('')
|
||||
self.writer.flush()
|
||||
except OSError:
|
||||
self.log.warning('closing term fail', exc_info=True)
|
||||
try:
|
||||
os.close(self.fd)
|
||||
except OSError:
|
||||
self.log.warning('closing fd fail', exc_info=True)
|
||||
try:
|
||||
os.waitpid(self.pid, 0)
|
||||
except OSError:
|
||||
self.log.warning('waitpid fail', exc_info=True)
|
||||
self.log.info('Websocket closed')
|
||||
|
||||
# New session, opening terminal
|
||||
terminal = Terminal(
|
||||
user, path, self.session, socket,
|
||||
self.request.full_url().replace('/ctl/', '/'), self.render_string,
|
||||
TermWebSocket.broadcast)
|
||||
|
||||
terminal.pty()
|
||||
self.log.info('Openning session %s for secure user %r' % (
|
||||
self.session, user))
|
||||
|
||||
@classmethod
|
||||
def broadcast(cls, session, message, emitter=None):
|
||||
for wsocket in cls.sessions[session]:
|
||||
try:
|
||||
if wsocket != emitter:
|
||||
wsocket.write_message(message)
|
||||
except Exception:
|
||||
wsocket.log.exception('Error on broadcast')
|
||||
wsocket.close()
|
||||
|
||||
def on_message(self, message):
|
||||
cmd = json.loads(message)
|
||||
if cmd['cmd'] == 'open':
|
||||
self.create_terminal()
|
||||
else:
|
||||
try:
|
||||
Terminal.sessions[self.session].ctl(cmd)
|
||||
except Exception:
|
||||
# FF strange bug
|
||||
pass
|
||||
self.broadcast(self.session, message, self)
|
||||
|
||||
def on_close(self):
|
||||
super(TermCtlWebSocket, self).on_close()
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
self.log.info('Websocket /ctl closed %r' % self)
|
||||
if self in self.sessions[self.session]:
|
||||
self.sessions[self.session].remove(self)
|
||||
|
||||
if tornado.options.options.one_shot or (
|
||||
getattr(self.application, 'systemd', False) and
|
||||
not sum([
|
||||
len(wsockets)
|
||||
for session, wsockets in self.sessions.items()])):
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@url(r'/ws/session/(?P<session>[^/]+)')
|
||||
class TermWebSocket(Route, KeptAliveWebSocketHandler):
|
||||
# List of websockets per session
|
||||
sessions = defaultdict(list)
|
||||
|
||||
# Last is kept for session shared history send
|
||||
last = None
|
||||
|
||||
# Session history
|
||||
history = {}
|
||||
|
||||
def open(self, session):
|
||||
super(TermWebSocket, self).open(session)
|
||||
self.set_nodelay(True)
|
||||
self.session = session
|
||||
self.closed = False
|
||||
self.sessions[session].append(self)
|
||||
self.__class__.last = self
|
||||
self.log.info('Websocket /ws opened %r' % self)
|
||||
|
||||
@classmethod
|
||||
def close_session(cls, session):
|
||||
wsockets = (cls.sessions.get(session, []) +
|
||||
TermCtlWebSocket.sessions.get(session, []))
|
||||
for wsocket in wsockets:
|
||||
wsocket.on_close()
|
||||
|
||||
wsocket.close()
|
||||
|
||||
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):
|
||||
if message is None:
|
||||
cls.close_session(session)
|
||||
return
|
||||
|
||||
wsockets = cls.sessions.get(session)
|
||||
for wsocket in wsockets:
|
||||
try:
|
||||
if wsocket != emitter:
|
||||
wsocket.write_message(message)
|
||||
except Exception:
|
||||
wsocket.log.exception('Error on broadcast')
|
||||
wsocket.close()
|
||||
|
||||
def on_message(self, message):
|
||||
Terminal.sessions[self.session].write(message)
|
||||
|
||||
def on_close(self):
|
||||
super(TermWebSocket, self).on_close()
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
self.log.info('Websocket /ws closed %r' % self)
|
||||
self.sessions[self.session].remove(self)
|
||||
|
||||
|
||||
@url(r'/sessions/list.json')
|
||||
class SessionsList(Route):
|
||||
"""Get the theme list"""
|
||||
|
||||
def get(self):
|
||||
if tornado.options.options.unsecure:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
cert = self.request.get_ssl_certificate()
|
||||
user = utils.parse_cert(cert)
|
||||
|
||||
if not user:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
self.set_header('Content-Type', 'application/json')
|
||||
self.write(tornado.escape.json_encode({
|
||||
'sessions': sorted(
|
||||
TermWebSocket.sessions),
|
||||
'user': user
|
||||
}))
|
||||
|
||||
|
||||
@url(r'/themes/list.json')
|
||||
class ThemesList(Route):
|
||||
"""Get the theme list"""
|
||||
|
||||
def get(self):
|
||||
|
||||
if os.path.exists(self.themes_dir):
|
||||
themes = [
|
||||
theme
|
||||
for theme in os.listdir(self.themes_dir)
|
||||
if os.path.isdir(os.path.join(self.themes_dir, theme)) and
|
||||
not theme.startswith('.')]
|
||||
else:
|
||||
themes = []
|
||||
|
||||
if os.path.exists(self.builtin_themes_dir):
|
||||
builtin_themes = [
|
||||
'built-in-%s' % theme
|
||||
for theme in os.listdir(self.builtin_themes_dir)
|
||||
if os.path.isdir(os.path.join(
|
||||
self.builtin_themes_dir, theme)) and
|
||||
not theme.startswith('.')]
|
||||
else:
|
||||
builtin_themes = []
|
||||
|
||||
self.set_header('Content-Type', 'application/json')
|
||||
self.write(tornado.escape.json_encode({
|
||||
'themes': sorted(themes),
|
||||
'builtin_themes': sorted(builtin_themes),
|
||||
'dir': self.themes_dir
|
||||
}))
|
||||
|
||||
|
||||
@url('/local.js')
|
||||
class LocalJsStatic(Route):
|
||||
def get(self):
|
||||
self.set_header("Content-Type", 'application/javascript')
|
||||
if os.path.exists(self.local_js_dir):
|
||||
for fn in os.listdir(self.local_js_dir):
|
||||
if not fn.endswith('.js'):
|
||||
continue
|
||||
with open(os.path.join(self.local_js_dir, fn), 'rb') as s:
|
||||
while True:
|
||||
data = s.read(16384)
|
||||
if data:
|
||||
self.write(data)
|
||||
else:
|
||||
self.write(';')
|
||||
break
|
||||
self.finish()
|
||||
|
||||
36
butterfly/sass/_16_colors.sass
Normal file
36
butterfly/sass/_16_colors.sass
Normal file
@@ -0,0 +1,36 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
|
||||
/* Here are the 16 "normal" colors for theming */
|
||||
|
||||
+termcolor(0, nth($colors, 1))
|
||||
+termcolor(1, nth($colors, 2))
|
||||
+termcolor(2, nth($colors, 3))
|
||||
+termcolor(3, nth($colors, 4))
|
||||
+termcolor(4, nth($colors, 5))
|
||||
+termcolor(5, nth($colors, 6))
|
||||
+termcolor(6, nth($colors, 7))
|
||||
+termcolor(7, nth($colors, 8))
|
||||
+termcolor(8, nth($colors, 9))
|
||||
+termcolor(9, nth($colors, 10))
|
||||
+termcolor(10, nth($colors, 11))
|
||||
+termcolor(11, nth($colors, 12))
|
||||
+termcolor(12, nth($colors, 13))
|
||||
+termcolor(13, nth($colors, 14))
|
||||
+termcolor(14, nth($colors, 15))
|
||||
+termcolor(15, nth($colors, 16))
|
||||
34
butterfly/sass/_256_colors.sass
Normal file
34
butterfly/sass/_256_colors.sass
Normal file
@@ -0,0 +1,34 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
/* Here are the 240 xterm colors */
|
||||
/* See http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg */
|
||||
|
||||
$st: 00, 95, 135, 175, 215, 255
|
||||
|
||||
@for $i from 0 through 215
|
||||
$r: nth($st, 1 + floor(($i / 36) % 6))
|
||||
$g: nth($st, 1 + floor(($i / 6) % 6))
|
||||
$b: nth($st, 1 + $i % 6)
|
||||
+termcolor($i + 16, rgb($r, $g, $b))
|
||||
|
||||
@for $i from 0 through 23
|
||||
$l: 8 + $i * 10
|
||||
+termcolor($i + 232, rgb($l, $l, $l))
|
||||
|
||||
+termcolor(256, $default-bg)
|
||||
+termcolor(257, $default-fg)
|
||||
6
butterfly/sass/_all_fx.sass
Normal file
6
butterfly/sass/_all_fx.sass
Normal file
@@ -0,0 +1,6 @@
|
||||
body
|
||||
&.copied
|
||||
transform: scale(1.05)
|
||||
|
||||
&.pasted
|
||||
transform: scale(.95)
|
||||
33
butterfly/sass/_colors.sass
Normal file
33
butterfly/sass/_colors.sass
Normal file
@@ -0,0 +1,33 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
=termcolor($i, $color)
|
||||
.bg-color-#{$i}
|
||||
background-color: $color
|
||||
&.reverse-video
|
||||
@if $color == transparent
|
||||
color: $reverse-transparent !important
|
||||
@else
|
||||
color: $color !important
|
||||
|
||||
.fg-color-#{$i}
|
||||
color: $color
|
||||
&.reverse-video
|
||||
background-color: $color !important
|
||||
|
||||
@if $shadow != 0
|
||||
text-shadow: 0 0 $shadow rgba($color, $shadow-alpha)
|
||||
22
butterfly/sass/_cursor.sass
Normal file
22
butterfly/sass/_cursor.sass
Normal file
@@ -0,0 +1,22 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
.focus .cursor
|
||||
transition: 300ms
|
||||
|
||||
.cursor.reverse-video
|
||||
box-shadow: 0 0 $shadow-alpha $fg
|
||||
32
butterfly/sass/_font.sass
Normal file
32
butterfly/sass/_font.sass
Normal file
@@ -0,0 +1,32 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
$weights: (ExtraLight 100) (Light 300) (Regular 400) (Medium 500) (Semibold 600) (Bold 700) (Black 900)
|
||||
|
||||
@if $font-family == "SourceCodePro"
|
||||
@each $weight in $weights
|
||||
$weight_name: nth($weight, 1)
|
||||
|
||||
@font-face
|
||||
font-family: "SourceCodePro"
|
||||
src: url("fonts/SourceCodePro-#{$weight_name}.otf") format("woff")
|
||||
font-weight: nth($weight, 2)
|
||||
|
||||
body
|
||||
font-family: $font-family
|
||||
font-size: $font-size
|
||||
line-height: $font-line-height
|
||||
112
butterfly/sass/_layout.sass
Normal file
112
butterfly/sass/_layout.sass
Normal file
@@ -0,0 +1,112 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
html, body
|
||||
margin: 0
|
||||
padding: 0
|
||||
background-color: $bg
|
||||
color: $fg
|
||||
|
||||
body
|
||||
padding-bottom: .5em
|
||||
white-space: nowrap
|
||||
overflow-x: hidden
|
||||
overflow-y: scroll
|
||||
a
|
||||
text-decoration: underline rgba($fg, .2)
|
||||
transition: text-decoration-color 500ms
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
.line.active
|
||||
background-color: $active-bg
|
||||
|
||||
.line.extended
|
||||
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%)
|
||||
|
||||
&.expanded
|
||||
cursor: zoom-out
|
||||
background-color: darken($bg, 3%)
|
||||
|
||||
.extra
|
||||
display: block
|
||||
white-space: pre-wrap
|
||||
word-break: break-all
|
||||
|
||||
&::-webkit-scrollbar
|
||||
background: $scroll-bg
|
||||
width: $scroll-width
|
||||
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: $scroll-fg
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover
|
||||
background: $scroll-fg-hover
|
||||
|
||||
/* Pop ups */
|
||||
.hidden
|
||||
display: none !important
|
||||
|
||||
#popup
|
||||
position: fixed
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
form, > div
|
||||
padding: 1.5em
|
||||
background: $popup-bg
|
||||
color: $popup-fg
|
||||
font-size: $popup-fs
|
||||
|
||||
h2
|
||||
margin: 0 .5em .5em .5em
|
||||
select
|
||||
min-width: 300px
|
||||
padding: .5em
|
||||
width: 100%
|
||||
label
|
||||
display: block
|
||||
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
|
||||
60
butterfly/sass/_light_fx.sass
Normal file
60
butterfly/sass/_light_fx.sass
Normal file
@@ -0,0 +1,60 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
body
|
||||
transition: filter 200ms
|
||||
transform-origin: bottom
|
||||
|
||||
&.bell
|
||||
filter: blur(2px)
|
||||
|
||||
&.skip
|
||||
filter: sepia(1)
|
||||
|
||||
&.selection
|
||||
filter: saturate(2)
|
||||
|
||||
&.alarm
|
||||
filter: hue-rotate(150deg)
|
||||
|
||||
&.dead
|
||||
filter: grayscale(1)
|
||||
|
||||
&:after
|
||||
content: "CLOSED"
|
||||
font-size: 15em
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
transform: rotate(-45deg)
|
||||
opacity: .2
|
||||
font-weight: 900
|
||||
|
||||
&.stopped
|
||||
filter: brightness(50%)
|
||||
|
||||
&.locked
|
||||
&::-webkit-scrollbar-thumb
|
||||
background: rgba(red, .7)
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover
|
||||
background: rgba(red, .8)
|
||||
38
butterfly/sass/_styles.sass
Normal file
38
butterfly/sass/_styles.sass
Normal file
@@ -0,0 +1,38 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
/* Theses are the various imported style files
|
||||
/* THIS NEEDS the python `libsass` library to be installed.
|
||||
/* You can copy the imported files in the theme dir, they will be imported prioritarily.
|
||||
|
||||
/* You can change this file to import any webfont:
|
||||
@import font
|
||||
|
||||
/* You can comment / uncomment the following to enable/disable terminal effects.
|
||||
@import light_fx
|
||||
/* Comment this one to remove the blurry text:
|
||||
@import text_fx
|
||||
/* @import all_fx
|
||||
|
||||
@import colors
|
||||
/* The color theme is defined in this one:
|
||||
@import 16_colors
|
||||
@import 256_colors
|
||||
|
||||
@import layout
|
||||
@import cursor
|
||||
@import term_styles
|
||||
73
butterfly/sass/_term_styles.sass
Normal file
73
butterfly/sass/_term_styles.sass
Normal file
@@ -0,0 +1,73 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
.bold
|
||||
font-weight: bold
|
||||
|
||||
.underline
|
||||
text-decoration: underline
|
||||
|
||||
.italic
|
||||
font-style: italic
|
||||
|
||||
.faint
|
||||
opacity: .6
|
||||
|
||||
.crossed
|
||||
text-decoration: line-through
|
||||
|
||||
/* Not supported, emulated
|
||||
/* .blink
|
||||
/* text-decoration: blink
|
||||
.blink
|
||||
animation: blink 1s ease-in-out infinite
|
||||
|
||||
.blink-fast
|
||||
animation: blink 250ms ease-in-out infinite
|
||||
|
||||
@keyframes blink
|
||||
0%
|
||||
opacity: 1
|
||||
50%
|
||||
opacity: 0
|
||||
100%
|
||||
opacity: 1
|
||||
|
||||
.invisible
|
||||
visibility: hidden
|
||||
|
||||
.reverse-video
|
||||
color: $bg
|
||||
background-color: $fg
|
||||
|
||||
.blur .cursor
|
||||
border: 1px solid $fg
|
||||
background: none
|
||||
|
||||
.nbsp
|
||||
@extend .underline
|
||||
@extend .fg-color-1
|
||||
|
||||
.inline-html
|
||||
overflow: hidden
|
||||
|
||||
.inline-image
|
||||
max-width: 100%
|
||||
max-height: 50vh
|
||||
|
||||
a
|
||||
color: inherit
|
||||
6
butterfly/sass/_text_fx.sass
Normal file
6
butterfly/sass/_text_fx.sass
Normal file
@@ -0,0 +1,6 @@
|
||||
$fg: #fff !default
|
||||
$shadow: 6px !default
|
||||
$shadow-alpha: .5 !default
|
||||
|
||||
body
|
||||
text-shadow: 0 0 $shadow rgba($fg, $shadow-alpha)
|
||||
54
butterfly/sass/_variables.sass
Normal file
54
butterfly/sass/_variables.sass
Normal file
@@ -0,0 +1,54 @@
|
||||
/* *-* 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. */
|
||||
|
||||
/* Variables */
|
||||
|
||||
/** Font
|
||||
$font-family: "SourceCodePro" !default
|
||||
$font-size: 1em !default
|
||||
$font-line-height: 1.2 !default
|
||||
|
||||
/** Colors */
|
||||
/* Foreground */
|
||||
$fg: #f4ead5 !default
|
||||
/* Background */
|
||||
$bg: #110f13 !default
|
||||
|
||||
$default-bg: transparent !default
|
||||
$active-bg: transparent !default
|
||||
$default-fg: $fg !default
|
||||
|
||||
$reverse-transparent: $bg !default
|
||||
|
||||
/* 16 Colors in this orders: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Bright Black, Bright Red, Bright Green, Bright Yellow, Bright Blue, Bright Magenta, Bright Cyan, Bright White */
|
||||
$colors: #2e3436, #cc0000, #4e9a06, #c4a000, #3465a4, #75507b, #06989a, #d3d7cf, #555753, #ef2929, #8ae234, #fce94f, #729fcf, #ad7fa8, #34e2e2, #eeeeec !default
|
||||
|
||||
/** Text effects */
|
||||
|
||||
/* The shadow is the size of the blur (in px for instance)
|
||||
$shadow: 0 !default
|
||||
/* The shadow alpha is the opacity of the shadow
|
||||
$shadow-alpha: 0 !default
|
||||
|
||||
/** Scroll */
|
||||
$scroll-bg: $bg !default
|
||||
$scroll-fg: rgba($fg, .1) !default
|
||||
$scroll-fg-hover: rgba($fg, .1) !default
|
||||
$scroll-width: .75em !default
|
||||
|
||||
/** Popup */
|
||||
$popup-bg: rgba(127, 127, 127, .5) !default
|
||||
$popup-fg: $fg !default
|
||||
$popup-fs: 1em !default
|
||||
|
||||
26
butterfly/sass/main.sass
Normal file
26
butterfly/sass/main.sass
Normal file
@@ -0,0 +1,26 @@
|
||||
/* *-* 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/>. */
|
||||
|
||||
/* Theses are the various imported style files
|
||||
/* THIS NEEDS the python `libsass` library to be installed.
|
||||
/* You can copy the imported files in the theme dir, they will be imported prioritarily.
|
||||
|
||||
/* These a the default variables */
|
||||
@import variables
|
||||
|
||||
/* These are all imported files */
|
||||
@import styles
|
||||
@@ -1,41 +0,0 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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/>.
|
||||
|
||||
state =
|
||||
x: null
|
||||
y: null
|
||||
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
if e.shiftKey and (37 <= e.keyCode <= 40)
|
||||
if state.y == null
|
||||
state.y = term.ybase + term.y
|
||||
if e.keyCode == 38
|
||||
state.y--
|
||||
if state.y < term.ybase
|
||||
state.y = term.ybase
|
||||
else if e.keyCode == 40
|
||||
state.y++
|
||||
if state.y > term.ybase + term.y
|
||||
state.y = term.ybase + term.y
|
||||
|
||||
term.emit('data', ' \x0b\x15')
|
||||
if state.y != term.ybase + term.y
|
||||
term.emit('data', term.grabText(0, term.cols - 1, state.y, state.y).replace('\n', ''))
|
||||
e.stopPropagation()
|
||||
return false
|
||||
else
|
||||
state.x = state.y = null
|
||||
@@ -1,79 +0,0 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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/>.
|
||||
|
||||
cols = rows = null
|
||||
quit = false
|
||||
|
||||
$ = document.querySelectorAll.bind(document)
|
||||
|
||||
send = (data) ->
|
||||
ws.send 'S' + data
|
||||
|
||||
ctl = (type, args...) ->
|
||||
params = args.join(',')
|
||||
if type == 'Resize'
|
||||
ws.send 'R' + params
|
||||
|
||||
ws_url = 'ws://' + document.location.host + '/ws' + location.pathname
|
||||
ws = new WebSocket ws_url
|
||||
|
||||
ws.addEventListener 'open', ->
|
||||
console.log "WebSocket open", arguments
|
||||
ws.send 'R' + term.cols + ',' + term.rows
|
||||
if location.hash
|
||||
setTimeout ->
|
||||
ws.send 'S' + location.hash.slice(1) + '\n'
|
||||
, 100
|
||||
|
||||
ws.addEventListener 'error', ->
|
||||
console.log "WebSocket error", arguments
|
||||
|
||||
ws.addEventListener 'message', (e) ->
|
||||
setTimeout ->
|
||||
term.write e.data
|
||||
, 1
|
||||
|
||||
ws.addEventListener 'close', ->
|
||||
console.log "WebSocket closed", arguments
|
||||
quit = true
|
||||
open('','_self').close()
|
||||
|
||||
|
||||
term = new Terminal $('#wrapper')[0], send, ctl
|
||||
addEventListener 'beforeunload', ->
|
||||
if not quit
|
||||
'This will exit the terminal session'
|
||||
|
||||
bench = (n=100000000) ->
|
||||
rnd = ''
|
||||
while rnd.length < n
|
||||
rnd += Math.random().toString(36).substring(2)
|
||||
|
||||
t0 = (new Date()).getTime()
|
||||
term.write rnd
|
||||
console.log "#{n} chars in #{(new Date()).getTime() - t0} ms"
|
||||
|
||||
|
||||
cbench = (n=100000000) ->
|
||||
rnd = ''
|
||||
while rnd.length < n
|
||||
rnd += "\x1b[#{30 + parseInt(Math.random() * 20)}m"
|
||||
rnd += Math.random().toString(36).substring(2)
|
||||
|
||||
t0 = (new Date()).getTime()
|
||||
term.write rnd
|
||||
console.log "#{n} chars + colors in #{(new Date()).getTime() - t0} ms"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,79 +0,0 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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
|
||||
virtual_input = document.createElement 'input'
|
||||
virtual_input.type = 'password'
|
||||
virtual_input.style.position = 'fixed'
|
||||
virtual_input.style.top = 0
|
||||
virtual_input.style.left = 0
|
||||
virtual_input.style.border = 'none'
|
||||
virtual_input.style.outline = 'none'
|
||||
virtual_input.style.opacity = 0
|
||||
virtual_input.value = '0'
|
||||
document.body.appendChild virtual_input
|
||||
|
||||
virtual_input.addEventListener 'blur', ->
|
||||
setTimeout((=> @focus()), 10)
|
||||
|
||||
addEventListener 'click', ->
|
||||
virtual_input.focus()
|
||||
|
||||
addEventListener 'touchstart', (e) ->
|
||||
if e.touches.length == 1
|
||||
ctrl = true
|
||||
else if e.touches.length == 2
|
||||
ctrl = false
|
||||
alt = true
|
||||
else if e.touches.length == 3
|
||||
ctrl = true
|
||||
alt = true
|
||||
|
||||
virtual_input.addEventListener 'keydown', (e) ->
|
||||
term.keyDown(e)
|
||||
return true
|
||||
|
||||
virtual_input.addEventListener 'input', (e) ->
|
||||
len = @value.length
|
||||
|
||||
if len == 0
|
||||
e.keyCode = 8
|
||||
term.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
|
||||
term.keyDown e
|
||||
@value = '0'
|
||||
ctrl = alt = false
|
||||
return true
|
||||
|
||||
term.keyPress e
|
||||
first = false
|
||||
@value = '0'
|
||||
true
|
||||
813
butterfly/static/ext.js
Normal file
813
butterfly/static/ext.js
Normal file
@@ -0,0 +1,813 @@
|
||||
(function() {
|
||||
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) {
|
||||
var c, i, out, state;
|
||||
if (data.indexOf('\x1b') < 0) {
|
||||
return data;
|
||||
}
|
||||
i = -1;
|
||||
out = '';
|
||||
state = 'normal';
|
||||
while (i < data.length - 1) {
|
||||
c = data.charAt(++i);
|
||||
switch (state) {
|
||||
case 'normal':
|
||||
if (c === '\x1b') {
|
||||
state = 'escaped';
|
||||
break;
|
||||
}
|
||||
out += c;
|
||||
break;
|
||||
case 'escaped':
|
||||
if (c === '[') {
|
||||
state = 'csi';
|
||||
break;
|
||||
}
|
||||
if (c === ']') {
|
||||
state = 'osc';
|
||||
break;
|
||||
}
|
||||
if ('#()%*+-./'.indexOf(c) >= 0) {
|
||||
i++;
|
||||
}
|
||||
state = 'normal';
|
||||
break;
|
||||
case 'csi':
|
||||
if ("?>!$\" '".indexOf(c) >= 0) {
|
||||
break;
|
||||
}
|
||||
if (('0' <= c && c <= '9')) {
|
||||
break;
|
||||
}
|
||||
if (c === ';') {
|
||||
break;
|
||||
}
|
||||
state = 'normal';
|
||||
break;
|
||||
case 'osc':
|
||||
if (c === "\x1b" || c === "\x07") {
|
||||
if (c === "\x1b") {
|
||||
i++;
|
||||
}
|
||||
state = 'normal';
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
setAlarm = function(notification, cond) {
|
||||
var alarm;
|
||||
alarm = function(data) {
|
||||
var message, note, notif;
|
||||
message = clean_ansi(data.data.slice(1));
|
||||
if (cond !== null && !cond.test(message)) {
|
||||
return;
|
||||
}
|
||||
butterfly.body.classList.remove('alarm');
|
||||
note = "Butterfly [" + butterfly.title + "]";
|
||||
if (notification) {
|
||||
notif = new Notification(note, {
|
||||
body: message,
|
||||
icon: '/static/images/favicon.png'
|
||||
});
|
||||
notif.onclick = function() {
|
||||
window.focus();
|
||||
return notif.close();
|
||||
};
|
||||
} else {
|
||||
alert(note + '\n' + message);
|
||||
}
|
||||
return butterfly.ws.shell.removeEventListener('message', alarm);
|
||||
};
|
||||
butterfly.ws.shell.addEventListener('message', alarm);
|
||||
return butterfly.body.classList.add('alarm');
|
||||
};
|
||||
|
||||
cancel = function(ev) {
|
||||
if (ev.preventDefault) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
if (ev.stopPropagation) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
ev.cancelBubble = true;
|
||||
return false;
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var cond;
|
||||
if (!(e.altKey && e.keyCode === 65)) {
|
||||
return true;
|
||||
}
|
||||
cond = null;
|
||||
if (e.shiftKey) {
|
||||
cond = prompt('Ring alarm when encountering the following text: (can be a regexp)');
|
||||
if (!cond) {
|
||||
return;
|
||||
}
|
||||
cond = new RegExp(cond);
|
||||
}
|
||||
if (Notification && Notification.permission === 'default') {
|
||||
Notification.requestPermission(function() {
|
||||
return setAlarm(Notification.permission === 'granted', cond);
|
||||
});
|
||||
} else {
|
||||
setAlarm(Notification.permission === 'granted', cond);
|
||||
}
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
addEventListener('copy', copy = function(e) {
|
||||
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, len = ref.length; j < len; j++) {
|
||||
line = ref[j];
|
||||
if (line.slice(-1) === '\u23CE') {
|
||||
end = '';
|
||||
line = line.slice(0, -1);
|
||||
} else {
|
||||
end = '\n';
|
||||
}
|
||||
data += line.replace(/\s*$/, '') + end;
|
||||
}
|
||||
e.clipboardData.setData('text/plain', data.slice(0, -1));
|
||||
return e.preventDefault();
|
||||
});
|
||||
|
||||
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');
|
||||
size = 1024;
|
||||
send = function() {
|
||||
butterfly.send(data.substring(0, size));
|
||||
data = data.substring(size);
|
||||
if (data.length) {
|
||||
return setTimeout(send, 25);
|
||||
}
|
||||
};
|
||||
send();
|
||||
return e.preventDefault();
|
||||
});
|
||||
|
||||
addEventListener('beforeunload', function(e) {
|
||||
if (!(butterfly.body.classList.contains('dead') || location.href.indexOf('session') > -1)) {
|
||||
return e.returnValue = 'This terminal is active and not in session. Are you sure you want to kill it?';
|
||||
}
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
||||
walk = function(node, callback) {
|
||||
var child, j, len, ref, results;
|
||||
ref = node.childNodes;
|
||||
results = [];
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
child = ref[j];
|
||||
callback.call(child);
|
||||
results.push(walk(child, callback));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
linkify = function(text) {
|
||||
var emailAddressPattern, pseudoUrlPattern, urlPattern;
|
||||
urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
|
||||
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
|
||||
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim;
|
||||
return text.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>');
|
||||
};
|
||||
|
||||
tags = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
escape = function(s) {
|
||||
return s.replace(/[&<>]/g, function(tag) {
|
||||
return tags[tag] || tag;
|
||||
});
|
||||
};
|
||||
|
||||
Terminal.on('change', function(line) {
|
||||
return walk(line, function() {
|
||||
var linkified, newNode, val;
|
||||
if (this.nodeType === 3) {
|
||||
val = this.nodeValue;
|
||||
linkified = linkify(escape(val));
|
||||
if (linkified !== val) {
|
||||
newNode = document.createElement('span');
|
||||
newNode.innerHTML = linkified;
|
||||
this.parentElement.replaceChild(newNode, this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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.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');
|
||||
this.bound_click_maybe_close = this.click_maybe_close.bind(this);
|
||||
this.bound_key_maybe_close = this.key_maybe_close.bind(this);
|
||||
}
|
||||
|
||||
Popup.prototype.open = function(html) {
|
||||
this.el.innerHTML = html;
|
||||
this.el.classList.remove('hidden');
|
||||
addEventListener('click', this.bound_click_maybe_close);
|
||||
return addEventListener('keydown', this.bound_key_maybe_close);
|
||||
};
|
||||
|
||||
Popup.prototype.close = function() {
|
||||
removeEventListener('click', this.bound_click_maybe_close);
|
||||
removeEventListener('keydown', this.bound_key_maybe_close);
|
||||
this.el.classList.add('hidden');
|
||||
return this.el.innerHTML = '';
|
||||
};
|
||||
|
||||
Popup.prototype.click_maybe_close = function(e) {
|
||||
var t;
|
||||
t = e.target;
|
||||
while (t.parentElement) {
|
||||
if (Array.prototype.slice.call(this.el.children).indexOf(t) > -1) {
|
||||
return true;
|
||||
}
|
||||
t = t.parentElement;
|
||||
}
|
||||
this.close();
|
||||
return cancel(e);
|
||||
};
|
||||
|
||||
Popup.prototype.key_maybe_close = function(e) {
|
||||
if (e.keyCode !== 27) {
|
||||
return true;
|
||||
}
|
||||
this.close();
|
||||
return cancel(e);
|
||||
};
|
||||
|
||||
return Popup;
|
||||
|
||||
})();
|
||||
|
||||
popup = new Popup();
|
||||
|
||||
selection = null;
|
||||
|
||||
cancel = function(ev) {
|
||||
if (ev.preventDefault) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
if (ev.stopPropagation) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
ev.cancelBubble = true;
|
||||
return false;
|
||||
};
|
||||
|
||||
previousLeaf = function(node) {
|
||||
var previous;
|
||||
previous = node.previousSibling;
|
||||
if (!previous) {
|
||||
previous = node.parentNode.previousSibling;
|
||||
}
|
||||
if (!previous) {
|
||||
previous = node.parentNode.parentNode.previousSibling;
|
||||
}
|
||||
while (previous.lastChild) {
|
||||
previous = previous.lastChild;
|
||||
}
|
||||
return previous;
|
||||
};
|
||||
|
||||
nextLeaf = function(node) {
|
||||
var next;
|
||||
next = node.nextSibling;
|
||||
if (!next) {
|
||||
next = node.parentNode.nextSibling;
|
||||
}
|
||||
if (!next) {
|
||||
next = node.parentNode.parentNode.nextSibling;
|
||||
}
|
||||
while (next != null ? next.firstChild : void 0) {
|
||||
next = next.firstChild;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
Selection = (function() {
|
||||
function Selection() {
|
||||
butterfly.body.classList.add('selection');
|
||||
this.selection = getSelection();
|
||||
}
|
||||
|
||||
Selection.prototype.reset = function() {
|
||||
var fakeRange, ref, results;
|
||||
this.selection = getSelection();
|
||||
fakeRange = document.createRange();
|
||||
fakeRange.setStart(this.selection.anchorNode, this.selection.anchorOffset);
|
||||
fakeRange.setEnd(this.selection.focusNode, this.selection.focusOffset);
|
||||
this.start = {
|
||||
node: this.selection.anchorNode,
|
||||
offset: this.selection.anchorOffset
|
||||
};
|
||||
this.end = {
|
||||
node: this.selection.focusNode,
|
||||
offset: this.selection.focusOffset
|
||||
};
|
||||
if (fakeRange.collapsed) {
|
||||
ref = [this.end, this.start], this.start = ref[0], this.end = ref[1];
|
||||
}
|
||||
this.startLine = this.start.node;
|
||||
while (!this.startLine.classList || indexOf.call(this.startLine.classList, 'line') < 0) {
|
||||
this.startLine = this.startLine.parentNode;
|
||||
}
|
||||
this.endLine = this.end.node;
|
||||
results = [];
|
||||
while (!this.endLine.classList || indexOf.call(this.endLine.classList, 'line') < 0) {
|
||||
results.push(this.endLine = this.endLine.parentNode);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
Selection.prototype.clear = function() {
|
||||
return this.selection.removeAllRanges();
|
||||
};
|
||||
|
||||
Selection.prototype.destroy = function() {
|
||||
butterfly.body.classList.remove('selection');
|
||||
return this.clear();
|
||||
};
|
||||
|
||||
Selection.prototype.text = function() {
|
||||
return this.selection.toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ');
|
||||
};
|
||||
|
||||
Selection.prototype.up = function() {
|
||||
return this.go(-1);
|
||||
};
|
||||
|
||||
Selection.prototype.down = function() {
|
||||
return this.go(+1);
|
||||
};
|
||||
|
||||
Selection.prototype.go = function(n) {
|
||||
var index;
|
||||
index = Array.prototype.indexOf.call(butterfly.term.childNodes, this.startLine) + n;
|
||||
if (!((0 <= index && index < butterfly.term.childElementCount))) {
|
||||
return;
|
||||
}
|
||||
while (!butterfly.term.childNodes[index].textContent.match(/\S/)) {
|
||||
index += n;
|
||||
if (!((0 <= index && index < butterfly.term.childElementCount))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return this.selectLine(index);
|
||||
};
|
||||
|
||||
Selection.prototype.apply = function() {
|
||||
var range;
|
||||
this.clear();
|
||||
range = document.createRange();
|
||||
range.setStart(this.start.node, this.start.offset);
|
||||
range.setEnd(this.end.node, this.end.offset);
|
||||
return this.selection.addRange(range);
|
||||
};
|
||||
|
||||
Selection.prototype.selectLine = function(index) {
|
||||
var line, lineEnd, lineStart;
|
||||
line = butterfly.term.childNodes[index];
|
||||
lineStart = {
|
||||
node: line.firstChild,
|
||||
offset: 0
|
||||
};
|
||||
lineEnd = {
|
||||
node: line.lastChild,
|
||||
offset: line.lastChild.textContent.length
|
||||
};
|
||||
this.start = this.walk(lineStart, /\S/);
|
||||
return this.end = this.walk(lineEnd, /\S/, true);
|
||||
};
|
||||
|
||||
Selection.prototype.collapsed = function(start, end) {
|
||||
var fakeRange;
|
||||
fakeRange = document.createRange();
|
||||
fakeRange.setStart(start.node, start.offset);
|
||||
fakeRange.setEnd(end.node, end.offset);
|
||||
return fakeRange.collapsed;
|
||||
};
|
||||
|
||||
Selection.prototype.shrinkRight = function() {
|
||||
var end, node;
|
||||
node = this.walk(this.end, /\s/, true);
|
||||
end = this.walk(node, /\S/, true);
|
||||
if (!this.collapsed(this.start, end)) {
|
||||
return this.end = end;
|
||||
}
|
||||
};
|
||||
|
||||
Selection.prototype.shrinkLeft = function() {
|
||||
var node, start;
|
||||
node = this.walk(this.start, /\s/);
|
||||
start = this.walk(node, /\S/);
|
||||
if (!this.collapsed(start, this.end)) {
|
||||
return this.start = start;
|
||||
}
|
||||
};
|
||||
|
||||
Selection.prototype.expandRight = function() {
|
||||
var node;
|
||||
node = this.walk(this.end, /\S/);
|
||||
return this.end = this.walk(node, /\s/);
|
||||
};
|
||||
|
||||
Selection.prototype.expandLeft = function() {
|
||||
var node;
|
||||
node = this.walk(this.start, /\S/, true);
|
||||
return this.start = this.walk(node, /\s/, true);
|
||||
};
|
||||
|
||||
Selection.prototype.walk = function(needle, til, backward) {
|
||||
var i, node, text;
|
||||
if (backward == null) {
|
||||
backward = false;
|
||||
}
|
||||
if (needle.node.firstChild) {
|
||||
node = needle.node.firstChild;
|
||||
} else {
|
||||
node = needle.node;
|
||||
}
|
||||
text = node != null ? node.textContent : void 0;
|
||||
i = needle.offset;
|
||||
if (backward) {
|
||||
while (node) {
|
||||
while (i > 0) {
|
||||
if (text[--i].match(til)) {
|
||||
return {
|
||||
node: node,
|
||||
offset: i + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
node = previousLeaf(node);
|
||||
text = node != null ? node.textContent : void 0;
|
||||
i = text.length;
|
||||
}
|
||||
} else {
|
||||
while (node) {
|
||||
while (i < text.length) {
|
||||
if (text[i++].match(til)) {
|
||||
return {
|
||||
node: node,
|
||||
offset: i - 1
|
||||
};
|
||||
}
|
||||
}
|
||||
node = nextLeaf(node);
|
||||
text = node != null ? node.textContent : void 0;
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
return needle;
|
||||
};
|
||||
|
||||
return Selection;
|
||||
|
||||
})();
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var r, ref, ref1;
|
||||
if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) {
|
||||
return true;
|
||||
}
|
||||
if (e.shiftKey && e.keyCode === 13 && !selection && !getSelection().isCollapsed) {
|
||||
butterfly.send(getSelection().toString());
|
||||
getSelection().removeAllRanges();
|
||||
return cancel(e);
|
||||
}
|
||||
if (selection) {
|
||||
selection.reset();
|
||||
if (!e.ctrlKey && e.shiftKey && (37 <= (ref1 = e.keyCode) && ref1 <= 40)) {
|
||||
return true;
|
||||
}
|
||||
if (e.shiftKey && e.ctrlKey) {
|
||||
if (e.keyCode === 38) {
|
||||
selection.up();
|
||||
} else if (e.keyCode === 40) {
|
||||
selection.down();
|
||||
}
|
||||
} else if (e.keyCode === 39) {
|
||||
selection.shrinkLeft();
|
||||
} else if (e.keyCode === 38) {
|
||||
selection.expandLeft();
|
||||
} else if (e.keyCode === 37) {
|
||||
selection.shrinkRight();
|
||||
} else if (e.keyCode === 40) {
|
||||
selection.expandRight();
|
||||
} else {
|
||||
return cancel(e);
|
||||
}
|
||||
if (selection != null) {
|
||||
selection.apply();
|
||||
}
|
||||
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(r + butterfly.y - 1);
|
||||
selection.apply();
|
||||
return cancel(e);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', function(e) {
|
||||
var ref, ref1;
|
||||
if (ref = e.keyCode, indexOf.call([16, 17, 18, 19], ref) >= 0) {
|
||||
return true;
|
||||
}
|
||||
if (selection) {
|
||||
if (e.keyCode === 13) {
|
||||
butterfly.send(selection.text());
|
||||
selection.destroy();
|
||||
selection = null;
|
||||
return cancel(e);
|
||||
}
|
||||
if (ref1 = e.keyCode, indexOf.call([37, 38, 39, 40], ref1) < 0) {
|
||||
selection.destroy();
|
||||
selection = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
document.addEventListener('dblclick', function(e) {
|
||||
var anchorNode, anchorOffset, newRange, range, sel;
|
||||
if (e.ctrlKey || e.altkey) {
|
||||
return;
|
||||
}
|
||||
sel = getSelection();
|
||||
if (sel.isCollapsed || sel.toString().match(/\s/)) {
|
||||
return;
|
||||
}
|
||||
range = document.createRange();
|
||||
range.setStart(sel.anchorNode, sel.anchorOffset);
|
||||
range.setEnd(sel.focusNode, sel.focusOffset);
|
||||
if (range.collapsed) {
|
||||
sel.removeAllRanges();
|
||||
newRange = document.createRange();
|
||||
newRange.setStart(sel.focusNode, sel.focusOffset);
|
||||
newRange.setEnd(sel.anchorNode, sel.anchorOffset);
|
||||
sel.addRange(newRange);
|
||||
}
|
||||
while (!(sel.toString().match(/\s/) || !sel.toString())) {
|
||||
sel.modify('extend', 'forward', 'character');
|
||||
}
|
||||
sel.modify('extend', 'backward', 'character');
|
||||
anchorNode = sel.anchorNode;
|
||||
anchorOffset = sel.anchorOffset;
|
||||
sel.collapseToEnd();
|
||||
sel.extend(anchorNode, anchorOffset);
|
||||
while (!(sel.toString().match(/\s/) || !sel.toString())) {
|
||||
sel.modify('extend', 'backward', 'character');
|
||||
}
|
||||
return sel.modify('extend', 'forward', 'character');
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var oReq;
|
||||
if (!(e.altKey && e.keyCode === 69)) {
|
||||
return true;
|
||||
}
|
||||
oReq = new XMLHttpRequest();
|
||||
oReq.addEventListener('load', function() {
|
||||
var j, len, out, ref, response, session;
|
||||
response = JSON.parse(this.responseText);
|
||||
out = '<div>';
|
||||
out += '<h2>Session list</h2>';
|
||||
if (response.sessions.length === 0) {
|
||||
out += "No current session for user " + response.user;
|
||||
} else {
|
||||
out += '<ul>';
|
||||
ref = response.sessions;
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
session = ref[j];
|
||||
out += "<li><a href=\"/session/" + session + "\">" + session + "</a></li>";
|
||||
}
|
||||
out += '</ul>';
|
||||
}
|
||||
out += '</div>';
|
||||
return popup.open(out);
|
||||
});
|
||||
oReq.open("GET", "/sessions/list.json");
|
||||
oReq.send();
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
_set_theme_href = function(href) {
|
||||
var img;
|
||||
document.getElementById('style').setAttribute('href', href);
|
||||
img = document.createElement('img');
|
||||
img.onerror = function() {
|
||||
return setTimeout((function() {
|
||||
return typeof butterfly !== "undefined" && butterfly !== null ? butterfly.resize() : void 0;
|
||||
}), 250);
|
||||
};
|
||||
return img.src = href;
|
||||
};
|
||||
|
||||
_theme = typeof localStorage !== "undefined" && localStorage !== null ? localStorage.getItem('theme') : void 0;
|
||||
|
||||
if (_theme) {
|
||||
_set_theme_href(_theme);
|
||||
}
|
||||
|
||||
this.set_theme = function(theme) {
|
||||
_theme = theme;
|
||||
if (typeof localStorage !== "undefined" && localStorage !== null) {
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
if (theme) {
|
||||
return _set_theme_href(theme);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var oReq, style;
|
||||
if (!(e.altKey && e.keyCode === 83)) {
|
||||
return true;
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
style = document.getElementById('style').getAttribute('href');
|
||||
style = style.split('?')[0];
|
||||
_set_theme_href(style + '?' + (new Date().getTime()));
|
||||
return cancel(e);
|
||||
}
|
||||
oReq = new XMLHttpRequest();
|
||||
oReq.addEventListener('load', function() {
|
||||
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;
|
||||
inner = "<form>\n <h2>Pick a theme:</h2>\n <select id=\"theme_list\">";
|
||||
option = function(url, theme) {
|
||||
inner += '<option ';
|
||||
if (_theme === url) {
|
||||
inner += 'selected ';
|
||||
}
|
||||
inner += "value=\"" + url + "\">";
|
||||
inner += theme;
|
||||
return inner += '</option>';
|
||||
};
|
||||
option("/static/main.css", 'default');
|
||||
if (themes.length) {
|
||||
inner += '<optgroup label="Local themes">';
|
||||
for (j = 0, len = themes.length; j < len; j++) {
|
||||
theme = themes[j];
|
||||
url = "/theme/" + theme + "/style.css";
|
||||
option(url, theme);
|
||||
}
|
||||
inner += '</optgroup>';
|
||||
}
|
||||
inner += '<optgroup label="Built-in themes">';
|
||||
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));
|
||||
}
|
||||
inner += '</optgroup>';
|
||||
inner += " </select>\n <label>You can create yours in " + response.dir + ".</label>\n</form>";
|
||||
popup.open(inner);
|
||||
theme_list = document.getElementById('theme_list');
|
||||
return theme_list.addEventListener('change', function() {
|
||||
return set_theme(theme_list.value);
|
||||
});
|
||||
});
|
||||
oReq.open("GET", "/themes/list.json");
|
||||
oReq.send();
|
||||
return cancel(e);
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
|
||||
//# sourceMappingURL=ext.js.map
|
||||
4
butterfly/static/ext.min.js
vendored
Normal file
4
butterfly/static/ext.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
butterfly/static/html-sanitizer.js
Normal file
1
butterfly/static/html-sanitizer.js
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 699 B After Width: | Height: | Size: 1.9 KiB |
File diff suppressed because it is too large
Load Diff
2964
butterfly/static/main.css
Normal file
2964
butterfly/static/main.css
Normal file
File diff suppressed because it is too large
Load Diff
3019
butterfly/static/main.js
Normal file
3019
butterfly/static/main.js
Normal file
File diff suppressed because it is too large
Load Diff
5
butterfly/static/main.min.js
vendored
Normal file
5
butterfly/static/main.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,147 +0,0 @@
|
||||
/* *-* coding: utf-8 *-*
|
||||
/* This file is part of butterfly
|
||||
/*
|
||||
/* butterfly Copyright (C) 2014 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/>.
|
||||
|
||||
@import compass/css3
|
||||
|
||||
+font-face("SourceCodePro", font-files("SourceCodePro-ExtraLight.otf"), "", 100, normal)
|
||||
+font-face("SourceCodePro", font-files("SourceCodePro-Light.otf"), "", 300, normal)
|
||||
+font-face("SourceCodePro", font-files("SourceCodePro-Regular.otf"), "", 400, normal)
|
||||
+font-face("SourceCodePro", font-files("SourceCodePro-Medium.otf"), "", 500, normal)
|
||||
+font-face("SourceCodePro", font-files("SourceCodePro-Semibold.otf"), "", 600, normal)
|
||||
+font-face("SourceCodePro", font-files("SourceCodePro-Bold.otf"), "", 700, normal)
|
||||
+font-face("SourceCodePro", font-files("SourceCodePro-Black.otf"), "", 900, normal)
|
||||
|
||||
$bg: #110f13
|
||||
$fg: #f4ead5
|
||||
|
||||
$shadow: 6px
|
||||
$shadow-alpha: .5
|
||||
|
||||
|
||||
html, body
|
||||
height: 100%
|
||||
font-family: "SourceCodePro"
|
||||
margin: 0
|
||||
padding: 0
|
||||
line-height: 1.2
|
||||
|
||||
|
||||
#wrapper
|
||||
height: 100%
|
||||
background-color: $bg
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
|
||||
.terminal
|
||||
outline: none
|
||||
background-color: $bg
|
||||
color: $fg
|
||||
text-shadow: 0 0 $shadow rgba($fg, $shadow-alpha)
|
||||
+transition(200ms)
|
||||
|
||||
&.bell
|
||||
+filter(blur(2px))
|
||||
|
||||
&.skip
|
||||
+filter(sepia(1))
|
||||
|
||||
.line
|
||||
overflow: visible
|
||||
|
||||
.inline-html
|
||||
white-space: normal
|
||||
|
||||
|
||||
.focus .cursor
|
||||
+transition(300ms)
|
||||
|
||||
::selection
|
||||
background-color: black
|
||||
|
||||
::-moz-selection
|
||||
background-color: black
|
||||
|
||||
.cursor.reverse-video
|
||||
box-shadow: 0 0 10px $fg
|
||||
|
||||
/* Terminal styles
|
||||
.bold
|
||||
font-weight: bold
|
||||
|
||||
.underline
|
||||
text-decoration: underline
|
||||
|
||||
.blink
|
||||
text-decoration: blink
|
||||
|
||||
.invisible
|
||||
visibility: hidden
|
||||
|
||||
.reverse-video
|
||||
color: $bg
|
||||
background-color: $fg
|
||||
|
||||
.blur .cursor.reverse-video
|
||||
background: none
|
||||
|
||||
=termcolor($i, $color)
|
||||
.bg-color-#{$i}
|
||||
background-color: $color
|
||||
&.reverse-video
|
||||
color: $color !important
|
||||
|
||||
.fg-color-#{$i}
|
||||
color: $color
|
||||
&.reverse-video
|
||||
background-color: $color !important
|
||||
|
||||
text-shadow: 0 0 $shadow rgba($color, $shadow-alpha)
|
||||
|
||||
|
||||
+termcolor(0, #2e3436)
|
||||
+termcolor(1, #cc0000)
|
||||
+termcolor(2, #4e9a06)
|
||||
+termcolor(3, #c4a000)
|
||||
+termcolor(4, #3465a4)
|
||||
+termcolor(5, #75507b)
|
||||
+termcolor(6, #06989a)
|
||||
+termcolor(7, #d3d7cf)
|
||||
+termcolor(8, #555753)
|
||||
+termcolor(9, #ef2929)
|
||||
+termcolor(10, #8ae234)
|
||||
+termcolor(11, #fce94f)
|
||||
+termcolor(12, #729fcf)
|
||||
+termcolor(13, #ad7fa8)
|
||||
+termcolor(14, #34e2e2)
|
||||
+termcolor(15, #eeeeec)
|
||||
|
||||
$st: 00, 95, 135, 175, 215, 255
|
||||
|
||||
@for $i from 0 through 215
|
||||
$r: nth($st, 1 + floor(($i / 36) % 6))
|
||||
$g: nth($st, 1 + floor(($i / 6) % 6))
|
||||
$b: nth($st, 1 + $i % 6)
|
||||
+termcolor($i + 16, rgb($r, $g, $b))
|
||||
|
||||
@for $i from 0 through 23
|
||||
$l: 8 + $i * 10
|
||||
+termcolor($i + 232, rgb($l, $l, $l))
|
||||
|
||||
|
||||
/* ??
|
||||
+termcolor(256, $bg)
|
||||
+termcolor(257, $fg)
|
||||
Binary file not shown.
@@ -1,229 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata></metadata>
|
||||
<defs>
|
||||
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
|
||||
<font-face units-per-em="1200" ascent="960" descent="-240" />
|
||||
<missing-glyph horiz-adv-x="500" />
|
||||
<glyph />
|
||||
<glyph />
|
||||
<glyph unicode="
" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
|
||||
<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode=" " horiz-adv-x="652" />
|
||||
<glyph unicode=" " horiz-adv-x="1304" />
|
||||
<glyph unicode=" " horiz-adv-x="652" />
|
||||
<glyph unicode=" " horiz-adv-x="1304" />
|
||||
<glyph unicode=" " horiz-adv-x="434" />
|
||||
<glyph unicode=" " horiz-adv-x="326" />
|
||||
<glyph unicode=" " horiz-adv-x="217" />
|
||||
<glyph unicode=" " horiz-adv-x="217" />
|
||||
<glyph unicode=" " horiz-adv-x="163" />
|
||||
<glyph unicode=" " horiz-adv-x="260" />
|
||||
<glyph unicode=" " horiz-adv-x="72" />
|
||||
<glyph unicode=" " horiz-adv-x="260" />
|
||||
<glyph unicode=" " horiz-adv-x="326" />
|
||||
<glyph unicode="€" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
|
||||
<glyph unicode="−" d="M200 400h900v300h-900v-300z" />
|
||||
<glyph unicode="☁" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
|
||||
<glyph unicode="✉" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
|
||||
<glyph unicode="✏" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
|
||||
<glyph unicode="" horiz-adv-x="500" d="M0 0z" />
|
||||
<glyph unicode="" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
|
||||
<glyph unicode="" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q17 -55 85.5 -75.5t147.5 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
|
||||
<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
|
||||
<glyph unicode="" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
|
||||
<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
|
||||
<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
|
||||
<glyph unicode="" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
|
||||
<glyph unicode="" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
|
||||
<glyph unicode="" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
|
||||
<glyph unicode="" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
|
||||
<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
|
||||
<glyph unicode="" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 299q-120 -77 -261 -77q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
|
||||
<glyph unicode="" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
|
||||
<glyph unicode="" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
|
||||
<glyph unicode="" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
|
||||
<glyph unicode="" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
|
||||
<glyph unicode="" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
|
||||
<glyph unicode="" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
|
||||
<glyph unicode="" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
|
||||
<glyph unicode="" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
|
||||
<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
|
||||
<glyph unicode="" d="M0 25v475l200 700h800q199 -700 200 -700v-475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
|
||||
<glyph unicode="" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
|
||||
<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
|
||||
<glyph unicode="" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
|
||||
<glyph unicode="" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
|
||||
<glyph unicode="" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
|
||||
<glyph unicode="" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
|
||||
<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
|
||||
<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
|
||||
<glyph unicode="" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
|
||||
<glyph unicode="" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
|
||||
<glyph unicode="" d="M1 700v475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
|
||||
<glyph unicode="" d="M2 700v475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
|
||||
<glyph unicode="" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
|
||||
<glyph unicode="" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
|
||||
<glyph unicode="" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
|
||||
<glyph unicode="" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
|
||||
<glyph unicode="" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v70h471q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
|
||||
<glyph unicode="" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
|
||||
<glyph unicode="" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
|
||||
<glyph unicode="" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
|
||||
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
|
||||
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
|
||||
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
|
||||
<glyph unicode="" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
|
||||
<glyph unicode="" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
|
||||
<glyph unicode="" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
|
||||
<glyph unicode="" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
|
||||
<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
|
||||
<glyph unicode="" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 138.5t-64 210.5zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l566 567l-136 137l-430 -431l-147 147z" />
|
||||
<glyph unicode="" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
|
||||
<glyph unicode="" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
|
||||
<glyph unicode="" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
|
||||
<glyph unicode="" d="M200 0l900 550l-900 550v-1100z" />
|
||||
<glyph unicode="" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
|
||||
<glyph unicode="" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
|
||||
<glyph unicode="" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
|
||||
<glyph unicode="" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
|
||||
<glyph unicode="" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
|
||||
<glyph unicode="" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
|
||||
<glyph unicode="" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
|
||||
<glyph unicode="" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM300 500h600v200h-600v-200z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM363 700h144q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5q19 0 30 -10t11 -26 q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-105 0 -172 -56t-67 -183zM500 300h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -300t-217.5 -218t-299.5 -80t-299.5 80t-217.5 218t-80 300zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M0 500v200h194q15 60 36 104.5t55.5 86t88 69t126.5 40.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200 v-206q149 48 201 206h-201v200h200q-25 74 -76 127.5t-124 76.5v-204h-200v203q-75 -24 -130 -77.5t-79 -125.5h209v-200h-210z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
|
||||
<glyph unicode="" d="M0 547l600 453v-300h600v-300h-600v-301z" />
|
||||
<glyph unicode="" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
|
||||
<glyph unicode="" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
|
||||
<glyph unicode="" d="M104 600h296v600h300v-600h298l-449 -600z" />
|
||||
<glyph unicode="" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
|
||||
<glyph unicode="" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
|
||||
<glyph unicode="" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
|
||||
<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-33 14.5h-207q-20 0 -32 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111v6t-1 15t-3 18l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6h-111v-100z M100 0h400v400h-400v-400zM200 900q-3 0 14 48t35 96l18 47l214 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
|
||||
<glyph unicode="" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
|
||||
<glyph unicode="" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
|
||||
<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
|
||||
<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
|
||||
<glyph unicode="" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 33 -48 36t-48 -29l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
|
||||
<glyph unicode="" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -21 -13 -29t-32 1l-94 78h-222l-94 -78q-19 -9 -32 -1t-13 29v64 q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
|
||||
<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
|
||||
<glyph unicode="" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
|
||||
<glyph unicode="" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
|
||||
<glyph unicode="" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
|
||||
<glyph unicode="" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
|
||||
<glyph unicode="" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
|
||||
<glyph unicode="" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
|
||||
<glyph unicode="" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
|
||||
<glyph unicode="" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
|
||||
<glyph unicode="" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
|
||||
<glyph unicode="" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
|
||||
<glyph unicode="" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
|
||||
<glyph unicode="" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM99 500v250v5q0 13 0.5 18.5t2.5 13t8 10.5t15 3h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35q-56 337 -56 351z M1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
|
||||
<glyph unicode="" d="M74 350q0 21 13.5 35.5t33.5 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-22 -9 -63 -23t-167.5 -37 t-251.5 -23t-245.5 20.5t-178.5 41.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
|
||||
<glyph unicode="" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
|
||||
<glyph unicode="" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q123 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 212l100 213h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
|
||||
<glyph unicode="" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q123 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
|
||||
<glyph unicode="" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
|
||||
<glyph unicode="" d="M-101 651q0 72 54 110t139 37h302l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 16.5 -10.5t26 -26t16.5 -36.5v-526q0 -13 -85.5 -93.5t-93.5 -80.5h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l106 89v502l-342 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM999 201v600h200v-600h-200z" />
|
||||
<glyph unicode="" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6v7.5v7v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
|
||||
<glyph unicode="" d="M1 585q-15 -31 7 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85l-1 -302q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM76 565l237 339h503l89 -100v-294l-340 -130 q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 500h300l-2 -194l402 294l-402 298v-197h-298v-201z" />
|
||||
<glyph unicode="" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l400 -294v194h302v201h-300v197z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
|
||||
<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -34 5.5 -93t7.5 -87q0 -9 17 -44t16 -60q12 0 23 -5.5 t23 -15t20 -13.5q20 -10 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55.5t-20 -57.5q12 -21 22.5 -34.5t28 -27t36.5 -17.5q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q101 -2 221 111q31 30 47 48t34 49t21 62q-14 9 -37.5 9.5t-35.5 7.5q-14 7 -49 15t-52 19 q-9 0 -39.5 -0.5t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q8 16 22 22q6 -1 26 -1.5t33.5 -4.5t19.5 -13q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5 t5.5 57.5q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 41 1 44q31 -13 58.5 -14.5t39.5 3.5l11 4q6 36 -17 53.5t-64 28.5t-56 23 q-19 -3 -37 0q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -46 0t-45 -3q-20 -6 -51.5 -25.5t-34.5 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -91t-29.5 -79zM518 915q3 12 16 30.5t16 25.5q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -18 8 -42.5t16.5 -44 t9.5 -23.5q-6 1 -39 5t-53.5 10t-36.5 16z" />
|
||||
<glyph unicode="" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
|
||||
<glyph unicode="" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
|
||||
<glyph unicode="" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
|
||||
<glyph unicode="" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM513 609q0 32 21 56.5t52 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-16 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5q-37 0 -62.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
|
||||
<glyph unicode="" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -79.5 -17t-67.5 -51l-388 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23q38 0 53 -36 q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60l517 511 q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
|
||||
<glyph unicode="" d="M79 784q0 131 99 229.5t230 98.5q144 0 242 -129q103 129 245 129q130 0 227 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100l-84.5 84.5t-68 74t-60 78t-33.5 70.5t-15 78z M250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-106 48.5q-73 0 -131 -83l-118 -171l-114 174q-51 80 -124 80q-59 0 -108.5 -49.5t-49.5 -118.5z" />
|
||||
<glyph unicode="" d="M57 353q0 -94 66 -160l141 -141q66 -66 159 -66q95 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-12 12 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141l19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -18q46 -46 77 -99l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
|
||||
<glyph unicode="" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
|
||||
<glyph unicode="" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
|
||||
<glyph unicode="" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335l-27 7q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5v-307l64 -14 q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5zM700 237 q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
|
||||
<glyph unicode="" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -11 2.5 -24.5t5.5 -24t9.5 -26.5t10.5 -25t14 -27.5t14 -25.5 t15.5 -27t13.5 -24h242v-100h-197q8 -50 -2.5 -115t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q32 1 102 -16t104 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10 t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5t-30 142.5h-221z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
|
||||
<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
|
||||
<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
|
||||
<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
|
||||
<glyph unicode="" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
|
||||
<glyph unicode="" d="M216 519q10 -19 32 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8l9 -1q13 0 26 16l538 630q15 19 6 36q-8 18 -32 16h-300q1 4 78 219.5t79 227.5q2 17 -6 27l-8 8h-9q-16 0 -25 -15q-4 -5 -98.5 -111.5t-228 -257t-209.5 -238.5q-17 -19 -7 -40z" />
|
||||
<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
|
||||
<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 401h700v699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l248 -237v700h-699zM900 150h100v50h-100v-50z" />
|
||||
<glyph unicode="" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
|
||||
<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
|
||||
<glyph unicode="" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
|
||||
<glyph unicode="" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
|
||||
<glyph unicode="" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -117q-25 -16 -43.5 -50.5t-18.5 -65.5v-359z" />
|
||||
<glyph unicode="" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
|
||||
<glyph unicode="" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
|
||||
<glyph unicode="" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q16 17 13 40.5t-22 37.5l-192 136q-19 14 -45 12t-42 -19l-119 -118q-143 103 -267 227q-126 126 -227 268l118 118q17 17 20 41.5 t-11 44.5l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
|
||||
<glyph unicode="" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -15 -35.5t-35 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
|
||||
<glyph unicode="" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
|
||||
<glyph unicode="" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
|
||||
<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
|
||||
<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
|
||||
<glyph unicode="" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
|
||||
<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300 h200l-300 -300z" />
|
||||
<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104t60.5 178q0 121 -85 207.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
|
||||
<glyph unicode="" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
|
||||
<glyph unicode="" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -12t1 -11q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
|
||||
</font>
|
||||
</defs></svg>
|
||||
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
{% from tornado.options import options %}
|
||||
{% from uuid import uuid4 %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -9,11 +11,26 @@
|
||||
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
|
||||
|
||||
<title>Butterfly</title>
|
||||
<link href="{{ static_url('stylesheets/main.css') }}" rel="stylesheet">
|
||||
<link href="{{ static_url('main.css') }}" rel="stylesheet" id="style">
|
||||
</head>
|
||||
|
||||
<body spellcheck="false">
|
||||
<main id="wrapper"> </main>
|
||||
<script src="{{ static_url('javascripts/main.js') }}"></script>
|
||||
<body spellcheck="false"
|
||||
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}"
|
||||
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>
|
||||
<script src="{{ static_url('main.%sjs' % (
|
||||
'' if options.unminified else 'min.')) }}"></script>
|
||||
<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>
|
||||
|
||||
26
butterfly/templates/motd
Normal file
26
butterfly/templates/motd
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
{{ colors.blue }} ` '
|
||||
;,,, ` ' ,,,;
|
||||
`Y888888bo. : : .od888888Y'
|
||||
8888888888b. : : .d8888888888
|
||||
88888Y' `Y8b. ` ' .d8Y' `Y88888
|
||||
j88888 {{ colors.white }}.db.{{ colors.blue }} Yb. ' ' .dY {{ colors.white }}.db.{{ colors.blue }} 88888k
|
||||
`888 {{ colors.white }}Y88Y{{ colors.blue }} `b ( ) d' {{ colors.white }}Y88Y{{ colors.blue }} 888'
|
||||
888b {{ colors.white }}'"{{ colors.blue }} ,', {{ colors.white }}"'{{ colors.blue }} d888
|
||||
j888888bd8gf"' ':' `"?g8bd888888k
|
||||
{{ colors.white }}'Y'{{ colors.blue }} .8' d' 'b '8. {{ colors.white }}'Y'{{ colors.reset }}
|
||||
{{ colors.white }}!{{ colors.blue }} .8' {{ colors.white }}db{{ colors.blue }} d'; ;`b {{ colors.white }}db{{ colors.blue }} '8. {{ colors.white }}!{{ colors.blue }}
|
||||
d88 {{ colors.white }}`'{{ colors.blue }} 8 ; ; 8 {{ colors.white }}`'{{ colors.blue }} 88b {{ colors.white }}butterfly {{ colors.yellow }}v {{ version }}{{ colors.blue }}
|
||||
d888b .g8 ',' 8g. d888b
|
||||
:888888888Y' 'Y888888888: {{ colors.light_white }}Connecting to:{{ colors.blue }}
|
||||
'! 8888888' `8888888 !' {{ colors.red if opts.unsecure else colors.green }}{{ butterfly.socket.local_addr }}:{{ butterfly.socket.local_port }}{{ colors.blue }}
|
||||
'8Y {{ colors.white }}`Y Y'{{ colors.blue }} Y8'
|
||||
{{ colors.white }} Y Y {{ colors.light_white }}From:{{ colors.white }}
|
||||
! ! {{ 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 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 }}
|
||||
{% end %}
|
||||
360
butterfly/terminal.py
Normal file
360
butterfly/terminal.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# *-* 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/>.
|
||||
|
||||
import fcntl
|
||||
import io
|
||||
import os
|
||||
import pty
|
||||
import random
|
||||
import signal
|
||||
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 butterfly import __version__, utils
|
||||
|
||||
log = getLogger('butterfly')
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
server = utils.User()
|
||||
daemon = utils.User(name='daemon')
|
||||
|
||||
|
||||
# Python 2 backward compatibility
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
class Terminal(object):
|
||||
sessions = {}
|
||||
|
||||
def __init__(self, user, path, session, socket, uri, render_string,
|
||||
broadcast):
|
||||
self.sessions[session] = self
|
||||
self.history_size = 50000
|
||||
self.history = ''
|
||||
self.uri = uri
|
||||
self.session = session
|
||||
self.broadcast = broadcast
|
||||
self.fd = None
|
||||
self.closed = False
|
||||
self.socket = socket
|
||||
log.info('Terminal opening with session: %s and socket %r' % (
|
||||
self.session, self.socket))
|
||||
self.path = path
|
||||
self.user = user if user else None
|
||||
self.caller = self.callee = None
|
||||
|
||||
# If local we have the user connecting
|
||||
if self.socket.local and self.socket.user is not None:
|
||||
self.caller = self.socket.user
|
||||
|
||||
if tornado.options.options.unsecure:
|
||||
if self.user:
|
||||
try:
|
||||
self.callee = self.user
|
||||
except LookupError:
|
||||
log.debug(
|
||||
"Can't switch to user %s" % self.user, exc_info=True)
|
||||
self.callee = None
|
||||
|
||||
# If no user where given and we are local, keep the same
|
||||
# user as the one who opened the socket ie: the one
|
||||
# openning a terminal in browser
|
||||
if not self.callee and not self.user and self.socket.local:
|
||||
self.user = self.callee = self.caller
|
||||
else:
|
||||
# Authed user
|
||||
self.callee = self.user
|
||||
|
||||
if tornado.options.options.motd != '':
|
||||
motd = (render_string(
|
||||
tornado.options.options.motd,
|
||||
butterfly=self,
|
||||
version=__version__,
|
||||
opts=tornado.options.options,
|
||||
uri=self.uri,
|
||||
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)
|
||||
|
||||
def send(self, message):
|
||||
if message is not None:
|
||||
self.history += message
|
||||
if len(self.history) > self.history_size:
|
||||
self.history = self.history[-self.history_size:]
|
||||
self.broadcast(self.session, message)
|
||||
|
||||
def pty(self):
|
||||
# Make a "unique" id in 4 bytes
|
||||
self.uid = ''.join(
|
||||
random.choice(
|
||||
string.ascii_lowercase + string.ascii_uppercase +
|
||||
string.digits)
|
||||
for _ in range(4))
|
||||
|
||||
self.pid, self.fd = pty.fork()
|
||||
if self.pid == 0:
|
||||
self.determine_user()
|
||||
log.debug('Pty forked for user %r caller %r callee %r' % (
|
||||
self.user, self.caller, self.callee))
|
||||
self.shell()
|
||||
else:
|
||||
self.communicate()
|
||||
|
||||
def determine_user(self):
|
||||
if not tornado.options.options.unsecure:
|
||||
# Secure mode we must have already a callee
|
||||
assert self.callee is not None
|
||||
return
|
||||
|
||||
# If we should login, login
|
||||
if tornado.options.options.login:
|
||||
user = ''
|
||||
while user == '':
|
||||
try:
|
||||
user = input('login: ')
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
log.debug("Error in login input", exc_info=True)
|
||||
pass
|
||||
|
||||
try:
|
||||
self.callee = utils.User(name=user)
|
||||
except Exception:
|
||||
log.debug("Can't switch to user %s" % user, exc_info=True)
|
||||
self.callee = utils.User(name='nobody')
|
||||
return
|
||||
|
||||
# if login is not required, we will use the same user as
|
||||
# butterfly is executed
|
||||
self.callee = self.callee or utils.User()
|
||||
|
||||
def shell(self):
|
||||
try:
|
||||
os.chdir(self.path or self.callee.dir)
|
||||
except Exception:
|
||||
log.debug(
|
||||
"Can't chdir to %s" % (self.path or self.callee.dir),
|
||||
exc_info=True)
|
||||
|
||||
# If local and local user is the same as login user
|
||||
# We set the env of the user from the browser
|
||||
# Usefull when running as root
|
||||
if self.caller == self.callee:
|
||||
env = os.environ
|
||||
env.update(self.socket.env)
|
||||
else:
|
||||
# May need more?
|
||||
env = {}
|
||||
env["TERM"] = "xterm-256color"
|
||||
env["COLORTERM"] = "butterfly"
|
||||
env["HOME"] = self.callee.dir
|
||||
env["LOCATION"] = self.uri
|
||||
env['BUTTERFLY_PATH'] = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), 'bin'))
|
||||
|
||||
try:
|
||||
tty = os.ttyname(0).replace('/dev/', '')
|
||||
except Exception:
|
||||
log.debug("Can't get ttyname", exc_info=True)
|
||||
tty = ''
|
||||
if self.caller != self.callee:
|
||||
try:
|
||||
os.chown(os.ttyname(0), self.callee.uid, -1)
|
||||
except Exception:
|
||||
log.debug("Can't chown ttyname", exc_info=True)
|
||||
|
||||
utils.add_user_info(
|
||||
self.uid,
|
||||
tty, os.getpid(),
|
||||
self.callee.name, self.uri)
|
||||
|
||||
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 secure and not local_login:
|
||||
# User is authed by ssl, setting groups
|
||||
try:
|
||||
os.initgroups(self.callee.name, self.callee.gid)
|
||||
os.setgid(self.callee.gid)
|
||||
os.setuid(self.callee.uid)
|
||||
# Apparently necessary for some cmd
|
||||
env['LOGNAME'] = env['USER'] = self.callee.name
|
||||
except Exception:
|
||||
log.error(
|
||||
'The server must be run as root '
|
||||
'if you want to log as different user\n',
|
||||
exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
if tornado.options.options.cmd:
|
||||
args = tornado.options.options.cmd.split(' ')
|
||||
else:
|
||||
args = [tornado.options.options.shell or self.callee.shell]
|
||||
args.append('-il')
|
||||
|
||||
# In some cases some shells don't export SHELL var
|
||||
env['SHELL'] = args[0]
|
||||
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:
|
||||
if self.callee != self.caller:
|
||||
# Force password prompt by dropping rights
|
||||
# to the daemon user
|
||||
os.setuid(daemon.uid)
|
||||
else:
|
||||
# We are not local so we should always get a password prompt
|
||||
if self.callee == daemon:
|
||||
# No logging from daemon
|
||||
sys.exit(1)
|
||||
os.setuid(daemon.uid)
|
||||
|
||||
if os.path.exists('/usr/bin/su'):
|
||||
args = ['/usr/bin/su']
|
||||
else:
|
||||
args = ['/bin/su']
|
||||
|
||||
args.append('-l')
|
||||
if sys.platform.startswith('linux') and tornado.options.options.shell:
|
||||
args.append('-s')
|
||||
args.append(tornado.options.options.shell)
|
||||
args.append(self.callee.name)
|
||||
os.execvpe(args[0], args, env)
|
||||
|
||||
def communicate(self):
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
|
||||
def utf8_error(e):
|
||||
log.error(e)
|
||||
|
||||
self.reader = io.open(
|
||||
self.fd,
|
||||
'rb',
|
||||
buffering=0,
|
||||
closefd=False
|
||||
)
|
||||
self.writer = io.open(
|
||||
self.fd,
|
||||
'wt',
|
||||
encoding='utf-8',
|
||||
closefd=False
|
||||
)
|
||||
ioloop.add_handler(
|
||||
self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR)
|
||||
|
||||
def write(self, message):
|
||||
if not hasattr(self, 'writer'):
|
||||
self.on_close()
|
||||
self.close()
|
||||
|
||||
log.debug('WRIT<%r' % message)
|
||||
self.writer.write(message)
|
||||
self.writer.flush()
|
||||
|
||||
def ctl(self, message):
|
||||
if message['cmd'] == 'size':
|
||||
cols = message['cols']
|
||||
rows = message['rows']
|
||||
s = struct.pack("HHHH", rows, cols, 0, 0)
|
||||
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, s)
|
||||
log.info('SIZE (%d, %d)' % (cols, rows))
|
||||
|
||||
def shell_handler(self, fd, events):
|
||||
if events & ioloop.READ:
|
||||
try:
|
||||
read = self.reader.read()
|
||||
except IOError:
|
||||
read = ''
|
||||
|
||||
log.debug('READ>%r' % read)
|
||||
if read and len(read) != 0:
|
||||
self.send(read.decode('utf-8', 'replace'))
|
||||
else:
|
||||
events = ioloop.ERROR
|
||||
|
||||
if events & ioloop.ERROR:
|
||||
log.info('Error on fd %d, closing' % fd)
|
||||
# Terminated
|
||||
self.send(None) # Close all
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
if self.fd is not None:
|
||||
log.info('Closing fd %d' % self.fd)
|
||||
|
||||
if getattr(self, 'pid', 0) == 0:
|
||||
log.info('pid is 0')
|
||||
return
|
||||
|
||||
utils.rm_user_info(self.uid, self.pid)
|
||||
|
||||
try:
|
||||
ioloop.remove_handler(self.fd)
|
||||
except Exception:
|
||||
log.error('handler removal fail', exc_info=True)
|
||||
|
||||
try:
|
||||
os.close(self.fd)
|
||||
except Exception:
|
||||
log.debug('closing fd fail', exc_info=True)
|
||||
|
||||
try:
|
||||
os.kill(self.pid, signal.SIGHUP)
|
||||
os.kill(self.pid, signal.SIGCONT)
|
||||
os.waitpid(self.pid, 0)
|
||||
except Exception:
|
||||
log.debug('waitpid fail', exc_info=True)
|
||||
|
||||
del self.sessions[self.session]
|
||||
1
butterfly/themes
Submodule
1
butterfly/themes
Submodule
Submodule butterfly/themes added at d640d1ec1c
@@ -1,7 +1,7 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright (C) 2014 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,61 @@
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
|
||||
|
||||
log = getLogger('butterfly')
|
||||
|
||||
|
||||
def get_hex_ip_port(remote):
|
||||
ip, port = remote
|
||||
if ip.startswith('::ffff:'):
|
||||
ip = ip[len('::ffff:'):]
|
||||
splits = ip.split('.')
|
||||
if ':' not in ip and len(splits) == 4:
|
||||
# Must be an ipv4
|
||||
return '%02X%02X%02X%02X:%04X' % (
|
||||
int(splits[3]),
|
||||
int(splits[2]),
|
||||
int(splits[1]),
|
||||
int(splits[0]),
|
||||
int(port)
|
||||
)
|
||||
try:
|
||||
import ipaddress
|
||||
except ImportError:
|
||||
print('Please install ipaddress backport for ipv6 user detection')
|
||||
return ''
|
||||
|
||||
# Endian reverse:
|
||||
ipv6_parts = ipaddress.IPv6Address(ip).exploded.split(':')
|
||||
for i in range(0, 8, 2):
|
||||
ipv6_parts[i], ipv6_parts[i + 1] = (
|
||||
ipv6_parts[i + 1][2:] + ipv6_parts[i + 1][:2],
|
||||
ipv6_parts[i][2:] + ipv6_parts[i][:2])
|
||||
|
||||
return ''.join(ipv6_parts) + ':%04X' % port
|
||||
|
||||
|
||||
def parse_cert(cert):
|
||||
user = None
|
||||
|
||||
for elt in cert['subject']:
|
||||
user = dict(elt).get('commonName', None)
|
||||
if user:
|
||||
break
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class User(object):
|
||||
def __init__(self, uid=None, name=None):
|
||||
if not uid and not name:
|
||||
if uid is None and not name:
|
||||
uid = os.getuid()
|
||||
if uid is not None:
|
||||
self.pw = pwd.getpwuid(uid)
|
||||
@@ -39,6 +85,10 @@ class User(object):
|
||||
def uid(self):
|
||||
return self.pw.pw_uid
|
||||
|
||||
@property
|
||||
def gid(self):
|
||||
return self.pw.pw_gid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.pw.pw_name
|
||||
@@ -56,38 +106,139 @@ class User(object):
|
||||
return self.uid == 0
|
||||
|
||||
def __eq__(self, other):
|
||||
if other is None:
|
||||
return False
|
||||
return self.uid == other.uid
|
||||
|
||||
def __repr__(self):
|
||||
return "%s [%r]" % (self.name, self.uid)
|
||||
|
||||
|
||||
def get_socket_line(port):
|
||||
class Socket(object):
|
||||
|
||||
def __init__(self, socket):
|
||||
sn = socket.getsockname()
|
||||
self.local_addr = sn[0]
|
||||
self.local_port = sn[1]
|
||||
try:
|
||||
pn = socket.getpeername()
|
||||
self.remote_addr = pn[0]
|
||||
self.remote_port = pn[1]
|
||||
except Exception:
|
||||
log.debug("Can't get peer name", exc_info=True)
|
||||
self.remote_addr = '???'
|
||||
self.remote_port = 0
|
||||
self.user = None
|
||||
self.env = {}
|
||||
|
||||
if not self.local:
|
||||
return
|
||||
|
||||
# If there is procfs, get as much info as we can
|
||||
if os.path.exists('/proc/net'):
|
||||
try:
|
||||
line = get_procfs_socket_line(get_hex_ip_port(pn[:2]))
|
||||
self.user = User(uid=int(line[7]))
|
||||
self.env = get_socket_env(line[9], self.user)
|
||||
except Exception:
|
||||
log.debug('procfs was no good, aight', exc_info=True)
|
||||
|
||||
if self.user is None:
|
||||
# Try with lsof
|
||||
try:
|
||||
self.user = User(name=get_lsof_socket_line(
|
||||
self.remote_addr, self.remote_port)[1])
|
||||
except Exception:
|
||||
log.debug('lsof was no good', exc_info=True)
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
return (self.remote_addr in ['127.0.0.1', '::1', '::ffff:127.0.0.1'] or
|
||||
self.local_addr == self.remote_addr)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Socket L: %s:%d R: %s:%d User: %r>' % (
|
||||
self.local_addr, self.local_port,
|
||||
self.remote_addr, self.remote_port,
|
||||
self.user)
|
||||
|
||||
|
||||
# Portable way to get the user, if lsof is installed
|
||||
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']).decode('utf-8')
|
||||
lines = output.split('\n')
|
||||
for line in lines:
|
||||
# Look for local address with peer port
|
||||
match = re.findall(regex, line)
|
||||
if len(match):
|
||||
match = match[0]
|
||||
if int(match[5]) == port:
|
||||
return match
|
||||
raise Exception("Couldn't find a match!")
|
||||
|
||||
|
||||
# Linux only socket line get
|
||||
def get_procfs_socket_line(hex_ip_port):
|
||||
fn = None
|
||||
if len(hex_ip_port) == 13: # ipv4
|
||||
fn = '/proc/net/tcp'
|
||||
elif len(hex_ip_port) == 37: # ipv6
|
||||
fn = '/proc/net/tcp6'
|
||||
if not fn:
|
||||
return
|
||||
try:
|
||||
with open('/proc/net/tcp') as k:
|
||||
with open(fn) as k:
|
||||
lines = k.readlines()
|
||||
for line in lines:
|
||||
# Look for local address with peer port
|
||||
if line.split()[1] == '0100007F:%X' % port:
|
||||
if line.split()[1] == hex_ip_port:
|
||||
# We got the socket
|
||||
return line.split()
|
||||
except:
|
||||
log.error('getting socket inet4 line fail', exc_info=True)
|
||||
|
||||
try:
|
||||
with open('/proc/net/tcp6') as k:
|
||||
lines = k.readlines()
|
||||
for line in lines:
|
||||
# Look for local address with peer port
|
||||
if line.split()[1] == (
|
||||
'00000000000000000000000001000000:%X' % port):
|
||||
# We got the socket
|
||||
return line.split()
|
||||
except:
|
||||
log.error('getting socket inet6 line fail', exc_info=True)
|
||||
except Exception:
|
||||
log.debug('getting socket %s line fail' % fn, exc_info=True)
|
||||
|
||||
|
||||
def get_env(inode):
|
||||
# Linux only browser environment far fetch
|
||||
def get_socket_env(inode, user):
|
||||
for pid in os.listdir("/proc/"):
|
||||
if not pid.isdigit():
|
||||
continue
|
||||
try:
|
||||
with open('/proc/%s/cmdline' % pid) as c:
|
||||
command = c.read().split('\x00')
|
||||
executable = command[0].split('/')[-1]
|
||||
if executable in ('sh', 'bash', 'zsh'):
|
||||
executable = command[1].split('/')[-1]
|
||||
if executable in [
|
||||
'gnome-session',
|
||||
'gnome-session-binary',
|
||||
'startkde',
|
||||
'startdde',
|
||||
'xfce4-session']:
|
||||
with open('/proc/%s/status' % pid) as e:
|
||||
uid = None
|
||||
for line in e.read().splitlines():
|
||||
parts = line.split('\t')
|
||||
if parts[0] == 'Uid:':
|
||||
uid = int(parts[1])
|
||||
break
|
||||
if not uid or uid != user.uid:
|
||||
continue
|
||||
|
||||
with open('/proc/%s/environ' % pid) as e:
|
||||
keyvals = e.read().split('\x00')
|
||||
env = {}
|
||||
for keyval in keyvals:
|
||||
if '=' in keyval:
|
||||
key, val = keyval.split('=', 1)
|
||||
env[key] = val
|
||||
return env
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for pid in os.listdir("/proc/"):
|
||||
if not pid.isdigit():
|
||||
continue
|
||||
@@ -110,36 +261,153 @@ def get_env(inode):
|
||||
return env
|
||||
|
||||
|
||||
class Socket(object):
|
||||
utmp_struct = struct.Struct('hi32s4s32s256shhiii4i20s')
|
||||
|
||||
def __init__(self, socket):
|
||||
sn = socket.getsockname()
|
||||
self.local_addr = sn[0]
|
||||
self.local_port = sn[1]
|
||||
pn = socket.getpeername()
|
||||
self.remote_addr = pn[0]
|
||||
self.remote_port = pn[1]
|
||||
line = get_socket_line(self.remote_port)
|
||||
if line:
|
||||
self.uid = int(line[7])
|
||||
self.inode = line[9]
|
||||
else:
|
||||
self.uid = None
|
||||
self.inode = None
|
||||
|
||||
self.env = {}
|
||||
if self.local:
|
||||
try:
|
||||
self.env = get_env(self.inode)
|
||||
except:
|
||||
log.warning('Unable to get env', exc_info=True)
|
||||
if sys.version_info[0] == 2:
|
||||
b = lambda x: x
|
||||
else:
|
||||
def b(x):
|
||||
if isinstance(x, str):
|
||||
return x.encode('utf-8')
|
||||
return x
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
return self.remote_addr in ['127.0.0.1', '::1']
|
||||
|
||||
def __repr__(self):
|
||||
return '<Socket L: %s:%d R: %s:%d Uid: %r Inode: %s %d>' % (
|
||||
self.local_addr, self.local_port,
|
||||
self.remote_addr, self.remote_port,
|
||||
self.uid, self.inode, len(self.env))
|
||||
def get_utmp_file():
|
||||
for file in (
|
||||
'/var/run/utmp',
|
||||
'/var/adm/utmp',
|
||||
'/var/adm/utmpx',
|
||||
'/etc/utmp',
|
||||
'/etc/utmpx',
|
||||
'/var/run/utx.active'):
|
||||
if os.path.exists(file):
|
||||
return file
|
||||
|
||||
|
||||
def get_wtmp_file():
|
||||
for file in (
|
||||
'/var/log/wtmp',
|
||||
'/var/adm/wtmp',
|
||||
'/var/adm/wtmpx',
|
||||
'/var/run/utx.log'):
|
||||
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'])
|
||||
|
||||
|
||||
def utmp_line(id, type, pid, fd, user, host, ts):
|
||||
return UTmp(
|
||||
type, # Type, 7 : user process
|
||||
pid, # pid
|
||||
b(fd), # line
|
||||
b(id), # id
|
||||
b(user), # user
|
||||
b(host), # host
|
||||
0, # exit 0
|
||||
0, # exit 1
|
||||
0, # session
|
||||
int(ts), # sec
|
||||
int(10 ** 6 * (ts - int(ts))), # usec
|
||||
0, # addr 0
|
||||
0, # addr 1
|
||||
0, # addr 2
|
||||
0, # addr 3
|
||||
b('') # unused
|
||||
)
|
||||
|
||||
|
||||
def add_user_info(id, fd, pid, user, host):
|
||||
# Freebsd format is not yet supported.
|
||||
# Please submit PR
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
utmp = utmp_line(id, 7, pid, fd, user, host, time.time())
|
||||
for kind, file in {
|
||||
'utmp': get_utmp_file(),
|
||||
'wtmp': get_wtmp_file()}.items():
|
||||
if not file:
|
||||
continue
|
||||
try:
|
||||
with open(file, 'rb+') as f:
|
||||
s = f.read(utmp_struct.size)
|
||||
while s:
|
||||
entry = UTmp(*utmp_struct.unpack(s))
|
||||
if kind == 'utmp' and entry.id == utmp.id:
|
||||
# Same id recycling
|
||||
f.seek(f.tell() - utmp_struct.size)
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
break
|
||||
s = f.read(utmp_struct.size)
|
||||
else:
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
except Exception:
|
||||
log.debug('Unable to write utmp info to ' + file, exc_info=True)
|
||||
|
||||
|
||||
def rm_user_info(id, pid):
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
utmp = utmp_line(id, 8, pid, '', '', '', time.time())
|
||||
for kind, file in {
|
||||
'utmp': get_utmp_file(),
|
||||
'wtmp': get_wtmp_file()}.items():
|
||||
if not file:
|
||||
continue
|
||||
try:
|
||||
with open(file, 'rb+') as f:
|
||||
s = f.read(utmp_struct.size)
|
||||
while s:
|
||||
entry = UTmp(*utmp_struct.unpack(s))
|
||||
if entry.id == utmp.id:
|
||||
if kind == 'utmp':
|
||||
# Same id closing
|
||||
f.seek(f.tell() - utmp_struct.size)
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
break
|
||||
else:
|
||||
utmp = utmp_line(
|
||||
id, 8, pid, entry.line, entry.user, '',
|
||||
time.time())
|
||||
|
||||
s = f.read(utmp_struct.size)
|
||||
else:
|
||||
f.write(utmp_struct.pack(*utmp))
|
||||
|
||||
except Exception:
|
||||
log.debug('Unable to update utmp info to ' + file, exc_info=True)
|
||||
|
||||
|
||||
class AnsiColors(object):
|
||||
colors = {
|
||||
'black': 30,
|
||||
'red': 31,
|
||||
'green': 32,
|
||||
'yellow': 33,
|
||||
'blue': 34,
|
||||
'magenta': 35,
|
||||
'cyan': 36,
|
||||
'white': 37
|
||||
}
|
||||
|
||||
def __getattr__(self, key):
|
||||
bold = True
|
||||
if key.startswith('light_'):
|
||||
bold = False
|
||||
key = key[len('light_'):]
|
||||
if key in self.colors:
|
||||
return '\x1b[%d%sm' % (
|
||||
self.colors[key],
|
||||
';1' if bold else '')
|
||||
if key == 'reset':
|
||||
return '\x1b[0m'
|
||||
return ''
|
||||
|
||||
|
||||
ansi_colors = AnsiColors()
|
||||
|
||||
94
coffees/ext/alarm.coffee
Normal file
94
coffees/ext/alarm.coffee
Normal file
@@ -0,0 +1,94 @@
|
||||
clean_ansi = (data) ->
|
||||
# Fast ansi clean (not complete)
|
||||
|
||||
if data.indexOf('\x1b') < 0
|
||||
return data
|
||||
i = -1
|
||||
out = ''
|
||||
state = 'normal'
|
||||
while i < data.length - 1
|
||||
c = data.charAt ++i
|
||||
switch state
|
||||
when 'normal'
|
||||
if c is '\x1b'
|
||||
state = 'escaped'
|
||||
break
|
||||
out += c
|
||||
|
||||
when 'escaped'
|
||||
if c is '['
|
||||
state = 'csi'
|
||||
break
|
||||
|
||||
if c is ']'
|
||||
state = 'osc'
|
||||
break
|
||||
|
||||
if '#()%*+-./'.indexOf(c) >= 0
|
||||
i++
|
||||
state = 'normal'
|
||||
|
||||
when 'csi'
|
||||
if "?>!$\" '".indexOf(c) >= 0
|
||||
break
|
||||
if '0' <= c <= '9'
|
||||
break
|
||||
break if c is ';'
|
||||
state = 'normal'
|
||||
when 'osc'
|
||||
if c is "\x1b" or c is "\x07"
|
||||
i++ if c is "\x1b"
|
||||
state = 'normal'
|
||||
|
||||
return out
|
||||
|
||||
|
||||
setAlarm = (notification, cond) ->
|
||||
alarm = (data) ->
|
||||
message = clean_ansi data.data.slice(1)
|
||||
return if cond isnt null and not cond.test(message)
|
||||
|
||||
butterfly.body.classList.remove 'alarm'
|
||||
note = "Butterfly [#{ butterfly.title }]"
|
||||
|
||||
if notification
|
||||
notif = new Notification(
|
||||
note,
|
||||
body: message,
|
||||
icon: '/static/images/favicon.png')
|
||||
notif.onclick = ->
|
||||
window.focus()
|
||||
notif.close()
|
||||
else
|
||||
alert(note + '\n' + message)
|
||||
|
||||
butterfly.ws.shell.removeEventListener 'message', alarm
|
||||
|
||||
butterfly.ws.shell.addEventListener 'message', alarm
|
||||
butterfly.body.classList.add 'alarm'
|
||||
|
||||
|
||||
cancel = (ev) ->
|
||||
ev.preventDefault() if ev.preventDefault
|
||||
ev.stopPropagation() if ev.stopPropagation
|
||||
ev.cancelBubble = true
|
||||
false
|
||||
|
||||
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 65
|
||||
|
||||
cond = null
|
||||
if e.shiftKey
|
||||
cond = prompt('Ring alarm when encountering the following text:
|
||||
(can be a regexp)')
|
||||
return unless cond
|
||||
cond = new RegExp(cond)
|
||||
|
||||
if Notification and Notification.permission is 'default'
|
||||
Notification.requestPermission ->
|
||||
setAlarm(Notification.permission is 'granted', cond)
|
||||
else
|
||||
setAlarm(Notification.permission is 'granted', cond)
|
||||
|
||||
cancel(e)
|
||||
52
coffees/ext/clipboard.coffee
Normal file
52
coffees/ext/clipboard.coffee
Normal file
@@ -0,0 +1,52 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
addEventListener 'copy', copy = (e) ->
|
||||
document.getElementsByTagName('body')[0].contentEditable = false
|
||||
butterfly.bell "copied"
|
||||
e.clipboardData.clearData()
|
||||
sel = getSelection().toString().replace(
|
||||
/\u00A0/g, ' ').replace(/\u2007/g, ' ')
|
||||
|
||||
data = ''
|
||||
for line in sel.split('\n')
|
||||
if line.slice(-1) is '\u23CE'
|
||||
end = ''
|
||||
line = line.slice(0, -1)
|
||||
else
|
||||
end = '\n'
|
||||
data += line.replace(/\s*$/, '') + end
|
||||
|
||||
e.clipboardData.setData 'text/plain', data.slice(0, -1)
|
||||
e.preventDefault()
|
||||
|
||||
|
||||
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')
|
||||
# Send big data in chunks to prevent data loss
|
||||
size = 1024
|
||||
send = ->
|
||||
butterfly.send data.substring(0, size)
|
||||
data = data.substring(size)
|
||||
if data.length
|
||||
setTimeout send, 25
|
||||
send()
|
||||
|
||||
e.preventDefault()
|
||||
5
coffees/ext/close_confirm.coffee
Normal file
5
coffees/ext/close_confirm.coffee
Normal file
@@ -0,0 +1,5 @@
|
||||
addEventListener 'beforeunload', (e) ->
|
||||
unless (butterfly.body.classList.contains('dead') or
|
||||
location.href.indexOf('session') > -1)
|
||||
e.returnValue = 'This terminal is active and not in session.
|
||||
Are you sure you want to kill it?'
|
||||
10
coffees/ext/expand_extended.coffee
Normal file
10
coffees/ext/expand_extended.coffee
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
33
coffees/ext/linkify.coffee
Normal file
33
coffees/ext/linkify.coffee
Normal file
@@ -0,0 +1,33 @@
|
||||
walk = (node, callback) ->
|
||||
for child in node.childNodes
|
||||
callback.call(child)
|
||||
walk child, callback
|
||||
|
||||
linkify = (text) ->
|
||||
# http://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
|
||||
urlPattern = (
|
||||
/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim)
|
||||
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim
|
||||
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim
|
||||
text
|
||||
.replace(urlPattern, '<a href="$&">$&</a>')
|
||||
.replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>')
|
||||
.replace(emailAddressPattern, '<a href="mailto:$&">$&</a>')
|
||||
|
||||
tags =
|
||||
'&': '&'
|
||||
'<': '<'
|
||||
'>': '>'
|
||||
|
||||
escape = (s) -> s.replace(/[&<>]/g, (tag) -> tags[tag] or tag)
|
||||
|
||||
Terminal.on 'change', (line) ->
|
||||
walk line, ->
|
||||
if @nodeType is 3
|
||||
val = @nodeValue
|
||||
linkified = linkify escape(val)
|
||||
if linkified isnt val
|
||||
newNode = document.createElement('span')
|
||||
newNode.innerHTML = linkified
|
||||
@parentElement.replaceChild newNode, @
|
||||
true
|
||||
52
coffees/ext/mobile.coffee
Normal file
52
coffees/ext/mobile.coffee
Normal file
@@ -0,0 +1,52 @@
|
||||
# *-* coding: utf-8 *-*
|
||||
# This file is part of butterfly
|
||||
#
|
||||
# butterfly Copyright(C) 2015-2017 Florian Mounier
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ctrl = false
|
||||
alt = false
|
||||
|
||||
addEventListener 'touchstart', (e) ->
|
||||
if e.touches.length == 2
|
||||
ctrl = true
|
||||
else if e.touches.length == 3
|
||||
ctrl = false
|
||||
alt = true
|
||||
else if e.touches.length == 4
|
||||
ctrl = true
|
||||
alt = true
|
||||
|
||||
# Dispatch a new event if the current event need to
|
||||
# be modified with ctrlKey and altKey from touch events
|
||||
# If so, this function will return true and dispatch the new event.
|
||||
# The caller should return immediately upon receiving true.
|
||||
window.mobileKeydown = (e) ->
|
||||
if ctrl or alt
|
||||
_ctrlKey = ctrl
|
||||
_altKey = alt
|
||||
_keyCode = e.keyCode
|
||||
if e.keyCode >= 97 && e.keyCode <= 122
|
||||
_keyCode -= 32
|
||||
e = new KeyboardEvent 'keydown',
|
||||
ctrlKey: _ctrlKey,
|
||||
altKey: _altKey,
|
||||
keyCode: _keyCode
|
||||
ctrl = alt = false
|
||||
setTimeout ->
|
||||
window.dispatchEvent e
|
||||
, 0
|
||||
return true
|
||||
else
|
||||
return false
|
||||
4
coffees/ext/new_term.coffee
Normal file
4
coffees/ext/new_term.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 79
|
||||
open(location.origin)
|
||||
cancel e
|
||||
29
coffees/ext/pack.coffee
Normal file
29
coffees/ext/pack.coffee
Normal file
@@ -0,0 +1,29 @@
|
||||
tid = null
|
||||
packSize = 1000
|
||||
histSize = 100
|
||||
|
||||
maybePack = ->
|
||||
return unless butterfly.term.childElementCount > packSize + butterfly.rows
|
||||
hist = document.getElementById 'packed'
|
||||
packfrag = document.createDocumentFragment 'fragment'
|
||||
for i in [0..packSize]
|
||||
packfrag.appendChild butterfly.term.firstChild
|
||||
pack = document.createElement 'div'
|
||||
pack.classList.add 'pack'
|
||||
pack.appendChild packfrag
|
||||
hist.appendChild pack
|
||||
|
||||
hist.firstChild.remove() if hist.childElementCount > histSize
|
||||
|
||||
tid = setTimeout maybePack
|
||||
|
||||
|
||||
Terminal.on 'refresh', ->
|
||||
clearTimeout tid if tid
|
||||
maybePack()
|
||||
|
||||
Terminal.on 'clear', ->
|
||||
newHist = document.createElement 'div'
|
||||
newHist.id = 'packed'
|
||||
hist = document.getElementById 'packed'
|
||||
butterfly.body.replaceChild newHist, hist
|
||||
36
coffees/ext/popup.coffee
Normal file
36
coffees/ext/popup.coffee
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
class Popup
|
||||
constructor: ->
|
||||
@el = document.getElementById('popup')
|
||||
@bound_click_maybe_close = @click_maybe_close.bind(@)
|
||||
@bound_key_maybe_close = @key_maybe_close.bind(@)
|
||||
|
||||
open: (html) ->
|
||||
@el.innerHTML = html
|
||||
@el.classList.remove 'hidden'
|
||||
|
||||
addEventListener 'click', @bound_click_maybe_close
|
||||
addEventListener 'keydown', @bound_key_maybe_close
|
||||
|
||||
close: ->
|
||||
removeEventListener 'click', @bound_click_maybe_close
|
||||
removeEventListener 'keydown', @bound_key_maybe_close
|
||||
|
||||
@el.classList.add 'hidden'
|
||||
@el.innerHTML = ''
|
||||
|
||||
click_maybe_close: (e) ->
|
||||
t = e.target
|
||||
while t.parentElement
|
||||
return true if Array.prototype.slice.call(@el.children).indexOf(t) > -1
|
||||
t = t.parentElement
|
||||
@close()
|
||||
cancel e
|
||||
|
||||
key_maybe_close: (e) ->
|
||||
return true unless e.keyCode is 27
|
||||
@close()
|
||||
cancel e
|
||||
|
||||
popup = new Popup()
|
||||
|
||||
259
coffees/ext/selection.coffee
Normal file
259
coffees/ext/selection.coffee
Normal file
@@ -0,0 +1,259 @@
|
||||
# *-* 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/>.
|
||||
selection = null
|
||||
|
||||
cancel = (ev) ->
|
||||
ev.preventDefault() if ev.preventDefault
|
||||
ev.stopPropagation() if ev.stopPropagation
|
||||
ev.cancelBubble = true
|
||||
false
|
||||
|
||||
previousLeaf = (node) ->
|
||||
previous = node.previousSibling
|
||||
if not previous
|
||||
previous = node.parentNode.previousSibling
|
||||
if not previous
|
||||
previous = node.parentNode.parentNode.previousSibling
|
||||
while previous.lastChild
|
||||
previous = previous.lastChild
|
||||
previous
|
||||
|
||||
nextLeaf = (node) ->
|
||||
next = node.nextSibling
|
||||
if not next
|
||||
next = node.parentNode.nextSibling
|
||||
if not next
|
||||
next = node.parentNode.parentNode.nextSibling
|
||||
while next?.firstChild
|
||||
next = next.firstChild
|
||||
next
|
||||
|
||||
class Selection
|
||||
constructor: ->
|
||||
butterfly.body.classList.add('selection')
|
||||
@selection = getSelection()
|
||||
|
||||
reset: ->
|
||||
@selection = getSelection()
|
||||
fakeRange = document.createRange()
|
||||
fakeRange.setStart(@selection.anchorNode, @selection.anchorOffset)
|
||||
fakeRange.setEnd(@selection.focusNode, @selection.focusOffset)
|
||||
@start =
|
||||
node: @selection.anchorNode
|
||||
offset: @selection.anchorOffset
|
||||
@end =
|
||||
node: @selection.focusNode
|
||||
offset: @selection.focusOffset
|
||||
|
||||
if fakeRange.collapsed
|
||||
[@start, @end] = [@end, @start]
|
||||
|
||||
@startLine = @start.node
|
||||
while not @startLine.classList or 'line' not in @startLine.classList
|
||||
@startLine = @startLine.parentNode
|
||||
|
||||
@endLine = @end.node
|
||||
while not @endLine.classList or 'line' not in @endLine.classList
|
||||
@endLine = @endLine.parentNode
|
||||
|
||||
clear: ->
|
||||
@selection.removeAllRanges()
|
||||
|
||||
destroy: ->
|
||||
butterfly.body.classList.remove('selection')
|
||||
@clear()
|
||||
|
||||
text: ->
|
||||
@selection.toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ')
|
||||
|
||||
up: ->
|
||||
@go -1
|
||||
|
||||
down: ->
|
||||
@go +1
|
||||
|
||||
go: (n) ->
|
||||
index = Array.prototype.indexOf.call(
|
||||
butterfly.term.childNodes, @startLine) + n
|
||||
return unless 0 <= index < butterfly.term.childElementCount
|
||||
|
||||
until butterfly.term.childNodes[index].textContent.match /\S/
|
||||
index += n
|
||||
return unless 0 <= index < butterfly.term.childElementCount
|
||||
|
||||
@selectLine index
|
||||
|
||||
apply: ->
|
||||
@clear()
|
||||
range = document.createRange()
|
||||
range.setStart @start.node, @start.offset
|
||||
range.setEnd @end.node, @end.offset
|
||||
@selection.addRange range
|
||||
|
||||
selectLine: (index) ->
|
||||
line = butterfly.term.childNodes[index]
|
||||
lineStart =
|
||||
node: line.firstChild
|
||||
offset: 0
|
||||
|
||||
lineEnd =
|
||||
node: line.lastChild
|
||||
offset: line.lastChild.textContent.length
|
||||
|
||||
@start = @walk lineStart, /\S/
|
||||
@end = @walk lineEnd, /\S/, true
|
||||
|
||||
collapsed: (start, end) ->
|
||||
fakeRange = document.createRange()
|
||||
fakeRange.setStart(start.node, start.offset)
|
||||
fakeRange.setEnd(end.node, end.offset)
|
||||
fakeRange.collapsed
|
||||
|
||||
shrinkRight: ->
|
||||
node = @walk @end, /\s/, true
|
||||
end = @walk node, /\S/, true
|
||||
if not @collapsed(@start, end)
|
||||
@end = end
|
||||
|
||||
shrinkLeft: ->
|
||||
node = @walk @start, /\s/
|
||||
start = @walk node, /\S/
|
||||
if not @collapsed(start, @end)
|
||||
@start = start
|
||||
|
||||
expandRight: ->
|
||||
node = @walk @end, /\S/
|
||||
@end = @walk node, /\s/
|
||||
|
||||
expandLeft: ->
|
||||
node = @walk @start, /\S/, true
|
||||
@start = @walk node, /\s/, true
|
||||
|
||||
walk: (needle, til, backward=false) ->
|
||||
if needle.node.firstChild
|
||||
node = needle.node.firstChild
|
||||
else
|
||||
node = needle.node
|
||||
|
||||
text = node?.textContent
|
||||
i = needle.offset
|
||||
if backward
|
||||
while node
|
||||
while i > 0
|
||||
if text[--i].match til
|
||||
return node: node, offset: i + 1
|
||||
node = previousLeaf node
|
||||
text = node?.textContent
|
||||
i = text.length
|
||||
else
|
||||
while node
|
||||
while i < text.length
|
||||
if text[i++].match til
|
||||
return node: node, offset: i - 1
|
||||
node = nextLeaf node
|
||||
text = node?.textContent
|
||||
i = 0
|
||||
|
||||
return needle
|
||||
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true if e.keyCode in [16..19]
|
||||
|
||||
# Paste natural selection too if shiftkey
|
||||
if e.shiftKey and e.keyCode is 13 and
|
||||
not selection and not getSelection().isCollapsed
|
||||
butterfly.send getSelection().toString()
|
||||
getSelection().removeAllRanges()
|
||||
return cancel e
|
||||
|
||||
if selection
|
||||
selection.reset()
|
||||
if not e.ctrlKey and e.shiftKey and 37 <= e.keyCode <= 40
|
||||
return true
|
||||
if e.shiftKey and e.ctrlKey
|
||||
if e.keyCode == 38
|
||||
selection.up()
|
||||
else if e.keyCode == 40
|
||||
selection.down()
|
||||
else if e.keyCode == 39
|
||||
selection.shrinkLeft()
|
||||
else if e.keyCode == 38
|
||||
selection.expandLeft()
|
||||
else if e.keyCode == 37
|
||||
selection.shrinkRight()
|
||||
else if e.keyCode == 40
|
||||
selection.expandRight()
|
||||
else
|
||||
return cancel e
|
||||
|
||||
selection?.apply()
|
||||
return cancel 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 r + butterfly.y - 1
|
||||
selection.apply()
|
||||
return cancel e
|
||||
true
|
||||
|
||||
document.addEventListener 'keyup', (e) ->
|
||||
return true if e.keyCode in [16..19]
|
||||
|
||||
if selection
|
||||
if e.keyCode == 13
|
||||
butterfly.send selection.text()
|
||||
selection.destroy()
|
||||
selection = null
|
||||
return cancel e
|
||||
if e.keyCode not in [37..40]
|
||||
selection.destroy()
|
||||
selection = null
|
||||
return true
|
||||
true
|
||||
|
||||
document.addEventListener 'dblclick', (e) ->
|
||||
return if e.ctrlKey or e.altkey
|
||||
sel = getSelection()
|
||||
return if sel.isCollapsed or sel.toString().match /\s/
|
||||
|
||||
range = document.createRange()
|
||||
range.setStart(sel.anchorNode, sel.anchorOffset)
|
||||
range.setEnd(sel.focusNode, sel.focusOffset)
|
||||
if range.collapsed
|
||||
sel.removeAllRanges()
|
||||
newRange = document.createRange()
|
||||
newRange.setStart(sel.focusNode, sel.focusOffset)
|
||||
newRange.setEnd(sel.anchorNode, sel.anchorOffset)
|
||||
sel.addRange(newRange)
|
||||
|
||||
until sel.toString().match(/\s/) or not sel.toString()
|
||||
sel.modify 'extend', 'forward', 'character'
|
||||
|
||||
sel.modify 'extend', 'backward', 'character'
|
||||
|
||||
# Return selection
|
||||
anchorNode = sel.anchorNode
|
||||
anchorOffset = sel.anchorOffset
|
||||
sel.collapseToEnd()
|
||||
sel.extend(anchorNode, anchorOffset)
|
||||
|
||||
until sel.toString().match(/\s/) or not sel.toString()
|
||||
sel.modify 'extend', 'backward', 'character'
|
||||
|
||||
sel.modify 'extend', 'forward', 'character'
|
||||
22
coffees/ext/sessions.coffee
Normal file
22
coffees/ext/sessions.coffee
Normal file
@@ -0,0 +1,22 @@
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 69
|
||||
oReq = new XMLHttpRequest()
|
||||
oReq.addEventListener 'load', ->
|
||||
response = JSON.parse(@responseText)
|
||||
out = '<div>'
|
||||
out += '<h2>Session list</h2>'
|
||||
if response.sessions.length is 0
|
||||
out += "No current session for user #{response.user}"
|
||||
else
|
||||
out += '<ul>'
|
||||
for session in response.sessions
|
||||
out += "<li><a href=\"/session/#{session}\">#{session}</a></li>"
|
||||
out += '</ul>'
|
||||
|
||||
out += '</div>'
|
||||
|
||||
popup.open out
|
||||
|
||||
oReq.open("GET", "/sessions/list.json")
|
||||
oReq.send()
|
||||
cancel e
|
||||
80
coffees/ext/theme.coffee
Normal file
80
coffees/ext/theme.coffee
Normal file
@@ -0,0 +1,80 @@
|
||||
_set_theme_href = (href) ->
|
||||
document.getElementById('style').setAttribute('href', href)
|
||||
img = document.createElement('img')
|
||||
img.onerror = ->
|
||||
setTimeout (-> butterfly?.resize()), 250
|
||||
img.src = href
|
||||
|
||||
_theme = localStorage?.getItem('theme')
|
||||
_set_theme_href(_theme) if _theme
|
||||
|
||||
@set_theme = (theme) ->
|
||||
_theme = theme
|
||||
localStorage?.setItem('theme', theme)
|
||||
_set_theme_href(theme) if theme
|
||||
|
||||
document.addEventListener 'keydown', (e) ->
|
||||
return true unless e.altKey and e.keyCode is 83
|
||||
if e.shiftKey
|
||||
style = document.getElementById('style').getAttribute('href')
|
||||
style = style.split('?')[0]
|
||||
_set_theme_href style + '?' + (new Date().getTime())
|
||||
return cancel(e)
|
||||
|
||||
|
||||
oReq = new XMLHttpRequest()
|
||||
oReq.addEventListener 'load', ->
|
||||
response = JSON.parse(@responseText)
|
||||
builtin_themes = response.builtin_themes
|
||||
themes = response.themes
|
||||
|
||||
# if themes.length is 0
|
||||
# alert("No themes found in #{response.dir}.\n
|
||||
# Please install themes with butterfly.server.py --install-themes")
|
||||
# return
|
||||
|
||||
inner = """
|
||||
<form>
|
||||
<h2>Pick a theme:</h2>
|
||||
<select id="theme_list">
|
||||
"""
|
||||
option = (url, theme) ->
|
||||
inner += '<option '
|
||||
|
||||
if _theme is url
|
||||
inner += 'selected '
|
||||
|
||||
inner += "value=\"#{url}\">"
|
||||
inner += theme
|
||||
inner += '</option>'
|
||||
|
||||
option "/static/main.css", 'default'
|
||||
|
||||
if themes.length
|
||||
inner += '<optgroup label="Local themes">'
|
||||
for theme in themes
|
||||
url = "/theme/#{theme}/style.css"
|
||||
option url, theme
|
||||
inner += '</optgroup>'
|
||||
|
||||
inner += '<optgroup label="Built-in themes">'
|
||||
for theme in builtin_themes
|
||||
url = "/theme/#{theme}/style.css"
|
||||
option url, theme.slice('built-in-'.length)
|
||||
inner += '</optgroup>'
|
||||
|
||||
inner += """
|
||||
</select>
|
||||
<label>You can create yours in #{response.dir}.</label>
|
||||
</form>
|
||||
"""
|
||||
popup.open inner
|
||||
|
||||
theme_list = document.getElementById('theme_list')
|
||||
|
||||
theme_list.addEventListener 'change', -> set_theme theme_list.value
|
||||
|
||||
oReq.open("GET", "/themes/list.json")
|
||||
oReq.send()
|
||||
|
||||
cancel e
|
||||
125
coffees/main.coffee
Normal file
125
coffees/main.coffee
Normal file
@@ -0,0 +1,125 @@
|
||||
# *-* 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/>.
|
||||
|
||||
cols = rows = null
|
||||
quit = false
|
||||
openTs = (new Date()).getTime()
|
||||
|
||||
ws =
|
||||
shell: null
|
||||
ctl: null
|
||||
|
||||
$ = document.querySelectorAll.bind(document)
|
||||
|
||||
document.addEventListener 'DOMContentLoaded', ->
|
||||
term = null
|
||||
|
||||
if location.protocol == 'https:'
|
||||
wsUrl = 'wss://'
|
||||
else
|
||||
wsUrl = 'ws://'
|
||||
|
||||
rootPath = document.body.getAttribute('data-root-path')
|
||||
rootPath = rootPath.replace(/^\/+|\/+$/g, '')
|
||||
if rootPath.length
|
||||
rootPath = "/#{rootPath}"
|
||||
|
||||
wsUrl += document.location.host + rootPath
|
||||
path = '/'
|
||||
if path.indexOf('/session') < 0
|
||||
path += "session/#{document.body.getAttribute('data-session-token')}"
|
||||
|
||||
path += location.search
|
||||
|
||||
ws.shell = new WebSocket wsUrl + '/ws' + path
|
||||
ws.ctl = new WebSocket wsUrl + '/ctl' + path
|
||||
|
||||
open = ->
|
||||
console.log "WebSocket open", arguments
|
||||
|
||||
if term
|
||||
term.body.classList.remove 'stopped'
|
||||
term.out = ws.shell.send.bind(ws.shell)
|
||||
term.out '\x03\n'
|
||||
return
|
||||
|
||||
if (ws.shell.readyState is WebSocket.OPEN and
|
||||
ws.ctl.readyState is WebSocket.OPEN)
|
||||
|
||||
term = new Terminal(
|
||||
document.body, ws.shell.send.bind(ws.shell), ws.ctl.send.bind(ws.ctl))
|
||||
term.ws = ws
|
||||
window.butterfly = term
|
||||
ws.ctl.send JSON.stringify(cmd: 'open')
|
||||
ws.ctl.send JSON.stringify(
|
||||
cmd: 'size', cols: term.cols, rows: term.rows)
|
||||
openTs = (new Date()).getTime()
|
||||
console.log "WebSocket open end", arguments
|
||||
|
||||
error = ->
|
||||
console.error "WebSocket error", arguments
|
||||
|
||||
close = ->
|
||||
console.log "WebSocket closed", arguments
|
||||
return if quit
|
||||
quit = true
|
||||
|
||||
term.write 'Closed'
|
||||
# Allow quick reload
|
||||
term.skipNextKey = true
|
||||
term.body.classList.add('dead')
|
||||
# Don't autoclose if websocket didn't last 1 minute
|
||||
if (new Date()).getTime() - openTs > 60 * 1000
|
||||
window.open('','_self').close()
|
||||
|
||||
reopenOnClose = ->
|
||||
setTimeout ->
|
||||
return if quit
|
||||
ws.shell = new WebSocket wsUrl + '/ws' + path
|
||||
init_shell_ws()
|
||||
, 100
|
||||
|
||||
write = (data) ->
|
||||
if term
|
||||
term.write data
|
||||
|
||||
write_request = (e) ->
|
||||
setTimeout write, 1, e.data
|
||||
|
||||
ctl = (e) ->
|
||||
cmd = JSON.parse(e.data)
|
||||
if cmd.cmd is 'size'
|
||||
term.resize cmd.cols, cmd.rows, true
|
||||
|
||||
init_shell_ws = ->
|
||||
ws.shell.addEventListener 'open', open
|
||||
ws.shell.addEventListener 'message', write_request
|
||||
ws.shell.addEventListener 'error', error
|
||||
ws.shell.addEventListener 'close', reopenOnClose
|
||||
|
||||
init_ctl_ws = ->
|
||||
ws.ctl.addEventListener 'open', open
|
||||
ws.ctl.addEventListener 'message', ctl
|
||||
ws.ctl.addEventListener 'error', error
|
||||
ws.ctl.addEventListener 'close', close
|
||||
|
||||
init_shell_ws()
|
||||
init_ctl_ws()
|
||||
|
||||
addEventListener 'beforeunload', ->
|
||||
if not quit
|
||||
'This will exit the terminal session'
|
||||
3332
coffees/term.coffee
Normal file
3332
coffees/term.coffee
Normal file
File diff suppressed because it is too large
Load Diff
49
dev.py
49
dev.py
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from multiprocessing import Process
|
||||
from subprocess import Popen
|
||||
from glob import glob
|
||||
import time
|
||||
import sys
|
||||
import shlex
|
||||
|
||||
commands = [
|
||||
'coffee -wcb -j butterfly/static/javascripts/main.js ' +
|
||||
'butterfly/static/coffees/term.coffee ' +
|
||||
'butterfly/static/coffees/backsel.coffee ' +
|
||||
'butterfly/static/coffees/virtual_input.coffee ' +
|
||||
'butterfly/static/coffees/main.coffee ',
|
||||
'coffee -wcb -o butterfly/static/javascripts/ ' +
|
||||
'butterfly/static/coffees/worker.coffee',
|
||||
'compass watch butterfly/static',
|
||||
'python butterfly.server.py ' + ' '.join(sys.argv[1:])
|
||||
]
|
||||
|
||||
|
||||
class Run(Process):
|
||||
daemon = True
|
||||
|
||||
def __init__(self, command, *args, **kwargs):
|
||||
super(Run, self).__init__(*args, **kwargs)
|
||||
self.cmd = command
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while True:
|
||||
self.proc = Popen(shlex.split(self.cmd))
|
||||
self.proc.wait()
|
||||
print(self.cmd + ' exited. Relaunching in 250ms')
|
||||
time.sleep(.25)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
process = [Run(cmd) for cmd in commands]
|
||||
for proc in process:
|
||||
print('Lauching %s' % proc.cmd.split(' ')[0])
|
||||
proc.start()
|
||||
|
||||
try:
|
||||
for proc in process:
|
||||
proc.join()
|
||||
print('Joined')
|
||||
except KeyboardInterrupt:
|
||||
print('\nGot [ctrl]+[c] -- bye bye')
|
||||
14
docker/run.sh
Executable file
14
docker/run.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
# if command starts with an option, prepend the default command and options
|
||||
if [ "${1:0:1}" = '-' ]; then
|
||||
set -- butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT:-57575} "$@"
|
||||
elif [ "$1" = 'butterfly.server.py' ]; then
|
||||
shift
|
||||
set -- butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT:-57575} "$@"
|
||||
fi
|
||||
|
||||
# Set password
|
||||
echo "root:${PASSWORD:-password}" | chpasswd
|
||||
|
||||
exec "$@"
|
||||
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "butterfly",
|
||||
"version": "3.0.0",
|
||||
"description": "A sleek web based terminal emulator",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paradoxxxzero/butterfly.git"
|
||||
},
|
||||
"private": true,
|
||||
"license": "GPLv3",
|
||||
"bugs": {
|
||||
"url": "https://github.com/paradoxxxzero/butterfly/issues"
|
||||
},
|
||||
"homepage": "https://github.com/paradoxxxzero/butterfly",
|
||||
"devDependencies": {
|
||||
"coffeelint": "^1.15.7",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-coffeelint": "0.0.15",
|
||||
"grunt-contrib-coffee": "^1.0.0",
|
||||
"grunt-contrib-cssmin": "^1.0.1",
|
||||
"grunt-contrib-uglify": "^1.0.1",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-sass": "^2.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
tornado
|
||||
tornado>=3.2
|
||||
|
||||
43
scripts/butterfly
Executable file
43
scripts/butterfly
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
if (os.getenv('COLORTERM', '') != 'butterfly' and
|
||||
len(sys.argv) == 1) or (
|
||||
os.getenv('COLORTERM', '') == 'butterfly' and
|
||||
len(sys.argv) > 1 and sys.argv[1] == 'run'):
|
||||
os.execvp('butterfly.server.py', [
|
||||
'butterfly', '--unsecure', '--port=0', '--one-shot'])
|
||||
|
||||
path = os.getenv('BUTTERFLY_PATH')
|
||||
if not path:
|
||||
try:
|
||||
import butterfly
|
||||
path = os.path.join(
|
||||
os.path.dirname(butterfly.__file__), 'bin')
|
||||
except Exception:
|
||||
pass
|
||||
os.putenv('BUTTERFLY_PATH', path)
|
||||
if path is None:
|
||||
print("Can't get butterfly path. Aborting.")
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
description='Butterfly launcher. Please specify a command')
|
||||
parser.add_argument('-h', '--help', action="store_true",
|
||||
help="show this help message and exit")
|
||||
parser.add_argument(
|
||||
'command',
|
||||
nargs='?',
|
||||
choices=[x[:-3] for x in os.listdir(path) if x.endswith('.py')])
|
||||
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
else:
|
||||
file_ = os.path.join(path, '%s.py' % args.command)
|
||||
sys.argv = sys.argv[1:]
|
||||
exec(compile(open(file_).read(), file_, 'exec'))
|
||||
7
setup.cfg
Normal file
7
setup.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[tool:pytest]
|
||||
flake8-ignore =
|
||||
*.py E731 E402
|
||||
butterfly/bin/help.py E501
|
||||
54
setup.py
54
setup.py
@@ -5,41 +5,53 @@
|
||||
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.server.py', 'scripts/butterfly', 'scripts/b'],
|
||||
packages=['butterfly'],
|
||||
install_requires=["tornado"],
|
||||
install_requires=["tornado>=3.2", "pyOpenSSL"],
|
||||
extras_require={
|
||||
'themes': ["libsass"],
|
||||
'systemd': ['tornado_systemd'],
|
||||
'lint': ['pytest', 'pytest-flake8', 'pytest-isort']
|
||||
},
|
||||
package_data={
|
||||
'butterfly': [
|
||||
'sass/*.sass',
|
||||
'themes/*.*',
|
||||
'themes/*/*.*',
|
||||
'themes/*/*/*.*',
|
||||
'static/fonts/*',
|
||||
'static/stylesheets/main.css',
|
||||
'static/javascripts/main.js',
|
||||
'templates/index.html'
|
||||
'static/images/favicon.png',
|
||||
'static/main.css',
|
||||
'static/html-sanitizer.js',
|
||||
'static/*.min.js',
|
||||
'templates/index.html',
|
||||
'bin/*',
|
||||
'templates/motd',
|
||||
'butterfly.conf.default'
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Terminals"])
|
||||
|
||||
setup(**options)
|
||||
|
||||
Reference in New Issue
Block a user