Skip to content

Add Project Tracker desklet#1845

Open
suleman-dawood wants to merge 4 commits into
linuxmint:masterfrom
suleman-dawood:add-project-tracker
Open

Add Project Tracker desklet#1845
suleman-dawood wants to merge 4 commits into
linuxmint:masterfrom
suleman-dawood:add-project-tracker

Conversation

@suleman-dawood

Copy link
Copy Markdown

New Desklet: Project Tracker

Desktop widget to track your VS Code projects at a glance.

Features

  • Reads VS Code workspaceStorage to find recent projects automatically
  • Shows last edited time (most recently modified file in project)
  • Shows git stats: total commits, .git size, last commit time
  • Pin favorite projects to the top
  • Scrollable list — 4 visible, scroll for more
  • Customizable via settings: background color, row color, font color, accent color, font scale
  • Git-only filter toggle
  • Click any project to open in VS Code

Screenshot

screenshot

Settings

  • Total projects (scrollable max)
  • Git-only checkbox
  • Font scale slider
  • Color pickers: background, row, font, accent
  • Hide decorations toggle

Desktop widget to track VS Code projects. Shows last edited time,
git stats (commits, repo size, last commit), pinning, scrollable list,
and full color customization via settings.
@github-actions

Copy link
Copy Markdown

Best-practices scanner

This is a regex-based check for API usage that can pose security, performance or
maintainability issues, or that may already be provided by Cinnamon. Most findings
are advisory and do not automatically disqualify a pull request.

This check is not perfect and will not replace a normal review.


Found 22 potential issue(s):

⚠️ WARNING

⚠️ sync_spawn_command_line

project-tracker@suleman/files/project-tracker@suleman/desklet.js:160

let [ok, stdout] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:203

let [ok1, stdout1] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:214

let [ok2, stdout2] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:227

let [ok3, stdout3] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:237

let [ok4, stdout4] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

⚠️ sync_file_get_contents

project-tracker@suleman/files/project-tracker@suleman/desklet.js:61

let [ok, contents] = GLib.file_get_contents(PINNED_FILE);

Synchronous file_get_contents() blocks the main loop.
Use Gio.File.load_contents_async() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:108

let [ok, contents] = GLib.file_get_contents(wsJsonPath);

Synchronous file_get_contents() blocks the main loop.
Use Gio.File.load_contents_async() instead.

⚠️ sync_file_query_info

project-tracker@suleman/files/project-tracker@suleman/desklet.js:123

let wsInfo = wsFile.query_info("time::modified", Gio.FileQueryInfoFlags.NONE, null);

Synchronous query_info() blocks the main loop.
Use query_info_async() instead.

⚠️ sync_file_enumerate_children

project-tracker@suleman/files/project-tracker@suleman/desklet.js:96

