You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
545 lines
22 KiB
545 lines
22 KiB
/**@license
|
|
* __ _____ ________ __
|
|
* / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / /
|
|
* __ / // // // // // _ // _// // / / // _ // _// // // \/ // _ \/ /
|
|
* / / // // // // // ___// / / // / / // ___// / / / / // // /\ // // / /__
|
|
* \___//____ \\___//____//_/ _\_ / /_//____//_/ /_/ /_//_//_/ /_/ \__\_\___/
|
|
* \/ /____/
|
|
* http://terminal.jcubic.pl
|
|
*
|
|
* This is example of how to create less like command for jQuery Terminal
|
|
* the code is based on the one from leash shell and written as jQuery plugin
|
|
*
|
|
* Copyright (c) 2018-2021 Jakub Jankiewicz <https://jcubic.pl/me>
|
|
* Released under the MIT license
|
|
*
|
|
*/
|
|
/* global define */
|
|
(function(factory, undefined) {
|
|
var root = typeof window !== 'undefined' ? window : global;
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD. Register as an anonymous module.
|
|
// istanbul ignore next
|
|
define(['jquery', 'jquery.terminal'], factory);
|
|
} else if (typeof module === 'object' && module.exports) {
|
|
// Node/CommonJS
|
|
module.exports = function(root, jQuery) {
|
|
if (jQuery === undefined) {
|
|
// require('jQuery') returns a factory that requires window to
|
|
// build a jQuery instance, we normalize how we use modules
|
|
// that require this pattern but the window provided is a noop
|
|
// if it's defined (how jquery works)
|
|
if (window !== undefined) {
|
|
jQuery = require('jquery');
|
|
} else {
|
|
jQuery = require('jquery')(root);
|
|
}
|
|
}
|
|
if (!jQuery.fn.terminal) {
|
|
if (window !== undefined) {
|
|
require('jquery.terminal');
|
|
} else {
|
|
require('jquery.terminal')(jQuery);
|
|
}
|
|
}
|
|
factory(jQuery);
|
|
return jQuery;
|
|
};
|
|
} else {
|
|
// Browser
|
|
// istanbul ignore next
|
|
factory(root.jQuery);
|
|
}
|
|
})(function($) {
|
|
var img_split_re = /(\[\[(?:[^;]*@[^;]*);[^;]*;[^\]]*\]\s*\])/;
|
|
var img_re = /\[\[(?:[^;]*@[^;]*);[^;]*;[^;]*;[^;]*;([^;]*)\] ?\]/;
|
|
// -------------------------------------------------------------------------
|
|
function find(arr, fn) {
|
|
for (var i in arr) {
|
|
if (fn(arr[i])) {
|
|
return arr[i];
|
|
}
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------------
|
|
// $.when is always async we don't want that for normal non images
|
|
// -------------------------------------------------------------------------
|
|
function unpromise(args, fn) {
|
|
var found = find(args, function(arg) {
|
|
return typeof arg.then === 'function';
|
|
});
|
|
if (found) {
|
|
return $.when.apply($, args).then(fn);
|
|
} else {
|
|
return fn.apply(null, args);
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------------
|
|
// slice images into terminal lines - each line is unique blob url
|
|
function slice_image(img_data, width, y1, y2) {
|
|
// render slice on canvas and get Blob Data URI
|
|
var canvas = document.createElement('canvas');
|
|
var ctx = canvas.getContext('2d');
|
|
canvas.width = width;
|
|
canvas.height = y2 - y1;
|
|
ctx.putImageData(img_data, 0, 0);
|
|
var defer = $.Deferred();
|
|
canvas.toBlob(function(blob) {
|
|
if (blob === null) {
|
|
defer.resolve(null);
|
|
} else {
|
|
defer.resolve(URL.createObjectURL(blob));
|
|
}
|
|
});
|
|
return defer.promise();
|
|
}
|
|
function slice(src, options) {
|
|
var settings = $.extend({
|
|
width: null,
|
|
line_height: null
|
|
}, options);
|
|
var img = new Image();
|
|
var defer = $.Deferred();
|
|
var slices = [];
|
|
img.onload = function() {
|
|
var height, width;
|
|
if (settings.width < img.width) {
|
|
height = Math.floor((img.height * settings.width) / img.width);
|
|
width = settings.width;
|
|
} else {
|
|
height = img.height;
|
|
width = img.width;
|
|
}
|
|
var canvas = document.createElement('canvas');
|
|
var ctx = canvas.getContext('2d');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
// scale the image to fit the terminal
|
|
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, width, height);
|
|
(function recur(start) {
|
|
// loop over slices
|
|
if (start < height) {
|
|
var y1 = start, y2 = start + settings.line_height;
|
|
if (y2 > height) {
|
|
y2 = height;
|
|
}
|
|
var img_data = ctx.getImageData(0, y1, width, y2);
|
|
slice_image(img_data, width, y1, y2).then(function(uri) {
|
|
slices.push(uri);
|
|
recur(y2);
|
|
});
|
|
} else {
|
|
defer.resolve(slices);
|
|
}
|
|
})(0);
|
|
};
|
|
img.onerror = function() {
|
|
defer.reject('Error loading the image: ' + src);
|
|
};
|
|
// images need to have CORS if on different server,
|
|
// without this it will throw error
|
|
img.crossOrigin = "anonymous";
|
|
img.src = src;
|
|
return defer.promise();
|
|
}
|
|
// -----------------------------------------------------------------------------------
|
|
function less(term, text, options) {
|
|
var export_data = term.export_view();
|
|
var cols, rows;
|
|
var pos = 0;
|
|
var original_lines;
|
|
var lines;
|
|
var prompt = '';
|
|
var left = 0;
|
|
var $output = term.find('.terminal-output');
|
|
var had_cache = term.option('useCache');
|
|
if (!had_cache) {
|
|
term.option('useCache', true);
|
|
}
|
|
var cmd = term.cmd();
|
|
var scroll_by = 3;
|
|
//term.on('mousewheel', wheel);
|
|
var in_search = false, last_found, search_string;
|
|
// -------------------------------------------------------------------------------
|
|
function print() {
|
|
// performance optimization
|
|
term.find('.terminal-output').css('visibilty', 'hidden');
|
|
term.clear();
|
|
if (lines.length - pos > rows - 1) {
|
|
prompt = ':';
|
|
} else {
|
|
prompt = '[[;;;cmd-inverted](END)]';
|
|
}
|
|
term.set_prompt(prompt);
|
|
var to_print = lines.slice(pos, pos + rows - 1);
|
|
var should_substring = options.wrap ? false : to_print.filter(function(line) {
|
|
var len = $.terminal.length(line);
|
|
return len > cols;
|
|
}).length;
|
|
if (should_substring) {
|
|
to_print = to_print.map(function(line) {
|
|
return $.terminal.substring(line, left, left + cols - 1);
|
|
});
|
|
}
|
|
if (to_print.length < rows - 1) {
|
|
while (rows - 1 > to_print.length) {
|
|
to_print.push('~');
|
|
}
|
|
}
|
|
term.echo(to_print.join('\n'));
|
|
if (term.find('.terminal-output').is(':empty')) {
|
|
// sometimes the output is not flushed not idea why
|
|
// TODO: investigate
|
|
term.flush();
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
function quit() {
|
|
term.pop().import_view(export_data);
|
|
clear_cache();
|
|
term.removeClass('terminal-less');
|
|
$output.css('height', '');
|
|
var exit = options.exit || options.onExit;
|
|
if ($.isFunction(exit)) {
|
|
exit();
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
var cache = {};
|
|
function clear_cache() {
|
|
if (!had_cache) {
|
|
term.option('useCache', false).clear_cache();
|
|
}
|
|
Object.keys(cache).forEach(function(width) {
|
|
Object.keys(cache[width]).forEach(function(img) {
|
|
cache[width][img].forEach(function(uri) {
|
|
URL.revokeObjectURL(uri);
|
|
});
|
|
});
|
|
});
|
|
cache = {};
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
function fixed_output() {
|
|
// this will not change on resize, but the font size may change
|
|
var height = cmd.outerHeight(true);
|
|
term.addClass('terminal-less');
|
|
$output.css('height', 'calc(100% - ' + height + 'px)');
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
function refresh_view() {
|
|
cols = term.cols();
|
|
rows = term.rows();
|
|
fixed_output();
|
|
function cont(l) {
|
|
original_lines = process_optional_wrap(l);
|
|
lines = original_lines.slice();
|
|
if (in_search) {
|
|
search(last_found);
|
|
} else {
|
|
print();
|
|
}
|
|
}
|
|
function process_optional_wrap(arg) {
|
|
if (arg instanceof Array) {
|
|
if (options.wrap) {
|
|
arg = arg.join('\n');
|
|
return $.terminal.split_equal(arg, cols, options.keepWords);
|
|
}
|
|
return arg;
|
|
} else if (options.wrap) {
|
|
return $.terminal.split_equal(arg, cols, options.keepWords);
|
|
} else {
|
|
return [arg];
|
|
}
|
|
}
|
|
function run(arg) {
|
|
var text;
|
|
if (arg instanceof Array) {
|
|
if (options.formatters) {
|
|
text = arg.join('\n');
|
|
} else {
|
|
original_lines = arg;
|
|
}
|
|
} else {
|
|
text = arg;
|
|
}
|
|
if (text) {
|
|
if (options.formatters) {
|
|
text = $.terminal.apply_formatters(text);
|
|
} else {
|
|
// prism text will be boken when there are nestings (xml)
|
|
// and empty formattings
|
|
text = $.terminal.nested_formatting(text);
|
|
text = $.terminal.normalize(text);
|
|
}
|
|
unpromise([image_formatter(text)], cont);
|
|
} else {
|
|
unpromise(original_lines.map(image_formatter), function() {
|
|
var l = Array.prototype.concat.apply([], arguments);
|
|
cont(l);
|
|
});
|
|
}
|
|
}
|
|
if ($.isFunction(text)) {
|
|
text(cols, run);
|
|
} else {
|
|
run(text);
|
|
}
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
function cursor_size() {
|
|
var cursor = term.find('.cmd-cursor')[0];
|
|
return cursor.getBoundingClientRect();
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
function image_formatter(text) {
|
|
var defer = $.Deferred();
|
|
if (!text.match(img_re)) {
|
|
return text.split('\n');
|
|
}
|
|
var parts = text.split(img_split_re).filter(Boolean);
|
|
var result = [];
|
|
(function recur() {
|
|
function concat_slices(slices) {
|
|
cache[width][img] = slices;
|
|
result = result.concat(slices.map(function(uri) {
|
|
return '[[@;;;;' + uri + ']]';
|
|
}));
|
|
recur();
|
|
}
|
|
if (!parts.length) {
|
|
return defer.resolve(result);
|
|
}
|
|
var part = parts.shift();
|
|
var m = part.match(img_re);
|
|
if (m) {
|
|
var img = m[1];
|
|
var rect = cursor_size();
|
|
var width = term.width();
|
|
var opts = {
|
|
width: width,
|
|
line_height: Math.floor(rect.height)
|
|
};
|
|
cache[width] = cache[width] || {};
|
|
if (cache[width][img]) {
|
|
concat_slices(cache[width][img]);
|
|
} else {
|
|
slice(img, opts).then(concat_slices).catch(function() {
|
|
var msg = $.terminal.escape_brackets('[BROKEN IMAGE]');
|
|
var cls = 'terminal-broken-image';
|
|
result.push('[[;#c00;;' + cls + ']' + msg + ']');
|
|
recur();
|
|
});
|
|
}
|
|
} else {
|
|
if (part !== '\n') {
|
|
result = result.concat(part.split('\n'));
|
|
}
|
|
recur();
|
|
}
|
|
})();
|
|
return defer.promise();
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
function search(start, reset) {
|
|
var escape = $.terminal.escape_brackets(search_string);
|
|
var flag = search_string.toLowerCase() === search_string ? 'i' : '';
|
|
var start_re = new RegExp('^(' + escape + ')', flag);
|
|
var index = -1;
|
|
var prev_format = '';
|
|
var formatting = false;
|
|
var in_text = false;
|
|
var count = 0;
|
|
lines = original_lines.slice();
|
|
if (reset) {
|
|
index = pos = 0;
|
|
}
|
|
for (var i = start; i < lines.length; ++i) {
|
|
var line = lines[i];
|
|
for (var j = 0, jlen = line.length; j < jlen; ++j) {
|
|
if (line[j] === '[' && line[j + 1] === '[') {
|
|
formatting = true;
|
|
in_text = false;
|
|
start = j;
|
|
} else if (formatting && line[j] === ']') {
|
|
if (in_text) {
|
|
formatting = false;
|
|
in_text = false;
|
|
} else {
|
|
in_text = true;
|
|
prev_format = line.substring(start, j + 1);
|
|
}
|
|
} else if (formatting && in_text || !formatting) {
|
|
if (line.substring(j).match(start_re)) {
|
|
var rep;
|
|
if (formatting && in_text) {
|
|
var style = prev_format.match(/\[\[([^;]+)/);
|
|
var new_format = ';;;terminal-inverted';
|
|
style = style ? style[1] : '';
|
|
if (style.match(/!/)) {
|
|
new_format = style + new_format + ';';
|
|
new_format += prev_format.replace(/]$/, '')
|
|
.split(';').slice(4).join(';');
|
|
}
|
|
rep = '][[' + new_format + ']$1]' + prev_format;
|
|
} else {
|
|
rep = '[[;;;terminal-inverted]$1]';
|
|
}
|
|
line = line.substring(0, j) +
|
|
line.substring(j).replace(start_re, rep);
|
|
j += rep.length - 2;
|
|
if (i >= pos && index === -1) {
|
|
index = pos = i;
|
|
}
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
lines[i] = line;
|
|
}
|
|
print();
|
|
term.set_command('');
|
|
term.set_prompt(prompt);
|
|
if (count === 1) {
|
|
return -1;
|
|
}
|
|
return index;
|
|
}
|
|
// -------------------------------------------------------------------------------
|
|
function scroll(delta, scroll_by) {
|
|
if (delta > 0) {
|
|
pos -= scroll_by;
|
|
if (pos < 0) {
|
|
pos = 0;
|
|
}
|
|
} else {
|
|
pos += scroll_by;
|
|
if (pos - 1 > lines.length - rows) {
|
|
pos = lines.length - rows + 1;
|
|
}
|
|
}
|
|
print();
|
|
return false;
|
|
}
|
|
term.push($.noop, {
|
|
onResize: refresh_view,
|
|
touchscroll: function(event, delta) {
|
|
var offset = Math.abs(delta);
|
|
scroll(delta, Math.round(offset / 14));
|
|
return false;
|
|
},
|
|
onPaste: function() {
|
|
if (term.get_prompt() !== '/') {
|
|
return false;
|
|
}
|
|
},
|
|
mousewheel: function(event, delta) {
|
|
return scroll(delta, scroll_by);
|
|
},
|
|
name: 'less',
|
|
keydown: function(e) {
|
|
var command = term.get_command();
|
|
var key = e.key.toUpperCase();
|
|
if (term.get_prompt() !== '/') {
|
|
if (key === '/') {
|
|
term.set_prompt('/');
|
|
} else if (in_search &&
|
|
$.inArray(e.which, [78, 80]) !== -1) {
|
|
if (key === 'N') { // search_string
|
|
if (last_found !== -1) {
|
|
var ret = search(last_found + 1);
|
|
if (ret !== -1) {
|
|
last_found = ret;
|
|
}
|
|
}
|
|
} else if (key === 'P') {
|
|
last_found = search(0, true);
|
|
}
|
|
} else if (key === 'Q') {
|
|
quit();
|
|
} else if (key === 'ARROWRIGHT') {
|
|
if (!options.wrap) {
|
|
left += Math.round(cols / 2);
|
|
print();
|
|
}
|
|
} else if (key === 'ARROWLEFT') {
|
|
if (!options.wrap) {
|
|
left -= Math.round(cols / 2);
|
|
if (left < 0) {
|
|
left = 0;
|
|
}
|
|
print();
|
|
// scroll
|
|
}
|
|
} else if (lines.length > rows) {
|
|
if (key === 'ARROWUP') { //up
|
|
if (pos > 0) {
|
|
--pos;
|
|
print();
|
|
}
|
|
} else if (key === 'ARROWDOWN') { //down
|
|
if (pos <= lines.length - rows) {
|
|
++pos;
|
|
print();
|
|
}
|
|
} else if (key === 'PAGEDOWN') {
|
|
pos += rows - 1;
|
|
var limit = lines.length - rows + 1;
|
|
if (pos > limit) {
|
|
pos = limit;
|
|
}
|
|
print();
|
|
} else if (key === 'PAGEUP') {
|
|
//Page Down
|
|
pos -= rows - 1;
|
|
if (pos < 0) {
|
|
pos = 0;
|
|
}
|
|
print();
|
|
}
|
|
}
|
|
if (!e.ctrlKey && !e.alKey) {
|
|
return false;
|
|
}
|
|
// search
|
|
} else if (e.which === 8 && command === '') {
|
|
// backspace
|
|
term.set_prompt(prompt);
|
|
} else if (e.which === 13) { // enter
|
|
// basic search find only first
|
|
if (command.length > 0) {
|
|
in_search = true;
|
|
pos = 0;
|
|
search_string = command;
|
|
last_found = search(0);
|
|
}
|
|
// this will disable history
|
|
return false;
|
|
}
|
|
},
|
|
prompt: prompt
|
|
});
|
|
// -------------------------------------------------------------------------------
|
|
refresh_view();
|
|
}
|
|
// -----------------------------------------------------------------------------------
|
|
$.fn.less = function(text, options) {
|
|
var settings = $.extend({
|
|
onExit: $.noop,
|
|
formatters: false
|
|
}, options);
|
|
if (!(this instanceof $.fn.init && this.terminal)) {
|
|
throw new Error('This plugin require jQuery Terminal');
|
|
}
|
|
var term = this.terminal();
|
|
if (!term) {
|
|
throw new Error(
|
|
'You need to invoke this plugin on selector that have ' +
|
|
'jQuery Terminal or on jQuery Terminal instance'
|
|
);
|
|
}
|
|
less(term, text, settings);
|
|
return term;
|
|
};
|
|
});
|
|
|