├── screenshot.png ├── icons ├── PNG │ ├── dark.png │ ├── light.png │ └── neutral.png ├── sq │ ├── dark-bg-sq.jpg │ └── light-bg-sq.jpg ├── cir │ ├── dark-bg-cir.png │ └── light-bg-cir.png └── squircle │ ├── dark-bg-squircle.png │ └── light-bg-squircle.png ├── weekly-commits-logo.png ├── schemas ├── gschemas.compiled └── org.gnome.shell.extensions.weekly-commits.gschema.xml ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── question.yml │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── metadata.json ├── LICENSE ├── helpers ├── settings.js ├── githubService.js └── about.js ├── README.md ├── prefs.js └── extension.js /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/screenshot.png -------------------------------------------------------------------------------- /icons/PNG/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/PNG/dark.png -------------------------------------------------------------------------------- /icons/PNG/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/PNG/light.png -------------------------------------------------------------------------------- /icons/PNG/neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/PNG/neutral.png -------------------------------------------------------------------------------- /icons/sq/dark-bg-sq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/sq/dark-bg-sq.jpg -------------------------------------------------------------------------------- /icons/sq/light-bg-sq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/sq/light-bg-sq.jpg -------------------------------------------------------------------------------- /weekly-commits-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/weekly-commits-logo.png -------------------------------------------------------------------------------- /icons/cir/dark-bg-cir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/cir/dark-bg-cir.png -------------------------------------------------------------------------------- /icons/cir/light-bg-cir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/cir/light-bg-cir.png -------------------------------------------------------------------------------- /schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/schemas/gschemas.compiled -------------------------------------------------------------------------------- /icons/squircle/dark-bg-squircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/squircle/dark-bg-squircle.png -------------------------------------------------------------------------------- /icons/squircle/light-bg-squircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funinkina/weekly-commits/HEAD/icons/squircle/light-bg-squircle.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [funinkina, Aryan-Techie] 4 | custom: ["https://www.buymeacoffee.com/funinkina"] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Ask for help 3 | title: "[Question] " 4 | labels: ["question"] 5 | body: 6 | - type: textarea 7 | id: question 8 | attributes: 9 | label: Question 10 | description: What do you need help with? 11 | placeholder: Ask your question here 12 | validations: 13 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 GitHub Discussions 4 | url: https://github.com/funinkina/weekly-commits/discussions 5 | about: Ask questions and discuss with the community 6 | - name: 🌐 GNOME Extensions Page 7 | url: https://extensions.gnome.org/extension/8146/weekly-commits/ 8 | about: Leave a review or see installation instructions 9 | - name: 📖 Developer's Website 10 | url: https://funinkina.is-a.dev/ 11 | about: Learn more about the developer and other projects -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature 3 | title: "[Feature] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: feature 8 | attributes: 9 | label: Feature Description 10 | description: What feature would you like? 11 | placeholder: Describe the feature you want 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: why 17 | attributes: 18 | label: Why is this needed? 19 | placeholder: Explain the use case or problem this would solve 20 | validations: 21 | required: true -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Weekly Commits", 3 | "version": "7", 4 | "description": "See your weekly GitHub commits in top bar", 5 | "uuid": "weekly-commits@funinkina.is-a.dev", 6 | "author": "Aryan Kushwaha ", 7 | "license": "MIT", 8 | "shell-version": [ 9 | "46", 10 | "47", 11 | "48", 12 | "49" 13 | ], 14 | "url": "https://github.com/funinkina/weekly-commits", 15 | "settings-schema": "org.gnome.shell.extensions.weekly-commits", 16 | "donation": { 17 | "buymeacoffee": "https://www.buymeacoffee.com/funinkina", 18 | "github": "https://github.com/sponsors/funinkina" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug 3 | title: "[Bug] " 4 | labels: ["bug"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Bug Description 10 | description: What's wrong? 11 | placeholder: Describe the issue 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: steps 17 | attributes: 18 | label: Steps to Reproduce 19 | placeholder: | 20 | 1. Go to... 21 | 2. Click... 22 | 3. See error 23 | validations: 24 | required: true 25 | 26 | - type: input 27 | id: system 28 | attributes: 29 | label: System Info 30 | placeholder: "GNOME Shell version, Extension version, OS" 31 | validations: 32 | required: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Aryan Kushwaha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /helpers/settings.js: -------------------------------------------------------------------------------- 1 | export class ExtensionSettings { 2 | constructor(extension) { 3 | this._extension = extension; 4 | this._settings = extension.getSettings(); 5 | } 6 | 7 | get githubUsername() { 8 | return this._settings.get_string('github-username') || ''; 9 | } 10 | 11 | set githubUsername(value) { 12 | this._settings.set_string('github-username', value || ''); 13 | } 14 | 15 | get githubToken() { 16 | return this._settings.get_string('github-token') || ''; 17 | } 18 | 19 | set githubToken(value) { 20 | this._settings.set_string('github-token', value || ''); 21 | } 22 | 23 | get refreshInterval() { 24 | return this._settings.get_int('refresh-interval'); 25 | } 26 | 27 | set refreshInterval(value) { 28 | this._settings.set_int('refresh-interval', value); 29 | } 30 | 31 | get panelPosition() { 32 | return this._settings.get_enum('panel-position'); 33 | } 34 | 35 | set panelPosition(value) { 36 | this._settings.set_enum('panel-position', value); 37 | } 38 | 39 | get panelIndex() { 40 | return this._settings.get_int('panel-index'); 41 | } 42 | 43 | set panelIndex(value) { 44 | this._settings.set_int('panel-index', value); 45 | } 46 | 47 | get highlightCurrentDay() { 48 | return this._settings.get_boolean('highlight-current-day'); 49 | } 50 | 51 | set highlightCurrentDay(value) { 52 | this._settings.set_boolean('highlight-current-day', value); 53 | } 54 | 55 | get showCurrentWeekOnly() { 56 | return this._settings.get_boolean('show-current-week-only'); 57 | } 58 | 59 | set showCurrentWeekOnly(value) { 60 | this._settings.set_boolean('show-current-week-only', value); 61 | } 62 | 63 | get weekStartDay() { 64 | return this._settings.get_enum('week-start-day'); 65 | } 66 | 67 | set weekStartDay(value) { 68 | this._settings.set_enum('week-start-day', value); 69 | } 70 | 71 | get themeName() { 72 | return this._settings.get_enum('theme-name'); 73 | } 74 | 75 | set themeName(value) { 76 | this._settings.set_enum('theme-name', value); 77 | } 78 | 79 | get colorMode() { 80 | return this._settings.get_enum('color-mode'); 81 | } 82 | 83 | set colorMode(value) { 84 | this._settings.set_enum('color-mode', value); 85 | } 86 | 87 | connectChanged(callback) { 88 | return this._settings.connect('changed', callback); 89 | } 90 | 91 | disconnectChanged(handlerId) { 92 | this._settings.disconnect(handlerId); 93 | } 94 | } -------------------------------------------------------------------------------- /schemas/org.gnome.shell.extensions.weekly-commits.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | '' 44 | GitHub Username 45 | Your GitHub username to fetch contribution data. 46 | 47 | 48 | '' 49 | GitHub Personal Access Token 50 | Your GitHub Personal Access Token for API authentication. 51 | 52 | 53 | 21600 54 | Refresh Interval 55 | Time in seconds between refreshing GitHub contribution data. 56 | 57 | 58 | 'right' 59 | Panel Position 60 | The position of the extension indicator in the panel. 61 | 62 | 63 | 0 64 | Position Index 65 | The index position within the chosen panel section. 66 | 67 | 68 | false 69 | Show Current Week's Commits Only 70 | Whether to show only commits from the current week instead of the last 7 days. 71 | 72 | 73 | 'monday' 74 | Week Start Day 75 | The day of the week on which the week starts. 76 | 77 | 78 | false 79 | Highlight Current Day 80 | Adds a white border around the current day's box for easier identification. 81 | 82 | 83 | 'standard' 84 | Color Theme 85 | The color theme to use for displaying commit activity. 86 | 87 | 88 | 'opacity' 89 | Color Mode 90 | Whether to use opacity-based or grade-based coloring for commit activity. 91 | 92 | 93 | -------------------------------------------------------------------------------- /helpers/githubService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import GLib from 'gi://GLib'; 3 | import Soup from 'gi://Soup'; 4 | 5 | /** 6 | * Get the dates for display based on settings 7 | * @param {boolean} asISOString - Whether to return dates as ISO strings or Date objects 8 | * @param {boolean} showCurrentWeekOnly - Whether to show only the current week 9 | * @param {number} weekStartDay - Day the week starts on (0 = Sunday, 1 = Monday, etc.) 10 | * @returns {(string[]|Date[])} Array of dates in the requested format 11 | */ 12 | export function getDates(asISOString = true, showCurrentWeekOnly = false, weekStartDay = 1) { 13 | const dates = []; 14 | const today = new Date(); 15 | 16 | if (showCurrentWeekOnly) { 17 | const currentDay = today.getDay(); 18 | let daysToSubtract = currentDay - weekStartDay; 19 | if (daysToSubtract < 0) daysToSubtract += 7; 20 | 21 | const weekStart = new Date(today); 22 | weekStart.setDate(today.getDate() - daysToSubtract); 23 | 24 | for (let i = 0; i < 7; i++) { 25 | const d = new Date(weekStart); 26 | d.setDate(weekStart.getDate() + i); 27 | 28 | if (asISOString) { 29 | const year = d.getFullYear(); 30 | const month = String(d.getMonth() + 1).padStart(2, '0'); 31 | const day = String(d.getDate()).padStart(2, '0'); 32 | dates.push(`${year}-${month}-${day}`); 33 | } else { 34 | dates.push(d); 35 | } 36 | } 37 | } else { 38 | for (let i = 6; i >= 0; i--) { 39 | const d = new Date(today); 40 | d.setDate(d.getDate() - i); 41 | 42 | if (asISOString) { 43 | const year = d.getFullYear(); 44 | const month = String(d.getMonth() + 1).padStart(2, '0'); 45 | const day = String(d.getDate()).padStart(2, '0'); 46 | dates.push(`${year}-${month}-${day}`); 47 | } else { 48 | dates.push(d); 49 | } 50 | } 51 | } 52 | 53 | return dates; 54 | } 55 | 56 | /** 57 | * @param {string} username 58 | * @param {string} token 59 | * @param {boolean} showCurrentWeekOnly 60 | * @param {number} weekStartDay 61 | * @returns {Promise} 62 | */ 63 | export async function fetchContributions(username, token, showCurrentWeekOnly = false, weekStartDay = 1) { 64 | if (!token || token === 'YOUR_GITHUB_PERSONAL_ACCESS_TOKEN' || !username || username === 'YOUR_GITHUB_USERNAME') { 65 | console.error('Weekly Commits Extension: GitHub token or username is not configured.'); 66 | return Array(7).fill(0); 67 | } 68 | 69 | const targetDates = getDates(true, showCurrentWeekOnly, weekStartDay); 70 | 71 | const queryFromDate = new Date(new Date().setDate(new Date().getDate() - 10)).toISOString(); 72 | const queryToDate = new Date(new Date().setDate(new Date().getDate() + 3)).toISOString(); 73 | 74 | const query = ` 75 | query { 76 | user(login: "${username}") { 77 | contributionsCollection(from: "${queryFromDate}", to: "${queryToDate}") { 78 | contributionCalendar { 79 | weeks { 80 | contributionDays { 81 | date 82 | contributionCount 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }`; 89 | 90 | const session = new Soup.Session(); 91 | const message = Soup.Message.new('POST', 'https://api.github.com/graphql'); 92 | if (!message) { 93 | throw new Error('Failed to create Soup.Message'); 94 | } 95 | message.request_headers.append('Authorization', `bearer ${token}`); 96 | message.request_headers.append('Content-Type', 'application/json'); 97 | message.request_headers.append('User-Agent', 'GNOME Shell Extension Weekly Commits'); 98 | 99 | const queryBytes = new GLib.Bytes(new TextEncoder().encode(JSON.stringify({ query }))); 100 | message.set_request_body_from_bytes('application/json', queryBytes); 101 | 102 | let responseBytes; 103 | try { 104 | responseBytes = await session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null); 105 | } catch (e) { 106 | console.error(`Weekly Commits Extension: Network error - ${e.message}`); 107 | throw e; 108 | } 109 | 110 | const responseStr = new TextDecoder().decode(responseBytes.get_data()); 111 | const result = JSON.parse(responseStr); 112 | 113 | if (result.errors) { 114 | console.error('Weekly Commits Extension: GitHub API Error:', result.errors); 115 | throw new Error(result.errors.map(e => e.message).join(', ')); 116 | } 117 | 118 | if (!result.data || !result.data.user || !result.data.user.contributionsCollection) { 119 | console.error('Weekly Commits Extension: Unexpected API response structure:', result); 120 | throw new Error('Unexpected API response structure.'); 121 | } 122 | 123 | const allContributionDays = result.data.user.contributionsCollection.contributionCalendar.weeks 124 | .flatMap(week => week.contributionDays); 125 | 126 | const contributionMap = new Map(); 127 | allContributionDays.forEach(day => contributionMap.set(day.date, day.contributionCount)); 128 | 129 | return targetDates.map(date => contributionMap.get(date) || 0); 130 | } -------------------------------------------------------------------------------- /helpers/about.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import Adw from 'gi://Adw'; 3 | import Gio from 'gi://Gio'; 4 | import GObject from 'gi://GObject'; 5 | import Gtk from 'gi://Gtk'; 6 | import GLib from 'gi://GLib'; 7 | import GdkPixbuf from 'gi://GdkPixbuf'; 8 | import { gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; 9 | 10 | export default class About extends Adw.PreferencesPage { 11 | static { 12 | GObject.registerClass(this); 13 | } 14 | 15 | constructor(extensionObject) { 16 | super({ 17 | title: _('About'), 18 | icon_name: 'help-about-symbolic', 19 | name: 'about' 20 | }); 21 | 22 | const extensionDir = extensionObject.path; 23 | const iconFile = GLib.build_filenamev([extensionDir, 'weekly-commits-logo.png']); 24 | const extensionName = extensionObject.metadata.name; 25 | const extensionVersion = extensionObject.metadata.version || '1.0'; 26 | const extensionDescription = extensionObject.metadata.description; 27 | const supportedShellVersions = extensionObject.metadata['shell-version']; 28 | const githubLink = 'https://github.com/funinkina/weekly-commits'; 29 | const issueFeatureLink = 'https://github.com/funinkina/weekly-commits/issues'; 30 | const authorBlogsLink = 'https://funinkina.is-a.dev/'; 31 | const gnomeExtensionsLink = 'https://extensions.gnome.org/extension/8146/weekly-commits/'; 32 | 33 | const headerGroup = new Adw.PreferencesGroup(); 34 | this.add(headerGroup); 35 | 36 | const headerBox = new Gtk.Box({ 37 | orientation: Gtk.Orientation.VERTICAL, 38 | spacing: 10, 39 | margin_top: 24, 40 | margin_bottom: 24, 41 | hexpand: true, 42 | halign: Gtk.Align.CENTER 43 | }); 44 | 45 | const iconImage = new Gtk.Image({ 46 | pixel_size: 128 47 | }); 48 | 49 | try { 50 | if (Gio.File.new_for_path(iconFile).query_exists(null)) { 51 | const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(iconFile, 128, 128); 52 | iconImage.set_from_pixbuf(pixbuf); 53 | } else { 54 | iconImage.set_from_icon_name('application-x-addon-symbolic'); 55 | } 56 | } catch (e) { 57 | console.error(`Error loading icon: ${e.message}`); 58 | iconImage.set_from_icon_name('application-x-addon-symbolic'); 59 | } 60 | 61 | const nameLabel = new Gtk.Label({ 62 | label: `${extensionName}`, 63 | use_markup: true 64 | }); 65 | 66 | const descriptionLabel = new Gtk.Label({ 67 | label: `${extensionDescription}`, 68 | use_markup: true, 69 | wrap: true, 70 | max_width_chars: 50, 71 | justify: Gtk.Justification.CENTER 72 | }); 73 | 74 | const versionLabel = new Gtk.Label({ 75 | label: `Version ${extensionVersion}`, 76 | use_markup: true 77 | }); 78 | 79 | headerBox.append(iconImage); 80 | headerBox.append(nameLabel); 81 | headerBox.append(descriptionLabel); 82 | headerBox.append(versionLabel); 83 | headerGroup.add(headerBox); 84 | 85 | // Quick overview 86 | const overviewGroup = new Adw.PreferencesGroup({ 87 | title: _('What This Extension Does') 88 | }); 89 | this.add(overviewGroup); 90 | 91 | const overviewRow = new Adw.ActionRow({ 92 | title: _('GitHub Commit Visualization'), 93 | subtitle: _('Shows your weekly GitHub activity in the top bar with 14 beautiful themes, smart coloring, auto-updates, and customizable positioning.') 94 | }); 95 | 96 | const overviewIcon = new Gtk.Image({ 97 | icon_name: 'view-grid-symbolic' 98 | }); 99 | overviewRow.add_prefix(overviewIcon); 100 | overviewGroup.add(overviewRow); 101 | 102 | 103 | 104 | const authorGroup = new Adw.PreferencesGroup({ 105 | title: _('The Developer') 106 | }); 107 | this.add(authorGroup); 108 | 109 | const moreInfo = new Adw.ActionRow({ 110 | title: _("Visit Developer's Website"), 111 | subtitle: _('Explore funinkina\'s projects, blog, work and more'), 112 | activatable: true 113 | }); 114 | 115 | const blogIcon = new Gtk.Image({ 116 | icon_name: 'user-info-symbolic' 117 | }); 118 | moreInfo.add_prefix(blogIcon); 119 | authorGroup.add(moreInfo); 120 | this._makeRowClickable(moreInfo, authorBlogsLink); 121 | 122 | // Sponsor button for funinkina (below developer info) 123 | const funinkinaSponsorsLink = 'https://github.com/sponsors/funinkina'; 124 | const funinkinaSponsorsRow = new Adw.ActionRow({ 125 | title: _('Sponsor funinkina'), 126 | subtitle: _('Support the creator on GitHub Sponsors'), 127 | activatable: true 128 | }); 129 | 130 | const funinkinaSponsorsIcon = new Gtk.Image({ 131 | icon_name: 'starred-symbolic', 132 | }); 133 | funinkinaSponsorsRow.add_prefix(funinkinaSponsorsIcon); 134 | authorGroup.add(funinkinaSponsorsRow); 135 | this._makeRowClickable(funinkinaSponsorsRow, funinkinaSponsorsLink); 136 | 137 | const buyMeCoffeeLink = 'https://www.buymeacoffee.com/funinkina'; 138 | const coffeeRow = new Adw.ActionRow({ 139 | title: _('Buy Me a Coffee'), 140 | subtitle: _('Quick one-time support for the project'), 141 | activatable: true 142 | }); 143 | 144 | const coffeeIcon = new Gtk.Image({ 145 | icon_name: 'cafe-symbolic' 146 | }); 147 | coffeeRow.add_prefix(coffeeIcon); 148 | authorGroup.add(coffeeRow); 149 | this._makeRowClickable(coffeeRow, buyMeCoffeeLink); 150 | 151 | const contributorsGroup = new Adw.PreferencesGroup({ 152 | title: _('Contributor') 153 | 154 | }); 155 | this.add(contributorsGroup); 156 | 157 | const contributorRow = new Adw.ActionRow({ 158 | title: _('Aryan-Techie'), 159 | subtitle: _('Developer - github.com/aryan-techie'), 160 | 161 | activatable: true 162 | }); 163 | 164 | const contributorIcon = new Gtk.Image({ 165 | icon_name: 'system-users-symbolic' 166 | }); 167 | contributorRow.add_prefix(contributorIcon); 168 | contributorsGroup.add(contributorRow); 169 | this._makeRowClickable(contributorRow, 'https://aryantechie.com'); 170 | 171 | // Sponsor button for Aryan-Techie (below contributor info) 172 | const aryanSponsorsLink = 'https://github.com/sponsors/Aryan-Techie'; 173 | const aryanSponsorsRow = new Adw.ActionRow({ 174 | title: _('Sponsor Aryan-Techie'), 175 | subtitle: _('Support the contributor on GitHub Sponsors'), 176 | activatable: true 177 | }); 178 | 179 | const aryanSponsorsIcon = new Gtk.Image({ 180 | icon_name: 'starred-symbolic', 181 | }); 182 | aryanSponsorsRow.add_prefix(aryanSponsorsIcon); 183 | contributorsGroup.add(aryanSponsorsRow); 184 | this._makeRowClickable(aryanSponsorsRow, aryanSponsorsLink); 185 | 186 | // Links section moved to bottom 187 | const linksGroup = new Adw.PreferencesGroup({ 188 | title: _('Get Involved'), 189 | description: _('Explore, contribute, and get help') 190 | }); 191 | this.add(linksGroup); 192 | 193 | const gnomeExtensionsRow = new Adw.ActionRow({ 194 | title: _('GNOME Extensions'), 195 | subtitle: _('Official extension page - Leave a review!'), 196 | activatable: true 197 | }); 198 | 199 | const gnomeIcon = new Gtk.Image({ 200 | icon_name: 'org.gnome.Extensions-symbolic' 201 | }); 202 | gnomeExtensionsRow.add_prefix(gnomeIcon); 203 | linksGroup.add(gnomeExtensionsRow); 204 | this._makeRowClickable(gnomeExtensionsRow, gnomeExtensionsLink); 205 | 206 | const githubRow = new Adw.ActionRow({ 207 | title: _('Source Code'), 208 | subtitle: _('View on GitHub - Star the repository!'), 209 | activatable: true 210 | }); 211 | 212 | const githubIcon = new Gtk.Image({ 213 | icon_name: 'text-x-generic-symbolic' 214 | }); 215 | githubRow.add_prefix(githubIcon); 216 | linksGroup.add(githubRow); 217 | this._makeRowClickable(githubRow, githubLink); 218 | 219 | const issueRow = new Adw.ActionRow({ 220 | title: _('Report Issue or Request Feature'), 221 | subtitle: _('Help us to make this extension even better'), 222 | activatable: true 223 | }); 224 | 225 | const issueIcon = new Gtk.Image({ 226 | icon_name: 'dialog-question-symbolic' 227 | }); 228 | issueRow.add_prefix(issueIcon); 229 | linksGroup.add(issueRow); 230 | this._makeRowClickable(issueRow, issueFeatureLink); 231 | } 232 | 233 | _makeRowClickable(row, link) { 234 | row.set_tooltip_text(link); 235 | row.connect('activated', () => { 236 | try { 237 | Gio.AppInfo.launch_default_for_uri_async(link, null, null, (result) => { 238 | try { 239 | Gio.AppInfo.launch_default_for_uri_finish(result); 240 | } catch (e) { 241 | console.error(`Error opening link ${link}: ${e.message}`); 242 | } 243 | }); 244 | } catch (e) { 245 | console.error(`Error launching URI ${link}: ${e.message}`); 246 | } 247 | }); 248 | } 249 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Weekly Commits Logo 3 |

