• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Tatsh / kate-wakatime / #7

04 Aug 2025 02:33PM UTC coverage: 0.0%. Remained the same
#7

push

travis-ci

Tatsh
wakatimeplugin: remove extra disconnect() call

0 of 194 relevant lines covered (0.0%)

0.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/wakatimeplugin.cpp
1
/**
2
 * This file is part of kate-wakatime.
3
 * Copyright 2014 Andrew Udvare <audvare@gmail.com>
4
 *
5
 *   This program is free software; you can redistribute it and/or modify
6
 *   it under the terms of the GNU Library General Public License version
7
 *   3, or (at your option) any later version, as published by the Free
8
 *   Software Foundation.
9
 *
10
 *   This library is distributed in the hope that it will be useful, but
11
 *   WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13
 *   Library General Public License for more details.
14
 *
15
 *   You should have received a copy of the GNU Library General Public
16
 *   License along with the kdelibs library; see the file COPYING.LIB. If
17
 *   not, write to the Free Software Foundation, Inc., 51 Franklin Street,
18
 *   Fifth Floor, Boston, MA 02110-1301, USA. or see
19
 *   <http://www.gnu.org/licenses/>.
20
 */
21

22
#include "wakatimeplugin.h"
23

24
#include <KTextEditor/Application>
25
#include <KTextEditor/Document>
26
#include <KTextEditor/Editor>
27
#include <KTextEditor/MainWindow>
28
#include <KTextEditor/View>
29

30
#include <KAboutData>
31
#include <KActionCollection>
32
#include <KF6/KTextEditor/ktexteditor_version.h>
33
#include <KLocalizedString>
34
#include <KMessageBox>
35
#include <KPluginFactory>
36
#include <KXMLGUIFactory>
37

38
#include <QtCore/QDateTime>
39
#include <QtCore/QDir>
40
#include <QtCore/QFile>
41
#include <QtCore/QJsonDocument>
42
#include <QtCore/QProcess>
43
#include <QtCore/QTimeZone>
44
#include <QtCore/QUrl>
45
#include <QtNetwork/QNetworkAccessManager>
46
#include <QtNetwork/QNetworkReply>
47
#include <QtNetwork/QNetworkRequest>
48
#include <QtWidgets/QDialog>
49

50
Q_LOGGING_CATEGORY(gLogWakaTime, "wakatime")
×
51

52
K_PLUGIN_FACTORY_WITH_JSON(WakaTimePluginFactory,
×
53
                           "ktexteditor_wakatime.json",
×
54
                           registerPlugin<WakaTimePlugin>();)
×
55

×
56
const QString kSettingsKeyApiKey = QStringLiteral("settings/api_key");
57
const QString kSettingsKeyApiUrl = QStringLiteral("settings/api_url");
58
const QString kSettingsKeyHideFilenames = QStringLiteral("settings/hidefilenames");
59
const QString kStringLiteralSlash = QStringLiteral("/");
60
const QString kWakaTimeCli = QStringLiteral("wakatime-cli");
61

62
WakaTimePlugin::WakaTimePlugin(QObject *parent, const QVariantList &args)
63
    : KTextEditor::Plugin(parent) {
64
    Q_UNUSED(args);
65
}
×
66

×
67
WakaTimePlugin::~WakaTimePlugin() {
68
}
69

70
void WakaTimeView::viewCreated(KTextEditor::View *view) {
×
71
    connectDocumentSignals(view->document());
72
}
×
73

74
void WakaTimeView::viewDestroyed(QObject *view) {
×
75
    disconnectDocumentSignals(static_cast<KTextEditor::View *>(view)->document());
76
}
77

×
78
WakaTimeView::WakaTimeView(KTextEditor::MainWindow *mainWindow)
×
79
    : QObject(mainWindow), m_mainWindow(mainWindow), apiKey(QString()),
80
      binPathCache(QMap<QString, QString>()), hasSent(false), lastFileSent(QString()),
