Browse Source

Added git console

Alexey Edelev 7 years ago
parent
commit
638ca3a34a
19 changed files with 467 additions and 29 deletions
  1. 6 2
      NiceGit.pro
  2. 39 0
      colorhandler.cpp
  3. 28 0
      colorhandler.h
  4. 6 6
      commitgraph.cpp
  5. 83 0
      gitconsole.cpp
  6. 67 0
      gitconsole.h
  7. 32 3
      githandler.cpp
  8. 11 0
      githandler.h
  9. 2 0
      gitrepository.cpp
  10. 3 3
      gitrepository.h
  11. 2 0
      main.cpp
  12. 1 1
      qml/CommitInfo.qml
  13. 16 1
      qml/CommitList.qml
  14. 138 0
      qml/ConsoleControl.qml
  15. 8 7
      qml/FlickPager.qml
  16. 1 0
      qml/FlickPagerArrow.qml
  17. 1 1
      qml/GraphAnnotation.qml
  18. 22 5
      qml/MainView.qml
  19. 1 0
      resources.qrc

+ 6 - 2
NiceGit.pro

@@ -26,7 +26,9 @@ SOURCES += \
     gitbaseoid.cpp \
     branchlistmodel.cpp \
     taglistmodel.cpp \
-    tooltipviewmodel.cpp
+    tooltipviewmodel.cpp \
+    gitconsole.cpp \
+    colorhandler.cpp
 
 HEADERS += \
     githandler.h \
@@ -49,7 +51,9 @@ HEADERS += \
     gitbaseoid.h \
     branchlistmodel.h \
     taglistmodel.h \
-    tooltipviewmodel.h
+    tooltipviewmodel.h \
+    gitconsole.h \
+    colorhandler.h
 
 RESOURCES += \
     resources.qrc

+ 39 - 0
colorhandler.cpp

@@ -0,0 +1,39 @@
+#include "colorhandler.h"
+
+#include <gitbranch.h>
+#include <gitrepository.h>
+
+#include <QDateTime>
+
+ColorHandler::ColorHandler()
+{
+    qsrand(QDateTime::currentMSecsSinceEpoch());
+
+    for(int i = 0; i < m_colors.size(); i++) {
+    }
+}
+
+QString ColorHandler::color(const GitOid& oid)
+{
+    return m_colors.value(oid);
+}
+
+void ColorHandler::updateColors(GitRepository* repo)
+{
+    int red = 0;
+    int green = 0;
+    int blue = 0;
+    BranchContainer &branches = repo->branches();
+
+    foreach(GitBranch* branch, branches) {
+        red = qrand() % 205 + 50;
+        green = qrand() % 205 + 50;
+        blue = qrand() % 205 + 50;
+        m_colors[branch->oid()] = QString::number(red, 16) + QString::number(green, 16) + QString::number(blue, 16);
+    }
+
+    red = qrand() % 205 + 50;
+    green = qrand() % 205 + 50;
+    blue = qrand() % 205 + 50;
+    m_colors[repo->head()] = QString::number(red, 16) + QString::number(green, 16) + QString::number(blue, 16);
+}

+ 28 - 0
colorhandler.h

@@ -0,0 +1,28 @@
+#ifndef COLORHANDLER_H
+#define COLORHANDLER_H
+
+#include <QString>
+#include <QMap>
+
+#include <gitoid.h>
+
+class ColorHandler
+{
+
+public:
+    static ColorHandler& instance() {
+        static ColorHandler _instance;
+        return _instance;
+    }
+
+    void updateColors(GitRepository* repo);
+    QString color(const GitOid& oid);
+
+private:
+    ColorHandler();
+    Q_DISABLE_COPY(ColorHandler)
+
+    QMap<GitOid, QString> m_colors;
+};
+
+#endif // COLORHANDLER_H

+ 6 - 6
commitgraph.cpp

@@ -4,6 +4,8 @@
 #include <gitbranch.h>
 #include <gittag.h>
 
+#include <colorhandler.h>
+
 #include <graphpoint.h>
 #include <graphlistmodel.h>
 
