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:
Gina Häußge 2015-02-24 14:14:52 +01:00
parent a6e5ea268b
commit e9623fdc36
6 changed files with 137 additions and 39 deletions

View file

@ -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

View file

@ -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();

View file

@ -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();
}
};
}

View file

@ -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;
}
}

View file

@ -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>&nbsp;|&nbsp;<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>