Skip to content

Commit 09c0ede

Browse files
massimopaganighmpagani
andauthored
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 <[email protected]>
1 parent 558eb7c commit 09c0ede

File tree

4 files changed

+157
-3
lines changed

4 files changed

+157
-3
lines changed

src/App.axaml.cs

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.IO;
5+
using System.IO.Pipes;
56
using System.Net.Http;
67
using System.Reflection;
78
using System.Text;
89
using System.Text.Json;
910
using System.Threading;
1011
using System.Threading.Tasks;
12+
using System.Linq;
1113

1214
using Avalonia;
1315
using Avalonia.Controls;
@@ -46,6 +48,8 @@ public static void Main(string[] args)
4648
Environment.Exit(exitTodo);
4749
else if (TryLaunchAsRebaseMessageEditor(args, out int exitMessage))
4850
Environment.Exit(exitMessage);
51+
else if (TrySendArgsToExistingInstance(args))
52+
Environment.Exit(0);
4953
else
5054
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
5155
}
@@ -77,6 +81,44 @@ public static AppBuilder BuildAvaloniaApp()
7781
return builder;
7882
}
7983

84+
private static bool TrySendArgsToExistingInstance(string[] args)
85+
{
86+
if (args == null || args.Length != 1 || !Directory.Exists(args[0]))
87+
return false;
88+
89+
var pref = ViewModels.Preferences.Instance;
90+
91+
if (!pref.OpenReposInNewTab)
92+
return false;
93+
94+
try
95+
{
96+
var processes = Process.GetProcessesByName("SourceGit");
97+
98+
if (processes.Length <= 1)
99+
return false;
100+
101+
using var client = new NamedPipeClientStream(".", "SourceGitIPC", PipeDirection.Out);
102+
103+
client.Connect(1000);
104+
105+
if (client.IsConnected)
106+
{
107+
using var writer = new StreamWriter(client);
108+
109+
writer.WriteLine(args[0]);
110+
writer.Flush();
111+
112+
return true;
113+
}
114+
}
115+
catch (Exception)
116+
{
117+
}
118+
119+
return false;
120+
}
121+
80122
private static void LogException(Exception ex)
81123
{
82124
if (ex == null)
@@ -328,7 +370,13 @@ public override void Initialize()
328370
AvaloniaXamlLoader.Load(this);
329371

330372
var pref = ViewModels.Preferences.Instance;
331-
pref.PropertyChanged += (_, _) => pref.Save();
373+
374+
pref.PropertyChanged += (s, e) => {
375+
pref.Save();
376+
377+
if (e.PropertyName.Equals(nameof(ViewModels.Preferences.OpenReposInNewTab)))
378+
HandleOpenReposInNewTabChanged();
379+
};
332380

333381
SetLocale(pref.Locale);
334382
SetTheme(pref.Theme, pref.ThemeOverrides);
@@ -488,13 +536,104 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop)
488536
_launcher = new ViewModels.Launcher(startupRepo);
489537
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
490538

491-
#if !DISABLE_UPDATE_DETECTION
492539
var pref = ViewModels.Preferences.Instance;
540+
541+
HandleOpenReposInNewTabChanged();
542+
543+
#if !DISABLE_UPDATE_DETECTION
493544
if (pref.ShouldCheck4UpdateOnStartup())
494545
Check4Update();
495546
#endif
496547
}
497548