Weekly Commits

4 |

A GNOME Shell extension to visualize your GitHub contributions in the top bar

5 | 6 | Weekly Commits Extension Screenshot 7 | 8 |

9 | 10 | Get it on GNOME Extensions 11 | 12 |

13 | 14 |

15 | License 16 | GitHub stars 17 | GitHub issues 18 | Last commit 19 | GNOME Shell Version 20 |

21 |
22 | 23 | ## 🔥 Features 24 | 25 | **Weekly Commits** transforms your GitHub activity into a beautiful visual representation directly in your GNOME Shell top bar. Stay motivated and track your coding consistency at a glance! 26 | 27 | ### ✨ Core Features 28 | - 📊 **Visual Contribution Calendar**: Seven colorful boxes representing your weekly commit activity 29 | - 🖱️ **Interactive Popup**: Click to see detailed daily commit counts 30 | - ⚙️ **Easy Configuration**: Simple GUI preferences for GitHub credentials and settings 31 | - 🔄 **Auto-sync**: Configurable intervals to keep your data fresh 32 | - 📍 **Flexible Positioning**: Place the widget anywhere on your top bar 33 | 34 | ### 🎨 Theming & Customization 35 | - **14+ Beautiful Themes**: GitHub, Dracula, Halloween, Panda, Solarized, and more 36 | - **Dual Coloring Modes**: 37 | - *Opacity-based*: Subtle transparency effects 38 | - *Grade-based*: Distinct color intensities 39 | - **Week Start Options**: Choose between Monday or Sunday start 40 | - **Custom Positioning**: Perfect alignment with your workflow 41 | 42 | ## 🚀 Installation 43 | 44 | ### Method 1: GNOME Extensions (Recommended) 45 | 1. Visit the [GNOME Extensions page](https://extensions.gnome.org/extension/8146/weekly-commits/) 46 | 2. Click "Install" and follow the browser prompts 47 | 3. Enable the extension in the GNOME Extensions app 48 | 49 | ### Method 2: Manual Installation 50 | 1. **Clone the repository**: 51 | ```bash 52 | git clone https://github.com/funinkina/weekly-commits.git 53 | ``` 54 | 55 | 2. **Install to extensions directory**: 56 | ```bash 57 | mv weekly-commits ~/.local/share/gnome-shell/extensions/weekly-commits@funinkina.is-a.dev 58 | ``` 59 | 60 | 3. **Restart GNOME Shell**: 61 | - **X11**: Press `Alt` + `F2`, type `r`, and press `Enter` 62 | - **Wayland**: Log out and log back in 63 | 64 | 4. **Enable the extension**: 65 | ```bash 66 | gnome-extensions enable weekly-commits@funinkina.is-a.dev 67 | ``` 68 | 69 | Or use the GNOME Extensions app 70 | 71 | ### System Requirements 72 | - GNOME Shell 46, 47, or 48 73 | - Internet connection for GitHub API access 74 | 75 | ## ⚙️ Configuration 76 | 77 | ### GitHub Authentication Setup 78 | 79 | To start tracking your commits, you'll need to configure your GitHub credentials: 80 | 81 | #### Step 1: Generate a Personal Access Token 82 | 1. Go to [GitHub Personal Access Tokens](https://github.com/settings/personal-access-tokens/new) 83 | 2. Create a **Fine-grained Personal Access Token** with: 84 | - **Repository Access**: "All repositories" or select specific ones 85 | - **Permissions**: Read access to repository metadata and contents 86 | 3. Copy the generated token 87 | 88 | #### Step 2: Configure the Extension 89 | 1. Right-click on the Weekly Commits widget in your top bar 90 | 2. Select "Preferences" or use the GNOME Extensions app 91 | 3. Enter your: 92 | - **GitHub Username**: Your GitHub account username 93 | - **Personal Access Token**: The token you generated in Step 1 94 | 95 | #### Step 3: Customize Settings (Optional) 96 | - **Update Interval**: How often to refresh data (default: 1 hour) 97 | - **Position**: Where to place the widget in the top bar 98 | - **Theme**: Choose from 14+ beautiful color themes 99 | - **Coloring Mode**: Opacity-based or grade-based visualization 100 | - **Week Start**: Monday or Sunday 101 | 102 | ### 🔒 Privacy & Security 103 | - Your token is stored locally and only used to fetch your public contribution data 104 | - No data is transmitted to third parties 105 | - The extension only reads your commit history, never modifies anything 106 | 107 | ## 🎨 Available Themes 108 | 109 | Weekly Commits comes with a variety of beautiful themes to match your desktop: 110 | 111 | | Theme | Description | 112 | |-------|-------------| 113 | | **GitHub** | Classic GitHub contribution graph colors | 114 | | **Dracula** | Popular dark theme with purple accents | 115 | | **Halloween** | Spooky orange and black theme | 116 | | **Panda** | Cute panda-inspired green theme | 117 | | **Solarized Dark/Light** | Popular developer color schemes | 118 | | **Blue, Pink, Teal** | Vibrant single-color themes | 119 | | **Sunny, YlGnBu** | Gradient and scientific visualization themes | 120 | 121 | ## 🛠️ Development & Contributing 122 | 123 | ### Building from Source 124 | ```bash 125 | # Clone the repository 126 | git clone https://github.com/funinkina/weekly-commits.git 127 | cd weekly-commits 128 | 129 | # Install to local extensions directory 130 | make install 131 | 132 | # Enable the extension 133 | make enable 134 | ``` 135 | 136 | ### Contributing 137 | Contributions are welcome! Please: 138 | 1. Fork the repository 139 | 2. Create a feature branch: `git checkout -b feature/amazing-feature` 140 | 3. Commit your changes: `git commit -m 'Add amazing feature'` 141 | 4. Push to the branch: `git push origin feature/amazing-feature` 142 | 5. Open a Pull Request 143 | 144 | ### Roadmap 145 | - [x] ✅ Settings page for GitHub credentials 146 | - [x] ✅ Automatic data fetching with configurable intervals 147 | - [x] ✅ Customizable top bar positioning 148 | - [x] ✅ Interactive daily commit popup 149 | - [x] ✅ Week start day configuration (Monday/Sunday) 150 | - [x] ✅ Multiple color themes and coloring modes 151 | - [ ] 🔄 Customizable commit view thresholds 152 | - [ ] 🔄 Internationalization and translations 153 | - [ ] 🔄 Support for multiple GitHub accounts 154 | - [ ] 🔄 Additional visualization modes 155 | 156 | ## 💖 Support the Project 157 | 158 | If you find Weekly Commits useful, consider supporting its development: 159 | 160 |
161 | 162 | Buy Me A Coffee 163 | 164 | 165 | GitHub Sponsors 166 | 167 |
168 | 169 | ## 🐛 Troubleshooting 170 | 171 | ### Common Issues 172 | 173 | **Extension not showing commits?** 174 | - Verify your GitHub username is correct 175 | - Ensure your Personal Access Token has proper permissions 176 | - Check your internet connection 177 | - Look for error messages in `journalctl -f` while testing 178 | 179 | **Widget not appearing in top bar?** 180 | - Make sure the extension is enabled in GNOME Extensions app 181 | - Try restarting GNOME Shell (`Alt+F2`, type `r`, press Enter on X11) 182 | - Check if the extension is compatible with your GNOME Shell version 183 | 184 | **Need help?** 185 | - [Open an issue](https://github.com/funinkina/weekly-commits/issues) on GitHub 186 | - Check existing issues for solutions 187 | - Provide your GNOME Shell version and error logs 188 | 189 | ## 👥 Credits 190 | 191 | This project exists thanks to the contributions of: 192 | 193 | ### 🚀 Core Development 194 | - **[Aryan Kushwaha (@funinkina)](https://github.com/funinkina)** - *Original Creator & Lead Developer* 195 | - Initial extension concept and implementation 196 | - Core GitHub API integration 197 | - Base UI and functionality 198 | - Project architecture and design decisions 199 | 200 | ### ✨ Feature Development & Enhancements 201 | - **[Aryan Techie (@Aryan-Techie)](https://github.com/aryan-techie)** - *Feature Developer & Contributor* 202 | - Theme system implementation (14+ color themes) 203 | - Settings page enhancements and instant save functionality 204 | - UI/UX improvements and bug fixes 205 | - About section enhancements 206 | - New Icon design and branding improvements 207 | - Comprehensive documentation overhaul 208 | 209 | --- 210 | 211 | *Want to contribute? Check our [Contributing Guidelines](#🛠️-development--contributing) and join us!* 212 | 213 | ## 📄 License 214 | 215 | This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. 216 | 217 | ## 🔗 Links 218 | 219 | - **Extension Page**: [GNOME Extensions](https://extensions.gnome.org/extension/8146/weekly-commits/) 220 | - **Source Code**: [GitHub Repository](https://github.com/funinkina/weekly-commits) 221 | - **Bug Reports**: [Issues](https://github.com/funinkina/weekly-commits/issues) 222 | - **Creator & Developer**: [Aryan Kushwaha](https://github.com/funinkina) 223 | - **Developer**: [Aryan Techie](https://github.com/aryan-techie) 224 | 225 | --- 226 | 227 |
228 |