81
      lastTimeSent(QDateTime::currentDateTime()) {
×
82
    KXMLGUIClient::setComponentName(QStringLiteral("katewakatime"), i18n("WakaTime"));
×
83
    setXMLFile(QStringLiteral("ui.rc"));
84
    QAction *const a = actionCollection()->addAction(QStringLiteral("configure_wakatime"));
85
    a->setText(i18n("Configure WakaTime..."));
×
86
    a->setIcon(QIcon::fromTheme(QStringLiteral("wakatime")));
×
87
    connect(a, &QAction::triggered, this, &WakaTimeView::slotConfigureWakaTime);
×
88
    mainWindow->guiFactory()->addClient(this);
×
89
    // Configuration
×
90
    const QString configFilePath =
×
91
        QDir::homePath() + QDir::separator() + QStringLiteral(".wakatime.cfg");
×
92
    config = new QSettings(configFilePath, QSettings::IniFormat, this);
×
93
    readConfig();
×
94
    // Connections
×
95
    connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &WakaTimeView::viewCreated);
×
96
    for (KTextEditor::View *const view : m_mainWindow->views()) {
97
        connectDocumentSignals(view->document());
98
    }
×
99
}
×
100

×
101
WakaTimeView::~WakaTimeView() {
102
    delete config;
×
103
    m_mainWindow->guiFactory()->removeClient(this);
×
104
}
×
105

106
QObject *WakaTimePlugin::createView(KTextEditor::MainWindow *mainWindow) {
107
    return new WakaTimeView(mainWindow);
108
}
×
109

×
110
void WakaTimeView::slotConfigureWakaTime() {
×
111
    QDialog dialog(m_mainWindow->window());
112
    Ui::ConfigureWakaTimeDialog ui;
×
113
    ui.setupUi(&dialog);
114
    ui.lineEdit_apiKey->setText(apiKey);
115
    if (apiKey.isNull() || !apiKey.size()) {
116
        ui.lineEdit_apiKey->setFocus();
×
117
    }
×
118
    ui.lineEdit_apiUrl->setText(apiUrl);
×
119
    ui.checkBox_hideFilenames->setChecked(hideFilenames);
120
    dialog.setWindowTitle(i18n("Configure WakaTime"));
121
    if (dialog.exec() == QDialog::Accepted) {
×
122
        QString newApiKey = ui.lineEdit_apiKey->text();
×
123
        if (newApiKey.size() >= 36 && newApiKey.size() <= 41) {
124
            apiKey = newApiKey;
125
        }
×
126
        hideFilenames = ui.checkBox_hideFilenames->isChecked();
×
127
        writeConfig();
128
    }
×
129
}
×
130

×
131
QString WakaTimeView::getBinPath(const QString &binName) {
×
132
#ifdef Q_OS_WIN
133
    return QString();
×
134
#endif
×
135
    if (binPathCache.contains(binName)) {
×
136
        return binPathCache.value(binName);
×
137
    }
×
138
    QString dotWakaTime = QStringLiteral("%1/.wakatime").arg(QDir::homePath());
×
139
    static const char *const kDefaultPath = "/usr/bin:/usr/local/bin:/opt/bin:/opt/local/bin";
×
140
    static const QString colon = QStringLiteral(":");
141

×
142
    const char *const path = getenv("PATH");
×
143
    QStringList paths =
144
        QString::fromUtf8(path ? path : kDefaultPath).split(colon, Qt::SkipEmptyParts);
145
    paths.insert(0, dotWakaTime);
146
    for (QString path : paths) {
×
147
        QStringList dirListing = QDir(path).entryList();
148
        for (QString entry : dirListing) {
149
            if (entry == binName) {
150
                entry = path.append(kStringLiteralSlash).append(entry);
×
151
                binPathCache[binName] = entry;
×
152
                return entry;
153
            }
×
154
        }
155
    }
156
    return QString();
157
}
×
158

159
QString WakaTimeView::getProjectDirectory(const QFileInfo &fileInfo) {
×
160
    QDir currentDirectory = QDir(fileInfo.canonicalPath());
×
161
    static QStringList filters;
×
162
    static const QString gitStr = QStringLiteral(".git");
×
163
    static const QString svnStr = QStringLiteral(".svn");
×
164
    filters << gitStr << svnStr;
×
165
    bool vcDirFound = false;
×
166
    while (!vcDirFound) {
×
167
        if (!currentDirectory.canonicalPath().compare(kStringLiteralSlash)) {
×
168
            break;
169
        }
170
        QFileInfoList entries =
171
            currentDirectory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden);
×
172
        for (QFileInfo entry : entries) {
173
            QString name = entry.fileName();
174
            if ((name.compare(gitStr) || name.compare(svnStr)) && entry.isDir()) {
×
175
                vcDirFound = true;
×
176
                return currentDirectory.dirName();
177
            }
178
        }
179
        currentDirectory.cdUp();
×
180
    }
