Skip to content

Commit aad0403

Browse files
committed
feature: use XDG standard directories on Linux
- Save `preference.json` to `${XDG_CONFIG_HOME}/SourceGit/` - Read `external_editors.json` from `${XDG_CONFIG_HOME}/SourceGit/` - Save avatars to `${XDG_CACHE_HOME}/SourceGit/avatars` - Save crash logs to `${XDG_CACHE_HOME}/SourceGit/crashes` - On Linux, `Open Data Storage Directory` menu has two sub-menu items: `Config` and `Cache` - Auto migrate legacy data dir `~/.sourcegit` to XDG standard Signed-off-by: leo <longshuang@msn.cn>
1 parent e308279 commit aad0403

15 files changed

Lines changed: 126 additions & 37 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ You can download the latest stable from [Releases](https://github.com/sourcegit-
7272

7373
This software creates a folder, which is platform-dependent, to store user settings, downloaded avatars and crash logs.
7474

75-
| OS | PATH |
76-
|---------|-------------------------------------------|
77-
| Windows | `%APPDATA%\SourceGit` |
78-
| Linux | `~/.sourcegit` |
79-
| macOS | `~/Library/Application Support/SourceGit` |
75+
| OS | PATH |
76+
|---------|--------------------------------------------------------------------------------------------------|
77+
| Windows | `%APPDATA%\SourceGit` |
78+
| Linux | `${XDG_CONFIG_HOME}/SourceGit` (preference.json) `${XDG_CACHE_HOME}/SourceGit` (avatars & logs) |
79+
| macOS | `~/Library/Application Support/SourceGit` |
8080

8181
> [!TIP]
8282
> * You can open this data storage directory from the main menu `Open Data Storage Directory`.
@@ -215,7 +215,7 @@ This app supports open repository in external tools listed in the table below.
215215

216216
> [!NOTE]
217217
> This app will try to find those tools based on some pre-defined or expected locations automatically. If you are using one portable version of these tools, it will not be detected by this app.
218-
> To solve this problem you can add a file named `external_editors.json` in app data storage directory and provide the path directly.
218+
> To solve this problem you can add a file named `external_editors.json` in app data storage directory (config dir for Linux) and provide the path directly.
219219
> User can also exclude some editors by using `external_editors.json`.
220220
221221
The format of `external_editors.json`:

src/App.Commands.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,14 @@ public static bool IsCheckForUpdateCommandVisible
5555
}
5656
});
5757

58+
public static readonly Command OpenAppConfigDirCommand = new Command(_ =>
59+
{
60+
Native.OS.OpenInFileManager(Native.OS.BasicDirectories.ConfigDir);
61+
});
62+
5863
public static readonly Command OpenAppDataDirCommand = new Command(_ =>
5964
{
60-
Native.OS.OpenInFileManager(Native.OS.DataDir);
65+
Native.OS.OpenInFileManager(Native.OS.BasicDirectories.CacheDir);
6166
});
6267

