diff --git a/README.md b/README.md index 023b1c96..170e7bd6 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Opensource Git GUI client. * GitFlow * Git LFS * Issue Link +* Workspace +* Using AI to generate commit message (C# port of [anjerodev/commitollama](https://github.com/anjerodev/commitollama)) > [!WARNING] > **Linux** only tested on **Debian 12** on both **X11** & **Wayland**. @@ -83,6 +85,20 @@ For **Linux** users: * `xdg-open` must be installed to support open native file manager. * Make sure [git-credential-manager](https://github.com/git-ecosystem/git-credential-manager/releases) is installed on your linux. +* Maybe you need to set environment variable `AVALONIA_SCREEN_SCALE_FACTORS`. See https://github.com/AvaloniaUI/Avalonia/wiki/Configuring-X11-per-monitor-DPI. + +## OpenAI + +This software supports using OpenAI or other AI service that has an OpenAI comaptible HTTP API to generate commit message. You need configurate the service in `Preference` window. + +For `OpenAI`: + +* `Server` must be `https://api.openai.com/v1/chat/completions` + +For other AI service: + +* The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1/chat/completions` +* The `API Key` is optional that depends on the service ## External Tools diff --git a/VERSION b/VERSION index 74966804..2f6c16f4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.29 \ No newline at end of file +8.30 \ No newline at end of file diff --git a/src/App.JsonCodeGen.cs b/src/App.JsonCodeGen.cs index 8daea4f9..70567af5 100644 --- a/src/App.JsonCodeGen.cs +++ b/src/App.JsonCodeGen.cs @@ -46,6 +46,8 @@ namespace SourceGit [JsonSerializable(typeof(Models.ExternalToolPaths))] [JsonSerializable(typeof(Models.InteractiveRebaseJobCollection))] [JsonSerializable(typeof(Models.JetBrainsState))] + [JsonSerializable(typeof(Models.OpenAIChatRequest))] + [JsonSerializable(typeof(Models.OpenAIChatResponse))] [JsonSerializable(typeof(Models.ThemeOverrides))] [JsonSerializable(typeof(Models.Version))] [JsonSerializable(typeof(Models.RepositorySettings))] diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs new file mode 100644 index 00000000..e71fb0b9 --- /dev/null +++ b/src/Commands/GenerateCommitMessage.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace SourceGit.Commands +{ + /// + /// A C# version of https://github.com/anjerodev/commitollama + /// + public class GenerateCommitMessage + { + public class GetDiffContent : Command + { + public GetDiffContent(string repo, Models.DiffOption opt) + { + WorkingDirectory = repo; + Context = repo; + Args = $"diff --diff-algorithm=minimal {opt}"; + } + } + + public GenerateCommitMessage(string repo, List changes, CancellationToken cancelToken, Action onProgress) + { + _repo = repo; + _changes = changes; + _cancelToken = cancelToken; + _onProgress = onProgress; + } + + public string Result() + { + try + { + var summaries = new List(); + foreach (var change in _changes) + { + if (_cancelToken.IsCancellationRequested) + return ""; + + _onProgress?.Invoke($"Analyzing {change.Path}..."); + var summary = GenerateChangeSummary(change); + summaries.Add(summary); + } + + if (_cancelToken.IsCancellationRequested) + return ""; + + _onProgress?.Invoke($"Generating commit message..."); + var builder = new StringBuilder(); + builder.Append(GenerateSubject(string.Join("", summaries))); + builder.Append("\n"); + foreach (var summary in summaries) + { + builder.Append("\n- "); + builder.Append(summary.Trim()); + } + + return builder.ToString(); + } + catch (Exception e) + { + App.RaiseException(_repo, $"Failed to generate commit message: {e}"); + return ""; + } + } + + private string GenerateChangeSummary(Models.Change change) + { + var rs = new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadToEnd(); + var diff = rs.IsSuccess ? rs.StdOut : "unknown change"; + + var prompt = new StringBuilder(); + prompt.AppendLine("You are an expert developer specialist in creating commits."); + prompt.AppendLine("Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules:"); + prompt.AppendLine("- Do not use any code snippets, imports, file routes or bullets points."); + prompt.AppendLine("- Do not mention the route of file that has been change."); + prompt.AppendLine("- Simply describe the MAIN GOAL of the changes."); + prompt.AppendLine("- Output directly the summary in plain text.`"); + + var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here is the `git diff` output: {diff}", _cancelToken); + if (rsp != null && rsp.Choices.Count > 0) + return rsp.Choices[0].Message.Content; + + return string.Empty; + } + + private string GenerateSubject(string summary) + { + var prompt = new StringBuilder(); + prompt.AppendLine("You are an expert developer specialist in creating commits messages."); + prompt.AppendLine("Your only goal is to retrieve a single commit message."); + prompt.AppendLine("Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules:"); + prompt.AppendLine("- Assign the commit {type} according to the next conditions:"); + prompt.AppendLine(" feat: Only when adding a new feature."); + prompt.AppendLine(" fix: When fixing a bug."); + prompt.AppendLine(" docs: When updating documentation."); + prompt.AppendLine(" style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic."); + prompt.AppendLine(" test: When adding or updating tests. "); + prompt.AppendLine(" chore: When making changes to the build process or auxiliary tools and libraries. "); + prompt.AppendLine(" revert: When undoing a previous commit."); + prompt.AppendLine(" refactor: When restructuring code without changing its external behavior, or is any of the other refactor types."); + prompt.AppendLine("- Do not add any issues numeration, explain your output nor introduce your answer."); + prompt.AppendLine("- Output directly only one commit message in plain text with the next format: {type}: {commit_message}."); + prompt.AppendLine("- Be as concise as possible, keep the message under 50 characters."); + + var rsp = Models.OpenAI.Chat(prompt.ToString(), $"Here are the summaries changes: {summary}", _cancelToken); + if (rsp != null && rsp.Choices.Count > 0) + return rsp.Choices[0].Message.Content; + + return string.Empty; + } + + private string _repo; + private List _changes; + private CancellationToken _cancelToken; + private Action _onProgress; + } +} diff --git a/src/Converters/BookmarkConverters.cs b/src/Converters/BookmarkConverters.cs deleted file mode 100644 index ea755a00..00000000 --- a/src/Converters/BookmarkConverters.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace SourceGit.Converters -{ - public static class BookmarkConverters - { - public static readonly FuncValueConverter ToBrush = - new FuncValueConverter(bookmark => - { - if (bookmark == 0) - return Application.Current?.FindResource("Brush.FG1") as IBrush; - else - return Models.Bookmarks.Brushes[bookmark]; - }); - } -} diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 765ff6ef..17a88da2 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -1,5 +1,7 @@ using Avalonia; +using Avalonia.Controls; using Avalonia.Data.Converters; +using Avalonia.Media; namespace SourceGit.Converters { @@ -28,5 +30,14 @@ namespace SourceGit.Converters public static readonly FuncValueConverter ToTreeMargin = new FuncValueConverter(v => new Thickness(v * 16, 0, 0, 0)); + + public static readonly FuncValueConverter ToBookmarkBrush = + new FuncValueConverter(bookmark => + { + if (bookmark == 0) + return Application.Current?.FindResource("Brush.FG1") as IBrush; + else + return Models.Bookmarks.Brushes[bookmark]; + }); } } diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs new file mode 100644 index 00000000..2619f299 --- /dev/null +++ b/src/Models/OpenAI.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; + +namespace SourceGit.Models +{ + public class OpenAIChatMessage + { + [JsonPropertyName("role")] + public string Role + { + get; + set; + } + + [JsonPropertyName("content")] + public string Content + { + get; + set; + } + } + + public class OpenAIChatChoice + { + [JsonPropertyName("index")] + public int Index + { + get; + set; + } + + [JsonPropertyName("message")] + public OpenAIChatMessage Message + { + get; + set; + } + } + + public class OpenAIChatResponse + { + [JsonPropertyName("choices")] + public List Choices + { + get; + set; + } = []; + } + + public class OpenAIChatRequest + { + [JsonPropertyName("model")] + public string Model + { + get; + set; + } + + [JsonPropertyName("messages")] + public List Messages + { + get; + set; + } = []; + + public void AddMessage(string role, string content) + { + Messages.Add(new OpenAIChatMessage { Role = role, Content = content }); + } + } + + public static class OpenAI + { + public static string Server + { + get; + set; + } + + public static string ApiKey + { + get; + set; + } + + public static string Model + { + get; + set; + } + + public static bool IsValid + { + get => !string.IsNullOrEmpty(Server) && !string.IsNullOrEmpty(Model); + } + + public static OpenAIChatResponse Chat(string prompt, string question, CancellationToken cancellation) + { + var chat = new OpenAIChatRequest() { Model = Model }; + chat.AddMessage("system", prompt); + chat.AddMessage("user", question); + + var client = new HttpClient() { Timeout = TimeSpan.FromSeconds(60) }; + client.DefaultRequestHeaders.Add("Content-Type", "application/json"); + if (!string.IsNullOrEmpty(ApiKey)) + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}"); + + var req = new StringContent(JsonSerializer.Serialize(chat, JsonCodeGen.Default.OpenAIChatRequest)); + try + { + var task = client.PostAsync(Server, req, cancellation); + task.Wait(); + + var rsp = task.Result; + if (!rsp.IsSuccessStatusCode) + throw new Exception($"AI service returns error code {rsp.StatusCode}"); + + var reader = rsp.Content.ReadAsStringAsync(cancellation); + reader.Wait(); + + return JsonSerializer.Deserialize(reader.Result, JsonCodeGen.Default.OpenAIChatResponse); + } + catch + { + if (cancellation.IsCancellationRequested) + return null; + + throw; + } + } + } +} diff --git a/src/Models/RevisionFile.cs b/src/Models/RevisionFile.cs index 59868fcc..f1f5265f 100644 --- a/src/Models/RevisionFile.cs +++ b/src/Models/RevisionFile.cs @@ -10,6 +10,9 @@ namespace SourceGit.Models public class RevisionImageFile { public Bitmap Image { get; set; } = null; + public long FileSize { get; set; } = 0; + public string ImageType { get; set; } = string.Empty; + public string ImageSize => Image != null ? $"{Image.PixelSize.Width} x {Image.PixelSize.Height}" : "0 x 0"; } public class RevisionTextFile diff --git a/src/Models/Shell.cs b/src/Models/Shell.cs deleted file mode 100644 index 9960d136..00000000 --- a/src/Models/Shell.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace SourceGit.Models -{ - public enum Shell - { - Default = 0, - PowerShell, - CommandPrompt, - DefaultShellOfWindowsTerminal, - } -} diff --git a/src/Models/ShellOrTerminal.cs b/src/Models/ShellOrTerminal.cs new file mode 100644 index 00000000..fa3a207b --- /dev/null +++ b/src/Models/ShellOrTerminal.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace SourceGit.Models +{ + public class ShellOrTerminal + { + public string Type { get; set; } + public string Name { get; set; } + public string Exec { get; set; } + + public Bitmap Icon + { + get + { + var icon = AssetLoader.Open(new Uri($"avares://SourceGit/Resources/Images/ShellIcons/{Type}.png", UriKind.RelativeOrAbsolute)); + return new Bitmap(icon); + } + } + + public static readonly List Supported; + + static ShellOrTerminal() + { + if (OperatingSystem.IsWindows()) + { + Supported = new List() + { + new ShellOrTerminal("git-bash", "Git Bash", "bash.exe"), + new ShellOrTerminal("pwsh", "PowerShell", "pwsh.exe|powershell.exe"), + new ShellOrTerminal("cmd", "Command Prompt", "cmd.exe"), + new ShellOrTerminal("wt", "Windows Terminal", "wt.exe") + }; + } + else if (OperatingSystem.IsMacOS()) + { + Supported = new List() + { + new ShellOrTerminal("mac-terminal", "Terminal", ""), + new ShellOrTerminal("iterm2", "iTerm", ""), + }; + } + else + { + Supported = new List() + { + new ShellOrTerminal("gnome-terminal", "Gnome Terminal", "gnome-terminal"), + new ShellOrTerminal("konsole", "Konsole", "konsole"), + new ShellOrTerminal("xfce4-terminal", "Xfce4 Terminal", "xfce4-terminal"), + new ShellOrTerminal("lxterminal", "LXTerminal", "lxterminal"), + new ShellOrTerminal("deepin-terminal", "Deepin Terminal", "deepin-terminal"), + new ShellOrTerminal("mate-terminal", "MATE Terminal", "mate-terminal"), + new ShellOrTerminal("foot", "Foot", "foot"), + }; + } + } + + public ShellOrTerminal(string type, string name, string exec) + { + Type = type; + Name = name; + Exec = exec; + } + } +} diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index ca6e7c56..9537a39a 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -11,31 +11,8 @@ namespace SourceGit.Native [SupportedOSPlatform("linux")] internal class Linux : OS.IBackend { - class Terminal - { - public string FilePath { get; set; } - public string OpenArgFormat { get; set; } - - public Terminal(string exec, string fmt) - { - FilePath = exec; - OpenArgFormat = fmt; - } - - public void Open(string dir) - { - Process.Start(FilePath, string.Format(OpenArgFormat, dir)); - } - } - - public Linux() - { - _xdgOpenPath = FindExecutable("xdg-open"); - _terminal = FindTerminal(); - } - public void SetupApp(AppBuilder builder) - { + { builder.With(new X11PlatformOptions() { EnableIme = true, @@ -47,6 +24,20 @@ namespace SourceGit.Native return FindExecutable("git"); } + public string FindTerminal(Models.ShellOrTerminal shell) + { + var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + var pathes = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in pathes) + { + var test = Path.Combine(path, shell.Exec); + if (File.Exists(test)) + return test; + } + + return string.Empty; + } + public List FindExternalTools() { var finder = new Models.ExternalToolsFinder(); @@ -61,50 +52,40 @@ namespace SourceGit.Native public void OpenBrowser(string url) { - if (string.IsNullOrEmpty(_xdgOpenPath)) - App.RaiseException("", $"Can NOT find `xdg-open` command!!!"); - else - Process.Start(_xdgOpenPath, $"\"{url}\""); + Process.Start("xdg-open", $"\"{url}\""); } public void OpenInFileManager(string path, bool select) { - if (string.IsNullOrEmpty(_xdgOpenPath)) - { - App.RaiseException("", $"Can NOT find `xdg-open` command!!!"); - return; - } - if (Directory.Exists(path)) { - Process.Start(_xdgOpenPath, $"\"{path}\""); + Process.Start("xdg-open", $"\"{path}\""); } else { var dir = Path.GetDirectoryName(path); if (Directory.Exists(dir)) - Process.Start(_xdgOpenPath, $"\"{dir}\""); + Process.Start("xdg-open", $"\"{dir}\""); } } public void OpenTerminal(string workdir) { - var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir; - if (_terminal == null) - App.RaiseException(dir, $"Only supports gnome-terminal/konsole/xfce4-terminal/lxterminal/deepin-terminal/mate-terminal/foot!"); - else - _terminal.Open(dir); + if (string.IsNullOrEmpty(OS.ShellOrTerminal) || !File.Exists(OS.ShellOrTerminal)) + { + App.RaiseException(workdir, $"Can not found terminal! Please confirm that the correct shell/terminal has been configured."); + return; + } + + var startInfo = new ProcessStartInfo(); + startInfo.WorkingDirectory = string.IsNullOrEmpty(workdir) ? "~" : workdir; + startInfo.FileName = OS.ShellOrTerminal; + Process.Start(startInfo); } public void OpenWithDefaultEditor(string file) { - if (string.IsNullOrEmpty(_xdgOpenPath)) - { - App.RaiseException("", $"Can NOT find `xdg-open` command!!!"); - return; - } - - var proc = Process.Start(_xdgOpenPath, $"\"{file}\""); + var proc = Process.Start("xdg-open", $"\"{file}\""); if (proc != null) { proc.WaitForExit(); @@ -130,51 +111,10 @@ namespace SourceGit.Native return string.Empty; } - private Terminal FindTerminal() - { - var pathVariable = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - var pathes = pathVariable.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); - foreach (var path in pathes) - { - var test = Path.Combine(path, "gnome-terminal"); - if (File.Exists(test)) - return new Terminal(test, "--working-directory=\"{0}\""); - - test = Path.Combine(path, "konsole"); - if (File.Exists(test)) - return new Terminal(test, "--workdir \"{0}\""); - - test = Path.Combine(path, "xfce4-terminal"); - if (File.Exists(test)) - return new Terminal(test, "--working-directory=\"{0}\""); - - test = Path.Combine(path, "lxterminal"); - if (File.Exists(test)) - return new Terminal(test, "--working-directory=\"{0}\""); - - test = Path.Combine(path, "deepin-terminal"); - if (File.Exists(test)) - return new Terminal(test, "--work-directory \"{0}\""); - - test = Path.Combine(path, "mate-terminal"); - if (File.Exists(test)) - return new Terminal(test, "--working-directory=\"{0}\""); - - test = Path.Combine(path, "foot"); - if (File.Exists(test)) - return new Terminal(test, "--working-directory=\"{0}\""); - } - - return null; - } - private string FindJetBrainsFleet() { var path = $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}/JetBrains/Toolbox/apps/fleet/bin/Fleet"; return File.Exists(path) ? path : FindExecutable("fleet"); } - - private string _xdgOpenPath = null; - private Terminal _terminal = null; } } diff --git a/src/Native/MacOS.cs b/src/Native/MacOS.cs index cf7b5b4c..0ad608b2 100644 --- a/src/Native/MacOS.cs +++ b/src/Native/MacOS.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.Versioning; -using System.Text; using Avalonia; @@ -12,12 +11,6 @@ namespace SourceGit.Native [SupportedOSPlatform("macOS")] internal class MacOS : OS.IBackend { - enum TerminalType - { - Default, - iTerm2, - } - public void SetupApp(AppBuilder builder) { builder.With(new MacOSPlatformOptions() @@ -31,6 +24,19 @@ namespace SourceGit.Native return File.Exists("/usr/bin/git") ? "/usr/bin/git" : string.Empty; } + public string FindTerminal(Models.ShellOrTerminal shell) + { + switch (shell.Type) + { + case "mac-terminal": + return "Terminal"; + case "iterm2": + return "iTerm"; + } + + return "InvalidTerminal"; + } + public List FindExternalTools() { var finder = new Models.ExternalToolsFinder(); @@ -58,54 +64,14 @@ namespace SourceGit.Native public void OpenTerminal(string workdir) { - var dir = string.IsNullOrEmpty(workdir) ? "~" : workdir; - dir = dir.Replace(" ", "\\ "); - - var terminal = DetectTerminal(); - var cmdBuilder = new StringBuilder(); - switch (terminal) - { - case TerminalType.iTerm2: - cmdBuilder.AppendLine("on run argv"); - cmdBuilder.AppendLine(" tell application \"iTerm2\""); - cmdBuilder.AppendLine(" create window with default profile"); - cmdBuilder.AppendLine(" tell the current session of the current window"); - cmdBuilder.AppendLine($" write text \"cd {dir}\""); - cmdBuilder.AppendLine(" end tell"); - cmdBuilder.AppendLine(" end tell"); - cmdBuilder.AppendLine("end run"); - break; - default: - cmdBuilder.AppendLine("on run argv"); - cmdBuilder.AppendLine(" tell application \"Terminal\""); - cmdBuilder.AppendLine($" do script \"cd {dir}\""); - cmdBuilder.AppendLine(" activate"); - cmdBuilder.AppendLine(" end tell"); - cmdBuilder.AppendLine("end run"); - break; - } - - var tmp = Path.GetTempFileName(); - File.WriteAllText(tmp, cmdBuilder.ToString()); - - var proc = Process.Start("osascript", $"\"{tmp}\""); - if (proc != null) - proc.Exited += (_, _) => File.Delete(tmp); - else - File.Delete(tmp); + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var dir = string.IsNullOrEmpty(workdir) ? home : workdir; + Process.Start("open", $"-a {OS.ShellOrTerminal} \"{dir}\""); } public void OpenWithDefaultEditor(string file) { Process.Start("open", $"\"{file}\""); } - - private TerminalType DetectTerminal() - { - if (Directory.Exists("/Applications/iTerm.app")) - return TerminalType.iTerm2; - - return TerminalType.Default; - } } } diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 0e1b8522..87655798 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -13,6 +13,7 @@ namespace SourceGit.Native void SetupApp(AppBuilder builder); string FindGitExecutable(); + string FindTerminal(Models.ShellOrTerminal shell); List FindExternalTools(); void OpenTerminal(string workdir); @@ -23,6 +24,7 @@ namespace SourceGit.Native public static string DataDir { get; private set; } = string.Empty; public static string GitExecutable { get; set; } = string.Empty; + public static string ShellOrTerminal { get; set; } = string.Empty; public static List ExternalTools { get; set; } = []; static OS() @@ -45,29 +47,6 @@ namespace SourceGit.Native } } - public static Models.Shell GetShell() - { - if (OperatingSystem.IsWindows()) - return (_backend as Windows)!.Shell; - - return Models.Shell.Default; - } - - public static bool SetShell(Models.Shell shell) - { - if (OperatingSystem.IsWindows()) - { - var windows = (_backend as Windows)!; - if (windows.Shell != shell) - { - windows.Shell = shell; - return true; - } - } - - return false; - } - public static void SetupApp(AppBuilder builder) { _backend.SetupApp(builder); @@ -95,6 +74,14 @@ namespace SourceGit.Native return _backend.FindGitExecutable(); } + public static void SetShellOrTerminal(Models.ShellOrTerminal shellOrTerminal) + { + if (shellOrTerminal == null) + ShellOrTerminal = string.Empty; + else + ShellOrTerminal = _backend.FindTerminal(shellOrTerminal); + } + public static void OpenInFileManager(string path, bool select = false) { _backend.OpenInFileManager(path, select); @@ -107,7 +94,10 @@ namespace SourceGit.Native public static void OpenTerminal(string workdir) { - _backend.OpenTerminal(workdir); + if (string.IsNullOrEmpty(ShellOrTerminal)) + App.RaiseException(workdir, $"Can not found terminal! Please confirm that the correct shell/terminal has been configured."); + else + _backend.OpenTerminal(workdir); } public static void OpenWithDefaultEditor(string file) diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index 18326fa6..19362d0c 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -53,12 +53,6 @@ namespace SourceGit.Native [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = false)] private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags); - public Models.Shell Shell - { - get; - set; - } = Models.Shell.Default; - public void SetupApp(AppBuilder builder) { // Fix drop shadow issue on Windows 10 @@ -98,6 +92,51 @@ namespace SourceGit.Native return null; } + public string FindTerminal(Models.ShellOrTerminal shell) + { + switch (shell.Type) + { + case "git-bash": + if (string.IsNullOrEmpty(OS.GitExecutable)) + break; + + var binDir = Path.GetDirectoryName(OS.GitExecutable)!; + var bash = Path.Combine(binDir, "bash.exe"); + if (!File.Exists(bash)) + break; + + return bash; + case "pwsh": + var localMachine = Microsoft.Win32.RegistryKey.OpenBaseKey( + Microsoft.Win32.RegistryHive.LocalMachine, + Microsoft.Win32.RegistryView.Registry64); + + var pwsh = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\pwsh.exe"); + if (pwsh != null) + { + var path = pwsh.GetValue(null) as string; + if (File.Exists(path)) + return path; + } + + var pwshFinder = new StringBuilder("powershell.exe", 512); + if (PathFindOnPath(pwshFinder, null)) + return pwshFinder.ToString(); + + break; + case "cmd": + return "C:\\Windows\\System32\\cmd.exe"; + case "wt": + var wtFinder = new StringBuilder("wt.exe", 512); + if (PathFindOnPath(wtFinder, null)) + return wtFinder.ToString(); + + break; + } + + return string.Empty; + } + public List FindExternalTools() { var finder = new Models.ExternalToolsFinder(); @@ -119,56 +158,15 @@ namespace SourceGit.Native public void OpenTerminal(string workdir) { - if (string.IsNullOrEmpty(workdir) || !Path.Exists(workdir)) + if (string.IsNullOrEmpty(OS.ShellOrTerminal) || !File.Exists(OS.ShellOrTerminal)) { - workdir = "."; + App.RaiseException(workdir, $"Can not found terminal! Please confirm that the correct shell/terminal has been configured."); + return; } var startInfo = new ProcessStartInfo(); startInfo.WorkingDirectory = workdir; - - switch (Shell) - { - case Models.Shell.Default: - if (string.IsNullOrEmpty(OS.GitExecutable)) - { - App.RaiseException(workdir, $"Can NOT found bash.exe"); - return; - } - - var binDir = Path.GetDirectoryName(OS.GitExecutable)!; - var bash = Path.Combine(binDir, "bash.exe"); - if (!File.Exists(bash)) - { - App.RaiseException(workdir, $"Can NOT found bash.exe under '{binDir}'"); - return; - } - - startInfo.FileName = bash; - break; - case Models.Shell.PowerShell: - startInfo.FileName = ChoosePowerShell(); - startInfo.Arguments = startInfo.FileName.EndsWith("pwsh.exe") ? $"-WorkingDirectory \"{workdir}\" -Nologo" : "-Nologo"; - break; - case Models.Shell.CommandPrompt: - startInfo.FileName = "cmd"; - break; - case Models.Shell.DefaultShellOfWindowsTerminal: - var wt = FindWindowsTerminalApp(); - if (!File.Exists(wt)) - { - App.RaiseException(workdir, $"Can NOT found wt.exe on your system!"); - return; - } - - startInfo.FileName = wt; - startInfo.Arguments = $"-d \"{workdir}\""; - break; - default: - App.RaiseException(workdir, $"Bad shell configuration!"); - return; - } - + startInfo.FileName = OS.ShellOrTerminal; Process.Start(startInfo); } @@ -218,52 +216,6 @@ namespace SourceGit.Native DwmExtendFrameIntoClientArea(platformHandle.Handle, ref margins); } - // There are two versions of PowerShell : pwsh.exe (preferred) and powershell.exe (system default) - private string ChoosePowerShell() - { - if (!string.IsNullOrEmpty(_powershellPath)) - return _powershellPath; - - var localMachine = Microsoft.Win32.RegistryKey.OpenBaseKey( - Microsoft.Win32.RegistryHive.LocalMachine, - Microsoft.Win32.RegistryView.Registry64); - - var pwsh = localMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\pwsh.exe"); - if (pwsh != null) - { - var path = pwsh.GetValue(null) as string; - if (File.Exists(path)) - { - _powershellPath = path; - return _powershellPath; - } - } - - var finder = new StringBuilder("powershell.exe", 512); - if (PathFindOnPath(finder, null)) - { - _powershellPath = finder.ToString(); - return _powershellPath; - } - - return string.Empty; - } - - private string FindWindowsTerminalApp() - { - if (!string.IsNullOrEmpty(_wtPath)) - return _wtPath; - - var finder = new StringBuilder("wt.exe", 512); - if (PathFindOnPath(finder, null)) - { - _wtPath = finder.ToString(); - return _wtPath; - } - - return string.Empty; - } - #region EXTERNAL_EDITOR_FINDER private string FindVSCode() { @@ -385,8 +337,5 @@ namespace SourceGit.Native ILFree(pidl); } } - - private string _powershellPath = string.Empty; - private string _wtPath = string.Empty; } } diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index 7ae35731..0b32369c 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -1,4 +1,5 @@ + M951 419a255 255 0 00-22-209 258 258 0 00-278-124A259 259 0 00213 178a255 255 0 00-171 124 258 258 0 0032 303 255 255 0 0022 210 258 258 0 00278 124A255 255 0 00566 1024a258 258 0 00246-179 256 256 0 00171-124 258 258 0 00-32-302zM566 957a191 191 0 01-123-44l6-3 204-118a34 34 0 0017-29v-287l86 50a3 3 0 012 2v238a192 192 0 01-192 192zM154 781a191 191 0 01-23-129l6 4 204 118a33 33 0 0033 0l249-144v99a3 3 0 01-1 3L416 851a192 192 0 01-262-70zM100 337a191 191 0 01101-84V495a33 33 0 0017 29l248 143-86 50a3 3 0 01-3 0l-206-119A192 192 0 01100 336zm708 164-249-145L645 307a3 3 0 013 0l206 119a192 192 0 01-29 346v-242a34 34 0 00-17-28zm86-129-6-4-204-119a33 33 0 00-33 0L401 394V294a3 3 0 011-3l206-119a192 192 0 01285 199zm-539 176-86-50a3 3 0 01-2-2V259a192 192 0 01315-147l-6 3-204 118a34 34 0 00-17 29zm47-101 111-64 111 64v128l-111 64-111-64z M296 392h64v64h-64zM296 582v160h128V582h-64v-62h-64v62zm80 48v64h-32v-64h32zM360 328h64v64h-64zM296 264h64v64h-64zM360 456h64v64h-64zM360 200h64v64h-64zM855 289 639 73c-6-6-14-9-23-9H192c-18 0-32 14-32 32v832c0 18 14 32 32 32h640c18 0 32-14 32-32V311c0-9-3-17-9-23zM790 326H602V138L790 326zm2 562H232V136h64v64h64v-64h174v216c0 23 19 42 42 42h216v494z M71 1024V0h661L953 219V1024H71zm808-731-220-219H145V951h735V293zM439 512h-220V219h220V512zm-74-219H292v146h74v-146zm0 512h74v73h-220v-73H292v-146H218V585h147v219zm294-366h74V512H512v-73h74v-146H512V219h147v219zm74 439H512V585h220v293zm-74-219h-74v146h74v-146z M128 256h192a64 64 0 110 128H128a64 64 0 110-128zm576 192h192a64 64 0 010 128h-192a64 64 0 010-128zm-576 192h192a64 64 0 010 128H128a64 64 0 010-128zm576 0h192a64 64 0 010 128h-192a64 64 0 010-128zm0-384h192a64 64 0 010 128h-192a64 64 0 010-128zM128 448h192a64 64 0 110 128H128a64 64 0 110-128zm384-320a64 64 0 0164 64v640a64 64 0 01-128 0V192a64 64 0 0164-64z @@ -119,6 +120,7 @@ M153 154h768v768h-768v-768zm64 64v640h640v-640h-640z M256 128l0 192L64 320l0 576 704 0 0-192 192 0L960 128 256 128zM704 832 128 832 128 384l576 0L704 832zM896 640l-128 0L768 320 320 320 320 192l576 0L896 640z M248 221a77 77 0 00-30-21c-18-7-40-10-68-5a224 224 0 00-45 13c-5 2-10 5-15 8l-3 2v68l11-9c10-8 21-14 34-19 13-5 26-7 39-7 12 0 21 3 28 10 6 6 9 16 9 29l-62 9c-14 2-26 6-36 11a80 80 0 00-25 20c-7 8-12 17-15 27-6 21-6 44 1 65a70 70 0 0041 43c10 4 21 6 34 6a80 80 0 0063-28v22h64V298c0-16-2-31-6-44a91 91 0 00-18-33zm-41 121v15c0 8-1 15-4 22a48 48 0 01-24 29 44 44 0 01-33 2 29 29 0 01-10-6 25 25 0 01-6-9 30 30 0 01-2-12c0-5 1-9 2-14a21 21 0 015-9 28 28 0 0110-7 83 83 0 0120-5l42-6zm323-68a144 144 0 00-16-42 87 87 0 00-28-29 75 75 0 00-41-11 73 73 0 00-44 14c-6 5-12 11-17 17V64H326v398h59v-18c8 10 18 17 30 21 6 2 13 3 21 3 16 0 31-4 43-11 12-7 23-18 31-31a147 147 0 0019-46 248 248 0 006-57c0-17-2-33-5-49zm-55 49c0 15-1 28-4 39-2 11-6 20-10 27a41 41 0 01-15 15 37 37 0 01-36 1 44 44 0 01-13-12 59 59 0 01-9-18A76 76 0 01384 352v-33c0-10 1-20 4-29 2-8 6-15 10-22a43 43 0 0115-13 37 37 0 0119-5 35 35 0 0132 18c4 6 7 14 9 23 2 9 3 20 3 31zM154 634a58 58 0 0120-15c14-6 35-7 49-1 7 3 13 6 20 12l21 17V572l-6-4a124 124 0 00-58-14c-20 0-38 4-54 11-16 7-30 17-41 30-12 13-20 29-26 46-6 17-9 36-9 57 0 18 3 36 8 52 6 16 14 30 24 42 10 12 23 21 38 28 15 7 32 10 50 10 15 0 28-2 39-5 11-3 21-8 30-14l5-4v-57l-13 6a26 26 0 01-5 2c-3 1-6 2-8 3-2 1-15 6-15 6-4 2-9 3-14 4a63 63 0 01-38-4 53 53 0 01-20-14 70 70 0 01-13-24 111 111 0 01-5-34c0-13 2-26 5-36 3-10 8-19 14-26zM896 384h-256V320h288c21 1 32 12 32 32v384c0 18-12 32-32 32H504l132 133-45 45-185-185c-16-21-16-25 0-45l185-185L637 576l-128 128H896V384z + M128 691H6V38h838v160h-64V102H70v525H128zM973 806H154V250h819v557zm-755-64h691V314H218v429zM365 877h448v64h-448z M512 0C229 0 0 72 0 160v128C0 376 229 448 512 448s512-72 512-160v-128C1024 72 795 0 512 0zM512 544C229 544 0 472 0 384v192c0 88 229 160 512 160s512-72 512-160V384c0 88-229 160-512 160zM512 832c-283 0-512-72-512-160v192C0 952 229 1024 512 1024s512-72 512-160v-192c0 88-229 160-512 160z M640 725 768 725 768 597 853 597 853 725 981 725 981 811 853 811 853 939 768 939 768 811 640 811 640 725M384 128C573 128 725 204 725 299 725 393 573 469 384 469 195 469 43 393 43 299 43 204 195 128 384 128M43 384C43 478 195 555 384 555 573 555 725 478 725 384L725 512 683 512 683 595C663 612 640 627 610 640L555 640 555 660C504 675 446 683 384 683 195 683 43 606 43 512L43 384M43 597C43 692 195 768 384 768 446 768 504 760 555 745L555 873C504 888 446 896 384 896 195 896 43 820 43 725L43 597Z diff --git a/src/Resources/Images/ShellIcons/deepin-terminal.png b/src/Resources/Images/ShellIcons/deepin-terminal.png new file mode 100644 index 00000000..78eef1b4 Binary files /dev/null and b/src/Resources/Images/ShellIcons/deepin-terminal.png differ diff --git a/src/Resources/Images/ShellIcons/foot.png b/src/Resources/Images/ShellIcons/foot.png new file mode 100644 index 00000000..c5dbd0a5 Binary files /dev/null and b/src/Resources/Images/ShellIcons/foot.png differ diff --git a/src/Resources/Images/ShellIcons/gnome-terminal.png b/src/Resources/Images/ShellIcons/gnome-terminal.png new file mode 100644 index 00000000..f9edd2e3 Binary files /dev/null and b/src/Resources/Images/ShellIcons/gnome-terminal.png differ diff --git a/src/Resources/Images/ShellIcons/iterm2.png b/src/Resources/Images/ShellIcons/iterm2.png new file mode 100644 index 00000000..16fbd3bd Binary files /dev/null and b/src/Resources/Images/ShellIcons/iterm2.png differ diff --git a/src/Resources/Images/ShellIcons/konsole.png b/src/Resources/Images/ShellIcons/konsole.png new file mode 100644 index 00000000..e1dbcd49 Binary files /dev/null and b/src/Resources/Images/ShellIcons/konsole.png differ diff --git a/src/Resources/Images/ShellIcons/lxterminal.png b/src/Resources/Images/ShellIcons/lxterminal.png new file mode 100644 index 00000000..99f62683 Binary files /dev/null and b/src/Resources/Images/ShellIcons/lxterminal.png differ diff --git a/src/Resources/Images/ShellIcons/mac-terminal.png b/src/Resources/Images/ShellIcons/mac-terminal.png new file mode 100644 index 00000000..569af756 Binary files /dev/null and b/src/Resources/Images/ShellIcons/mac-terminal.png differ diff --git a/src/Resources/Images/ShellIcons/mate-terminal.png b/src/Resources/Images/ShellIcons/mate-terminal.png new file mode 100644 index 00000000..d48cedfa Binary files /dev/null and b/src/Resources/Images/ShellIcons/mate-terminal.png differ diff --git a/src/Resources/Images/ShellIcons/xfce4-terminal.png b/src/Resources/Images/ShellIcons/xfce4-terminal.png new file mode 100644 index 00000000..9eda3d00 Binary files /dev/null and b/src/Resources/Images/ShellIcons/xfce4-terminal.png differ diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index adc22a10..586e2ed9 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -20,6 +20,8 @@ Optional. Standard ist der Zielordnername. Branch verfolgen: Remote-Branch verfolgen + OpenAI Assistent + Verwende OpenAI, um Commit-Nachrichten zu generieren Patch Fehler Fehler werfen und anwenden des Patches verweigern @@ -147,6 +149,9 @@ HTTP Proxy für dieses Repository Benutzername Benutzername für dieses Repository + Arbeitsplätze + Name + Farbe Kopieren Kopiere gesamten Text COMMIT-NACHRICHT KOPIEREN @@ -320,6 +325,7 @@ REPOSITORY Gestagte Änderungen committen Gestagte Änderungen committen und pushen + Ausgewählte Änderungen verwerfen Dashboard Modus (Standard) Erzwinge Neuladen des Repositorys Ausgewählte Änderungen stagen/unstagen @@ -354,6 +360,8 @@ Ziel-Branch: Merge Option: Quell-Branch: + Bewege Repository Knoten + Wähle Vorgänger-Knoten für: Name: Git wurde NICHT konfiguriert. Gehe bitte zuerst in die [Einstellungen] und konfiguriere Git. BENACHRICHTIGUNG @@ -379,6 +387,10 @@ Leztes Jahr Vor {0} Jahren Einstellungen + OPEN AI + Server + API Schlüssel + Modell DARSTELLUNG Standardschriftart Standardschriftgröße @@ -388,11 +400,14 @@ Design-Anpassungen Fixe Tab-Breite in Titelleiste Verwende nativen Fensterrahmen + DIFF/MERGE TOOL + Installationspfad + Installationspfad zum Diff/Merge Tool + Tool ALLGEMEIN Beim Starten nach Updates suchen Sprache Commit-Historie - Zuletzt geöffnete Tabs beim Starten wiederherstellen Längenvorgabe für Commit-Nachrichten GIT Remotes automatisch fetchen @@ -403,7 +418,6 @@ Benutzer Email Globale Git Benutzer Email Installationspfad - Shell Benutzername Globaler Git Benutzername Git Version @@ -416,10 +430,6 @@ Installationspfad zum GPG Programm Benutzer Signierungsschlüssel GPG Benutzer Signierungsschlüssel - DIFF/MERGE TOOL - Installationspfad - Installationspfad zum Diff/Merge Tool - Tool Remote löschen Ziel: Worktrees löschen @@ -482,7 +492,7 @@ Repository Einstellungen WEITER Öffne im Datei-Browser - Suche Branches & Tags & Submodules + Suche Branches & Tags & Submodule GEFILTERT: LOKALE BRANCHES Zum HEAD wechseln @@ -526,6 +536,8 @@ SPEICHERN Speichern als... Patch wurde erfolgreich gespeichert! + Durchsuche Repositories + Hauptverzeichnis: Suche nach Updates... Neue Version ist verfügbar: Suche nach Updates fehlgeschlagen! @@ -585,9 +597,11 @@ Lösche DRAG & DROP VON ORDNER UNTERSTÜTZT. BENUTZERDEFINIERTE GRUPPIERUNG UNTERSTÜTZT. Bearbeiten + Bewege in eine andere Gruppe Öffne alle Repositories Öffne Repository Öffne Terminal + Clone Standardordner erneut nach Repositories durchsuchen Suche Repositories... Sortieren Änderungen @@ -617,6 +631,8 @@ ALS UNVERÄNDERT ANGENOMMENE ANZEIGEN Template: ${0}$ Rechtsklick auf selektierte Dateien und wähle die Konfliktlösungen aus. + ARBEITSPLATZ: + Arbeitsplätze konfigurieren... WORKTREE Pfad kopieren Sperren diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 68957769..72a6a8ba 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -17,6 +17,8 @@ Optional. Default is the destination folder name. Track Branch: Tracking remote branch + OpenAI Assistant + Use OpenAI to generate commit message Patch Error Raise errors and refuses to apply the patch @@ -146,6 +148,9 @@ HTTP proxy used by this repository User Name User name for this repository + Workspaces + Name + Color Copy Copy All Text COPY MESSAGE @@ -381,6 +386,10 @@ Last year {0} years ago Preference + OPEN AI + Server + API Key + Model APPEARANCE Default Font Default Font Size @@ -390,11 +399,14 @@ Theme Overrides Use fixed tab width in titlebar Use native window frame + DIFF/MERGE TOOL + Install Path + Input path for diff/merge tool + Tool GENERAL Check for updates on startup Language History Commits - Restore last opened tab(s) on startup Subject Guide Length GIT Fetch remotes automatically @@ -405,7 +417,6 @@ User Email Global git user email Install Path - Shell User Name Global git user name Git version @@ -418,10 +429,10 @@ Input path for installed gpg program User Signing Key User's gpg signing key - DIFF/MERGE TOOL - Install Path - Input path for diff/merge tool - Tool + INTEGRATION + SHELL/TERMINAL + Shell/Terminal + Path Prune Remote Target: Prune Worktrees @@ -624,6 +635,8 @@ VIEW ASSUME UNCHANGED Template: ${0}$ Right-click the selected file(s), and make your choice to resolve conflicts. + WORKSPACE: + Configure Workspaces... WORKTREE Copy Path Lock diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index b9639064..40df6d99 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -387,11 +387,14 @@ Dérogations de thème Utiliser des onglets de taille fixe dans la barre de titre Utiliser un cadre de fenêtre natif + OUTIL DIFF/MERGE + Chemin d'installation + Saisir le chemin d'installation de l'outil diff/merge + Outil GÉNÉRAL Vérifier les mises à jour au démarrage Language Historique de commits - Restaurer les onglets au démarrage Guide de longueur du sujet GIT Fetch les dépôts distants automatiquement @@ -402,7 +405,6 @@ E-mail utilsateur E-mail utilsateur global Chemin d'installation - Shell Nom d'utilisateur Nom d'utilisateur global Version de Git @@ -415,10 +417,6 @@ Saisir le chemin d'installation vers le programme GPG Clé de signature de l'utilisateur Clé de signature GPG de l'utilisateur - OUTIL DIFF/MERGE - Chemin d'installation - Saisir le chemin d'installation de l'outil diff/merge - Outil Élaguer une branche distant Cible : Élaguer les Worktrees diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index d7e3456f..96cdda3e 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -381,11 +381,14 @@ Tema Sobrescrever Tema Usar largura fixa da aba na barra de título + FERRAMENTA DE DIF/MERGE + Caminho de Instalação + Insira o caminho para a ferramenta de dif/merge + Ferramenta GERAL Verificar atualizações na inicialização Idioma Commits do Histórico - Restaurar as últimas abas abertas na inicialização Comprimento do Guia de Assunto GIT Buscar remotos automaticamente @@ -396,7 +399,6 @@ E-mail do Usuário E-mail global do usuário git Caminho de Instalação - Shell Nome do Usuário Nome global do usuário git Versão do Git @@ -409,10 +411,6 @@ Insira o caminho do programa gpg instalado Chave de Assinatura do Usuário Chave de assinatura gpg do usuário - FERRAMENTA DE DIF/MERGE - Caminho de Instalação - Insira o caminho para a ferramenta de dif/merge - Ferramenta Prunar Remoto Alvo: Podar Worktrees diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 550750a6..6c8739fe 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -20,6 +20,8 @@ 选填。默认使用目标文件夹名称。 跟踪分支 设置上游跟踪分支 + OpenAI助手 + 使用OpenAI助手生成提交信息 应用补丁(apply) 错误 输出错误,并终止应用补丁 @@ -149,6 +151,9 @@ HTTP网络代理 用户名 应用于本仓库的用户名 + 工作区 + 名称 + 颜色 复制 复制全部文本 复制内容 @@ -393,11 +398,14 @@ 主题自定义 主标签使用固定宽度 使用系统默认窗体样式 + 对比/合并工具 + 安装路径 + 填写工具可执行文件所在位置 + 工具 通用配置 启动时检测软件更新 显示语言 最大历史提交数 - 启动时恢复上次打开的仓库 SUBJECT字数检测 GIT配置 启用定时自动拉取远程更新 @@ -408,7 +416,6 @@ 邮箱 默认GIT用户邮箱 安装路径 - 终端Shell 用户名 默认GIT用户名 Git 版本 @@ -421,10 +428,10 @@ 签名程序所在路径 用户签名KEY 输入签名提交所使用的KEY - 对比/合并工具 - 安装路径 - 填写工具可执行文件所在位置 - 工具 + 第三方工具集成 + 终端/SHELL + 终端/SHELL + 安装路径 清理远程已删除分支 目标 : 清理工作树 @@ -626,6 +633,8 @@ 查看忽略变更文件 模板:${0}$ 请选中冲突文件,打开右键菜单,选择合适的解决方式 + 工作区: + 配置工作区... 本地工作树 复制工作树路径 锁定工作树 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 279f63b0..defd33d1 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -20,6 +20,8 @@ 選填。預設使用目標資料夾名稱。 追蹤分支 設定遠端追蹤分支 + OpenAI 助理 + 使用 OpenAI 產生提交訊息 套用修補檔 (apply patch) 錯誤 輸出錯誤,並中止套用修補檔 @@ -149,6 +151,9 @@ HTTP 網路代理 使用者名稱 用於本存放庫的使用者名稱 + 工作區 + 名稱 + 顏色 複製 複製全部內容 複製內容 @@ -358,7 +363,7 @@ 合併方式: 合併分支: 調整存放庫分組 - 請選擇目標分組: + 請選擇目標分組: 名稱: 尚未設定 Git。請開啟 [偏好設定] 以設定 Git 路徑。 系統提示 @@ -384,6 +389,10 @@ 一年前 {0} 年前 偏好設定 + OpenAI + 伺服器 + API 金鑰 + 模型 外觀設定 預設字型 預設字型大小 @@ -393,11 +402,14 @@ 自訂主題 使用固定寬度的分頁標籤 使用系統原生預設視窗樣式 + 對比/合併工具 + 安裝路徑 + 填寫可執行檔案所在路徑 + 工具 一般設定 啟動時檢查軟體更新 顯示語言 最大歷史提交數 - 啟動時還原上次開啟的存放庫 提交標題字數偵測 Git 設定 啟用定時自動提取 (fetch) 遠端更新 @@ -408,7 +420,6 @@ 電子郵件 預設 Git 使用者電子郵件 安裝路徑 - 終端 Shell 使用者名稱 預設 Git 使用者名稱 Git 版本 @@ -421,10 +432,10 @@ 填寫 gpg.exe 所在路徑 使用者簽章金鑰 填寫簽章提交所使用的金鑰 - 對比/合併工具 - 安裝路徑 - 填寫可執行檔案所在路徑 - 工具 + 第三方工具整合 + 終端機/Shell + 終端機/Shell + 安裝路徑 清理遠端已刪除分支 目標: 清理工作區 @@ -627,6 +638,8 @@ 檢視不追蹤變更的檔案 範本: ${0}$ 請選擇發生衝突的檔案,開啟右鍵選單,選擇合適的解決方式 + 工作區: + 設定工作區... 本機工作區 複製工作區路徑 鎖定工作區 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index b5d4dddb..6de1a479 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -466,21 +466,6 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/ConfigureWorkspace.axaml.cs b/src/Views/ConfigureWorkspace.axaml.cs new file mode 100644 index 00000000..82d8cd30 --- /dev/null +++ b/src/Views/ConfigureWorkspace.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class ConfigureWorkspace : ChromelessWindow + { + public ConfigureWorkspace() + { + InitializeComponent(); + } + + protected override void OnClosing(WindowClosingEventArgs e) + { + ViewModels.Preference.Instance.Save(); + base.OnClosing(e); + } + + private void BeginMoveWindow(object _, PointerPressedEventArgs e) + { + BeginMoveDrag(e); + } + } +} diff --git a/src/Views/DeleteRepositoryNode.axaml b/src/Views/DeleteRepositoryNode.axaml index 6a9c2891..f4e041e5 100644 --- a/src/Views/DeleteRepositoryNode.axaml +++ b/src/Views/DeleteRepositoryNode.axaml @@ -24,7 +24,7 @@ Text="{DynamicResource Text.DeleteRepositoryNode.Target}"/> diff --git a/src/Views/EditRepositoryNode.axaml b/src/Views/EditRepositoryNode.axaml index f444ad20..ac8f50f3 100644 --- a/src/Views/EditRepositoryNode.axaml +++ b/src/Views/EditRepositoryNode.axaml @@ -40,7 +40,7 @@ diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 7cc0bea0..3996554d 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -10,8 +10,19 @@ x:Class="SourceGit.Views.Histories" x:DataType="vm:Histories" x:Name="ThisControl"> - + + + + + + + + + + + + + diff --git a/src/Views/ImageContainer.cs b/src/Views/ImageContainer.cs new file mode 100644 index 00000000..aecea0b2 --- /dev/null +++ b/src/Views/ImageContainer.cs @@ -0,0 +1,344 @@ +using System; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Styling; + +namespace SourceGit.Views +{ + public class ImageContainer : Control + { + public override void Render(DrawingContext context) + { + if (_bgBrush == null) + { + var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB); + var bg = new DrawingGroup() + { + Children = + { + new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) }, + new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) }, + } + }; + + _bgBrush = new DrawingBrush(bg) + { + AlignmentX = AlignmentX.Left, + AlignmentY = AlignmentY.Top, + DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute), + Stretch = Stretch.None, + TileMode = TileMode.Tile, + }; + } + + context.FillRectangle(_bgBrush, new Rect(Bounds.Size)); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property.Name == "ActualThemeVariant") + { + _bgBrush = null; + InvalidateVisual(); + } + } + + private DrawingBrush _bgBrush = null; + } + + public class ImageView : ImageContainer + { + public static readonly StyledProperty ImageProperty = + AvaloniaProperty.Register(nameof(Image)); + + public Bitmap Image + { + get => GetValue(ImageProperty); + set => SetValue(ImageProperty, value); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (Image is { } image) + context.DrawImage(image, new Rect(0, 0, Bounds.Width, Bounds.Height)); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ImageProperty) + InvalidateMeasure(); + } + + protected override Size MeasureOverride(Size availableSize) + { + if (Image is { } image) + { + var imageSize = image.Size; + var scaleW = availableSize.Width / imageSize.Width; + var scaleH = availableSize.Height / imageSize.Height; + var scale = Math.Min(scaleW, scaleH); + return new Size(scale * imageSize.Width, scale * imageSize.Height); + } + + return availableSize; + } + } + + public class ImageSwipeControl : ImageContainer + { + public static readonly StyledProperty AlphaProperty = + AvaloniaProperty.Register(nameof(Alpha), 0.5); + + public double Alpha + { + get => GetValue(AlphaProperty); + set => SetValue(AlphaProperty, value); + } + + public static readonly StyledProperty OldImageProperty = + AvaloniaProperty.Register(nameof(OldImage)); + + public Bitmap OldImage + { + get => GetValue(OldImageProperty); + set => SetValue(OldImageProperty, value); + } + + public static readonly StyledProperty NewImageProperty = + AvaloniaProperty.Register(nameof(NewImage)); + + public Bitmap NewImage + { + get => GetValue(NewImageProperty); + set => SetValue(NewImageProperty, value); + } + + static ImageSwipeControl() + { + AffectsMeasure(OldImageProperty, NewImageProperty); + AffectsRender(AlphaProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var alpha = Alpha; + var w = Bounds.Width; + var h = Bounds.Height; + var x = w * alpha; + var left = OldImage; + if (left != null && alpha > 0) + { + var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height); + var dst = new Rect(0, 0, x, h); + context.DrawImage(left, src, dst); + } + + var right = NewImage; + if (right != null && alpha < 1) + { + var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height); + var dst = new Rect(x, 0, w - x, h); + context.DrawImage(right, src, dst); + } + + context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height)); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + var p = e.GetPosition(this); + var hitbox = new Rect(Math.Max(Bounds.Width * Alpha - 2, 0), 0, 4, Bounds.Height); + var pointer = e.GetCurrentPoint(this); + if (pointer.Properties.IsLeftButtonPressed && hitbox.Contains(p)) + { + _pressedOnSlider = true; + Cursor = new Cursor(StandardCursorType.SizeWestEast); + e.Pointer.Capture(this); + e.Handled = true; + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + _pressedOnSlider = false; + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + var w = Bounds.Width; + var p = e.GetPosition(this); + + if (_pressedOnSlider) + { + SetCurrentValue(AlphaProperty, Math.Clamp(p.X, 0, w) / w); + } + else + { + var hitbox = new Rect(Math.Max(w * Alpha - 2, 0), 0, 4, Bounds.Height); + if (hitbox.Contains(p)) + { + if (!_lastInSlider) + { + _lastInSlider = true; + Cursor = new Cursor(StandardCursorType.SizeWestEast); + } + } + else + { + if (_lastInSlider) + { + _lastInSlider = false; + Cursor = null; + } + } + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var left = OldImage; + var right = NewImage; + + if (left == null) + return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); + + if (right == null) + return GetDesiredSize(left.Size, availableSize); + + var ls = GetDesiredSize(left.Size, availableSize); + var rs = GetDesiredSize(right.Size, availableSize); + return ls.Width > rs.Width ? ls : rs; + } + + private Size GetDesiredSize(Size img, Size available) + { + var sw = available.Width / img.Width; + var sh = available.Height / img.Height; + var scale = Math.Min(sw, sh); + return new Size(scale * img.Width, scale * img.Height); + } + + private bool _pressedOnSlider = false; + private bool _lastInSlider = false; + } + + public class ImageBlendControl : ImageContainer + { + public static readonly StyledProperty AlphaProperty = + AvaloniaProperty.Register(nameof(Alpha), 1.0); + + public double Alpha + { + get => GetValue(AlphaProperty); + set => SetValue(AlphaProperty, value); + } + + public static readonly StyledProperty OldImageProperty = + AvaloniaProperty.Register(nameof(OldImage)); + + public Bitmap OldImage + { + get => GetValue(OldImageProperty); + set => SetValue(OldImageProperty, value); + } + + public static readonly StyledProperty NewImageProperty = + AvaloniaProperty.Register(nameof(NewImage)); + + public Bitmap NewImage + { + get => GetValue(NewImageProperty); + set => SetValue(NewImageProperty, value); + } + + static ImageBlendControl() + { + AffectsMeasure(OldImageProperty, NewImageProperty); + AffectsRender(AlphaProperty); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); + var alpha = Alpha; + var left = OldImage; + var right = NewImage; + var drawLeft = left != null && alpha < 1.0; + var drawRight = right != null && alpha > 0; + + if (drawLeft && drawRight) + { + using (var rt = new RenderTargetBitmap(right.PixelSize, right.Dpi)) + { + var rtRect = new Rect(rt.Size); + using (var dc = rt.CreateDrawingContext()) + { + using (dc.PushRenderOptions(RO_SRC)) + using (dc.PushOpacity(1 - alpha)) + dc.DrawImage(left, rtRect); + + using (dc.PushRenderOptions(RO_DST)) + using (dc.PushOpacity(alpha)) + dc.DrawImage(right, rtRect); + } + + context.DrawImage(rt, rtRect, rect); + } + } + else if (drawLeft) + { + using (context.PushOpacity(1 - alpha)) + context.DrawImage(left, rect); + } + else if (drawRight) + { + using (context.PushOpacity(alpha)) + context.DrawImage(right, rect); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var left = OldImage; + var right = NewImage; + + if (left == null) + return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); + + if (right == null) + return GetDesiredSize(left.Size, availableSize); + + var ls = GetDesiredSize(left.Size, availableSize); + var rs = GetDesiredSize(right.Size, availableSize); + return ls.Width > rs.Width ? ls : rs; + } + + private Size GetDesiredSize(Size img, Size available) + { + var sw = available.Width / img.Width; + var sh = available.Height / img.Height; + var scale = Math.Min(sw, sh); + return new Size(scale * img.Width, scale * img.Height); + } + + private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; + private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; + } +} diff --git a/src/Views/ImageDiffView.axaml b/src/Views/ImageDiffView.axaml index 43cc3d6a..18d8cef9 100644 --- a/src/Views/ImageDiffView.axaml +++ b/src/Views/ImageDiffView.axaml @@ -28,17 +28,14 @@ - + - - - - + @@ -51,17 +48,14 @@ - + - - - - + @@ -81,7 +75,7 @@ - + @@ -89,16 +83,16 @@ - + - + @@ -117,7 +111,7 @@ - + @@ -125,7 +119,7 @@ - + diff --git a/src/Views/ImageDiffView.axaml.cs b/src/Views/ImageDiffView.axaml.cs index bf13e7ba..bc516b5a 100644 --- a/src/Views/ImageDiffView.axaml.cs +++ b/src/Views/ImageDiffView.axaml.cs @@ -5,300 +5,12 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Styling; namespace SourceGit.Views { - public class ImageContainer : Control - { - public override void Render(DrawingContext context) - { - if (_bgBrush == null) - { - var maskBrush = new SolidColorBrush(ActualThemeVariant == ThemeVariant.Dark ? 0xFF404040 : 0xFFBBBBBB); - var bg = new DrawingGroup() - { - Children = - { - new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(0, 0, 12, 12)) }, - new GeometryDrawing() { Brush = maskBrush, Geometry = new RectangleGeometry(new Rect(12, 12, 12, 12)) }, - } - }; + - _bgBrush = new DrawingBrush(bg) - { - AlignmentX = AlignmentX.Left, - AlignmentY = AlignmentY.Top, - DestinationRect = new RelativeRect(new Size(24, 24), RelativeUnit.Absolute), - Stretch = Stretch.None, - TileMode = TileMode.Tile, - }; - } - - context.FillRectangle(_bgBrush, new Rect(Bounds.Size)); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property.Name == "ActualThemeVariant") - { - _bgBrush = null; - InvalidateVisual(); - } - } - - private DrawingBrush _bgBrush = null; - } - - public class ImagesSwipeControl : ImageContainer - { - public static readonly StyledProperty AlphaProperty = - AvaloniaProperty.Register(nameof(Alpha), 0.5); - - public double Alpha - { - get => GetValue(AlphaProperty); - set => SetValue(AlphaProperty, value); - } - - public static readonly StyledProperty OldImageProperty = - AvaloniaProperty.Register(nameof(OldImage)); - - public Bitmap OldImage - { - get => GetValue(OldImageProperty); - set => SetValue(OldImageProperty, value); - } - - public static readonly StyledProperty NewImageProperty = - AvaloniaProperty.Register(nameof(NewImage)); - - public Bitmap NewImage - { - get => GetValue(NewImageProperty); - set => SetValue(NewImageProperty, value); - } - - static ImagesSwipeControl() - { - AffectsMeasure(OldImageProperty, NewImageProperty); - AffectsRender(AlphaProperty); - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - var alpha = Alpha; - var w = Bounds.Width; - var h = Bounds.Height; - var x = w * alpha; - var left = OldImage; - if (left != null && alpha > 0) - { - var src = new Rect(0, 0, left.Size.Width * alpha, left.Size.Height); - var dst = new Rect(0, 0, x, h); - context.DrawImage(left, src, dst); - } - - var right = NewImage; - if (right != null && alpha < 1) - { - var src = new Rect(right.Size.Width * alpha, 0, right.Size.Width * (1 - alpha), right.Size.Height); - var dst = new Rect(x, 0, w - x, h); - context.DrawImage(right, src, dst); - } - - context.DrawLine(new Pen(Brushes.DarkGreen, 2), new Point(x, 0), new Point(x, Bounds.Height)); - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - base.OnPointerPressed(e); - - var p = e.GetPosition(this); - var hitbox = new Rect(Math.Max(Bounds.Width * Alpha - 2, 0), 0, 4, Bounds.Height); - var pointer = e.GetCurrentPoint(this); - if (pointer.Properties.IsLeftButtonPressed && hitbox.Contains(p)) - { - _pressedOnSlider = true; - Cursor = new Cursor(StandardCursorType.SizeWestEast); - e.Pointer.Capture(this); - e.Handled = true; - } - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - base.OnPointerReleased(e); - _pressedOnSlider = false; - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - var w = Bounds.Width; - var p = e.GetPosition(this); - - if (_pressedOnSlider) - { - SetCurrentValue(AlphaProperty, Math.Clamp(p.X, 0, w) / w); - } - else - { - var hitbox = new Rect(Math.Max(w * Alpha - 2, 0), 0, 4, Bounds.Height); - if (hitbox.Contains(p)) - { - if (!_lastInSlider) - { - _lastInSlider = true; - Cursor = new Cursor(StandardCursorType.SizeWestEast); - } - } - else - { - if (_lastInSlider) - { - _lastInSlider = false; - Cursor = null; - } - } - } - } - - protected override Size MeasureOverride(Size availableSize) - { - var left = OldImage; - var right = NewImage; - - if (left == null) - return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); - - if (right == null) - return GetDesiredSize(left.Size, availableSize); - - var ls = GetDesiredSize(left.Size, availableSize); - var rs = GetDesiredSize(right.Size, availableSize); - return ls.Width > rs.Width ? ls : rs; - } - - private Size GetDesiredSize(Size img, Size available) - { - var sw = available.Width / img.Width; - var sh = available.Height / img.Height; - var scale = Math.Min(sw, sh); - return new Size(scale * img.Width, scale * img.Height); - } - - private bool _pressedOnSlider = false; - private bool _lastInSlider = false; - } - - public class ImageBlendControl : ImageContainer - { - public static readonly StyledProperty AlphaProperty = - AvaloniaProperty.Register(nameof(Alpha), 1.0); - - public double Alpha - { - get => GetValue(AlphaProperty); - set => SetValue(AlphaProperty, value); - } - - public static readonly StyledProperty OldImageProperty = - AvaloniaProperty.Register(nameof(OldImage)); - - public Bitmap OldImage - { - get => GetValue(OldImageProperty); - set => SetValue(OldImageProperty, value); - } - - public static readonly StyledProperty NewImageProperty = - AvaloniaProperty.Register(nameof(NewImage)); - - public Bitmap NewImage - { - get => GetValue(NewImageProperty); - set => SetValue(NewImageProperty, value); - } - - static ImageBlendControl() - { - AffectsMeasure(OldImageProperty, NewImageProperty); - AffectsRender(AlphaProperty); - } - - public override void Render(DrawingContext context) - { - base.Render(context); - - var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); - var alpha = Alpha; - var left = OldImage; - var right = NewImage; - var drawLeft = left != null && alpha < 1.0; - var drawRight = right != null && alpha > 0; - - if (drawLeft && drawRight) - { - using (var rt = new RenderTargetBitmap(right.PixelSize, right.Dpi)) - { - var rtRect = new Rect(rt.Size); - using (var dc = rt.CreateDrawingContext()) - { - using (dc.PushRenderOptions(RO_SRC)) - using (dc.PushOpacity(1 - alpha)) - dc.DrawImage(left, rtRect); - - using (dc.PushRenderOptions(RO_DST)) - using (dc.PushOpacity(alpha)) - dc.DrawImage(right, rtRect); - } - - context.DrawImage(rt, rtRect, rect); - } - } - else if (drawLeft) - { - using (context.PushOpacity(1 - alpha)) - context.DrawImage(left, rect); - } - else if (drawRight) - { - using (context.PushOpacity(alpha)) - context.DrawImage(right, rect); - } - } - - protected override Size MeasureOverride(Size availableSize) - { - var left = OldImage; - var right = NewImage; - - if (left == null) - return right == null ? availableSize : GetDesiredSize(right.Size, availableSize); - - if (right == null) - return GetDesiredSize(left.Size, availableSize); - - var ls = GetDesiredSize(left.Size, availableSize); - var rs = GetDesiredSize(right.Size, availableSize); - return ls.Width > rs.Width ? ls : rs; - } - - private Size GetDesiredSize(Size img, Size available) - { - var sw = available.Width / img.Width; - var sh = available.Height / img.Height; - var scale = Math.Min(sw, sh); - return new Size(scale * img.Width, scale * img.Height); - } - - private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; - private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Plus, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality }; - } + public partial class ImageDiffView : UserControl { diff --git a/src/Views/Launcher.axaml b/src/Views/Launcher.axaml index a7ee7626..3d1b1bfd 100644 --- a/src/Views/Launcher.axaml +++ b/src/Views/Launcher.axaml @@ -20,9 +20,9 @@ - + - + + + - + - + diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index 49bcecf0..359a80fa 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -3,6 +3,7 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.VisualTree; namespace SourceGit.Views @@ -220,13 +221,7 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { - var pref = ViewModels.Preference.Instance; - pref.Layout.LauncherWidth = Width; - pref.Layout.LauncherHeight = Height; - - var vm = DataContext as ViewModels.Launcher; - vm?.Quit(); - + (DataContext as ViewModels.Launcher)?.Quit(Width, Height); base.OnClosing(e); } @@ -248,6 +243,17 @@ namespace SourceGit.Views e.Handled = true; } + private void OnOpenWorkspaceMenu(object sender, RoutedEventArgs e) + { + if (sender is Button btn && DataContext is ViewModels.Launcher launcher) + { + var menu = launcher.CreateContextForWorkspace(); + btn.OpenContextMenu(menu); + } + + e.Handled = true; + } + private KeyModifiers _unhandledModifiers = KeyModifiers.None; } } diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index 45cd414a..40a2efe6 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -52,7 +52,7 @@ diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index 67e771ff..18cf64a8 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -52,7 +52,7 @@ - + + + + + + + + - - - + - - + + + + + + + + + @@ -198,7 +235,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -418,51 +375,142 @@ - + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Preference.axaml.cs b/src/Views/Preference.axaml.cs index 25dbfd96..af70ee8e 100644 --- a/src/Views/Preference.axaml.cs +++ b/src/Views/Preference.axaml.cs @@ -128,16 +128,16 @@ namespace SourceGit.Views protected override void OnClosing(WindowClosingEventArgs e) { var config = new Commands.Config(null).ListAll(); - SetIfChanged(config, "user.name", DefaultUser); - SetIfChanged(config, "user.email", DefaultEmail); - SetIfChanged(config, "user.signingkey", GPGUserKey); - SetIfChanged(config, "core.autocrlf", CRLFMode != null ? CRLFMode.Value : null); - SetIfChanged(config, "commit.gpgsign", EnableGPGCommitSigning ? "true" : "false"); - SetIfChanged(config, "tag.gpgsign", EnableGPGTagSigning ? "true" : "false"); - SetIfChanged(config, "gpg.format", GPGFormat.Value); + SetIfChanged(config, "user.name", DefaultUser, ""); + SetIfChanged(config, "user.email", DefaultEmail, ""); + SetIfChanged(config, "user.signingkey", GPGUserKey, ""); + SetIfChanged(config, "core.autocrlf", CRLFMode != null ? CRLFMode.Value : null, null); + SetIfChanged(config, "commit.gpgsign", EnableGPGCommitSigning ? "true" : "false", "false"); + SetIfChanged(config, "tag.gpgsign", EnableGPGTagSigning ? "true" : "false", "false"); + SetIfChanged(config, "gpg.format", GPGFormat.Value, "openpgp"); if (!GPGFormat.Value.Equals("ssh", StringComparison.Ordinal)) - SetIfChanged(config, $"gpg.{GPGFormat.Value}.program", GPGExecutableFile); + SetIfChanged(config, $"gpg.{GPGFormat.Value}.program", GPGExecutableFile, ""); base.OnClosing(e); } @@ -183,7 +183,7 @@ namespace SourceGit.Views e.Handled = true; } - private async void SelectDefaultCloneDir(object _1, RoutedEventArgs _2) + private async void SelectDefaultCloneDir(object _, RoutedEventArgs e) { var options = new FolderPickerOpenOptions() { AllowMultiple = false }; try @@ -194,13 +194,15 @@ namespace SourceGit.Views ViewModels.Preference.Instance.GitDefaultCloneDir = selected[0].Path.LocalPath; } } - catch (Exception e) + catch (Exception ex) { - App.RaiseException(string.Empty, $"Failed to select default clone directory: {e.Message}"); + App.RaiseException(string.Empty, $"Failed to select default clone directory: {ex.Message}"); } + + e.Handled = true; } - private async void SelectGPGExecutable(object _1, RoutedEventArgs _2) + private async void SelectGPGExecutable(object _, RoutedEventArgs e) { var patterns = new List(); if (OperatingSystem.IsWindows()) @@ -219,14 +221,39 @@ namespace SourceGit.Views { GPGExecutableFile = selected[0].Path.LocalPath; } + + e.Handled = true; } - private async void SelectExternalMergeTool(object _1, RoutedEventArgs _2) + private async void SelectShellOrTerminal(object _, RoutedEventArgs e) + { + var type = ViewModels.Preference.Instance.ShellOrTerminal; + if (type == -1) + return; + + var shell = Models.ShellOrTerminal.Supported[type]; + var options = new FilePickerOpenOptions() + { + FileTypeFilter = [new FilePickerFileType(shell.Name) { Patterns = [shell.Exec] }], + AllowMultiple = false, + }; + + var selected = await StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1) + { + ViewModels.Preference.Instance.ShellOrTerminalPath = selected[0].Path.LocalPath; + } + + e.Handled = true; + } + + private async void SelectExternalMergeTool(object _, RoutedEventArgs e) { var type = ViewModels.Preference.Instance.ExternalMergeToolType; if (type < 0 || type >= Models.ExternalMerger.Supported.Count) { ViewModels.Preference.Instance.ExternalMergeToolType = 0; + e.Handled = true; return; } @@ -242,14 +269,16 @@ namespace SourceGit.Views { ViewModels.Preference.Instance.ExternalMergeToolPath = selected[0].Path.LocalPath; } + + e.Handled = true; } - private void SetIfChanged(Dictionary cached, string key, string value) + private void SetIfChanged(Dictionary cached, string key, string value, string defValue) { bool changed = false; if (cached.TryGetValue(key, out var old)) changed = old != value; - else if (!string.IsNullOrEmpty(value)) + else if (!string.IsNullOrEmpty(value) && value != defValue) changed = true; if (changed) diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 44ffe4b6..346639e2 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -225,7 +225,7 @@ - + diff --git a/src/Views/RevisionFileContentViewer.axaml b/src/Views/RevisionFileContentViewer.axaml index 67ba5913..2084a8b2 100644 --- a/src/Views/RevisionFileContentViewer.axaml +++ b/src/Views/RevisionFileContentViewer.axaml @@ -23,16 +23,25 @@ - - - - - - - + + + + + + - + + + + + + + + + + + diff --git a/src/Views/Welcome.axaml b/src/Views/Welcome.axaml index 0e84b790..6bd735ea 100644 --- a/src/Views/Welcome.axaml +++ b/src/Views/Welcome.axaml @@ -117,7 +117,7 @@ diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 7997e64a..60777e41 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -5,7 +5,7 @@ xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" x:Class="SourceGit.Views.WorkingCopy" x:DataType="vm:WorkingCopy"> @@ -42,7 +42,7 @@ Classes="icon_button" Width="26" Height="14" Padding="0" - Command="{Binding StageSelected}"> + Click="OnStageSelectedButtonClicked"> @@ -64,6 +64,8 @@ - - + + + + - - + -