Improved behaviour of terminal window
* Disabling autoscrolling now also stops cutting of the log while it's enabled, effectively preventing log lines from being modified at all * Applying filters displays "[...]" where lines where removed * Added a link to scroll to the end of the terminal log (useful for when autoscroll is disabled) * Added a link to select all current contents of the terminal log for easy copy-pasting * Added a display of how many lines are displayed, how many are filtered and how many are available in total Closes #735
This commit is contained in:
parent
a6e5ea268b
commit
e9623fdc36
6 changed files with 137 additions and 39 deletions
|
|
@ -83,6 +83,13 @@
|
|||
message for now.
|
||||
* Daemonized OctoPrint now cleans up its pidfile when receiving a TERM signal ([#711](https://github.com/foosel/OctoPrint/issues/711))
|
||||
* Added serial types for OpenBSD ([#551](https://github.com/foosel/OctoPrint/pull/551))
|
||||
* Improved behaviour of terminal:
|
||||
* Disabling autoscrolling now also stops cutting of the log while it's enabled, effectively preventing log lines from
|
||||
being modified at all ([#735](https://github.com/foosel/OctoPrint/issues/735))
|
||||
* Applying filters displays ``[...]`` where lines where removed
|
||||
* Added a link to scroll to the end of the terminal log (useful for when autoscroll is disabled)
|
||||
* Added a link to select all current contents of the terminal log for easy copy-pasting
|
||||
* Added a display of how many lines are displayed, how many are filtered and how many are available in total
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -236,6 +236,26 @@ $(function() {
|
|||
return $(window).height() - 165;
|
||||
};
|
||||
|
||||
// jquery plugin to select all text in an element
|
||||
// originally from: http://stackoverflow.com/a/987376
|
||||
$.fn.selectText = function() {
|
||||
var doc = document;
|
||||
var element = this[0];
|
||||
var range, selection;
|
||||
|
||||
if (doc.body.createTextRange) {
|
||||
range = document.body.createTextRange();
|
||||
range.moveToElementText(element);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
selection = window.getSelection();
|
||||
range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
};
|
||||
|
||||
// Use bootstrap tabdrop for tabs and pills
|
||||
$('.nav-pills, .nav-tabs').tabdrop();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ $(function() {
|
|||
self.loginState = parameters[0];
|
||||
self.settings = parameters[1];
|
||||
|
||||
self.log = [];
|
||||
self.log = ko.observableArray([]);
|
||||
self.buffer = ko.observable(300);
|
||||
|
||||
self.command = ko.observable(undefined);
|
||||
|
||||
|
|
@ -20,15 +21,56 @@ $(function() {
|
|||
self.autoscrollEnabled = ko.observable(true);
|
||||
|
||||
self.filters = self.settings.terminalFilters;
|
||||
self.filterRegex = undefined;
|
||||
self.filterRegex = ko.observable();
|
||||
|
||||
self.cmdHistory = [];
|
||||
self.cmdHistoryIdx = -1;
|
||||
|
||||
self.displayedLines = ko.computed(function() {
|
||||
var regex = self.filterRegex();
|
||||
var lineVisible = function(entry) {
|
||||
return regex == undefined || !entry.line.match(regex);
|
||||
};
|
||||
|
||||
var filtered = false;
|
||||
var result = [];
|
||||
_.each(self.log(), function(entry) {
|
||||
if (lineVisible(entry)) {
|
||||
result.push(entry);
|
||||
filtered = false;
|
||||
} else if (!filtered) {
|
||||
result.push(self._toInternalFormat("[...]", "filtered"));
|
||||
filtered = true;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
self.displayedLines.subscribe(function() {
|
||||
self.updateOutput();
|
||||
});
|
||||
|
||||
self.lineCount = ko.computed(function() {
|
||||
var total = self.log().length;
|
||||
var displayed = _.filter(self.displayedLines(), function(entry) { return entry.type == "line" }).length;
|
||||
var filtered = total - displayed;
|
||||
|
||||
if (total == displayed) {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed});
|
||||
} else {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered});
|
||||
}
|
||||
});
|
||||
|
||||
self.autoscrollEnabled.subscribe(function(newValue) {
|
||||
if (newValue) {
|
||||
self.log(self.log.slice(-self.buffer()));
|
||||
}
|
||||
});
|
||||
|
||||
self.activeFilters = ko.observableArray([]);
|
||||
self.activeFilters.subscribe(function(e) {
|
||||
self.updateFilterRegex();
|
||||
self.updateOutput();
|
||||
});
|
||||
|
||||
self.fromCurrentData = function(data) {
|
||||
|
|
@ -42,16 +84,21 @@ $(function() {
|
|||
};
|
||||
|
||||
self._processCurrentLogData = function(data) {
|
||||
if (!self.log)
|
||||
self.log = [];
|
||||
self.log = self.log.concat(data);
|
||||
self.log = self.log.slice(-300);
|
||||
self.updateOutput();
|
||||
self.log(self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) })));
|
||||
if (self.autoscrollEnabled()) {
|
||||
self.log(self.log.slice(-300));
|
||||
}
|
||||
};
|
||||
|
||||
self._processHistoryLogData = function(data) {
|
||||
self.log = data;
|
||||
self.updateOutput();
|
||||
self.log(_.map(data, function(line) { return self._toInternalFormat(line) }));
|
||||
};
|
||||
|
||||
self._toInternalFormat = function(line, type) {
|
||||
if (type == undefined) {
|
||||
type = "line";
|
||||
}
|
||||
return {line: line, type: type}
|
||||
};
|
||||
|
||||
self._processStateData = function(data) {
|
||||
|
|
@ -67,29 +114,34 @@ $(function() {
|
|||
self.updateFilterRegex = function() {
|
||||
var filterRegexStr = self.activeFilters().join("|").trim();
|
||||
if (filterRegexStr == "") {
|
||||
self.filterRegex = undefined;
|
||||
self.filterRegex(undefined);
|
||||
} else {
|
||||
self.filterRegex = new RegExp(filterRegexStr);
|
||||
self.filterRegex(new RegExp(filterRegexStr));
|
||||
}
|
||||
self.updateOutput();
|
||||
};
|
||||
|
||||
self.updateOutput = function() {
|
||||
if (!self.log)
|
||||
return;
|
||||
|
||||
var output = "";
|
||||
for (var i = 0; i < self.log.length; i++) {
|
||||
if (self.filterRegex !== undefined && self.log[i].match(self.filterRegex)) continue;
|
||||
output += self.log[i] + "\n";
|
||||
if (self.autoscrollEnabled()) {
|
||||
self.scrollToEnd();
|
||||
}
|
||||
};
|
||||
|
||||
self.toggleAutoscroll = function() {
|
||||
self.autoscrollEnabled(!self.autoscrollEnabled());
|
||||
};
|
||||
|
||||
self.selectAll = function() {
|
||||
var container = $("#terminal-output");
|
||||
if (container.length) {
|
||||
container.text(output);
|
||||
container.selectText();
|
||||
}
|
||||
};
|
||||
|
||||
if (self.autoscrollEnabled()) {
|
||||
container.scrollTop(container[0].scrollHeight - container.height())
|
||||
}
|
||||
self.scrollToEnd = function() {
|
||||
var container = $("#terminal-output");
|
||||
if (container.length) {
|
||||
container.scrollTop(container[0].scrollHeight - container.height())
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -155,10 +207,12 @@ $(function() {
|
|||
};
|
||||
|
||||
self.onAfterTabChange = function(current, previous) {
|
||||
if (current != "#terminal") {
|
||||
if (current != "#term") {
|
||||
return;
|
||||
}
|
||||
self.updateOutput();
|
||||
if (self.autoscrollEnabled()) {
|
||||
self.scrollToEnd();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -590,8 +590,17 @@ ul.dropdown-menu li a {
|
|||
/** Terminal output */
|
||||
|
||||
#term {
|
||||
#terminal-output {
|
||||
min-height: 340px;
|
||||
.terminal {
|
||||
#terminal-output {
|
||||
min-height: 340px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
#terminal-sendpanel {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
<pre id="terminal-output" class="pre-scrollable"></pre>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> {{ _('Autoscroll') }}
|
||||
</label>
|
||||
<div data-bind="foreach: filters">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
|
||||
</label>
|
||||
<div class="terminal">
|
||||
<pre id="terminal-output" class="pre-scrollable" data-bind="foreach: displayedLines"><span data-bind="text: line, css: {muted: type == 'filtered'}"></span><br></pre>
|
||||
<small class="pull-left"><button class="btn btn-mini" data-bind="click: toggleAutoscroll, css: {active: autoscrollEnabled}">{{ _('Autoscroll') }}</button> <span data-bind="text: lineCount"></span></small>
|
||||
<small class="pull-right"><a href="#" data-bind="click: scrollToEnd">{{ _("Scroll to end") }}</a> | <a href="#" data-bind="click: selectAll">{{ _("Select all") }}</a></small>
|
||||
</div>
|
||||
|
||||
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
|
||||
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
|
||||
<div class="row-fluid">
|
||||
<div class="span6" id="termin-filterpanel">
|
||||
<div data-bind="foreach: filters">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span6" id="terminal-sendpanel" style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<div class="input-append">
|
||||
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
|
||||
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
|
||||
</div>
|
||||
<small class="muted">{{ _('Hint: Use the arrow up/down keys to recall commands sent previously') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue