diff --git a/application.cpp b/application.cpp index d80bda5..410f3c2 100644 --- a/application.cpp +++ b/application.cpp @@ -1,4 +1,7 @@ // SPDX-License-Identifier: MIT + +#include "application.h" + #define _X_OPEN_SOURCE #include "tui.h" @@ -35,6 +38,8 @@ size_t title_offset = 0; bool any_title_in_next_half = false; bool clear_channels_on_change = false; +static application_host *host = nullptr; + static termpaint_attr* get_attr(const AttributeSetType type, const bool highlight=false) { return highlight ? attributes[type].highlight : attributes[type].normal; @@ -368,18 +373,25 @@ void action_refresh_channel() { const int new_videos = fetch_videos_for_channel(channels.at(selected_channel)); if(new_videos == 0 || ch.is_virtual) return; - if(new_videos == 1 && !notify_channel_new_video_command.empty()) { - run_command(notify_channel_new_video_command, { - {"{{channelName}}", ch.name}, - {"{{videoTitle}}", videos[ch.id].front().title}, - }); - } else if(notify_channel_new_videos_command.size()) { - run_command(notify_channel_new_videos_command, { - {"{{channelName}}", ch.name}, - {"{{newVideos}}", std::to_string(new_videos)} - }); + if(new_videos == 1) { + if(host && host->notify_channel_single_video) { + host->notify_channel_single_video(ch.name, videos[ch.id].front().title); + } else if(!notify_channel_new_video_command.empty()) { + run_command(notify_channel_new_video_command, { + {"{{channelName}}", ch.name}, + {"{{videoTitle}}", videos[ch.id].front().title}, + }); + } + } else { + if(host && host->notify_channel_multiple_videos) { + host->notify_channel_multiple_videos(ch.name, new_videos); + } else if(notify_channel_new_videos_command.size()) { + run_command(notify_channel_new_videos_command, { + {"{{channelName}}", ch.name}, + {"{{newVideos}}", std::to_string(new_videos)} + }); + } } - } void action_refresh_all_channels(bool ask=true) { @@ -395,11 +407,15 @@ void action_refresh_all_channels(bool ask=true) { if(count) updated_channels++; } - if(updated_channels && new_videos && !notify_channels_new_videos_command.empty()) { - run_command(notify_channels_new_videos_command, { - {"{{updatedChannels}}", std::to_string(updated_channels)}, - {"{{newVideos}}", std::to_string(new_videos)} - }); + if(updated_channels && new_videos) { + if(host && host->notify_channels_multiple_videos) { + host->notify_channels_multiple_videos(updated_channels, new_videos); + } else if(!notify_channels_new_videos_command.empty()) { + run_command(notify_channels_new_videos_command, { + {"{{updatedChannels}}", std::to_string(updated_channels)}, + {"{{newVideos}}", std::to_string(new_videos)} + }); + } } } @@ -645,6 +661,10 @@ static void run() bool draw = true; do { + if(host && host->quit && host->quit()) { + break; + } + if(draw) { Channel &channel = channels.at(selected_channel); termpaint_surface_clear(surface, TERMPAINT_DEFAULT_COLOR, TERMPAINT_DEFAULT_COLOR); @@ -654,7 +674,7 @@ static void run() } draw = true; - auto event = tp_wait_for_event(1000); + auto event = tp_wait_for_event(500); if(!event) abort(); @@ -683,9 +703,10 @@ void run_standalone() tp_shutdown(); } -void run_embedded(int pty_fd) +void run_embedded(int pty_fd, application_host *_host) { tp_init_from_fd(pty_fd); + host = _host; run(); tp_shutdown(); } diff --git a/application.h b/application.h index 816a080..e261ca4 100644 --- a/application.h +++ b/application.h @@ -1,4 +1,14 @@ #pragma once +#include +#include + +struct application_host { + std::function quit = nullptr; + std::function notify_channel_single_video = nullptr; + std::function notify_channel_multiple_videos = nullptr; + std::function notify_channels_multiple_videos = nullptr; +}; + void run_standalone(); -void run_embedded(int pty_fd); +void run_embedded(int pty_fd, application_host *host); diff --git a/icons/icon_0.png b/icons/icon_0.png new file mode 100644 index 0000000..155a0b6 Binary files /dev/null and b/icons/icon_0.png differ diff --git a/icons/icon_0.svg b/icons/icon_0.svg new file mode 100644 index 0000000..38ba004 --- /dev/null +++ b/icons/icon_0.svg @@ -0,0 +1,96 @@ + + + + + + + + image/svg+xml + + + + + + + + + + >yt + + + diff --git a/meson.build b/meson.build index 3c87c1d..e21bb17 100644 --- a/meson.build +++ b/meson.build @@ -30,3 +30,23 @@ tui_files = [ 'yttui.cpp', ] executable('yttui', tui_files, link_with: [application], install: true) + +qt5 = import('qt5') +qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Widgets'], required: false) +if qt5_dep.found() + kf5service_dep = dependency('KF5Service', required: false) + kf5core_dep = dependency('KF5CoreAddons', required: false) + kf5parts_dep = dependency('KF5Parts', required: false) + + cppc = meson.get_compiler('cpp') + libutil_dep = cppc.find_library('util') + + moc_files = qt5.preprocess(moc_sources: ['yttui-qt5.cpp'], qresources: ['yttui-qt5.qrc']) + + executable('yttui-qt5', + ['yttui-qt5.cpp', moc_files], + link_with: [application], + dependencies: [libutil_dep, qt5_dep, kf5service_dep, kf5core_dep, kf5parts_dep], + install: true + ) +endif diff --git a/yttui-qt5.cpp b/yttui-qt5.cpp new file mode 100644 index 0000000..c5288bf --- /dev/null +++ b/yttui-qt5.cpp @@ -0,0 +1,194 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "application.h" + +application_host *host = nullptr; + +class AppThread: public QThread +{ + Q_OBJECT + int app_fd; +public: + AppThread(int fd): QThread(), app_fd(fd){} + ~AppThread() = default; + + friend bool app_quit(void*); +protected: + void run() override + { + run_embedded(app_fd, host); + } +}; + +class RightClickFilter: public QObject +{ + Q_OBJECT +protected: + bool eventFilter(QObject *obj, QEvent *event) + { + if((event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)) { + QMouseEvent *mouseEvent = static_cast(event); + if(mouseEvent->buttons() & Qt::RightButton) { + return true; + } + } + else if(event->type() == QEvent::ContextMenu) { + return true; + } + return QObject::eventFilter(obj, event); + } +}; + +QObject* find_obj_by_classname(QObject *in, const char* name) +{ + if(strcmp(name, in->metaObject()->className()) == 0) + return in; + + for(QObject *child: in->children()) + { + QObject *maybe = find_obj_by_classname(child, name); + if(maybe) + return maybe; + } + return nullptr; +} + +class ApplicationWindow: public QMainWindow +{ + Q_OBJECT + + QIcon icon; + QSystemTrayIcon *systray = nullptr; + KParts::ReadOnlyPart *terminal = nullptr; + QWidget *konsole = nullptr; + +public: + ApplicationWindow(int fd); + + void showMessage(const QString &title, const QString &message); + + // QWidget interface +protected: + void closeEvent(QCloseEvent *event) override; +}; + +ApplicationWindow::ApplicationWindow(int fd): icon(":/icons/icon_0.png"), systray(new QSystemTrayIcon(icon, this)) +{ + setWindowTitle("yttui-qt5"); + setWindowIcon(icon); + + KService::Ptr service = KService::serviceByDesktopName(QStringLiteral("konsolepart")); + Q_ASSERT(service); + KPluginFactory* factory = KPluginLoader(service->library()).factory(); + Q_ASSERT(factory); + + terminal = factory->create(this); + if(!QMetaObject::invokeMethod(terminal, "openTeletype", Qt::AutoConnection, Q_ARG(int, fd), Q_ARG(bool, false))) { + fputs("Failed to set KonsolePart PTY\n", stderr); + abort(); + } + + konsole = terminal->widget(); + setCentralWidget(konsole); + + QObject *td = find_obj_by_classname(konsole, "Konsole::TerminalDisplay"); + if(td) { + td->installEventFilter(new RightClickFilter()); + } + + QMenu *systrayContextMenu = new QMenu(this); + systrayContextMenu->addAction(QIcon::fromTheme("application-exit"), "Exit", QApplication::instance(), &QApplication::quit); + + systray->setContextMenu(systrayContextMenu); + connect(systray, &QSystemTrayIcon::activated, [this](QSystemTrayIcon::ActivationReason reason) { + if(reason == QSystemTrayIcon::Trigger) { + if(isVisible()) { + hide(); + } else { + show(); + } + } + }); + systray->show(); +} + +void ApplicationWindow::showMessage(const QString &title, const QString &message) +{ + systray->showMessage(title, message, icon); +} + +void ApplicationWindow::closeEvent(QCloseEvent *event) +{ + QMainWindow::closeEvent(event); + systray->hide(); +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + int term_fd = -1, app_fd = -1; + if(openpty(&term_fd, &app_fd, 0, 0, 0)) { + perror("openpty"); + } + + ApplicationWindow window(term_fd); + window.show(); + + bool app_quit = false; + + AppThread appthread(app_fd); + QObject::connect(&appthread, &AppThread::finished, [&] { + window.close(); + }); + QObject::connect(&app, &QApplication::aboutToQuit, [&] { + app_quit = true; + appthread.wait(); + }); + + host = new application_host; + host->quit = [&]{ return app_quit; }; + host->notify_channel_single_video = [&](const std::string &channel, const std::string &title) { + window.showMessage(QStringLiteral("New video from %1").arg(QString::fromStdString(channel)), QString::fromStdString(title)); + }; + host->notify_channel_multiple_videos = [&](const std::string &channel, const int videos) { + window.showMessage(QStringLiteral("New videos from %1").arg(QString::fromStdString(channel)), + QStringLiteral("There are %1 new videos.").arg(QString::number(videos))); + }; + host->notify_channels_multiple_videos = [&](const int channels, const int videos) { + window.showMessage("New videos from multiple channels", + QStringLiteral("There are %2 new videos from %1 channels.").arg(QString::number(channels), QString::number(videos))); + }; + + appthread.start(); + int rc = app.exec(); + close(term_fd); + close(app_fd); + + delete host; + + return rc; +} + +#include "yttui-qt5.moc" diff --git a/yttui-qt5.qrc b/yttui-qt5.qrc new file mode 100644 index 0000000..a62f325 --- /dev/null +++ b/yttui-qt5.qrc @@ -0,0 +1,5 @@ + + + icons/icon_0.png + +