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

Tatsh / kate-wakatime / #8

04 Aug 2025 10:01PM UTC coverage: 0.0%. Remained the same
#8

push

travis-ci

Tatsh
wakatimeplugin: auto

0 of 14 new or added lines in 1 file covered. (0.0%)

3 existing lines in 1 file now uncovered.

0 of 193 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 auto kSettingsKeyApiKey = QStringLiteral("settings/api_key");
57
const auto kSettingsKeyApiUrl = QStringLiteral("settings/api_url");
58
const auto kSettingsKeyHideFilenames = QStringLiteral("settings/hidefilenames");
59
const auto kStringLiteralSlash = QStringLiteral("/");
60
const auto 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
    auto 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
×
NEW
90
    const auto 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) {
×
NEW
122
        auto 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
    }
×
NEW
138
    auto dotWakaTime = QStringLiteral("%1/.wakatime").arg(QDir::homePath());
×
NEW
139
    static const auto *const kDefaultPath = "/usr/bin:/usr/local/bin:/opt/bin:/opt/local/bin";
×
140
    static const auto colon = QStringLiteral(":");
141

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

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

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

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

281
void WakaTimeView::readConfig(void) {
×
NEW
282
    const auto apiKeyPath = kSettingsKeyApiKey;
×
NEW
283
    const auto apiUrlPath = kSettingsKeyApiUrl;
×
284

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

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

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

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

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

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

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

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

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

×
343
    connectedDocuments << document;
344
}
345

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

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

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

365
#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