@@ -28,12 +30,10 @@ void CommitGraph::addHead(GitBranch* branch)
 
 void CommitGraph::addHead(const GitOid &oid)
 {
-    //Random color generation to be replaced with resets
-    int red = qrand() % 205 + 50;
-    int green = qrand() % 205 + 50;
-    int blue = qrand() % 205 + 50;
-    m_color = QString::number(red, 16) + QString::number(green, 16) + QString::number(blue, 16);
+    //Read color for this head
+    m_color = ColorHandler::instance().color(oid);
 
+    //Random color generation to be replaced with resets
     git_revwalk* walk;
     git_revwalk_new(&walk, oid.repository()->raw());
 
@@ -58,7 +58,6 @@ void CommitGraph::addHead(const GitOid &oid)
         GitCommit *commit = GitCommit::fromOid(point->oid());
         git_commit* commitRaw = nullptr;
         int parentCount = git_commit_parentcount(commit->raw());
-        delete commit;
 //        qDebug() << "New commit: " << point->oid().toString() << point->x() << point->y();
         for(int j = 0; j < parentCount; j++) {//Add connection to parent in case if count of parents > 1
             git_commit_parent(&commitRaw, commit->raw(), j);
@@ -95,6 +94,7 @@ void CommitGraph::addHead(const GitOid &oid)
                 branchStarted.removeAll(point->x());
             }
         }
+        delete commit;
         m_branchesCount = m_branchesCount < (point->x() + 1) ? (point->x() + 1) : m_branchesCount;
     }
     m_pointsModel->reset(m_sortedPoints);

+ 83 - 0
gitconsole.cpp

@@ -0,0 +1,83 @@
+#include "gitconsole.h"
+
+#include <QProcess>
+#include <gitrepository.h>
+
+GitConsole::GitConsole(QObject *parent) : QObject(parent)
+  ,m_process(new QProcess(this))
+  ,m_busy(false)
+  ,m_recentIndex(-1)
+{
+    connect(m_process, &QProcess::readyRead, this, &GitConsole::onOutputReady);
+    connect(m_process, SIGNAL(finished(int)), this, SLOT(onFinished(int)));
+}
+
+GitConsole::~GitConsole()
+{
+    if(m_process && m_process->state() != QProcess::NotRunning) {
+        m_process->terminate();
+    }
+}
+
+void GitConsole::exec(const QString& command)
+{
+    m_recentIndex = -1;
+    if(!command.startsWith("git ")) {
+        emit commandError();
+        return;
+    }
+    qDebug() << "Execute:" << command << "in" << m_process->workingDirectory();
+    m_process->start(command);
+    emit commandLog(QString("<b>$&nbsp;") + command.toHtmlEscaped() + QString("</b><br/>"), false);
+    setBusy(true);
+    if(m_recentContainer.count() <= 0 || m_recentContainer.first() != command) {
+        m_recentContainer.push_front(command);
+    }
+}
+
+void GitConsole::setRepository(GitRepository *repo)
+{
+    m_repo = repo;
+    m_process->setWorkingDirectory(m_repo->path());
+}
+
+void GitConsole::onFinished(int exitCode)
+{
+    if(exitCode != 0 ) {
+        emit commandError();
+    }
+    setBusy(false);
+}
+
+void GitConsole::onOutputReady()
+{
+    QByteArray log = m_process->readAll();
+    emit commandLog(QString::fromUtf8(log).toHtmlEscaped().replace("\n","<br/>"), true);
+}
+
+void GitConsole::recentUp()
+{
+    if(m_recentContainer.count() <= 0) {
+        return;
+    }
+
+    if(m_recentIndex >= (m_recentContainer.count() - 2)) {
+        m_recentIndex = m_recentContainer.count() - 2;
+    }
+
+    emit recentChanged(m_recentContainer.at(++m_recentIndex));
+}
+
+void GitConsole::recentDown()
+{
+    if(m_recentContainer.count() <= 0) {
+        return;
+    }
+
+    if(m_recentIndex <= 0) {
+        m_recentIndex = -1;
+        emit recentChanged(QString());
+        return;
+    }
+    emit recentChanged(m_recentContainer.at(--m_recentIndex));
+}

+ 67 - 0
gitconsole.h

