diff --git a/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs
new file mode 100644
index 00000000..2de5223f
--- /dev/null
+++ b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs
@@ -0,0 +1,84 @@
+using Godot;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using System.IO; // For Path.GetFullPath
+
+namespace SharpIDE.Godot.Features.SlnPicker;
+
+public partial class GitCommandLine : Node
+{
+ ///
+ /// Initializes a Git repository in the specified base path by running 'git init'.
+ /// Runs asynchronously to avoid blocking the Godot main thread.
+ ///
+ /// The directory path where the repo should be created.
+ /// A task that completes when the command finishes, with success indicator.
+ public async Task InitializeGitRepoAsync(string basePath)
+ {
+ // Normalize and resolve absolute path for cross-platform reliability
+ basePath = Path.GetFullPath(basePath);
+ // First, check if Git is installed
+ bool gitInstalled = await CheckGitInstalledAsync();
+ if (!gitInstalled)
+ {
+ GD.Print("Git is not installed or not found in PATH. Cannot initialize repo.");
+ return false;
+ }
+
+ // Run 'git init' asynchronously
+ (string output, string error, int exitCode) = await RunGitCommandAsync("init", basePath);
+
+ if (exitCode == 0)
+ {
+ GD.Print($"Git repo initialized successfully in {basePath}. Output: {output}");
+ return true;
+ }
+ else
+ {
+ GD.Print($"Failed to initialize Git repo. Error: {error}");
+ return false;
+ }
+ }
+
+ ///
+ /// Checks if Git is installed by running 'git --version'.
+ ///
+ private async Task CheckGitInstalledAsync()
+ {
+ (string output, string error, int exitCode) = await RunGitCommandAsync("--version", Directory.GetCurrentDirectory());
+ return exitCode == 0 && !string.IsNullOrEmpty(output);
+ }
+
+ ///
+ /// Helper to run any Git command asynchronously in a specified working directory.
+ /// Captures output and errors.
+ ///
+ private async Task<(string Output, string Error, int ExitCode)> RunGitCommandAsync(string arguments, string workingDirectory)
+ {
+ return await Task.Run(() =>
+ {
+ var process = new Process();
+ process.StartInfo.FileName = "git";
+ process.StartInfo.Arguments = arguments;
+ process.StartInfo.WorkingDirectory = workingDirectory;
+ process.StartInfo.UseShellExecute = false; // Required for redirection
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.CreateNoWindow = true; // Hide console window
+
+ try
+ {
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+ return (output, error, process.ExitCode);
+ }
+ catch (Exception ex)
+ {
+ GD.Print($"Exception running Git command: {ex.Message}");
+ return (string.Empty, ex.Message, -1);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs.uid b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs.uid
new file mode 100644
index 00000000..48c98ded
--- /dev/null
+++ b/src/SharpIDE.Godot/Features/SlnPicker/GitCommandLine.cs.uid
@@ -0,0 +1 @@
+uid://btg7j07v7s3ni
diff --git a/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs
new file mode 100644
index 00000000..29fbc58f
--- /dev/null
+++ b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs
@@ -0,0 +1,215 @@
+using System.Diagnostics;
+using Godot;
+using Directory = System.IO.Directory;
+using Exception = System.Exception;
+using GD = Godot.GD;
+using Path = System.IO.Path;
+using System.Text.RegularExpressions; // For parsing SDK list
+using System.Collections.Generic; // For lists
+using System.Linq; // For sorting and selecting
+using File = System.IO.File; // For File.Exists
+
+namespace SharpIDE.Godot.Features.SlnPicker;
+
+public partial class ProjectCreator : Node
+{
+ // Creates a new project with the specified parameters.
+ public void CreateProject(string basePath, string solutionName, string projectName, string template,
+ string language, string sdkVersion, string extraArgs = "")
+ {
+ //"blazor" for Blazor, "webapi" for API, etc.
+
+ // Normalize and resolve absolute paths for cross-platform reliability
+ basePath = Path.GetFullPath(basePath);
+ string solutionDir = Path.Combine(basePath, solutionName);
+ string projectDir = Path.Combine(solutionDir, projectName);
+ string framework = "net" + sdkVersion.Split('.')[0] + ".0";
+
+
+ try
+ {
+ string dotnetPath = GetDotnetPath();
+ GD.Print($"Using dotnet CLI path: {dotnetPath}");
+
+ // Check if dotnet is available at the resolved path
+ if (!IsDotnetAvailable(dotnetPath))
+ {
+ throw new Exception($"dotnet CLI not found at {dotnetPath}. Ensure .NET SDK is installed and update GetDotnetPath() if needed.");
+ }
+
+ // Require sdkVersion to be specified
+ if (string.IsNullOrEmpty(sdkVersion))
+ {
+ throw new Exception("SDK version must be specified (e.g., '8.0.411').");
+ }
+
+ // Validate the specified SDK is installed using the system CLI
+ string installedSdks = GetInstalledSdks(dotnetPath);
+ if (!Regex.IsMatch(installedSdks, $@"\b{Regex.Escape(sdkVersion)}\b"))
+ {
+ OS.Alert($"Specified SDK {sdkVersion} not installed. Available: {installedSdks}. Install from https://dotnet.microsoft.com.","Error");
+ }
+
+ Directory.CreateDirectory(solutionDir);
+ // Pin dotnet conf for solution creation
+ PinGlobalConfig(solutionDir, sdkVersion);
+
+ // Create solution file
+ ExecuteDotnetCommand(dotnetPath, solutionDir, $"new sln -n {solutionName}");
+
+ // Create project subdirectory
+ Directory.CreateDirectory(projectDir);
+
+ // Create project with generic template
+ string projectArgs = $"new {template} -n {projectName} --framework {framework} --language {language} {extraArgs}";
+ ExecuteDotnetCommand(dotnetPath, solutionDir, projectArgs);
+
+ // Delete dotnet conf
+ PinGlobalConfig(solutionDir, sdkVersion,true);
+ // Add project to solution
+ var solutionArgs = $"sln add {projectName}/{projectName}.csproj";
+ ExecuteDotnetCommand(dotnetPath, solutionDir, solutionArgs);
+
+ GD.Print($"Project ({template}) created successfully at {solutionDir}!");
+
+ }
+ catch (Exception ex)
+ {
+ OS.Alert($"Error creating project: {ex.Message}. Folder may be partially created at {solutionDir}. If access denied to template cache, try running 'sudo chown -R $(whoami) ~/.templateengine' or 'rm -rf ~/.templateengine/dotnetcli/' in terminal.","Error");
+ }
+ }
+
+ // Get platform-specific path to system dotnet CLI
+ public string GetDotnetPath()
+ {
+ string osName = OS.GetName();
+ string dotnetPath = "dotnet"; // Fallback
+
+ if (osName == "macOS")
+ {
+ dotnetPath = "/usr/local/share/dotnet/dotnet";
+ }
+ else if (osName == "Linux")
+ {
+ dotnetPath = "/usr/share/dotnet/dotnet";
+ }
+ else if (osName == "Windows")
+ {
+ dotnetPath = @"C:\Program Files\dotnet\dotnet.exe";
+ }
+
+ if (File.Exists(dotnetPath))
+ {
+ return dotnetPath;
+ }
+ else
+ {
+ OS.Alert($"System dotnet not found at {dotnetPath}; falling back to 'dotnet'. Check your install.","Warning");
+ return "dotnet";
+ }
+ }
+
+ // Get list of installed SDKs using specified dotnetPath
+ public string GetInstalledSdks(string dotnetPath)
+ {
+ var processInfo = new ProcessStartInfo
+ {
+ FileName = dotnetPath,
+ Arguments = "--list-sdks",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using (var process = Process.Start(processInfo))
+ {
+ process?.WaitForExit();
+ string output = process?.StandardOutput.ReadToEnd() ?? "";
+ // Parse versions (e.g., lines like "8.0.411 [/path]") with Multiline option
+ var versions = Regex.Matches(output, @"^(\d+\.\d+\.\d+)", RegexOptions.Multiline);
+ return string.Join(", ", versions.Select(m => m.Groups[1].Value));
+ }
+ }
+
+ // Check if dotnet is available using specified dotnetPath
+ private bool IsDotnetAvailable(string dotnetPath)
+ {
+ try
+ {
+ var processInfo = new ProcessStartInfo
+ {
+ FileName = dotnetPath,
+ Arguments = "--version",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ using (var process = Process.Start(processInfo))
+ {
+ process?.WaitForExit();
+ return process?.ExitCode == 0;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ // Execute dotnet commands using specified dotnetPath
+ private void ExecuteDotnetCommand(string dotnetPath, string workingDir, string arguments)
+ {
+ workingDir = Path.GetFullPath(workingDir); // Ensure absolute for OS consistency
+ var processInfo = new ProcessStartInfo
+ {
+ FileName = dotnetPath,
+ Arguments = arguments,
+ WorkingDirectory = workingDir,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ processInfo.EnvironmentVariables["DOTNET_MULTILEVEL_LOOKUP"] = "0";
+
+ using (var process = new Process { StartInfo = processInfo })
+ {
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd();
+ string error = process.StandardError.ReadToEnd();
+ process.WaitForExit();
+
+ if (process.ExitCode != 0) OS.Alert($"dotnet command failed in {workingDir}: {error}", "Error");
+ // if (!string.IsNullOrEmpty(error)) OS.Alert($"Command Error: {error}", "Error");;
+
+ GD.Print($"dotnet output in {workingDir}: {output}");
+ }
+ }
+
+ private void PinGlobalConfig(string solutionDir, string sdkVersion, bool deleteExistingGlobalJson = false)
+ {
+ if (!deleteExistingGlobalJson)
+ {
+ // Create global.json to pin the specified SDK
+ string globalJsonPath = Path.Combine(solutionDir, "global.json");
+ string globalJsonContent = $@"{{
+""sdk"": {{
+ ""version"": ""{sdkVersion}""
+ }}
+}}";
+ File.WriteAllText(globalJsonPath, globalJsonContent);
+ GD.Print($"Pinned SDK to {sdkVersion} via global.json in {solutionDir}");
+ }
+ else
+ {
+ string globalJsonPath = Path.Combine(solutionDir, "global.json");
+ if (File.Exists(globalJsonPath))
+ {
+ File.Delete(globalJsonPath);
+ GD.Print($"Deleted global.json from {solutionDir}");
+ }
+ }
+
+ }
+}
diff --git a/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs.uid b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs.uid
new file mode 100644
index 00000000..9cc4cbf4
--- /dev/null
+++ b/src/SharpIDE.Godot/Features/SlnPicker/ProjectCreator.cs.uid
@@ -0,0 +1 @@
+uid://b07ijdcta2ixt
diff --git a/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs b/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs
index 9966335d..e3418e59 100644
--- a/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs
+++ b/src/SharpIDE.Godot/Features/SlnPicker/SlnPicker.cs
@@ -7,62 +7,91 @@ namespace SharpIDE.Godot.Features.SlnPicker;
// This is a bit of a mess intertwined with the optional popup window
public partial class SlnPicker : Control
{
- private FileDialog _fileDialog = null!;
- private Button _openSlnButton = null!;
- private VBoxContainer _previousSlnsVBoxContainer = null!;
- private Label _versionLabel = null!;
- private static NuGetVersion? _version;
+ private FileDialog _fileDialog = null!;
+ private AcceptDialog _solutionDialog = null!;
+ private Button _newSolutionButton = null!;
+ private Button _openSlnButton = null!;
+ private VBoxContainer _previousSlnsVBoxContainer = null!;
+ private Label _versionLabel = null!;
+ private static NuGetVersion? _version;
- private PackedScene _previousSlnEntryScene = ResourceLoader.Load("res://Features/SlnPicker/PreviousSlnEntry.tscn");
+ private PackedScene _previousSlnEntryScene = ResourceLoader.Load("res://Features/SlnPicker/PreviousSlnEntry.tscn");
- private readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ private readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
- public override void _ExitTree()
- {
- if (!_tcs.Task.IsCompleted) _tcs.SetResult(null);
- }
- public override void _Ready()
- {
- _previousSlnsVBoxContainer = GetNode("%PreviousSlnsVBoxContainer");
- _versionLabel = GetNode