let enumerator = dir.enumerate_children(

Synchronous enumerate_children() blocks the main loop.
Use enumerate_children_async() instead.

⚠️ sync_file_test

project-tracker@suleman/files/project-tracker@suleman/desklet.js:60

if (GLib.file_test(PINNED_FILE, GLib.FileTest.EXISTS)) {

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:105

if (!GLib.file_test(wsJsonPath, GLib.FileTest.EXISTS)) continue;

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:118

if (!GLib.file_test(path, GLib.FileTest.IS_DIR)) continue;

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:120

if (this.gitOnly && !GLib.file_test(path + "/.git", GLib.FileTest.IS_DIR)) continue;

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:197

if (!GLib.file_test(projectPath + "/.git", GLib.FileTest.IS_DIR))

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

⚠️ hardcoded_config_dir

project-tracker@suleman/files/project-tracker@suleman/desklet.js:91

let wsDir = GLib.get_home_dir() + "/.config/Code/User/workspaceStorage";

Avoid hardcoding .config in paths. Use GLib.get_user_config_dir() instead,
which respects the XDG_CONFIG_HOME environment variable.

⚠️ lang_bind

project-tracker@suleman/files/project-tracker@suleman/desklet.js:342

Lang.bind(this, this.refreshData)

Lang.bind() is deprecated. Use arrow functions (() => {}) or Function.prototype.bind() instead.

ℹ️ INFO

ℹ️ shell_string_spawn

project-tracker@suleman/files/project-tracker@suleman/desklet.js:160

let [ok, stdout] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:203

let [ok1, stdout1] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:214

let [ok2, stdout2] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:227

let [ok3, stdout3] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:237

let [ok4, stdout4] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:363

GLib.spawn_command_line_async("code " + GLib.shell_quote(projectPath));

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.


Automated pattern check.

Each source can be enabled/disabled independently in settings.
Projects are deduplicated across editors.
@github-actions

Copy link
Copy Markdown

Best-practices scanner

This is a regex-based check for API usage that can pose security, performance or
maintainability issues, or that may already be provided by Cinnamon. Most findings
are advisory and do not automatically disqualify a pull request.

This check is not perfect and will not replace a normal review.


Found 25 potential issue(s):

⚠️ WARNING

⚠️ sync_spawn_command_line

project-tracker@suleman/files/project-tracker@suleman/desklet.js:181

let [ok, stdout] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:224

let [ok1, stdout1] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:235

let [ok2, stdout2] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:248

let [ok3, stdout3] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:258

let [ok4, stdout4] = GLib.spawn_command_line_sync(

Synchronous process spawning blocks the main loop.
Use GLib.spawn_command_line_async() or Gio.Subprocess instead.

⚠️ sync_file_get_contents

project-tracker@suleman/files/project-tracker@suleman/desklet.js:64

let [ok, contents] = GLib.file_get_contents(PINNED_FILE);

Synchronous file_get_contents() blocks the main loop.
Use Gio.File.load_contents_async() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:143

let [ok, contents] = GLib.file_get_contents(wsJsonPath);

Synchronous file_get_contents() blocks the main loop.
Use Gio.File.load_contents_async() instead.

⚠️ sync_file_query_info

project-tracker@suleman/files/project-tracker@suleman/desklet.js:158

let wsInfo = wsFile.query_info("time::modified", Gio.FileQueryInfoFlags.NONE, null);

Synchronous query_info() blocks the main loop.
Use query_info_async() instead.

⚠️ sync_file_enumerate_children

project-tracker@suleman/files/project-tracker@suleman/desklet.js:131

let enumerator = dir.enumerate_children(

Synchronous enumerate_children() blocks the main loop.
Use enumerate_children_async() instead.

⚠️ sync_file_test

project-tracker@suleman/files/project-tracker@suleman/desklet.js:63

if (GLib.file_test(PINNED_FILE, GLib.FileTest.EXISTS)) {

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:127

if (!GLib.file_test(wsDir, GLib.FileTest.IS_DIR)) return;

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:140

if (!GLib.file_test(wsJsonPath, GLib.FileTest.EXISTS)) continue;

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:153

if (!GLib.file_test(path, GLib.FileTest.IS_DIR)) continue;

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:155

if (this.gitOnly && !GLib.file_test(path + "/.git", GLib.FileTest.IS_DIR)) continue;

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:218

if (!GLib.file_test(projectPath + "/.git", GLib.FileTest.IS_DIR))

file_test() is a synchronous stat call that can block on slow/network filesystems.
Prefer attempting the operation and handling a Gio.IOErrorEnum.NOT_FOUND error instead.

⚠️ hardcoded_config_dir

project-tracker@suleman/files/project-tracker@suleman/desklet.js:100

sources.push(home + "/.config/Code/User/workspaceStorage");

Avoid hardcoding .config in paths. Use GLib.get_user_config_dir() instead,
which respects the XDG_CONFIG_HOME environment variable.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:102

sources.push(home + "/.config/VSCodium/User/workspaceStorage");

Avoid hardcoding .config in paths. Use GLib.get_user_config_dir() instead,
which respects the XDG_CONFIG_HOME environment variable.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:104

sources.push(home + "/.config/Cursor/User/workspaceStorage");

Avoid hardcoding .config in paths. Use GLib.get_user_config_dir() instead,
which respects the XDG_CONFIG_HOME environment variable.

⚠️ lang_bind

project-tracker@suleman/files/project-tracker@suleman/desklet.js:363

Lang.bind(this, this.refreshData)

Lang.bind() is deprecated. Use arrow functions (() => {}) or Function.prototype.bind() instead.

ℹ️ INFO

ℹ️ shell_string_spawn

project-tracker@suleman/files/project-tracker@suleman/desklet.js:181

let [ok, stdout] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:224

let [ok1, stdout1] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:235

let [ok2, stdout2] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:248

let [ok3, stdout3] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:258

let [ok4, stdout4] = GLib.spawn_command_line_sync(

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:384

GLib.spawn_command_line_async("code " + GLib.shell_quote(projectPath));

Prefer argument vector spawn functions over shell command strings.
This is especially important when arguments include untrusted input (user data,
filenames, settings values, etc.) as shell strings are vulnerable to injection.
Static command strings are generally fine, but argv is always safer.
Use Util.spawn(["cmd", "arg1", "arg2"]) or Util.trySpawn() instead.


Automated pattern check.

- Replace GLib.spawn_command_line_sync with Gio.Subprocess (argv-style)
- Replace GLib.file_test + file_get_contents with Gio.File.load_contents
- Replace file_test existence checks with query_info try/catch
- Use GLib.get_user_config_dir() instead of hardcoded .config paths
- Replace deprecated Lang.bind with Function.prototype.bind
- Use Gio.Subprocess.new for async launch instead of spawn_command_line_async
- Remove unused Lang import
@github-actions

Copy link
Copy Markdown

Best-practices scanner

This is a regex-based check for API usage that can pose security, performance or
maintainability issues, or that may already be provided by Cinnamon. Most findings
are advisory and do not automatically disqualify a pull request.

This check is not perfect and will not replace a normal review.


Found 7 potential issue(s):

⚠️ WARNING

⚠️ sync_file_load_contents

project-tracker@suleman/files/project-tracker@suleman/desklet.js:80

let [ok, contents] = file.load_contents(null);

Synchronous load_contents() blocks the main loop.
Use load_contents_async() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:155

let [ok, contents] = wsFile.load_contents(null);

Synchronous load_contents() blocks the main loop.
Use load_contents_async() instead.

⚠️ sync_file_query_info

project-tracker@suleman/files/project-tracker@suleman/desklet.js:168

projInfo = projDir.query_info("standard::type", Gio.FileQueryInfoFlags.NONE, null);

Synchronous query_info() blocks the main loop.
Use query_info_async() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:175

let gitInfo = gitDir.query_info("standard::type", Gio.FileQueryInfoFlags.NONE, null);

Synchronous query_info() blocks the main loop.
Use query_info_async() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:180

let wsInfo = wsFile.query_info("time::modified", Gio.FileQueryInfoFlags.NONE, null);

Synchronous query_info() blocks the main loop.
Use query_info_async() instead.

project-tracker@suleman/files/project-tracker@suleman/desklet.js:239

let gitInfo = gitDir.query_info("standard::type", Gio.FileQueryInfoFlags.NONE, null);

Synchronous query_info() blocks the main loop.
Use query_info_async() instead.

⚠️ sync_file_enumerate_children

project-tracker@suleman/files/project-tracker@suleman/desklet.js:143

let enumerator = dir.enumerate_children(

Synchronous enumerate_children() blocks the main loop.
Use enumerate_children_async() instead.


Automated pattern check.

The desklet caused Cinnamon to freeze video playback for ~1 second
on every refresh tick. Three causes, all fixed:

1. Synchronous subprocess spawns. Each refresh ran up to 41
   spawn_command_line_sync calls (find + 4 git/du per project x 10).
   Each one blocked the JS thread — which is also the compositor
   thread — until the subprocess exited. Replaced with a
   Gio.Subprocess communicate_utf8_async helper so all spawns are
   non-blocking.

2. Synchronous workspaceStorage scan. file_get_contents + JSON.parse
   + query_info per workspace.json (often 50-100 entries) ran on the
   main thread. Converted to load_contents_async + query_info_async,
   yielding via Mainloop.idle_add between entries.

3. Widget churn. Every refresh called destroy_all_children on the
   row container and rebuilt ~80 St widgets, each with a CSS-parsed
   inline style. Restructured into an ensureShell + buildRow +
   updateRow pattern: row widgets are created once and refreshes
   only mutate label text in place.

Also added an mtime-keyed cache so unchanged projects skip the 4
subprocess calls on subsequent refreshes. Default refresh bumped
from 120s to 600s — frequent refresh is no longer needed now that
updates are cheap.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant