diff --git a/UnityExtension/Commands/OpenUnityCommand.cs b/UnityExtension/Commands/OpenUnityCommand.cs
index ac27029..320fa7d 100644
--- a/UnityExtension/Commands/OpenUnityCommand.cs
+++ b/UnityExtension/Commands/OpenUnityCommand.cs
@@ -24,6 +24,14 @@ public OpenUnityCommand(UnityProject project)
public override CommandResult Invoke()
{
+ // Dead reference: the project's folder no longer exists on disk. Don't try to
+ // launch Unity against a missing path (also guards against the folder being
+ // deleted after the list was built).
+ if (!Directory.Exists(_projectPath))
+ {
+ return CommandResult.ShowToast($"Project folder no longer exists:\n{_projectPath}");
+ }
+
// Lazy-load only when invoking
_editorPath ??= EditorParser.GetExecutablePathForVersion(_projectVersion);
diff --git a/UnityExtension/Data/UnityProject.cs b/UnityExtension/Data/UnityProject.cs
index 0e2831a..510ca18 100644
--- a/UnityExtension/Data/UnityProject.cs
+++ b/UnityExtension/Data/UnityProject.cs
@@ -7,4 +7,10 @@ internal sealed record UnityProject
public string Version { get; init; } = string.Empty;
public long LastModified { get; init; }
public bool IsFavorite { get; init; }
+
+ ///
+ /// Whether the project's folder still exists on disk. Dead references
+ /// (moved/deleted projects still listed by Unity Hub) have this set to false.
+ ///
+ public bool Exists { get; init; }
}
diff --git a/UnityExtension/Helpers/ProjectParser.cs b/UnityExtension/Helpers/ProjectParser.cs
index 852d831..3c2e43d 100644
--- a/UnityExtension/Helpers/ProjectParser.cs
+++ b/UnityExtension/Helpers/ProjectParser.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
@@ -9,49 +9,122 @@ public static class ProjectParser
{
internal static List GetUnityProjects()
{
- var result = new List();
- var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
- var projectsJsonFilePath = Path.Combine(appDataPath, Resources.ProjectsJsonPath);
+ var projectsJsonFilePath = Resources.ProjectsJsonPath;
if (!File.Exists(projectsJsonFilePath))
{
- return result;
+ return new List();
}
+ string jsonContent;
try
{
- var jsonContent = File.ReadAllText(projectsJsonFilePath);
- var projectsRoot = JsonDocument.Parse(jsonContent).RootElement;
+ jsonContent = File.ReadAllText(projectsJsonFilePath);
+ }
+ catch (Exception)
+ {
+ // File exists but couldn't be read (locked, permissions, etc.)
+ return new List();
+ }
+
+ return ParseProjects(jsonContent);
+ }
+
+ ///
+ /// Parses the contents of Unity Hub's projects-v1.json into a list of projects.
+ /// Each entry is parsed independently: a single malformed or incomplete entry
+ /// (e.g. a dead reference to a moved/deleted project) is skipped rather than
+ /// aborting the whole list.
+ ///
+ internal static List ParseProjects(string jsonContent)
+ {
+ var result = new List();
+
+ JsonElement projectsRoot;
+ try
+ {
+ projectsRoot = JsonDocument.Parse(jsonContent).RootElement;
+ }
+ catch (JsonException)
+ {
+ // Corrupt/unreadable JSON → empty list; the page shows "No projects found".
+ return result;
+ }
+
+ if (
+ !projectsRoot.TryGetProperty("data", out var data)
+ || data.ValueKind != JsonValueKind.Object
+ )
+ {
+ return result;
+ }
- if (projectsRoot.TryGetProperty("data", out var data))
+ foreach (var property in data.EnumerateObject())
+ {
+ try
{
- foreach (var property in data.EnumerateObject())
+ var projectPath = property.Name;
+ var projectInfo = property.Value;
+
+ var fallbackTitle = Path.GetFileName(projectPath.TrimEnd('/', '\\'));
+
+ var title =
+ projectInfo.TryGetProperty("title", out var titleEl)
+ && titleEl.ValueKind == JsonValueKind.String
+ && !string.IsNullOrEmpty(titleEl.GetString())
+ ? titleEl.GetString()!
+ : fallbackTitle;
+
+ var version =
+ projectInfo.TryGetProperty("version", out var versionEl)
+ && versionEl.ValueKind == JsonValueKind.String
+ ? versionEl.GetString()!
+ : "Unknown";
+
+ long lastModified = 0;
+ if (
+ projectInfo.TryGetProperty("lastModified", out var lastModifiedEl)
+ && lastModifiedEl.ValueKind == JsonValueKind.Number
+ && lastModifiedEl.TryGetInt64(out var parsedLastModified)
+ )
{
- var projectPath = property.Name;
- var projectInfo = property.Value;
+ lastModified = parsedLastModified;
+ }
+
+ var isFavorite =
+ projectInfo.TryGetProperty("isFavorite", out var isFavoriteEl)
+ && isFavoriteEl.ValueKind == JsonValueKind.True;
- var project = new UnityProject
+ result.Add(
+ new UnityProject
{
Path = projectPath,
- Title =
- projectInfo.GetProperty("title").GetString()
- ?? Path.GetFileName(projectPath),
- Version = projectInfo.GetProperty("version").GetString() ?? "Unknown",
- LastModified = projectInfo.GetProperty("lastModified").GetInt64(),
- IsFavorite =
- projectInfo.TryGetProperty("isFavorite", out var isFavorite)
- && isFavorite.GetBoolean(),
- };
-
- result.Add(project);
- }
+ Title = title,
+ Version = version,
+ LastModified = lastModified,
+ IsFavorite = isFavorite,
+ Exists = SafeDirectoryExists(projectPath),
+ }
+ );
+ }
+ catch (Exception)
+ {
+ // Skip only this malformed entry; keep parsing the rest.
}
}
+
+ return result;
+ }
+
+ private static bool SafeDirectoryExists(string path)
+ {
+ try
+ {
+ return !string.IsNullOrEmpty(path) && Directory.Exists(path);
+ }
catch (Exception)
{
- // Any error in parsing will result in an empty list
+ return false;
}
-
- return result;
}
}
diff --git a/UnityExtension/Helpers/Resources.cs b/UnityExtension/Helpers/Resources.cs
index d4ca297..b3b4a1e 100644
--- a/UnityExtension/Helpers/Resources.cs
+++ b/UnityExtension/Helpers/Resources.cs
@@ -13,5 +13,9 @@ public static class Resources
);
public static IconInfo IconUrl => new("\uE8A7");
+
+ // Segoe Fluent "Warning" glyph \u2014 used to flag projects whose folder is gone from disk.
+ public static IconInfo IconMissing => new("\uE7BA");
+
public static IconInfo IconUnity => IconHelpers.FromRelativePath("Assets\\UnityLogo.png");
}
diff --git a/UnityExtension/Helpers/SettingsManager.cs b/UnityExtension/Helpers/SettingsManager.cs
index 817f07a..a6c6cf1 100644
--- a/UnityExtension/Helpers/SettingsManager.cs
+++ b/UnityExtension/Helpers/SettingsManager.cs
@@ -9,6 +9,9 @@ internal sealed class SettingsManager : JsonSettingsManager
private ToggleSetting GroupFavoritesFirstSetting { get; }
public bool GroupFavoritesFirst => GroupFavoritesFirstSetting.Value;
+ private ToggleSetting HideMissingProjectsSetting { get; }
+ public bool HideMissingProjects => HideMissingProjectsSetting.Value;
+
private static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("UnityExtension");
@@ -27,7 +30,15 @@ public SettingsManager()
defaultValue: true
);
+ HideMissingProjectsSetting = new ToggleSetting(
+ key: "hideMissingProjects",
+ label: "Hide projects missing from disk",
+ description: "Don't list Unity projects whose folder no longer exists on disk",
+ defaultValue: false
+ );
+
Settings.Add(GroupFavoritesFirstSetting);
+ Settings.Add(HideMissingProjectsSetting);
LoadSettings();
diff --git a/UnityExtension/Pages/UnityExtensionPage.cs b/UnityExtension/Pages/UnityExtensionPage.cs
index 09536ba..af7e135 100644
--- a/UnityExtension/Pages/UnityExtensionPage.cs
+++ b/UnityExtension/Pages/UnityExtensionPage.cs
@@ -34,6 +34,11 @@ public override IListItem[] GetItems()
{
var projects = ProjectParser.GetUnityProjects();
+ if (_settingsManager.HideMissingProjects)
+ {
+ projects = projects.Where(p => p.Exists).ToList();
+ }
+
if (projects.Count == 0)
{
items.Add(
@@ -50,14 +55,11 @@ public override IListItem[] GetItems()
{
projects.Sort((a, b) => b.LastModified.CompareTo(a.LastModified));
- if (_settingsManager.GroupFavoritesFirst)
- {
- var favoriteProjects = projects.Where(p => p.IsFavorite).ToList();
- var nonFavoriteProjects = projects.Where(p => !p.IsFavorite).ToList();
+ IEnumerable ordered = _settingsManager.GroupFavoritesFirst
+ ? projects.Where(p => p.IsFavorite).Concat(projects.Where(p => !p.IsFavorite))
+ : projects;
- items.AddRange(favoriteProjects.Select(CreateProjectListItem));
- items.AddRange(nonFavoriteProjects.Select(CreateProjectListItem));
- }
+ items.AddRange(ordered.Select(CreateProjectListItem));
}
}
catch (Exception ex)
@@ -94,13 +96,18 @@ private static ListItem CreateProjectListItem(UnityProject project)
tags.Add(new Tag("⭐ Favorite") { Foreground = ColorHelpers.FromRgb(222, 186, 56) });
}
+ if (!project.Exists)
+ {
+ tags.Add(new Tag("Missing") { Foreground = ColorHelpers.FromRgb(224, 108, 117) });
+ }
+
tags.Add(new Tag(project.Version));
return new ListItem(defaultCommand)
{
Title = project.Title,
- Subtitle = project.Path,
- Icon = Resources.IconUnity,
+ Subtitle = project.Exists ? project.Path : $"{project.Path} (folder not found)",
+ Icon = project.Exists ? Resources.IconUnity : Resources.IconMissing,
Tags = tags.ToArray(),
MoreCommands = contextCommands.ToArray(),
};