Skip to content
100 changes: 100 additions & 0 deletions src/Models/ExternalTerminal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace SourceGit.Models
{
public record ExternalTerminal
{
public string Name { get; init; } = string.Empty;
public string Icon { get; init; } = string.Empty;
public string Executable { get; init; } = string.Empty;
public string OpenCmdArgs { get; init; } = string.Empty;

public virtual 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", "", platform_finder);
}

public void Gnome(Func<string> platform_finder)
{
TryAdd("gnome-terminal", "gnome.png", "gnome", "--working-directory=\"{0}\"", platform_finder);
}

public void Konsole(Func<string> platform_finder)
{
TryAdd("konsole", "konsole.png", "konsole", "--workdir \"{0}\"", platform_finder);
}

public void AppleScript(ExternalTerminal terminal)
{
var path = terminal.Executable;

if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return;
}

Terminals.Add(terminal with
{
Name = "osascript",
Icon = "osascript.png",
});
}

public void PowerShell(Func<string> platform_finder)
{
TryAdd("pwsh", "pwsh.png", "pwsh", "-WorkingDirectory \"{0}\"", platform_finder);
}

public void WindowsTerminal(Func<string> platform_finder)
{
TryAdd("wt", "wt.png", "wt", "-d \"{0}\"", platform_finder);
}

public void Xfce4(Func<string> platform_finder)
{
TryAdd("xfce4", "xfce4.png", "xfce4", "--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,
});
}
}
}
9 changes: 9 additions & 0 deletions src/Native/Linux.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ public string FindGitExecutable()
return string.Empty;
}

public IReadOnlyList<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();
Expand Down
49 changes: 35 additions & 14 deletions src/Native/MacOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ public string FindGitExecutable()
return string.Empty;
}

public IReadOnlyList<Models.ExternalTerminal> FindExternalTerminals()
{
var finder = new Models.ExternalTerminalFinder();
finder.AppleScript(new AppleScriptTerminal());
return finder.Terminals;
}

public List<Models.ExternalEditor> FindExternalEditors()
{
var finder = new Models.ExternalEditorFinder();
Expand Down Expand Up @@ -57,25 +64,39 @@ public void OpenInFileManager(string path, bool select)

public void OpenTerminal(string workdir)
{
var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir;
var builder = new StringBuilder();
builder.AppendLine("on run argv");
builder.AppendLine(" tell application \"Terminal\"");
builder.AppendLine($" do script \"cd '{dir}'\"");
builder.AppendLine(" activate");
builder.AppendLine(" end tell");
builder.AppendLine("end run");

var tmp = Path.GetTempFileName();
File.WriteAllText(tmp, builder.ToString());

var proc = Process.Start("/usr/bin/osascript", $"\"{tmp}\"");
proc.Exited += (o, e) => File.Delete(tmp);
new AppleScriptTerminal().Open(workdir);
}

public void OpenWithDefaultEditor(string file)
{
Process.Start("open", file);
}

private sealed record AppleScriptTerminal : Models.ExternalTerminal
{
public AppleScriptTerminal()
{
Executable = "/usr/bin/osascript";
OpenCmdArgs = "";
}

public override void Open(string repo)
{
var dir = string.IsNullOrEmpty(repo) ? "~" : repo;
var tmp = Path.GetTempFileName();
File.WriteAllText(tmp,
$"""
on run argv
tell application "Terminal"
do script "cd '{dir}'"
activate
end tell
end run
""");

var proc = Process.Start("/usr/bin/osascript", $"\"{tmp}\"");
proc.Exited += (o, e) => File.Delete(tmp);
}
}
}
}
4 changes: 4 additions & 0 deletions src/Native/OS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface IBackend
void SetupApp(AppBuilder builder);

string FindGitExecutable();

IReadOnlyList<Models.ExternalTerminal> FindExternalTerminals();
List<Models.ExternalEditor> FindExternalEditors();

void OpenTerminal(string workdir);
Expand All @@ -21,6 +23,7 @@ public interface IBackend
}

public static string GitExecutable { get; set; } = string.Empty;
public static IReadOnlyList<Models.ExternalTerminal> ExternalTerminals { get; private set; }
public static List<Models.ExternalEditor> ExternalEditors { get; set; } = new List<Models.ExternalEditor>();

static OS()
Expand All @@ -42,6 +45,7 @@ static OS()
throw new Exception("Platform unsupported!!!");
}

ExternalTerminals = _backend.FindExternalTerminals();
ExternalEditors = _backend.FindExternalEditors();
}

Expand Down
29 changes: 29 additions & 0 deletions src/Native/Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ public string FindGitExecutable()
return null;
}

public IReadOnlyList<Models.ExternalTerminal> FindExternalTerminals()
{
var finder = new Models.ExternalTerminalFinder();
finder.WindowsGitBash(() => FindExternalTerminal("bash"));
finder.WindowsTerminal(() => FindExternalTerminal("wt"));
finder.PowerShell(() => FindExternalTerminal("pwsh"));
return finder.Terminals;
}

public List<Models.ExternalEditor> FindExternalEditors()
{
var finder = new Models.ExternalEditorFinder();
Expand All @@ -130,6 +139,26 @@ public void OpenBrowser(string url)
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.Trim();
}

