diff --git a/browser_ext/background.js b/browser_ext/background.js
index 85846c7fe..5e34c3a4d 100644
--- a/browser_ext/background.js
+++ b/browser_ext/background.js
@@ -9,19 +9,18 @@ const ignoredPages = {
 };
 
 const appName = "com.chatterino.chatterino";
-
-/// Connect to port
-
 let port = null;
 
+
+/// Connect to port
 function connectPort() {
   port = chrome.runtime.connectNative("com.chatterino.chatterino");
   console.log("port connected");
 
-  port.onMessage.addListener(function(msg) {
+  port.onMessage.addListener(function (msg) {
     console.log(msg);
   });
-  port.onDisconnect.addListener(function() {
+  port.onDisconnect.addListener(function () {
     console.log("port disconnected");
 
     port = null;
@@ -39,8 +38,8 @@ function getPort() {
   }
 }
 
-/// Tab listeners
 
+/// Tab listeners
 chrome.tabs.onActivated.addListener((activeInfo) => {
   chrome.tabs.get(activeInfo.tabId, (tab) => {
     if (!tab)
@@ -49,7 +48,7 @@ chrome.tabs.onActivated.addListener((activeInfo) => {
     if (!tab.url)
       return;
 
-    matchUrl(tab.url);
+    matchUrl(tab.url, tab);
   });
 });
 
@@ -57,32 +56,47 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
   if (!tab.highlighted)
     return;
 
-  matchUrl(changeInfo.url);
+  matchUrl(changeInfo.url, tab);
 });
 
 
 /// Misc
-
-function matchUrl(url) {
+function matchUrl(url, tab) {
   if (!url)
     return;
 
   const match = url.match(/^https?:\/\/(www\.)?twitch.tv\/([a-zA-Z0-9]+)\/?$/);
 
-  if (match) {
-    const channelName = match[2];
+  let channelName;
 
-    if (!ignoredPages[channelName]) {
-      selectChannel(channelName);
+  console.log(tab);
+
+  if (match && (channelName = match[2], !ignoredPages[channelName])) {
+    console.log("channelName " + channelName);
+    console.log("winId " + tab.windowId);
+
+    chrome.windows.get(tab.windowId, {}, (window) => {
+      let yOffset = window.height - tab.height;
+
+      let port = getPort();
+      if (port) {
+        port.postMessage({
+          action: "select",
+          attach: true,
+          type: "twitch",
+          name: channelName,
+          winId: "" + tab.windowId,
+          yOffset: yOffset
+        });
+      }
+    });
+  } else {
+    let port = getPort();
+    if (port) {
+      port.postMessage({
+        action: "detach",
+        winId: "" + tab.windowId
+      })
     }
   }
 }
-
-function selectChannel(channelName) {
-  console.log("select" + channelName);
-
-  let port = getPort();
-  if (port) {
-    port.postMessage({action: "select", type: "twitch", name: channelName});
-  }
-}
diff --git a/chatterino.pro b/chatterino.pro
index 679c6d80f..49afc8881 100644
--- a/chatterino.pro
+++ b/chatterino.pro
@@ -181,7 +181,9 @@ SOURCES += \
     src/singletons/helper/pubsubactions.cpp \
     src/widgets/selectchanneldialog.cpp \
     src/singletons/updatemanager.cpp \
-    src/widgets/lastruncrashdialog.cpp
+    src/widgets/lastruncrashdialog.cpp \
+    src/widgets/attachedwindow.cpp \
+    src/util/tupletablemodel.cpp
 
 HEADERS  += \
     src/precompiled_header.hpp \
@@ -305,7 +307,9 @@ HEADERS  += \
     src/singletons/helper/pubsubactions.hpp \
     src/widgets/selectchanneldialog.hpp \
     src/singletons/updatemanager.hpp \
-    src/widgets/lastruncrashdialog.hpp
+    src/widgets/lastruncrashdialog.hpp \
+    src/widgets/attachedwindow.hpp \
+    src/util/tupletablemodel.hpp
 
 RESOURCES += \
     resources/resources.qrc
@@ -359,10 +363,6 @@ win32-msvc* {
 
 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0
 
-win32::exists(C:\fourtf) {
-    DEFINES += "OHHEYITSFOURTF"
-}
-
 linux {
     QMAKE_LFLAGS += -lrt
 }
diff --git a/src/messages/highlightphrase.hpp b/src/messages/highlightphrase.hpp
index f5ea0f29b..93721b1c8 100644
--- a/src/messages/highlightphrase.hpp
+++ b/src/messages/highlightphrase.hpp
@@ -10,13 +10,14 @@ namespace messages {
 
 struct HighlightPhrase {
     QString key;
-    bool sound;
     bool alert;
+    bool sound;
+    bool regex;
 
-    bool operator==(const HighlightPhrase &rhs) const
+    bool operator==(const HighlightPhrase &other) const
     {
-        return std::tie(this->key, this->sound, this->alert) ==
-               std::tie(rhs.key, rhs.sound, rhs.alert);
+        return std::tie(this->key, this->sound, this->alert, this->regex) ==
+               std::tie(other.key, other.sound, other.alert, other.regex);
     }
 };
 }  // namespace messages
@@ -35,6 +36,7 @@ struct Serialize<chatterino::messages::HighlightPhrase> {
         AddMember(ret, "key", value.key, a);
         AddMember(ret, "alert", value.alert, a);
         AddMember(ret, "sound", value.sound, a);
+        AddMember(ret, "regex", value.regex, a);
 
         return ret;
     }
@@ -70,6 +72,13 @@ struct Deserialize<chatterino::messages::HighlightPhrase> {
             }
         }
 
+        if (value.HasMember("regex")) {
+            const rapidjson::Value &regex = value["regex"];
+            if (regex.IsBool()) {
+                ret.regex = regex.GetBool();
+            }
+        }
+
         return ret;
     }
 };
diff --git a/src/messages/layouts/messagelayout.cpp b/src/messages/layouts/messagelayout.cpp
index db9efa6a4..025ffaea6 100644
--- a/src/messages/layouts/messagelayout.cpp
+++ b/src/messages/layouts/messagelayout.cpp
@@ -198,7 +198,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int messageIndex, Selection &s
     // draw message
     this->container.paintElements(painter);
 
-#ifdef OHHEYITSFOURTF
+#ifdef FOURTF
     // debug
     painter.setPen(QColor(255, 0, 0));
     painter.drawRect(buffer->rect().x(), buffer->rect().y(), buffer->rect().width() - 1,
diff --git a/src/messages/layouts/messagelayoutcontainer.cpp b/src/messages/layouts/messagelayoutcontainer.cpp
index c6e6f9db4..52fd29323 100644
--- a/src/messages/layouts/messagelayoutcontainer.cpp
+++ b/src/messages/layouts/messagelayoutcontainer.cpp
@@ -191,7 +191,7 @@ MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point)
 void MessageLayoutContainer::paintElements(QPainter &painter)
 {
     for (const std::unique_ptr<MessageLayoutElement> &element : this->elements) {
-#ifdef OHHEYITSFOURTF
+#ifdef FOURTF
         painter.setPen(QColor(0, 255, 0));
         painter.drawRect(element->getRect());
 #endif
@@ -214,12 +214,14 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex,
     QColor selectionColor = themeManager.messages.selection;
 
     // don't draw anything
-    if (selection.selectionMin.messageIndex > messageIndex || selection.selectionMax.messageIndex < messageIndex) {
+    if (selection.selectionMin.messageIndex > messageIndex ||
+        selection.selectionMax.messageIndex < messageIndex) {
         return;
     }
 
     // fully selected
-    if (selection.selectionMin.messageIndex < messageIndex && selection.selectionMax.messageIndex > messageIndex) {
+    if (selection.selectionMin.messageIndex < messageIndex &&
+        selection.selectionMax.messageIndex > messageIndex) {
         for (Line &line : this->lines) {
             QRect rect = line.rect;
 
@@ -267,8 +269,8 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex,
                             int c = this->elements[i]->getSelectionIndexCount();
 
                             if (index + c > selection.selectionMax.charIndex) {
-                                r = this->elements[i]->getXFromIndex(selection.selectionMax.charIndex -
-                                                                     index);
+                                r = this->elements[i]->getXFromIndex(
+                                    selection.selectionMax.charIndex - index);
                                 break;
                             }
                             index += c;
diff --git a/src/singletons/nativemessagingmanager.cpp b/src/singletons/nativemessagingmanager.cpp
index 369d41ec8..9c3bb907d 100644
--- a/src/singletons/nativemessagingmanager.cpp
+++ b/src/singletons/nativemessagingmanager.cpp
@@ -16,6 +16,10 @@ namespace ipc = boost::interprocess;
 
 #ifdef Q_OS_WIN
 #include <QProcess>
+
+#include <windows.h>
+#include "singletons/windowmanager.hpp"
+#include "widgets/attachedwindow.hpp"
 #endif
 
 #include <iostream>
@@ -56,9 +60,14 @@ void NativeMessagingManager::registerHost()
     root_obj.insert("path", QCoreApplication::applicationFilePath());
     root_obj.insert("type", "stdio");
 
+    // chrome
     QJsonArray allowed_origins_arr = {"chrome-extension://aeicjepmjkgmbeohnchmpfjbpchogmjn/"};
     root_obj.insert("allowed_origins", allowed_origins_arr);
 
+    // firefox
+    QJsonArray allowed_extensions = {"585a153c7e1ac5463478f25f8f12220e9097e716@temporary-addon"};
+    root_obj.insert("allowed_extensions", allowed_extensions);
+
     // save the manifest
     QString manifestPath =
         PathManager::getInstance().settingsFolderPath + "/native-messaging-manifest.json";
@@ -70,13 +79,11 @@ void NativeMessagingManager::registerHost()
     file.write(document.toJson());
     file.flush();
 
-#ifdef XD
 #ifdef Q_OS_WIN
     // clang-format off
     QProcess::execute("REG ADD \"HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.chatterino.chatterino\" /ve /t REG_SZ /d \"" + manifestPath + "\" /f");
 // clang-format on
 #endif
-#endif
 }
 
 void NativeMessagingManager::openGuiMessageQueue()
@@ -135,22 +142,45 @@ void NativeMessagingManager::ReceiverThread::handleMessage(const QJsonObject &ro
 
     if (action == "select") {
         QString _type = root.value("type").toString();
+        bool attach = root.value("attach").toBool();
         QString name = root.value("name").toString();
+        QString winId = root.value("winId").toString();
+        int yOffset = root.value("yOffset").toInt(-1);
 
-        if (_type.isNull() || name.isNull()) {
-            qDebug() << "NM type or name missing";
+        if (_type.isNull() || name.isNull() || winId.isNull()) {
+            qDebug() << "NM type, name or winId missing";
+            attach = false;
             return;
         }
 
         if (_type == "twitch") {
-            util::postToThread([name] {
+            util::postToThread([name, attach, winId, yOffset] {
                 auto &ts = providers::twitch::TwitchServer::getInstance();
 
                 ts.watchingChannel.update(ts.getOrAddChannel(name));
+
+                if (attach) {
+                    auto *window =
+                        widgets::AttachedWindow::get(::GetForegroundWindow(), winId, yOffset);
+                    window->setChannel(ts.getOrAddChannel(name));
+                    window->show();
+                }
             });
+
         } else {
             qDebug() << "NM unknown channel type";
         }
+    } else if (action == "detach") {
+        QString winId = root.value("winId").toString();
+
+        if (winId.isNull()) {
+            qDebug() << "NM winId missing";
+            return;
+        }
+
+        util::postToThread([winId] { widgets::AttachedWindow::detach(winId); });
+    } else {
+        qDebug() << "NM unknown action " + action;
     }
 }
 
diff --git a/src/util/tupletablemodel.cpp b/src/util/tupletablemodel.cpp
new file mode 100644
index 000000000..ee804e5c1
--- /dev/null
+++ b/src/util/tupletablemodel.cpp
@@ -0,0 +1,7 @@
+#include "tupletablemodel.hpp"
+
+namespace chatterino {
+namespace util {
+
+}  // namespace util
+}  // namespace chatterino
diff --git a/src/util/tupletablemodel.hpp b/src/util/tupletablemodel.hpp
new file mode 100644
index 000000000..5b7dd2906
--- /dev/null
+++ b/src/util/tupletablemodel.hpp
@@ -0,0 +1,211 @@
+#pragma once
+
+#include <utility>
+#include <vector>
+
+#include <QAbstractTableModel>
+
+#include <pajlada/signals/signal.hpp>
+
+namespace chatterino {
+namespace util {
+
+namespace {
+
+template <int I>
+struct TupleConverter {
+    template <typename... Args>
+    static void tupleToVariants(const std::tuple<Args...> &t, std::vector<QVariant> &row)
+    {
+        row[I - 1] = QVariant(std::get<I - 1>(t));
+        TupleConverter<I - 1>::tupleToVariants<Args...>(t, row);
+    }
+
+    template <typename... Args>
+    static void variantsToTuple(std::vector<QVariant> &row, std::tuple<Args...> &t)
+    {
+        std::get<I - 1>(t) = (decltype(std::get<I - 1>(t))) row[I - 1];
+        TupleConverter<I - 1>::variantsToTuple<Args...>(row, t);
+    }
+};
+
+template <>
+struct TupleConverter<0> {
+    template <typename... Args>
+    static void tupleToVariants(const std::tuple<Args...> &t, std::vector<QVariant> &row)
+    {
+    }
+
+    template <typename... Args>
+    static void variantsToTuple(std::vector<QVariant> &row, std::tuple<Args...> &t)
+    {
+    }
+};
+
+}  // namespace
+
+template <typename... Args>
+class TupleTableModel : public QAbstractTableModel
+{
+    std::vector<std::vector<QVariant>> rows;
+    std::vector<QMap<int, QVariant>> titleData;
+
+public:
+    pajlada::Signals::NoArgSignal itemsChanged;
+
+    TupleTableModel()
+    {
+        titleData.resize(sizeof...(Args));
+    }
+
+    void addRow(const std::tuple<Args...> &row)
+    {
+        this->beginInsertRows(QModelIndex(), this->rows.size(), this->rows.size());
+        std::vector<QVariant> variants;
+        variants.resize(sizeof...(Args));
+        TupleConverter<sizeof...(Args)>::tupleToVariants<Args...>(row, variants);
+        this->rows.push_back(variants);
+        this->endInsertRows();
+        this->itemsChanged.invoke();
+    }
+
+    void addRow(Args... args)
+    {
+        this->beginInsertRows(QModelIndex(), this->rows.size(), this->rows.size());
+        std::vector<QVariant> variants;
+        variants.resize(sizeof...(Args));
+        TupleConverter<sizeof...(Args)>::tupleToVariants<Args...>(std::tuple<Args...>(args...),
+                                                                  variants);
+        this->rows.push_back(variants);
+        this->endInsertRows();
+        this->itemsChanged.invoke();
+    }
+
+    std::tuple<Args...> getRow(int index)
+    {
+        std::tuple<Args...> row;
+        TupleConverter<sizeof...(Args)>::variantsToTuple<Args...>(this->rows[index], row);
+        return row;
+    }
+
+    void removeRow(int index)
+    {
+        this->beginRemoveRows(QModelIndex(), index, index);
+        this->rows.erase(this->rows.begin() + index);
+        this->endRemoveRows();
+        this->itemsChanged.invoke();
+    }
+
+    void setTitles(std::initializer_list<QString> titles)
+    {
+        int i = 0;
+
+        for (const QString &title : titles) {
+            this->setHeaderData(i++, Qt::Horizontal, title, Qt::DisplayRole);
+
+            if (i >= sizeof...(Args))
+                break;
+        }
+    }
+
+    int getRowCount() const
+    {
+        return this->rows.size();
+    }
+
+protected:
+    virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        return this->rows.size();
+    }
+
+    virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override
+    {
+        return sizeof...(Args);
+    }
+
+    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
+    {
+        QVariant data = this->rows[index.row()][index.column()];
+
+        switch (role) {
+            case Qt::DisplayRole: {
+                if (data.type() == QVariant::Bool)
+                    return QVariant();
+                else
+                    return data;
+            } break;
+            case Qt::EditRole: {
+                return data;
+            } break;
+            case Qt::CheckStateRole: {
+                if (data.type() == QVariant::Bool)
+                    return data;
+                else
+                    return QVariant();
+            } break;
+        }
+        return QVariant();
+    }
+
+    virtual bool setData(const QModelIndex &index, const QVariant &value,
+                         int role = Qt::EditRole) override
+    {
+        QVariant data = this->rows[index.row()][index.column()];
+
+        switch (role) {
+            case (Qt::EditRole): {
+                this->rows[index.row()][index.column()] = value;
+                this->itemsChanged.invoke();
+                return true;
+            } break;
+            case (Qt::CheckStateRole): {
+                if (data.type() == QVariant::Bool) {
+                    this->rows[index.row()][index.column()] = !data.toBool();
+                    this->itemsChanged.invoke();
+                    return true;
+                }
+            } break;
+        }
+
+        return false;
+    }
+
+    virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const
+    {
+        if (orientation != Qt::Horizontal)
+            return QVariant();
+        if (section < 0 || section >= sizeof...(Args))
+            return QVariant();
+
+        auto it = this->titleData[section].find(role);
+        return it == this->titleData[section].end() ? QVariant() : it.value();
+    }
+
+    virtual bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value,
+                               int role)
+    {
+        if (orientation != Qt::Horizontal)
+            return false;
+        if (section < 0 || section >= sizeof...(Args))
+            return false;
+
+        this->titleData[section][role] = value;
+        return true;
+    }
+
+    virtual Qt::ItemFlags flags(const QModelIndex &index) const
+    {
+        QVariant data = this->rows[index.row()][index.column()];
+
+        if (data.type() == QVariant::Bool) {
+            return Qt::ItemIsUserCheckable | Qt::ItemIsEditable | Qt::ItemIsEnabled |
+                   Qt::ItemIsSelectable;
+        }
+
+        return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+    }
+};
+
+}  // namespace util
+}  // namespace chatterino
diff --git a/src/widgets/attachedwindow.cpp b/src/widgets/attachedwindow.cpp
new file mode 100644
index 000000000..d6c0110fb
--- /dev/null
+++ b/src/widgets/attachedwindow.cpp
@@ -0,0 +1,112 @@
+#include "attachedwindow.hpp"
+
+#include <QTimer>
+#include <QVBoxLayout>
+
+#include "widgets/split.hpp"
+
+#include "Windows.h"
+#pragma comment(lib, "Dwmapi.lib")
+
+namespace chatterino {
+namespace widgets {
+
+AttachedWindow::AttachedWindow(void *_target, int _yOffset)
+    : target(_target)
+    , yOffset(_yOffset)
+    , QWidget(nullptr, Qt::FramelessWindowHint | Qt::Window)
+{
+    QLayout *layout = new QVBoxLayout(this);
+    layout->setMargin(0);
+    this->setLayout(layout);
+
+    auto *split = new Split(singletons::ThemeManager::getInstance(), this);
+    this->ui.split = split;
+    split->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::MinimumExpanding);
+    layout->addWidget(split);
+}
+
+AttachedWindow::~AttachedWindow()
+{
+    for (auto it = items.begin(); it != items.end(); it++) {
+        if (it->window == this) {
+            items.erase(it);
+            break;
+        }
+    }
+}
+
+AttachedWindow *AttachedWindow::get(void *target, const QString &winId, int yOffset)
+{
+    for (Item &item : items) {
+        if (item.hwnd == target) {
+            return item.window;
+        }
+    }
+
+    auto *window = new AttachedWindow(target, yOffset);
+    items.push_back(Item{target, window, winId});
+    return window;
+}
+
+void AttachedWindow::detach(const QString &winId)
+{
+    for (Item &item : items) {
+        if (item.winId == winId) {
+            item.window->deleteLater();
+        }
+    }
+}
+
+void AttachedWindow::setChannel(ChannelPtr channel)
+{
+    this->ui.split->setChannel(channel);
+}
+
+void AttachedWindow::showEvent(QShowEvent *)
+{
+    attachToHwnd(this->target);
+}
+
+void AttachedWindow::attachToHwnd(void *_hwnd)
+{
+    QTimer *timer = new QTimer(this);
+    timer->setInterval(1);
+
+    HWND hwnd = (HWND)this->winId();
+    HWND attached = (HWND)_hwnd;
+    QObject::connect(timer, &QTimer::timeout, [this, hwnd, attached, timer] {
+        ::SetLastError(0);
+        RECT xD;
+        ::GetWindowRect(attached, &xD);
+
+        if (::GetLastError() != 0) {
+            timer->stop();
+            timer->deleteLater();
+            this->deleteLater();
+        }
+
+        HWND next = ::GetNextWindow(attached, GW_HWNDPREV);
+
+        ::SetWindowPos(hwnd, next ? next : HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
+        ::MoveWindow(hwnd, xD.right - 360, xD.top + this->yOffset - 8, 360 - 8,
+                     xD.bottom - xD.top - this->yOffset, false);
+        //        ::MoveWindow(hwnd, xD.right - 360, xD.top + 82, 360 - 8, xD.bottom - xD.top - 82 -
+        //        8,
+        //                     false);
+    });
+    timer->start();
+}
+
+// void AttachedWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
+//{
+//    MSG *msg = reinterpret_cast
+
+//    case WM_NCCALCSIZE: {
+//    }
+//}
+
+std::vector<AttachedWindow::Item> AttachedWindow::items;
+
+}  // namespace widgets
+}  // namespace chatterino
diff --git a/src/widgets/attachedwindow.hpp b/src/widgets/attachedwindow.hpp
new file mode 100644
index 000000000..a161f4164
--- /dev/null
+++ b/src/widgets/attachedwindow.hpp
@@ -0,0 +1,48 @@
+#pragma once
+
+#include <QWidget>
+
+#include "channel.hpp"
+#include "widgets/split.hpp"
+
+namespace chatterino {
+namespace widgets {
+
+class AttachedWindow : public QWidget
+{
+    AttachedWindow(void *target, int asdf);
+
+public:
+    ~AttachedWindow();
+
+    static AttachedWindow *get(void *target, const QString &winId, int yOffset);
+    static void detach(const QString &winId);
+
+    void setChannel(ChannelPtr channel);
+
+protected:
+    virtual void showEvent(QShowEvent *) override;
+    //    virtual void nativeEvent(const QByteArray &eventType, void *message, long *result)
+    //    override;
+
+private:
+    void *target;
+    int yOffset;
+
+    struct {
+        Split *split;
+    } ui;
+
+    void attachToHwnd(void *hwnd);
+
+    struct Item {
+        void *hwnd;
+        AttachedWindow *window;
+        QString winId;
+    };
+
+    static std::vector<Item> items;
+};
+
+}  // namespace widgets
+}  // namespace chatterino
diff --git a/src/widgets/basewidget.hpp b/src/widgets/basewidget.hpp
index 4b1e3f5fb..068297e7c 100644
--- a/src/widgets/basewidget.hpp
+++ b/src/widgets/basewidget.hpp
@@ -30,7 +30,7 @@ public:
     QSize getScaleIndependantSize() const;
     int getScaleIndependantWidth() const;
     int getScaleIndependantHeight() const;
-    void setScaleIndependantSize(int width, int height);
+    void setScaleIndependantSize(int width, int yOffset);
     void setScaleIndependantSize(QSize);
     void setScaleIndependantWidth(int value);
     void setScaleIndependantHeight(int value);
diff --git a/src/widgets/helper/channelview.cpp b/src/widgets/helper/channelview.cpp
index 5e89545bd..628a2c0f2 100644
--- a/src/widgets/helper/channelview.cpp
+++ b/src/widgets/helper/channelview.cpp
@@ -98,7 +98,7 @@ ChannelView::ChannelView(BaseWidget *parent)
     //    this->resizeEvent(e);
     //    delete e;
 
-    this->scrollBar.resize(this->scrollBar.width(), height() + 1);
+    this->scrollBar.resize(this->scrollBar.width(), this->height() + 1);
 
     singletons::SettingManager::getInstance().showLastMessageIndicator.connect(
         [this](auto, auto) { this->update(); }, this->managedConnections);
@@ -198,14 +198,14 @@ void ChannelView::actuallyLayoutMessages()
 
             y += message->getHeight();
 
-            if (y >= height()) {
+            if (y >= this->height()) {
                 break;
             }
         }
     }
 
     // layout the messages at the bottom to determine the scrollbar thumb size
-    int h = height() - 8;
+    int h = this->height() - 8;
 
     for (int i = (int)messagesSnapshot.getLength() - 1; i >= 0; i--) {
         auto *message = messagesSnapshot[i].get();
@@ -472,7 +472,7 @@ void ChannelView::updateLastReadMessage()
 
 void ChannelView::resizeEvent(QResizeEvent *)
 {
-    this->scrollBar.resize(this->scrollBar.width(), height());
+    this->scrollBar.resize(this->scrollBar.width(), this->height());
     this->scrollBar.move(this->width() - this->scrollBar.width(), 0);
 
     this->goToBottom->setGeometry(0, this->height() - 32, this->width(), 32);
@@ -559,7 +559,7 @@ void ChannelView::drawMessages(QPainter &painter)
         y += layout->getHeight();
 
         end = layout;
-        if (y > height()) {
+        if (y > this->height()) {
             break;
         }
     }
diff --git a/src/widgets/settingspages/highlightingpage.cpp b/src/widgets/settingspages/highlightingpage.cpp
index bbcd1d349..40842ba11 100644
--- a/src/widgets/settingspages/highlightingpage.cpp
+++ b/src/widgets/settingspages/highlightingpage.cpp
@@ -4,11 +4,13 @@
 #include <QListWidget>
 #include <QPushButton>
 #include <QTabWidget>
+#include <QTableView>
 #include <QTextEdit>
 
 #include "debug/log.hpp"
 #include "singletons/settingsmanager.hpp"
 #include "util/layoutcreator.hpp"
+#include "util/tupletablemodel.hpp"
 
 #define ENABLE_HIGHLIGHTS "Enable Highlighting"
 #define HIGHLIGHT_MSG "Highlight messages containing your name"
@@ -54,15 +56,47 @@ HighlightingPage::HighlightingPage()
             // HIGHLIGHTS
             auto highlights = tabs.appendTab(new QVBoxLayout, "Highlights");
             {
-                highlights.emplace<QListWidget>().assign(&this->highlightList);
+                QTableView *view = *highlights.emplace<QTableView>();
+                auto *model = new util::TupleTableModel<QString, bool, bool, bool>;
+                model->setTitles({"Pattern", "Flash taskbar", "Play sound", "Regex"});
+
+                // fourtf: could crash
+                for (const messages::HighlightPhrase &phrase :
+                     settings.highlightProperties.getValue()) {
+                    model->addRow(phrase.key, phrase.alert, phrase.sound, phrase.regex);
+                }
+
+                view->setModel(model);
+                view->setSelectionMode(QAbstractItemView::SingleSelection);
+                view->setSelectionBehavior(QAbstractItemView::SelectRows);
+                view->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
+                view->resizeColumnsToContents();
+                view->setColumnWidth(0, 250);
 
                 auto buttons = highlights.emplace<QHBoxLayout>();
 
-                buttons.emplace<QPushButton>("Add").assign(&this->highlightAdd);
-                buttons.emplace<QPushButton>("Edit").assign(&this->highlightEdit);
-                buttons.emplace<QPushButton>("Remove").assign(&this->highlightRemove);
+                model->itemsChanged.connect([model] {
+                    std::vector<messages::HighlightPhrase> phrases;
+                    for (int i = 0; i < model->getRowCount(); i++) {
+                        auto t = model->getRow(i);
+                        phrases.push_back(messages::HighlightPhrase{
+                            std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t),
+                        });
+                    }
+                    singletons::SettingManager::getInstance().highlightProperties.setValue(phrases);
+                });
 
-                this->addHighlightTabSignals();
+                auto add = buttons.emplace<QPushButton>("Add");
+                QObject::connect(*add, &QPushButton::clicked,
+                                 [model] { model->addRow("", true, false, false); });
+                auto remove = buttons.emplace<QPushButton>("Remove");
+                QObject::connect(*remove, &QPushButton::clicked, [view, model] {
+                    if (view->selectionModel()->hasSelection()) {
+                        model->removeRow(view->selectionModel()->selectedRows()[0].row());
+                    }
+                });
+
+                view->hideColumn(3);
             }
             // DISABLED USERS
             auto disabledUsers = tabs.appendTab(new QVBoxLayout, "Disabled Users");
@@ -90,157 +124,6 @@ HighlightingPage::HighlightingPage()
     this->disabledUsersChangedTimer.setSingleShot(true);
 }
 
-//
-// DISCLAIMER:
-//
-// If you are trying to learn from reading the chatterino code please ignore this segment.
-//
-void HighlightingPage::addHighlightTabSignals()
-{
-    singletons::SettingManager &settings = singletons::SettingManager::getInstance();
-
-    auto addBtn = this->highlightAdd;
-    auto editBtn = this->highlightEdit;
-    auto delBtn = this->highlightRemove;
-    auto highlights = this->highlightList;
-
-    // Open "Add new highlight" dialog
-    QObject::connect(addBtn, &QPushButton::clicked, this, [highlights, this, &settings] {
-        auto show = new QWidget();
-        auto box = new QBoxLayout(QBoxLayout::TopToBottom);
-
-        auto edit = new QLineEdit();
-        auto add = new QPushButton("Add");
-
-        auto sound = new QCheckBox("Play sound");
-        auto task = new QCheckBox("Flash taskbar");
-
-        // Save highlight
-        QObject::connect(add, &QPushButton::clicked, this, [=, &settings] {
-            if (edit->text().length()) {
-                QString highlightKey = edit->text();
-                highlights->addItem(highlightKey);
-
-                auto properties = settings.highlightProperties.getValue();
-
-                messages::HighlightPhrase newHighlightProperty;
-                newHighlightProperty.key = highlightKey;
-                newHighlightProperty.sound = sound->isChecked();
-                newHighlightProperty.alert = task->isChecked();
-
-                properties.push_back(newHighlightProperty);
-
-                settings.highlightProperties = properties;
-
-                show->close();
-            }
-        });
-        box->addWidget(edit);
-        box->addWidget(add);
-        box->addWidget(sound);
-        box->addWidget(task);
-        show->setLayout(box);
-        show->show();
-    });
-
-    // Open "Edit selected highlight" dialog
-    QObject::connect(editBtn, &QPushButton::clicked, this, [highlights, this, &settings] {
-        if (highlights->selectedItems().isEmpty()) {
-            // No item selected
-            return;
-        }
-
-        QListWidgetItem *selectedHighlight = highlights->selectedItems().first();
-        QString highlightKey = selectedHighlight->text();
-        auto properties = settings.highlightProperties.getValue();
-        auto highlightIt = std::find_if(properties.begin(), properties.end(),
-                                        [highlightKey](const auto &highlight) {
-                                            return highlight.key == highlightKey;  //
-                                        });
-
-        if (highlightIt == properties.end()) {
-            debug::Log("Unable to find highlight key {} in highlight properties. "
-                       "This is weird",
-                       highlightKey);
-            return;
-        }
-
-        messages::HighlightPhrase &selectedSetting = *highlightIt;
-        auto show = new QWidget();
-        auto box = new QBoxLayout(QBoxLayout::TopToBottom);
-
-        auto edit = new QLineEdit(highlightKey);
-        auto apply = new QPushButton("Apply");
-
-        auto sound = new QCheckBox("Play sound");
-        sound->setChecked(selectedSetting.sound);
-        auto task = new QCheckBox("Flash taskbar");
-        task->setChecked(selectedSetting.alert);
-
-        // Apply edited changes
-        QObject::connect(apply, &QPushButton::clicked, this, [=, &settings] {
-            QString newHighlightKey = edit->text();
-
-            if (newHighlightKey.length() == 0) {
-                return;
-            }
-
-            auto properties = settings.highlightProperties.getValue();
-            auto highlightIt =
-                std::find_if(properties.begin(), properties.end(), [=](const auto &highlight) {
-                    return highlight.key == highlightKey;  //
-                });
-
-            if (highlightIt == properties.end()) {
-                debug::Log("Unable to find highlight key {} in highlight properties. "
-                           "This is weird",
-                           highlightKey);
-                return;
-            }
-            auto &highlightProperty = *highlightIt;
-            highlightProperty.key = newHighlightKey;
-            highlightProperty.sound = sound->isCheckable();
-            highlightProperty.alert = task->isCheckable();
-
-            settings.highlightProperties = properties;
-
-            selectedHighlight->setText(newHighlightKey);
-            selectedHighlight->setText(newHighlightKey);
-
-            show->close();
-        });
-
-        box->addWidget(edit);
-        box->addWidget(apply);
-        box->addWidget(sound);
-        box->addWidget(task);
-        show->setLayout(box);
-        show->show();
-    });
-
-    // Delete selected highlight
-    QObject::connect(delBtn, &QPushButton::clicked, this, [highlights, &settings] {
-        if (highlights->selectedItems().isEmpty()) {
-            // No highlight selected
-            return;
-        }
-
-        QListWidgetItem *selectedHighlight = highlights->selectedItems().first();
-        QString highlightKey = selectedHighlight->text();
-
-        auto properties = settings.highlightProperties.getValue();
-        properties.erase(std::remove_if(properties.begin(), properties.end(),
-                                        [highlightKey](const auto &highlight) {
-                                            return highlight.key == highlightKey;  //
-                                        }),
-                         properties.end());
-
-        settings.highlightProperties = properties;
-
-        delete selectedHighlight;
-    });
-}
-
 }  // namespace settingspages
 }  // namespace widgets
 }  // namespace chatterino
diff --git a/src/widgets/settingspages/highlightingpage.hpp b/src/widgets/settingspages/highlightingpage.hpp
index 6bf447ece..5a1c3c756 100644
--- a/src/widgets/settingspages/highlightingpage.hpp
+++ b/src/widgets/settingspages/highlightingpage.hpp
@@ -2,6 +2,7 @@
 
 #include "widgets/settingspages/settingspage.hpp"
 
+#include <QAbstractTableModel>
 #include <QTimer>
 
 class QPushButton;
@@ -17,14 +18,7 @@ public:
     HighlightingPage();
 
 private:
-    QListWidget *highlightList;
-    QPushButton *highlightAdd;
-    QPushButton *highlightEdit;
-    QPushButton *highlightRemove;
-
     QTimer disabledUsersChangedTimer;
-
-    void addHighlightTabSignals();
 };
 
 }  // namespace settingspages
diff --git a/src/widgets/split.cpp b/src/widgets/split.cpp
index 9135a926c..ea20069e0 100644
--- a/src/widgets/split.cpp
+++ b/src/widgets/split.cpp
@@ -37,16 +37,24 @@ namespace chatterino {
 namespace widgets {
 
 Split::Split(SplitContainer *parent)
-    : BaseWidget(parent)
-    , parentPage(*parent)
+    : Split((BaseWidget *)parent)
+{
+    this->container = parent;
+}
+
+Split::Split(BaseWidget *widget)
+    : Split(widget->themeManager, widget)
+{
+}
+
+Split::Split(singletons::ThemeManager &manager, QWidget *parent)
+    : BaseWidget(manager, parent)
+    , container(nullptr)
     , channel(Channel::getEmpty())
     , vbox(this)
     , header(this)
     , view(this)
     , input(this)
-    , flexSizeX(1)
-    , flexSizeY(1)
-    , moderationMode(false)
 {
     this->setMouseTracking(true);
 
@@ -122,6 +130,11 @@ Split::~Split()
     this->indirectChannelChangedConnection.disconnect();
 }
 
+bool Split::isInContainer() const
+{
+    return this->container != nullptr;
+}
+
 IndirectChannel Split::getIndirectChannel()
 {
     return this->channel;
@@ -160,24 +173,26 @@ void Split::setChannel(IndirectChannel newChannel)
 
 void Split::setFlexSizeX(double x)
 {
-    this->flexSizeX = x;
-    this->parentPage.updateFlexValues();
+    //    this->flexSizeX = x;
+    //    this->parentPage->updateFlexValues();
 }
 
 double Split::getFlexSizeX()
 {
-    return this->flexSizeX;
+    //    return this->flexSizeX;
+    return 1;
 }
 
 void Split::setFlexSizeY(double y)
 {
-    this->flexSizeY = y;
-    this->parentPage.updateFlexValues();
+    //    this->flexSizeY = y;
+    //    this->parentPage.updateFlexValues();
 }
 
 double Split::getFlexSizeY()
 {
-    return this->flexSizeY;
+    //    return this->flexSizeY;
+    return 1;
 }
 
 void Split::setModerationMode(bool value)
@@ -206,7 +221,9 @@ void Split::showChangeChannelPopup(const char *dialogTitle, bool empty,
     dialog->closed.connect([=] {
         if (dialog->hasSeletedChannel()) {
             this->setChannel(dialog->getSelectedChannel());
-            this->parentPage.refreshTitle();
+            if (this->isInContainer()) {
+                this->container->refreshTitle();
+            }
         }
 
         callback(dialog->hasSeletedChannel());
@@ -286,15 +303,17 @@ void Split::handleModifiers(QEvent *event, Qt::KeyboardModifiers modifiers)
 /// Slots
 void Split::doAddSplit()
 {
-    SplitContainer *page = static_cast<SplitContainer *>(this->parentWidget());
-    page->addChat(true);
+    if (this->container) {
+        this->container->addChat(true);
+    }
 }
 
 void Split::doCloseSplit()
 {
-    SplitContainer *page = static_cast<SplitContainer *>(this->parentWidget());
-    page->removeFromLayout(this);
-    deleteLater();
+    if (this->container) {
+        this->container->removeFromLayout(this);
+        deleteLater();
+    }
 }
 
 void Split::doChangeChannel()
diff --git a/src/widgets/split.hpp b/src/widgets/split.hpp
index 871b7169a..ae64db3a2 100644
--- a/src/widgets/split.hpp
+++ b/src/widgets/split.hpp
@@ -43,7 +43,9 @@ class Split : public BaseWidget
     Q_OBJECT
 
 public:
-    Split(SplitContainer *parent);
+    explicit Split(SplitContainer *parent);
+    explicit Split(BaseWidget *widget);
+    explicit Split(singletons::ThemeManager &manager, QWidget *parent);
     ~Split() override;
 
     pajlada::Signals::NoArgSignal channelChanged;
@@ -75,6 +77,8 @@ public:
 
     void drag();
 
+    bool isInContainer() const;
+
 protected:
     void paintEvent(QPaintEvent *event) override;
     void mouseMoveEvent(QMouseEvent *event) override;
@@ -83,21 +87,22 @@ protected:
     void keyReleaseEvent(QKeyEvent *event) override;
 
 private:
-    SplitContainer &parentPage;
+    SplitContainer *container;
     IndirectChannel channel;
 
     QVBoxLayout vbox;
     SplitHeader header;
     ChannelView view;
     SplitInput input;
-    double flexSizeX;
-    double flexSizeY;
+    double flexSizeX = 1;
+    double flexSizeY = 1;
 
-    bool moderationMode;
+    bool moderationMode = false;
 
     pajlada::Signals::Connection channelIDChangedConnection;
     pajlada::Signals::Connection usermodeChangedConnection;
     pajlada::Signals::Connection indirectChannelChangedConnection;
+
     void doOpenAccountPopupWidget(AccountPopupWidget *widget, QString user);
     void channelNameUpdated(const QString &newChannelName);
     void handleModifiers(QEvent *event, Qt::KeyboardModifiers modifiers);
diff --git a/src/widgets/window.cpp b/src/widgets/window.cpp
index 94971f4ce..7c7504c29 100644
--- a/src/widgets/window.cpp
+++ b/src/widgets/window.cpp
@@ -11,10 +11,14 @@
 #include "widgets/split.hpp"
 
 #include <QApplication>
+#include <QHeaderView>
 #include <QPalette>
 #include <QShortcut>
 #include <QVBoxLayout>
 
+#include <QTableView>
+#include "util/tupletablemodel.hpp"
+
 namespace chatterino {
 namespace widgets {
 
diff --git a/src/widgets/window.hpp b/src/widgets/window.hpp
index b3b7a098f..850fb06b3 100644
--- a/src/widgets/window.hpp
+++ b/src/widgets/window.hpp
@@ -23,7 +23,7 @@ class Window : public BaseWindow
     Q_OBJECT
 
 public:
-    enum WindowType { Main, Popup };
+    enum WindowType { Main, Popup, Attached };
 
     explicit Window(singletons::ThemeManager &_themeManager, WindowType type);