Add a default conf file. Handle themes in a far more intelligent way by adding a popup and saving conf in localStorage.

This commit is contained in:
Florian Mounier
2015-10-07 16:03:32 +02:00
parent f67054f9ff
commit 0f7a51d451
17 changed files with 516 additions and 212 deletions

View File

@@ -26,6 +26,7 @@ import uuid
import ssl
import getpass
import os
import shutil
import stat
import socket
import sys
@@ -33,6 +34,9 @@ import sys
tornado.options.define("debug", default=False, help="Debug mode")
tornado.options.define("more", default=False,
help="Debug mode with more verbosity")
tornado.options.define("unminified", default=False,
help="Use the unminified js (for development only)")
tornado.options.define("host", default='localhost', help="Server host")
tornado.options.define("port", default=57575, type=int, help="Server port")
tornado.options.define("shell", help="Shell to execute at login")
@@ -41,12 +45,12 @@ tornado.options.define("cmd",
help="Command to run instead of shell, f.i.: 'ls -l'")
tornado.options.define("unsecure", default=False,
help="Don't use ssl not recommended")
tornado.options.define("login", default=True,
help="Use login screen at start")
tornado.options.define("force_unicode_width",
default=False,
help="Force all unicode characters to the same width."
"Useful for avoiding layout mess.")
tornado.options.define("login", default=True,
help="Use login screen at start")
tornado.options.define("ssl_version", default=None,
help="SSL protocol version")
tornado.options.define("generate_certs", default=False,
@@ -57,19 +61,33 @@ tornado.options.define("generate_current_user_pkcs", default=False,
tornado.options.define("generate_user_pkcs", default='',
help="Generate user pfx for client authentication "
"(Must be root to create for another user)")
tornado.options.define("unminified", default=False,
help="Use the unminified js (for development only)")
tornado.options.define("theme", default=None,
help="Specify a theme for butterfly.")
tornado.options.define("install_themes", default=False,
help="Install or update themes from butterfly-themes")
tornado.options.define("install_themes_from",
default='https://github.com/paradoxxxzero/'
'butterfly-themes/archive/master.zip',
help="Url to download themes from")
if os.getuid() == 0:
conf_file = os.path.join(
os.path.abspath(os.sep), 'etc', 'butterfly', 'butterfly.conf')
ssl_dir = os.path.join(os.path.abspath(os.sep), 'etc', 'butterfly', 'ssl')
ev = os.getenv('XDG_CONFIG_DIRS', '/etc')
else:
conf_file = os.path.join(
os.path.expanduser('~'), '.butterfly', 'butterfly.conf')
ssl_dir = os.path.join(os.path.expanduser('~'), '.butterfly', 'ssl')
ev = os.getenv(
'XDG_CONFIG_HOME', os.path.join(os.getenv('HOME'), '.config'))
butterfly_dir = os.path.join(ev, 'butterfly')
conf_file = os.path.join(butterfly_dir, 'butterfly.conf')
ssl_dir = os.path.join(butterfly_dir, 'ssl')
if not os.path.exists(conf_file):
try:
shutil.copy(
os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'butterfly',
'butterfly.conf.default'), conf_file)
print('butterfly.conf installed in %s' % conf_file)
except:
pass
tornado.options.define("conf", default=conf_file,
help="Butterfly configuration file. "
@@ -78,11 +96,15 @@ tornado.options.define("conf", default=conf_file,
tornado.options.define("ssl_dir", default=ssl_dir,
help="Force SSL directory location")
# Do it once to get the conf path
tornado.options.parse_command_line()
if os.path.exists(tornado.options.options.conf):
tornado.options.parse_config_file(tornado.options.options.conf)
# Do it again to overwrite conf with args
tornado.options.parse_command_line()
options = tornado.options.options
for logger in ('tornado.access', 'tornado.application',
@@ -95,7 +117,49 @@ for logger in ('tornado.access', 'tornado.application',
logging.getLogger(logger).setLevel(level)
log = logging.getLogger('butterfly')
log.info('Starting server')
if options.install_themes:
from io import BytesIO
from shutil import move, rmtree
from zipfile import ZipFile
from tempfile import mkdtemp
try:
from urllib.request import urlopen
except ImportError:
from urllib import urlopen
try:
import sass as _
_.CompileError
except Exception:
print('You must install libsass to use themes '
'(run: pip install libsass)')
sys.exit(1)
themes_url = options.install_themes_from
print('Downloading %s...' % themes_url)
zip_ = ZipFile(BytesIO(urlopen(themes_url).read()))
print('Extracting in %s' % butterfly_dir)
zip_.extractall(butterfly_dir)
zip_dest = os.path.join(butterfly_dir, 'butterfly-themes-master')
theme_dir = os.path.join(butterfly_dir, 'themes')
if not os.path.exists(theme_dir):
os.makedirs(theme_dir)
tmp_dir = mkdtemp()
for dir_ in os.listdir(zip_dest):
if dir_ == 'README.md':
continue
new_dir = os.path.join(theme_dir, dir_)
if os.path.exists(new_dir):
move(new_dir, tmp_dir)
print('Old theme %s has been backed up in %s' % (
new_dir, tmp_dir))
move(os.path.join(zip_dest, dir_), theme_dir)
rmtree(zip_dest)
print('%s extracted.' % theme_dir)
sys.exit(0)
host = options.host
port = options.port
@@ -272,7 +336,8 @@ else:
ssl, 'PROTOCOL_%s' % options.ssl_version)
from butterfly import application
application.butterfly_dir = butterfly_dir
log.info('Starting server')
http_server = tornado_systemd.SystemdHTTPServer(
application, ssl_options=ssl_opts)
http_server.listen(port, address=host)

View File

@@ -1,96 +0,0 @@
#!/usr/bin/env python
import argparse
import os
import sys
try:
from urllib.request import urlopen
except ImportError:
from urllib import urlopen
from io import BytesIO
from shutil import move, rmtree
from zipfile import ZipFile
from tempfile import mkdtemp
from butterfly.escapes import sass
try:
import sass as _
_.CompileError
except Exception:
print('You must install libsass to use themes '
'(run: pip install libsass)')
sys.exit(1)
themes_url = ("https://github.com/paradoxxxzero/"
"butterfly-themes/archive/master.zip")
parser = argparse.ArgumentParser(description='Butterfly session opener.')
parser.add_argument(
'--install',
action="store_true",
help='Install preset themes from '
'https://github.com/paradoxxxzero/butterfly-themes')
parser.add_argument(
'--refresh',
action="store_true",
help='Refresh the current style')
parser.add_argument(
'-g',
action="store_true",
help='Install system wide for usage with butterfly as daemon')
parser.add_argument(
'theme',
nargs='?',
help='Theme to load. '
'Can be a sass file or a directory containing a sass file.')
args = parser.parse_args()
if args.g:
home_dir = os.path.join('/etc', 'butterfly')
else:
home_dir = os.path.expanduser(os.path.join('~', '.butterfly'))
if args.install:
print('Downloading %s...' % themes_url)
zip_ = ZipFile(BytesIO(urlopen(themes_url).read()))
print('Extracting in %s' % home_dir)
zip_.extractall(home_dir)
zip_dest = os.path.join(home_dir, 'butterfly-themes-master')
theme_dir = os.path.join(home_dir, 'themes')
if not os.path.exists(theme_dir):
os.makedirs(theme_dir)
tmp_dir = mkdtemp()
for dir_ in os.listdir(zip_dest):
if dir_ == 'README.md':
continue
new_dir = os.path.join(theme_dir, dir_)
if os.path.exists(new_dir):
move(new_dir, tmp_dir)
print('Old theme %s has been backed up in %s' % (
new_dir, tmp_dir))
move(os.path.join(zip_dest, dir_), theme_dir)
rmtree(zip_dest)
print('%s extracted.' % theme_dir)
elif args.refresh:
with sass():
pass
else:
sss = os.path.abspath(args.theme or home_dir)
if not os.path.exists(sss):
sss = os.path.join(home_dir, 'themes', args.theme)
if not os.path.exists(sss):
print('%s not found' % sss)
sys.exit(1)
print('Loading %s' % sss)
with sass():
sys.stdout.write(sss)

View File

@@ -3,6 +3,7 @@ from butterfly.escapes import image
from butterfly.utils import ansi_colors
import os
import base64
import shutil
print(ansi_colors.white + "Welcome to the butterfly help." + ansi_colors.reset)
with image('image/png'):
@@ -17,9 +18,11 @@ Butterfly is a xterm compliant terminal built with python and javascript.
{title}Terminal functionalities:{reset}
{strong}[Alt] + [a] : {reset}Set an alarm which sends a notification when a modification is detected.
{strong}[Ctrl] + [Shift] + [Up] : {reset}Trigger visual selection mode. Hitting [Enter] inserts the selection in the prompt.
{strong}[Alt] + [s] : {reset}Open theme selection prompt. Use [Alt] + [Shift] + [s] to refresh current theme.
{strong}[ScrollLock] : {reset}Lock the scrolling to the current position. Press again to release.
{strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key. Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]).
{strong}[Ctrl] + [c] <<hold>> : {reset}Cut the output when [Ctrl] + [c] is not enough.
{strong}[Alt] + [z] : {reset}Escape: don't catch the next pressed key.
Useful for using native search for example. ([Alt] + [z] then [Ctrl] + [f]).
{title}Butterfly programs:{reset}
@@ -29,35 +32,26 @@ Butterfly is a xterm compliant terminal built with python and javascript.
{strong}b16M : {reset}Test the 16M colors support in terminal.
{strong}bhr : {reset}Put a html hr. This is a test for html output.
{strong}bcal : {reset}Display current month using html. This is also a test for html output.
{strong}btheme : {reset}Set a theme as current theme. (Can also install preset themes)
{title}Styling butterfly:{reset}
To style butterfly in sass, you need to have the libsass python library installed.
Install official themes with {code}butterfly.server.py --install-themes{reset}
First check out existing themes, run:
{code}$ btheme --install{reset}
Then try:
{code}$ btheme solarized-dark {comment}# or C64 or abstract-image{reset}
If you want to set a theme as default add a 'theme="theme"' line in your butterfly.conf
Theming is done by overiding the default sass files located in {code}{main}{reset} in a theme directory.
This directory can include images and custom fonts.
Please take a look at official themes here: https://github.com/paradoxxxzero/butterfly-themes
and submit your best themes as pull request!
If you want to style it yourself do:
{code}$ cp {main} ~/.butterfly/style.sass {comment}# or /etc/butterfly/style.sass for system wide{reset}
and then edit this file.
You can also copy the imported sass files in the same dir. Run:
{code}$ btheme --refresh{reset}
To update the style in the current terminal.
Finally you can make a theme in the themes directory and submit it to https://github.com/paradoxxxzero/butterfly-themes
It is also possible to use a style.css file and re do all the styling in css exclusively.\
\x1b[{rcol}G\x1b[3m{dark}butterfly @ 2015 Mounier Florian{reset}\
""".format(
title=ansi_colors.light_blue,
dark=ansi_colors.light_black,
strong=ansi_colors.white,
code=ansi_colors.light_yellow,
comment=ansi_colors.light_magenta,
reset=ansi_colors.reset,
rcol=shutil.get_terminal_size()[0] - 31,
main=os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'../sass/main.sass'))))
'../sass/'))))

View File

@@ -0,0 +1,58 @@
# Butterfly autogenerated config file
# Activate debug mode
#
#debug=False
# In debug mode produce more verbose output
#
#more=False
# Use unminified version of js for development
#
#unminified=False
# Server host
# Use 'localhost' for local only
# Use your ip to share over your network
# Use '0.0.0.0' to listen to every network
#
#host='localhost'
# Server port
#
#port=57575
# Shell to launch at start (defaults to user shell)
#
#shell=None # shell='/bin/bash' for instance
# Motd, path to custom message of the day file
#
#motd='motd'
# Command to run instead of shell
#
#cmd=None # cmd='ls -l'
# Unsecure mode
# This mode use http without ssl and is therefore NOT RECOMMENDED
# Please generate yourself a certificate using the butterfly.server.py command
#
#unsecure=False
# Force user login in unsecure mode
#
#login=False
# Force unicode width
# This mode force every character to be the same width
# Which can be useful in some case
# But this breaks unicode display of varying width character
#
#force_unicode_width=False
# SSL version defaults to auto
#
#ssl_version=None

View File

@@ -20,6 +20,7 @@ import os
import sys
import tornado.options
import tornado.process
import tornado.escape
import tornado.web
import tornado.websocket
from mimetypes import guess_type
@@ -42,61 +43,67 @@ class Index(Route):
return self.render('index.html')
@url(r'/style.css')
class Style(Route):
def get(self):
default_style = os.path.join(
os.path.dirname(__file__), 'static', 'main.css')
@url(r'/theme/([^/]+)/style.css')
class Theme(Route):
def get(self, theme):
self.log.info('Getting style')
css = utils.get_style()
try:
import sass
sass.CompileError
except Exception:
self.log.error(
'You must install libsass to use sass '
'(pip install libsass)')
return
themes_dir = os.path.join(
self.application.butterfly_dir, 'themes')
base_dir = os.path.join(themes_dir, theme)
style = None
for ext in ['css', 'scss', 'sass']:
probable_style = os.path.join(base_dir, 'style.%s' % ext)
if os.path.exists(probable_style):
style = probable_style
if not style:
raise tornado.web.HTTPError(404)
sass_path = os.path.join(
os.path.dirname(__file__), 'sass')
css = None
try:
css = sass.compile(filename=style, include_paths=[
base_dir, sass_path])
except sass.CompileError:
self.log.error(
'Unable to compile style (filename: %s, paths: %r) ' % (
style, [base_dir, sass_path]), exc_info=True)
if not style:
raise tornado.web.HTTPError(500)
self.log.debug('Style ok')
self.set_header("Content-Type", "text/css")
if css:
self.write(css)
else:
with open(default_style) as s:
while True:
data = s.read(16384)
if data:
self.write(data)
else:
break
self.finish()
@url(r'/theme/font/([^/]+)')
class ThemeFont(Route):
@url(r'/theme/([^/]+)/(.+)')
class ThemeStatic(Route):
def get(self, name):
if not tornado.options.options.theme or not name:
raise tornado.web.HTTPError(404)
fn = os.path.join(
os.path.dirname(utils.get_style_path()), 'font', name)
if os.path.exists(fn):
self.set_header("Content-Type", guess_type(fn)[0])
with open(fn, 'rb') as s:
while True:
data = s.read(16384)
if data:
self.write(data)
else:
break
self.finish()
raise tornado.web.HTTPError(404)
def get(self, theme, name):
if '..' in name:
raise tornado.web.HTTPError(403)
themes_dir = os.path.join(
self.application.butterfly_dir, 'themes')
base_dir = os.path.join(themes_dir, theme)
@url(r'/theme/image/([^/]+)')
class ThemeImage(Route):
fn = os.path.normpath(os.path.join(base_dir, name))
if not fn.startswith(base_dir):
raise tornado.web.HTTPError(403)
def get(self, name):
if not tornado.options.options.theme or not name:
raise tornado.web.HTTPError(404)
fn = os.path.join(
os.path.dirname(utils.get_style_path()), 'image', name)
if os.path.exists(fn):
self.set_header("Content-Type", guess_type(fn)[0])
with open(fn, 'rb') as s:
@@ -115,7 +122,7 @@ class ThemeImage(Route):
'(?:session/(?P<session>[^/]+))?/?'
'(?:/wd/(?P<path>.+))?')
class TermWebSocket(Route, tornado.websocket.WebSocketHandler):
session_history_size = 100000
session_history_size = 50000
# List of websockets per session per user
# dict: user -> dict: session -> [TermWebSocket]
sessions = defaultdict(dict)
@@ -299,3 +306,21 @@ class Sessions(Route):
return self.render(
'list.html', sessions=TermWebSocket.sessions.get(user, []))
@url(r'/themes/list.json')
class ThemesList(Route):
"""Get the theme list"""
def get(self):
themes_dir = os.path.join(
self.application.butterfly_dir, 'themes')
self.set_header('Content-Type', 'application/json')
self.write(tornado.escape.json_encode({
'themes': sorted(
[theme
for theme in os.listdir(themes_dir)
if os.path.isdir(os.path.join(themes_dir, theme)) and
not theme.startswith('.')]),
'dir': themes_dir
}))

View File

@@ -38,5 +38,31 @@ body
&::-webkit-scrollbar-thumb:hover
background: rgba($fg, .15)
/* Pop ups */
.hidden
display: none
#popup
position: fixed
display: flex
align-items: center
justify-content: center
width: 100%
height: 100%
form
padding: 1.5em
background: rgba(127, 127, 127, .5)
h2
margin: .5em
select
min-width: 300px
padding: .5em
width: 100%
label
display: block
padding: .5em
font-size: .75em
.terminal
outline: none

View File

@@ -1,5 +1,5 @@
(function() {
var Selection, alt, cancel, copy, ctrl, first, nextLeaf, previousLeaf, selection, setAlarm, virtualInput,
var Popup, Selection, _set_theme_href, _theme, alt, cancel, copy, ctrl, first, nextLeaf, popup, previousLeaf, selection, setAlarm, virtualInput,
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; };
setAlarm = function(notification) {
@@ -77,6 +77,54 @@
return e.preventDefault();
});
Popup = (function() {
function Popup() {
this.el = document.getElementById('popup');
this.bound_click_maybe_close = this.click_maybe_close.bind(this);
this.bound_key_maybe_close = this.key_maybe_close.bind(this);
}
Popup.prototype.open = function(html) {
this.el.innerHTML = html;
this.el.classList.remove('hidden');
addEventListener('click', this.bound_click_maybe_close);
return addEventListener('keydown', this.bound_key_maybe_close);
};
Popup.prototype.close = function() {
removeEventListener('click', this.bound_click_maybe_close);
removeEventListener('keydown', this.bound_key_maybe_close);
this.el.classList.add('hidden');
return this.el.innerHTML = '';
};
Popup.prototype.click_maybe_close = function(e) {
var t;
t = e.target;
while (t.parentElement) {
if (Array.prototype.slice.call(this.el.children).indexOf(t) > -1) {
return true;
}
t = t.parentElement;
}
this.close();
return cancel(e);
};
Popup.prototype.key_maybe_close = function(e) {
if (e.keyCode !== 27) {
return true;
}
this.close();
return cancel(e);
};
return Popup;
})();
popup = new Popup();
selection = null;
cancel = function(ev) {
@@ -401,6 +449,85 @@
return sel.modify('extend', 'forward', 'character');
});
_set_theme_href = function(href) {
var img;
document.getElementById('style').setAttribute('href', href);
img = document.createElement('img');
img.onerror = function() {
return setTimeout((function() {
return typeof butterfly !== "undefined" && butterfly !== null ? butterfly.resize() : void 0;
}), 50);
};
return img.src = href;
};
_theme = typeof localStorage !== "undefined" && localStorage !== null ? localStorage.getItem('theme') : void 0;
if (_theme) {
_set_theme_href(_theme);
}
this.set_theme = function(theme) {
_theme = theme;
if (typeof localStorage !== "undefined" && localStorage !== null) {
localStorage.setItem('theme', theme);
}
if (theme) {
return _set_theme_href(theme);
}
};
document.addEventListener('keydown', function(e) {});
document.addEventListener('keydown', function(e) {
var oReq, style;
if (!(e.altKey && e.keyCode === 83)) {
return true;
}
if (e.shiftKey) {
style = document.getElementById('style').getAttribute('href');
style = style.split('?')[0];
document.getElementById('style').setAttribute('href', style + '?' + (new Date().getTime()));
return cancel(e);
}
oReq = new XMLHttpRequest();
oReq.addEventListener('load', function() {
var inner, j, len1, ref, response, theme, theme_list, themes, url;
response = JSON.parse(this.responseText);
themes = response.themes;
if (themes.length === 0) {
alert("No themes found in " + response.dir + ".\n Please install themes with butterfly.server.py --install-themes");
return;
}
inner = "<form>\n <h2>Pick a theme:</h2>\n <select id=\"theme_list\">";
ref = ['default'].concat(themes);
for (j = 0, len1 = ref.length; j < len1; j++) {
theme = ref[j];
if (theme === 'default') {
url = "/static/main.css";
} else {
url = "theme/" + theme + "/style.css";
}
inner += '<option ';
if (_theme === url) {
inner += 'selected ';
}
inner += "value=\"" + url + "\">";
inner += theme;
inner += '</option>';
}
inner += " </select>\n <label>You can create yours in " + response.dir + ".</label>\n</form>";
popup.open(inner);
theme_list = document.getElementById('theme_list');
return theme_list.addEventListener('change', function() {
return set_theme(theme_list.value);
});
});
oReq.open("GET", "/themes/list.json");
oReq.send();
return cancel(e);
});
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
ctrl = false;
alt = false;

File diff suppressed because one or more lines are too long

View File

@@ -3048,7 +3048,8 @@ html, body {
body {
white-space: nowrap;
overflow-x: hidden;
overflow-y: scroll; }
overflow-y: scroll;
/* Pop ups */ }
body::-webkit-scrollbar {
background: #110f13;
width: .75em; }
@@ -3056,6 +3057,28 @@ body {
background: rgba(255, 255, 255, 0.1); }
body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15); }
body .hidden {
display: none; }
body #popup {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%; }
body #popup form {
padding: 1.5em;
background: rgba(127, 127, 127, 0.5); }
body #popup form h2 {
margin: .5em; }
body #popup form select {
min-width: 300px;
padding: .5em;
width: 100%; }
body #popup form label {
display: block;
padding: .5em;
font-size: .75em; }
.terminal {
outline: none; }

View File

@@ -23,8 +23,6 @@
params = args.join(',');
if (type === 'Resize') {
return ws.send('R' + params);
} else if (type === 'Theme') {
return ws.send('T' + params);
}
};
if (location.protocol === 'https:') {
@@ -1245,7 +1243,7 @@
}
pt = pt.slice(1);
ref3 = pt.split('|', 2), type = ref3[0], content = ref3[1];
if (!content && type !== 'SASS') {
if (!content) {
console.error("No type for inline DECUDK: " + pt);
break;
}
@@ -1283,12 +1281,6 @@
l += content.length;
data = data.slice(0, i + 1) + content + data.slice(i + 1);
break;
case "SASS":
if (content.length) {
this.ctl('Theme', content);
}
setTimeout(this.refreshStyle.bind(this), 50);
break;
default:
console.error("Unknown type " + type + " for DECUDK");
}
@@ -2768,11 +2760,6 @@
return results;
};
Terminal.prototype.refreshStyle = function() {
document.getElementById('style').setAttribute('href', '/style.css?' + new Date().getTime());
return setTimeout(this.resize.bind(this), 300);
};
Terminal.prototype.charsets = {
SCLD: {
"`": "◆",

File diff suppressed because one or more lines are too long

View File

@@ -10,11 +10,13 @@
<link rel="shortcut icon" href="{{ static_url('images/favicon.png') }}">
<title>Butterfly</title>
<link href="/style.css" rel="stylesheet" id="style">
<link href="{{ static_url('main.css') }}" rel="stylesheet" id="style">
</head>
<body spellcheck="false"
data-force-unicode-width="{{ 'yes' if options.force_unicode_width else 'no' }}">
<div id="popup" class="hidden">
</div>
<script src="{{ static_url('html-sanitizer.js') }}"></script>
<script src="{{ static_url('main.%sjs' % (
'' if options.unminified else 'min.')) }}"></script>

35
coffees/ext/popup.coffee Normal file
View File

@@ -0,0 +1,35 @@
class Popup
constructor: ->
@el = document.getElementById('popup')
@bound_click_maybe_close = @click_maybe_close.bind(@)
@bound_key_maybe_close = @key_maybe_close.bind(@)
open: (html) ->
@el.innerHTML = html
@el.classList.remove 'hidden'
addEventListener 'click', @bound_click_maybe_close
addEventListener 'keydown', @bound_key_maybe_close
close: ->
removeEventListener 'click', @bound_click_maybe_close
removeEventListener 'keydown', @bound_key_maybe_close
@el.classList.add 'hidden'
@el.innerHTML = ''
click_maybe_close: (e) ->
t = e.target
while t.parentElement
return true if Array.prototype.slice.call(@el.children).indexOf(t) > -1
t = t.parentElement
@close()
cancel e
key_maybe_close: (e) ->
return true unless e.keyCode is 27
@close()
cancel e
popup = new Popup()

70
coffees/ext/theme.coffee Normal file
View File

@@ -0,0 +1,70 @@
_set_theme_href = (href) ->
document.getElementById('style').setAttribute('href', href)
img = document.createElement('img')
img.onerror = ->
setTimeout (-> butterfly?.resize()), 50
img.src = href;
_theme = localStorage?.getItem('theme')
_set_theme_href(_theme) if _theme
@set_theme = (theme) ->
_theme = theme
localStorage?.setItem('theme', theme)
_set_theme_href(theme) if theme
document.addEventListener 'keydown', (e) ->
document.addEventListener 'keydown', (e) ->
return true unless e.altKey and e.keyCode is 83
if e.shiftKey
style = document.getElementById('style').getAttribute('href')
style = style.split('?')[0]
document.getElementById('style').setAttribute(
'href', style + '?' + (new Date().getTime()))
return cancel(e)
oReq = new XMLHttpRequest()
oReq.addEventListener 'load', ->
response = JSON.parse(@responseText)
themes = response.themes
if themes.length is 0
alert("No themes found in #{response.dir}.\n
Please install themes with butterfly.server.py --install-themes")
return
inner = """
<form>
<h2>Pick a theme:</h2>
<select id="theme_list">
"""
for theme in ['default'].concat(themes)
if theme is 'default'
url = "/static/main.css"
else
url = "theme/#{theme}/style.css"
inner += '<option '
if _theme is url
inner += 'selected '
inner += "value=\"#{url}\">"
inner += theme
inner += '</option>'
inner += """
</select>
<label>You can create yours in #{response.dir}.</label>
</form>
"""
popup.open inner
theme_list = document.getElementById('theme_list')
theme_list.addEventListener 'change', -> set_theme theme_list.value
oReq.open("GET", "/themes/list.json")
oReq.send()
cancel e

View File

@@ -30,8 +30,6 @@ document.addEventListener 'DOMContentLoaded', ->
params = args.join(',')
if type == 'Resize'
ws.send 'R' + params
else if type == 'Theme'
ws.send 'T' + params
if location.protocol == 'https:'
wsUrl = 'wss://'

View File

@@ -1130,7 +1130,7 @@ class Terminal
[type, content] = pt.split('|', 2)
if not content and type isnt 'SASS'
if not content
console.error "No type for inline DECUDK: #{pt}"
break
@@ -1170,12 +1170,6 @@ class Terminal
l += content.length
data = data.slice(0, i + 1) + content + data.slice(i + 1)
when "SASS"
# sass file
if content.length
@ctl 'Theme', content
setTimeout @refreshStyle.bind(@), 50
else
console.error "Unknown type #{type} for DECUDK"
@@ -3120,12 +3114,6 @@ class Terminal
@screen[i].wrap = false
i++
refreshStyle: ->
document.getElementById('style').setAttribute(
'href', '/style.css?' + new Date().getTime())
setTimeout @resize.bind(@), 300
# DEC Special Character and Line Drawing Set.
# http://vt100.net/docs/vt102-ug/table5-13.html
# A lot of curses apps use this if they see TERM=xterm.

View File

@@ -25,6 +25,7 @@ options = dict(
scripts=['butterfly.server.py'],
packages=['butterfly'],
install_requires=["tornado>=3.2", "pyOpenSSL", 'tornado_systemd'],
extras_requires=["libsass"],
package_data={
'butterfly': [
'sass/*.sass',
@@ -35,7 +36,8 @@ options = dict(
'static/*.min.js',
'templates/index.html',
'bin/*',
'templates/motd'
'templates/motd',
'butterfly.conf.default'
]
},
classifiers=[