×
181
    return QString();
×
182
}
×
183

×
184
void WakaTimeView::sendAction(KTextEditor::Document *doc, bool isWrite) {
185
    auto wakatimeCliPath = getBinPath(kWakaTimeCli);
186
    if (wakatimeCliPath.isEmpty()) {
×
187
        wakatimeCliPath = getBinPath(QStringLiteral("wakatime"));
×
188
        if (!wakatimeCliPath.isEmpty()) {
×
189
            qCWarning(gLogWakaTime) << "wakatime-cli not found in PATH.";
×
190
            return;
×
191
        }
×
192
    }
193
    auto filePath = doc->url().toLocalFile();
194
    // Could be untitled, or a URI (including HTTP). Only local files are
×
195
    // handled for now.
196
    if (filePath.isEmpty()) {
×
197
        qCDebug(gLogWakaTime) << "Nothing to send about";
198
        return;
199
    }
×
200
    QStringList arguments;
×
201
    const QFileInfo fileInfo(filePath);
×
202
    // They have it sending the real file path, maybe not respecting symlinks,
×
203
    // etc.
×
204
    filePath = fileInfo.canonicalFilePath();
×
205
    qCDebug(gLogWakaTime) << "File path:" << filePath;
×
206
    // Compare date and make sure it has been at least 15 minutes.
207
    const auto currentMs = QDateTime::currentMSecsSinceEpoch();
208
    const auto deltaMs = currentMs - lastTimeSent.toMSecsSinceEpoch();
×
209
    static const auto intervalMs = 120000; // ms
210
    // If the current file has not changed and it has not been 2 minutes since the last heartbeat
211
    // was sent, do NOT send this heartbeat. This does not apply to write events as they are
×
212
    // always sent.
×
213
    if (!isWrite) {
×
214
        if (hasSent && deltaMs <= intervalMs && lastFileSent == filePath) {
215
            qCDebug(gLogWakaTime) << "Not enough time has passed since last send";
×
216
            qCDebug(gLogWakaTime) << "Delta:" << deltaMs / 1000 / 60 << "/ 2 minutes";
×
217
            return;
218
        }
219
    }
×
220
    arguments << QStringLiteral("--entity") << filePath;
×
221
    arguments << QStringLiteral("--plugin")
222
              << QStringLiteral("ktexteditor-wakatime/%1").arg(VERSION);
×
223
    arguments << QStringLiteral("--key") << apiKey;
×
224
    if (!apiUrl.isEmpty()) {
225
        arguments << QStringLiteral("--api-url") << apiUrl;
226
    }
227
    if (hideFilenames) {
228
        arguments << QStringLiteral("--hide-filenames");
×
229
    }
×
230
    // Get the project name by traversing up until .git or .svn is found.
×
231
    auto projectName = getProjectDirectory(fileInfo);
×
232
    if (!projectName.isEmpty()) {
×
233
        arguments << QStringLiteral("--alternate-project") << projectName;
234
    } else {
235
        qCDebug(gLogWakaTime) << "Warning: No project name found";
×
236
    }
×
237
    if (isWrite) {
×
238
        arguments << QStringLiteral("--write");
×
239
    }
×
240
    // This is good enough for the language most of the time.
×
241
    QString mode = doc->mode();
242
    static const QString keyLanguage = QStringLiteral("language");
×
243
    if (!mode.isEmpty()) {
×
244
        arguments << QStringLiteral("--language") << mode;
245
    } else {
246
        mode = doc->highlightingMode();
×
247
        if (!mode.isEmpty()) {
×
248
            arguments << QStringLiteral("--language") << mode;
×
249
        }
250
    }
×
251
    for (KTextEditor::View *view : m_mainWindow->views()) {
252
        if (view->document() == doc) {
×
253
            arguments << QStringLiteral("--lineno")
×
254
                      << QString::number(view->cursorPosition().line() + 1);
255
            arguments << QStringLiteral("--cursorpos")
256
                      << QString::number(view->cursorPosition().column() + 1);
×
257
            arguments << QStringLiteral("--lines-in-file")
258
                      << QString::number(view->document()->lines());
×
259
            break;
×
260
        }
261
    }
×
262
    qCDebug(gLogWakaTime) << "Running:" << wakatimeCliPath << arguments.join(QStringLiteral(" "));
×
263
    auto ret = QProcess::execute(wakatimeCliPath, arguments);
×
264
    if (ret != 0) {
265
        qCWarning(gLogWakaTime) << "wakatime-cli returned error code" << ret;
266
        return;
×
267
    }
×
268
    lastTimeSent = QDateTime::currentDateTime();
×
269
    lastFileSent = filePath;
×
270
    hasSent = true;
×
271
}
×
272

