Merge branch 'master' into 2ws

This commit is contained in:
Florian Mounier
2016-10-07 09:59:14 +02:00
18 changed files with 426 additions and 148 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.gitignore
.dockerignore
Dockerfile
README.md
butterfly.png

View File

@@ -1,18 +1,25 @@
FROM ubuntu:14.04.1
FROM ubuntu:14.04
RUN apt-get update -y
RUN apt-get install -y python-setuptools python-dev build-essential libffi-dev libssl-dev
RUN apt-get update \
&& apt-get install -y -q --no-install-recommends \
build-essential \
libffi-dev \
libssl-dev \
python-dev \
python-setuptools \
&& apt-get clean \
&& rm -r /var/lib/apt/lists/*
WORKDIR /opt
ADD . /opt/app
WORKDIR /opt/app
RUN python setup.py build
RUN python setup.py install
RUN python setup.py build \
&& python setup.py install
ADD docker/run.sh /opt/run.sh
RUN chmod 777 /opt/run.sh
EXPOSE 57575
CMD ["/opt/run.sh"]
CMD ["butterfly.server.py", "--unsecure", "--host=0.0.0.0"]
ENTRYPOINT ["docker/run.sh"]

View File

@@ -24,16 +24,16 @@ Butterfly is a xterm compatible terminal that runs in your browser.
## Try it
```bash
$ pip install butterfly
$ pip install libsass # If you want to use themes
$ butterfly
``` bash
$ pip install butterfly
$ pip install libsass # If you want to use themes
$ butterfly
```
A new tab should appear in your browser. Then type
```bash
$ butterfly help
``` bash
$ butterfly help
```
To get an overview of butterfly features.
@@ -41,8 +41,8 @@ To get an overview of butterfly features.
## Run it as a server
```bash
$ butterfly.server.py --host=myhost --port=57575
``` bash
$ butterfly.server.py --host=myhost --port=57575
```
The first time it will ask you to generate the certificates (see: [here](http://paradoxxxzero.github.io/2014/03/21/butterfly-with-ssl-auth.html))
@@ -52,12 +52,12 @@ The first time it will ask you to generate the certificates (see: [here](http://
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
``` bash
$ cd /etc/systemd/system
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.service
$ curl -O https://raw.githubusercontent.com/paradoxxxzero/butterfly/master/butterfly.socket
$ systemctl enable butterfly.socket
$ systemctl start butterfly.socket
```
Don't forget to update the /etc/butterfly/butterfly.conf file with your server options (host, port, shell, ...)
@@ -74,7 +74,6 @@ If you want to motivate me to continue working on this project you can tip me, s
Client side development use [grunt](http://gruntjs.com/) and [bower](http://bower.io/).
## Credits
The js part is based on [term.js](https://github.com/chjj/term.js/) which is based on [jslinux](http://bellard.org/jslinux/).
@@ -82,35 +81,45 @@ The js part is based on [term.js](https://github.com/chjj/term.js/) which is bas
[Florian Mounier](http://paradoxxxzero.github.io/)
## License
```
butterfly Copyright (C) 2015 Florian Mounier
butterfly Copyright (C) 2015 Florian Mounier
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
```
## Docker Usage
## Docker
There is a docker repository created for this project that is set to automatically rebuild when there is a push
into this repository: https://registry.hub.docker.com/u/garland/butterfly/
### Starting
### Example usage
docker run \
--env PASSWORD=password \
--env PORT=57575 \
-p 57575:57575 \
-d garland/butterfly
Starting with login and password
``` bash
docker run --env PASSWORD=password -d garland/butterfly --login
```
Starting with no password
``` bash
docker run -d -p 57575:57575 garland/butterfly
```
Starting with a different port
``` bash
docker run -d -p 12345:12345 garland/butterfly --port=12345
```

View File

@@ -14,7 +14,7 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__version__ = '2.1.0'
__version__ = '3.0.0-alpha'
import os
@@ -46,13 +46,18 @@ class Route(tornado.web.RequestHandler):
@property
def builtin_themes_dir(self):
return os.path.join(
os.path.dirname(__file__), 'themes')
os.path.dirname(__file__), 'themes')
@property
def themes_dir(self):
return os.path.join(
self.application.butterfly_dir, 'themes')
@property
def local_js_dir(self):
return os.path.join(
self.application.butterfly_dir, 'js')
def get_theme_dir(self, theme):
if theme.startswith('built-in-'):
return os.path.join(

View File

@@ -319,7 +319,7 @@ class ThemesList(Route):
'built-in-%s' % theme
for theme in os.listdir(self.builtin_themes_dir)
if os.path.isdir(os.path.join(
self.builtin_themes_dir, theme)) and
self.builtin_themes_dir, theme)) and
not theme.startswith('.')]
else:
builtin_themes = []
@@ -330,3 +330,22 @@ class ThemesList(Route):
'builtin_themes': sorted(builtin_themes),
'dir': self.themes_dir
}))
@url('/local.js')
class LocalJsStatic(Route):
def get(self):
self.set_header("Content-Type", 'application/javascript')
if os.path.exists(self.local_js_dir):
for fn in os.listdir(self.local_js_dir):
if not fn.endswith('.js'):
continue
with open(os.path.join(self.local_js_dir, fn), 'rb') as s:
while True:
data = s.read(16384)
if data:
self.write(data)
else:
self.write(';')
break
self.finish()

View File

@@ -29,6 +29,34 @@ body
.line.active
background-color: $active-bg
.line.extended
overflow-x: auto
overflow-x: overlay
cursor: zoom-in
background-image: linear-gradient(90deg, rgba(darken($bg, 3%), 0), 95%, darken($bg, 3%))
&:not(.expanded):hover
background-color: lighten($bg, 2%)
&.expanded
cursor: zoom-out
background-color: darken($bg, 3%)
.extra
display: block
white-space: pre-line
word-break: break-all
&::-webkit-scrollbar
background: rgba($scroll-bg, .1)
height: 0
&::-webkit-scrollbar-thumb
background: rgba($scroll-fg, .1)
&::-webkit-scrollbar-thumb:hover
background: rgba($scroll-fg-hover, .1)
&::-webkit-scrollbar
background: $scroll-bg
width: $scroll-width

View File

@@ -1,5 +1,5 @@
(function() {
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, first, nextLeaf, popup, previousLeaf, selection, setAlarm, virtualInput,
var Popup, Selection, _set_theme_href, _theme, alt, cancel, clean_ansi, copy, ctrl, first, linkify, nextLeaf, popup, previousLeaf, selection, setAlarm, virtualInput, walk,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
clean_ansi = function(data) {
@@ -156,6 +156,73 @@
}
});
Terminal.on('change', function(lines) {
var j, len1, line, results;
results = [];
for (j = 0, len1 = lines.length; j < len1; j++) {
line = lines[j];
if (indexOf.call(line.classList, 'extended') >= 0) {
results.push(line.addEventListener('click', (function(line) {
return function() {
var after, before;
if (indexOf.call(line.classList, 'expanded') >= 0) {
return line.classList.remove('expanded');
} else {
before = line.getBoundingClientRect().height;
line.classList.add('expanded');
after = line.getBoundingClientRect().height;
return document.body.scrollTop += after - before;
}
};
})(line)));
} else {
results.push(void 0);
}
}
return results;
});
walk = function(node, callback) {
var child, j, len1, ref, results;
ref = node.childNodes;
results = [];
for (j = 0, len1 = ref.length; j < len1; j++) {
child = ref[j];
callback.call(child);
results.push(walk(child, callback));
}
return results;
};
linkify = function(text) {
var emailAddressPattern, pseudoUrlPattern, urlPattern;
urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim;
return text.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>');
};
Terminal.on('change', function(lines) {
var j, len1, line, results;
results = [];
for (j = 0, len1 = lines.length; j < len1; j++) {
line = lines[j];
results.push(walk(line, function() {
var linkified, newNode;
if (this.nodeType === 3) {
linkified = linkify(this.nodeValue);
if (linkified !== this.nodeValue) {
newNode = document.createElement('span');
newNode.innerHTML = linkified;
this.parentElement.replaceChild(newNode, this);
return true;
}
}
}));
}
return results;
});
document.addEventListener('keydown', function(e) {
if (!(e.altKey && e.keyCode === 79)) {
return true;

File diff suppressed because one or more lines are too long

View File

@@ -2810,6 +2810,27 @@ body {
/* Pop ups */ }
body .line.active {
background-color: transparent; }
body .line.extended {
overflow-x: auto;
overflow-x: overlay;
cursor: zoom-in;
background-image: linear-gradient(90deg, rgba(9, 8, 10, 0), 95%, #09080a); }
body .line.extended:not(.expanded):hover {
background-color: #161419; }
body .line.extended.expanded {
cursor: zoom-out;
background-color: #09080a; }
body .line.extended.expanded .extra {
display: block;
white-space: pre-line;
word-break: break-all; }
body .line.extended::-webkit-scrollbar {
background: rgba(17, 15, 19, 0.1);
height: 0; }
body .line.extended::-webkit-scrollbar-thumb {
background: rgba(244, 234, 213, 0.1); }
body .line.extended::-webkit-scrollbar-thumb:hover {
background: rgba(244, 234, 213, 0.1); }
body::-webkit-scrollbar {
background: #110f13;
width: 0.75em; }

View File

@@ -1,5 +1,6 @@
(function() {
var $, State, Terminal, cancel, cols, openTs, quit, rows, s, uuid, ws,
slice = [].slice,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
cols = rows = null;
@@ -153,6 +154,22 @@
};
Terminal = (function() {
Terminal.hooks = {};
Terminal.on = function(hook, fun) {
if (Terminal.hooks[hook] == null) {
Terminal.hooks[hook] = [];
}
return Terminal.hooks[hook].push(fun);
};
Terminal.off = function(hook, fun) {
if (Terminal.hooks[hook] == null) {
Terminal.hooks[hook] = [];
}
return Terminal.hooks[hook].pop(fun);
};
function Terminal(parent, out1, ctl1) {
var div, px;
this.parent = parent;
@@ -208,8 +225,24 @@
return _this.resize();
};
})(this));
this.emit('load');
}
Terminal.prototype.emit = function() {
var args, fun, hook, k, len, ref, results;
hook = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
if (Terminal.hooks[hook] == null) {
Terminal.hooks[hook] = [];
}
ref = Terminal.hooks[hook];
results = [];
for (k = 0, len = ref.length; k < len; k++) {
fun = ref[k];
results.push(fun.apply(this, args));
}
return results;
};
Terminal.prototype.cloneAttr = function(a, char) {
if (char == null) {
char = null;
@@ -257,6 +290,7 @@
this.applicationCursor = false;
this.originMode = false;
this.autowrap = true;
this.horizontalWrap = false;
this.normal = null;
this.charset = null;
this.gcharset = null;
@@ -509,25 +543,8 @@
})(this));
};
Terminal.prototype.linkify = function(t) {
var emailAddressPattern, part, pseudoUrlPattern, urlPattern;
urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim;
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim;
return ((function() {
var k, len, ref, results;
ref = t.split('&nbsp;');
results = [];
for (k = 0, len = ref.length; k < len; k++) {
part = ref[k];
results.push(part.replace(urlPattern, '<a href="$&">$&</a>').replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>').replace(emailAddressPattern, '<a href="mailto:$&">$&</a>'));
}
return results;
})()).join('&nbsp;');
};
Terminal.prototype.refresh = function(force) {
var active, attr, ch, classes, cursor, data, fg, group, i, j, k, len, len1, len2, len3, len4, line, lines, m, n, newOut, o, out, q, ref, ref1, ref2, ref3, ref4, ref5, skipnext, styles, u, x;
var active, attr, ch, classes, cls, cursor, data, fg, group, i, j, k, len, len1, len2, len3, len4, line, lines, m, modified, n, newOut, o, out, q, ref, ref1, ref2, ref3, ref4, ref5, skipnext, styles, u, x;
if (force == null) {
force = false;
}
@@ -542,6 +559,7 @@
active.classList.remove('active');
}
newOut = '';
modified = [];
ref2 = this.screen;
for (j = n = 0, len2 = ref2.length; n < len2; j = ++n) {
line = ref2[j];
@@ -666,19 +684,30 @@
if (!this.equalAttr(attr, this.defAttr)) {
out += "</span>";
}
if (!(j === this.y + this.shift || (data != null ? data.html : void 0))) {
out = this.linkify(out);
}
if (line.wrap) {
out += '\u23CE';
}
if (line.extra) {
out += '<span class="extra">' + line.extra + '</span>';
}
if (this.children[j]) {
this.children[j].innerHTML = out;
modified.push(this.children[j]);
if (x !== -Infinity) {
this.children[j].classList.add('active');
}
if (line.extra) {
this.children[j].classList.add('extended');
}
} else {
newOut += "<div class=\"line" + (x !== -Infinity && ' active' || '') + "\">" + out + "</div>";
cls = ['line'];
if (x !== -Infinity) {
cls.push('active');
}
if (line.extra) {
cls.push('extended');
}
newOut += "<div class=\"" + (cls.join(' ')) + "\">" + out + "</div>";
}
this.screen[j].dirty = false;
}
@@ -686,6 +715,7 @@
group = this.document.createElement('div');
group.className = 'group';
group.innerHTML = newOut;
modified.push(group);
this.body.appendChild(group);
this.screen = this.screen.slice(-this.rows);
this.shift = 0;
@@ -705,7 +735,8 @@
}
this.children = Array.prototype.slice.call(lines, -this.rows);
}
return this.nativeScrollTo();
this.nativeScrollTo();
return this.emit('change', modified);
};
Terminal.prototype._cursorBlink = function() {
@@ -822,11 +853,17 @@
case "\n":
case "\x0b":
case "\x0c":
this.screen[this.y + this.shift].dirty = true;
this.nextLine();
if (this.horizontalWrap) {
this.screen[this.y + this.shift].extra += ch;
} else {
this.screen[this.y + this.shift].dirty = true;
this.nextLine();
}
break;
case "\r":
this.x = 0;
if (!this.horizontalWrap) {
this.x = 0;
}
break;
case "\b":
if (this.x >= this.cols) {
@@ -868,11 +905,15 @@
ch = this.charset[ch];
}
if (this.x >= this.cols) {
if (this.autowrap) {
this.screen[this.y + this.shift].wrap = true;
this.nextLine();
if (this.horizontalWrap) {
this.screen[this.y + this.shift].extra += ch;
} else {
if (this.autowrap) {
this.screen[this.y + this.shift].wrap = true;
this.nextLine();
}
this.x = 0;
}
this.x = 0;
}
this.putChar(ch);
this.x++;
@@ -1282,8 +1323,7 @@
attr = this.cloneAttr(this.curAttr);
attr.html = "<div class=\"inline-html\">" + safe + "</div>";
this.screen[this.y + this.shift].chars[this.x] = attr;
this.screen[this.y + this.shift].dirty = true;
this.screen[this.y + this.shift].wrap = false;
this.resetLine(this.screen[this.y + this.shift]);
this.nextLine();
break;
case "IMAGE":
@@ -1298,8 +1338,7 @@
attr = this.cloneAttr(this.curAttr);
attr.html = "<img class=\"inline-image\" src=\"data:" + mime + ";base64," + b64 + "\" />";
this.screen[this.y + this.shift].chars[this.x] = attr;
this.screen[this.y + this.shift].dirty = true;
this.screen[this.y + this.shift].wrap = false;
this.resetLine(this.screen[this.y + this.shift]);
break;
case "PROMPT":
this.send(content);
@@ -1846,8 +1885,7 @@
line[x] = this.eraseAttr();
x++;
}
this.screen[y + this.shift].dirty = true;
return this.screen[y + this.shift].wrap = false;
return this.resetLine(this.screen[y + this.shift]);
};
Terminal.prototype.eraseLeft = function(x, y) {
@@ -1855,14 +1893,19 @@
while (x--) {
this.screen[y + this.shift].chars[x] = this.eraseAttr();
}
this.screen[y + this.shift].dirty = true;
return this.screen[y + this.shift].wrap = false;
return this.resetLine(this.screen[y + this.shift]);
};
Terminal.prototype.eraseLine = function(y) {
return this.eraseRight(0, y);
};
Terminal.prototype.resetLine = function(l) {
l.dirty = true;
l.wrap = false;
return l.extra = '';
};
Terminal.prototype.blankLine = function(cur, dirty) {
var attr, i, line;
if (cur == null) {
@@ -1881,7 +1924,8 @@
return {
chars: line,
dirty: dirty,
wrap: false
wrap: false,
extra: ''
};
};
@@ -2285,8 +2329,7 @@
this.screen[this.y + this.shift].chars.splice(this.x, 1);
this.screen[this.y + this.shift].chars.push(this.eraseAttr());
}
this.screen[this.y + this.shift].dirty = true;
return this.screen[this.y + this.shift].wrap = false;
return this.resetLine(this.screen[this.y + this.shift]);
};
Terminal.prototype.eraseChars = function(params) {
@@ -2299,8 +2342,7 @@
while (param-- && j < this.cols) {
this.screen[this.y + this.shift].chars[j++] = this.eraseAttr();
}
this.screen[this.y + this.shift].dirty = true;
return this.screen[this.y + this.shift].wrap = false;
return this.resetLine(this.screen[this.y + this.shift]);
};
Terminal.prototype.charPosAbsolute = function(params) {
@@ -2436,6 +2478,8 @@
return this.autowrap = true;
case 66:
return this.applicationKeypad = true;
case 77:
return this.horizontalWrap = true;
case 9:
case 1000:
case 1002:
@@ -2515,6 +2559,8 @@
return this.autowrap = false;
case 66:
return this.applicationKeypad = false;
case 77:
return this.horizontalWrap = false;
case 9:
case 1000:
case 1002:
@@ -2815,8 +2861,7 @@
while (i < l) {
this.screen[i].chars.splice(this.x, 1);
this.screen[i].chars.push(this.eraseAttr());
this.screen[i].dirty = true;
this.screen[i].wrap = false;
this.resetLine(this.screen[i].dirty);
results1.push(i++);
}
return results1;

File diff suppressed because one or more lines are too long

View File

@@ -23,5 +23,6 @@
'' if options.unminified else 'min.')) }}"></script>
<script src="{{ static_url('ext.%sjs' % (
'' if options.unminified else 'min.')) }}"></script>
<script src="{{ reverse_url('LocalJsStatic') }}"></script>
</body>
</html>

