mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-05-26 07:08:08 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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/'))))
|
||||
|
||||
58
butterfly/butterfly.conf.default
Normal file
58
butterfly/butterfly.conf.default
Normal 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
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
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
@@ -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; }
|
||||
|
||||
@@ -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: {
|
||||
"`": "◆",
|
||||
|
||||
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
@@ -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
35
coffees/ext/popup.coffee
Normal 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
70
coffees/ext/theme.coffee
Normal 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
|
||||
@@ -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://'
|
||||
|
||||
@@ -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.
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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=[
|
||||
|
||||
Reference in New Issue
Block a user