Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
const Desklet = imports.ui.desklet;
const GLib = imports.gi.GLib;
const Mainloop = imports.mainloop;
const St = imports.gi.St;
const Gettext = imports.gettext;
const Util = imports.misc.util;
const Settings = imports.ui.settings;
const Tooltips = imports.ui.tooltips;

const UUID = "github-contribution-grid@KopfdesDaemons";

const { GitHubHelper } = require("./helpers/github.helper");

Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale");

function _(str) {
return Gettext.dgettext(UUID, str);
}

class MyDesklet extends Desklet.Desklet {
constructor(metadata, deskletId) {
super(metadata, deskletId);
this.githubUsername = "";
this.githubToken = "";
this.blockSize = 11;
this.refreshInterval = 15;
this.backgroundColor = "rgba(255, 255, 255, 0)";
this.showContributionCount = false;
this.showUsername = true;
this.timeoutId = null;
this.contributionData = null;
this.mainContainer = null;

this.bindSettings(metadata, deskletId);

this.setHeader(_("GitHub Contribution Grid"));

this.mainContainer = new St.BoxLayout({ vertical: true, style_class: "github-contribution-grid-main-container" });
this.mainContainer.add_child(this._createHeader());
this.setContent(this.mainContainer);

this._updateLoop();

// The first request after system start will fail
// Delay to ensure network services are ready and try again
Mainloop.timeout_add_seconds(10, () => {
this._setupContributionData();
});
}

bindSettings(metadata, deskletId) {
this.settings = new Settings.DeskletSettings(this, metadata["uuid"], deskletId);
this.settings.bindProperty(Settings.BindingDirection.IN, "github-username", "githubUsername", this.onDataSettingChanged);
this.settings.bindProperty(Settings.BindingDirection.IN, "github-token", "githubToken", this.onDataSettingChanged);
this.settings.bindProperty(Settings.BindingDirection.IN, "refresh-interval", "refreshInterval", this.onDataSettingChanged);
this.settings.bindProperty(Settings.BindingDirection.IN, "block-size", "blockSize", this.onStyleSettingChanged);
this.settings.bindProperty(Settings.BindingDirection.IN, "background-color", "backgroundColor", this.onStyleSettingChanged);
this.settings.bindProperty(Settings.BindingDirection.IN, "show-username", "showUsername", this.onStyleSettingChanged);
this.settings.bindProperty(Settings.BindingDirection.IN, "show-contribution-count", "showContributionCount", this.onStyleSettingChanged);
}

on_desklet_removed() {
if (this.timeoutId) {
Mainloop.source_remove(this.timeoutId);
this.timeoutId = null;
}
}

onDataSettingChanged = () => {
this.mainContainer.remove_all_children();
this.mainContainer.add_child(this._createHeader());
this._setupContributionData();
};

onStyleSettingChanged = () => {
this.mainContainer.remove_all_children();
this.mainContainer.add_child(this._createHeader());
this._renderContent(this.contributionData);
};

async _setupContributionData() {
this.contributionData = null;

if (!this.githubUsername || !this.githubToken) {
this._renderContent(null, _("Please configure username and token in settings."));
return;
}

try {
const response = await GitHubHelper.getContributionData(this.githubUsername, this.githubToken);
this.contributionData = response;
this._renderContent(this.contributionData);
} catch (e) {
global.logError(`[${UUID}] Error fetching contribution data: ${e}`);
this._renderContent(null, e.message);
}
}

_createHeader() {
const headerContainer = new St.BoxLayout({ style_class: "github-contribution-grid-header-container" });

// Reload button
const reloadButton = new St.Button({ style_class: "github-contribution-grid-reload-bin" });
reloadButton.connect("button-press-event", () => this._setupContributionData());
const reloadIcon = new St.Icon({
icon_name: "view-refresh-symbolic",
icon_type: St.IconType.SYMBOLIC,
icon_size: 16,
});
new Tooltips.Tooltip(reloadButton, _("Reload"));
reloadButton.set_child(reloadIcon);
headerContainer.add_child(reloadButton);

// Username
if (this.showUsername) {
const usernameButton = new St.Button({ label: this.githubUsername, style_class: "github-contribution-grid-label-bin" });
usernameButton.connect("button-press-event", () => Util.spawnCommandLine(`xdg-open "https://github.com/${this.githubUsername}"`));
new Tooltips.Tooltip(usernameButton, _("Open GitHub profile"));
headerContainer.add_child(usernameButton);
}

return headerContainer;
}

_renderContent(weeks, error = null) {
if (this.contentContainer) {
this.mainContainer.remove_child(this.contentContainer);
this.contentContainer.destroy();
}

this.contentContainer = new St.BoxLayout({
style_class: "github-contribution-grid-container",
x_expand: true,
style: `background-color: ${this.backgroundColor};`,
});

if (!this.githubUsername || !this.githubToken) {
// UI for Desklet Setup
const setupBox = new St.BoxLayout({ vertical: true, style_class: "github-contribution-grid-setup-container" });
setupBox.add_child(new St.Label({ text: "GitHub Contribution Grid", style_class: "github-contribution-grid-setup-headline" }));
setupBox.add_child(new St.Label({ text: _("Please configure username and token in settings.") }));

const createTokenButton = new St.Button({ style_class: "github-contribution-grid-link", label: _("Create a GitHub token") });
createTokenButton.connect("clicked", () => Util.spawnCommandLine(`xdg-open "${GitHubHelper.gitHubTokenCrationURL}"`));
setupBox.add_child(createTokenButton);

this.contentContainer.add_child(setupBox);
} else if (error) {
// Error UI
const errorBox = new St.BoxLayout({ vertical: true, style_class: "github-contribution-grid-error-container" });
errorBox.add_child(new St.Label({ text: "GitHub Contribution Grid", style_class: "github-contribution-grid-error-headline" }));
errorBox.add_child(new St.Label({ text: _("Error:") }));
errorBox.add_child(new St.Label({ text: error, style_class: "github-contribution-grid-error-message" }));
const reloadButton = new St.Button({ style_class: "github-contribution-grid-error-reload-button", label: _("Reload") });
reloadButton.connect("clicked", () => this._setupContributionData());
errorBox.add_child(reloadButton);
this.contentContainer.add_child(errorBox);
} else if (weeks) {
// Render GitHub Grid
const gridBox = new St.BoxLayout({ style_class: "github-contribution-grid-grid-box" });

for (const week of weeks) {
const weekBox = new St.BoxLayout({ vertical: true, style_class: "week-container" });

for (const day of week.contributionDays) {
const dayBin = new St.Bin({
style_class: "day-bin",
reactive: true,
track_hover: true,
style: `font-size: ${this.blockSize}px; background-color: ${GitHubHelper.getContributionColor(day.contributionCount)};`,
});

new Tooltips.Tooltip(dayBin, `${day.date} ${day.contributionCount} ` + _("contributions"));

if (this.showContributionCount) {
const countLabel = new St.Label({ text: day.contributionCount.toString() });
dayBin.set_child(countLabel);
}
weekBox.add_child(dayBin);
}
gridBox.add_child(weekBox);
}
this.contentContainer.add_child(gridBox);
}

this.mainContainer.add_child(this.contentContainer);
}

_updateLoop() {
this._setupContributionData().finally(() => {
this.timeoutId = Mainloop.timeout_add_seconds(this.refreshInterval * 60, () => {
this._updateLoop();
});
});
}
}

