feature: Find external terminals.

This commit is contained in:
walterlv 2024-04-08 12:03:55 +08:00
parent 718788e07e
commit 421127bec9
8 changed files with 264 additions and 35 deletions

View 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,
});
}
}
}

View file

@ -31,6 +31,15 @@ namespace SourceGit.Native
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()
{
var finder = new Models.ExternalEditorFinder();

View file

@ -28,6 +28,13 @@ namespace SourceGit.Native
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()
{
var finder = new Models.ExternalEditorFinder();

View file

@ -12,6 +12,8 @@ namespace SourceGit.Native
void SetupApp(AppBuilder builder);
string FindGitExecutable();
List<Models.ExternalTerminal> FindExternalTerminals();
List<Models.ExternalEditor> FindExternalEditors();
void OpenTerminal(string workdir);
@ -21,6 +23,7 @@ namespace SourceGit.Native
}
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>();
static OS()
@ -42,6 +45,7 @@ namespace SourceGit.Native
throw new Exception("Platform unsupported!!!");
}
ExternalTerminals = _backend.FindExternalTerminals();
ExternalEditors = _backend.FindExternalEditors();
}

View file

@ -114,6 +114,15 @@ namespace SourceGit.Native
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()
{
var finder = new Models.ExternalEditorFinder();
@ -130,6 +139,26 @@ namespace SourceGit.Native
info.CreateNoWindow = true;
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)
{

View file

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Collections;
using Avalonia.Controls;
@ -12,6 +14,9 @@ using Avalonia.Platform;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SourceGit.Commands;
namespace SourceGit.ViewModels
{
@ -89,6 +94,16 @@ namespace SourceGit.ViewModels
get => _selectedView;
set => SetProperty(ref _selectedView, value);
}
public AvaloniaList<ExternalMenuItem> ExternalTerminals
{
get;
} = new AvaloniaList<ExternalMenuItem>();
public AvaloniaList<ExternalMenuItem> ExternalEditors
{
get;
} = new AvaloniaList<ExternalMenuItem>();
[JsonIgnore]
public List<Models.Remote> Remotes
@ -244,6 +259,9 @@ namespace SourceGit.ViewModels
Task.Run(RefreshWorkingCopyChanges);
Task.Run(RefreshStashes);
Task.Run(RefreshGitFlow);
RefreshExternalTerminals();
RefreshExternalEditors();
}
public void Close()
@ -287,36 +305,77 @@ namespace SourceGit.ViewModels
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;
if (editors.Count == 0)
{
App.RaiseException(_fullpath, "No available external editors found!");
return null;
return [new ExternalMenuItem("No editor found")
{
IsEnabled = false,
}];
}
var menu = new ContextMenu();
menu.Placement = PlacementMode.BottomEdgeAlignedLeft;
RenderOptions.SetBitmapInterpolationMode(menu, BitmapInterpolationMode.HighQuality);
var items = new List<ExternalMenuItem>(editors.Count);
foreach (var editor in editors)
{
var dupEditor = editor;
var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/ExternalToolIcons/{dupEditor.Icon}", UriKind.RelativeOrAbsolute));
var item = new MenuItem();
item.Header = App.Text("Repository.OpenIn", dupEditor.Name);
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);
var item = new ExternalMenuItem(App.Text("Repository.OpenIn", dupEditor.Name), new Bitmap(icon), () => dupEditor.Open(_fullpath));
items.Add(item);
}
return menu;
return [..items];
}
public void Fetch()
@ -1356,4 +1415,44 @@ namespace SourceGit.ViewModels
private InProgressContext _inProgressContext = null;
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; }
}
}

View file

@ -12,12 +12,8 @@
<UserControl.Resources>
<MenuFlyout x:Key="TerminalShellsMenuFlyout" Placement="Bottom">
<MenuItem Header="git bash" />
<MenuItem Header="PowerShell" />
</MenuFlyout>
<MenuFlyout x:Key="ExternalToolsMenuFlyout" Placement="Bottom">
<MenuItem Header="Visual Studio Code" />
<MenuItem Header="Sublime Text" />
</MenuFlyout>
</UserControl.Resources>
@ -36,7 +32,7 @@
<Path Width="13" Height="13" Data="{StaticResource Icons.Terminal}"/>
</SplitButton>
<SplitButton Classes="icon_button" Click="OnOpenWithExternalEditor"
<SplitButton Classes="icon_button" Command="{Binding OpenWithExternalTool}"
Flyout="{StaticResource ExternalToolsMenuFlyout}"
ToolTip.Tip="{DynamicResource Text.Repository.OpenWithExternalTools}">
<Path Width="13" Height="13" Data="{StaticResource Icons.OpenWith}"/>

View file

@ -61,19 +61,6 @@ namespace SourceGit.Views
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)
{
if (sender is TreeView tree)