public void OpenTerminal(string workdir)
{
Expand Down
Binary file added src/Resources/ExternalTerminalIcons/git-bash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/Resources/ExternalTerminalIcons/gnome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/Resources/ExternalTerminalIcons/konsole.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/Resources/ExternalTerminalIcons/osascript.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/Resources/ExternalTerminalIcons/pwsh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/Resources/ExternalTerminalIcons/wt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/Resources/ExternalTerminalIcons/xfce4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@
<x:String x:Key="Text.Preference.Appearance.Theme" xml:space="preserve">Theme</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">GENERAL</x:String>
<x:String x:Key="Text.Preference.General.AvatarServer" xml:space="preserve">Avatar Server</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminal" xml:space="preserve">Default Terminal</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell" xml:space="preserve">System Default</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.wt" xml:space="preserve">Windows Terminal</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.git-bash" xml:space="preserve">Git Bash</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.pwsh" xml:space="preserve">PowerShell</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.gnome" xml:space="preserve">Gnome</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.konsole" xml:space="preserve">Konsole</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.osascript" xml:space="preserve">AppleScript</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.xfce4" xml:space="preserve">xfce4</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">Check for updates on startup</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">Language</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">History Commits</x:String>
Expand Down
9 changes: 9 additions & 0 deletions src/Resources/Locales/zh_CN.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@
<x:String x:Key="Text.Preference.Appearance.Theme" xml:space="preserve">主题</x:String>
<x:String x:Key="Text.Preference.General" xml:space="preserve">通用配置</x:String>
<x:String x:Key="Text.Preference.General.AvatarServer" xml:space="preserve">头像服务</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminal" xml:space="preserve">默认终端</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell" xml:space="preserve">系统默认</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.wt" xml:space="preserve">终端 (Windows Terminal)</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.git-bash" xml:space="preserve">Git Bash</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.pwsh" xml:space="preserve">PowerShell</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.gnome" xml:space="preserve">Gnome</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.konsole" xml:space="preserve">Konsole</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.osascript" xml:space="preserve">AppleScript</x:String>
<x:String x:Key="Text.Preference.General.DefaultTerminalOrShell.xfce4" xml:space="preserve">xfce4</x:String>
<x:String x:Key="Text.Preference.General.Check4UpdatesOnStartup" xml:space="preserve">启动时检测软件更新</x:String>
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">显示语言</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">最大历史提交数</x:String>
Expand Down
31 changes: 31 additions & 0 deletions src/Resources/Styles.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,37 @@
<Setter Property="Opacity" Value="1"/>
</Style>

<Style Selector="SplitButton.icon_button">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="4,0,0,0" />
<Setter Property="MinHeight" Value="26" />
<Style.Resources>
<x:Double x:Key="SplitButtonPrimaryButtonSize">24</x:Double>
<x:Double x:Key="SplitButtonSecondaryButtonSize">40</x:Double>
<x:Double x:Key="SplitButtonSeparatorWidth">0</x:Double>
<SolidColorBrush x:Key="SplitButtonBackgroundPressed" Color="Transparent" />
</Style.Resources>
</Style>
<Style Selector="SplitButton.icon_button /template/ Button#PART_SecondaryButton">
<Setter Property="Margin" Value="-24,0,0,0"/>
<Setter Property="Padding" Value="24,0,4,0"/>
<Setter Property="ZIndex" Value="-1"/>
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}"/>
</Style>
<Style Selector="SplitButton.icon_button /template/ PathIcon">
<Setter Property="Width" Value="8"/>
<Setter Property="Height" Value="8"/>
</Style>
<Style Selector="SplitButton.icon_button /template/ Button#PART_PrimaryButton /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Opacity" Value="0.8"/>
</Style>
<Style Selector="SplitButton.icon_button:pointerover /template/ Button#PART_PrimaryButton /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Opacity" Value="1"/>
</Style>

<Style Selector="Button.flat">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontWeight" Value="Bold"/>
Expand Down
5 changes: 5 additions & 0 deletions src/SourceGit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ItemGroup>
<AvaloniaResource Include="App.ico" />
<AvaloniaResource Include="Resources/Fonts/*" />
<AvaloniaResource Include="Resources/ExternalTerminalIcons/*" />
<AvaloniaResource Include="Resources/ExternalToolIcons/*" />
</ItemGroup>

Expand All @@ -42,4 +43,8 @@
<TrimmerRootAssembly Include="SourceGit" />
<TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
</ItemGroup>

<ItemGroup>
<Folder Include="Resources\ExternalTerminalIcons\" />
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions src/ViewModels/Preference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ public string AvatarServer
}
}

public string DefaultTerminal
{
get => _defaultTerminal ??= "";
set => SetProperty(ref _defaultTerminal, value ?? "");
}

public int MaxHistoryCommits
{
get => _maxHistoryCommits;
Expand Down Expand Up @@ -429,6 +435,7 @@ private static bool RemoveNodeRecursive(RepositoryNode node, AvaloniaList<Reposi

private string _locale = "en_US";
private string _theme = "Default";
private string _defaultTerminal;
private FontFamily _defaultFont = null;
private FontFamily _monospaceFont = null;
private double _defaultFontSize = 13;
Expand Down
Loading