Skip to content

Commit

Permalink
Improved behaviour of terminal window
Browse files Browse the repository at this point in the history
  * 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
  • Loading branch information
foosel committed Feb 24, 2015
1 parent a6e5ea2 commit e9623fd
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 39 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/octoprint/static/css/octoprint.css

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions src/octoprint/static/js/app/main.js
Expand Up @@ -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();

Expand Down
104 changes: 79 additions & 25 deletions src/octoprint/static/js/app/viewmodels/terminal.js
Expand Up @@ -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);

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

Expand Down Expand Up @@ -155,10 +207,12 @@ $(function() {
};

self.onAfterTabChange = function(current, previous) {
if (current != "#terminal") {
if (current != "#term") {
return;
}
self.updateOutput();
if (self.autoscrollEnabled()) {
self.scrollToEnd();
}
};

}
Expand Down
13 changes: 11 additions & 2 deletions src/octoprint/static/less/octoprint.less
Expand Up @@ -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;
}
}

Expand Down
30 changes: 19 additions & 11 deletions src/octoprint/templates/tabs/terminal.jinja2
@@ -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>

0 comments on commit e9623fd

Please sign in to comment.