6368
public static readonly Command OpenAboutCommand = new Command(async _ =>

src/App.axaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public partial class App : Application
2323
[STAThread]
2424
public static void Main(string[] args)
2525
{
26-
Native.OS.SetupDataDir();
26+
Native.OS.SetupBasicDirectories();
2727

2828
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
2929
{

src/Models/AvatarManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static AvatarManager Instance
4444

4545
public void Start()
4646
{
47-
_storePath = Path.Combine(Native.OS.DataDir, "avatars");
47+
_storePath = Path.Combine(Native.OS.BasicDirectories.CacheDir, "avatars");
4848
if (!Directory.Exists(_storePath))
4949
Directory.CreateDirectory(_storePath);
5050

src/Models/ExternalTool.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public List<ExternalTool> Tools
120120

121121
public ExternalToolsFinder()
122122
{
123-
var customPathsConfig = Path.Combine(Native.OS.DataDir, "external_editors.json");
123+
var customPathsConfig = Path.Combine(Native.OS.BasicDirectories.ConfigDir, "external_editors.json");
124124
try
125125
{
126126
if (File.Exists(customPathsConfig))

src/Models/IpcChannel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public IpcChannel()
1818
{
1919
try
2020
{
21-
_singletonLock = File.Open(Path.Combine(Native.OS.DataDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
21+
_singletonLock = File.Open(Path.Combine(Native.OS.BasicDirectories.CacheDir, "process.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
2222
IsFirstInstance = true;
2323
_server = new NamedPipeServerStream(
2424
GetPipeName(),
@@ -76,7 +76,7 @@ private static string GetPipeName()
7676
return "SourceGit";
7777

7878
// Windows and Linux can have multiple instances of SourceGit running (portable-mode), so we need to generate a unique pipe name based on the data directory.
79-
var dataDir = Native.OS.DataDir.Replace('\\', '/').TrimEnd('/');
79+
var dataDir = Native.OS.BasicDirectories.CacheDir.Replace('\\', '/').TrimEnd('/');
8080
var hashStr = $"{Environment.UserName}_{dataDir}";
8181
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(hashStr))).Substring(0, 10);
8282
return $"SG_{hash}";

src/Native/Linux.cs

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,55 @@ public void SetupWindow(Window window)
3939
}
4040
}
4141

42-
public string GetDataDir()
42+
public OS.Directories GetOrCreateDirectories()
4343
{
44+
var dirs = new OS.Directories();
45+
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
46+
4447
// AppImage supports portable mode
4548
var appImage = Environment.GetEnvironmentVariable("APPIMAGE");
4649
if (!string.IsNullOrEmpty(appImage) && File.Exists(appImage))
4750
{
4851
var portableDir = Path.Combine(Path.GetDirectoryName(appImage)!, "data");
4952
if (Directory.Exists(portableDir))
50-
return portableDir;
53+
{
54+
dirs.ConfigDir = portableDir;
55+
dirs.CacheDir = portableDir;
56+
return dirs;
57+
}
5158
}
5259

53-
// Runtime data dir: ~/.sourcegit
54-
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
55-
return Path.Combine(home, ".sourcegit");
60+
// XDG Base Directory Specification: https://specifications.freedesktop.org/basedir/latest/
61+
dirs.ConfigDir = GetXdgDirectory("XDG_CONFIG_HOME", Path.Combine(home, ".config"), "SourceGit");
62+
dirs.CacheDir = GetXdgDirectory("XDG_CACHE_HOME", Path.Combine(home, ".cache"), "SourceGit");
63+
64+
// If the app basic dirs already exist, we can skip the migration step
65+
if (Directory.Exists(dirs.ConfigDir))
66+
return dirs;
67+
68+
// Migrate legacy data dir: ~/.sourcegit to XDG standard directories
69+
var legacyDir = Path.Combine(home, ".sourcegit");
70+
if (Directory.Exists(legacyDir))
71+
{
72+
try
73+
{
74+
File.Copy(Path.Combine(legacyDir, "preference.json"), Path.Combine(dirs.ConfigDir, "preference.json"), true);
75+
Directory.Move(Path.Combine(legacyDir, "avatars"), Path.Combine(dirs.CacheDir, "avatars"));
76+
Directory.Delete(legacyDir, true);
77+
}
78+
catch
79+
{
80+
// Ignore any errors during migration
81+
}
82+
}
83+
84+
// Create the config and cache directories if they don't exist
85+
if (!Directory.Exists(dirs.ConfigDir))
86+
Directory.CreateDirectory(dirs.ConfigDir);
87+
if (!Directory.Exists(dirs.CacheDir))
88+
Directory.CreateDirectory(dirs.CacheDir);
89+
90+
return dirs;
5691
}
5792

5893
public string FindGitExecutable()
@@ -162,5 +197,14 @@ private string FindExecutable(string filename)
162197
var local = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", filename);
163198
return File.Exists(local) ? local : string.Empty;
164199
}
200+
201+
private string GetXdgDirectory(string envVar, string fallback, string subDirName)
202+
{
203+
var dir = Environment.GetEnvironmentVariable(envVar);
204+
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
205+
return Path.Combine(dir, subDirName);
206+
207+
return Path.Combine(fallback, subDirName);
208+
}
165209
}
166210
}

src/Native/MacOS.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void SetupApp(AppBuilder builder)
2929
else if (!path.Contains("/opt/homebrew/", StringComparison.Ordinal))
3030
path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path;
3131

32-
var customPathFile = Path.Combine(OS.DataDir, "PATH");
32+
var customPathFile = Path.Combine(OS.BasicDirectories.ConfigDir, "PATH");
3333
if (File.Exists(customPathFile))
3434
{
3535
var env = File.ReadAllText(customPathFile).Trim();
@@ -47,11 +47,19 @@ public void SetupWindow(Window window)
4747
window.BorderThickness = new Thickness(0);
4848
}
4949

50-
public string GetDataDir()
50+
public OS.Directories GetOrCreateDirectories()
5151
{
52-
return Path.Combine(
52+
var dirs = new OS.Directories();
53+
var appDataDir = Path.Combine(
5354
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
5455
"SourceGit");
56+
57+
if (!Directory.Exists(appDataDir))
58+
Directory.CreateDirectory(appDataDir);
59+
60+
dirs.ConfigDir = appDataDir;
61+
dirs.CacheDir = appDataDir;
62+
return dirs;
5563
}
5664

5765
public string FindGitExecutable()

src/Native/OS.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ namespace SourceGit.Native
1414
{
1515
public static partial class OS
1616
{
17+
public class Directories
18+
{
19+
public string ConfigDir { get; set; } = string.Empty;
20+
public string CacheDir { get; set; } = string.Empty;
21+
}
22+
1723
public interface IBackend
1824
{
1925
void SetupApp(AppBuilder builder);
2026
void SetupWindow(Window window);
2127

22-
string GetDataDir();
28+
Directories GetOrCreateDirectories();
2329
string FindGitExecutable();
2430
string FindTerminal(Models.ShellOrTerminal shell);
2531
List<Models.ExternalTool> FindExternalTools();
@@ -32,11 +38,11 @@ public interface IBackend
3238
void TerminateProcess(Process proc);
3339
}
3440

35-
public static string DataDir
41+
public static Directories BasicDirectories
3642
{
3743
get;
3844
private set;
39-
} = string.Empty;
45+
}
4046

4147
public static string GitExecutable
4248
{
@@ -135,11 +141,9 @@ static OS()
135141
throw new PlatformNotSupportedException();
136142
}
137143

138-
public static void SetupDataDir()
144+
public static void SetupBasicDirectories()
139145
{
140-
DataDir = _backend.GetDataDir();
141-
if (!Directory.Exists(DataDir))
142-
Directory.CreateDirectory(DataDir);
146+
BasicDirectories = _backend.GetOrCreateDirectories();
143147
}
144148

145149
public static void SetupApp(AppBuilder builder)
@@ -162,7 +166,7 @@ public static void LogException(Exception ex)
162166
if (ex == null)
163167
return;
164168

165-
var crashDir = Path.Combine(DataDir, "crashes");
169+
var crashDir = Path.Combine(BasicDirectories.CacheDir, "crashes");
166170
if (!Directory.Exists(crashDir))
167171
Directory.CreateDirectory(crashDir);
168172

src/Native/Windows.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,30 @@ public void SetupWindow(Window window)
5454
window.Padding = new Thickness(0);
5555
}
5656

57-
public string GetDataDir()
57+
public OS.Directories GetOrCreateDirectories()
5858
{
59+
var dirs = new OS.Directories();
5960
var execFile = Environment.ProcessPath;
6061
var portableDir = Path.Combine(Path.GetDirectoryName(execFile)!, "data");
6162
if (Directory.Exists(portableDir))
62-
return portableDir;
63+
{
64+
dirs.ConfigDir = portableDir;
65+
dirs.CacheDir = portableDir;
66+
}
67+
else
68+
{
69+
var appDataDir = Path.Combine(
70+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
71+
"SourceGit");
72+
73+
if (!Directory.Exists(appDataDir))
74+
Directory.CreateDirectory(appDataDir);
75+
76+
dirs.ConfigDir = appDataDir;
77+
dirs.CacheDir = appDataDir;
78+
}
6379

64-
return Path.Combine(
65-
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
66-
"SourceGit");
80+
return dirs;
6781
}
6882

6983
public string FindGitExecutable()

0 commit comments

Comments
 (0)