@@ -0,0 +1,67 @@
+#ifndef GITCONSOLE_H
+#define GITCONSOLE_H
+
+#include <QObject>
+#include <QPointer>
+#include <QList>
+#include <QString>
+
+class QProcess;
+class GitRepository;
+
+class GitConsole : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged)
+    Q_PROPERTY(QString recent READ recent NOTIFY recentChanged)
+public:
+    GitConsole(QObject* parent = 0);
+    ~GitConsole();
+    Q_INVOKABLE void exec(const QString& command);
+    bool busy() const
+    {
+        return m_busy;
+    }
+
+    QString recent() const
+    {
+        if(m_recentIndex < 0) {
+            return QString();
+        }
+
+        return m_recentContainer.at(m_recentIndex);
+    }
+
+public slots:
+    void setRepository(GitRepository* repo);
+
+    void setBusy(bool busy)
+    {
+        if (m_busy == busy)
+            return;
+
+        m_busy = busy;
+        emit busyChanged(busy);
+    }
+
+    void onFinished(int exitCode);
+    void onOutputReady();
+
+    Q_INVOKABLE void recentUp();
+    Q_INVOKABLE void recentDown();
+
+signals:
+    void busyChanged(bool busy);
+    void commandLog(const QString& data, bool prepend);
+    void commandError();
+    void recentChanged(QString recent);
+
+private:
+    QProcess* m_process;
+    QPointer<GitRepository> m_repo;
+    bool m_busy;
+    QList<QString> m_recentContainer;
+    int m_recentIndex;
+};
+
+#endif // GITCONSOLE_H

+ 32 - 3
githandler.cpp

@@ -3,6 +3,8 @@
 #include <QDebug>
 #include <QUrl>
 #include <QFileSystemWatcher>
+#include <QGuiApplication>
+#include <QClipboard>
 #include <qqml.h>
 
 #include <gitrepository.h>
@@ -12,10 +14,12 @@
 #include <tagmodel.h>
 #include <git2.h>
 
+#include <colorhandler.h>
 #include <commitgraph.h>
 #include <graphpoint.h>
 #include <branchlistmodel.h>
 #include <taglistmodel.h>
+#include <gitconsole.h>
 
 GitHandler::GitHandler() : QObject()
   ,m_repositories(new RepositoryModel(this))
@@ -26,6 +30,7 @@ GitHandler::GitHandler() : QObject()
   ,m_branchList(new BranchListModel(this))
   ,m_tagList(new TagListModel(this))
   ,m_activeRepoWatcher(new QFileSystemWatcher(this))
+  ,m_console(new GitConsole(this))
 {
     git_libgit2_init();
 }
@@ -60,14 +65,18 @@ void GitHandler::open(const QString &path)
         qDebug() << lastError();
         return;
     }
+    ColorHandler::instance().updateColors(m_activeRepo);
+    m_constantHead = m_activeRepo->head();
+    m_console->setRepository(m_activeRepo);
 
     connect(m_activeRepo, &GitRepository::branchesChanged, this, &GitHandler::updateModels);
     updateModels();
-    m_activeRepo->updateHead();
 
     m_activeRepoWatcher->addPath(m_activeRepo->root());
+    connect(m_activeRepoWatcher, &QFileSystemWatcher::directoryChanged, m_activeRepo, &GitRepository::readBranches);
+    connect(m_activeRepoWatcher, &QFileSystemWatcher::directoryChanged, m_activeRepo, &GitRepository::readRemotes);
+    connect(m_activeRepoWatcher, &QFileSystemWatcher::directoryChanged, m_activeRepo, &GitRepository::readTags);
     connect(m_activeRepoWatcher, &QFileSystemWatcher::directoryChanged, this, &GitHandler::updateModels);
-    connect(m_activeRepoWatcher, &QFileSystemWatcher::directoryChanged, m_activeRepo, &GitRepository::updateHead);
     //TODO: opened repositories configuraion TBD
     //    m_repositories->add(m_activeRepo);
 }
@@ -119,10 +128,24 @@ void GitHandler::updateModels()
     if(!m_activeRepo) {
         return;
     }
+    m_activeRepo->updateHead();
 
     BranchContainer &branches = m_activeRepo->branches();
     CommitGraph* graph = new CommitGraph();
-    graph->addHead(branches.value("master").data()->oid());
+
+    bool headIsBranch = false;
+    //TODO: Need to think about constant head more deeply
+//    foreach(GitBranch* branch, branches) {
+//        if(branch->oid() == m_constantHead) {
+//            graph->addHead(branch);
+//            headIsBranch = true;
+//            break;
+//        }
+//    }
+
+    if(!headIsBranch) {
+        graph->addHead(m_activeRepo->head());
+    }
 
     foreach(GitBranch* branch, branches) {
         qDebug() << "Next head " << branch->fullName();
@@ -139,3 +162,9 @@ void GitHandler::updateModels()
     m_branchList->reset(m_activeRepo->branches().values());
     m_tagList->reset(m_activeRepo->tags().values());
 }
