Merge branch 'master' into domscroll_try2

This commit is contained in:
Florian Mounier
2015-04-08 15:13:02 +02:00
44 changed files with 1550 additions and 979 deletions

18
Dockerfile Normal file
View 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"]

View File

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

View File

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

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
print('\x1b]99;<hr />\x07') print('\x1bP;HTML|<hr />\x1bP')

View File

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

View File

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

View File

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

@@ -0,0 +1,5 @@
[Socket]
ListenStream=57575
[Install]
WantedBy=sockets.target

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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 += "&gt;" out += "&gt;"
else else
if ch <= " " if ch == " "
out += '<span class="nbsp">\u2007</span>'
else if ch <= " "
out += "&nbsp;" out += "&nbsp;"
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
View 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

View File

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

View File

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