aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/StateHandler.qml158
-rw-r--r--src/command/CommandInput.qml146
-rw-r--r--src/command/commands.js55
-rw-r--r--src/list/TerminalItem.qml176
-rw-r--r--src/list/TerminalList.qml125
-rw-r--r--src/main.qml53
-rw-r--r--src/ui.qrc12
-rw-r--r--src/widget/EmbeddedTerminal.qml153
-rw-r--r--src/widget/Highlighter.qml33
9 files changed, 911 insertions, 0 deletions
diff --git a/src/StateHandler.qml b/src/StateHandler.qml
new file mode 100644
index 0000000..d9f5f5a
--- /dev/null
+++ b/src/StateHandler.qml
@@ -0,0 +1,158 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.2
+import Qt.labs.settings 1.0
+
+Item {
+ id: item
+
+ property Item terminalList : null
+ property CommandInput commandInput : null
+
+ Settings {
+ id: settings
+ category: "keybinding"
+
+ property string insertMode : "i"
+ property string normalMode : "Shift+ESC"
+ property string commandMode : ":"
+ property string nextItem : "j"
+ property string prevItem : "k"
+ property string firstItem : "g"
+ property string resetItem : "d"
+ property string lastItem : "Shift+G"
+ property string heightenItem : "Shift+J"
+ property string shortenItem : "Shift+K"
+ }
+
+ state: "INSERT"
+
+ function enterInsertMode() {
+ enterInsertAction.trigger();
+ }
+
+ function enterNormalMode() {
+ enterNormalAction.trigger();
+ }
+
+ states: [
+ State {
+ name: "NORMAL"
+
+ PropertyChanges { target: enterNormalAction; enabled: false }
+ PropertyChanges { target: enterInsertAction; enabled: true }
+ PropertyChanges { target: enterCommandAction; enabled: true }
+ PropertyChanges { target: nextTerminalAction; enabled: true }
+ PropertyChanges { target: heightenTerminalAction; enabled: true }
+ PropertyChanges { target: shortenTerminalAction; enabled: true }
+ PropertyChanges { target: prevTerminalAction; enabled: true }
+ PropertyChanges { target: lastTerminalAction; enabled: true }
+ PropertyChanges { target: firstTerminalAction; enabled: true }
+ PropertyChanges { target: resetTerminalAction; enabled: true }
+ },
+ State {
+ name: "INSERT"
+
+ PropertyChanges { target: enterNormalAction; enabled: true }
+ PropertyChanges { target: enterInsertAction; enabled: false }
+ PropertyChanges { target: enterCommandAction; enabled: false }
+ PropertyChanges { target: nextTerminalAction; enabled: false }
+ PropertyChanges { target: heightenTerminalAction; enabled: false }
+ PropertyChanges { target: shortenTerminalAction; enabled: false }
+ PropertyChanges { target: prevTerminalAction; enabled: false }
+ PropertyChanges { target: lastTerminalAction; enabled: false }
+ PropertyChanges { target: firstTerminalAction; enabled: false }
+ PropertyChanges { target: resetTerminalAction; enabled: false }
+ },
+ State {
+ name: "COMMAND"
+
+ PropertyChanges { target: enterNormalAction; enabled: true }
+ PropertyChanges { target: enterInsertAction; enabled: false }
+ PropertyChanges { target: enterCommandAction; enabled: false }
+ PropertyChanges { target: nextTerminalAction; enabled: false }
+ PropertyChanges { target: heightenTerminalAction; enabled: false }
+ PropertyChanges { target: shortenTerminalAction; enabled: false }
+ PropertyChanges { target: prevTerminalAction; enabled: false }
+ PropertyChanges { target: lastTerminalAction; enabled: false }
+ PropertyChanges { target: firstTerminalAction; enabled: false }
+ PropertyChanges { target: resetTerminalAction; enabled: false }
+ }
+ ]
+
+ Action {
+ id: enterNormalAction
+ shortcut: settings.normalMode
+ onTriggered: {
+ item.state = "NORMAL";
+
+ terminalList.forceActiveFocus();
+ terminalList.unfocusCurrent();
+ commandInput.unfocus();
+ }
+ }
+
+ Action {
+ id: enterInsertAction
+ shortcut: settings.insertMode
+ onTriggered: {
+ item.state = "INSERT";
+
+ terminalList.focusCurrent();
+ }
+ }
+
+ Action {
+ id: enterCommandAction
+ shortcut: settings.commandMode
+ onTriggered: {
+ item.state = "COMMAND";
+
+ commandInput.focus(shortcut);
+ }
+ }
+
+ Action {
+ id: nextTerminalAction
+ shortcut: settings.nextItem
+ onTriggered: terminalList.selectNext()
+ }
+
+ Action {
+ id: heightenTerminalAction
+ shortcut: settings.heightenItem
+ onTriggered: terminalList.getCurrent().heighten()
+ }
+
+ Action {
+ id: shortenTerminalAction
+ shortcut: settings.shortenItem
+ onTriggered: terminalList.getCurrent().shorten()
+ }
+
+ Action {
+ id: prevTerminalAction
+ shortcut: settings.prevItem
+ onTriggered: terminalList.selectPrev()
+ }
+
+ Action {
+ id: lastTerminalAction
+ shortcut: settings.lastItem
+ onTriggered: terminalList.selectItem(terminalList.children.length - 1)
+ }
+
+ Action {
+ id: firstTerminalAction
+ shortcut: settings.firstItem
+ onTriggered: terminalList.selectItem(0)
+ }
+
+ Action {
+ id: resetTerminalAction
+ shortcut: settings.resetItem
+ onTriggered: {
+ terminalList.getCurrent().reset();
+ terminalList.getCurrent().select();
+ }
+ }
+}
diff --git a/src/command/CommandInput.qml b/src/command/CommandInput.qml
new file mode 100644
index 0000000..99f5d0e
--- /dev/null
+++ b/src/command/CommandInput.qml
@@ -0,0 +1,146 @@
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import Qt.labs.settings 1.0
+
+import "commands.js" as Commands
+
+Item {
+ id: item
+
+ signal executed
+
+ visible: false
+
+ Layout.preferredHeight: container.height
+
+ Settings {
+ id: settings
+ category: "command"
+
+ property string background : "black"
+ property int fontSize : 12
+ property string fontFamily : "Monospace"
+ property string fontColor : "white"
+ property string errorColor : "red"
+ }
+
+ onVisibleChanged: container.reset()
+
+ function focus(prefix) {
+ visible = true;
+ command.text = prefix;
+ command.forceActiveFocus();
+ }
+
+ function unfocus() {
+ visible = false;
+ }
+
+ Rectangle {
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ }
+
+ height: container.height
+
+ color: settings.background
+
+ ColumnLayout {
+ id: container
+
+ function reset() {
+ command.initialize();
+ output.initialize();
+ }
+
+ TextInput {
+ id: command
+
+ Layout.fillWidth: true
+
+ font {
+ family: settings.fontFamily
+ pointSize: settings.fontSize
+ }
+
+ color: settings.fontColor
+ selectionColor: settings.fontColor
+ selectedTextColor: settings.background
+ selectByMouse: true
+
+ function initialize() {
+ text = '';
+ }
+
+ onAccepted: {
+ output.initialize();
+
+ const prefix = String(text).charAt(0);
+ const cmd = String(text).substring(1, String(text).length);
+
+ switch ( prefix ) {
+ case ':': {
+ Commands.execute(output, cmd);
+ break;
+ }
+ default: {
+ output.error('"' + prefix + '"' + ' is not a command prefix.');
+ }
+ }
+
+ if ( output.isInitial() ) {
+ item.executed();
+ }
+ }
+ }
+
+ Text {
+ id: output
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 0
+
+ font {
+ family: settings.fontFamily
+ pointSize: settings.fontSize
+ }
+
+ color: settings.fontColor
+
+ function isInitial() {
+ return text === '';
+ }
+
+ function initialize() {
+ text = '';
+ }
+
+ function log(msg) {
+ if ( isInitial() ) {
+ text = msg;
+ } else {
+ text += '<br/>' + msg;
+ }
+ }
+
+ function error(msg) {
+ text = '<i><font color="'
+ + settings.errorColor
+ + '">'
+ + msg
+ + '</font></i>';
+ }
+
+ onTextChanged: {
+ if ( isInitial() ) {
+ Layout.preferredHeight = 0;
+ } else {
+ Layout.preferredHeight = contentHeight;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/command/commands.js b/src/command/commands.js
new file mode 100644
index 0000000..f76af01
--- /dev/null
+++ b/src/command/commands.js
@@ -0,0 +1,55 @@
+function execute(output, command) {
+ var notImplemented = function(name) {
+ output.error('"' + name + '"' + ' is not implemented.');
+ };
+ var args = command.split(' ');
+
+ try {
+ var closure = eval(args[0]);
+
+ if ( typeof closure === 'function' ) {
+ args.shift();
+ closure(output, args);
+ } else {
+ notImplemented(args[0]);
+ }
+ } catch (exception) {
+ notImplemented(args[0]);
+ }
+}
+
+function exec(output, args) {
+ try {
+ var result = eval(args.join(' '));
+
+ if ( typeof result !== 'undefined' ) {
+ output.log(result);
+ }
+ } catch (exception) {
+ output.error(exception);
+ }
+}
+
+function jump(output, index) {
+ terminalList.selectItem(index);
+}
+
+function kill(output, index) {
+ terminalList.get(index).reset();
+}
+
+function next() {
+ terminalList.selectNext();
+}
+
+function prev() {
+ terminalList.selectPrev();
+}
+
+function ls(output) {
+ terminalList.iterate(function(item) {
+ if ( item.terminal !== null ) {
+ output.log(item.index + ': ' + item.terminal.program);
+ }
+ });
+}
diff --git a/src/list/TerminalItem.qml b/src/list/TerminalItem.qml
new file mode 100644
index 0000000..57197bd
--- /dev/null
+++ b/src/list/TerminalItem.qml
@@ -0,0 +1,176 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import Qt.labs.settings 1.0
+
+Item {
+ id: item
+
+ property int index : 0
+ property EmbeddedTerminal terminal : null
+
+ signal executed (int index)
+
+ Settings {
+ id: settings
+ category: "item"
+
+ property int fontSize : 18
+ property string fontFamily : "Monospace"
+ property string fontColor : "white"
+ }
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+
+ height: elementList.height
+
+ function select() {
+ if ( terminal === null ) {
+ highlighter.select();
+ } else {
+ terminal.select();
+ }
+ }
+
+ function deselect() {
+ if ( terminal === null ) {
+ highlighter.deselect();
+ } else {
+ terminal.deselect();
+ }
+ }
+
+ function forceActiveFocus() {
+ scope.forceActiveFocus();
+
+ if ( terminal === null ) {
+ scope.forceActiveFocus();
+ highlighter.select();
+ highlighter.focus();
+ }
+ }
+
+ function unfocus() {
+ if ( terminal === null ) {
+ highlighter.unfocus();
+ }
+ }
+
+ function heighten() {
+ if ( terminal !== null ) {
+ terminal.lines += 1;
+ }
+ }
+
+ function shorten() {
+ if ( terminal !== null ) {
+ if ( terminal.lines > 10 ) {
+ terminal.lines -= 1;
+ } else {
+ terminal.displayOverlay();
+ }
+ }
+ }
+
+ function reset() {
+ if ( terminal !== null ) {
+ terminal.destroy();
+
+ terminal = null;
+ command.readOnly = false;
+ command.focus = true;
+
+ unfocus();
+ }
+ }
+
+ FocusScope {
+ id: scope
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+
+ Column {
+ id: elementList
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+
+ function createTerminal(program) {
+ var terminalComponent = Qt.createComponent("qrc:/EmbeddedTerminal.qml");
+ var instantiateTerminal = function() {
+ item.terminal = terminalComponent.createObject(elementList, {
+ "program" : program,
+ "workingDirectory" : "$HOME",
+ "focus" : true
+ });
+ }
+
+ if ( terminalComponent.status === Component.Ready ) {
+ instantiateTerminal();
+ } else {
+ terminalComponent.statusChanged.connect(instantiateTerminal);
+ }
+ }
+
+ RowLayout {
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+
+ Highlighter {
+ id: highlighter
+
+ width: 10
+ height: command.height
+ }
+
+ TextInput {
+ id: command
+
+ font {
+ family: settings.fontFamily
+ pointSize: settings.fontSize
+ }
+
+ color: settings.fontColor
+ selectionColor: settings.fontColor
+ selectedTextColor: "#161616"
+
+ selectByMouse: true
+ focus: true
+ Layout.fillWidth: true
+
+ onAccepted: {
+ if ( item.terminal === null ) {
+ readOnly = true;
+ focus = false;
+
+ elementList.createTerminal(text);
+ item.executed(item.index);
+ highlighter.deselect();
+ }
+ }
+ }
+
+ Text {
+ font {
+ family: settings.fontFamily
+ pointSize: settings.fontSize / 1.5
+ }
+ color: settings.fontColor
+
+ text: item.index
+ }
+ }
+ }
+ }
+}
diff --git a/src/list/TerminalList.qml b/src/list/TerminalList.qml
new file mode 100644
index 0000000..6c6465b
--- /dev/null
+++ b/src/list/TerminalList.qml
@@ -0,0 +1,125 @@
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+
+Item {
+ id: item
+
+ property StateHandler state : null
+ property int activeItem : 0
+ property int itemIndex : 0
+
+ property alias children : column.children
+
+ function onItemExecuted(index) {
+ if ( index === (children.length - 1) ) {
+ createItem();
+ }
+ }
+
+ function createItem() {
+ var terminalItem = Qt.createComponent("qrc:/TerminalItem.qml");
+ var instantiateTerminal = function() {
+ var instance = terminalItem.createObject(column, {
+ "index": itemIndex,
+ "width": flickable.width
+ });
+ instance.onExecuted.connect(onItemExecuted);
+
+ ++itemIndex;
+ }
+
+ if ( terminalItem.status === Component.Ready ) {
+ instantiateTerminal();
+ } else {
+ terminalItem.statusChanged.connect(instantiateTerminal);
+ }
+ }
+
+ function scrollTo(index) {
+ if ( column.height >= flickable.height ) {
+ var offset = children[index].y
+ + (children[index].height / 2)
+ - (flickable.height / 2);
+
+ var bound = column.height
+ - flickable.height;
+
+ if ( offset < 0 ) {
+ flickable.contentY = 0;
+ } else if ( offset >= bound ) {
+ flickable.contentY = bound;
+ } else {
+ flickable.contentY = offset;
+ }
+ }
+ }
+
+ function selectItem(index) {
+ children[activeItem].deselect();
+ children[index ].select();
+
+ activeItem = typeof index === "number" ? index : parseInt(index);
+
+ scrollTo(index);
+ }
+
+ function selectNext() {
+ if ( activeItem < (children.length - 1) ) {
+ selectItem(activeItem + 1);
+ } else {
+ state.enterInsertMode();
+ }
+ }
+
+ function selectPrev() {
+ if ( activeItem > 0 ) {
+ selectItem(activeItem - 1);
+ }
+ }
+
+ function focusCurrent() {
+ children[activeItem].forceActiveFocus();
+ }
+
+ function unfocusCurrent() {
+ children[activeItem].unfocus();
+ }
+
+ function getCurrent() {
+ return children[activeItem];
+ }
+
+ function get(index) {
+ return children[index];
+ }
+
+ function iterate(func) {
+ for ( var i = 0; i < children.length; i++ ) {
+ func(children[i]);
+ }
+ }
+
+ Flickable {
+ id: flickable
+
+ anchors.fill: parent
+
+ boundsBehavior: Flickable.StopAtBounds
+ contentHeight: column.height
+ contentWidth: parent.width
+ pixelAligned: true
+
+ Column {
+ id: column
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+
+ spacing: 10
+
+ onHeightChanged: scrollTo(activeItem)
+ }
+ }
+}
diff --git a/src/main.qml b/src/main.qml
new file mode 100644
index 0000000..f7673dc
--- /dev/null
+++ b/src/main.qml
@@ -0,0 +1,53 @@
+import QtQuick 2.0
+import QtQuick.Window 2.0
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import Qt.labs.settings 1.0
+
+ApplicationWindow {
+ id: root
+
+ visible: true
+
+ Settings {
+ id: settings
+ category: "window"
+
+ property string background : "#161616"
+ }
+
+ color: settings.background
+
+ Component.onCompleted: {
+ terminalList.createItem();
+ terminalList.focusCurrent();
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ TerminalList {
+ id: terminalList
+
+ state: state
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+
+ CommandInput {
+ id: command
+
+ Layout.fillWidth: true
+
+ onExecuted: state.enterNormalMode()
+ }
+ }
+
+ StateHandler {
+ id: state
+
+ terminalList: terminalList
+ commandInput: command
+ }
+}
diff --git a/src/ui.qrc b/src/ui.qrc
new file mode 100644
index 0000000..48554f7
--- /dev/null
+++ b/src/ui.qrc
@@ -0,0 +1,12 @@
+<RCC>
+ <qresource prefix="/">
+ <file>main.qml</file>
+ <file>StateHandler.qml</file>
+ <file alias="TerminalItem.qml">list/TerminalItem.qml</file>
+ <file alias="TerminalList.qml">list/TerminalList.qml</file>
+ <file alias="CommandInput.qml">command/CommandInput.qml</file>
+ <file alias="commands.js">command/commands.js</file>
+ <file alias="EmbeddedTerminal.qml">widget/EmbeddedTerminal.qml</file>
+ <file alias="Highlighter.qml">widget/Highlighter.qml</file>
+ </qresource>
+</RCC>
diff --git a/src/widget/EmbeddedTerminal.qml b/src/widget/EmbeddedTerminal.qml
new file mode 100644
index 0000000..6d0dc6e
--- /dev/null
+++ b/src/widget/EmbeddedTerminal.qml
@@ -0,0 +1,153 @@
+import QtQuick 2.0
+import QMLTermWidget 1.0
+import QtQuick.Layouts 1.1
+import Qt.labs.settings 1.0
+
+Item {
+ id: item
+
+ property string program
+ property string workingDirectory
+
+ Settings {
+ id: settings
+ category: "terminal"
+
+ property int initialLines : 20
+ property int frameWidth : 10
+ property int fontSize : 8
+ property string fontFamily : "Monospace"
+ property string colorScheme : "cool-retro-term"
+ property string overlayBackground : "black"
+ property string overlayFontColor : "white"
+ }
+
+ property int lines : settings.initialLines
+
+ height: terminal.height
+ width: parent.width - settings.frameWidth
+
+ function select() { highlighter.select() }
+ function deselect() { highlighter.deselect() }
+ function displayOverlay() { overlay.displayBriefly() }
+
+ RowLayout {
+ id: container
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+
+ spacing: 0
+
+ Highlighter {
+ id: highlighter
+
+ width: settings.frameWidth
+ Layout.fillHeight: true
+ }
+
+ QMLTermWidget {
+ id: terminal
+
+ font {
+ family: settings.fontFamily
+ pointSize: settings.fontSize
+ }
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: fontMetrics.height * item.lines
+
+ colorScheme: settings.colorScheme
+
+ session: QMLTermSession {
+ initialWorkingDirectory: item.workingDirectory
+
+ shellProgram: {
+ return (item.program).split(" ")[0];
+ }
+
+ shellProgramArgs: {
+ const elements = (item.program).split(" ");
+ elements.shift();
+
+ return elements;
+ }
+ }
+
+ Component.onCompleted: {
+ forceActiveFocus();
+ highlighter.select();
+ session.startShellProgram();
+ overlay.enabled = true;
+ }
+
+ onTermGetFocus: highlighter.focus()
+ onTermLostFocus: highlighter.unfocus()
+ onHeightChanged: overlay.displayBriefly();
+ onWidthChanged: overlay.displayBriefly();
+
+ Rectangle {
+ id: overlay
+
+ property bool enabled : false
+
+ function displayBriefly() {
+ if ( enabled ) { animation.restart() }
+ }
+
+ anchors.fill: parent
+ opacity: 0
+ color: settings.overlayBackground
+
+ SequentialAnimation {
+ id: animation
+
+ ScriptAction {
+ script: overlay.opacity = 0.8
+ }
+
+ PauseAnimation {
+ duration: 500
+ }
+
+ NumberAnimation {
+ target: overlay
+ property: "opacity"
+
+ easing.type: Easing.InSine
+ duration: 300
+ from: 0.8
+ to: 0
+ }
+ }
+
+ Text {
+ anchors {
+ horizontalCenter: overlay.horizontalCenter
+ verticalCenter: overlay.verticalCenter
+ }
+
+ font {
+ family: settings.fontFamily
+ pointSize: settings.fontSize * 2
+ }
+ color: settings.overlayFontColor
+
+ text: {
+ return item.lines
+ + 'x'
+ + Math.floor(terminal.width / terminal.fontMetrics.width);
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton
+ onWheel: { }
+ }
+ }
+ }
+}
diff --git a/src/widget/Highlighter.qml b/src/widget/Highlighter.qml
new file mode 100644
index 0000000..e42aeb1
--- /dev/null
+++ b/src/widget/Highlighter.qml
@@ -0,0 +1,33 @@
+import QtQuick 2.0
+import Qt.labs.settings 1.0
+
+Item {
+ Settings {
+ id: settings
+ category: "highlighter"
+
+ property string defaultColor : "#909636"
+ property string focusColor : "#352F6A"
+ }
+
+ function select() { bar.opacity = 1 }
+ function deselect() { bar.opacity = 0 }
+ function focus() { bar.color = settings.focusColor }
+ function unfocus() { bar.color = settings.defaultColor }
+
+ Rectangle {
+ id: bar
+
+ anchors.fill: parent
+
+ opacity: 0
+ color: settings.defaultColor
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 300
+ easing.type: Easing.OutCubic
+ }
+ }
+ }
+}