+
+void GitHandler::copySha1(const QString& sha1)
+{
+    QGuiApplication::clipboard()->setText(sha1);
+}
+

+ 11 - 0
githandler.h

@@ -10,6 +10,7 @@ class CommitGraph;
 class BranchListModel;
 class TagListModel;
 class QFileSystemWatcher;
+class GitConsole;
 
 typedef QHash<QString, QPointer<CommitModel>> CommitModelContainer;
 
@@ -23,6 +24,7 @@ class GitHandler : public QObject
     Q_PROPERTY(CommitModel* commits READ commits WRITE setCommits NOTIFY commitsChanged)
     Q_PROPERTY(BranchListModel* branchList READ branchList CONSTANT)
     Q_PROPERTY(TagListModel* tagList READ tagList CONSTANT)
+    Q_PROPERTY(GitConsole* console READ console CONSTANT)
 
 public:
     GitHandler();
@@ -32,6 +34,8 @@ public:
 
     Q_INVOKABLE GitDiff* diff(GitCommit* a, GitCommit* b);
 
+    Q_INVOKABLE void copySha1(const QString& sha1);
+
     RepositoryModel* repositories() const
     {
         return m_repositories;
@@ -66,6 +70,11 @@ public:
         return m_tagList;
     }
 
+    GitConsole* console() const
+    {
+        return m_console;
+    }
+
 public slots:
     void setActiveDiff(GitDiff* activeDiff);
 
@@ -101,6 +110,8 @@ private:
     BranchListModel* m_branchList;
     TagListModel* m_tagList;
     QFileSystemWatcher* m_activeRepoWatcher;
+    GitConsole* m_console;
+    GitOid m_constantHead;
 };
 
 #endif // GITHANDLER_H

+ 2 - 0
gitrepository.cpp

@@ -12,6 +12,7 @@
 
 #include <git2.h>
 
+
 GitRepository::GitRepository(const QString& root) : QObject(nullptr)
 {
     if(git_repository_open(&m_raw, root.toUtf8().data()) != 0) {
@@ -27,6 +28,7 @@ GitRepository::GitRepository(const QString& root) : QObject(nullptr)
     readBranches();
     readTags();
     readRemotes();
+    updateHead();
 }
 
 GitRepository::~GitRepository()

+ 3 - 3
gitrepository.h

@@ -103,6 +103,9 @@ public slots:
     }
 
     void updateHead();
+    void readBranches();
+    void readTags();
+    void readRemotes();
 
 signals:
     void rootChanged(QString root);
@@ -113,9 +116,6 @@ signals:
 
 private:
     void close();
-    void readBranches();
-    void readTags();
-    void readRemotes();
 
     QString m_root;
     QString m_name;

+ 2 - 0
main.cpp

@@ -5,6 +5,7 @@
 #include <QFontDatabase>
 
 #include <githandler.h>
+#include <gitconsole.h>
 #include <gitrepository.h>
 #include <repositorymodel.h>
 #include <gitbranch.h>
@@ -43,6 +44,7 @@ int main(int argc, char *argv[])
     qmlRegisterUncreatableType<GraphListModel>("org.semlanik.nicegit", 1, 0, "GraphListModel", "Owned only by GitHandler");
     qmlRegisterUncreatableType<BranchListModel>("org.semlanik.nicegit", 1, 0, "BranchListModel", "Owned only by GitHandler");
     qmlRegisterUncreatableType<TagListModel>("org.semlanik.nicegit", 1, 0, "TagListModel", "Owned only by GitHandler");
