feat(ui): ssh workspaces settings tab
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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>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();
|
||||
foreach (var w in await _worker.GetSshWorkspacesAsync())
|
||||
Workspaces.Add(new SshWorkspaceRowViewModel(w.Id, Remove) { Name = w.Name, Host = w.Host, RemotePath = w.RemotePath });
|
||||
}
|
||||
|
||||
[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.</summary>
|
||||
public Task SaveAsync() => _worker.SetSshWorkspacesAsync(Workspaces.Select(r => r.ToModel()).ToList());
|
||||
|
||||
/// <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());
|
||||
}
|
||||
Reference in New Issue
Block a user