Add notebook save
diff --git a/ide/Cell.cpp b/ide/Cell.cpp
index bf87abb..d00a219 100644
--- a/ide/Cell.cpp
+++ b/ide/Cell.cpp
@@ -89,3 +89,15 @@
     else
         return nullptr;
 }
+
+QJsonObject Cell::toJson() const
+{
+    QJsonObject object;
+
+    object["code"] = code();
+    object["result"] = result();
+    object["status"] = status();
+    object["resultType"] = resultType();
+
+    return object;
+}
diff --git a/ide/Cell.h b/ide/Cell.h
index 6085b81..55da010 100644
--- a/ide/Cell.h
+++ b/ide/Cell.h
@@ -4,6 +4,7 @@
 #include <qqml.h>
 #include <QUuid>
 #include <QHash>
+#include <QJsonObject>
 
 class Cell : public QObject
 {
@@ -37,6 +38,8 @@
 
     Q_INVOKABLE static Cell *cellFromUuid(QUuid uuid);
 
+    Q_INVOKABLE QJsonObject toJson() const;
+
     enum Status
     {
         RUNNING,
diff --git a/ide/IDE.pri b/ide/IDE.pri
index 0341a0b..69856d7 100644
--- a/ide/IDE.pri
+++ b/ide/IDE.pri
@@ -1,4 +1,4 @@
-QT += quick quickcontrols2
+QT += quick widgets quickcontrols2
 QT -= core
 
 CONFIG += qmltypes
diff --git a/ide/IdeMain.cpp b/ide/IdeMain.cpp
index ec6627e..caa0c85 100644
--- a/ide/IdeMain.cpp
+++ b/ide/IdeMain.cpp
@@ -5,7 +5,7 @@
 
 #include "CellModel.h"
 
-int ideMain(QGuiApplication *app)
+int ideMain(Application *app)
 {
 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
     QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
diff --git a/ide/IdeMain.h b/ide/IdeMain.h
index 6b911ba..8e45052 100644
--- a/ide/IdeMain.h
+++ b/ide/IdeMain.h
@@ -1,7 +1,7 @@
 #pragma once
 
-#include <QGuiApplication>
+#include <QApplication>
 
-using Application = QGuiApplication;
+using Application = QApplication;
 
-int ideMain(QGuiApplication *app);
+int ideMain(QApplication *app);
diff --git a/ide/Notebook.cpp b/ide/Notebook.cpp
index 97df181..8d0e8ee 100644
--- a/ide/Notebook.cpp
+++ b/ide/Notebook.cpp
@@ -2,6 +2,10 @@
 #include "CellModel.h"
 #include "../PPrint.h"
 
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QFileDialog>
+
 // TODO: avoid potential race condition if Cell is deleted, pass by value with same UUID instead
 
 Notebook::~Notebook()
@@ -53,6 +57,58 @@
     _rt->unqueueCell(Cell::cellFromUuid(uuid));
 }
 
+QJsonDocument Notebook::toJson() const
+{
+    QJsonObject nb;
+    QJsonArray cellArray;
+
+    for (const Cell *cell : _cells)
+    {
+        cellArray.append(cell->toJson());
+    }
+
+    nb["cells"] = cellArray;
+
+    return QJsonDocument(nb);
+}
+
+void Notebook::save()
+{
+    if (_savePath == "")
+    {
+        setSavePath(QFileDialog::getSaveFileName(nullptr, "Open Refal Notebook", "", "Refal Notebooks (*.refnb)"));
+    }
+
+    QJsonDocument doc = toJson();
+    QFile save(_savePath);
+    save.open(QFile::WriteOnly);
+
+    if (!save.isOpen())
+    {
+        emit saveError(save.errorString());
+        return;
+    }
+
+    save.write(doc.toJson(QJsonDocument::Indented));
+    save.close();
+}
+
+bool Notebook::savePathSet()
+{
+    return _savePath != "";
+}
+
+QString Notebook::savePath()
+{
+    return _savePath;
+}
+
+void Notebook::setSavePath(QString savePath)
+{
+    _savePath = savePath;
+    emit savePathChanged(savePath);
+}
+
 void Notebook::cellFinishedRunning(Cell *cell, RuntimeResult result)
 {
     qInfo() << "cellFinishedRunning" << cell->uuid() << pprint(result);
diff --git a/ide/Notebook.h b/ide/Notebook.h
index 1a0206e..dda07bc 100644
--- a/ide/Notebook.h
+++ b/ide/Notebook.h
@@ -1,6 +1,8 @@
 #pragma once
 
 #include <QObject>
+#include <QJsonDocument>
+#include <QFile>
 
 #include "Cell.h"
 #include "NbRuntime.h"
@@ -12,6 +14,7 @@
     QML_ELEMENT
 
     Q_PROPERTY(CellModel *cellModel READ cellModel NOTIFY cellModelChanged)
+    Q_PROPERTY(QString savePath READ savePath WRITE setSavePath NOTIFY savePathChanged)
 
 public:
     ~Notebook();
@@ -23,8 +26,18 @@
     Q_INVOKABLE void runCell(QUuid uuid);
     Q_INVOKABLE void quitCell(QUuid uuid);
 
+    Q_INVOKABLE QJsonDocument toJson() const;
+    Q_INVOKABLE void save();
+
+    Q_INVOKABLE bool savePathSet();
+
+    QString savePath();
+    void setSavePath(QString savePath);
+
 signals:
     void cellModelChanged();
+    void saveError(QString message);
+    void savePathChanged(QString savePath);
 
 protected slots:
     void cellFinishedRunning(Cell *cell, RuntimeResult result);
@@ -40,6 +53,7 @@
     CellModel *_cellModel;
     QThread *_rtThread;
     NbRuntime *_rt;
+    QString _savePath = "";
 };
 
 Q_DECLARE_METATYPE(Notebook)
diff --git a/ide/qml/main.qml b/ide/qml/main.qml
index 2439e6c..ab86efd 100644
--- a/ide/qml/main.qml
+++ b/ide/qml/main.qml
@@ -9,7 +9,7 @@
     id: root
     width: 1080
     height: 720
-    title: "Notebook"
+    title: "Refal Notebook -- " + notebook.savePath
     visible: true
 
     Material.theme: Material.Light
@@ -18,6 +18,15 @@
     menuBar: MenuBar {
         Menu {
             title: qsTr("&File")
+
+            Action {
+                text: "&Save"
+                shortcut: "Ctrl+s"
+
+                onTriggered: {
+                    notebook.save()
+                }
+            }
         }
 
         Menu {
@@ -38,6 +47,11 @@
 
     Notebook {
         id: notebook
+
+        onSaveError: (message) =>
+        {
+            console.error(message)
+        }
     }
 
     ColumnLayout {