111 lines
4.6 KiB
C#
111 lines
4.6 KiB
C#
using System.Collections.ObjectModel;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Ui.Localization;
|
|
using ClaudeDo.Ui.Services;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
|
|
|
/// <summary>Settings tab for SSH workspaces offered as remote quick sessions in Mission Control.</summary>
|
|
public sealed partial class SshSettingsTabViewModel : ViewModelBase
|
|
{
|
|
private readonly IWorkerClient _worker;
|
|
private readonly Func<string> _readSshConfig;
|
|
|
|
/// <summary>True once the stored workspaces were read; gates <see cref="SaveAsync"/>.</summary>
|
|
private bool _loaded;
|
|
|
|
/// <summary>Configured workspaces, edited in place.</summary>
|
|
public ObservableCollection<SshWorkspaceRowViewModel> Workspaces { get; } = new();
|
|
|
|
/// <summary>Host aliases from the user's ssh config, offered as suggestions.</summary>
|
|
public ObservableCollection<string> Hosts { get; } = new();
|
|
|
|
/// <summary>Creates the tab; <paramref name="readSshConfig"/> defaults to <c>~/.ssh/config</c>.</summary>
|
|
public SshSettingsTabViewModel(IWorkerClient worker, Func<string>? readSshConfig = null)
|
|
{
|
|
_worker = worker;
|
|
_readSshConfig = readSshConfig ?? (() => File.ReadAllText(Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh", "config")));
|
|
}
|
|
|
|
/// <summary>Loads workspaces from the worker and host aliases from the ssh config.</summary>
|
|
public async Task LoadAsync()
|
|
{
|
|
Hosts.Clear();
|
|
try { foreach (var h in ParseHosts(_readSshConfig())) Hosts.Add(h); }
|
|
catch (Exception) { /* no ssh config = no suggestions, hosts can still be typed */ }
|
|
|
|
Workspaces.Clear();
|
|
_loaded = false;
|
|
List<SshWorkspace> stored;
|
|
try { stored = await _worker.GetSshWorkspacesAsync(); }
|
|
catch (Exception) { return; } // worker offline: the modal already reports it, and Save must not wipe what it couldn't read
|
|
foreach (var w in stored)
|
|
Workspaces.Add(new SshWorkspaceRowViewModel(w.Id, Remove) { Name = w.Name, Host = w.Host, RemotePath = w.RemotePath });
|
|
_loaded = true;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Add() => Workspaces.Add(new SshWorkspaceRowViewModel(Guid.NewGuid().ToString("n"), Remove));
|
|
|
|
private void Remove(SshWorkspaceRowViewModel row) => Workspaces.Remove(row);
|
|
|
|
/// <summary>Localized error for the first invalid row, or null.</summary>
|
|
public string? Validate()
|
|
{
|
|
foreach (var row in Workspaces)
|
|
if (SshWorkspace.Validate(row.ToModel()) is { } field)
|
|
return Loc.T($"settings.ssh.invalid.{field}", row.Name);
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Persists all rows via the worker; a no-op when the load failed.</summary>
|
|
public Task SaveAsync() => _loaded
|
|
? _worker.SetSshWorkspacesAsync(Workspaces.Select(r => r.ToModel()).ToList())
|
|
: Task.CompletedTask;
|
|
|
|
/// <summary>Concrete <c>Host</c> aliases from ssh config text; wildcard patterns and <c>Match</c> blocks are skipped.</summary>
|
|
public static List<string> ParseHosts(string configText)
|
|
{
|
|
var hosts = new List<string>();
|
|
foreach (var raw in configText.Split('\n'))
|
|
{
|
|
var parts = raw.Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length < 2 || !parts[0].Equals("Host", StringComparison.OrdinalIgnoreCase)) continue;
|
|
hosts.AddRange(parts.Skip(1).Where(p => p.IndexOfAny(['*', '?', '!']) < 0 && !hosts.Contains(p)));
|
|
}
|
|
return hosts;
|
|
}
|
|
}
|
|
|
|
/// <summary>One editable SSH workspace row.</summary>
|
|
public sealed partial class SshWorkspaceRowViewModel : ObservableObject
|
|
{
|
|
private readonly string _id;
|
|
private readonly Action<SshWorkspaceRowViewModel> _remove;
|
|
|
|
/// <summary>Display name shown in Mission Control.</summary>
|
|
[ObservableProperty] private string _name = "";
|
|
|
|
/// <summary>ssh config host alias.</summary>
|
|
[ObservableProperty] private string _host = "";
|
|
|
|
/// <summary>Absolute or <c>~</c>-relative directory on the server.</summary>
|
|
[ObservableProperty] private string _remotePath = "";
|
|
|
|
/// <summary>Creates a row with a stable id and a callback that removes it from the owner.</summary>
|
|
public SshWorkspaceRowViewModel(string id, Action<SshWorkspaceRowViewModel> remove)
|
|
{
|
|
_id = id;
|
|
_remove = remove;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Remove() => _remove(this);
|
|
|
|
/// <summary>Snapshot as the persisted model, trimmed.</summary>
|
|
public SshWorkspace ToModel() => new(_id, Name.Trim(), Host.Trim(), RemotePath.Trim());
|
|
}
|