View File

@@ -225,7 +225,7 @@ class Terminal(object):
args = tornado.options.options.cmd.split(' ')
else:
args = [tornado.options.options.shell or self.callee.shell]
args.append('-i')
args.append('-il')
# In some cases some shells don't export SHELL var
env['SHELL'] = args[0]

View File

@@ -208,10 +208,15 @@ def get_socket_env(inode, user):
continue
try:
with open('/proc/%s/cmdline' % pid) as c:
if c.read().split('\x00')[0].split('/')[-1] in [
command = c.read().split('\x00')
executable = command[0].split('/')[-1]
if executable in ('sh', 'bash', 'zsh'):
executable = command[1].split('/')[-1]
if executable in [
'gnome-session',
'gnome-session-binary',
'startkde',
'startdde',
'xfce4-session']:
with open('/proc/%s/status' % pid) as e:
uid = None

View File

@@ -0,0 +1,11 @@
Terminal.on 'change', (lines) ->
for line in lines
if 'extended' in line.classList
line.addEventListener 'click', do (line) -> ->
if 'expanded' in line.classList
line.classList.remove 'expanded'
else
before = line.getBoundingClientRect().height
line.classList.add 'expanded'
after = line.getBoundingClientRect().height
document.body.scrollTop += after - before

View File

@@ -0,0 +1,26 @@
walk = (node, callback) ->
for child in node.childNodes
callback.call(child)
walk child, callback
linkify = (text) ->
# http://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
urlPattern = (
/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim)
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim
text
.replace(urlPattern, '<a href="$&">$&</a>')
.replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>')
.replace(emailAddressPattern, '<a href="mailto:$&">$&</a>')
Terminal.on 'change', (lines) ->
for line in lines
walk line, ->
if @nodeType is 3
linkified = linkify @nodeValue
if linkified isnt @nodeValue
newNode = document.createElement('span')
newNode.innerHTML = linkified
@parentElement.replaceChild newNode, @
true

View File

@@ -28,7 +28,6 @@
# http://bellard.org/jslinux/
cancel = (ev) ->
ev.preventDefault() if ev.preventDefault
ev.stopPropagation() if ev.stopPropagation
@@ -45,7 +44,20 @@ State =
dcs: s++
ignore: s++
class Terminal
@hooks: {}
# Mini implementation of event
@on: (hook, fun) ->
unless Terminal.hooks[hook]?
Terminal.hooks[hook] = []
Terminal.hooks[hook].push(fun)
@off: (hook, fun) ->
unless Terminal.hooks[hook]?
Terminal.hooks[hook] = []
Terminal.hooks[hook].pop(fun)
constructor: (@parent, @out, @ctl=->) ->
# Global elements
@document = @parent.ownerDocument
@@ -101,6 +113,13 @@ class Terminal
@initmouse()
addEventListener 'load', => @resize()
@emit 'load'
emit: (hook, args...) ->
unless Terminal.hooks[hook]?
Terminal.hooks[hook] = []
for fun in Terminal.hooks[hook]
fun.apply(@, args)
cloneAttr: (a, char=null) ->
bg: a.bg
@@ -148,6 +167,7 @@ class Terminal
@applicationCursor = false
@originMode = false
@autowrap = true
@horizontalWrap = false
@normal = null
# charset
@@ -395,27 +415,17 @@ class Terminal
sendButton ev
cancel ev
linkify: (t) ->
# http://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
urlPattern = (
/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim)
pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim
emailAddressPattern = /[\w.]+@[a-zA-Z_-]+?(?:\.[a-zA-Z]{2,6})+/gim
(part
.replace(urlPattern, '<a href="$&">$&</a>')
.replace(pseudoUrlPattern, '$1<a href="http://$2">$2</a>')
.replace(emailAddressPattern, '<a href="mailto:$&">$&</a>'
) for part in t.split('&nbsp;')).join('&nbsp;')
refresh: (force=false) ->
for cursor in @body.querySelectorAll(".cursor")
cursor.parentNode.replaceChild(
@document.createTextNode(cursor.textContent), cursor)
for active in @body.querySelectorAll(".line.active")
active.classList.remove('active')
# for active in @body.querySelectorAll(".line.extended")
# active.classList.remove('extended')
newOut = ''
modified = []
for line, j in @screen
continue unless line.dirty or force
out = ""
@@ -517,21 +527,30 @@ class Terminal
out += "</span>" if i is x
attr = data
out += "</span>" unless @equalAttr attr, @defAttr
out = @linkify(out) unless j is @y + @shift or data?.html
out += '\u23CE' if line.wrap
if line.extra
out += '<span class="extra">' + line.extra + '</span>'
if @children[j]
@children[j].innerHTML = out
modified.push @children[j]
if x isnt -Infinity
@children[j].classList.add 'active'
if line.extra
@children[j].classList.add 'extended'
else
newOut += "<div class=\"line#{
x isnt -Infinity and ' active' or ''}\">#{out}</div>"
cls = ['line']
if x isnt -Infinity
cls.push 'active'
if line.extra
cls.push 'extended'
newOut += "<div class=\"#{cls.join(' ')}\">#{out}</div>"
@screen[j].dirty = false
if newOut isnt ''
group = @document.createElement('div')
group.className = 'group'
group.innerHTML = newOut
modified.push group
@body.appendChild group
@screen = @screen.slice(-@rows)
@shift = 0
@@ -548,6 +567,7 @@ class Terminal
lines, -@rows)
@nativeScrollTo()
@emit 'change', modified
_cursorBlink: ->
@cursorState ^= 1
@@ -634,12 +654,16 @@ class Terminal
# '\n', '\v', '\f'
when "\n", "\x0b", "\x0c"
# @x = 0 if @convertEol
@screen[@y + @shift].dirty = true
@nextLine()
if @horizontalWrap
@screen[@y + @shift].extra += ch
else
@screen[@y + @shift].dirty = true
@nextLine()
# '\r'
when "\r"
@x = 0
unless @horizontalWrap
@x = 0
# '\b'
when "\b"
@@ -687,11 +711,13 @@ class Terminal
if ch >= " "
ch = @charset[ch] if @charset?[ch]
if @x >= @cols
if @autowrap
@screen[@y + @shift].wrap = true
@nextLine()
@x = 0
if @horizontalWrap
@screen[@y + @shift].extra += ch
else
if @autowrap
@screen[@y + @shift].wrap = true
@nextLine()
@x = 0
@putChar ch
@x++
if @forceWidth and "\uff00" < ch < "\uffef"
@@ -1140,8 +1166,6 @@ class Terminal
switch @prefix
# User-Defined Keys (DECUDK).
when ""
# 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}"
@@ -1161,8 +1185,7 @@ class Terminal
attr.html = (
"<div class=\"inline-html\">#{safe}</div>")
@screen[@y + @shift].chars[@x] = attr
@screen[@y + @shift].dirty = true
@screen[@y + @shift].wrap = false
@resetLine @screen[@y + @shift]
@nextLine()
when "IMAGE"
@@ -1180,8 +1203,7 @@ class Terminal
"<img class=\"inline-image\" src=\"data:#{mime};base64,#{
b64}\" />")
@screen[@y + @shift].chars[@x] = attr
@screen[@y + @shift].dirty = true
@screen[@y + @shift].wrap = false
@resetLine @screen[@y + @shift]
when "PROMPT"
@send content
@@ -1673,18 +1695,21 @@ class Terminal
while x < @cols
line[x] = @eraseAttr()
x++
@screen[y + @shift].dirty = true
@screen[y + @shift].wrap = false
@resetLine @screen[y + @shift]
eraseLeft: (x, y) ->
x++
@screen[y + @shift].chars[x] = @eraseAttr() while x--
@screen[y + @shift].dirty = true
@screen[y + @shift].wrap = false
@resetLine @screen[y + @shift]
eraseLine: (y) ->
@eraseRight 0, y
resetLine: (l) ->
l.dirty = true
l.wrap = false
l.extra = ''
blankLine: (cur=false, dirty=true) ->
attr = (if cur then @eraseAttr() else @defAttr)
line = []
@@ -1696,6 +1721,7 @@ class Terminal
chars: line
dirty: dirty
wrap: false
extra: ''
ch: (cur) ->
if cur then @eraseAttr() else @defAttr
@@ -2176,8 +2202,7 @@ class Terminal
while param--
@screen[@y + @shift].chars.splice @x, 1
@screen[@y + @shift].chars.push @eraseAttr()
@screen[@y + @shift].dirty = true
@screen[@y + @shift].wrap = false
@resetLine @screen[@y + @shift]
# CSI Ps X
# Erase Ps Character(s) (default = 1) (ECH).
@@ -2187,8 +2212,7 @@ class Terminal
j = @x
# xterm
@screen[@y + @shift].chars[j++] = @eraseAttr() while param-- and j < @cols
@screen[@y + @shift].dirty = true
@screen[@y + @shift].wrap = false
@resetLine @screen[@y + @shift]
# CSI Pm ` Character Position Absolute
# [column] (default = [row,1]) (HPA).
@@ -2417,6 +2441,8 @@ class Terminal
@autowrap = true
when 66
@applicationKeypad = true
when 77
@horizontalWrap = true
# X10 Mouse
# no release, no motion, no wheel, no modifiers.
when 9, 1000, 1002, 1003 # any event mouse
@@ -2578,6 +2604,8 @@ class Terminal
@autowrap = false
when 66
@applicationKeypad = false
when 77
@horizontalWrap = false
when 9, 1000, 1002 , 1003 # any event mouse
@x10Mouse = false
@vt200Mouse = false
@@ -3158,8 +3186,7 @@ class Terminal
while i < l
@screen[i].chars.splice @x, 1
@screen[i].chars.push @eraseAttr()
@screen[i].dirty = true
@screen[i].wrap = false
@resetLine @screen[i].dirty
i++
# DEC Special Character and Line Drawing Set.

21
docker/run.sh Normal file → Executable file
View File

@@ -1,13 +1,14 @@
#!/bin/sh
#!/bin/bash -e
# if command starts with an option, prepend the default command and options
if [ "${1:0:1}" = '-' ]; then
set -- butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT:-57575} "$@"
elif [ "$1" = 'butterfly.server.py' ]; then
shift
set -- butterfly.server.py --unsecure --host=0.0.0.0 --port=${PORT:-57575} "$@"
fi
# Set password
echo "root:${PASSWORD}" | chpasswd
echo "root:${PASSWORD:-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
exec "$@"