549+
private void HandleOpenReposInNewTabChanged()
550+
{
551+
var pref = ViewModels.Preferences.Instance;
552+
553+
if (pref.OpenReposInNewTab)
554+
{
555+
if (_ipcServerTask == null || _ipcServerTask.IsCompleted)
556+
{
557+
// Start IPC server
558+
_ipcServerCts = new CancellationTokenSource();
559+
_ipcServerTask = Task.Run(() => StartIPCServer(_ipcServerCts.Token));
560+
}
561+
}
562+
else
563+
{
564+
// Stop IPC server if running
565+
if (_ipcServerCts != null && !_ipcServerCts.IsCancellationRequested)
566+
{
567+
_ipcServerCts.Cancel();
568+
_ipcServerCts.Dispose();
569+
_ipcServerCts = null;
570+
}
571+
_ipcServerTask = null;
572+
}
573+
}
574+
575+
private void StartIPCServer(CancellationToken cancellationToken)
576+
{
577+
try
578+
{
579+
while (!cancellationToken.IsCancellationRequested)
580+
{
581+
using var server = new NamedPipeServerStream("SourceGitIPC", PipeDirection.In);
582+
583+
// Use WaitForConnectionAsync with cancellation token
584+
try
585+
{
586+
Task connectionTask = server.WaitForConnectionAsync(cancellationToken);
587+
connectionTask.Wait(cancellationToken);
588+
}
589+
catch (OperationCanceledException)
590+
{
591+
return;
592+
}
593+
catch (AggregateException ae) when (ae.InnerExceptions.Any(e => e is OperationCanceledException))
594+
{
595+
return;
596+
}
597+
598+
// Process the connection
599+
using var reader = new StreamReader(server);
600+
var repoPath = reader.ReadLine();
601+
602+
if (!string.IsNullOrEmpty(repoPath) && Directory.Exists(repoPath))
603+
{
604+
Dispatcher.UIThread.Post(() =>
605+
{
606+
try
607+
{
608+
var test = new Commands.QueryRepositoryRootPath(repoPath).ReadToEnd();
609+
610+
if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut))
611+
{
612+
var repoRootPath = test.StdOut.Trim();
613+
var pref = ViewModels.Preferences.Instance;
614+
var node = pref.FindOrAddNodeByRepositoryPath(repoRootPath, null, false);
615+
616+
ViewModels.Welcome.Instance.Refresh();
617+
618+
_launcher?.OpenRepositoryInTab(node, null);
619+
620+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null)
621+
desktop.MainWindow.Activate();
622+
}
623+
}
624+
catch (Exception)
625+
{
626+
}
627+
});
628+
}
629+
}
630+
}
631+
catch (Exception)
632+
{
633+
// Pipe server failed, we can just exit the thread
634+
}
635+
}
636+
498637
private void Check4Update(bool manually = false)
499638
{
500639
Task.Run(async () =>
@@ -584,5 +723,7 @@ private string FixFontFamilyName(string input)
584723
private ResourceDictionary _activeLocale = null;
585724
private ResourceDictionary _themeOverrides = null;
586725
private ResourceDictionary _fontsOverrides = null;
726+
private Task _ipcServerTask = null;
727+
private CancellationTokenSource _ipcServerCts = null;
587728
}
588729
}

src/Resources/Locales/en_US.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,4 +752,5 @@
752752
<x:String x:Key="Text.Worktree.Lock" xml:space="preserve">Lock</x:String>
753753
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
754754
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
755+
<x:String x:Key="Text.Preferences.General.OpenReposInNewTab" xml:space="preserve">Open repositories in new tab instead of new window</x:String>
755756
</ResourceDictionary>

src/ViewModels/Preferences.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ public bool OnlyUseMonoFontInEditor
9090
}
9191
}
9292

93+
public bool OpenReposInNewTab
94+
{
95+
get => _openReposInNewTab;
96+
set => SetProperty(ref _openReposInNewTab, value);
97+
}
98+
9399
public bool UseSystemWindowFrame
94100
{
95101
get => _useSystemWindowFrame;
@@ -632,6 +638,7 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)
632638
private string _defaultFontFamily = string.Empty;
633639
private string _monospaceFontFamily = string.Empty;
634640
private bool _onlyUseMonoFontInEditor = false;
641+
private bool _openReposInNewTab = false;
635642
private bool _useSystemWindowFrame = false;
636643
private double _defaultFontSize = 13;
637644
private double _editorFontSize = 13;

src/Views/Preferences.axaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<TabItem.Header>
4747
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preferences.General}"/>
4848
</TabItem.Header>
49-
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
49+
<Grid Margin="8" RowDefinitions="32,32,32,32,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
5050
<TextBlock Grid.Row="0" Grid.Column="0"
5151
Text="{DynamicResource Text.Preferences.General.Locale}"
5252
HorizontalAlignment="Right"
@@ -142,6 +142,11 @@
142142
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=ShowChildren, Mode=TwoWay}"/>
143143

144144
<CheckBox Grid.Row="8" Grid.Column="1"
145+
Height="32"
146+
Content="{DynamicResource Text.Preferences.General.OpenReposInNewTab}"
147+
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=OpenReposInNewTab, Mode=TwoWay}"/>
148+
149+
<CheckBox Grid.Row="9" Grid.Column="1"
145150
Height="32"
146151
Content="{DynamicResource Text.Preferences.General.Check4UpdatesOnStartup}"
147152
IsVisible="{x:Static s:App.IsCheckForUpdateCommandVisible}"

0 commit comments

Comments
 (0)