Made with ❤️ for the GNOME community

229 | 230 | 231 | > **Legal Notice**: This project is not affiliated with or endorsed by GitHub, Inc. or the GNOME Foundation. 232 | > The use of the GitHub logo and name is for informational purposes only and does not imply any 233 | > endorsement or affiliation with GitHub, Inc. All trademarks and copyrights are the property of their respective owners. 234 |
-------------------------------------------------------------------------------- /prefs.js: -------------------------------------------------------------------------------- 1 | import Adw from 'gi://Adw'; 2 | import Gtk from 'gi://Gtk'; 3 | 4 | import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; 5 | import About from './helpers/about.js'; 6 | 7 | export default class WeeklyCommitsPreferences extends ExtensionPreferences { 8 | fillPreferencesWindow(window) { 9 | const settings = this.getSettings('org.gnome.shell.extensions.weekly-commits'); 10 | 11 | const page = new Adw.PreferencesPage(); 12 | page.set_title(_('Settings')); 13 | page.set_icon_name('preferences-system-symbolic'); 14 | 15 | const group = new Adw.PreferencesGroup(); 16 | group.set_title(_('GitHub Credentials')); 17 | group.set_description(_('Enter your GitHub username and personal access token')); 18 | 19 | const usernameRow = new Adw.EntryRow({ 20 | title: _('GitHub Username'), 21 | text: settings.get_string('github-username') || '' 22 | }); 23 | usernameRow.connect('notify::text', () => { 24 | settings.set_string('github-username', usernameRow.text); 25 | }); 26 | group.add(usernameRow); 27 | 28 | const tokenRow = new Adw.PasswordEntryRow({ 29 | title: _('GitHub Personal Access Token'), 30 | text: settings.get_string('github-token') || '' 31 | }); 32 | tokenRow.connect('notify::text', () => { 33 | settings.set_string('github-token', tokenRow.text); 34 | }); 35 | group.add(tokenRow); 36 | 37 | const refreshGroup = new Adw.PreferencesGroup(); 38 | refreshGroup.set_title(_('Auto Update Settings')); 39 | 40 | const intervalRow = new Adw.ComboRow({ 41 | title: _('Refresh Interval'), 42 | subtitle: _('How often to check for new contributions?') 43 | }); 44 | 45 | const intervals = [ 46 | { value: 900, label: _('15 minutes') }, 47 | { value: 1800, label: _('30 minutes') }, 48 | { value: 3600, label: _('1 hour') }, 49 | { value: 7200, label: _('2 hours') }, 50 | { value: 14400, label: _('4 hours') }, 51 | { value: 21600, label: _('6 hours') }, 52 | { value: 43200, label: _('12 hours') }, 53 | { value: 86400, label: _('24 hours') } 54 | ]; 55 | 56 | const intervalModel = new Gtk.StringList(); 57 | intervals.forEach(interval => intervalModel.append(interval.label)); 58 | intervalRow.model = intervalModel; 59 | 60 | const currentInterval = settings.get_int('refresh-interval'); 61 | let activeIndex = intervals.findIndex(interval => interval.value === currentInterval); 62 | if (activeIndex === -1) activeIndex = 5; 63 | intervalRow.selected = activeIndex; 64 | 65 | intervalRow.connect('notify::selected', () => { 66 | settings.set_int('refresh-interval', intervals[intervalRow.selected].value); 67 | }); 68 | 69 | refreshGroup.add(intervalRow); 70 | 71 | const displayGroup = new Adw.PreferencesGroup(); 72 | displayGroup.set_title(_('Display Settings')); 73 | displayGroup.set_description(_('Configure how commit data is displayed')); 74 | 75 | const highlightCurrentDayRow = new Adw.SwitchRow({ 76 | title: _('Highlight current day'), 77 | subtitle: _('Add a white border around the current day\'s box') 78 | }); 79 | highlightCurrentDayRow.set_active(settings.get_boolean('highlight-current-day')); 80 | highlightCurrentDayRow.connect('notify::active', () => { 81 | settings.set_boolean('highlight-current-day', highlightCurrentDayRow.get_active()); 82 | }); 83 | displayGroup.add(highlightCurrentDayRow); 84 | 85 | const showWeekOnlyRow = new Adw.SwitchRow({ 86 | title: _('Show current week\'s commits only'), 87 | subtitle: _('Display commits for the current week instead of the last 7 days') 88 | }); 89 | showWeekOnlyRow.set_active(settings.get_boolean('show-current-week-only')); 90 | showWeekOnlyRow.connect('notify::active', () => { 91 | settings.set_boolean('show-current-week-only', showWeekOnlyRow.get_active()); 92 | }); 93 | displayGroup.add(showWeekOnlyRow); 94 | 95 | const weekStartRow = new Adw.ComboRow({ 96 | title: _('Week starts on'), 97 | subtitle: _('Select which day the week begins') 98 | }); 99 | 100 | const weekDays = [ 101 | _('Sunday'), 102 | _('Monday'), 103 | _('Tuesday'), 104 | _('Wednesday'), 105 | _('Thursday'), 106 | _('Friday'), 107 | _('Saturday') 108 | ]; 109 | 110 | const weekDayModel = new Gtk.StringList(); 111 | weekDays.forEach(day => weekDayModel.append(day)); 112 | weekStartRow.model = weekDayModel; 113 | weekStartRow.selected = settings.get_enum('week-start-day'); 114 | 115 | weekStartRow.connect('notify::selected', () => { 116 | settings.set_enum('week-start-day', weekStartRow.selected); 117 | }); 118 | 119 | weekStartRow.set_sensitive(showWeekOnlyRow.get_active()); 120 | showWeekOnlyRow.connect('notify::active', () => { 121 | weekStartRow.set_sensitive(showWeekOnlyRow.get_active()); 122 | }); 123 | 124 | displayGroup.add(weekStartRow); 125 | 126 | const colorModeRow = new Adw.ComboRow({ 127 | title: _('Color Mode'), 128 | subtitle: _('Choose between opacity-based or grade-based coloring') 129 | }); 130 | 131 | const colorModes = [ 132 | _('Opacity Mode'), 133 | _('Grade Mode') 134 | ]; 135 | 136 | const colorModeModel = new Gtk.StringList(); 137 | colorModes.forEach(mode => colorModeModel.append(mode)); 138 | colorModeRow.model = colorModeModel; 139 | colorModeRow.selected = settings.get_enum('color-mode'); 140 | 141 | colorModeRow.connect('notify::selected', () => { 142 | settings.set_enum('color-mode', colorModeRow.selected); 143 | }); 144 | 145 | displayGroup.add(colorModeRow); 146 | 147 | const themeRow = new Adw.ComboRow({ 148 | title: _('Color Theme'), 149 | subtitle: _('Select a color theme for commit visualization') 150 | }); 151 | 152 | const themes = [ 153 | { key: 'standard', label: _('GitHub') }, 154 | { key: 'classic', label: _('GitHub Classic') }, 155 | { key: 'githubDark', label: _('GitHub Dark') }, 156 | { key: 'halloween', label: _('Halloween') }, 157 | { key: 'teal', label: _('Teal') }, 158 | { key: 'leftPad', label: _('@left_pad') }, 159 | { key: 'dracula', label: _('Dracula') }, 160 | { key: 'blue', label: _('Blue') }, 161 | { key: 'panda', label: _('Panda 🐼') }, 162 | { key: 'sunny', label: _('Sunny') }, 163 | { key: 'pink', label: _('Pink') }, 164 | { key: 'YlGnBu', label: _('YlGnBu') }, 165 | { key: 'solarizedDark', label: _('Solarized Dark') }, 166 | { key: 'solarizedLight', label: _('Solarized Light') } 167 | ]; 168 | 169 | const themeModel = new Gtk.StringList(); 170 | themes.forEach(theme => themeModel.append(theme.label)); 171 | themeRow.model = themeModel; 172 | themeRow.selected = settings.get_enum('theme-name'); 173 | 174 | themeRow.connect('notify::selected', () => { 175 | settings.set_enum('theme-name', themeRow.selected); 176 | }); 177 | 178 | displayGroup.add(themeRow); 179 | 180 | const positionGroup = new Adw.PreferencesGroup(); 181 | positionGroup.set_title(_('Panel Position')); 182 | positionGroup.set_description(_('Customize the position of the extension in the panel')); 183 | 184 | const positionRow = new Adw.ComboRow({ 185 | title: _('Location'), 186 | subtitle: _('Which section of the panel to use') 187 | }); 188 | 189 | const positions = [ 190 | { value: 0, label: _('Left') }, 191 | { value: 1, label: _('Center') }, 192 | { value: 2, label: _('Right') } 193 | ]; 194 | 195 | const positionModel = new Gtk.StringList(); 196 | positions.forEach(position => positionModel.append(position.label)); 197 | positionRow.model = positionModel; 198 | 199 | const currentPosition = settings.get_enum('panel-position'); 200 | positionRow.selected = currentPosition; 201 | 202 | positionRow.connect('notify::selected', () => { 203 | settings.set_enum('panel-position', positionRow.selected); 204 | }); 205 | 206 | positionGroup.add(positionRow); 207 | 208 | const indexRow = new Adw.SpinRow({ 209 | title: _('Index'), 210 | subtitle: _('Position within the chosen section (0 is leftmost)'), 211 | adjustment: new Gtk.Adjustment({ 212 | lower: 0, 213 | upper: 20, 214 | step_increment: 1, 215 | page_increment: 5, 216 | value: settings.get_int('panel-index') 217 | }) 218 | }); 219 | 220 | indexRow.connect('notify::value', () => { 221 | settings.set_int('panel-index', indexRow.value); 222 | }); 223 | 224 | positionGroup.add(indexRow); 225 | 226 | page.add(group); 227 | page.add(refreshGroup); 228 | page.add(displayGroup); 229 | page.add(positionGroup); 230 | 231 | const spacerGroup = new Adw.PreferencesGroup(); 232 | spacerGroup.set_vexpand(true); 233 | page.add(spacerGroup); 234 | 235 | const infoGroup = new Adw.PreferencesGroup(); 236 | infoGroup.set_vexpand(false); 237 | infoGroup.set_valign(Gtk.Align.END); 238 | 239 | const infoRow = new Adw.ActionRow({ 240 | title: _('About Personal Access Tokens'), 241 | subtitle: _('Generate a fine grained personal access token with "All Repositories" access.') 242 | }); 243 | 244 | const linkButton = new Gtk.LinkButton({ 245 | label: _('Open GitHub Token Settings'), 246 | uri: 'https://github.com/settings/personal-access-tokens/new' 247 | }); 248 | infoRow.add_suffix(linkButton); 249 | infoGroup.add(infoRow); 250 | 251 | page.add(infoGroup); 252 | 253 | window.add(page); 254 | window.add(new About(this)); 255 | window.set_title(_('Weekly Commits Settings')); 256 | window.set_default_size(650, 750); 257 | } 258 | } -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | import GObject from 'gi://GObject'; 2 | import St from 'gi://St'; 3 | import GLib from 'gi://GLib'; 4 | import Clutter from 'gi://Clutter'; 5 | 6 | import { Extension, gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'; 7 | import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; 8 | import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; 9 | import * as Main from 'resource:///org/gnome/shell/ui/main.js'; 10 | 11 | import { fetchContributions, getDates } from './helpers/githubService.js'; 12 | import { ExtensionSettings } from './helpers/settings.js'; 13 | 14 | // Visual constants for the commit boxes in the top bar 15 | const BOX_SIZE = 14; // Size of each commit box in pixels 16 | const BOX_MARGIN = 4; // Space between each box 17 | const BORDER_RADIUS = 3; // Rounded corners for the boxes 18 | const COLORS = { 19 | ACTIVE: '#4CAF50', // Green color for days with commits 20 | INACTIVE: '#8e8e8e', // Gray color for days without commits 21 | DEFAULT: '#888888' // Default fallback color 22 | }; 23 | 24 | // All available color themes - these match what users see in settings 25 | const THEME_NAMES = { 26 | standard: "GitHub", // Classic GitHub green theme 27 | classic: "GitHub Classic", // Original GitHub contribution colors 28 | githubDark: "GitHub Dark", // GitHub's dark mode colors 29 | halloween: "Halloween", // Orange and dark spooky colors 30 | teal: "Teal", // Calming teal/aqua colors 31 | leftPad: "@left_pad", // Grayscale theme inspired by the infamous npm package 32 | dracula: "Dracula", // Popular dark theme with purple/pink accents 33 | blue: "Blue", // Cool blue gradient theme 34 | panda: "Panda 🐼", // Black and white with colorful accents 35 | sunny: "Sunny", // Bright yellow/gold theme 36 | pink: "Pink", // Pink/magenta gradient theme 37 | YlGnBu: "YlGnBu", // Yellow-Green-Blue scientific colormap 38 | solarizedDark: 'Solarized Dark', // Popular developer theme (dark) 39 | solarizedLight: 'Solarized Light' // Popular developer theme (light) 40 | }; 41 | 42 | const THEMES = { 43 | standard: { 44 | text: "#000000", 45 | meta: "#666666", 46 | grade4: "#216e39", 47 | grade3: "#30a14e", 48 | grade2: "#40c463", 49 | grade1: "#9be9a8", 50 | grade0: "#ebedf0" 51 | }, 52 | classic: { 53 | text: "#000000", 54 | meta: "#666666", 55 | grade4: "#196127", 56 | grade3: "#239a3b", 57 | grade2: "#7bc96f", 58 | grade1: "#c6e48b", 59 | grade0: "#ebedf0" 60 | }, 61 | githubDark: { 62 | text: "#ffffff", 63 | meta: "#dddddd", 64 | grade4: "#27d545", 65 | grade3: "#10983d", 66 | grade2: "#00602d", 67 | grade1: "#003820", 68 | grade0: "#161b22" 69 | }, 70 | halloween: { 71 | text: "#000000", 72 | meta: "#666666", 73 | grade4: "#03001C", 74 | grade3: "#FE9600", 75 | grade2: "#FFC501", 76 | grade1: "#FFEE4A", 77 | grade0: "#ebedf0" 78 | }, 79 | teal: { 80 | text: "#000000", 81 | meta: "#666666", 82 | grade4: "#458B74", 83 | grade3: "#66CDAA", 84 | grade2: "#76EEC6", 85 | grade1: "#7FFFD4", 86 | grade0: "#ebedf0" 87 | }, 88 | leftPad: { 89 | text: "#ffffff", 90 | meta: "#999999", 91 | grade4: "#F6F6F6", 92 | grade3: "#DDDDDD", 93 | grade2: "#A5A5A5", 94 | grade1: "#646464", 95 | grade0: "#2F2F2F" 96 | }, 97 | dracula: { 98 | text: "#f8f8f2", 99 | meta: "#666666", 100 | grade4: "#ff79c6", 101 | grade3: "#bd93f9", 102 | grade2: "#6272a4", 103 | grade1: "#44475a", 104 | grade0: "#282a36" 105 | }, 106 | blue: { 107 | text: "#C0C0C0", 108 | meta: "#666666", 109 | grade4: "#4F83BF", 110 | grade3: "#416895", 111 | grade2: "#344E6C", 112 | grade1: "#263342", 113 | grade0: "#222222" 114 | }, 115 | panda: { 116 | text: "#E6E6E6", 117 | meta: "#676B79", 118 | grade4: "#FF4B82", 119 | grade3: "#19f9d8", 120 | grade2: "#6FC1FF", 121 | grade1: "#34353B", 122 | grade0: "#242526" 123 | }, 124 | sunny: { 125 | text: "#000000", 126 | meta: "#666666", 127 | grade4: "#a98600", 128 | grade3: "#dab600", 129 | grade2: "#e9d700", 130 | grade1: "#f8ed62", 131 | grade0: "#fff9ae" 132 | }, 133 | pink: { 134 | text: "#000000", 135 | meta: "#666666", 136 | grade4: "#61185f", 137 | grade3: "#a74aa8", 138 | grade2: "#ca5bcc", 139 | grade1: "#e48bdc", 140 | grade0: "#ebedf0" 141 | }, 142 | YlGnBu: { 143 | text: "#000000", 144 | meta: "#666666", 145 | grade4: "#253494", 146 | grade3: "#2c7fb8", 147 | grade2: "#41b6c4", 148 | grade1: "#a1dab4", 149 | grade0: "#ebedf0" 150 | }, 151 | solarizedDark: { 152 | text: "#93a1a1", 153 | meta: "#586e75", 154 | grade4: "#d33682", 155 | grade3: "#b58900", 156 | grade2: "#2aa198", 157 | grade1: "#268bd2", 158 | grade0: "#073642" 159 | }, 160 | solarizedLight: { 161 | text: "#586e75", 162 | meta: "#93a1a1", 163 | grade4: "#6c71c4", 164 | grade3: "#dc322f", 165 | grade2: "#cb4b16", 166 | grade1: "#b58900", 167 | grade0: "#eee8d5" 168 | } 169 | }; 170 | 171 | 172 | // Commit count thresholds for grade-based coloring 173 | const COMMIT_THRESHOLDS = { 174 | grade1: 1, // 1-2 commits 175 | grade2: 3, // 3-5 commits 176 | grade3: 6, // 6-10 commits 177 | grade4: 11 // 11+ commits 178 | }; 179 | 180 | const MESSAGES = { 181 | NO_DATA: 'No data available', 182 | NO_COMMITS: 'No commit data available', 183 | MISSING_CREDENTIALS: 'Missing GitHub username or token', 184 | PREFS_ERROR: 'Failed to open extension preferences.' 185 | }; 186 | 187 | // Display and animation settings 188 | const DATE_FORMAT = { month: 'short' }; // How dates appear in the menu 189 | const DEFAULT_OPACITY = 50; // Base opacity for boxes with no commits 190 | const MAX_OPACITY_INCREASE = 205; // Maximum opacity boost for active boxes 191 | const OPACITY_PER_COMMIT = 20; // How much opacity increases per commit 192 | 193 | const Indicator = GObject.registerClass( 194 | class Indicator extends PanelMenu.Button { 195 | _init(preferences, extension) { 196 | super._init(0.0, _('Weekly Commits')); 197 | 198 | this.menu.setSourceAlignment(0); 199 | 200 | this._preferences = preferences; 201 | this._extension = extension; 202 | this._prefsChangedId = null; 203 | this._boxes = []; 204 | this._refreshTimeoutId = null; 205 | this._commitSection = null; 206 | this._separator = null; 207 | 208 | this._buildUI(); 209 | this._setupMenuItems(); 210 | this._updateContributionDisplay(); 211 | 212 | this._prefsChangedId = this._preferences.connectChanged(() => { 213 | this._clearCommitInfoItems(); 214 | this._updateContributionDisplay().finally(() => { 215 | this._refreshMenu(); 216 | }); 217 | }); 218 | } 219 | 220 | _buildUI() { 221 | // Create the main container that holds our week of commit boxes 222 | const containerBox = new St.BoxLayout({ 223 | vertical: true, 224 | x_expand: false, 225 | y_expand: true, 226 | y_align: Clutter.ActorAlign.CENTER 227 | }); 228 | 229 | // Horizontal row that will contain all 7 day boxes 230 | const hbox = new St.BoxLayout({ 231 | x_expand: false, 232 | y_expand: false, 233 | y_align: Clutter.ActorAlign.CENTER 234 | }); 235 | 236 | // Create 7 boxes (one for each day of the week) 237 | for (let i = 0; i < 7; i++) { 238 | // Container for each individual day box 239 | const boxContainer = new St.Widget({ 240 | layout_manager: new Clutter.BinLayout(), 241 | x_expand: false, 242 | y_expand: false, 243 | height: BOX_SIZE, 244 | width: BOX_SIZE, 245 | style: `margin-right: ${BOX_MARGIN}px;` 246 | }); 247 | 248 | // The actual visual box that shows commit activity 249 | const box = new St.Widget({ 250 | style_class: 'commit-box', 251 | height: BOX_SIZE, 252 | width: BOX_SIZE, 253 | style: this._getBoxStyle(COLORS.DEFAULT, true), // Start with empty styling 254 | opacity: DEFAULT_OPACITY, 255 | }); 256 | 257 | boxContainer.add_child(box); 258 | this._boxes.push(box); // Keep track of all boxes for updates 259 | hbox.add_child(boxContainer); 260 | } 261 | 262 | containerBox.add_child(hbox); 263 | this.add_child(containerBox); // Add to the panel button 264 | } 265 | 266 | _setupMenuItems() { 267 | // Add "Refresh Now" button to the dropdown menu 268 | const refreshItem = new PopupMenu.PopupMenuItem(_('Refresh Now')); 269 | refreshItem.connect('activate', () => { 270 | // Show user we're working on it 271 | refreshItem.label.text = _('Refreshing...'); 272 | 273 | // Clear old commit info and fetch new data 274 | this._clearCommitInfoItems(); 275 | 276 | this._updateContributionDisplay().finally(() => { 277 | // Reset button text when done 278 | refreshItem.label.text = _('Refresh Now'); 279 | this._refreshMenu(); 280 | }); 281 | }); 282 | this.menu.addMenuItem(refreshItem); 283 | 284 | // Add "Settings" button to open extension preferences 285 | const settingsItem = new PopupMenu.PopupMenuItem(_('Settings')); 286 | settingsItem.connect('activate', () => { 287 | this._openPreferences() 288 | }); 289 | this.menu.addMenuItem(settingsItem); 290 | } 291 | 292 | async _openPreferences() { 293 | try { 294 | await this._extension.openPreferences(); 295 | } catch (e) { 296 | console.error('Failed to open preferences:', e); 297 | Main.notify(_('Error'), _(MESSAGES.PREFS_ERROR)); 298 | } 299 | } 300 | 301 | _getBoxStyle(bgColor, isEmpty = false) { 302 | // Create the CSS styling for each commit activity box 303 | let style = `background-color: ${bgColor}; width: ${BOX_SIZE}px; height: ${BOX_SIZE}px; border-radius: ${BORDER_RADIUS}px;`; 304 | 305 | // Add a very subtle border so boxes are always visible, even on pure black backgrounds 306 | style += ' border: 1px solid rgba(255, 255, 255, 0.08);'; 307 | 308 | return style; 309 | } 310 | 311 | _getCommitGrade(count) { 312 | // Determine how "intense" the color should be based on commit count 313 | // This follows GitHub's contribution graph logic 314 | if (count === 0) return 'grade0'; // No commits = lightest/empty 315 | if (count < COMMIT_THRESHOLDS.grade2) return 'grade1'; // 1-2 commits = light 316 | if (count < COMMIT_THRESHOLDS.grade3) return 'grade2'; // 3-5 commits = medium 317 | if (count < COMMIT_THRESHOLDS.grade4) return 'grade3'; // 6-10 commits = dark 318 | return 'grade4'; // 11+ commits = darkest 319 | } 320 | 321 | _getThemedColor(count, themeName, colorMode) { 322 | // Get the color scheme for the current theme 323 | const theme = THEMES[themeName] || THEMES.standard; 324 | 325 | if (colorMode === 'grade') { 326 | // Grade mode: different colors for different activity levels (like GitHub) 327 | const grade = this._getCommitGrade(count); 328 | return theme[grade]; 329 | } else { 330 | // Opacity mode: same color for all, just varies transparency 331 | return count > 0 ? theme.grade3 : theme.grade0; 332 | } 333 | } 334 | 335 | _getCommitGrade(count) { 336 | if (count === 0) return 'grade0'; 337 | if (count < COMMIT_THRESHOLDS.grade2) return 'grade1'; 338 | if (count < COMMIT_THRESHOLDS.grade3) return 'grade2'; 339 | if (count < COMMIT_THRESHOLDS.grade4) return 'grade3'; 340 | return 'grade4'; 341 | } 342 | 343 | _getThemedColor(count, themeName, colorMode) { 344 | const theme = THEMES[themeName] || THEMES.standard; 345 | 346 | if (colorMode === 'grade') { 347 | const grade = this._getCommitGrade(count); 348 | return theme[grade]; 349 | } else { 350 | // Opacity mode - use grade1 color as base for active, grade0 for inactive 351 | return count > 0 ? theme.grade3 : theme.grade0; 352 | } 353 | } 354 | 355 | _formatDateWithCommits(date, count) { 356 | const today = new Date(); 357 | const isToday = date.getDate() === today.getDate() && 358 | date.getMonth() === today.getMonth() && 359 | date.getFullYear() === today.getFullYear(); 360 | 361 | const commitText = count === 1 ? 'commit' : 'commits'; 362 | 363 | if (isToday) { 364 | return { 365 | label: `Today: ${count} ${commitText}`, 366 | }; 367 | } else { 368 | const monthName = date.toLocaleString('en-US', DATE_FORMAT); 369 | const day = date.getDate(); 370 | return { 371 | label: `${monthName} ${day}: ${count} ${commitText}`, 372 | }; 373 | } 374 | } 375 | 376 | _updateCommitInfoSection(dates, counts) { 377 | if (!this._commitSection) { 378 | this._commitSection = new PopupMenu.PopupMenuSection(); 379 | this.menu.addMenuItem(this._commitSection, 0); 380 | 381 | this._commitItems = []; 382 | 383 | for (let i = 0; i < 7; i++) { 384 | const textItem = new St.Label({ 385 | text: '', 386 | style_class: 'commit-text-item', 387 | x_align: Clutter.ActorAlign.START, 388 | y_align: Clutter.ActorAlign.CENTER, 389 | style: 'font-family: monospace;' 390 | }); 391 | 392 | const itemBin = new St.BoxLayout({ 393 | style_class: 'popup-menu-item', 394 | reactive: false, 395 | can_focus: false, 396 | track_hover: false, 397 | style: 'padding-top: 2px; padding-bottom: 2px;' 398 | }); 399 | 400 | itemBin.add_child(textItem); 401 | this._commitSection.box.add_child(itemBin); 402 | this._commitItems.push({ bin: itemBin, label: textItem }); 403 | } 404 | 405 | if (!this._separator) { 406 | this._separator = new PopupMenu.PopupSeparatorMenuItem(); 407 | this.menu.addMenuItem(this._separator, 1); 408 | } 409 | } 410 | 411 | if (this._commitItems) { 412 | dates.forEach((date, index) => { 413 | const count = counts[index]; 414 | const { label } = this._formatDateWithCommits(date, count); 415 | 416 | if (this._commitItems[index]) { 417 | this._commitItems[index].label.text = label; 418 | } 419 | }); 420 | } 421 | } 422 | 423 | async _updateContributionDisplay() { 424 | try { 425 | // Make sure we have boxes to update 426 | if (!this._boxes || !this._boxes.length) { 427 | return; 428 | } 429 | 430 | // Get user's settings from the preferences 431 | const { 432 | githubUsername: username, 433 | githubToken: token, 434 | showCurrentWeekOnly, 435 | weekStartDay, 436 | highlightCurrentDay 437 | } = this._preferences; 438 | 439 | // Can't do anything without GitHub creds 440 | if (!username || !token) { 441 | console.warn(`Weekly Commits Extension: ${MESSAGES.MISSING_CREDENTIALS}`); 442 | this._setDefaultBoxAppearance(); 443 | return; 444 | } 445 | 446 | // Fetch commit data from GitHub API 447 | const counts = await fetchContributions(username, token, showCurrentWeekOnly, weekStartDay); 448 | 449 | // Double-check boxes still exist (user might have disabled extension) 450 | if (!this._boxes || !this._boxes.length) { 451 | return; 452 | } 453 | 454 | if (counts && counts.length === 7) { 455 | //Update both the boxes and the dropdown menu 456 | const dates = getDates(false, showCurrentWeekOnly, weekStartDay); 457 | 458 | this._updateCommitInfoSection(dates, counts); 459 | 460 | // Update each box with its commit count and styling 461 | counts.forEach((count, index) => { 462 | if (this._boxes[index]) { 463 | const isToday = this._isToday(dates[index]); 464 | const shouldHighlight = highlightCurrentDay && isToday; 465 | 466 | this._setBoxAppearance(this._boxes[index], count, shouldHighlight); 467 | } 468 | }); 469 | } else { 470 | // Something went wrong with the GitHub API 471 | console.error('Weekly Commits Extension: Failed to get valid contribution counts.'); 472 | this._setDefaultBoxAppearance(); 473 | } 474 | } catch (e) { 475 | // Handle errors 476 | console.error(`Weekly Commits Extension: Error updating display - ${e.message}`); 477 | if (this._boxes && this._boxes.length) { 478 | this._setDefaultBoxAppearance(); 479 | } 480 | } 481 | 482 | // Set up the next automatic refresh 483 | this._scheduleNextRefresh(); 484 | return Promise.resolve(); 485 | } 486 | 487 | _isToday(date) { 488 | const today = new Date(); 489 | return date.getDate() === today.getDate() && 490 | date.getMonth() === today.getMonth() && 491 | date.getFullYear() === today.getFullYear(); 492 | } 493 | 494 | _setBoxAppearance(box, count = 0, highlight = false) { 495 | 496 | // Get current theme settings - map enum index to theme key according to schema 497 | const themeKeys = [ 498 | 'standard', 'classic', 'githubDark', 'halloween', 'teal', 'leftPad', 499 | 'dracula', 'blue', 'panda', 'sunny', 'pink', 'YlGnBu', 500 | 'solarizedDark', 'solarizedLight' 501 | ]; 502 | const currentThemeName = themeKeys[this._preferences.themeName] || 'standard'; 503 | 504 | // Convert user's color mode preference (number from settings) to mode name 505 | const colorModeNames = ['opacity', 'grade']; 506 | const currentColorMode = colorModeNames[this._preferences.colorMode] || 'opacity'; 507 | 508 | // Get the appropriate color for this day's commit count 509 | let color = this._getThemedColor(count, currentThemeName, currentColorMode); 510 | const isEmpty = count === 0; 511 | 512 | // Special case: empty boxes get a subtle white fill so they're visible on dark backgrounds 513 | if (isEmpty) { 514 | color = 'rgba(255, 255, 255, 0.12)'; // Just enough white to see on pure black 515 | } 516 | 517 | let opacity = 255; // Default to full opacity 518 | 519 | if (currentColorMode === 'opacity') { 520 | // In opacity mode, boxes get more opaque with more commits 521 | opacity = count > 0 522 | ? DEFAULT_OPACITY + Math.min(count * OPACITY_PER_COMMIT, MAX_OPACITY_INCREASE) 523 | : 255; // Empty boxes stay fully opaque so the subtle fill is visible 524 | } 525 | 526 | if (highlight) { 527 | box.opacity = opacity; 528 | box.style = `${this._getBoxStyle(color, isEmpty)} border: 2px solid rgba(255, 255, 255, 0.6); box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);`; 529 | } else { 530 | // Regular days just get the themed color and opacity 531 | box.opacity = opacity; 532 | box.style = this._getBoxStyle(color, isEmpty); 533 | } 534 | } 535 | 536 | _scheduleNextRefresh() { 537 | if (this._refreshTimeoutId) { 538 | GLib.Source.remove(this._refreshTimeoutId); 539 | } 540 | 541 | const interval = this._preferences.refreshInterval; 542 | this._refreshTimeoutId = GLib.timeout_add_seconds( 543 | GLib.PRIORITY_DEFAULT, 544 | interval, 545 | () => { 546 | this._updateContributionDisplay(); 547 | return GLib.SOURCE_CONTINUE; 548 | } 549 | ); 550 | } 551 | 552 | _clearCommitInfoItems() { 553 | if (this._commitItems) { 554 | this._commitItems = []; 555 | } 556 | 557 | if (this._commitSection) { 558 | try { 559 | this._commitSection.destroy(); 560 | } catch (e) { 561 | console.log('Error destroying commit section:', e); 562 | } 563 | this._commitSection = null; 564 | } 565 | 566 | if (this._separator) { 567 | try { 568 | this._separator.destroy(); 569 | } catch (e) { 570 | console.log('Error destroying separator:', e); 571 | } 572 | this._separator = null; 573 | } 574 | } 575 | 576 | _setDefaultBoxAppearance() { 577 | this._boxes.forEach(box => { 578 | this._setBoxAppearance(box, 0, false); 579 | }); 580 | 581 | this._clearCommitInfoItems(); 582 | 583 | const commitSection = new PopupMenu.PopupMenuSection(); 584 | const item = new PopupMenu.PopupMenuItem(MESSAGES.NO_COMMITS); 585 | commitSection.addMenuItem(item); 586 | this.menu.addMenuItem(commitSection, 0); 587 | this._commitSection = commitSection; 588 | 589 | if (!this._separator) { 590 | this._separator = new PopupMenu.PopupSeparatorMenuItem(); 591 | this.menu.addMenuItem(this._separator, 1); 592 | } 593 | } 594 | 595 | _refreshMenu() { 596 | if (this.menu.isOpen) { 597 | this.menu.close(); 598 | this.menu.open(); 599 | } 600 | } 601 | 602 | destroy() { 603 | this._boxes.forEach(box => { 604 | box.remove_all_transitions(); 605 | }); 606 | 607 | if (this._refreshTimeoutId) { 608 | GLib.Source.remove(this._refreshTimeoutId); 609 | this._refreshTimeoutId = null; 610 | } 611 | 612 | if (this._prefsChangedId) { 613 | this._preferences.disconnectChanged(this._prefsChangedId); 614 | this._prefsChangedId = null; 615 | } 616 | 617 | this._clearCommitInfoItems(); 618 | this._boxes = null; 619 | this._cache = null; 620 | this._commitItems = null; 621 | 622 | super.destroy(); 623 | } 624 | }); 625 | 626 | export default class WeeklyCommitsExtension extends Extension { 627 | enable() { 628 | // Set up user preferences and settings 629 | this._preferences = new ExtensionSettings(this); 630 | 631 | // Listen for changes to panel position settings so we can move the indicator 632 | this._positionChangedId = this._preferences._settings.connect('changed', (settings, key) => { 633 | if (key === 'panel-position' || key === 'panel-index') { 634 | this._updateIndicatorPosition(); 635 | } 636 | }); 637 | 638 | // Wait a bit before creating the indicator to ensure GNOME Shell is ready 639 | // This prevents issues during login/startup 640 | this._enableTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000, () => { 641 | if (this._preferences) { 642 | this._indicator = new Indicator(this._preferences, this); 643 | this._updateIndicatorPosition(); 644 | } 645 | this._enableTimeoutId = null; 646 | return GLib.SOURCE_REMOVE; // Don't repeat this timeout 647 | }); 648 | } 649 | 650 | _updateIndicatorPosition() { 651 | // Don't do anything if there's no indicator yet 652 | if (!this._indicator) return; 653 | 654 | // Convert user's position preference to actual panel position 655 | const position = ['left', 'center', 'right'][this._preferences.panelPosition] || 'right'; 656 | const index = this._preferences.panelIndex || 0; 657 | 658 | // Remove the old indicator from the panel 659 | this._indicator.destroy(); 660 | this._indicator = null; 661 | 662 | // Create a new indicator in the new position 663 | if (this._preferences) { 664 | this._indicator = new Indicator(this._preferences, this); 665 | Main.panel.addToStatusArea(this.uuid, this._indicator, index, position); 666 | } 667 | } 668 | 669 | disable() { 670 | // Clean up any pending timeout from the enable phase 671 | if (this._enableTimeoutId) { 672 | GLib.Source.remove(this._enableTimeoutId); 673 | this._enableTimeoutId = null; 674 | } 675 | 676 | // Stop listening for settings changes 677 | if (this._positionChangedId) { 678 | this._preferences._settings.disconnect(this._positionChangedId); 679 | this._positionChangedId = null; 680 | } 681 | 682 | // Remove the indicator from the panel 683 | if (this._indicator) { 684 | this._indicator.destroy(); 685 | this._indicator = null; 686 | } 687 | 688 | // Clean up preferences 689 | this._preferences = null; 690 | } 691 | } 692 | --------------------------------------------------------------------------------