function main(metadata, deskletId) {
return new MyDesklet(metadata, deskletId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const Soup = imports.gi.Soup;
const ByteArray = imports.byteArray;
const GLib = imports.gi.GLib;

const _httpSession = new Soup.Session();

class GitHubHelper {
static gitHubTokenCrationURL = "https://github.com/settings/tokens/new?description=Cinnamon%20Desklet";

static async getContributionData(username, token) {
const query = `
query {
user(login: "${username}") {
contributionsCollection {
contributionCalendar {
weeks {
contributionDays {
date
contributionCount
}
}
}
}
}
}
`;

const response = await GitHubHelper._makeGitHubGraphQLRequest(query, token);

if (response && response.data && response.data.user && response.data.user.contributionsCollection) {
return response.data.user.contributionsCollection.contributionCalendar.weeks;
}
throw new Error("Could not retrieve contribution data.");
}

static _makeGitHubGraphQLRequest(query, token) {
return new Promise((resolve, reject) => {
const url = "https://api.github.com/graphql";
const body = JSON.stringify({ query });

const message = Soup.Message.new("POST", url);
if (!message) {
return reject(new Error("Failed to create new Soup.Message"));
}

message.request_headers.append("Authorization", `bearer ${token}`);
message.request_headers.append("User-Agent", "cinnamon-github-contribution-grid-desklet");
message.request_headers.set_content_type("application/json", null);
message.set_request_body_from_bytes("application/json", new GLib.Bytes(body));

_httpSession.send_and_read_async(message, Soup.MessagePriority.NORMAL, null, (session, result) => {
if (message.get_status() === 200) {
const bytes = _httpSession.send_and_read_finish(result);
const responseJson = JSON.parse(ByteArray.toString(bytes.get_data()));
responseJson.errors ? reject(new Error(responseJson.errors.map(e => e.message).join(", "))) : resolve(responseJson);
} else {
reject(new Error(`Failed to fetch data: ${message.get_status()} ${message.get_reason_phrase()}`));
}
});
});
}

static getContributionColor(count) {
if (count >= 10) return "#56d364";
if (count >= 9) return "#2ea043";
if (count >= 6) return "#196c2e";
if (count >= 4) return "#196c2e";
if (count > 0) return "#033a16";
if (count === 0) return "#151b23";
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"uuid": "github-contribution-grid@KopfdesDaemons",
"name": "GitHub Contribution Grid",
"description": "Displays the daily contributions heatmap for a specified GitHub user profile.",
"version": "1.0",
"max-instances": "50"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# GITHUB CONTRIBUTION GRID
# This file is put in the public domain.
# KopfdesDaemons, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: github-contribution-grid@KopfdesDaemons 1.0\n"
"Report-Msgid-Bugs-To: https://github.com/linuxmint/cinnamon-spices-desklets/"
"issues\n"
"POT-Creation-Date: 2025-10-13 22:42+0200\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4.2\n"

#. metadata.json->name
#. desklet.js:36
msgid "GitHub Contribution Grid"
msgstr "GitHub Contribution Grid"

#. desklet.js:85 desklet.js:141
msgid "Please configure username and token in settings."
msgstr "Bitte konfiguriere Benutzername und Token in den Einstellungen."

#. desklet.js:110 desklet.js:154
msgid "Reload"
msgstr "Neuladen"

#. desklet.js:118
msgid "Open GitHub profile"
msgstr "GitHub Profil öffnen"

#. desklet.js:143
msgid "Create a GitHub token"
msgstr "Einen GitHub Token erstellen"

#. desklet.js:152
msgid "Error:"
msgstr "Error:"

#. desklet.js:173
msgid "contributions"
msgstr "Beiträge"

#. metadata.json->description
msgid ""
"Displays the daily contributions heatmap for a specified GitHub user profile."
msgstr ""
"Zeigt die Heatmap der täglichen Beiträge für ein angegebenes GitHub-"
"Benutzerprofil an."

#. settings-schema.json->head0->description
msgid "API Settings"
msgstr "API-Einstellungen"

#. settings-schema.json->github-username->description
msgid "GitHub Username"
msgstr "GitHub Benutzername"

#. settings-schema.json->github-token->description
msgid "GitHub Token"
msgstr "GitHub Token"

#. settings-schema.json->refresh-interval->description
msgid "Refresh interval in minutes"
msgstr "Aktualisierungsintervall in Minuten"

#. settings-schema.json->head1->description
msgid "Style"
msgstr "Stil"

#. settings-schema.json->block-size->description
msgid "Block size in pixels"
msgstr "Blockgröße in Pixel"

#. settings-schema.json->background-color->description
msgid "Background color"
msgstr "Hintergrundfarbe"

#. settings-schema.json->show-contribution-count->description
msgid "Show contribution count"
msgstr "Beitragsanzahl anzeigen"

#. settings-schema.json->show-username->description
msgid "Show username"
msgstr "Benutzername anzeigen"
Loading