+    qmlRegisterUncreatableType<GitConsole>("org.semlanik.nicegit", 1, 0, "GitConsole", "Owned only by GitHandler");
     qmlRegisterSingletonType<TooltipViewModel>("org.semlanik.nicegit", 1, 0,"TooltipViewModel",
                                                [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject*
     {

+ 1 - 1
qml/CommitInfo.qml

@@ -37,7 +37,7 @@ Item {
         }
 
         CommitInfoLine {
-            field: qsTr("Autor")
+            field: qsTr("Author")
             value: commit ? commit.author : ""
         }
 

+ 16 - 1
qml/CommitList.qml

@@ -5,6 +5,7 @@ FlickPager {
     id: root
     property QtObject graphModel: null
     property QtObject commitsModel: null
+    property QtObject activeCommit: null
     signal commitClicked(var commit)
 
     QtObject {
@@ -66,7 +67,13 @@ FlickPager {
                 Rectangle {
                     width: parent.width
                     height: graph.elementHeight
-                    color: textSelector.containsMouse ? "#bbbbbb" : "#00bbbbbb"
+                    color: {
+                        if(activeCommit && activeCommit.sha1 === model.sha1){
+                            return "#bbeebb"
+                        }
+
+                        return textSelector.containsMouse ? "#bbbbbb" : "#00bbbbbb"
+                    }
                     Item {
                         width: root.width - graph.width - graphAnnotation.width
                         height: sha1.height
@@ -118,11 +125,19 @@ FlickPager {
                                 commitMenu.popup()
                             } else {
                                 root.commitClicked(model.modelData)
+                                //TODO: Because active commit is used in multiple places need to make it part of some model (e.g. git handler)
+                                activeCommit = model.modelData;
                             }
                         }
                     }
                     Menu {
                         id: commitMenu
+                        MenuItem {
+                            text: "Copy sha-id"
+                            onTriggered: {
+                                _handler.copySha1(model.sha1)
+                            }
+                        }
                         MenuItem {
                             text: "Checkout commit"
                             onTriggered: {

+ 138 - 0
qml/ConsoleControl.qml

@@ -0,0 +1,138 @@
+import QtQuick 2.0
+import QtQuick.Controls 1.4
+
+Item {
+    id: root
+    Rectangle {
+        anchors.fill: parent
+        color: "#839496"
+
+    }
+
+    states: [
+        State {
+            name: "opened"
+            when: consoleInput.focus === true
+            PropertyChanges {
+                target: root
+                height: 250
+            }
+            PropertyChanges {
+                target: consoleInput
+                focus: true
+            }
+        },
+        State {
+            name: "closed"
+            PropertyChanges {
+                target: root
+                height: consoleInput.height
+            }
+            PropertyChanges {
+                target: consoleInput
+                focus: false
+            }
+        }
+    ]
+
+    state: "closed"
+
+    FlickPager {
+        id: flick
+        clip: true
+        anchors.top: parent.top
+        anchors.bottom:consoleInput.top
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.rightMargin: 5
+        anchors.leftMargin: 5
+        color: "#839496"
+        content: Text {
+            id: consoleLog
+            width: root.width - 10
+            height: contentHeight
+            textFormat: Text.RichText
+            color: "#002b36"
+            Connections {
+                target: _handler.console
+                onCommandLog: {
+                    consoleLog.text = consoleLog.text + data
+                    if(flick.flickable.contentHeight > flick.flickable.height) {
+                        flick.flickable.contentY = flick.flickable.contentHeight - flick.flickable.height
+                    }
+                }
+                onCommandError: {
+                    fadeIn.start()
+                }
+            }
+        }
+    }
+
+    Rectangle {
+        id: errorRect
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.top: consoleInput.top
+        anchors.bottom: consoleInput.bottom
+        color: "#ff0000"
+        opacity: 0.0
+        visible: opacity > 0
+
+        NumberAnimation {
+            id: fadeIn
+            target: errorRect
+            property: "opacity"
+            duration: 350
+            from: 0.0
+            to: 1.0
+            onStopped: {
+                fadeOut.start()
+            }
+        }
+
+        NumberAnimation {
+            id: fadeOut
+            target: errorRect
+            property: "opacity"
+            duration: 350
+            from: 1.0
+            to: 0.0
+        }
+    }
+
+    TextInput {
+        id: consoleInput
+        anchors.bottom: parent.bottom
+        height: 30
+        font.weight: Font.Bold
+        color: "#002b36"
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.rightMargin: 5
+        anchors.leftMargin: 5
+        enabled: !_handler.console.busy
+        onAccepted: {
+            _handler.console.exec(consoleInput.text);
+            consoleInput.text = ""
+        }
+        Keys.onPressed: {
+            switch(event.key) {
+            case Qt.Key_Up:
+                event.accepted = true
+                _handler.console.recentUp();
+                break;
+            case Qt.Key_Down:
+                event.accepted = true
+                _handler.console.recentDown();
+                break;
+            }
+        }
+    }
+
+    Connections {
+        target: _handler.console
+        onRecentChanged: {
+            consoleInput.text = _handler.console.recent
+        }
+    }
+}

+ 8 - 7
qml/FlickPager.qml

@@ -3,11 +3,12 @@ import QtQuick 2.0
 Item {
     id: root
     property Component content: null
+    property color color: "#ffffffff"
+    property alias flickable: flick
     QtObject {
         id: d
         property Item flickItem: null
     }
-
     Flickable {
         id: flick
         anchors.top: arrowUp.bottom
@@ -22,13 +23,13 @@ Item {
         id: arrowUp
         anchors.top: parent.top
         source: "arrow-141-16"
-        active: flick.contentY > 0
+        active: flick.contentY > 0 && root.height > 0
         onClicked: {
             flick.contentY -= (flick.contentY - flick.height) < 0 ? flick.contentY : flick.height
         }
         gradient: Gradient {
-            GradientStop { position: 0.0; color: "#ffffffff" }
-            GradientStop { position: 0.8; color: "#ffffffff" }
+            GradientStop { position: 0.0; color: root.color }
+            GradientStop { position: 0.8; color: root.color }
             GradientStop { position: 1.0; color: "#00ffffff" }
         }
     }
@@ -37,14 +38,14 @@ Item {
         id: arrowDown
         anchors.bottom: parent.bottom
         source: "arrow-203-16"
-        active: flick.contentY < (flick.contentHeight - flick.height)
+        active: flick.contentY < (flick.contentHeight - flick.height) && root.height > 0
         onClicked: {
             flick.contentY += (flick.contentY + flick.height*2) >= flick.contentHeight ? flick.contentHeight - flick.contentY - flick.height : flick.height
         }
         gradient: Gradient {
             GradientStop { position: 0.0; color: "#00ffffff" }
-            GradientStop { position: 0.2; color: "#ffffffff" }
-            GradientStop { position: 1.0; color: "#ffffffff" }
+            GradientStop { position: 0.2; color: root.color }
+            GradientStop { position: 1.0; color: root.color }
         }
     }
 

+ 1 - 0
qml/FlickPagerArrow.qml

@@ -26,6 +26,7 @@ Rectangle {
         id: control
         anchors.fill: parent
         hoverEnabled: true
+        enabled: arrow.active
         onClicked: {
             arrow.clicked()
         }

+ 1 - 1
qml/GraphAnnotation.qml

@@ -100,7 +100,7 @@ Item {
             anchors.right: parent.right
             height: d.fullHeight
             width: root.width / 2
-            y: _handler.graph.point(model.targetId).y*(root.elementHeight + root.spacing) - spacing/2
+            y: _handler.graph.point(model.targetId).y*d.fullHeight - spacing/2
             clip: true
             Rectangle {
                 anchors.left: parent.left

+ 22 - 5
qml/MainView.qml

@@ -2,16 +2,18 @@ import QtQuick 2.0
 import QtQuick.Controls 1.4
 import org.semlanik.nicegit 1.0
 
-Item {
+FocusScope {
     id: root
     property var commitsForDiff: null
     property bool controlActive: false
+    focus: true
     TopBar {
         id: topBar
         onCloseClicked: {
             commitPlane.diff = null
             commitPlane.commit = null
             commitList.state = "full"
+            commitList.activeCommit = null
         }
         closeVisible: commitPlane.diff != null
     }
@@ -25,7 +27,7 @@ Item {
     CommitList {
         id: commitList
         anchors.top: topBar.bottom
-        anchors.bottom: parent.bottom
+        anchors.bottom: consoleContol.top
         anchors.left: parent.left
 
         commitsModel: _handler.commits
@@ -64,15 +66,30 @@ Item {
         anchors.left: commitList.right
         anchors.right: parent.right
         anchors.top: topBar.bottom
-        anchors.bottom: parent.bottom
+        anchors.bottom: consoleContol.top
 //        visible: diff != null
 //        opacity: diff != null ? 1.0 : 0.0
     }
 
+    ConsoleControl {
+        id: consoleContol
+        anchors.left: parent.left
+        anchors.right: parent.right
+        anchors.bottom: parent.bottom
+    }
+
     Keys.onPressed: {
-        if(event.key === Qt.Key_Control) {
+        event.accepted = true
+        switch(event.key) {
+        case Qt.Key_Control:
             root.controlActive = true
-            console.log("control pressed");
+            console.log("control pressed")
+            break
+        case Qt.Key_F4:
+            consoleContol.state = consoleContol.state === "closed" ? "opened" : "closed"
+            break
+        default:
+            event.accepted = false
         }
     }
 

+ 1 - 0
resources.qrc

@@ -35,5 +35,6 @@
         <file>qml/Tooltip.qml</file>
         <file>images/aim.svg</file>
         <file>images/aim.png</file>
+        <file>qml/ConsoleControl.qml</file>
     </qresource>
 </RCC>