mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-06-09 22:04:40 +00:00
Merge branch 'master' into domscroll_try2
This commit is contained in:
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM ubuntu:14.04.1
|
||||||
|
|
||||||
|
RUN apt-get update -y
|
||||||
|
RUN apt-get install -y python-setuptools python-dev build-essential libffi-dev libssl-dev
|
||||||
|
|
||||||
|
WORKDIR /opt
|
||||||
|
ADD . /opt/app
|
||||||
|
WORKDIR /opt/app
|
||||||
|
|
||||||
|
RUN python setup.py build
|
||||||
|
RUN python setup.py install
|
||||||
|
|
||||||
|
ADD docker/run.sh /opt/run.sh
|
||||||
|
RUN chmod 777 /opt/run.sh
|
||||||
|
|
||||||
|
EXPOSE 57575
|
||||||
|
|
||||||
|
CMD ["/opt/run.sh"]
|
||||||
@@ -12,21 +12,16 @@ module.exports = (grunt) ->
|
|||||||
butterfly:
|
butterfly:
|
||||||
files:
|
files:
|
||||||
'butterfly/static/main.min.js': 'butterfly/static/main.js'
|
'butterfly/static/main.min.js': 'butterfly/static/main.js'
|
||||||
|
'butterfly/static/ext.min.js': 'butterfly/static/ext.js'
|
||||||
|
|
||||||
sass_to_scss:
|
|
||||||
butterfly:
|
|
||||||
expand: true
|
|
||||||
cwd: 'sass/'
|
|
||||||
src: '*.sass'
|
|
||||||
dest: 'butterfly/scss/'
|
|
||||||
ext: '.scss'
|
|
||||||
|
|
||||||
sass:
|
sass:
|
||||||
|
options:
|
||||||
|
includePaths: ['butterfly/sass/']
|
||||||
|
|
||||||
butterfly:
|
butterfly:
|
||||||
expand: true
|
expand: true
|
||||||
cwd: 'butterfly/scss'
|
cwd: 'butterfly/sass/'
|
||||||
src: '*.scss'
|
src: '*.sass'
|
||||||
dest: 'butterfly/static/'
|
dest: 'butterfly/static/'
|
||||||
ext: '.css'
|
ext: '.css'
|
||||||
|
|
||||||
@@ -36,12 +31,8 @@ module.exports = (grunt) ->
|
|||||||
|
|
||||||
butterfly:
|
butterfly:
|
||||||
files:
|
files:
|
||||||
'butterfly/static/main.js': [
|
'butterfly/static/main.js': 'coffees/*.coffee'
|
||||||
'coffees/term.coffee'
|
'butterfly/static/ext.js': 'coffees/ext/*.coffee'
|
||||||
'coffees/selection.coffee'
|
|
||||||
'coffees/virtual_input.coffee'
|
|
||||||
'coffees/main.coffee'
|
|
||||||
]
|
|
||||||
|
|
||||||
coffeelint:
|
coffeelint:
|
||||||
butterfly:
|
butterfly:
|
||||||
@@ -52,6 +43,7 @@ module.exports = (grunt) ->
|
|||||||
livereload: true
|
livereload: true
|
||||||
coffee:
|
coffee:
|
||||||
files: [
|
files: [
|
||||||
|
'coffees/ext/*.coffee'
|
||||||
'coffees/*.coffee'
|
'coffees/*.coffee'
|
||||||
'Gruntfile.coffee'
|
'Gruntfile.coffee'
|
||||||
]
|
]
|
||||||
@@ -59,9 +51,9 @@ module.exports = (grunt) ->
|
|||||||
|
|
||||||
sass:
|
sass:
|
||||||
files: [
|
files: [
|
||||||
'sass/*.sass'
|
'butterfly/sass/*.sass'
|
||||||
]
|
]
|
||||||
tasks: ['sass_to_scss', 'sass']
|
tasks: ['sass']
|
||||||
|
|
||||||
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||||
@@ -69,12 +61,8 @@ module.exports = (grunt) ->
|
|||||||
grunt.loadNpmTasks 'grunt-contrib-cssmin'
|
grunt.loadNpmTasks 'grunt-contrib-cssmin'
|
||||||
grunt.loadNpmTasks 'grunt-coffeelint'
|
grunt.loadNpmTasks 'grunt-coffeelint'
|
||||||
grunt.loadNpmTasks 'grunt-sass'
|
grunt.loadNpmTasks 'grunt-sass'
|
||||||
grunt.loadNpmTasks 'grunt-sass-to-scss'
|
|
||||||
|
|
||||||
grunt.registerTask 'dev', [
|
grunt.registerTask 'dev', [
|
||||||
'coffeelint', 'coffee', 'sass_to_scss', 'sass', 'watch']
|
'coffeelint', 'coffee', 'sass', 'watch']
|
||||||
grunt.registerTask 'css', ['sass_to_scss', 'sass']
|
grunt.registerTask 'css', ['sass']
|
||||||
grunt.registerTask 'default', [
|
grunt.registerTask 'default', [
|
||||||
'coffeelint', 'coffee',
|
'coffeelint', 'coffee', 'sass', 'uglify']
|
||||||
'sass_to_scss', 'sass',
|
|
||||||
'uglify']
|
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -19,6 +19,17 @@ The js part is heavily based on [term.js](https://github.com/chjj/term.js/) whic
|
|||||||
|
|
||||||
Then open [localhost:57575](http://localhost:57575) in your favorite browser and done.
|
Then open [localhost:57575](http://localhost:57575) in your favorite browser and done.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
@@ -56,3 +67,16 @@ Run `python dev.py --debug --port=12345` and you are set (yes you can launch it
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docker Usage
|
||||||
|
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/
|
||||||
|
|
||||||
|
### Starting
|
||||||
|
|
||||||
|
docker run \
|
||||||
|
--env PASSWORD=password \
|
||||||
|
--env PORT=57575 \
|
||||||
|
-p 57575:57575 \
|
||||||
|
-d garland/butterfly
|
||||||
|
|
||||||
|
|||||||
2
bin/hr
2
bin/hr
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
|
||||||
print('\x1b]99;<hr />\x07')
|
print('\x1bP;HTML|<hr />\x1bP')
|
||||||
|
|||||||
4
bin/ils
4
bin/ils
@@ -6,7 +6,7 @@ import os
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import base64
|
import base64
|
||||||
import io
|
import io
|
||||||
print('\x1b]99;')
|
print('\x1bP;HTML|')
|
||||||
|
|
||||||
out = ''
|
out = ''
|
||||||
|
|
||||||
@@ -29,4 +29,4 @@ for f in os.listdir(os.getcwd()):
|
|||||||
|
|
||||||
print(out)
|
print(out)
|
||||||
|
|
||||||
print('\x07')
|
print('\x1bP')
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ calendar = LocaleHTMLCalendar(locale=locale.getlocale())
|
|||||||
calendar_table = calendar.formatmonth(now.year, now.month)
|
calendar_table = calendar.formatmonth(now.year, now.month)
|
||||||
calendar_table = calendar_table.replace('border="0"', 'border="1"')
|
calendar_table = calendar_table.replace('border="0"', 'border="1"')
|
||||||
|
|
||||||
print('\x1b]99;')
|
print('\x1bP;HTML|')
|
||||||
print(calendar_table)
|
print(calendar_table)
|
||||||
print('\x07')
|
print('\x1bP')
|
||||||
|
|||||||
31
butterfly.server.py
Normal file → Executable file
31
butterfly.server.py
Normal file → Executable file
@@ -20,6 +20,7 @@
|
|||||||
import tornado.options
|
import tornado.options
|
||||||
import tornado.ioloop
|
import tornado.ioloop
|
||||||
import tornado.httpserver
|
import tornado.httpserver
|
||||||
|
import tornado_systemd
|
||||||
import uuid
|
import uuid
|
||||||
import ssl
|
import ssl
|
||||||
import getpass
|
import getpass
|
||||||
@@ -35,16 +36,23 @@ tornado.options.define("more", default=False,
|
|||||||
tornado.options.define("host", default='localhost', help="Server host")
|
tornado.options.define("host", default='localhost', help="Server host")
|
||||||
tornado.options.define("port", default=57575, type=int, help="Server port")
|
tornado.options.define("port", default=57575, type=int, help="Server port")
|
||||||
tornado.options.define("shell", help="Shell to execute at login")
|
tornado.options.define("shell", help="Shell to execute at login")
|
||||||
|
tornado.options.define("cmd",
|
||||||
|
help="Command to run instead of shell, f.i.: 'ls -l'")
|
||||||
tornado.options.define("unsecure", default=False,
|
tornado.options.define("unsecure", default=False,
|
||||||
help="Don't use ssl not recommended")
|
help="Don't use ssl not recommended")
|
||||||
|
tornado.options.define("allow_html_escapes", default=False,
|
||||||
|
help="Allow use of HTML escapes. "
|
||||||
|
"Really unsafe as it is now.")
|
||||||
|
tornado.options.define("native_scroll", default=False,
|
||||||
|
help="Use experimental native scroll")
|
||||||
tornado.options.define("login", default=True,
|
tornado.options.define("login", default=True,
|
||||||
help="Use login screen at start")
|
help="Use login screen at start")
|
||||||
|
tornado.options.define("ssl_version", default=None,
|
||||||
|
help="SSL protocol version")
|
||||||
tornado.options.define("generate_certs", default=False,
|
tornado.options.define("generate_certs", default=False,
|
||||||
help="Generate butterfly certificates")
|
help="Generate butterfly certificates")
|
||||||
tornado.options.define("generate_user_pkcs", default='',
|
tornado.options.define("generate_user_pkcs", default='',
|
||||||
help="Generate user pfx for client authentication")
|
help="Generate user pfx for client authentication")
|
||||||
|
|
||||||
tornado.options.define("unminified", default=False,
|
tornado.options.define("unminified", default=False,
|
||||||
help="Use the unminified js (for development only)")
|
help="Use the unminified js (for development only)")
|
||||||
|
|
||||||
@@ -217,15 +225,28 @@ else:
|
|||||||
'ca_certs': ca,
|
'ca_certs': ca,
|
||||||
'cert_reqs': ssl.CERT_REQUIRED
|
'cert_reqs': ssl.CERT_REQUIRED
|
||||||
}
|
}
|
||||||
|
if tornado.options.options.ssl_version is not None:
|
||||||
|
if not hasattr(
|
||||||
|
ssl, 'PROTOCOL_%s' % tornado.options.options.ssl_version):
|
||||||
|
print(
|
||||||
|
"Unknown SSL protocol %s" %
|
||||||
|
tornado.options.options.ssl_version)
|
||||||
|
sys.exit(1)
|
||||||
|
ssl_opts['ssl_version'] = getattr(
|
||||||
|
ssl, 'PROTOCOL_%s' % tornado.options.options.ssl_version)
|
||||||
|
|
||||||
from butterfly import application
|
from butterfly import application
|
||||||
http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_opts)
|
|
||||||
http_server.listen(port, address=host)
|
|
||||||
|
|
||||||
|
http_server = tornado_systemd.SystemdHTTPServer(
|
||||||
|
application, ssl_options=ssl_opts)
|
||||||
|
http_server.listen(port, address=host)
|
||||||
url = "http%s://%s:%d/*" % (
|
url = "http%s://%s:%d/*" % (
|
||||||
"s" if not tornado.options.options.unsecure else "", host, port)
|
"s" if not tornado.options.options.unsecure else "", host, port)
|
||||||
|
|
||||||
|
if http_server.systemd:
|
||||||
|
os.environ.pop('LISTEN_PID')
|
||||||
|
os.environ.pop('LISTEN_FDS')
|
||||||
|
|
||||||
# This is for debugging purpose
|
# This is for debugging purpose
|
||||||
try:
|
try:
|
||||||
from wsreload.client import sporadic_reload, watch
|
from wsreload.client import sporadic_reload, watch
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Butterfly Terminal Server
|
Description=Butterfly Terminal Server
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/butterfly.server.py
|
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
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
__version__ = '1.5.2'
|
__version__ = '1.5.10'
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ class Style(Route):
|
|||||||
@url(r'/ws(?:/user/([^/]+))?/?(?:/wd/(.+))?')
|
@url(r'/ws(?:/user/([^/]+))?/?(?:/wd/(.+))?')
|
||||||
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
|
terminals = set()
|
||||||
|
|
||||||
def pty(self):
|
def pty(self):
|
||||||
self.pid, self.fd = pty.fork()
|
self.pid, self.fd = pty.fork()
|
||||||
if self.pid == 0:
|
if self.pid == 0:
|
||||||
@@ -174,7 +176,11 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
|||||||
# or login is explicitly turned off
|
# or login is explicitly turned off
|
||||||
if (
|
if (
|
||||||
not tornado.options.options.unsecure and
|
not tornado.options.options.unsecure and
|
||||||
tornado.options.options.login):
|
tornado.options.options.login and not (
|
||||||
|
self.socket.local and
|
||||||
|
self.caller == self.callee and
|
||||||
|
server == self.callee
|
||||||
|
)):
|
||||||
# User is authed by ssl, setting groups
|
# User is authed by ssl, setting groups
|
||||||
try:
|
try:
|
||||||
os.initgroups(self.callee.name, self.callee.gid)
|
os.initgroups(self.callee.name, self.callee.gid)
|
||||||
@@ -185,8 +191,12 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
|||||||
'if you want to log as different user\n')
|
'if you want to log as different user\n')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
args = [tornado.options.options.shell or self.callee.shell]
|
if tornado.options.options.cmd:
|
||||||
args.append('-i')
|
args = tornado.options.options.cmd.split(' ')
|
||||||
|
else:
|
||||||
|
args = [tornado.options.options.shell or self.callee.shell]
|
||||||
|
args.append('-i')
|
||||||
|
|
||||||
os.execvpe(args[0], args, env)
|
os.execvpe(args[0], args, env)
|
||||||
# This process has been replaced
|
# This process has been replaced
|
||||||
|
|
||||||
@@ -239,6 +249,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
|||||||
self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR)
|
self.fd, self.shell_handler, ioloop.READ | ioloop.ERROR)
|
||||||
|
|
||||||
def open(self, user, path):
|
def open(self, user, path):
|
||||||
|
self.fd = None
|
||||||
if self.request.headers['Origin'] not in (
|
if self.request.headers['Origin'] not in (
|
||||||
'http://%s' % self.request.headers['Host'],
|
'http://%s' % self.request.headers['Host'],
|
||||||
'https://%s' % self.request.headers['Host']):
|
'https://%s' % self.request.headers['Host']):
|
||||||
@@ -273,7 +284,7 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
|||||||
if not self.callee and not self.user and self.socket.local:
|
if not self.callee and not self.user and self.socket.local:
|
||||||
self.callee = self.caller
|
self.callee = self.caller
|
||||||
else:
|
else:
|
||||||
user = utils.parse_cert(self.request.get_ssl_certificate())
|
user = utils.parse_cert(self.stream.socket.getpeercert())
|
||||||
assert user, 'No user in certificate'
|
assert user, 'No user in certificate'
|
||||||
self.user = user
|
self.user = user
|
||||||
try:
|
try:
|
||||||
@@ -281,6 +292,8 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
|||||||
except LookupError:
|
except LookupError:
|
||||||
raise Exception('Invalid user in certificate')
|
raise Exception('Invalid user in certificate')
|
||||||
|
|
||||||
|
TermWebSocket.terminals.add(self)
|
||||||
|
|
||||||
self.write_message(motd(self.socket))
|
self.write_message(motd(self.socket))
|
||||||
self.pty()
|
self.pty()
|
||||||
|
|
||||||
@@ -319,7 +332,8 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
|||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def on_close(self):
|
def on_close(self):
|
||||||
self.log.info('Closing fd %d' % self.fd)
|
if self.fd is not None:
|
||||||
|
self.log.info('Closing fd %d' % self.fd)
|
||||||
|
|
||||||
if getattr(self, 'pid', 0) == 0:
|
if getattr(self, 'pid', 0) == 0:
|
||||||
self.log.info('pid is 0')
|
self.log.info('pid is 0')
|
||||||
@@ -341,4 +355,9 @@ class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.log.debug('waitpid fail', exc_info=True)
|
self.log.debug('waitpid fail', exc_info=True)
|
||||||
|
|
||||||
|
TermWebSocket.terminals.remove(self)
|
||||||
self.log.info('Websocket closed')
|
self.log.info('Websocket closed')
|
||||||
|
|
||||||
|
if self.application.systemd and not len(TermWebSocket.terminals):
|
||||||
|
self.log.info('No more terminals, exiting...')
|
||||||
|
sys.exit(0)
|
||||||
|
|||||||
@@ -25,15 +25,23 @@ $shadow-alpha: .5 !default
|
|||||||
|
|
||||||
&.bell
|
&.bell
|
||||||
-webkit-filter: blur(2px)
|
-webkit-filter: blur(2px)
|
||||||
|
filter: blur(2px)
|
||||||
|
|
||||||
&.skip
|
&.skip
|
||||||
-webkit-filter: sepia(1)
|
-webkit-filter: sepia(1)
|
||||||
|
filter: sepia(1)
|
||||||
|
|
||||||
&.selection
|
&.selection
|
||||||
-webkit-filter: unquote("saturate(2)")
|
-webkit-filter: unquote("saturate(2)")
|
||||||
|
filter: unquote("saturate(2)")
|
||||||
|
|
||||||
|
&.alarm
|
||||||
|
-webkit-filter: hue-rotate(150deg)
|
||||||
|
filter: hue-rotate(150deg)
|
||||||
|
|
||||||
&.dead
|
&.dead
|
||||||
-webkit-filter: unquote("grayscale(1)")
|
-webkit-filter: unquote("grayscale(1)")
|
||||||
|
filter: unquote("grayscale(1)")
|
||||||
|
|
||||||
&:after
|
&:after
|
||||||
content: "CLOSED"
|
content: "CLOSED"
|
||||||
@@ -49,3 +57,9 @@ $shadow-alpha: .5 !default
|
|||||||
transform: rotate(-45deg)
|
transform: rotate(-45deg)
|
||||||
opacity: .2
|
opacity: .2
|
||||||
font-weight: 900
|
font-weight: 900
|
||||||
|
|
||||||
|
&.copied
|
||||||
|
transform: scale(1.05)
|
||||||
|
|
||||||
|
&.pasted
|
||||||
|
transform: scale(.95)
|
||||||
@@ -23,9 +23,11 @@ html, body
|
|||||||
|
|
||||||
#wrapper
|
#wrapper
|
||||||
height: 100%
|
height: 100%
|
||||||
overflow-x: hidden
|
overflow: hidden
|
||||||
overflow-y: auto
|
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
|
|
||||||
|
[data-native-scroll="yes"]
|
||||||
|
overflow-y: auto
|
||||||
|
|
||||||
.terminal
|
.terminal
|
||||||
outline: none
|
outline: none
|
||||||
@@ -38,3 +38,9 @@ $bg: #000 !default
|
|||||||
.blur .cursor.reverse-video
|
.blur .cursor.reverse-video
|
||||||
background: none
|
background: none
|
||||||
|
|
||||||
|
.nbsp
|
||||||
|
@extend .underline
|
||||||
|
@extend .fg-color-1
|
||||||
|
|
||||||
|
.inline-html
|
||||||
|
overflow: hidden
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
/* You should have received a copy of the GNU General Public License */
|
/* You should have received a copy of the GNU General Public License */
|
||||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
@import 'font'
|
@import font
|
||||||
@import 'layout'
|
@import layout
|
||||||
@import 'fx'
|
@import fx
|
||||||
@import 'colors'
|
@import colors
|
||||||
@import '16_colors'
|
@import 16_colors
|
||||||
@import '256_colors'
|
@import 256_colors
|
||||||
@import 'cursor'
|
@import cursor
|
||||||
@import 'term_styles'
|
@import term_styles
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
@include termcolor(0, #2e3436);
|
|
||||||
@include termcolor(1, #cc0000);
|
|
||||||
@include termcolor(2, #4e9a06);
|
|
||||||
@include termcolor(3, #c4a000);
|
|
||||||
@include termcolor(4, #3465a4);
|
|
||||||
@include termcolor(5, #75507b);
|
|
||||||
@include termcolor(6, #06989a);
|
|
||||||
@include termcolor(7, #d3d7cf);
|
|
||||||
@include termcolor(8, #555753);
|
|
||||||
@include termcolor(9, #ef2929);
|
|
||||||
@include termcolor(10, #8ae234);
|
|
||||||
@include termcolor(11, #fce94f);
|
|
||||||
@include termcolor(12, #729fcf);
|
|
||||||
@include termcolor(13, #ad7fa8);
|
|
||||||
@include termcolor(14, #34e2e2);
|
|
||||||
@include termcolor(15, #eeeeec);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
$fg: #fff !default;
|
|
||||||
$bg: #000 !default;
|
|
||||||
$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);
|
|
||||||
@include termcolor($i + 16, rgb($r, $g, $b));}
|
|
||||||
@for $i from 0 through 23{
|
|
||||||
$l: 8 + $i * 10;
|
|
||||||
@include termcolor($i + 232, rgb($l, $l, $l));}
|
|
||||||
@include termcolor(256, $bg);
|
|
||||||
@include termcolor(257, $fg);
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
$shadow: 0 !default;
|
|
||||||
$shadow-alpha: 0 !default;
|
|
||||||
$bg: #110f13;
|
|
||||||
$fg: #f4ead5;
|
|
||||||
#wrapper{
|
|
||||||
background-color: $bg;}
|
|
||||||
.terminal{
|
|
||||||
background-color: $bg;
|
|
||||||
color: $fg;}
|
|
||||||
@mixin 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;}
|
|
||||||
@if $shadow != 0{
|
|
||||||
text-shadow: 0 0 $shadow rgba($color, $shadow-alpha);}}}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
$fg: #fff !default;
|
|
||||||
$shadow-alpha: 0 !default;
|
|
||||||
.focus .cursor{
|
|
||||||
transition: 300ms;}
|
|
||||||
.cursor.reverse-video{
|
|
||||||
box-shadow: 0 0 $shadow-alpha $fg;}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
$weights: (ExtraLight 100) (Light 300) (Regular 400) (Medium 500) (Semibold 600) (Bold 700) (Black 900);
|
|
||||||
@each $weight in $weights{
|
|
||||||
$weight_name: nth($weight, 1);
|
|
||||||
@font-face{
|
|
||||||
font-family: "SourceCodePro";
|
|
||||||
src: url("/static/fonts/SourceCodePro-#{$weight_name}.otf") format("woff");
|
|
||||||
font-weight: nth($weight, 2);}}
|
|
||||||
body{
|
|
||||||
font-family: "SourceCodePro";
|
|
||||||
line-height: 1.2;}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
$fg: #fff !default;
|
|
||||||
$shadow: 6px !default;
|
|
||||||
$shadow-alpha: .5 !default;
|
|
||||||
.terminal{
|
|
||||||
text-shadow: 0 0 $shadow rgba($fg, $shadow-alpha);
|
|
||||||
transition: 200ms;
|
|
||||||
&.bell{
|
|
||||||
-webkit-filter: blur(2px);}
|
|
||||||
&.skip{
|
|
||||||
-webkit-filter: sepia(1);}
|
|
||||||
&.selection{
|
|
||||||
-webkit-filter: unquote("saturate(2)");}
|
|
||||||
&.dead{
|
|
||||||
-webkit-filter: unquote("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;}}}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
$fg: #fff !default;
|
|
||||||
$bg: #000 !default;
|
|
||||||
.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;}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
@import 'font';
|
|
||||||
@import 'layout';
|
|
||||||
@import 'fx';
|
|
||||||
@import 'colors';
|
|
||||||
@import '16_colors';
|
|
||||||
@import '256_colors';
|
|
||||||
@import 'cursor';
|
|
||||||
@import 'term_styles';
|
|
||||||
475
butterfly/static/ext.js
Normal file
475
butterfly/static/ext.js
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
(function() {
|
||||||
|
var Selection, alt, cancel, copy, ctrl, first, next_leaf, previous_leaf, selection, set_alarm, virtual_input,
|
||||||
|
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; };
|
||||||
|
|
||||||
|
set_alarm = function(notification) {
|
||||||
|
var alarm;
|
||||||
|
alarm = function(data) {
|
||||||
|
var note;
|
||||||
|
butterfly.element.classList.remove('alarm');
|
||||||
|
note = "New activity on butterfly terminal [" + butterfly.title + "]";
|
||||||
|
if (notification) {
|
||||||
|
new Notification(note, {
|
||||||
|
body: data.data,
|
||||||
|
icon: '/static/images/favicon.png'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert(note + '\n' + data.data);
|
||||||
|
}
|
||||||
|
return butterfly.ws.removeEventListener('message', alarm);
|
||||||
|
};
|
||||||
|
butterfly.ws.addEventListener('message', alarm);
|
||||||
|
return butterfly.element.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) {
|
||||||
|
if (!(e.altKey && e.keyCode === 65)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Notification && Notification.permission === 'default') {
|
||||||
|
Notification.requestPermission(function() {
|
||||||
|
return set_alarm(Notification.permission === 'granted');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
set_alarm(Notification.permission === 'granted');
|
||||||
|
}
|
||||||
|
return cancel(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('copy', copy = function(e) {
|
||||||
|
var data, end, j, len1, line, ref, sel;
|
||||||
|
butterfly.bell("copied");
|
||||||
|
e.clipboardData.clearData();
|
||||||
|
sel = getSelection().toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ');
|
||||||
|
data = '';
|
||||||
|
ref = sel.split('\n');
|
||||||
|
for (j = 0, len1 = ref.length; j < len1; j++) {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('paste', function(e) {
|
||||||
|
var data;
|
||||||
|
butterfly.bell("pasted");
|
||||||
|
data = e.clipboardData.getData('text/plain');
|
||||||
|
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r');
|
||||||
|
butterfly.send(data);
|
||||||
|
return e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
selection = null;
|
||||||
|
|
||||||
|
cancel = function(ev) {
|
||||||
|
if (ev.preventDefault) {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
if (ev.stopPropagation) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
ev.cancelBubble = true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
previous_leaf = 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
next_leaf = function(node) {
|
||||||
|
var next;
|
||||||
|
next = node.nextSibling;
|
||||||
|
if (!next) {
|
||||||
|
next = node.parentNode.nextSibling;
|
||||||
|
}
|
||||||
|
if (!next) {
|
||||||
|
next = node.parentNode.parentNode.nextSibling;
|
||||||
|
}
|
||||||
|
while (next.firstChild) {
|
||||||
|
next = next.firstChild;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
Selection = (function() {
|
||||||
|
function Selection() {
|
||||||
|
butterfly.element.classList.add('selection');
|
||||||
|
this.selection = getSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection.prototype.reset = function() {
|
||||||
|
var fake_range, ref, results;
|
||||||
|
this.selection = getSelection();
|
||||||
|
fake_range = document.createRange();
|
||||||
|
fake_range.setStart(this.selection.anchorNode, this.selection.anchorOffset);
|
||||||
|
fake_range.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 (fake_range.collapsed) {
|
||||||
|
ref = [this.end, this.start], this.start = ref[0], this.end = ref[1];
|
||||||
|
}
|
||||||
|
this.start_line = this.start.node;
|
||||||
|
while (!this.start_line.classList || indexOf.call(this.start_line.classList, 'line') < 0) {
|
||||||
|
this.start_line = this.start_line.parentNode;
|
||||||
|
}
|
||||||
|
this.end_line = this.end.node;
|
||||||
|
results = [];
|
||||||
|
while (!this.end_line.classList || indexOf.call(this.end_line.classList, 'line') < 0) {
|
||||||
|
results.push(this.end_line = this.end_line.parentNode);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
Selection.prototype.clear = function() {
|
||||||
|
return this.selection.removeAllRanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
Selection.prototype.destroy = function() {
|
||||||
|
butterfly.element.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 = butterfly.children.indexOf(this.start_line) + n;
|
||||||
|
if (!((0 <= index && index < butterfly.children.length))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (!butterfly.children[index].textContent.match(/\S/)) {
|
||||||
|
index += n;
|
||||||
|
if (!((0 <= index && index < butterfly.children.length))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.select_line(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.select_line = function(index) {
|
||||||
|
var line, line_end, line_start;
|
||||||
|
line = butterfly.children[index];
|
||||||
|
line_start = {
|
||||||
|
node: line.firstChild,
|
||||||
|
offset: 0
|
||||||
|
};
|
||||||
|
line_end = {
|
||||||
|
node: line.lastChild,
|
||||||
|
offset: line.lastChild.textContent.length
|
||||||
|
};
|
||||||
|
this.start = this.walk(line_start, /\S/);
|
||||||
|
return this.end = this.walk(line_end, /\S/, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
Selection.prototype.collapsed = function(start, end) {
|
||||||
|
var fake_range;
|
||||||
|
fake_range = document.createRange();
|
||||||
|
fake_range.setStart(start.node, start.offset);
|
||||||
|
fake_range.setEnd(end.node, end.offset);
|
||||||
|
return fake_range.collapsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
Selection.prototype.shrink_right = 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.shrink_left = 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.expand_right = function() {
|
||||||
|
var node;
|
||||||
|
node = this.walk(this.end, /\S/);
|
||||||
|
return this.end = this.walk(node, /\s/);
|
||||||
|
};
|
||||||
|
|
||||||
|
Selection.prototype.expand_left = 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.textContent;
|
||||||
|
i = needle.offset;
|
||||||
|
if (backward) {
|
||||||
|
while (node) {
|
||||||
|
while (i > 0) {
|
||||||
|
if (text[--i].match(til)) {
|
||||||
|
return {
|
||||||
|
node: node,
|
||||||
|
offset: i + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = previous_leaf(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 = next_leaf(node);
|
||||||
|
text = node.textContent;
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return needle;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Selection;
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
var 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.shrink_left();
|
||||||
|
} else if (e.keyCode === 38) {
|
||||||
|
selection.expand_left();
|
||||||
|
} else if (e.keyCode === 37) {
|
||||||
|
selection.shrink_right();
|
||||||
|
} else if (e.keyCode === 40) {
|
||||||
|
selection.expand_right();
|
||||||
|
} else {
|
||||||
|
return cancel(e);
|
||||||
|
}
|
||||||
|
if (selection != null) {
|
||||||
|
selection.apply();
|
||||||
|
}
|
||||||
|
return cancel(e);
|
||||||
|
}
|
||||||
|
if (!selection && e.ctrlKey && e.shiftKey && e.keyCode === 38) {
|
||||||
|
selection = new Selection();
|
||||||
|
selection.select_line(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, new_range, 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();
|
||||||
|
new_range = document.createRange();
|
||||||
|
new_range.setStart(sel.focusNode, sel.focusOffset);
|
||||||
|
new_range.setEnd(sel.anchorNode, sel.anchorOffset);
|
||||||
|
sel.addRange(new_range);
|
||||||
|
}
|
||||||
|
range.detach();
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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', function() {
|
||||||
|
return setTimeout(((function(_this) {
|
||||||
|
return function() {
|
||||||
|
return _this.focus();
|
||||||
|
};
|
||||||
|
})(this)), 10);
|
||||||
|
});
|
||||||
|
addEventListener('click', function() {
|
||||||
|
return virtual_input.focus();
|
||||||
|
});
|
||||||
|
addEventListener('touchstart', function(e) {
|
||||||
|
if (e.touches.length === 2) {
|
||||||
|
return ctrl = true;
|
||||||
|
} else if (e.touches.length === 3) {
|
||||||
|
ctrl = false;
|
||||||
|
return alt = true;
|
||||||
|
} else if (e.touches.length === 4) {
|
||||||
|
ctrl = true;
|
||||||
|
return alt = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtual_input.addEventListener('keydown', function(e) {
|
||||||
|
butterfly.keyDown(e);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
virtual_input.addEventListener('input', function(e) {
|
||||||
|
var len;
|
||||||
|
len = this.value.length;
|
||||||
|
if (len === 0) {
|
||||||
|
e.keyCode = 8;
|
||||||
|
butterfly.keyDown(e);
|
||||||
|
this.value = '0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
e.keyCode = this.value.charAt(1).charCodeAt(0);
|
||||||
|
if ((ctrl || alt) && !first) {
|
||||||
|
e.keyCode = this.value.charAt(1).charCodeAt(0);
|
||||||
|
e.ctrlKey = ctrl;
|
||||||
|
e.altKey = alt;
|
||||||
|
if (e.keyCode >= 97 && e.keyCode <= 122) {
|
||||||
|
e.keyCode -= 32;
|
||||||
|
}
|
||||||
|
butterfly.keyDown(e);
|
||||||
|
this.value = '0';
|
||||||
|
ctrl = alt = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
butterfly.keyPress(e);
|
||||||
|
first = false;
|
||||||
|
this.value = '0';
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}).call(this);
|
||||||
|
|
||||||
|
//# sourceMappingURL=ext.js.map
|
||||||
4
butterfly/static/ext.min.js
vendored
Normal file
4
butterfly/static/ext.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,3 +1,29 @@
|
|||||||
|
/* *-* 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/>. */
|
||||||
|
/* *-* 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/>. */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "SourceCodePro";
|
font-family: "SourceCodePro";
|
||||||
src: url("/static/fonts/SourceCodePro-ExtraLight.otf") format("woff");
|
src: url("/static/fonts/SourceCodePro-ExtraLight.otf") format("woff");
|
||||||
@@ -37,6 +63,19 @@ body {
|
|||||||
font-family: "SourceCodePro";
|
font-family: "SourceCodePro";
|
||||||
line-height: 1.2; }
|
line-height: 1.2; }
|
||||||
|
|
||||||
|
/* *-* 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/>. */
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -45,24 +84,45 @@ html, body {
|
|||||||
|
|
||||||
#wrapper {
|
#wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
|
||||||
white-space: nowrap; }
|
white-space: nowrap; }
|
||||||
|
#wrapper [data-native-scroll="yes"] {
|
||||||
|
overflow-y: auto; }
|
||||||
|
|
||||||
.terminal {
|
.terminal {
|
||||||
outline: none; }
|
outline: none; }
|
||||||
|
|
||||||
|
/* *-* 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/>. */
|
||||||
.terminal {
|
.terminal {
|
||||||
text-shadow: 0 0 6px rgba(255, 255, 255, 0.5);
|
text-shadow: 0 0 6px rgba(255, 255, 255, 0.5);
|
||||||
transition: 200ms; }
|
transition: 200ms; }
|
||||||
.terminal.bell {
|
.terminal.bell {
|
||||||
-webkit-filter: blur(2px); }
|
-webkit-filter: blur(2px);
|
||||||
|
filter: blur(2px); }
|
||||||
.terminal.skip {
|
.terminal.skip {
|
||||||
-webkit-filter: sepia(1); }
|
-webkit-filter: sepia(1);
|
||||||
|
filter: sepia(1); }
|
||||||
.terminal.selection {
|
.terminal.selection {
|
||||||
-webkit-filter: saturate(2); }
|
-webkit-filter: saturate(2);
|
||||||
|
filter: saturate(2); }
|
||||||
|
.terminal.alarm {
|
||||||
|
-webkit-filter: hue-rotate(150deg);
|
||||||
|
filter: hue-rotate(150deg); }
|
||||||
.terminal.dead {
|
.terminal.dead {
|
||||||
-webkit-filter: grayscale(1); }
|
-webkit-filter: grayscale(1);
|
||||||
|
filter: grayscale(1); }
|
||||||
.terminal.dead:after {
|
.terminal.dead:after {
|
||||||
content: "CLOSED";
|
content: "CLOSED";
|
||||||
font-size: 15em;
|
font-size: 15em;
|
||||||
@@ -75,9 +135,26 @@ html, body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
opacity: 0.2;
|
opacity: .2;
|
||||||
font-weight: 900; }
|
font-weight: 900; }
|
||||||
|
.terminal.copied {
|
||||||
|
transform: scale(1.05); }
|
||||||
|
.terminal.pasted {
|
||||||
|
transform: scale(.95); }
|
||||||
|
|
||||||
|
/* *-* 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/>. */
|
||||||
#wrapper {
|
#wrapper {
|
||||||
background-color: #110f13; }
|
background-color: #110f13; }
|
||||||
|
|
||||||
@@ -85,6 +162,20 @@ html, body {
|
|||||||
background-color: #110f13;
|
background-color: #110f13;
|
||||||
color: #f4ead5; }
|
color: #f4ead5; }
|
||||||
|
|
||||||
|
/* *-* 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/>. */
|
||||||
|
/* Here are the 16 "normal" colors for theming */
|
||||||
.bg-color-0 {
|
.bg-color-0 {
|
||||||
background-color: #2e3436; }
|
background-color: #2e3436; }
|
||||||
.bg-color-0.reverse-video {
|
.bg-color-0.reverse-video {
|
||||||
@@ -101,10 +192,10 @@ html, body {
|
|||||||
.bg-color-1.reverse-video {
|
.bg-color-1.reverse-video {
|
||||||
color: #cc0000 !important; }
|
color: #cc0000 !important; }
|
||||||
|
|
||||||
.fg-color-1 {
|
.fg-color-1, .nbsp {
|
||||||
color: #cc0000;
|
color: #cc0000;
|
||||||
text-shadow: 0 0 6px rgba(204, 0, 0, 0.5); }
|
text-shadow: 0 0 6px rgba(204, 0, 0, 0.5); }
|
||||||
.fg-color-1.reverse-video {
|
.fg-color-1.reverse-video, .reverse-video.nbsp {
|
||||||
background-color: #cc0000 !important; }
|
background-color: #cc0000 !important; }
|
||||||
|
|
||||||
.bg-color-2 {
|
.bg-color-2 {
|
||||||
@@ -261,6 +352,21 @@ html, body {
|
|||||||
.fg-color-15.reverse-video {
|
.fg-color-15.reverse-video {
|
||||||
background-color: #eeeeec !important; }
|
background-color: #eeeeec !important; }
|
||||||
|
|
||||||
|
/* *-* 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/>. */
|
||||||
|
/* Here are the 240 xterm colors */
|
||||||
|
/* See http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg */
|
||||||
.bg-color-16 {
|
.bg-color-16 {
|
||||||
background-color: black; }
|
background-color: black; }
|
||||||
.bg-color-16.reverse-video {
|
.bg-color-16.reverse-video {
|
||||||
@@ -2770,15 +2876,15 @@ html, body {
|
|||||||
background-color: #767676 !important; }
|
background-color: #767676 !important; }
|
||||||
|
|
||||||
.bg-color-244 {
|
.bg-color-244 {
|
||||||
background-color: grey; }
|
background-color: gray; }
|
||||||
.bg-color-244.reverse-video {
|
.bg-color-244.reverse-video {
|
||||||
color: grey !important; }
|
color: gray !important; }
|
||||||
|
|
||||||
.fg-color-244 {
|
.fg-color-244 {
|
||||||
color: grey;
|
color: gray;
|
||||||
text-shadow: 0 0 6px rgba(128, 128, 128, 0.5); }
|
text-shadow: 0 0 6px rgba(128, 128, 128, 0.5); }
|
||||||
.fg-color-244.reverse-video {
|
.fg-color-244.reverse-video {
|
||||||
background-color: grey !important; }
|
background-color: gray !important; }
|
||||||
|
|
||||||
.bg-color-245 {
|
.bg-color-245 {
|
||||||
background-color: #8a8a8a; }
|
background-color: #8a8a8a; }
|
||||||
@@ -2923,16 +3029,42 @@ html, body {
|
|||||||
.fg-color-257.reverse-video {
|
.fg-color-257.reverse-video {
|
||||||
background-color: #f4ead5 !important; }
|
background-color: #f4ead5 !important; }
|
||||||
|
|
||||||
|
/* *-* 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/>. */
|
||||||
.focus .cursor {
|
.focus .cursor {
|
||||||
transition: 300ms; }
|
transition: 300ms; }
|
||||||
|
|
||||||
.cursor.reverse-video {
|
.cursor.reverse-video {
|
||||||
box-shadow: 0 0 0.5 #f4ead5; }
|
box-shadow: 0 0 0.5 #f4ead5; }
|
||||||
|
|
||||||
|
/* *-* 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/>. */
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: bold; }
|
font-weight: bold; }
|
||||||
|
|
||||||
.underline {
|
.underline, .nbsp {
|
||||||
text-decoration: underline; }
|
text-decoration: underline; }
|
||||||
|
|
||||||
.blink {
|
.blink {
|
||||||
@@ -2947,3 +3079,6 @@ html, body {
|
|||||||
|
|
||||||
.blur .cursor.reverse-video {
|
.blur .cursor.reverse-video {
|
||||||
background: none; }
|
background: none; }
|
||||||
|
|
||||||
|
.inline-html {
|
||||||
|
overflow: hidden; }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
8
butterfly/static/main.min.js
vendored
8
butterfly/static/main.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -13,9 +13,13 @@
|
|||||||
<link href="/style.css" rel="stylesheet">
|
<link href="/style.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body spellcheck="false">
|
<body spellcheck="false"
|
||||||
|
data-allow-html="{{ 'yes' if options.allow_html_escapes else 'no' }}"
|
||||||
|
data-native-scroll="{{ 'yes' if options.native_scroll else 'no' }}">
|
||||||
<main id="wrapper"> </main>
|
<main id="wrapper"> </main>
|
||||||
<script src="{{ static_url('main.%sjs' % (
|
<script src="{{ static_url('main.%sjs' % (
|
||||||
'' if options.unminified else 'min.')) }}"></script>
|
'' if options.unminified else 'min.')) }}"></script>
|
||||||
|
<script src="{{ static_url('ext.%sjs' % (
|
||||||
|
'' if options.unminified else 'min.')) }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -37,15 +37,9 @@ def get_style():
|
|||||||
if style is None:
|
if style is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if style.endswith('.sass'):
|
if style.endswith('.scss') or style.endswith('.sass'):
|
||||||
log.error('SASS syntax is not yet supported (see: '
|
sass_path = os.path.join(
|
||||||
'https://github.com/hcatlin/libsass/issues/16'
|
os.path.dirname(__file__), 'sass')
|
||||||
') please use SCSS')
|
|
||||||
return
|
|
||||||
|
|
||||||
if style.endswith('.scss'):
|
|
||||||
scss_path = os.path.join(
|
|
||||||
os.path.dirname(__file__), 'scss')
|
|
||||||
try:
|
try:
|
||||||
import sass
|
import sass
|
||||||
except:
|
except:
|
||||||
@@ -54,9 +48,11 @@ def get_style():
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return sass.compile(filename=style, include_paths=[scss_path])
|
return sass.compile(filename=style, include_paths=[sass_path])
|
||||||
except sass.CompileError:
|
except sass.CompileError:
|
||||||
log.error('Unable to compile style.scss', exc_info=True)
|
log.error(
|
||||||
|
'Unable to compile style.scss (filename: %s, paths: %r) ' % (
|
||||||
|
style, [sass_path]), exc_info=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(style) as s:
|
with open(style) as s:
|
||||||
|
|||||||
36
coffees/ext/alarm.coffee
Normal file
36
coffees/ext/alarm.coffee
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
set_alarm = (notification) ->
|
||||||
|
alarm = (data) ->
|
||||||
|
butterfly.element.classList.remove 'alarm'
|
||||||
|
note = "New activity on butterfly terminal [#{ butterfly.title }]"
|
||||||
|
|
||||||
|
if notification
|
||||||
|
new Notification(
|
||||||
|
note,
|
||||||
|
body: data.data,
|
||||||
|
icon: '/static/images/favicon.png')
|
||||||
|
else
|
||||||
|
alert(note + '\n' + data.data)
|
||||||
|
|
||||||
|
butterfly.ws.removeEventListener 'message', alarm
|
||||||
|
|
||||||
|
butterfly.ws.addEventListener 'message', alarm
|
||||||
|
butterfly.element.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
|
||||||
|
|
||||||
|
if Notification and Notification.permission is 'default'
|
||||||
|
Notification.requestPermission ->
|
||||||
|
set_alarm(Notification.permission is 'granted')
|
||||||
|
else
|
||||||
|
set_alarm(Notification.permission is 'granted')
|
||||||
|
|
||||||
|
cancel(e)
|
||||||
41
coffees/ext/clipboard.coffee
Normal file
41
coffees/ext/clipboard.coffee
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# *-* 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/>.
|
||||||
|
|
||||||
|
document.addEventListener 'copy', copy = (e) ->
|
||||||
|
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()
|
||||||
|
|
||||||
|
document.addEventListener 'paste', (e) ->
|
||||||
|
butterfly.bell "pasted"
|
||||||
|
data = e.clipboardData.getData 'text/plain'
|
||||||
|
data = data.replace(/\r\n/g, '\n').replace(/\n/g, '\r')
|
||||||
|
butterfly.send data
|
||||||
|
e.preventDefault()
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
selection = null
|
selection = null
|
||||||
|
|
||||||
|
cancel = (ev) ->
|
||||||
|
ev.preventDefault() if ev.preventDefault
|
||||||
|
ev.stopPropagation() if ev.stopPropagation
|
||||||
|
ev.cancelBubble = true
|
||||||
|
false
|
||||||
|
|
||||||
previous_leaf = (node) ->
|
previous_leaf = (node) ->
|
||||||
previous = node.previousSibling
|
previous = node.previousSibling
|
||||||
if not previous
|
if not previous
|
||||||
@@ -38,7 +44,7 @@ next_leaf = (node) ->
|
|||||||
|
|
||||||
class Selection
|
class Selection
|
||||||
constructor: ->
|
constructor: ->
|
||||||
term.element.classList.add('selection')
|
butterfly.element.classList.add('selection')
|
||||||
@selection = getSelection()
|
@selection = getSelection()
|
||||||
|
|
||||||
reset: ->
|
reset: ->
|
||||||
@@ -68,11 +74,11 @@ class Selection
|
|||||||
@selection.removeAllRanges()
|
@selection.removeAllRanges()
|
||||||
|
|
||||||
destroy: ->
|
destroy: ->
|
||||||
term.element.classList.remove('selection')
|
butterfly.element.classList.remove('selection')
|
||||||
@clear()
|
@clear()
|
||||||
|
|
||||||
text: ->
|
text: ->
|
||||||
@selection.toString()
|
@selection.toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ')
|
||||||
|
|
||||||
up: ->
|
up: ->
|
||||||
@go -1
|
@go -1
|
||||||
@@ -81,12 +87,12 @@ class Selection
|
|||||||
@go +1
|
@go +1
|
||||||
|
|
||||||
go: (n) ->
|
go: (n) ->
|
||||||
index = term.children.indexOf(@start_line) + n
|
index = butterfly.children.indexOf(@start_line) + n
|
||||||
return unless 0 <= index < term.children.length
|
return unless 0 <= index < butterfly.children.length
|
||||||
|
|
||||||
until term.children[index].textContent.match /\S/
|
until butterfly.children[index].textContent.match /\S/
|
||||||
index += n
|
index += n
|
||||||
return unless 0 <= index < term.children.length
|
return unless 0 <= index < butterfly.children.length
|
||||||
|
|
||||||
@select_line index
|
@select_line index
|
||||||
|
|
||||||
@@ -98,7 +104,7 @@ class Selection
|
|||||||
@selection.addRange range
|
@selection.addRange range
|
||||||
|
|
||||||
select_line: (index) ->
|
select_line: (index) ->
|
||||||
line = term.children[index]
|
line = butterfly.children[index]
|
||||||
line_start =
|
line_start =
|
||||||
node: line.firstChild
|
node: line.firstChild
|
||||||
offset: 0
|
offset: 0
|
||||||
@@ -170,7 +176,7 @@ document.addEventListener 'keydown', (e) ->
|
|||||||
# Paste natural selection too if shiftkey
|
# Paste natural selection too if shiftkey
|
||||||
if e.shiftKey and e.keyCode is 13 and
|
if e.shiftKey and e.keyCode is 13 and
|
||||||
not selection and not getSelection().isCollapsed
|
not selection and not getSelection().isCollapsed
|
||||||
term.handler getSelection().toString()
|
butterfly.send getSelection().toString()
|
||||||
getSelection().removeAllRanges()
|
getSelection().removeAllRanges()
|
||||||
return cancel e
|
return cancel e
|
||||||
|
|
||||||
@@ -200,7 +206,7 @@ document.addEventListener 'keydown', (e) ->
|
|||||||
# Start selection mode with shift up
|
# Start selection mode with shift up
|
||||||
if not selection and e.ctrlKey and e.shiftKey and e.keyCode == 38
|
if not selection and e.ctrlKey and e.shiftKey and e.keyCode == 38
|
||||||
selection = new Selection()
|
selection = new Selection()
|
||||||
selection.select_line term.y - 1
|
selection.select_line butterfly.y - 1
|
||||||
selection.apply()
|
selection.apply()
|
||||||
return cancel e
|
return cancel e
|
||||||
true
|
true
|
||||||
@@ -210,7 +216,7 @@ document.addEventListener 'keyup', (e) ->
|
|||||||
|
|
||||||
if selection
|
if selection
|
||||||
if e.keyCode == 13
|
if e.keyCode == 13
|
||||||
term.handler selection.text()
|
butterfly.send selection.text()
|
||||||
selection.destroy()
|
selection.destroy()
|
||||||
selection = null
|
selection = null
|
||||||
return cancel e
|
return cancel e
|
||||||
@@ -49,7 +49,7 @@ if /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
|
|||||||
alt = true
|
alt = true
|
||||||
|
|
||||||
virtual_input.addEventListener 'keydown', (e) ->
|
virtual_input.addEventListener 'keydown', (e) ->
|
||||||
term.keyDown(e)
|
butterfly.keyDown(e)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
virtual_input.addEventListener 'input', (e) ->
|
virtual_input.addEventListener 'input', (e) ->
|
||||||
@@ -57,7 +57,7 @@ if /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
|
|||||||
|
|
||||||
if len == 0
|
if len == 0
|
||||||
e.keyCode = 8
|
e.keyCode = 8
|
||||||
term.keyDown e
|
butterfly.keyDown e
|
||||||
@value = '0'
|
@value = '0'
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -69,12 +69,12 @@ if /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
|
|||||||
e.altKey = alt
|
e.altKey = alt
|
||||||
if e.keyCode >= 97 && e.keyCode <= 122
|
if e.keyCode >= 97 && e.keyCode <= 122
|
||||||
e.keyCode -= 32
|
e.keyCode -= 32
|
||||||
term.keyDown e
|
butterfly.keyDown e
|
||||||
@value = '0'
|
@value = '0'
|
||||||
ctrl = alt = false
|
ctrl = alt = false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
term.keyPress e
|
butterfly.keyPress e
|
||||||
first = false
|
first = false
|
||||||
@value = '0'
|
@value = '0'
|
||||||
true
|
true
|
||||||
@@ -21,72 +21,74 @@ open_ts = (new Date()).getTime()
|
|||||||
|
|
||||||
$ = document.querySelectorAll.bind(document)
|
$ = document.querySelectorAll.bind(document)
|
||||||
|
|
||||||
send = (data) ->
|
document.addEventListener 'DOMContentLoaded', ->
|
||||||
ws.send 'S' + data
|
|
||||||
|
|
||||||
ctl = (type, args...) ->
|
send = (data) ->
|
||||||
params = args.join(',')
|
ws.send 'S' + data
|
||||||
if type == 'Resize'
|
|
||||||
ws.send 'R' + params
|
|
||||||
|
|
||||||
if location.protocol == 'https:'
|
ctl = (type, args...) ->
|
||||||
ws_url = 'wss://'
|
params = args.join(',')
|
||||||
else
|
if type == 'Resize'
|
||||||
ws_url = 'ws://'
|
ws.send 'R' + params
|
||||||
|
|
||||||
ws_url += document.location.host + '/ws' + location.pathname
|
if location.protocol == 'https:'
|
||||||
ws = new WebSocket ws_url
|
ws_url = 'wss://'
|
||||||
|
else
|
||||||
|
ws_url = 'ws://'
|
||||||
|
|
||||||
ws.addEventListener 'open', ->
|
ws_url += document.location.host + '/ws' + location.pathname
|
||||||
console.log "WebSocket open", arguments
|
ws = new WebSocket ws_url
|
||||||
ws.send 'R' + term.cols + ',' + term.rows
|
|
||||||
open_ts = (new Date()).getTime()
|
|
||||||
|
|
||||||
ws.addEventListener 'error', ->
|
ws.addEventListener 'open', ->
|
||||||
console.log "WebSocket error", arguments
|
console.log "WebSocket open", arguments
|
||||||
|
ws.send 'R' + term.cols + ',' + term.rows
|
||||||
|
open_ts = (new Date()).getTime()
|
||||||
|
|
||||||
ws.addEventListener 'message', (e) ->
|
ws.addEventListener 'error', ->
|
||||||
setTimeout ->
|
console.log "WebSocket error", arguments
|
||||||
term.write e.data
|
|
||||||
, 1
|
|
||||||
|
|
||||||
ws.addEventListener 'close', ->
|
ws.addEventListener 'message', (e) ->
|
||||||
console.log "WebSocket closed", arguments
|
setTimeout ->
|
||||||
setTimeout ->
|
term.write e.data
|
||||||
term.write 'Closed'
|
, 1
|
||||||
# Allow quick reload
|
|
||||||
term.skipNextKey = true
|
|
||||||
term.element.classList.add('dead')
|
|
||||||
, 1
|
|
||||||
quit = true
|
|
||||||
# Don't autoclose if websocket didn't last 1 minute
|
|
||||||
if (new Date()).getTime() - open_ts > 60 * 1000
|
|
||||||
open('','_self').close()
|
|
||||||
|
|
||||||
term = new Terminal $('#wrapper')[0], send, ctl
|
ws.addEventListener 'close', ->
|
||||||
addEventListener 'beforeunload', ->
|
console.log "WebSocket closed", arguments
|
||||||
if not quit
|
setTimeout ->
|
||||||
'This will exit the terminal session'
|
term.write 'Closed'
|
||||||
|
# Allow quick reload
|
||||||
|
term.skipNextKey = true
|
||||||
|
term.element.classList.add('dead')
|
||||||
|
, 1
|
||||||
|
quit = true
|
||||||
|
# Don't autoclose if websocket didn't last 1 minute
|
||||||
|
if (new Date()).getTime() - open_ts > 60 * 1000
|
||||||
|
open('','_self').close()
|
||||||
|
|
||||||
bench = (n=100000000) ->
|
term = new Terminal $('#wrapper')[0], send, ctl
|
||||||
rnd = ''
|
addEventListener 'beforeunload', ->
|
||||||
while rnd.length < n
|
if not quit
|
||||||
rnd += Math.random().toString(36).substring(2)
|
'This will exit the terminal session'
|
||||||
|
|
||||||
t0 = (new Date()).getTime()
|
bench = (n=100000000) ->
|
||||||
term.write rnd
|
rnd = ''
|
||||||
console.log "#{n} chars in #{(new Date()).getTime() - t0} ms"
|
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) ->
|
cbench = (n=100000000) ->
|
||||||
rnd = ''
|
rnd = ''
|
||||||
while rnd.length < n
|
while rnd.length < n
|
||||||
rnd += "\x1b[#{30 + parseInt(Math.random() * 20)}m"
|
rnd += "\x1b[#{30 + parseInt(Math.random() * 20)}m"
|
||||||
rnd += Math.random().toString(36).substring(2)
|
rnd += Math.random().toString(36).substring(2)
|
||||||
|
|
||||||
t0 = (new Date()).getTime()
|
t0 = (new Date()).getTime()
|
||||||
term.write rnd
|
term.write rnd
|
||||||
console.log "#{n} chars + colors in #{(new Date()).getTime() - t0} ms"
|
console.log "#{n} chars + colors in #{(new Date()).getTime() - t0} ms"
|
||||||
|
|
||||||
|
term.ws = ws
|
||||||
window.butterfly = term
|
window.butterfly = term
|
||||||
|
|||||||
@@ -52,12 +52,15 @@ class Terminal
|
|||||||
@context = @parent.ownerDocument.defaultView
|
@context = @parent.ownerDocument.defaultView
|
||||||
@document = @parent.ownerDocument
|
@document = @parent.ownerDocument
|
||||||
@body = @document.getElementsByTagName('body')[0]
|
@body = @document.getElementsByTagName('body')[0]
|
||||||
|
@html_escapes_enabled = @body.getAttribute('data-allow-html') is 'yes'
|
||||||
|
@native_scroll = @body.getAttribute('data-native-scroll') is 'yes'
|
||||||
|
|
||||||
# Main terminal element
|
# Main terminal element
|
||||||
@element = @document.createElement('div')
|
@element = @document.createElement('div')
|
||||||
@element.className = 'terminal focus'
|
@element.className = 'terminal focus'
|
||||||
@element.style.outline = 'none'
|
@element.style.outline = 'none'
|
||||||
@element.setAttribute 'tabindex', 0
|
@element.setAttribute 'tabindex', 0
|
||||||
|
@element.setAttribute 'spellcheck', 'false'
|
||||||
|
|
||||||
@parent.appendChild(@element)
|
@parent.appendChild(@element)
|
||||||
|
|
||||||
@@ -68,16 +71,18 @@ class Terminal
|
|||||||
@children = [div]
|
@children = [div]
|
||||||
|
|
||||||
@compute_char_size()
|
@compute_char_size()
|
||||||
|
div.style.height = @char_size.height + 'px' unless @native_scroll
|
||||||
term_size = @parent.getBoundingClientRect()
|
term_size = @parent.getBoundingClientRect()
|
||||||
@cols = Math.floor(term_size.width / @char_size.width)
|
@cols = Math.floor(term_size.width / @char_size.width)
|
||||||
@rows = Math.floor(term_size.height / @char_size.height)
|
@rows = Math.floor(term_size.height / @char_size.height)
|
||||||
@element.style['padding-bottom'] = "#{
|
px = term_size.height % @char_size.height
|
||||||
term_size.height % @char_size.height}px"
|
@element.style['padding-bottom'] = "#{px}px"
|
||||||
|
|
||||||
@html = {}
|
@html = {}
|
||||||
i = @rows - 1
|
i = @rows - 1
|
||||||
while i--
|
while i--
|
||||||
div = @document.createElement('div')
|
div = @document.createElement('div')
|
||||||
|
div.style.height = @char_size.height + 'px' unless @native_scroll
|
||||||
div.className = 'line'
|
div.className = 'line'
|
||||||
@element.appendChild(div)
|
@element.appendChild(div)
|
||||||
@children.push(div)
|
@children.push(div)
|
||||||
@@ -93,6 +98,8 @@ class Terminal
|
|||||||
@last_cc = 0
|
@last_cc = 0
|
||||||
@reset_vars()
|
@reset_vars()
|
||||||
|
|
||||||
|
@refresh 0, @rows - 1 unless @native_scroll
|
||||||
|
|
||||||
@focus()
|
@focus()
|
||||||
|
|
||||||
@startBlink()
|
@startBlink()
|
||||||
@@ -100,7 +107,6 @@ class Terminal
|
|||||||
addEventListener 'keypress', @keyPress.bind(@)
|
addEventListener 'keypress', @keyPress.bind(@)
|
||||||
addEventListener 'focus', @focus.bind(@)
|
addEventListener 'focus', @focus.bind(@)
|
||||||
addEventListener 'blur', @blur.bind(@)
|
addEventListener 'blur', @blur.bind(@)
|
||||||
addEventListener 'paste', @paste.bind(@)
|
|
||||||
addEventListener 'resize', @resize.bind(@)
|
addEventListener 'resize', @resize.bind(@)
|
||||||
|
|
||||||
# Horrible Firefox paste workaround
|
# Horrible Firefox paste workaround
|
||||||
@@ -111,19 +117,23 @@ class Terminal
|
|||||||
if sel.startOffset is sel.endOffset
|
if sel.startOffset is sel.endOffset
|
||||||
getSelection().removeAllRanges()
|
getSelection().removeAllRanges()
|
||||||
|
|
||||||
# @initmouse()
|
@initmouse() unless @native_scroll
|
||||||
|
|
||||||
setTimeout(@resize.bind(@), 100)
|
setTimeout(@resize.bind(@), 100)
|
||||||
|
|
||||||
reset_vars: ->
|
reset_vars: ->
|
||||||
# @ybase = 0
|
|
||||||
# @ydisp = 0
|
|
||||||
@x = 0
|
@x = 0
|
||||||
@y = 0
|
@y = 0
|
||||||
@cursorHidden = false
|
@cursorHidden = false
|
||||||
@state = State.normal
|
@state = State.normal
|
||||||
@queue = ''
|
@queue = ''
|
||||||
|
|
||||||
|
@ybase = 0
|
||||||
|
@ydisp = 0
|
||||||
|
unless @native_scroll
|
||||||
|
@scrollTop = 0
|
||||||
|
@scrollBottom = @rows - 1
|
||||||
|
|
||||||
# modes
|
# modes
|
||||||
@applicationKeypad = false
|
@applicationKeypad = false
|
||||||
@applicationCursor = false
|
@applicationCursor = false
|
||||||
@@ -174,13 +184,6 @@ class Terminal
|
|||||||
@element.classList.add('blur')
|
@element.classList.add('blur')
|
||||||
@element.classList.remove('focus')
|
@element.classList.remove('focus')
|
||||||
|
|
||||||
paste: (ev) ->
|
|
||||||
if ev.clipboardData
|
|
||||||
@send ev.clipboardData.getData('text/plain')
|
|
||||||
else if @context.clipboardData
|
|
||||||
@send @context.clipboardData.getData('Text')
|
|
||||||
cancel(ev)
|
|
||||||
|
|
||||||
# XTerm mouse events
|
# XTerm mouse events
|
||||||
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
|
# http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
|
||||||
# To better understand these
|
# To better understand these
|
||||||
@@ -375,30 +378,30 @@ class Terminal
|
|||||||
return if @x10Mouse
|
return if @x10Mouse
|
||||||
sendButton ev
|
sendButton ev
|
||||||
else
|
else
|
||||||
return if @applicationKeypad
|
return true if @applicationKeypad or @native_scroll
|
||||||
return true
|
@scroll_display if ev.deltaY > 0 then 5 else -5
|
||||||
cancel ev
|
cancel ev
|
||||||
|
|
||||||
|
|
||||||
refresh: (start, end) ->
|
refresh: (start, end) ->
|
||||||
if end - start >= 3
|
if not @native_scroll and end - start >= @rows / 3
|
||||||
parent = @element.parentNode
|
parent = @element.parentNode
|
||||||
parent?.removeChild @element
|
parent?.removeChild @element
|
||||||
|
|
||||||
# @missing_lines = Math.min(@missing_lines, @rows - 1)
|
if @native_scroll
|
||||||
|
if @missing_lines
|
||||||
if @missing_lines
|
for i in [1..@missing_lines]
|
||||||
for i in [1..@missing_lines]
|
@new_line()
|
||||||
@new_line()
|
@missing_lines = 0
|
||||||
@missing_lines = 0
|
|
||||||
|
|
||||||
end = Math.min(end, @screen.length - 1)
|
end = Math.min(end, @screen.length - 1)
|
||||||
|
|
||||||
for j in [start..end]
|
for j in [start..end]
|
||||||
line = @screen[j]
|
line = @screen[row + @ydisp]
|
||||||
out = ""
|
out = ""
|
||||||
|
|
||||||
if j is @y and not @cursorHidden
|
if j is @y and not @cursorHidden and (
|
||||||
|
@native_scroll or @ydisp is @ybase or @selectMode)
|
||||||
x = @x
|
x = @x
|
||||||
else
|
else
|
||||||
x = -Infinity
|
x = -Infinity
|
||||||
@@ -450,7 +453,9 @@ class Terminal
|
|||||||
when ">"
|
when ">"
|
||||||
out += ">"
|
out += ">"
|
||||||
else
|
else
|
||||||
if ch <= " "
|
if ch == " "
|
||||||
|
out += '<span class="nbsp">\u2007</span>'
|
||||||
|
else if ch <= " "
|
||||||
out += " "
|
out += " "
|
||||||
else
|
else
|
||||||
i++ if "\uff00" < ch < "\uffef"
|
i++ if "\uff00" < ch < "\uffef"
|
||||||
@@ -461,10 +466,12 @@ class Terminal
|
|||||||
@children[j].innerHTML = out
|
@children[j].innerHTML = out
|
||||||
|
|
||||||
parent?.appendChild @element
|
parent?.appendChild @element
|
||||||
for l, html of @html
|
|
||||||
@element.insertBefore(html, @children[l])
|
if @native_scroll
|
||||||
@html = {}
|
for l, html of @html
|
||||||
@parent.scrollTop = @parent.scrollHeight
|
@element.insertBefore(html, @children[l])
|
||||||
|
@html = {}
|
||||||
|
@parent.scrollTop = @parent.scrollHeight
|
||||||
|
|
||||||
|
|
||||||
_cursorBlink: ->
|
_cursorBlink: ->
|
||||||
@@ -496,15 +503,55 @@ class Terminal
|
|||||||
|
|
||||||
|
|
||||||
scroll: ->
|
scroll: ->
|
||||||
@screen.shift()
|
if @native_scroll
|
||||||
@screen.push @blank_line()
|
@screen.shift()
|
||||||
@refreshStart = Math.max(@refreshStart - 1, 0)
|
@screen.push @blank_line()
|
||||||
@missing_lines++
|
@refreshStart = Math.max(@refreshStart - 1, 0)
|
||||||
if @missing_lines >= @rows
|
@missing_lines++
|
||||||
@refresh 0, @rows - 1
|
if @missing_lines >= @rows
|
||||||
|
@refresh 0, @rows - 1
|
||||||
|
else
|
||||||
|
if ++@ybase is @scrollback
|
||||||
|
@ybase = @ybase / 2 | 0
|
||||||
|
@screen = @screen.slice(-(@ybase + @rows) + 1)
|
||||||
|
|
||||||
|
@ydisp = @ybase
|
||||||
|
|
||||||
|
# last line
|
||||||
|
row = @ybase + @rows - 1
|
||||||
|
|
||||||
|
# subtract the bottom scroll region
|
||||||
|
row -= @rows - 1 - @scrollBottom
|
||||||
|
if row is @screen.length
|
||||||
|
# potential optimization:
|
||||||
|
# pushing is faster than splicing
|
||||||
|
# when they amount to the same
|
||||||
|
# behavior.
|
||||||
|
@screen.push @blankLine()
|
||||||
|
else
|
||||||
|
# add our new line
|
||||||
|
@screen.splice row, 0, @blankLine()
|
||||||
|
|
||||||
|
if @scrollTop isnt 0
|
||||||
|
if @ybase isnt 0
|
||||||
|
@ybase--
|
||||||
|
@ydisp = @ybase
|
||||||
|
@screen.splice @ybase + @scrollTop, 1
|
||||||
|
|
||||||
|
@updateRange @scrollTop
|
||||||
|
@updateRange @scrollBottom
|
||||||
|
|
||||||
scroll_display: (disp) ->
|
scroll_display: (disp) ->
|
||||||
@parent.scrollTop += disp * @char_size.height
|
if @native_scroll
|
||||||
|
@parent.scrollTop += disp * @char_size.height
|
||||||
|
else
|
||||||
|
@ydisp += disp
|
||||||
|
if @ydisp > @ybase
|
||||||
|
@ydisp = @ybase
|
||||||
|
else
|
||||||
|
@ydisp = 0 if @ydisp < 0
|
||||||
|
|
||||||
|
@refresh 0, @rows - 1
|
||||||
|
|
||||||
new_line: ->
|
new_line: ->
|
||||||
div = @document.createElement('div')
|
div = @document.createElement('div')
|
||||||
@@ -518,7 +565,7 @@ class Terminal
|
|||||||
|
|
||||||
next_line: ->
|
next_line: ->
|
||||||
@y++
|
@y++
|
||||||
if @y >= @rows
|
if @y >= (if @native_scroll then @rows else @scrollBottom)
|
||||||
@y--
|
@y--
|
||||||
@scroll()
|
@scroll()
|
||||||
|
|
||||||
@@ -526,6 +573,11 @@ class Terminal
|
|||||||
@refreshStart = @y
|
@refreshStart = @y
|
||||||
@refreshEnd = @y
|
@refreshEnd = @y
|
||||||
|
|
||||||
|
unless @native_scroll
|
||||||
|
if @ybase isnt @ydisp
|
||||||
|
@ydisp = @ybase
|
||||||
|
@maxRange()
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
l = data.length
|
l = data.length
|
||||||
while i < l
|
while i < l
|
||||||
@@ -572,17 +624,19 @@ class Terminal
|
|||||||
if ch >= " "
|
if ch >= " "
|
||||||
ch = @charset[ch] if @charset?[ch]
|
ch = @charset[ch] if @charset?[ch]
|
||||||
if @x >= @cols
|
if @x >= @cols
|
||||||
|
@lines[@y + @ybase][@x] = [@curAttr, '\u23CE']
|
||||||
@x = 0
|
@x = 0
|
||||||
@next_line()
|
@next_line()
|
||||||
@screen[@y][@x] = [@curAttr, ch]
|
|
||||||
|
@screen[@y + @ybase][@x] = [@curAttr, ch]
|
||||||
@x++
|
@x++
|
||||||
@updateRange @y
|
@updateRange @y
|
||||||
if "\uff00" < ch < "\uffef"
|
if "\uff00" < ch < "\uffef"
|
||||||
if @cols < 2 or @x >= @cols
|
if @cols < 2 or @x >= @cols
|
||||||
@screen[@y][@x - 1] = [@curAttr, " "]
|
@screen[@y + @ybase][@x - 1] = [@curAttr, " "]
|
||||||
break
|
break
|
||||||
|
|
||||||
@screen[@y][@x] = [@curAttr, " "]
|
@screen[@y + @ybase][@x] = [@curAttr, " "]
|
||||||
@x++
|
@x++
|
||||||
|
|
||||||
when State.escaped
|
when State.escaped
|
||||||
@@ -765,20 +819,11 @@ class Terminal
|
|||||||
i++ if ch is "\x1b"
|
i++ if ch is "\x1b"
|
||||||
@params.push @currentParam
|
@params.push @currentParam
|
||||||
switch @params[0]
|
switch @params[0]
|
||||||
when 0, 1 , 2
|
when 0, 1, 2
|
||||||
if @params[1]
|
if @params[1]
|
||||||
@title = @params[1] + " - ƸӜƷ butterfly"
|
@title = @params[1] + " - ƸӜƷ butterfly"
|
||||||
@handleTitle @title
|
@handleTitle @title
|
||||||
|
|
||||||
when 99
|
|
||||||
# Custom escape to produce raw html
|
|
||||||
html = document.createElement('div')
|
|
||||||
html.innerHTML = @params[1]
|
|
||||||
@next_line()
|
|
||||||
@html[@y] = html
|
|
||||||
@updateRange @y
|
|
||||||
@next_line()
|
|
||||||
|
|
||||||
# reset colors
|
# reset colors
|
||||||
@params = []
|
@params = []
|
||||||
@currentParam = 0
|
@currentParam = 0
|
||||||
@@ -967,7 +1012,6 @@ class Terminal
|
|||||||
# CSI Ps ; Ps ; Ps ; Ps ; Ps T
|
# CSI Ps ; Ps ; Ps ; Ps ; Ps T
|
||||||
# CSI > Ps; Ps T
|
# CSI > Ps; Ps T
|
||||||
when "T"
|
when "T"
|
||||||
""
|
|
||||||
@scrollDown @params if @params.length < 2 and not @prefix
|
@scrollDown @params if @params.length < 2 and not @prefix
|
||||||
|
|
||||||
# CSI Ps Z
|
# CSI Ps Z
|
||||||
@@ -1004,7 +1048,54 @@ class Terminal
|
|||||||
switch @prefix
|
switch @prefix
|
||||||
# User-Defined Keys (DECUDK).
|
# User-Defined Keys (DECUDK).
|
||||||
when ""
|
when ""
|
||||||
break
|
# Disabling this for now as we need a good script
|
||||||
|
# striper to avoid malicious script injection
|
||||||
|
pt = @currentParam
|
||||||
|
unless pt[0] is ';'
|
||||||
|
console.error "Unknown DECUDK: #{pt}"
|
||||||
|
break
|
||||||
|
pt = pt.slice(1)
|
||||||
|
|
||||||
|
[type, content] = pt.split('|', 2)
|
||||||
|
|
||||||
|
unless content
|
||||||
|
console.error "No type for inline DECUDK: #{pt}"
|
||||||
|
break
|
||||||
|
|
||||||
|
switch type
|
||||||
|
when "HTML"
|
||||||
|
unless @html_escapes_enabled
|
||||||
|
console.log "HTML escapes are disabled"
|
||||||
|
break
|
||||||
|
|
||||||
|
html = "<div class=\"inline-html\">" + content + "</div>"
|
||||||
|
if @native_scroll
|
||||||
|
@next_line()
|
||||||
|
@html[@y] = html
|
||||||
|
@updateRange @y
|
||||||
|
@next_line()
|
||||||
|
else
|
||||||
|
@lines[@y + @ybase][@x] = [
|
||||||
|
@curAttr
|
||||||
|
html
|
||||||
|
]
|
||||||
|
line = 0
|
||||||
|
|
||||||
|
while line < @get_html_height_in_lines(html) - 1
|
||||||
|
@y++
|
||||||
|
if @y > @scrollBottom
|
||||||
|
@y--
|
||||||
|
@scroll()
|
||||||
|
line++
|
||||||
|
|
||||||
|
when "PROMPT"
|
||||||
|
@send content
|
||||||
|
|
||||||
|
when "TEXT"
|
||||||
|
l += content.length
|
||||||
|
data = data.slice(0, i + 1) + content + data.slice(i + 1)
|
||||||
|
else
|
||||||
|
console.error "Unknown type #{type} for DECUDK"
|
||||||
|
|
||||||
# Request Status String (DECRQSS).
|
# Request Status String (DECRQSS).
|
||||||
# test: echo -e '\eP$q"p\e\\'
|
# test: echo -e '\eP$q"p\e\\'
|
||||||
@@ -1302,6 +1393,10 @@ class Terminal
|
|||||||
@leavePrefix()
|
@leavePrefix()
|
||||||
return cancel(ev)
|
return cancel(ev)
|
||||||
|
|
||||||
|
if not @native_scroll and @selectMode
|
||||||
|
@keySelect ev, key
|
||||||
|
return cancel(ev)
|
||||||
|
|
||||||
@showCursor()
|
@showCursor()
|
||||||
@handler(key)
|
@handler(key)
|
||||||
cancel ev
|
cancel ev
|
||||||
@@ -1347,11 +1442,11 @@ class Terminal
|
|||||||
|
|
||||||
@queue += data
|
@queue += data
|
||||||
|
|
||||||
bell: ->
|
bell: (cls="bell")->
|
||||||
return unless @visualBell
|
return unless @visualBell
|
||||||
@element.classList.add "bell"
|
@element.classList.add cls
|
||||||
@t_bell = setTimeout (=>
|
@t_bell = setTimeout (=>
|
||||||
@element.classList.remove "bell"
|
@element.classList.remove cls
|
||||||
), @visualBell
|
), @visualBell
|
||||||
|
|
||||||
resize: ->
|
resize: ->
|
||||||
@@ -1361,8 +1456,8 @@ class Terminal
|
|||||||
term_size = @parent.getBoundingClientRect()
|
term_size = @parent.getBoundingClientRect()
|
||||||
@cols = Math.floor(term_size.width / @char_size.width)
|
@cols = Math.floor(term_size.width / @char_size.width)
|
||||||
@rows = Math.floor(term_size.height / @char_size.height)
|
@rows = Math.floor(term_size.height / @char_size.height)
|
||||||
@element.style['padding-bottom'] = "#{
|
@element.style['padding-bottom'] = "#{term_size.height %
|
||||||
term_size.height % @char_size.height}px"
|
@char_size.height}px"
|
||||||
|
|
||||||
if old_cols == @cols and old_rows == @rows
|
if old_cols == @cols and old_rows == @rows
|
||||||
return
|
return
|
||||||
@@ -1457,6 +1552,8 @@ class Terminal
|
|||||||
@updateRange y
|
@updateRange y
|
||||||
|
|
||||||
eraseLeft: (x, y) ->
|
eraseLeft: (x, y) ->
|
||||||
|
unless @native_scroll
|
||||||
|
y += @ybase
|
||||||
line = @screen[y]
|
line = @screen[y]
|
||||||
# xterm
|
# xterm
|
||||||
ch = [@eraseAttr(), " "]
|
ch = [@eraseAttr(), " "]
|
||||||
@@ -1465,6 +1562,8 @@ class Terminal
|
|||||||
@updateRange y
|
@updateRange y
|
||||||
|
|
||||||
eraseLine: (y) ->
|
eraseLine: (y) ->
|
||||||
|
unless @native_scroll
|
||||||
|
y += @ybase
|
||||||
@eraseRight 0, y
|
@eraseRight 0, y
|
||||||
|
|
||||||
blank_line: (cur) ->
|
blank_line: (cur) ->
|
||||||
@@ -1472,7 +1571,7 @@ class Terminal
|
|||||||
ch = [attr, " "]
|
ch = [attr, " "]
|
||||||
line = []
|
line = []
|
||||||
i = 0
|
i = 0
|
||||||
while i < @cols
|
while i < @cols + 1
|
||||||
line[i] = ch
|
line[i] = ch
|
||||||
i++
|
i++
|
||||||
line
|
line
|
||||||
@@ -1499,23 +1598,21 @@ class Terminal
|
|||||||
# ESC M Reverse Index (RI is 0x8d).
|
# ESC M Reverse Index (RI is 0x8d).
|
||||||
reverseIndex: ->
|
reverseIndex: ->
|
||||||
console.log('TODO: Reverse index')
|
console.log('TODO: Reverse index')
|
||||||
# @y--
|
unless @native_scroll
|
||||||
# if @y < @scrollTop
|
@y--
|
||||||
# @y++
|
if @y < @scrollTop
|
||||||
|
@y++
|
||||||
|
# possibly move the code below to term.reverseScroll();
|
||||||
|
# test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
|
||||||
|
# blank_line(true) is xterm/linux behavior
|
||||||
|
@screen.splice @y + @ybase, 0, @blank_line(true)
|
||||||
|
j = @rows - 1 - @scrollBottom
|
||||||
|
@screen.splice @rows - 1 + @ybase - j + 1, 1
|
||||||
|
|
||||||
# # possibly move the code below to term.reverseScroll();
|
@updateRange @scrollTop
|
||||||
# # test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
|
@updateRange @scrollBottom
|
||||||
# # blank_line(true) is xterm/linux behavior
|
|
||||||
# @screen.splice @y, 0, @blank_line(true)
|
|
||||||
# j = @rows - 1 - @scrollBottom
|
|
||||||
# @screen.splice @rows - 1 - j + 1, 1
|
|
||||||
|
|
||||||
# # @maxRange();
|
|
||||||
# @updateRange @scrollTop
|
|
||||||
# @updateRange @scrollBottom
|
|
||||||
@state = State.normal
|
@state = State.normal
|
||||||
|
|
||||||
|
|
||||||
# ESC c Full Reset (RIS).
|
# ESC c Full Reset (RIS).
|
||||||
reset: ->
|
reset: ->
|
||||||
@reset_vars()
|
@reset_vars()
|
||||||
@@ -1845,7 +1942,7 @@ class Terminal
|
|||||||
insertChars: (params) ->
|
insertChars: (params) ->
|
||||||
param = params[0]
|
param = params[0]
|
||||||
param = 1 if param < 1
|
param = 1 if param < 1
|
||||||
row = @y
|
row = @y + @ybase
|
||||||
j = @x
|
j = @x
|
||||||
# xterm
|
# xterm
|
||||||
ch = [@eraseAttr(), " "]
|
ch = [@eraseAttr(), " "]
|
||||||
@@ -1889,39 +1986,48 @@ class Terminal
|
|||||||
insertLines: (params) ->
|
insertLines: (params) ->
|
||||||
param = params[0]
|
param = params[0]
|
||||||
param = 1 if param < 1
|
param = 1 if param < 1
|
||||||
|
row = @y + @ybase
|
||||||
|
|
||||||
while param--
|
while param--
|
||||||
@screen.splice @y, 0, @blank_line(true)
|
@screen.splice row, 0, @blank_line(true)
|
||||||
@screen.pop()
|
|
||||||
|
|
||||||
# blank_line(true) - xterm/linux behavior
|
# blank_line(true) - xterm/linux behavior
|
||||||
|
if @native_scroll
|
||||||
|
@screen.pop()
|
||||||
|
else
|
||||||
|
j = @rows - 1 - @scrollBottom
|
||||||
|
j = @rows - 1 + @ybase - j + 1
|
||||||
|
@screen.splice j, 1
|
||||||
|
|
||||||
@updateRange @y
|
@updateRange @y
|
||||||
@updateRange @screen.length - 1
|
@updateRange if @native_scroll then @screen.length - 1 else @scrollBottom
|
||||||
|
|
||||||
|
|
||||||
# CSI Ps M
|
# CSI Ps M
|
||||||
# Delete Ps Line(s) (default = 1) (DL).
|
# Delete Ps Line(s) (default = 1) (DL).
|
||||||
deleteLines: (params) ->
|
deleteLines: (params) ->
|
||||||
param = params[0]
|
param = params[0]
|
||||||
param = 1 if param < 1
|
param = 1 if param < 1
|
||||||
|
row = @y + @ybase
|
||||||
|
|
||||||
while param--
|
while param--
|
||||||
# test: echo -e '\e[44m\e[1M\e[0m'
|
# test: echo -e '\e[44m\e[1M\e[0m'
|
||||||
# blank_line(true) - xterm/linux behavior
|
# blank_line(true) - xterm/linux behavior
|
||||||
@screen.push @blank_line(true)
|
if @native_scroll
|
||||||
|
@screen.push @blank_line(true)
|
||||||
|
else
|
||||||
|
j = @rows - 1 - @scrollBottom
|
||||||
|
j = @rows - 1 + @ybase - j
|
||||||
|
@screen.splice j + 1, 0, @blankLine(true)
|
||||||
@screen.splice @y, 1
|
@screen.splice @y, 1
|
||||||
|
|
||||||
@updateRange @y
|
@updateRange @y
|
||||||
@updateRange @screen.length - 1
|
@updateRange if @native_scroll then @screen.length - 1 else @scrollBottom
|
||||||
|
|
||||||
|
|
||||||
# CSI Ps P
|
# CSI Ps P
|
||||||
# Delete Ps Character(s) (default = 1) (DCH).
|
# Delete Ps Character(s) (default = 1) (DCH).
|
||||||
deleteChars: (params) ->
|
deleteChars: (params) ->
|
||||||
param = params[0]
|
param = params[0]
|
||||||
param = 1 if param < 1
|
param = 1 if param < 1
|
||||||
row = @y
|
row = @y + @ybase
|
||||||
# xterm
|
# xterm
|
||||||
ch = [@eraseAttr(), " "]
|
ch = [@eraseAttr(), " "]
|
||||||
while param--
|
while param--
|
||||||
@@ -1934,7 +2040,7 @@ class Terminal
|
|||||||
eraseChars: (params) ->
|
eraseChars: (params) ->
|
||||||
param = params[0]
|
param = params[0]
|
||||||
param = 1 if param < 1
|
param = 1 if param < 1
|
||||||
row = @y
|
row = @y + @ybase
|
||||||
j = @x
|
j = @x
|
||||||
# xterm
|
# xterm
|
||||||
ch = [@eraseAttr(), " "]
|
ch = [@eraseAttr(), " "]
|
||||||
@@ -2377,24 +2483,26 @@ class Terminal
|
|||||||
|
|
||||||
# CSI Ps S Scroll up Ps lines (default = 1) (SU).
|
# CSI Ps S Scroll up Ps lines (default = 1) (SU).
|
||||||
scrollUp: (params) ->
|
scrollUp: (params) ->
|
||||||
# param = params[0] or 1
|
return if @native_scroll
|
||||||
# while param--
|
param = params[0] or 1
|
||||||
# @lines.splice @ybase + @scrollTop, 1
|
while param--
|
||||||
# @lines.splice @ybase + @scrollBottom, 0, @blank_line()
|
@screen.splice @ybase + @scrollTop, 1
|
||||||
|
@screen.splice @ybase + @scrollBottom, 0, @blank_line()
|
||||||
|
|
||||||
# @updateRange @scrollTop
|
@updateRange @scrollTop
|
||||||
# @updateRange @scrollBottom
|
@updateRange @scrollBottom
|
||||||
|
|
||||||
|
|
||||||
# CSI Ps T Scroll down Ps lines (default = 1) (SD).
|
# CSI Ps T Scroll down Ps lines (default = 1) (SD).
|
||||||
scrollDown: (params) ->
|
scrollDown: (params) ->
|
||||||
# param = params[0] or 1
|
return if @native_scroll
|
||||||
# while param--
|
param = params[0] or 1
|
||||||
# @lines.splice @ybase + @scrollBottom, 1
|
while param--
|
||||||
# @lines.splice @ybase + @scrollTop, 0, @blank_line()
|
@screen.splice @ybase + @scrollBottom, 1
|
||||||
|
@screen.splice @ybase + @scrollTop, 0, @blank_line()
|
||||||
|
|
||||||
# @updateRange @scrollTop
|
@updateRange @scrollTop
|
||||||
# @updateRange @scrollBottom
|
@updateRange @scrollBottom
|
||||||
|
|
||||||
|
|
||||||
# CSI Ps ; Ps ; Ps ; Ps ; Ps T
|
# CSI Ps ; Ps ; Ps ; Ps ; Ps T
|
||||||
@@ -2967,3 +3075,5 @@ class Terminal
|
|||||||
Swedish: null # (H or (7
|
Swedish: null # (H or (7
|
||||||
Swiss: null # (=
|
Swiss: null # (=
|
||||||
ISOLatin: null # /A
|
ISOLatin: null # /A
|
||||||
|
|
||||||
|
window.Terminal = Terminal
|
||||||
|
|||||||
13
docker/run.sh
Normal file
13
docker/run.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Set password
|
||||||
|
echo "root:${PASSWORD}" | chpasswd
|
||||||
|
|
||||||
|
if [ -z ${PORT} ]
|
||||||
|
then
|
||||||
|
echo "Starting on default port: 57575"
|
||||||
|
/opt/app/butterfly.server.py --unsecure --host=0.0.0.0
|
||||||
|
else
|
||||||
|
echo "Starting on port: ${PORT}"
|
||||||
|
/opt/app/butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT}
|
||||||
|
fi
|
||||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "butterfly",
|
"name": "butterfly",
|
||||||
"version": "1.5.2",
|
"version": "1.5.10",
|
||||||
"description": "A sleek web based terminal emulator",
|
"description": "A sleek web based terminal emulator",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/paradoxxxzero/butterfly",
|
"homepage": "https://github.com/paradoxxxzero/butterfly",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"grunt": "~0.4.4",
|
"coffeelint": "^1.9.3",
|
||||||
"grunt-contrib-coffee": "^0.10.1",
|
"grunt": "^0.4.5",
|
||||||
|
"grunt-coffeelint": "0.0.13",
|
||||||
|
"grunt-contrib-coffee": "^0.13.0",
|
||||||
|
"grunt-contrib-cssmin": "^0.12.2",
|
||||||
|
"grunt-contrib-uglify": "^0.9.1",
|
||||||
"grunt-contrib-watch": "^0.6.1",
|
"grunt-contrib-watch": "^0.6.1",
|
||||||
"grunt-contrib-uglify": "^0.4.0",
|
"grunt-sass": "^0.18.1"
|
||||||
"grunt-contrib-cssmin": "^0.9.0",
|
|
||||||
"grunt-coffeelint": "0.0.8",
|
|
||||||
"grunt-sass": "^0.12.1",
|
|
||||||
"grunt-sass-to-scss": "^0.1.9"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
setup.py
6
setup.py
@@ -24,14 +24,14 @@ options = dict(
|
|||||||
platforms="Any",
|
platforms="Any",
|
||||||
scripts=['butterfly.server.py'],
|
scripts=['butterfly.server.py'],
|
||||||
packages=['butterfly'],
|
packages=['butterfly'],
|
||||||
install_requires=["tornado>=3.2", "pyOpenSSL"],
|
install_requires=["tornado>=3.2", "pyOpenSSL", 'tornado_systemd'],
|
||||||
package_data={
|
package_data={
|
||||||
'butterfly': [
|
'butterfly': [
|
||||||
'scss/*.scss',
|
'sass/*.sass',
|
||||||
'static/fonts/*',
|
'static/fonts/*',
|
||||||
'static/images/favicon.png',
|
'static/images/favicon.png',
|
||||||
'static/main.css',
|
'static/main.css',
|
||||||
'static/main.min.js',
|
'static/*.min.js',
|
||||||
'templates/index.html'
|
'templates/index.html'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user