mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-06-26 12:55:00 +00:00
feature: Find external terminals.
This commit is contained in:
parent
718788e07e
commit
421127bec9
8 changed files with 264 additions and 35 deletions
98
src/Models/ExternalTerminal.cs
Normal file
98
src/Models/ExternalTerminal.cs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace SourceGit.Models
|
||||||
|
{
|
||||||
|
public class ExternalTerminal
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Icon { get; set; } = string.Empty;
|
||||||
|
public string Executable { get; set; } = string.Empty;
|
||||||
|
public string OpenCmdArgs { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public void Open(string repo)
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
WorkingDirectory = repo,
|
||||||
|
FileName = Executable,
|
||||||
|
Arguments = string.Format(OpenCmdArgs, repo),
|
||||||
|
UseShellExecute = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ExternalTerminalFinder
|
||||||
|
{
|
||||||
|
public List<ExternalTerminal> Terminals
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
} = new List<ExternalTerminal>();
|
||||||
|
|
||||||
|
public void WindowsGitBash(Func<string> platform_finder)
|
||||||
|
{
|
||||||
|
TryAdd("Git Bash", "git-bash.png", "bash", "\"{0}\"", platform_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Gnome(Func<string> platform_finder)
|
||||||
|
{
|
||||||
|
TryAdd("gnome-terminal", "gnome.png", "/usr/bin/gnome-terminal", "--working-directory=\"{0}\"", platform_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Konsole(Func<string> platform_finder)
|
||||||
|
{
|
||||||
|
TryAdd("gnome-terminal", "gnome.png", "/usr/bin/konsole", "--workdir \"{0}\"", platform_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void osaScript(Func<string> platform_finder)
|
||||||
|
{
|
||||||
|
TryAdd("AppleScript", "osascript.png", "/usr/bin/osascript",
|
||||||
|
"""
|
||||||
|
on run argv
|
||||||
|
tell application "Terminal"
|
||||||
|
do script "cd '{0}'"
|
||||||
|
activate
|
||||||
|
end tell
|
||||||
|
end run
|
||||||
|
""",
|
||||||
|
platform_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PowerShell(Func<string> platform_finder)
|
||||||
|
{
|
||||||
|
TryAdd("PowerShell", "pwsh.png", "pwsh", "-WorkingDirectory \"{0}\"", platform_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WindowsTerminal(Func<string> platform_finder)
|
||||||
|
{
|
||||||
|
TryAdd("Windows Terminal", "wt.png", "wt", "-d \"{0}\"", platform_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Xfce4(Func<string> platform_finder)
|
||||||
|
{
|
||||||
|
TryAdd("gnome-terminal", "xfce4.png", "/usr/bin/xfce4-terminal", "--working-directory=\"{0}\"", platform_finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryAdd(string name, string icon, string cmd, string args, Func<string> finder)
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable(cmd);
|
||||||
|
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||||
|
{
|
||||||
|
path = finder();
|
||||||
|
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Terminals.Add(new ExternalTerminal
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Icon = icon,
|
||||||
|
OpenCmdArgs = args,
|
||||||
|
Executable = path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,15 @@ namespace SourceGit.Native
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Models.ExternalTerminal> FindExternalTerminals()
|
||||||
|
{
|
||||||
|
var finder = new Models.ExternalTerminalFinder();
|
||||||
|
finder.Gnome(() => "/usr/bin/gnome-terminal");
|
||||||
|
finder.Konsole(() => "/usr/bin/konsole");
|
||||||
|
finder.Xfce4(() => "/usr/bin/xfce4-terminal");
|
||||||
|
return finder.Terminals;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Models.ExternalEditor> FindExternalEditors()
|
public List<Models.ExternalEditor> FindExternalEditors()
|
||||||
{
|
{
|
||||||
var finder = new Models.ExternalEditorFinder();
|
var finder = new Models.ExternalEditorFinder();
|
||||||
|
|
|
@ -28,6 +28,13 @@ namespace SourceGit.Native
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Models.ExternalTerminal> FindExternalTerminals()
|
||||||
|
{
|
||||||
|
var finder = new Models.ExternalTerminalFinder();
|
||||||
|
finder.osaScript(() => "/usr/bin/osascript");
|
||||||
|
return finder.Terminals;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Models.ExternalEditor> FindExternalEditors()
|
public List<Models.ExternalEditor> FindExternalEditors()
|
||||||
{
|
{
|
||||||
var finder = new Models.ExternalEditorFinder();
|
var finder = new Models.ExternalEditorFinder();
|
||||||
|
|
|
@ -12,6 +12,8 @@ namespace SourceGit.Native
|
||||||
void SetupApp(AppBuilder builder);
|
void SetupApp(AppBuilder builder);
|
||||||
|
|
||||||
string FindGitExecutable();
|
string FindGitExecutable();
|
||||||
|
|
||||||
|
List<Models.ExternalTerminal> FindExternalTerminals();
|
||||||
List<Models.ExternalEditor> FindExternalEditors();
|
List<Models.ExternalEditor> FindExternalEditors();
|
||||||
|
|
||||||
void OpenTerminal(string workdir);
|
void OpenTerminal(string workdir);
|
||||||
|
@ -21,6 +23,7 @@ namespace SourceGit.Native
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GitExecutable { get; set; } = string.Empty;
|
public static string GitExecutable { get; set; } = string.Empty;
|
||||||
|
public static List<Models.ExternalTerminal> ExternalTerminals { get; set; } = new List<Models.ExternalTerminal>();
|
||||||
public static List<Models.ExternalEditor> ExternalEditors { get; set; } = new List<Models.ExternalEditor>();
|
public static List<Models.ExternalEditor> ExternalEditors { get; set; } = new List<Models.ExternalEditor>();
|
||||||
|
|
||||||
static OS()
|
static OS()
|
||||||
|
@ -42,6 +45,7 @@ namespace SourceGit.Native
|
||||||
throw new Exception("Platform unsupported!!!");
|
throw new Exception("Platform unsupported!!!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExternalTerminals = _backend.FindExternalTerminals();
|
||||||
ExternalEditors = _backend.FindExternalEditors();
|
ExternalEditors = _backend.FindExternalEditors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,15 @@ namespace SourceGit.Native
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Models.ExternalTerminal> FindExternalTerminals()
|
||||||
|
{
|
||||||
|
var finder = new Models.ExternalTerminalFinder();
|
||||||
|
finder.WindowsGitBash(() => FindExternalTerminal("bash"));
|
||||||
|
finder.PowerShell(() => FindExternalTerminal("pwsh"));
|
||||||
|
finder.WindowsTerminal(() => FindExternalTerminal("wt"));
|
||||||
|
return finder.Terminals;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Models.ExternalEditor> FindExternalEditors()
|
public List<Models.ExternalEditor> FindExternalEditors()
|
||||||
{
|
{
|
||||||
var finder = new Models.ExternalEditorFinder();
|
var finder = new Models.ExternalEditorFinder();
|
||||||
|
@ -131,6 +140,26 @@ namespace SourceGit.Native
|
||||||
Process.Start(info);
|
Process.Start(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find the external terminal full path via the command name (e.g. "bash").
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cmd"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string FindExternalTerminal(string cmd)
|
||||||
|
{
|
||||||
|
using var process = Process.Start(new ProcessStartInfo(
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "where.exe"),
|
||||||
|
cmd)
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
})!;
|
||||||
|
var fullPath = process.StandardOutput.ReadToEnd();
|
||||||
|
process.WaitForExit();
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
public void OpenTerminal(string workdir)
|
public void OpenTerminal(string workdir)
|
||||||
{
|
{
|
||||||
var binDir = Path.GetDirectoryName(OS.GitExecutable);
|
var binDir = Path.GetDirectoryName(OS.GitExecutable);
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
@ -12,6 +14,9 @@ using Avalonia.Platform;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
using SourceGit.Commands;
|
||||||
|
|
||||||
namespace SourceGit.ViewModels
|
namespace SourceGit.ViewModels
|
||||||
{
|
{
|
||||||
|
@ -90,6 +95,16 @@ namespace SourceGit.ViewModels
|
||||||
set => SetProperty(ref _selectedView, value);
|
set => SetProperty(ref _selectedView, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AvaloniaList<ExternalMenuItem> ExternalTerminals
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
} = new AvaloniaList<ExternalMenuItem>();
|
||||||
|
|
||||||
|
public AvaloniaList<ExternalMenuItem> ExternalEditors
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
} = new AvaloniaList<ExternalMenuItem>();
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public List<Models.Remote> Remotes
|
public List<Models.Remote> Remotes
|
||||||
{
|
{
|
||||||
|
@ -244,6 +259,9 @@ namespace SourceGit.ViewModels
|
||||||
Task.Run(RefreshWorkingCopyChanges);
|
Task.Run(RefreshWorkingCopyChanges);
|
||||||
Task.Run(RefreshStashes);
|
Task.Run(RefreshStashes);
|
||||||
Task.Run(RefreshGitFlow);
|
Task.Run(RefreshGitFlow);
|
||||||
|
|
||||||
|
RefreshExternalTerminals();
|
||||||
|
RefreshExternalEditors();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
|
@ -287,36 +305,77 @@ namespace SourceGit.ViewModels
|
||||||
Native.OS.OpenTerminal(_fullpath);
|
Native.OS.OpenTerminal(_fullpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContextMenu CreateContextMenuForExternalEditors()
|
public void OpenWithExternalTool()
|
||||||
|
{
|
||||||
|
Native.OS.OpenWithDefaultEditor(_fullpath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshExternalTerminals()
|
||||||
|
{
|
||||||
|
ExternalTerminals.Clear();
|
||||||
|
var terminals = CreateContextMenuForExternalTerminals();
|
||||||
|
foreach (var terminal in terminals)
|
||||||
|
{
|
||||||
|
ExternalTerminals.Add(terminal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshExternalEditors()
|
||||||
|
{
|
||||||
|
ExternalEditors.Clear();
|
||||||
|
var editors = CreateContextMenuForExternalEditors();
|
||||||
|
foreach (var editor in editors)
|
||||||
|
{
|
||||||
|
ExternalEditors.Add(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableArray<ExternalMenuItem> CreateContextMenuForExternalTerminals()
|
||||||
|
{
|
||||||
|
var terminals = Native.OS.ExternalTerminals;
|
||||||
|
if (terminals.Count == 0)
|
||||||
|
{
|
||||||
|
App.RaiseException(_fullpath, "No available external terminals found!");
|
||||||
|
return [new ExternalMenuItem("No terminal found")
|
||||||
|
{
|
||||||
|
IsEnabled = false,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<ExternalMenuItem>(terminals.Count);
|
||||||
|
foreach (var terminal in terminals)
|
||||||
|
{
|
||||||
|
var dupTerminal = terminal;
|
||||||
|
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalTerminalIcons/{dupTerminal.Icon}", UriKind.RelativeOrAbsolute));
|
||||||
|
var item = new ExternalMenuItem(App.Text("Repository.OpenIn", dupTerminal.Name), new Bitmap(icon), () => dupTerminal.Open(_fullpath));
|
||||||
|
items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [..items];
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableArray<ExternalMenuItem> CreateContextMenuForExternalEditors()
|
||||||
{
|
{
|
||||||
var editors = Native.OS.ExternalEditors;
|
var editors = Native.OS.ExternalEditors;
|
||||||
if (editors.Count == 0)
|
if (editors.Count == 0)
|
||||||
{
|
{
|
||||||
App.RaiseException(_fullpath, "No available external editors found!");
|
App.RaiseException(_fullpath, "No available external editors found!");
|
||||||
return null;
|
return [new ExternalMenuItem("No editor found")
|
||||||
|
{
|
||||||
|
IsEnabled = false,
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
var menu = new ContextMenu();
|
var items = new List<ExternalMenuItem>(editors.Count);
|
||||||
menu.Placement = PlacementMode.BottomEdgeAlignedLeft;
|
|
||||||
RenderOptions.SetBitmapInterpolationMode(menu, BitmapInterpolationMode.HighQuality);
|
|
||||||
|
|
||||||
foreach (var editor in editors)
|
foreach (var editor in editors)
|
||||||
{
|
{
|
||||||
var dupEditor = editor;
|
var dupEditor = editor;
|
||||||
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalToolIcons/{dupEditor.Icon}", UriKind.RelativeOrAbsolute));
|
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalToolIcons/{dupEditor.Icon}", UriKind.RelativeOrAbsolute));
|
||||||
var item = new MenuItem();
|
var item = new ExternalMenuItem(App.Text("Repository.OpenIn", dupEditor.Name), new Bitmap(icon), () => dupEditor.Open(_fullpath));
|
||||||
item.Header = App.Text("Repository.OpenIn", dupEditor.Name);
|
items.Add(item);
|
||||||
item.Icon = new Image { Width = 16, Height = 16, Source = new Bitmap(icon) };
|
|
||||||
item.Click += (o, e) =>
|
|
||||||
{
|
|
||||||
dupEditor.Open(_fullpath);
|
|
||||||
e.Handled = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
menu.Items.Add(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return menu;
|
return [..items];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Fetch()
|
public void Fetch()
|
||||||
|
@ -1356,4 +1415,44 @@ namespace SourceGit.ViewModels
|
||||||
private InProgressContext _inProgressContext = null;
|
private InProgressContext _inProgressContext = null;
|
||||||
private bool _hasUnsolvedConflicts = false;
|
private bool _hasUnsolvedConflicts = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A menu item for external tools.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct ExternalMenuItem
|
||||||
|
{
|
||||||
|
public ExternalMenuItem(string header)
|
||||||
|
{
|
||||||
|
Header = header;
|
||||||
|
Icon = null;
|
||||||
|
Command = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExternalMenuItem(string header, Bitmap icon, Action click)
|
||||||
|
{
|
||||||
|
Header = header;
|
||||||
|
Icon = icon;
|
||||||
|
Command = new RelayCommand(click);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The external tool name.
|
||||||
|
/// </summary>
|
||||||
|
public string Header { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The resource key of the icon.
|
||||||
|
/// </summary>
|
||||||
|
public Bitmap? Icon { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The command when the user click the menu item.
|
||||||
|
/// </summary>
|
||||||
|
public ICommand Command { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see langword="true"/> if the menu item is enabled; otherwise, <see langword="false"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEnabled { get; init; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,8 @@
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<MenuFlyout x:Key="TerminalShellsMenuFlyout" Placement="Bottom">
|
<MenuFlyout x:Key="TerminalShellsMenuFlyout" Placement="Bottom">
|
||||||
<MenuItem Header="git bash" />
|
|
||||||
<MenuItem Header="PowerShell" />
|
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
<MenuFlyout x:Key="ExternalToolsMenuFlyout" Placement="Bottom">
|
<MenuFlyout x:Key="ExternalToolsMenuFlyout" Placement="Bottom">
|
||||||
<MenuItem Header="Visual Studio Code" />
|
|
||||||
<MenuItem Header="Sublime Text" />
|
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
@ -36,7 +32,7 @@
|
||||||
<Path Width="13" Height="13" Data="{StaticResource Icons.Terminal}"/>
|
<Path Width="13" Height="13" Data="{StaticResource Icons.Terminal}"/>
|
||||||
</SplitButton>
|
</SplitButton>
|
||||||
|
|
||||||
<SplitButton Classes="icon_button" Click="OnOpenWithExternalEditor"
|
<SplitButton Classes="icon_button" Command="{Binding OpenWithExternalTool}"
|
||||||
Flyout="{StaticResource ExternalToolsMenuFlyout}"
|
Flyout="{StaticResource ExternalToolsMenuFlyout}"
|
||||||
ToolTip.Tip="{DynamicResource Text.Repository.OpenWithExternalTools}">
|
ToolTip.Tip="{DynamicResource Text.Repository.OpenWithExternalTools}">
|
||||||
<Path Width="13" Height="13" Data="{StaticResource Icons.OpenWith}"/>
|
<Path Width="13" Height="13" Data="{StaticResource Icons.OpenWith}"/>
|
||||||
|
|
|
@ -61,19 +61,6 @@ namespace SourceGit.Views
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOpenWithExternalEditor(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is Button button && DataContext is ViewModels.Repository repo)
|
|
||||||
{
|
|
||||||
var menu = repo.CreateContextMenuForExternalEditors();
|
|
||||||
if (menu != null)
|
|
||||||
{
|
|
||||||
menu.Open(button);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLocalBranchTreeLostFocus(object sender, RoutedEventArgs e)
|
private void OnLocalBranchTreeLostFocus(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is TreeView tree)
|
if (sender is TreeView tree)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue