From 09c0edef8e97f00e3dc479a2abd02ca416ac9d78 Mon Sep 17 00:00:00 2001 From: Massimo Date: Mon, 14 Apr 2025 13:16:15 +0200 Subject: [PATCH] feat: implement IPC for opening repositories in new tabs (#1185) * refactor: improve diff handling for EOL changes and enhance text diff display - Updated `Diff.cs` to streamline whitespace handling in diff arguments. - Enhanced `DiffContext.cs` to check for EOL changes when old and new hashes differ, creating a text diff if necessary. - Added support for showing end-of-line symbols in `TextDiffView.axaml.cs` options. * localization: update translations to include EOF handling in ignore whitespace messages - Modified the ignore whitespace text in multiple language files to specify that EOF changes are also ignored. - Ensured consistency across all localization files for the patch application feature. * revert: Typo in DiffResult comment * revert: update diff arguments to ignore CR at EOL in whitespace handling (like before changes) * revert: update translations to remove EOF references in Text.Apply.IgnoreWS and fixed typo in Text.Diff.IgnoreWhitespace (EOF => EOL) * feat: add workspace-specific default clone directory functionality - Implemented logic in Clone.cs to set ParentFolder based on the active workspace's DefaultCloneDir if available, falling back to the global GitDefaultCloneDir. - Added DefaultCloneDir property to Workspace.cs to store the default clone directory for each workspace. - Updated ConfigureWorkspace.axaml to include a TextBox and Button for setting the DefaultCloneDir in the UI. - Implemented folder selection functionality in ConfigureWorkspace.axaml.cs to allow users to choose a directory for cloning. - This closes issue #1145 * feat: implement IPC for opening repositories in new tabs - Added functionality to send repository paths to an existing instance of the application using named pipes. - Introduced a new preference option to open repositories in a new tab instead of a new window. - Updated UI to include a checkbox for the new preference. - Enhanced the handling of IPC server lifecycle based on the new preference setting. - This closes issue #1184 --------- Co-authored-by: mpagani --- src/App.axaml.cs | 145 +++++++++++++++++++++++++++++- src/Resources/Locales/en_US.axaml | 1 + src/ViewModels/Preferences.cs | 7 ++ src/Views/Preferences.axaml | 7 +- 4 files changed, 157 insertions(+), 3 deletions(-) diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 0448a247..b1be7072 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -2,12 +2,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.IO.Pipes; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Linq; using Avalonia; using Avalonia.Controls; @@ -46,6 +48,8 @@ namespace SourceGit Environment.Exit(exitTodo); else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage)) Environment.Exit(exitMessage); + else if (TrySendArgsToExistingInstance(args)) + Environment.Exit(0); else BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } @@ -77,6 +81,44 @@ namespace SourceGit return builder; } + private static bool TrySendArgsToExistingInstance(string[] args) + { + if (args == null || args.Length != 1 || !Directory.Exists(args[0])) + return false; + + var pref = ViewModels.Preferences.Instance; + + if (!pref.OpenReposInNewTab) + return false; + + try + { + var processes = Process.GetProcessesByName("SourceGit"); + + if (processes.Length <= 1) + return false; + + using var client = new NamedPipeClientStream(".", "SourceGitIPC", PipeDirection.Out); + + client.Connect(1000); + + if (client.IsConnected) + { + using var writer = new StreamWriter(client); + + writer.WriteLine(args[0]); + writer.Flush(); + + return true; + } + } + catch (Exception) + { + } + + return false; + } + private static void LogException(Exception ex) { if (ex == null) @@ -328,7 +370,13 @@ namespace SourceGit AvaloniaXamlLoader.Load(this); var pref = ViewModels.Preferences.Instance; - pref.PropertyChanged += (_, _) => pref.Save(); + + pref.PropertyChanged += (s, e) => { + pref.Save(); + + if (e.PropertyName.Equals(nameof(ViewModels.Preferences.OpenReposInNewTab))) + HandleOpenReposInNewTabChanged(); + }; SetLocale(pref.Locale); SetTheme(pref.Theme, pref.ThemeOverrides); @@ -488,13 +536,104 @@ namespace SourceGit _launcher = new ViewModels.Launcher(startupRepo); desktop.MainWindow = new Views.Launcher() { DataContext = _launcher }; -#if !DISABLE_UPDATE_DETECTION var pref = ViewModels.Preferences.Instance; + + HandleOpenReposInNewTabChanged(); + +#if !DISABLE_UPDATE_DETECTION if (pref.ShouldCheck4UpdateOnStartup()) Check4Update(); #endif } + private void HandleOpenReposInNewTabChanged() + { + var pref = ViewModels.Preferences.Instance; + + if (pref.OpenReposInNewTab) + { + if (_ipcServerTask == null || _ipcServerTask.IsCompleted) + { + // Start IPC server + _ipcServerCts = new CancellationTokenSource(); + _ipcServerTask = Task.Run(() => StartIPCServer(_ipcServerCts.Token)); + } + } + else + { + // Stop IPC server if running + if (_ipcServerCts != null && !_ipcServerCts.IsCancellationRequested) + { + _ipcServerCts.Cancel(); + _ipcServerCts.Dispose(); + _ipcServerCts = null; + } + _ipcServerTask = null; + } + } + + private void StartIPCServer(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + using var server = new NamedPipeServerStream("SourceGitIPC", PipeDirection.In); + + // Use WaitForConnectionAsync with cancellation token + try + { + Task connectionTask = server.WaitForConnectionAsync(cancellationToken); + connectionTask.Wait(cancellationToken); + } + catch (OperationCanceledException) + { + return; + } + catch (AggregateException ae) when (ae.InnerExceptions.Any(e => e is OperationCanceledException)) + { + return; + } + + // Process the connection + using var reader = new StreamReader(server); + var repoPath = reader.ReadLine(); + + if (!string.IsNullOrEmpty(repoPath) && Directory.Exists(repoPath)) + { + Dispatcher.UIThread.Post(() => + { + try + { + var test = new Commands.QueryRepositoryRootPath(repoPath).ReadToEnd(); + + if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) + { + var repoRootPath = test.StdOut.Trim(); + var pref = ViewModels.Preferences.Instance; + var node = pref.FindOrAddNodeByRepositoryPath(repoRootPath, null, false); + + ViewModels.Welcome.Instance.Refresh(); + + _launcher?.OpenRepositoryInTab(node, null); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) + desktop.MainWindow.Activate(); + } + } + catch (Exception) + { + } + }); + } + } + } + catch (Exception) + { + // Pipe server failed, we can just exit the thread + } + } + private void Check4Update(bool manually = false) { Task.Run(async () => @@ -584,5 +723,7 @@ namespace SourceGit private ResourceDictionary _activeLocale = null; private ResourceDictionary _themeOverrides = null; private ResourceDictionary _fontsOverrides = null; + private Task _ipcServerTask = null; + private CancellationTokenSource _ipcServerCts = null; } } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 7e931998..123b6381 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -752,4 +752,5 @@ Lock Remove Unlock + Open repositories in new tab instead of new window diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index cad6c224..d1e78d6a 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -90,6 +90,12 @@ namespace SourceGit.ViewModels } } + public bool OpenReposInNewTab + { + get => _openReposInNewTab; + set => SetProperty(ref _openReposInNewTab, value); + } + public bool UseSystemWindowFrame { get => _useSystemWindowFrame; @@ -632,6 +638,7 @@ namespace SourceGit.ViewModels private string _defaultFontFamily = string.Empty; private string _monospaceFontFamily = string.Empty; private bool _onlyUseMonoFontInEditor = false; + private bool _openReposInNewTab = false; private bool _useSystemWindowFrame = false; private double _defaultFontSize = 13; private double _editorFontSize = 13; diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index 702ec20f..704757df 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -46,7 +46,7 @@ - + + +