mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-05-26 07:08:08 +00:00
Merge branch 'master' into 2ws
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
README.md
|
||||
butterfly.png
|
||||
21
Dockerfile
21
Dockerfile
@@ -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"]
|
||||
|
||||
77
README.md
77
README.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
butterfly/static/ext.min.js
vendored
4
butterfly/static/ext.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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; }
|
||||
|
||||
@@ -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(' ');
|
||||
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(' ');
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
6
butterfly/static/main.min.js
vendored
6
butterfly/static/main.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
11
coffees/ext/expand_extended.coffee
Normal file
11
coffees/ext/expand_extended.coffee
Normal 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
|
||||
26
coffees/ext/linkify.coffee
Normal file
26
coffees/ext/linkify.coffee
Normal 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
|
||||
@@ -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(' ')).join(' ')
|
||||
|
||||
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
21
docker/run.sh
Normal file → Executable 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 "$@"
|
||||
|
||||
Reference in New Issue
Block a user