×
273
void WakaTimeView::writeConfig(void) {
×
274
    config->setValue(kSettingsKeyApiKey, apiKey);
×
275
    config->setValue(kSettingsKeyApiUrl, apiUrl);
276
    config->setValue(kSettingsKeyHideFilenames, hideFilenames);
277
    config->sync();
×
278
    if (config->status() != QSettings::NoError) {
×
279
        qCDebug(gLogWakaTime) << "Failed to save WakaTime settings:" << config->status();
×
280
    }
×
281
}
×
282

283
void WakaTimeView::readConfig(void) {
×
284
    const QString apiKeyPath = kSettingsKeyApiKey;
×
285
    const QString apiUrlPath = kSettingsKeyApiUrl;
×
286

287
    if (!config->contains(kSettingsKeyApiKey)) {
288
        qCDebug(gLogWakaTime) << "No API key set in ~/.wakatime.cfg";
×
289
        return;
×
290
    }
×
291

×
292
    const QString key = config->value(kSettingsKeyApiKey).toString().trimmed();
×
293
    if (!key.length()) {
×
294
        qCDebug(gLogWakaTime) << "API Key is blank";
×
295
        return;
296
    }
297

298
    QString url = QStringLiteral("https://api.wakatime.com/api/v1/");
×
299
    if (config->contains(kSettingsKeyApiUrl) &&
×
300
        QString(config->value(kSettingsKeyApiUrl).toString()).trimmed().length()) {
×
301
        url = QString(config->value(kSettingsKeyApiUrl).toString()).trimmed();
302
    }
×
303

×
304
    // Assume valid at this point
×
305
    apiKey = key;
306
    apiUrl = url;
307
    hideFilenames = config->value(kSettingsKeyHideFilenames).toBool();
×
308
}
×
309

×
310
bool WakaTimeView::documentIsConnected(KTextEditor::Document *document) {
×
311
    for (const KTextEditor::Document *const doc : connectedDocuments) {
312
        if (doc == document) {
313
            return true;
×
314
        }
×
315
    }
×
316
    return false;
×
317
}
318

319
void WakaTimeView::connectDocumentSignals(KTextEditor::Document *document) {
320
    if (!document || documentIsConnected(document)) {
×
321
        return;
×
322
    }
×
323

324
    // When document goes from saved state to changed state (not yet saved on
325
    // disk)
×
326
    connect(document,
×
327
            &KTextEditor::Document::modifiedChanged,
×
328
            this,
×
329
            &WakaTimeView::slotDocumentModifiedChanged);
330

331
    // Written to disk
×
332
    connect(document,
333
            &KTextEditor::Document::documentSavedOrUploaded,
334
            this,
×
335
            &WakaTimeView::slotDocumentWrittenToDisk);
×
336

×
337
    // Text changes (might be heavy)
338
    // This event unfortunately is emitted twice in separate threads for every
339
    // key stroke (maybe key up and down is the reason)
340
    connect(document,
341
            &KTextEditor::Document::textChanged,
×
342
            this,
343
            &WakaTimeView::slotDocumentModifiedChanged);
344

×
345
    connectedDocuments << document;
346
}
347

×
348
void WakaTimeView::disconnectDocumentSignals(KTextEditor::Document *document) {
349
    if (!documentIsConnected(document)) {
350
        return;
×
351
    }
352
    disconnect(document, SIGNAL(modifiedChanged(KTextEditor::Document *)));
353
    disconnect(document, SIGNAL(documentSavedOrUploaded(KTextEditor::Document *, bool)));
354
    disconnect(document, SIGNAL(textChanged(KTextEditor::Document *)));
355
    connectedDocuments.removeOne(document);
×
356
}
357

358
// Slots
×
359
void WakaTimeView::slotDocumentModifiedChanged(KTextEditor::Document *doc) {
360
    sendAction(doc, false);
×
361
}
362

363
void WakaTimeView::slotDocumentWrittenToDisk(KTextEditor::Document *doc) {
×
364
    sendAction(doc, true);
×
365
}
×
366

367
#include "wakatimeplugin.moc"
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc