Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90c299da83 | ||
|
|
02a695056a | ||
|
|
50a3a7c422 | ||
|
|
3f699f40e3 | ||
|
|
b87ec9f348 | ||
|
|
79dda1d4fd | ||
|
|
5467bafaed | ||
|
|
ac3190111d | ||
|
|
298b402d5d | ||
|
|
a7b734b3bf | ||
|
|
3dd95e994f | ||
|
|
96e32fcc96 |
No files matched your search
@@ -8,5 +8,6 @@
|
||||
public IApplicationDataContainer KeyBindings { get; set; }
|
||||
public IApplicationDataContainer ShellProfiles { get; set; }
|
||||
public IApplicationDataContainer SshProfiles { get; set; }
|
||||
public IApplicationDataContainer HistoryContainer { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,8 @@
|
||||
public const string ShellProfilesContainerName = "ShellProfiles";
|
||||
public const string SshProfilesContainerName = "SshProfiles";
|
||||
public const byte TerminalBufferRequestIdentifier = 0;
|
||||
public const string SshCommandName = "ssh";
|
||||
public const string MoshCommandName = "mosh";
|
||||
public const string ExecutedCommandsContainerName = "HistoryContainer";
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@ namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ISshConnectionInfoDialog
|
||||
{
|
||||
Task<ISshConnectionInfo> GetSshConnectionInfoAsync(ISshConnectionInfo input);
|
||||
Task<SshProfile> GetSshConnectionInfoAsync(SshProfile input = null);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace FluentTerminal.App.Services
|
||||
int Id { get; }
|
||||
string Title { get; set; }
|
||||
|
||||
Task RunOnDispatcherThread(Action action);
|
||||
Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true);
|
||||
|
||||
Task<bool> TryClose();
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace FluentTerminal.App.Services
|
||||
Task<KeyBinding> ShowCreateKeyBindingDialog();
|
||||
Task<string> ShowInputDialogAsync(string title);
|
||||
|
||||
Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync(ISshConnectionInfo input);
|
||||
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null);
|
||||
|
||||
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ISshHelperService
|
||||
{
|
||||
bool IsSsh(Uri uri);
|
||||
|
||||
ISshConnectionInfo ParseSsh(Uri uri);
|
||||
|
||||
Task<SshProfile> GetSshProfileAsync(SshProfile profile);
|
||||
|
||||
Task<SshProfile> GetSavedSshProfileAsync();
|
||||
|
||||
Task<string> ConvertToUriAsync(ISshConnectionInfo sshConnectionInfo);
|
||||
}
|
||||
}
|
||||
@@ -35,5 +35,7 @@ namespace FluentTerminal.App.Services
|
||||
Task<string[]> GetFilesFromSshConfigDirAsync();
|
||||
|
||||
Task<bool> CheckFileExistsAsync(string path);
|
||||
|
||||
void MuteTerminal(bool mute);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
ShowCustomTitleInTitlebar = true,
|
||||
UseMoshByDefault = true,
|
||||
AutoFallbackToWindowsUsernameInLinks = true,
|
||||
RTrimCopiedLines = true
|
||||
UseQuickSshConnectByDefault = false,
|
||||
RTrimCopiedLines = true,
|
||||
MuteTerminalBeeps = true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return dialog.SelectProfile();
|
||||
}
|
||||
|
||||
public Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync(ISshConnectionInfo input) =>
|
||||
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) =>
|
||||
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
|
||||
|
||||
public Task<SshProfile> ShowSshProfileSelectionDialogAsync()
|
||||
|
||||
@@ -138,6 +138,11 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return JsonConvert.DeserializeObject<CommonResponse>((string) responseMessage[MessageKeys.Content]).Success;
|
||||
}
|
||||
|
||||
public void MuteTerminal(bool mute)
|
||||
{
|
||||
_appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest { Mute = mute }));
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
|
||||
{
|
||||
var request = new CreateTerminalRequest
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly ISshHelperService _sshHelperService;
|
||||
private ApplicationSettings _applicationSettings;
|
||||
private string _background;
|
||||
private double _backgroundOpacity;
|
||||
@@ -30,7 +29,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
private string _windowTitle;
|
||||
|
||||
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, ISshHelperService sshHelperService)
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
@@ -46,7 +45,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
ApplicationView = applicationView;
|
||||
_dispatcherTimer = dispatcherTimer;
|
||||
_clipboardService = clipboardService;
|
||||
_sshHelperService = sshHelperService;
|
||||
_keyboardCommandService = keyboardCommandService;
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), async () => await AddLocalTabAsync());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshTab), async () => await AddSshTabAsync());
|
||||
@@ -56,7 +54,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.SavedSshNewTab), async () => await AddSavedSshTerminalAsync());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.SavedSshNewWindow), () => NewWindow(NewWindowAction.ShowSshProfileSelection));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshWindow), () => NewWindow(NewWindowAction.ShowSshInfoDialog));
|
||||
|
||||
|
||||
// Add all of the commands for switching to a tab of a given ID, if there's one open there
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
@@ -246,7 +244,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public async Task AddSshTabAsync()
|
||||
{
|
||||
SshProfile profile = await _sshHelperService.GetSshProfileAsync(new SshProfile());
|
||||
SshProfile profile = await _dialogService.ShowSshConnectionInfoDialogAsync();
|
||||
if (profile == null)
|
||||
{
|
||||
// User selected "Cancel"
|
||||
@@ -260,6 +258,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
await AddTerminalAsync(profile);
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddSshTabOrWindowAsync(Guid shellProfileId)
|
||||
{
|
||||
switch (_applicationSettings.NewTerminalLocation)
|
||||
@@ -277,7 +276,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public async Task AddSavedSshTerminalAsync()
|
||||
{
|
||||
ShellProfile profile = await _sshHelperService.GetSavedSshProfileAsync();
|
||||
ShellProfile profile = await _dialogService.ShowSshProfileSelectionDialogAsync();
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
public class ExecutedCommand
|
||||
{
|
||||
public string Command { get; set; }
|
||||
|
||||
public string Args { get; set; }
|
||||
|
||||
public DateTime LastExecution { get; set; }
|
||||
|
||||
public int ExecutionCount { get; set; }
|
||||
}
|
||||
|
||||
public class ExecutedCommandHistory
|
||||
{
|
||||
public List<ExecutedCommand> ExecutedCommands { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for common profiles (PowerShell, CMD, WSL).
|
||||
/// </summary>
|
||||
public class CommonProfileProviderViewModel : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
public bool PreInstalled { get; private set; }
|
||||
|
||||
private string _location;
|
||||
|
||||
public string Location
|
||||
{
|
||||
get => _location;
|
||||
set => Set(ref _location, value);
|
||||
}
|
||||
|
||||
private string _workingDirectory;
|
||||
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get => _workingDirectory;
|
||||
set => Set(ref _workingDirectory, value);
|
||||
}
|
||||
|
||||
private string _arguments;
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => _arguments;
|
||||
set => Set(ref _arguments, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForCustomShellCommand { get; }
|
||||
|
||||
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructor
|
||||
|
||||
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
|
||||
original)
|
||||
{
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
|
||||
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
|
||||
|
||||
Initialize(Model);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
private void Initialize(ShellProfile profile)
|
||||
{
|
||||
PreInstalled = profile.PreInstalled;
|
||||
Location = profile.Location;
|
||||
Arguments = profile.Arguments;
|
||||
WorkingDirectory = profile.WorkingDirectory;
|
||||
}
|
||||
|
||||
protected override void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
base.LoadFromProfile(profile);
|
||||
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected override void CopyToProfile(ShellProfile profile)
|
||||
{
|
||||
base.CopyToProfile(profile);
|
||||
|
||||
profile.Location = _location;
|
||||
profile.Arguments = _arguments;
|
||||
profile.WorkingDirectory = _workingDirectory;
|
||||
}
|
||||
|
||||
public override async Task<string> ValidateAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_location))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
error = I18N.Translate("LocationCantBeEmpty");
|
||||
|
||||
return string.IsNullOrEmpty(error) ? "Location cannot be empty." : error;
|
||||
}
|
||||
|
||||
public override bool HasChanges()
|
||||
{
|
||||
return base.HasChanges() || !Model.Location.NullableEqualTo(_location) ||
|
||||
!Model.WorkingDirectory.NullableEqualTo(_workingDirectory) ||
|
||||
!Model.Arguments.NullableEqualTo(_arguments);
|
||||
}
|
||||
|
||||
private async Task BrowseForCustomShell()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
Location = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseForWorkingDirectory()
|
||||
{
|
||||
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
|
||||
if (directory != null)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all profile view model classes. It contains properties shared by all other view models
|
||||
/// (theme-related properties, line-ending translation, and WinPTY/ConPTY selection).
|
||||
/// </summary>
|
||||
public abstract class ProfileProviderViewModelBase : ViewModelBase
|
||||
{
|
||||
#region Static
|
||||
|
||||
private static readonly LineEndingStyle[] LineEndingStylesArray =
|
||||
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Fields
|
||||
|
||||
protected readonly ISettingsService SettingsService;
|
||||
protected readonly IApplicationView ApplicationView;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
private ShellProfile _model;
|
||||
|
||||
public ShellProfile Model
|
||||
{
|
||||
get => _model;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (ReferenceEquals(_model, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_model != null && _model.GetType() != value.GetType())
|
||||
{
|
||||
throw new ArgumentException($"The value has to be of type {_model.GetType().Name}.");
|
||||
}
|
||||
|
||||
_model = value;
|
||||
|
||||
LoadFromProfile(value);
|
||||
}
|
||||
}
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
|
||||
|
||||
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
|
||||
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
|
||||
|
||||
private LineEndingStyle _lineEndingTranslation;
|
||||
|
||||
public LineEndingStyle LineEndingTranslation
|
||||
{
|
||||
get => _lineEndingTranslation;
|
||||
set => Set(ref _lineEndingTranslation, value);
|
||||
}
|
||||
|
||||
private TabTheme _selectedTabTheme;
|
||||
|
||||
public TabTheme SelectedTabTheme
|
||||
{
|
||||
get => _selectedTabTheme ?? (_selectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(_tabThemeId)));
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// Ignore attempt setting null.
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that it's from the list
|
||||
var theme = TabThemes.Contains(value)
|
||||
? value
|
||||
: TabThemes.FirstOrDefault(t => t.Id == value.Id) ?? TabThemes.First();
|
||||
|
||||
if (Set(ref _selectedTabTheme, theme))
|
||||
{
|
||||
_tabThemeId = theme.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _tabThemeId;
|
||||
|
||||
public int TabThemeId
|
||||
{
|
||||
get => _tabThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _tabThemeId, value))
|
||||
{
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalTheme _selectedTerminalTheme;
|
||||
|
||||
public TerminalTheme SelectedTerminalTheme
|
||||
{
|
||||
get => _selectedTerminalTheme ??
|
||||
(_selectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(_terminalThemeId)));
|
||||
set
|
||||
{
|
||||
TerminalTheme theme = value;
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
// How to handle attempt to setting null theme?
|
||||
// 1) Throw an exception:
|
||||
//throw new ArgumentNullException();
|
||||
// 2) Ignore the attempt:
|
||||
//return;
|
||||
// 3) Use default theme (probably the best):
|
||||
theme = TerminalThemes.First();
|
||||
}
|
||||
else if (!TerminalThemes.Contains(theme))
|
||||
{
|
||||
// Ensure that it's from the list
|
||||
theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(theme.Id)) ?? TerminalThemes.First();
|
||||
}
|
||||
|
||||
if (Set(ref _selectedTerminalTheme, theme))
|
||||
{
|
||||
_terminalThemeId = theme.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Guid _terminalThemeId;
|
||||
|
||||
public Guid TerminalThemeId
|
||||
{
|
||||
get => _terminalThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _terminalThemeId, value))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _useConPty;
|
||||
|
||||
public bool UseConPty
|
||||
{
|
||||
get => _useConPty;
|
||||
set => Set(ref _useConPty, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected ProfileProviderViewModelBase(ISettingsService settingsService, IApplicationView applicationView,
|
||||
ShellProfile original = null)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
ApplicationView = applicationView;
|
||||
|
||||
_model = original ?? new ShellProfile();
|
||||
|
||||
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
|
||||
|
||||
SelectedTabTheme = TabThemes.First();
|
||||
|
||||
TerminalThemes = new ObservableCollection<TerminalTheme>(settingsService.GetThemes());
|
||||
|
||||
TerminalThemes.Insert(0, new TerminalTheme { Id = Guid.Empty, Name = "Default" });
|
||||
|
||||
SelectedTerminalTheme = TerminalThemes.First();
|
||||
|
||||
settingsService.ThemeAdded += OnThemeAdded;
|
||||
settingsService.ThemeDeleted += OnThemeDeleted;
|
||||
|
||||
Initialize(Model);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
private void OnThemeDeleted(object sender, Guid e)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(e));
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TerminalThemes.Remove(theme);
|
||||
|
||||
if (_terminalThemeId.Equals(e))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.First();
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
private void OnThemeAdded(object sender, TerminalTheme e)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => TerminalThemes.Add(e), false);
|
||||
}
|
||||
|
||||
private void Initialize(ShellProfile profile)
|
||||
{
|
||||
LineEndingTranslation = profile.LineEndingTranslation;
|
||||
UseConPty = profile.UseConPty;
|
||||
TerminalThemeId = profile.TerminalThemeId;
|
||||
TabThemeId = profile.TabThemeId;
|
||||
}
|
||||
|
||||
protected virtual void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected virtual void CopyToProfile(ShellProfile profile)
|
||||
{
|
||||
profile.LineEndingTranslation = _lineEndingTranslation;
|
||||
profile.UseConPty = _useConPty;
|
||||
profile.TerminalThemeId = _terminalThemeId;
|
||||
profile.TabThemeId = _tabThemeId;
|
||||
}
|
||||
|
||||
public virtual Task<string> ValidateAsync()
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if view model values aren't equal to the corresponding values of the
|
||||
/// underlying <see cref="ShellProfile"/>.
|
||||
/// </summary>
|
||||
public virtual bool HasChanges()
|
||||
{
|
||||
return Model.LineEndingTranslation != _lineEndingTranslation ||
|
||||
Model.UseConPty != _useConPty ||
|
||||
!Model.TerminalThemeId.Equals(_terminalThemeId) ||
|
||||
Model.TabThemeId != _tabThemeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies values from the view model to the underlying <see cref="ShellProfile"/>.
|
||||
/// </summary>
|
||||
/// <param name="acceptIfInvalid">Defines what will happen if the view model data is invalid (if
|
||||
/// <see cref="ValidateAsync"/> returns non-empty string). If this parameter is set to <c>true</c>, the
|
||||
/// data will be copied to the underlying <see cref="ShellProfile"/> anyway. If it's set to <c>false</c>,
|
||||
/// (default) the underlying <see cref="ShellProfile"/> won't be changed at all.</param>
|
||||
/// <returns><c>null</c> or empty string if the operation was successful (the data is valid), or an error
|
||||
/// message returned by <see cref="ValidateAsync"/> method.</returns>
|
||||
public async Task<string> AcceptChangesAsync(bool acceptIfInvalid = false)
|
||||
{
|
||||
var error = await ValidateAsync();
|
||||
|
||||
if (acceptIfInvalid || string.IsNullOrEmpty(error))
|
||||
{
|
||||
CopyToProfile(Model);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public void RejectChanges()
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => LoadFromProfile(Model), false);
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Links/shortcuts related
|
||||
|
||||
private const string UseConPtyQueryStringName = "conpty";
|
||||
private const string LineEndingQueryStringName = "lineending";
|
||||
private const string TerminalThemeIdQueryStringName = "theme";
|
||||
private const string TabThemeIdQueryStringName = "tab";
|
||||
|
||||
private const string ShortcutFileFormat = @"[{{000214A0-0000-0000-C000-000000000046}}]
|
||||
Prop3=19,0
|
||||
[InternetShortcut]
|
||||
IDList=
|
||||
URL={0}
|
||||
";
|
||||
|
||||
public static string GetShortcutFileContent(string url) => string.Format(ShortcutFileFormat, url);
|
||||
|
||||
public static IEnumerable<Tuple<string, string>> ParseParams(string uriOpts, char separator) =>
|
||||
uriOpts.Split(separator).Select(ParseSshOptionFromUri).Where(p => p != null);
|
||||
|
||||
private static Tuple<string, string> ParseSshOptionFromUri(string option)
|
||||
{
|
||||
string[] nv = option.Split('=');
|
||||
|
||||
if (nv.Length != 2 || string.IsNullOrEmpty(nv[0]))
|
||||
{
|
||||
// For now simply ignore invalid options
|
||||
return null;
|
||||
//throw new FormatException($"Invalid SSH option '{option}'.");
|
||||
}
|
||||
|
||||
return Tuple.Create(HttpUtility.UrlDecode(nv[0]), HttpUtility.UrlDecode(nv[1]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates URL.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Tuple"/> whose first item represents success. If the first item is <c>true</c>,
|
||||
/// the second contains the URL. If the first item is <c>false</c>, the second contains error message.</returns>
|
||||
public virtual Task<Tuple<bool, string>> GetUrlAsync() =>
|
||||
Task.FromResult(Tuple.Create(false, "Not supported."));
|
||||
|
||||
public string GetBaseQueryString()
|
||||
{
|
||||
var queryString =
|
||||
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
|
||||
|
||||
if (_tabThemeId != TabThemes.First().Id)
|
||||
{
|
||||
queryString += $"&{TabThemeIdQueryStringName}={_tabThemeId:##########}";
|
||||
}
|
||||
|
||||
if (!_terminalThemeId.Equals(TerminalThemes.First().Id))
|
||||
{
|
||||
queryString += $"&{TerminalThemeIdQueryStringName}={_terminalThemeId}";
|
||||
}
|
||||
|
||||
return queryString;
|
||||
}
|
||||
|
||||
public void LoadBaseFromQueryString(IList<Tuple<string, string>> queryStringParams)
|
||||
{
|
||||
if (queryStringParams == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
UseConPtyQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useConPty))
|
||||
{
|
||||
UseConPty = useConPty;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
LineEndingQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && Enum.TryParse(keyValue.Item2, true, out LineEndingStyle lineEndingTranslation))
|
||||
{
|
||||
LineEndingTranslation = lineEndingTranslation;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
TabThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && int.TryParse(keyValue.Item2, out int tabThemeId))
|
||||
{
|
||||
TabThemeId = tabThemeId;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
TerminalThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && Guid.TryParse(keyValue.Item2, out Guid terminalThemeId))
|
||||
{
|
||||
TerminalThemeId = terminalThemeId;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Links/shortcuts related
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,860 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// View-model for rich-UI based SSH profiles.
|
||||
/// </summary>
|
||||
public class SshConnectViewModel : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Static
|
||||
|
||||
private static readonly Regex CommandValidationRx = new Regex(@"^(?<cmd>[^\s\.]+)(\.exe)?(\s+(?<args>\S.*))?$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly string[] AcceptableCommands = { Constants.SshCommandName };
|
||||
|
||||
public static string GetErrorString(SshConnectionInfoValidationResult result, string separator = "; ") =>
|
||||
string.Join(separator, GetErrors(result));
|
||||
|
||||
public static IEnumerable<string> GetErrors(SshConnectionInfoValidationResult result)
|
||||
{
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var value in Enum.GetValues(typeof(SshConnectionInfoValidationResult))
|
||||
.Cast<SshConnectionInfoValidationResult>().Where(r => r != SshConnectionInfoValidationResult.Valid))
|
||||
{
|
||||
if ((value & result) == value)
|
||||
{
|
||||
yield return I18N.Translate($"{nameof(SshConnectionInfoValidationResult)}.{value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly IApplicationDataContainer _historyContainer;
|
||||
|
||||
private bool _commandInputOriginal;
|
||||
|
||||
// To prevent validating the existence of the same file multiple times because it's kinda expensive
|
||||
private string _validatedIdentityFile;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
private bool _commandInput;
|
||||
|
||||
public bool CommandInput
|
||||
{
|
||||
get => _commandInput;
|
||||
set
|
||||
{
|
||||
if (Set(ref _commandInput, value) && _useMosh)
|
||||
{
|
||||
RaisePropertyChanged(nameof(MoshVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Quick SSH properties
|
||||
|
||||
private string _command;
|
||||
|
||||
public string Command
|
||||
{
|
||||
get => _command;
|
||||
set => Set(ref _command, value);
|
||||
}
|
||||
|
||||
#endregion Quick SSH properties
|
||||
|
||||
#region Full UI properties
|
||||
|
||||
private string _host;
|
||||
|
||||
public string Host
|
||||
{
|
||||
get => _host;
|
||||
set => Set(ref _host, value);
|
||||
}
|
||||
|
||||
private ushort _sshPort;
|
||||
|
||||
public ushort SshPort
|
||||
{
|
||||
get => _sshPort;
|
||||
set => Set(ref _sshPort, value);
|
||||
}
|
||||
|
||||
private string _username;
|
||||
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => Set(ref _username, value);
|
||||
}
|
||||
|
||||
private string _identityFile;
|
||||
|
||||
public string IdentityFile
|
||||
{
|
||||
get => _identityFile;
|
||||
set => Set(ref _identityFile, value);
|
||||
}
|
||||
|
||||
private bool _useMosh;
|
||||
|
||||
public bool UseMosh
|
||||
{
|
||||
get => _useMosh;
|
||||
set
|
||||
{
|
||||
if (Set(ref _useMosh, value) && !_commandInput)
|
||||
{
|
||||
RaisePropertyChanged(nameof(MoshVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ushort _moshPortFrom;
|
||||
|
||||
public ushort MoshPortFrom
|
||||
{
|
||||
get => _moshPortFrom;
|
||||
set => Set(ref _moshPortFrom, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortTo;
|
||||
|
||||
public ushort MoshPortTo
|
||||
{
|
||||
get => _moshPortTo;
|
||||
set => Set(ref _moshPortTo, value);
|
||||
}
|
||||
|
||||
#endregion Full UI properties
|
||||
|
||||
public bool MoshVisible => _useMosh && !_commandInput;
|
||||
|
||||
public ObservableCollection<string> CommandHistory { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForIdentityFileCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructors
|
||||
|
||||
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
|
||||
IApplicationDataContainer historyContainer, SshProfile original = null) : base(settingsService, applicationView,
|
||||
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_historyContainer = historyContainer;
|
||||
|
||||
if (string.IsNullOrEmpty(Model.Location))
|
||||
{
|
||||
_commandInput = settingsService.GetApplicationSettings().UseQuickSshConnectByDefault;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(((SshProfile)Model).Host))
|
||||
{
|
||||
_commandInput = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_commandInput = false;
|
||||
}
|
||||
|
||||
_commandInputOriginal = _commandInput;
|
||||
|
||||
FillCommandHistory();
|
||||
|
||||
Initialize((SshProfile)Model);
|
||||
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
|
||||
}
|
||||
|
||||
private SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
|
||||
IApplicationDataContainer historyContainer, bool useCommandInput) : base(settingsService, applicationView,
|
||||
new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_historyContainer = historyContainer;
|
||||
_commandInput = useCommandInput;
|
||||
_commandInputOriginal = useCommandInput;
|
||||
|
||||
FillCommandHistory();
|
||||
|
||||
Initialize((SshProfile)Model);
|
||||
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Methods
|
||||
|
||||
// Fills the view model properties from the input sshProfile
|
||||
private void Initialize(SshProfile sshProfile)
|
||||
{
|
||||
Command = string.IsNullOrEmpty(sshProfile.Location) ? string.Empty : $"{sshProfile.Location} {sshProfile.Arguments}";
|
||||
|
||||
Host = sshProfile.Host;
|
||||
SshPort = sshProfile.SshPort;
|
||||
|
||||
Username = sshProfile.Username;
|
||||
|
||||
if (string.IsNullOrEmpty(Username))
|
||||
{
|
||||
_trayProcessCommunicationService.GetUserName().ContinueWith(t =>
|
||||
{
|
||||
var username = t.Result;
|
||||
|
||||
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => Username = username, false);
|
||||
}
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
|
||||
IdentityFile = sshProfile.IdentityFile;
|
||||
UseMosh = sshProfile.UseMosh;
|
||||
MoshPortFrom = sshProfile.MoshPortFrom;
|
||||
MoshPortTo = sshProfile.MoshPortTo;
|
||||
}
|
||||
|
||||
private async Task BrowseForIdentityFile()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { "*" });
|
||||
if (file != null)
|
||||
{
|
||||
IdentityFile = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetArgumentsString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (_sshPort != SshProfile.DefaultSshPort)
|
||||
sb.Append($"-p {_sshPort:#####} ");
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
sb.Append($"-i \"{_identityFile}\" ");
|
||||
|
||||
sb.Append($"{_username}@{_host}");
|
||||
|
||||
if (_useMosh)
|
||||
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
protected override void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
base.LoadFromProfile(profile);
|
||||
|
||||
CommandInput = _commandInputOriginal;
|
||||
|
||||
Initialize((SshProfile)profile);
|
||||
}
|
||||
|
||||
protected override void CopyToProfile(ShellProfile profile)
|
||||
{
|
||||
base.CopyToProfile(profile);
|
||||
|
||||
_commandInputOriginal = _commandInput;
|
||||
|
||||
if (_commandInput)
|
||||
{
|
||||
CopyToProfileQuick((SshProfile)profile);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyToProfileFull((SshProfile)profile);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyToProfileFull(SshProfile sshProfile)
|
||||
{
|
||||
sshProfile.Location = _useMosh ? Constants.MoshCommandName : Constants.SshCommandName;
|
||||
sshProfile.Arguments = GetArgumentsString();
|
||||
|
||||
sshProfile.WorkingDirectory = null;
|
||||
|
||||
sshProfile.Host = _host;
|
||||
sshProfile.SshPort = _sshPort;
|
||||
sshProfile.Username = _username;
|
||||
sshProfile.IdentityFile = _identityFile;
|
||||
sshProfile.UseMosh = _useMosh;
|
||||
sshProfile.MoshPortFrom = _moshPortFrom;
|
||||
sshProfile.MoshPortTo = _moshPortTo;
|
||||
}
|
||||
|
||||
private void CopyToProfileQuick(SshProfile sshProfile)
|
||||
{
|
||||
var command = _command?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
sshProfile.Location = null;
|
||||
sshProfile.Arguments = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var match = CommandValidationRx.Match(command);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
// Should not happen ever because this method gets called only if validation succeeds.
|
||||
throw new Exception("Invalid command.");
|
||||
}
|
||||
|
||||
sshProfile.Location = match.Groups["cmd"].Value;
|
||||
sshProfile.Arguments = match.Groups["args"].Success ? match.Groups["args"].Value.Trim() : null;
|
||||
|
||||
sshProfile.WorkingDirectory = null;
|
||||
|
||||
sshProfile.Host = null;
|
||||
sshProfile.SshPort = 0;
|
||||
sshProfile.Username = null;
|
||||
sshProfile.IdentityFile = null;
|
||||
sshProfile.UseMosh = false;
|
||||
sshProfile.MoshPortFrom = 0;
|
||||
sshProfile.MoshPortTo = 0;
|
||||
}
|
||||
|
||||
public override Task<string> ValidateAsync()
|
||||
{
|
||||
return _commandInput ? ValidateQuickAsync() : ValidateFullAsync();
|
||||
}
|
||||
|
||||
private async Task<string> ValidateFullAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync();
|
||||
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
error = GetErrorString(result, Environment.NewLine);
|
||||
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
error = "Invalid input.";
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
private async Task<string> ValidateQuickAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
var match = CommandValidationRx.Match(_command?.Trim() ?? string.Empty);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
error = I18N.Translate("InvalidCommand");
|
||||
|
||||
return string.IsNullOrEmpty(error) ? "Invalid command." : error;
|
||||
}
|
||||
|
||||
if (!AcceptableCommands.Any(c => c.Equals(match.Groups["cmd"].Value, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
error = I18N.Translate("UnsupportedCommand");
|
||||
|
||||
return string.IsNullOrEmpty(error)
|
||||
? $"Unsupported command: {match.Groups["cmd"]}."
|
||||
: $"{error} {match.Groups["cmd"]}";
|
||||
}
|
||||
|
||||
if (!match.Groups["args"].Success)
|
||||
{
|
||||
error = I18N.Translate("CommandArgumentsMandatory");
|
||||
|
||||
return string.IsNullOrEmpty(error) ? "Command arguments are missing." : error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override bool HasChanges()
|
||||
{
|
||||
// ReSharper disable once ArrangeRedundantParentheses
|
||||
if (base.HasChanges() || (_commandInput != _commandInputOriginal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_commandInput)
|
||||
{
|
||||
return !_command.NullableEqualTo($"{Model.Location} {Model.Arguments}");
|
||||
}
|
||||
|
||||
var original = (SshProfile)Model;
|
||||
|
||||
return base.HasChanges() || !original.Host.NullableEqualTo(_host) || original.SshPort != _sshPort ||
|
||||
!original.Username.NullableEqualTo(_username) ||
|
||||
!original.IdentityFile.NullableEqualTo(_identityFile) || original.UseMosh != _useMosh ||
|
||||
original.MoshPortFrom != _moshPortFrom || original.MoshPortTo != _moshPortTo;
|
||||
}
|
||||
|
||||
public async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
|
||||
{
|
||||
var result = SshConnectionInfoValidationResult.Valid;
|
||||
|
||||
if (string.IsNullOrEmpty(_username))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.UsernameEmpty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_host))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.HostEmpty;
|
||||
}
|
||||
|
||||
if (_sshPort < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (!await CheckIdentityFileExistsAsync())
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
|
||||
}
|
||||
|
||||
if (!_useMosh)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (_moshPortFrom < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (_moshPortFrom > _moshPortTo)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortRangeInvalid;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> CheckIdentityFileExistsAsync()
|
||||
{
|
||||
var identityFile = _identityFile;
|
||||
|
||||
if (string.IsNullOrEmpty(identityFile) ||
|
||||
identityFile.Equals(_validatedIdentityFile, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Here we need to take into account that files from ssh config dir can be provided by name, without full path.
|
||||
string fullPath;
|
||||
|
||||
if (Path.IsPathRooted(identityFile))
|
||||
{
|
||||
fullPath = identityFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(sshConfigDir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
fullPath = Path.Combine(sshConfigDir, identityFile);
|
||||
}
|
||||
|
||||
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
|
||||
IdentityFile = fullPath;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetValidatedIdentityFile(string identityFile)
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
IdentityFile = identityFile;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Command history
|
||||
|
||||
private const string ExecutedCommandsKey = "ExecutedCommands";
|
||||
private const int CommandHistoryLimit = 20;
|
||||
|
||||
private void FillCommandHistory()
|
||||
{
|
||||
var commands = _historyContainer.ReadValueFromJson<ExecutedCommandHistory>(ExecutedCommandsKey, null)
|
||||
?.ExecutedCommands;
|
||||
|
||||
CommandHistory = commands == null
|
||||
? new ObservableCollection<string>()
|
||||
: new ObservableCollection<string>(commands.Select(c => $"{c.Command} {c.Args}"));
|
||||
}
|
||||
|
||||
public void SaveCommand(string cmd, string args)
|
||||
{
|
||||
cmd = cmd?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(cmd))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cmd));
|
||||
}
|
||||
|
||||
var commandHistory =
|
||||
_historyContainer.ReadValueFromJson<ExecutedCommandHistory>(ExecutedCommandsKey, null) ??
|
||||
new ExecutedCommandHistory { ExecutedCommands = new List<ExecutedCommand>() };
|
||||
|
||||
var command = commandHistory.ExecutedCommands.FirstOrDefault(c =>
|
||||
string.Equals(cmd, c.Command, StringComparison.Ordinal) && args.NullableEqualTo(c.Args));
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
command = new ExecutedCommand
|
||||
{ Command = cmd, Args = args, ExecutionCount = 1, LastExecution = DateTime.UtcNow };
|
||||
}
|
||||
else
|
||||
{
|
||||
command.ExecutionCount++;
|
||||
|
||||
command.LastExecution = DateTime.UtcNow;
|
||||
|
||||
commandHistory.ExecutedCommands.Remove(command);
|
||||
}
|
||||
|
||||
commandHistory.ExecutedCommands.Insert(0, command);
|
||||
|
||||
while (commandHistory.ExecutedCommands.Count > CommandHistoryLimit)
|
||||
{
|
||||
commandHistory.ExecutedCommands.RemoveAt(CommandHistoryLimit);
|
||||
}
|
||||
|
||||
_historyContainer.WriteValueAsJson(ExecutedCommandsKey, commandHistory);
|
||||
}
|
||||
|
||||
#endregion Command history
|
||||
|
||||
#region Links/shortcuts related
|
||||
|
||||
// Resources:
|
||||
// https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04#section-3
|
||||
// https://man.openbsd.org/ssh
|
||||
|
||||
private const string SshUriScheme = "ssh";
|
||||
private const string MoshUriScheme = "mosh";
|
||||
|
||||
private const string CustomSshUriScheme = "sshft";
|
||||
private const string CustomUriHost = "fluent.terminal";
|
||||
private const string CustomCommandQueryStringName = "cmd";
|
||||
private const string CustomArgumentsQueryStringName = "args";
|
||||
|
||||
// Constant derived from https://man.openbsd.org/ssh
|
||||
private const string IdentityFileOptionName = "IdentityFile";
|
||||
|
||||
private static readonly string[] ValidMoshPortsNames = { "mosh_ports", "mosh-ports" };
|
||||
|
||||
private static readonly Regex MoshRangeRx =
|
||||
new Regex(@"^(?<from>\d{1,5})[:-](?<to>\d{1,5})$", RegexOptions.Compiled);
|
||||
|
||||
public static bool CheckScheme(Uri uri) => CheckSchemeStandard(uri) || CheckSchemeCustom(uri);
|
||||
|
||||
private static bool CheckSchemeStandard(Uri uri) =>
|
||||
SshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase) ||
|
||||
MoshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool CheckSchemeCustom(Uri uri) =>
|
||||
CustomSshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static SshConnectViewModel ParseUri(Uri uri, ISettingsService settingsService,
|
||||
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
|
||||
{
|
||||
if (CheckSchemeStandard(uri))
|
||||
{
|
||||
return ParseUriStandard(uri, settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer);
|
||||
}
|
||||
|
||||
if (CheckSchemeCustom(uri))
|
||||
{
|
||||
return ParseUriCustom(uri, settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer);
|
||||
}
|
||||
|
||||
// Won't happen ever
|
||||
throw new Exception($"Unsupported URI scheme: {uri.Scheme}");
|
||||
}
|
||||
|
||||
private static SshConnectViewModel ParseUriStandard(Uri uri, ISettingsService settingsService,
|
||||
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
|
||||
{
|
||||
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer, false)
|
||||
{
|
||||
Host = uri.Host,
|
||||
UseMosh = MoshUriScheme.Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
|
||||
if (uri.Port >= 0)
|
||||
vm.SshPort = (ushort)uri.Port;
|
||||
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
string[] parts = uri.UserInfo.Split(';');
|
||||
|
||||
if (parts.Length > 2)
|
||||
throw new FormatException($"UserInfo part contains {parts.Length} elements.");
|
||||
|
||||
vm.Username = HttpUtility.UrlDecode(parts[0]);
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
// For now we are only interested in IdentityFile option
|
||||
Tuple<string, string> identityFileOption = ParseParams(parts[1], ',').FirstOrDefault(p =>
|
||||
string.Equals(p.Item1, IdentityFileOptionName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
vm.IdentityFile = identityFileOption?.Item2;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once ConstantConditionalAccessQualifier
|
||||
string queryString = uri.Query?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(queryString))
|
||||
{
|
||||
return vm;
|
||||
}
|
||||
|
||||
if (queryString.StartsWith("?", StringComparison.Ordinal))
|
||||
{
|
||||
queryString = queryString.Substring(1);
|
||||
}
|
||||
|
||||
var queryStringParams = ParseParams(queryString, '&').ToList();
|
||||
|
||||
var moshPorts = queryStringParams.FirstOrDefault(p =>
|
||||
ValidMoshPortsNames.Any(n => string.Equals(p.Item1, n, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (moshPorts != null)
|
||||
{
|
||||
Match match = MoshRangeRx.Match(moshPorts.Item2);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
vm.MoshPortFrom = ushort.Parse(match.Groups["from"].Value);
|
||||
vm.MoshPortTo = ushort.Parse(match.Groups["to"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
vm.LoadBaseFromQueryString(queryStringParams);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static SshConnectViewModel ParseUriCustom(Uri uri, ISettingsService settingsService,
|
||||
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
|
||||
{
|
||||
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer, true);
|
||||
|
||||
// ReSharper disable once ConstantConditionalAccessQualifier
|
||||
string queryString = uri.Query?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(queryString))
|
||||
{
|
||||
return vm;
|
||||
}
|
||||
|
||||
if (queryString.StartsWith("?", StringComparison.Ordinal))
|
||||
{
|
||||
queryString = queryString.Substring(1);
|
||||
}
|
||||
|
||||
var queryStringParams = ParseParams(queryString, '&').ToList();
|
||||
|
||||
var cmdParam = queryStringParams.FirstOrDefault(t =>
|
||||
CustomCommandQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var argsParam = queryStringParams.FirstOrDefault(t =>
|
||||
CustomArgumentsQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
vm._command =
|
||||
$"{(string.IsNullOrEmpty(cmdParam?.Item2) ? Constants.SshCommandName : cmdParam.Item2)} {argsParam?.Item2}"
|
||||
.Trim();
|
||||
|
||||
vm.LoadBaseFromQueryString(queryStringParams);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
public override Task<Tuple<bool, string>> GetUrlAsync()
|
||||
{
|
||||
return _commandInput ? GetUrlQuickAsync() : GetUrlFullAsync();
|
||||
}
|
||||
|
||||
private async Task<Tuple<bool, string>> GetUrlFullAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync();
|
||||
|
||||
if (result != SshConnectionInfoValidationResult.Valid &&
|
||||
// For links we can ignore missing username
|
||||
result != SshConnectionInfoValidationResult.UsernameEmpty)
|
||||
{
|
||||
|
||||
error = GetErrorString(result, Environment.NewLine);
|
||||
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
error = "Invalid input.";
|
||||
}
|
||||
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
|
||||
|
||||
sb.Append("://");
|
||||
|
||||
bool containsUserInfo = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
sb.Append(HttpUtility.UrlEncode(_username));
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
sb.Append(";");
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
sb.Append($"{IdentityFileOptionName}={HttpUtility.UrlEncode(_identityFile)}");
|
||||
}
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (containsUserInfo)
|
||||
sb.Append("@");
|
||||
|
||||
sb.Append(_host);
|
||||
|
||||
if (_sshPort != SshProfile.DefaultSshPort)
|
||||
{
|
||||
sb.Append($":{_sshPort:#####}");
|
||||
}
|
||||
|
||||
sb.Append("/");
|
||||
|
||||
bool queryStringAdded = false;
|
||||
|
||||
if (_useMosh)
|
||||
{
|
||||
sb.Append(
|
||||
$"?{ValidMoshPortsNames[0]}={_moshPortFrom:#####}-{_moshPortTo:#####}");
|
||||
|
||||
queryStringAdded = true;
|
||||
}
|
||||
|
||||
var terminalInfoQueryString = GetBaseQueryString();
|
||||
|
||||
if (!string.IsNullOrEmpty(terminalInfoQueryString))
|
||||
{
|
||||
sb.Append($"{(queryStringAdded ? "&" : "?")}{terminalInfoQueryString}");
|
||||
}
|
||||
|
||||
return Tuple.Create(true, sb.ToString());
|
||||
}
|
||||
|
||||
private async Task<Tuple<bool, string>> GetUrlQuickAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
var match = CommandValidationRx.Match(_command);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
error = I18N.Translate("InvalidCommand");
|
||||
|
||||
return Tuple.Create(false, string.IsNullOrEmpty(error) ? "Invalid command." : error);
|
||||
}
|
||||
|
||||
return Tuple.Create(true,
|
||||
$"{CustomSshUriScheme}://{CustomUriHost}/?{CustomCommandQueryStringName}={Constants.SshCommandName}&{CustomArgumentsQueryStringName}={HttpUtility.UrlEncode(match.Groups["args"].Value.Trim())}&{GetBaseQueryString()}");
|
||||
}
|
||||
|
||||
#endregion Links/shortcuts related
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.Models.Enums
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
[Flags]
|
||||
public enum SshConnectionInfoValidationResult
|
||||
@@ -22,15 +22,17 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private string _startupTaskErrorMessage;
|
||||
private bool _needsToRestart;
|
||||
private readonly IApplicationLanguageService _applicationLanguageService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
|
||||
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
|
||||
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService)
|
||||
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService, ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_startupTaskService = startupTaskService;
|
||||
_applicationLanguageService = applicationLanguageService;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
|
||||
@@ -154,6 +156,20 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseQuickSshConnectByDefault
|
||||
{
|
||||
get => _applicationSettings.UseQuickSshConnectByDefault;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.UseQuickSshConnectByDefault != value)
|
||||
{
|
||||
_applicationSettings.UseQuickSshConnectByDefault = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RTrimCopiedLines
|
||||
{
|
||||
get => _applicationSettings.RTrimCopiedLines;
|
||||
@@ -168,6 +184,22 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool MuteTerminalBeeps
|
||||
{
|
||||
get => _applicationSettings.MuteTerminalBeeps;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.MuteTerminalBeeps != value)
|
||||
{
|
||||
_applicationSettings.MuteTerminalBeeps = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
|
||||
_trayProcessCommunicationService.MuteTerminal(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool BottomIsSelected
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Bottom;
|
||||
@@ -335,6 +367,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
UseMoshByDefault = defaults.UseMoshByDefault;
|
||||
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
|
||||
RTrimCopiedLines = defaults.RTrimCopiedLines;
|
||||
MuteTerminalBeeps = defaults.MuteTerminalBeeps;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
private void Clone(ShellProfileViewModel shellProfile)
|
||||
{
|
||||
var cloned = shellProfile.Model.Clone();
|
||||
var cloned = shellProfile.ProfileVm.Model.Clone();
|
||||
|
||||
cloned.Id = Guid.NewGuid();
|
||||
cloned.PreInstalled = false;
|
||||
@@ -109,8 +109,6 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
private void AddShellProfile(ShellProfile shellProfile)
|
||||
{
|
||||
_settingsService.SaveShellProfile(shellProfile, true);
|
||||
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider, true);
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
|
||||
|
||||
@@ -16,28 +16,28 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private SshProfileViewModel _selectedShellProfile;
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IApplicationDataContainer _historyContainer;
|
||||
|
||||
public SshProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService,
|
||||
IFileSystemService fileSystemService, IApplicationView applicationView,
|
||||
IDefaultValueProvider defaultValueProvider,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IApplicationDataContainer historyContainer)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_applicationView = applicationView;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_historyContainer = historyContainer;
|
||||
|
||||
CreateSshProfileCommand = new RelayCommand(CreateSshProfile);
|
||||
CloneCommand = new RelayCommand<SshProfileViewModel>(Clone);
|
||||
|
||||
foreach (var sshProfile in _settingsService.GetSshProfiles())
|
||||
{
|
||||
var viewModel = new SshProfileViewModel(sshProfile, settingsService, dialogService,
|
||||
fileSystemService, applicationView, defaultValueProvider, _trayProcessCommunicationService, false);
|
||||
var viewModel = new SshProfileViewModel(sshProfile, settingsService, dialogService, fileSystemService,
|
||||
applicationView, _trayProcessCommunicationService, historyContainer, false);
|
||||
viewModel.Deleted += OnSshProfileDeleted;
|
||||
SshProfiles.Add(viewModel);
|
||||
}
|
||||
@@ -83,7 +83,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
private void Clone(SshProfileViewModel shellProfile)
|
||||
{
|
||||
var cloned = (SshProfile) shellProfile.Model.Clone();
|
||||
var cloned = (SshProfile) shellProfile.ProfileVm.Model.Clone();
|
||||
|
||||
cloned.Id = Guid.NewGuid();
|
||||
cloned.PreInstalled = false;
|
||||
@@ -99,7 +99,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private void AddSshProfile(SshProfile sshProfile)
|
||||
{
|
||||
var viewModel = new SshProfileViewModel(sshProfile, _settingsService, _dialogService, _fileSystemService,
|
||||
_applicationView, _defaultValueProvider, _trayProcessCommunicationService, true);
|
||||
_applicationView, _trayProcessCommunicationService, _historyContainer, true);
|
||||
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.Deleted += OnSshProfileDeleted;
|
||||
|
||||
@@ -9,16 +9,18 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
|
||||
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView applicationView, IApplicationLanguageService applicationLanguageService)
|
||||
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView applicationView,
|
||||
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers)
|
||||
{
|
||||
About = new AboutPageViewModel(settingsService, updateService, applicationView);
|
||||
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, defaultValueProvider, trayProcessCommunicationService);
|
||||
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService);
|
||||
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService);
|
||||
Profiles = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
|
||||
Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService);
|
||||
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService);
|
||||
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
|
||||
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider, trayProcessCommunicationService);
|
||||
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService,
|
||||
applicationView, trayProcessCommunicationService, containers.HistoryContainer);
|
||||
}
|
||||
|
||||
public event EventHandler Closed;
|
||||
|
||||
@@ -1,73 +1,28 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class ShellProfileViewModel : ViewModelBase
|
||||
/// <summary>
|
||||
/// Extends <see cref="ShellProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
|
||||
/// </summary>
|
||||
public class ShellProfileViewModel : ShellProfileViewModelBase<CommonProfileProviderViewModel>
|
||||
{
|
||||
#region Static
|
||||
|
||||
private static readonly LineEndingStyle[] LineEndingStylesArray =
|
||||
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Fields
|
||||
|
||||
private ShellProfile _fallbackProfile;
|
||||
|
||||
protected readonly IDialogService DialogService;
|
||||
protected readonly IFileSystemService FileSystemService;
|
||||
protected readonly ISettingsService SettingsService;
|
||||
protected readonly IApplicationView ApplicationView;
|
||||
protected readonly IDefaultValueProvider DefaultValueProvider;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
protected bool IsNew { get; set; }
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
public ShellProfile Model { get; private set; }
|
||||
|
||||
public bool PreInstalled { get; private set; }
|
||||
|
||||
private bool _useConPty;
|
||||
|
||||
public bool UseConPty
|
||||
{
|
||||
get => _useConPty;
|
||||
set => Set(ref _useConPty, value);
|
||||
}
|
||||
|
||||
private string _arguments;
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => _arguments;
|
||||
set => Set(ref _arguments, value);
|
||||
}
|
||||
|
||||
private bool _inEditMode;
|
||||
|
||||
public bool InEditMode
|
||||
{
|
||||
get => _inEditMode;
|
||||
set => Set(ref _inEditMode, value);
|
||||
}
|
||||
|
||||
private bool _isDefault;
|
||||
|
||||
public bool IsDefault
|
||||
@@ -76,180 +31,35 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _isDefault, value);
|
||||
}
|
||||
|
||||
private string _location;
|
||||
|
||||
public string Location
|
||||
{
|
||||
get => _location;
|
||||
set => Set(ref _location, value);
|
||||
}
|
||||
|
||||
private string _name;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(ref _name, value);
|
||||
}
|
||||
|
||||
private TabTheme _selectedTabTheme;
|
||||
|
||||
public TabTheme SelectedTabTheme
|
||||
{
|
||||
get => _selectedTabTheme ?? (_selectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(_tabThemeId)));
|
||||
set
|
||||
{
|
||||
if (value != null && Set(ref _selectedTabTheme, value))
|
||||
{
|
||||
_tabThemeId = value.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _tabThemeId;
|
||||
|
||||
public int TabThemeId
|
||||
{
|
||||
get => _tabThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _tabThemeId, value))
|
||||
{
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalTheme _selectedTerminalTheme;
|
||||
|
||||
public TerminalTheme SelectedTerminalTheme
|
||||
{
|
||||
get => _selectedTerminalTheme ??
|
||||
(_selectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(_terminalThemeId)));
|
||||
set
|
||||
{
|
||||
if (value != null && Set(ref _selectedTerminalTheme, value))
|
||||
{
|
||||
_terminalThemeId = value.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Guid _terminalThemeId;
|
||||
|
||||
public Guid TerminalThemeId
|
||||
{
|
||||
get => _terminalThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _terminalThemeId, value))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _workingDirectory;
|
||||
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get => _workingDirectory;
|
||||
set => Set(ref _workingDirectory, value);
|
||||
}
|
||||
|
||||
private LineEndingStyle _lineEndingTranslation;
|
||||
|
||||
public LineEndingStyle LineEndingTranslation
|
||||
{
|
||||
get => _lineEndingTranslation;
|
||||
set => Set(ref _lineEndingTranslation, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public KeyBindingsViewModel KeyBindings { get; }
|
||||
|
||||
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
|
||||
|
||||
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
|
||||
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler Deleted;
|
||||
|
||||
public event EventHandler SetAsDefault;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForCustomShellCommand { get; }
|
||||
|
||||
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
|
||||
|
||||
public IAsyncCommand CancelEditCommand { get; }
|
||||
|
||||
public IAsyncCommand DeleteCommand { get; }
|
||||
|
||||
public RelayCommand EditCommand { get; }
|
||||
|
||||
public IAsyncCommand SaveChangesCommand { get; }
|
||||
|
||||
public RelayCommand SetDefaultCommand { get; }
|
||||
|
||||
public IAsyncCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
public IAsyncCommand AddKeyboardShortcutCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constrcutor
|
||||
|
||||
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService,
|
||||
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
|
||||
IDefaultValueProvider defaultValueProvider, bool isNew)
|
||||
IDefaultValueProvider defaultValueProvider, bool isNew) : base(shellProfile, settingsService, dialogService,
|
||||
isNew)
|
||||
{
|
||||
Model = shellProfile;
|
||||
SettingsService = settingsService;
|
||||
DialogService = dialogService;
|
||||
FileSystemService = fileSystemService;
|
||||
ApplicationView = applicationView;
|
||||
DefaultValueProvider = defaultValueProvider;
|
||||
IsNew = isNew;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
|
||||
SettingsService.ThemeAdded += OnThemeAdded;
|
||||
SettingsService.ThemeDeleted += OnThemeDeleted;
|
||||
|
||||
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
|
||||
|
||||
TerminalThemes = new ObservableCollection<TerminalTheme>
|
||||
{
|
||||
new TerminalTheme
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Name = "Default"
|
||||
}
|
||||
};
|
||||
foreach (var theme in SettingsService.GetThemes())
|
||||
{
|
||||
TerminalThemes.Add(theme);
|
||||
}
|
||||
|
||||
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), DialogService, string.Empty, false);
|
||||
|
||||
InitializeViewModelPropertiesPrivate(shellProfile);
|
||||
ProfileVm = new CommonProfileProviderViewModel(settingsService, applicationView, fileSystemService,
|
||||
shellProfile);
|
||||
|
||||
SetDefaultCommand = new RelayCommand(SetDefault);
|
||||
DeleteCommand = new AsyncCommand(Delete, CanDelete);
|
||||
EditCommand = new RelayCommand(Edit);
|
||||
CancelEditCommand = new AsyncCommand(CancelEdit);
|
||||
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
|
||||
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
|
||||
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
|
||||
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
|
||||
}
|
||||
|
||||
@@ -257,97 +67,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
#region Methods
|
||||
|
||||
// Loads view model properties from the input shellProfile
|
||||
private void InitializeViewModelPropertiesPrivate(ShellProfile shellProfile)
|
||||
{
|
||||
Id = shellProfile.Id;
|
||||
Name = shellProfile.Name;
|
||||
Arguments = shellProfile.Arguments;
|
||||
Location = shellProfile.Location;
|
||||
WorkingDirectory = shellProfile.WorkingDirectory;
|
||||
TerminalThemeId = shellProfile.TerminalThemeId;
|
||||
TabThemeId = shellProfile.TabThemeId;
|
||||
PreInstalled = shellProfile.PreInstalled;
|
||||
LineEndingTranslation = shellProfile.LineEndingTranslation;
|
||||
UseConPty = shellProfile.UseConPty;
|
||||
|
||||
KeyBindings.Clear();
|
||||
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
}
|
||||
}
|
||||
|
||||
// Loads view model properties from the input shellProfile
|
||||
protected virtual void InitializeViewModelProperties(ShellProfile shellProfile) =>
|
||||
InitializeViewModelPropertiesPrivate(shellProfile);
|
||||
|
||||
// Fills the input profile properties the view model properties. (Inverse of InitializeViewModelProperties()).
|
||||
protected virtual Task FillProfileAsync(ShellProfile profile)
|
||||
{
|
||||
profile.Id = Id;
|
||||
profile.Name = Name;
|
||||
profile.Arguments = Arguments;
|
||||
profile.Location = Location;
|
||||
profile.WorkingDirectory = WorkingDirectory;
|
||||
profile.TerminalThemeId = TerminalThemeId;
|
||||
profile.TabThemeId = TabThemeId;
|
||||
profile.PreInstalled = PreInstalled;
|
||||
profile.LineEndingTranslation = LineEndingTranslation;
|
||||
profile.UseConPty = UseConPty;
|
||||
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Used to "remember" the current state.
|
||||
protected virtual async Task<ShellProfile> CreateProfileAsync()
|
||||
{
|
||||
var profile = new ShellProfile();
|
||||
|
||||
await FillProfileAsync(profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void OnThemeAdded(object sender, TerminalTheme e)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
TerminalThemes.Add(e);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeDeleted(object sender, Guid e)
|
||||
{
|
||||
if (SelectedTerminalTheme.Id == e)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(x => x.Id == Guid.Empty);
|
||||
Model.TerminalThemeId = Guid.Empty;
|
||||
if (_fallbackProfile != null)
|
||||
{
|
||||
_fallbackProfile.TerminalThemeId = Guid.Empty;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task SaveChangesAsync()
|
||||
{
|
||||
await FillProfileAsync(Model);
|
||||
|
||||
SettingsService.SaveShellProfile(Model);
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
IsNew = false;
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
if (InEditMode || !PreInstalled)
|
||||
if (InEditMode || !ProfileVm.PreInstalled)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
@@ -357,87 +79,24 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Model = DefaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id == Model.Id);
|
||||
InitializeViewModelProperties(Model);
|
||||
SettingsService.SaveShellProfile(Model);
|
||||
}
|
||||
}
|
||||
var profile = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id.Equals(Id));
|
||||
|
||||
public Task AddKeyboardShortcut()
|
||||
{
|
||||
return KeyBindings.ShowAddKeyBindingDialog();
|
||||
}
|
||||
|
||||
private async Task BrowseForCustomShell()
|
||||
{
|
||||
var file = await FileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
Location = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseForWorkingDirectory()
|
||||
{
|
||||
var directory = await FileSystemService.BrowseForDirectory().ConfigureAwait(true);
|
||||
if (directory != null)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
if (IsNew)
|
||||
{
|
||||
await Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShellProfile changedProfile = await CreateProfileAsync();
|
||||
|
||||
if (!_fallbackProfile.EqualTo(changedProfile))
|
||||
if (profile == null)
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
// Cancelled, so rollback
|
||||
InitializeViewModelProperties(_fallbackProfile);
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
// Should not happen ever, but...
|
||||
return;
|
||||
}
|
||||
|
||||
ProfileVm.Model = profile;
|
||||
Initialize(profile);
|
||||
|
||||
SettingsService.SaveShellProfile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanDelete()
|
||||
protected override bool CanDelete()
|
||||
{
|
||||
return !Model.PreInstalled;
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Deleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
_fallbackProfile = Model.Clone();
|
||||
|
||||
KeyBindings.Editable = true;
|
||||
InEditMode = true;
|
||||
return !ProfileVm.PreInstalled;
|
||||
}
|
||||
|
||||
private void SetDefault()
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all shell profile view models. Implements logic for saving, editing, resetting, etc.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class ShellProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
protected readonly IDialogService DialogService;
|
||||
protected readonly ISettingsService SettingsService;
|
||||
|
||||
private List<KeyBinding> _bindingsBeforeEdit;
|
||||
private string _nameBeforeEdit;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
protected bool IsNew { get; private set; }
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
private bool _inEditMode;
|
||||
|
||||
public bool InEditMode
|
||||
{
|
||||
get => _inEditMode;
|
||||
set => Set(ref _inEditMode, value);
|
||||
}
|
||||
|
||||
private string _name;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(ref _name, value);
|
||||
}
|
||||
|
||||
public KeyBindingsViewModel KeyBindings { get; }
|
||||
|
||||
public T ProfileVm { get; protected set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler Deleted;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand CancelEditCommand { get; }
|
||||
|
||||
public IAsyncCommand DeleteCommand { get; }
|
||||
|
||||
public RelayCommand EditCommand { get; }
|
||||
|
||||
public IAsyncCommand SaveChangesCommand { get; }
|
||||
|
||||
public IAsyncCommand AddKeyboardShortcutCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected ShellProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
DialogService = dialogService;
|
||||
IsNew = isNew;
|
||||
|
||||
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), dialogService, string.Empty, false);
|
||||
|
||||
Initialize(shellProfile);
|
||||
|
||||
DeleteCommand = new AsyncCommand(Delete, CanDelete);
|
||||
EditCommand = new RelayCommand(Edit);
|
||||
CancelEditCommand = new AsyncCommand(CancelEdit);
|
||||
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
|
||||
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
// Loads view model properties from the input shellProfile
|
||||
protected void Initialize(ShellProfile shellProfile)
|
||||
{
|
||||
Id = shellProfile.Id;
|
||||
Name = shellProfile.Name;
|
||||
|
||||
KeyBindings.Clear();
|
||||
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool HasChanges()
|
||||
{
|
||||
return ProfileVm.HasChanges() || !_nameBeforeEdit.NullableEqualTo(_name) ||
|
||||
!(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ??
|
||||
false);
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Deleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool CanDelete();
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
if (InEditMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bindingsBeforeEdit = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
_nameBeforeEdit = _name;
|
||||
|
||||
KeyBindings.Editable = true;
|
||||
InEditMode = true;
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
if (IsNew)
|
||||
{
|
||||
await Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HasChanges())
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
// Cancelled, so rollback
|
||||
ProfileVm.RejectChanges();
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task SaveChangesAsync()
|
||||
{
|
||||
var error = await ProfileVm.AcceptChangesAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), error, DialogButton.OK);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var profile = ProfileVm.Model;
|
||||
|
||||
profile.Id = Id;
|
||||
profile.Name = _name;
|
||||
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
|
||||
if (profile is SshProfile sshProfile)
|
||||
{
|
||||
SettingsService.SaveSshProfile(sshProfile, IsNew);
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingsService.SaveShellProfile(profile, IsNew);
|
||||
}
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
IsNew = false;
|
||||
}
|
||||
|
||||
public Task AddKeyboardShortcut()
|
||||
{
|
||||
return KeyBindings.ShowAddKeyBindingDialog();
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -1,275 +1,34 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class SshProfileViewModel : ShellProfileViewModel, ISshConnectionInfo
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
|
||||
// To prevent validating the existence of the same file multiple times because it's kinda expensive
|
||||
private string _validatedIdentityFile;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
private string _host;
|
||||
|
||||
public string Host
|
||||
{
|
||||
get => _host;
|
||||
set => Set(ref _host, value);
|
||||
}
|
||||
|
||||
private ushort _sshPort;
|
||||
|
||||
public ushort SshPort
|
||||
{
|
||||
get => _sshPort;
|
||||
set => Set(ref _sshPort, value);
|
||||
}
|
||||
|
||||
private string _username;
|
||||
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => Set(ref _username, value);
|
||||
}
|
||||
|
||||
private string _identityFile;
|
||||
|
||||
public string IdentityFile
|
||||
{
|
||||
get => _identityFile;
|
||||
set => Set(ref _identityFile, value);
|
||||
}
|
||||
|
||||
private bool _useMosh;
|
||||
|
||||
public bool UseMosh
|
||||
{
|
||||
get => _useMosh;
|
||||
set => Set(ref _useMosh, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortFrom;
|
||||
|
||||
public ushort MoshPortFrom
|
||||
{
|
||||
get => _moshPortFrom;
|
||||
set => Set(ref _moshPortFrom, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortTo;
|
||||
|
||||
public ushort MoshPortTo
|
||||
{
|
||||
get => _moshPortTo;
|
||||
set => Set(ref _moshPortTo, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForIdentityFileCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
/// <summary>
|
||||
/// Extends <see cref="ShellProfileViewModelBase{T}"/>, and doesn't implement any additional logic because in
|
||||
/// case of SSH profiles no additional logic is needed.
|
||||
/// </summary>
|
||||
public class SshProfileViewModel : ShellProfileViewModelBase<SshConnectViewModel>
|
||||
{
|
||||
#region Constructor
|
||||
|
||||
public SshProfileViewModel(SshProfile sshProfile, ISettingsService settingsService,
|
||||
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
|
||||
IDefaultValueProvider defaultValueProvider,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, bool isNew) : base(
|
||||
sshProfile ?? new SshProfile(), settingsService, dialogService, fileSystemService, applicationView,
|
||||
defaultValueProvider, isNew)
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IApplicationDataContainer historyContainer, bool isNew) : base(sshProfile, settingsService, dialogService,
|
||||
isNew)
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
|
||||
InitializeViewModelPropertiesPrivate(sshProfile ?? new SshProfile());
|
||||
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
|
||||
ProfileVm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer, sshProfile);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
// Fills the remaining view model properties (those which aren't filled by the base method) from the input sshProfile
|
||||
private void InitializeViewModelPropertiesPrivate(SshProfile sshProfile)
|
||||
|
||||
protected override bool CanDelete()
|
||||
{
|
||||
Host = sshProfile.Host;
|
||||
SshPort = sshProfile.SshPort;
|
||||
|
||||
Username = sshProfile.Username;
|
||||
|
||||
if (string.IsNullOrEmpty(Username))
|
||||
{
|
||||
_trayProcessCommunicationService.GetUserName().ContinueWith(t =>
|
||||
{
|
||||
var username = t.Result;
|
||||
|
||||
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => Username = username);
|
||||
}
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
|
||||
IdentityFile = sshProfile.IdentityFile;
|
||||
UseMosh = sshProfile.UseMosh;
|
||||
MoshPortFrom = sshProfile.MoshPortFrom;
|
||||
MoshPortTo = sshProfile.MoshPortTo;
|
||||
}
|
||||
|
||||
protected override void InitializeViewModelProperties(ShellProfile sshProfile)
|
||||
{
|
||||
base.InitializeViewModelProperties(sshProfile);
|
||||
|
||||
InitializeViewModelPropertiesPrivate((SshProfile)sshProfile);
|
||||
}
|
||||
|
||||
protected override async Task FillProfileAsync(ShellProfile profile)
|
||||
{
|
||||
await base.FillProfileAsync(profile);
|
||||
|
||||
profile.Location = _useMosh ? ShellLocation.Mosh : ShellLocation.SSH;
|
||||
profile.Arguments = GetArgumentsString();
|
||||
profile.WorkingDirectory = null;
|
||||
|
||||
var sshProfile = (SshProfile)profile;
|
||||
|
||||
sshProfile.Host = Host;
|
||||
sshProfile.SshPort = SshPort;
|
||||
sshProfile.Username = Username;
|
||||
sshProfile.IdentityFile = IdentityFile;
|
||||
sshProfile.UseMosh = UseMosh;
|
||||
sshProfile.MoshPortFrom = MoshPortFrom;
|
||||
sshProfile.MoshPortTo = MoshPortTo;
|
||||
}
|
||||
|
||||
public virtual Task AcceptChangesAsync() => FillProfileAsync(Model);
|
||||
|
||||
protected override async Task<ShellProfile> CreateProfileAsync()
|
||||
{
|
||||
var profile = new SshProfile();
|
||||
|
||||
await FillProfileAsync(profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private string GetArgumentsString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
|
||||
if (_sshPort != SshProfile.DefaultSshPort)
|
||||
sb.Append($"-p {_sshPort:#####} ");
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
sb.Append($"-i \"{_identityFile}\" ");
|
||||
|
||||
sb.Append($"{_username}@{_host}");
|
||||
|
||||
if (_useMosh)
|
||||
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override async Task SaveChangesAsync()
|
||||
{
|
||||
var result = await ValidateAsync();
|
||||
|
||||
if (result != SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"),
|
||||
result.GetErrorString(Environment.NewLine), DialogButton.OK);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await AcceptChangesAsync();
|
||||
|
||||
SettingsService.SaveSshProfile((SshProfile) Model, IsNew);
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
IsNew = false;
|
||||
}
|
||||
|
||||
private async Task BrowseForIdentityFile()
|
||||
{
|
||||
var file = await FileSystemService.OpenFile(new[] { "*" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
IdentityFile = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SshConnectionInfoValidationResult> ValidateAsync()
|
||||
{
|
||||
var result = this.GetValidationResult();
|
||||
|
||||
var identityFile = _identityFile;
|
||||
|
||||
if (!string.IsNullOrEmpty(identityFile) && !string.Equals(identityFile, _validatedIdentityFile,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Here we need to take into account that files from ssh config dir can be provided by name, without full path.
|
||||
string fullPath;
|
||||
|
||||
if (identityFile.Contains(Path.PathSeparator))
|
||||
{
|
||||
fullPath = identityFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(sshConfigDir))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fullPath = Path.Combine(sshConfigDir, identityFile);
|
||||
}
|
||||
|
||||
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
|
||||
IdentityFile = fullPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SetValidatedIdentityFile(string identityFile)
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
IdentityFile = identityFile;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
@@ -47,8 +47,15 @@ namespace FluentTerminal.App.Adapters
|
||||
return ApiInformation.IsApiContractPresent(api, version);
|
||||
}
|
||||
|
||||
public Task RunOnDispatcherThread(Action action)
|
||||
public Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true)
|
||||
{
|
||||
if (!enforceNewSchedule && _dispatcher.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ using Windows.UI.Xaml.Controls;
|
||||
using FluentTerminal.App.Utilities;
|
||||
using IContainer = Autofac.IContainer;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
|
||||
namespace FluentTerminal.App
|
||||
{
|
||||
@@ -44,7 +45,6 @@ namespace FluentTerminal.App
|
||||
private readonly TaskCompletionSource<int> _trayReady = new TaskCompletionSource<int>();
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly Lazy<ISshHelperService> _sshHelperService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private bool _alreadyLaunched;
|
||||
private bool _isLaunching;
|
||||
@@ -73,7 +73,8 @@ namespace FluentTerminal.App
|
||||
KeyBindings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.KeyBindingsContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
ShellProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings.CreateContainer(Constants.ShellProfilesContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
Themes = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ThemesContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
SshProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.SshProfilesContainerName, ApplicationDataCreateDisposition.Always))
|
||||
SshProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.SshProfilesContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
HistoryContainer = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ExecutedCommandsContainerName, ApplicationDataCreateDisposition.Always))
|
||||
};
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
|
||||
@@ -100,7 +101,6 @@ namespace FluentTerminal.App
|
||||
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
|
||||
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
|
||||
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
|
||||
builder.RegisterType<SshHelperService>().As<ISshHelperService>().SingleInstance();
|
||||
builder.RegisterType<ApplicationLanguageService>().As<IApplicationLanguageService>().SingleInstance();
|
||||
builder.RegisterInstance(applicationDataContainers);
|
||||
|
||||
@@ -111,8 +111,6 @@ namespace FluentTerminal.App
|
||||
|
||||
_trayProcessCommunicationService = _container.Resolve<ITrayProcessCommunicationService>();
|
||||
|
||||
_sshHelperService = new Lazy<ISshHelperService>(() => _container.Resolve<ISshHelperService>());
|
||||
|
||||
_dialogService = _container.Resolve<IDialogService>();
|
||||
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
@@ -169,21 +167,31 @@ namespace FluentTerminal.App
|
||||
if (args is ProtocolActivatedEventArgs protocolActivated)
|
||||
{
|
||||
MainViewModel mainViewModel = null;
|
||||
// IApplicationView to use for creating view models
|
||||
IApplicationView applicationView;
|
||||
|
||||
if (!_alreadyLaunched)
|
||||
if (_alreadyLaunched)
|
||||
{
|
||||
applicationView =
|
||||
(_mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId) ??
|
||||
_mainViewModels.Last()).ApplicationView;
|
||||
}
|
||||
else
|
||||
{
|
||||
// App wasn't launched before double clicking a shortcut, so we have to create a window
|
||||
// in order to be able to communicate with user.
|
||||
mainViewModel = _container.Resolve<MainViewModel>();
|
||||
|
||||
await CreateMainView(typeof(MainPage), mainViewModel, true);
|
||||
|
||||
applicationView = mainViewModel.ApplicationView;
|
||||
}
|
||||
|
||||
bool isSsh;
|
||||
|
||||
try
|
||||
{
|
||||
isSsh = _sshHelperService.Value.IsSsh(protocolActivated.Uri);
|
||||
isSsh = SshConnectViewModel.CheckScheme(protocolActivated.Uri);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -196,11 +204,13 @@ namespace FluentTerminal.App
|
||||
|
||||
if (isSsh)
|
||||
{
|
||||
SshProfileViewModel connectionInfo;
|
||||
SshConnectViewModel vm;
|
||||
|
||||
try
|
||||
{
|
||||
connectionInfo = (SshProfileViewModel) _sshHelperService.Value.ParseSsh(protocolActivated.Uri);
|
||||
vm = SshConnectViewModel.ParseUri(protocolActivated.Uri, _settingsService, applicationView,
|
||||
_trayProcessCommunicationService, _container.Resolve<IFileSystemService>(),
|
||||
_container.Resolve<ApplicationDataContainers>().HistoryContainer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -211,25 +221,22 @@ namespace FluentTerminal.App
|
||||
return;
|
||||
}
|
||||
|
||||
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks &&
|
||||
string.IsNullOrEmpty(connectionInfo.Username))
|
||||
if (!vm.CommandInput && _applicationSettings.AutoFallbackToWindowsUsernameInLinks &&
|
||||
string.IsNullOrEmpty(vm.Username))
|
||||
{
|
||||
connectionInfo.Username = await _trayProcessCommunicationService.GetUserName();
|
||||
vm.Username = await _trayProcessCommunicationService.GetUserName();
|
||||
}
|
||||
|
||||
SshConnectionInfoValidationResult result = await connectionInfo.ValidateAsync();
|
||||
var error = await vm.AcceptChangesAsync(true);
|
||||
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
await connectionInfo.AcceptChangesAsync();
|
||||
}
|
||||
else
|
||||
SshProfile profile = (SshProfile) vm.Model;
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
// Link is valid, but incomplete (i.e. username missing), so we need to show dialog.
|
||||
connectionInfo =
|
||||
(SshProfileViewModel) await _dialogService.ShowSshConnectionInfoDialogAsync(connectionInfo);
|
||||
profile = await _dialogService.ShowSshConnectionInfoDialogAsync(profile);
|
||||
|
||||
if (connectionInfo == null)
|
||||
if (profile == null)
|
||||
{
|
||||
// User clicked "Cancel" in the dialog.
|
||||
mainViewModel?.ApplicationView.TryClose();
|
||||
@@ -239,9 +246,9 @@ namespace FluentTerminal.App
|
||||
}
|
||||
|
||||
if (mainViewModel == null)
|
||||
await CreateTerminal(connectionInfo.Model, _applicationSettings.NewTerminalLocation);
|
||||
await CreateTerminal(profile, _applicationSettings.NewTerminalLocation);
|
||||
else
|
||||
await mainViewModel.AddTerminalAsync(connectionInfo.Model);
|
||||
await mainViewModel.AddTerminalAsync(profile);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -551,7 +558,7 @@ namespace FluentTerminal.App
|
||||
}
|
||||
break;
|
||||
case NewWindowAction.ShowSshProfileSelection:
|
||||
profile = await _sshHelperService.Value.GetSavedSshProfileAsync();
|
||||
profile = await _dialogService.ShowSshProfileSelectionDialogAsync();
|
||||
if (profile == null)
|
||||
{
|
||||
// Nothing to do if user cancels.
|
||||
@@ -559,7 +566,7 @@ namespace FluentTerminal.App
|
||||
}
|
||||
break;
|
||||
case NewWindowAction.ShowSshInfoDialog:
|
||||
profile = await _sshHelperService.Value.GetSshProfileAsync(new SshProfile());
|
||||
profile = await _dialogService.ShowSshConnectionInfoDialogAsync();
|
||||
if (profile == null)
|
||||
{
|
||||
// Nothing to do if user cancels.
|
||||
|
||||
@@ -6,15 +6,23 @@ namespace FluentTerminal.App.Converters
|
||||
{
|
||||
public class I18NConverter : IValueConverter
|
||||
{
|
||||
// To avoid empty translations
|
||||
private static string Translate(string resource)
|
||||
{
|
||||
var translation = I18N.Translate(resource);
|
||||
|
||||
return string.IsNullOrEmpty(translation) ? $"[ {resource} ]" : translation;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Enum enumValue && parameter is string enumType)
|
||||
{
|
||||
return I18N.Translate($"{enumType}.{enumValue}");
|
||||
return Translate($"{enumType}.{enumValue}");
|
||||
}
|
||||
else if (parameter is string resource)
|
||||
{
|
||||
return I18N.Translate(resource);
|
||||
return Translate(resource);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,271 +6,268 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:enums="using:FluentTerminal.Models.Enums"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:FluentTerminal.Models"
|
||||
xmlns:views="using:FluentTerminal.App.Views"
|
||||
xmlns:vm="using:FluentTerminal.App.ViewModels"
|
||||
xmlns:profiles="using:FluentTerminal.App.ViewModels.Profiles"
|
||||
x:Uid="SshInfoDialog"
|
||||
Title="SSH Info"
|
||||
d:DataContext="{d:DesignInstance vm:SshProfileViewModel,
|
||||
d:DataContext="{d:DesignInstance profiles:SshConnectViewModel,
|
||||
IsDesignTimeCreatable=False}"
|
||||
DefaultButton="Primary"
|
||||
Loading="OnLoading"
|
||||
PrimaryButtonClick="SshInfoDialog_OnPrimaryButtonClick"
|
||||
mc:Ignorable="d">
|
||||
<ContentDialog.Resources>
|
||||
<views:TabThemeTemplateSelector x:Key="TabThemeTemplateSelector">
|
||||
<views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="{x:Bind Color}"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
</views:TabThemeTemplateSelector>
|
||||
<views:TerminalThemeTemplateSelector x:Key="TerminalThemeTemplateSelector">
|
||||
<views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock FontStyle="Italic" Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
</views:TerminalThemeTemplateSelector>
|
||||
|
||||
<ContentDialog.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<views:TemplateSelectors />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</ContentDialog.Resources>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<Grid Width="500">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox
|
||||
x:Name="UserTextBox"
|
||||
x:Uid="UserTextBox"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Width="140"
|
||||
PlaceholderText="user"
|
||||
Text="{Binding Username, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
FontSize="22"
|
||||
Text="@" />
|
||||
<TextBox
|
||||
x:Name="HostTextBox"
|
||||
x:Uid="HostTextBox"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
PlaceholderText="host"
|
||||
Text="{Binding Host, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Margin="2,0,0,0"
|
||||
FontSize="22"
|
||||
Text=":" />
|
||||
<TextBox
|
||||
x:Uid="PortTextBox"
|
||||
Grid.Row="0"
|
||||
Grid.Column="4"
|
||||
Margin="0,0,4,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
BeforeTextChanging="Port_OnBeforeTextChanging"
|
||||
PlaceholderText="port"
|
||||
Text="{Binding SshPort, Mode=TwoWay}" />
|
||||
<TextBox
|
||||
x:Uid="IdentityFileTextBox"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Margin="0,10,0,0"
|
||||
IsReadOnly="False"
|
||||
PlaceholderText="Identity file path"
|
||||
Text="{Binding IdentityFile, Mode=TwoWay}" />
|
||||
<Button
|
||||
x:Uid="BrowseButton"
|
||||
Grid.Row="1"
|
||||
Grid.Column="4"
|
||||
Margin="0,10,4,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="BrowseButtonOnClick"
|
||||
Content="Browse" />
|
||||
<CheckBox
|
||||
x:Uid="UseMoshCheckBox"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
Margin="0,10,0,0"
|
||||
Content="Use Mosh"
|
||||
IsChecked="{Binding UseMosh, Mode=TwoWay}" />
|
||||
<controls:Expander
|
||||
x:Uid="AdvancedExpander"
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
Margin="0,10,0,0"
|
||||
Padding="0,10,0,10"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Advanced"
|
||||
IsExpanded="False">
|
||||
<Grid Margin="5,20,5,20">
|
||||
<StackPanel Orientation="Vertical" Width="500">
|
||||
|
||||
<CheckBox
|
||||
x:Uid="UseCommandInput"
|
||||
Content="Use quick SSH"
|
||||
IsChecked="{Binding CommandInput, Mode=TwoWay}" />
|
||||
|
||||
<Grid HorizontalAlignment="Stretch"
|
||||
Visibility="{Binding CommandInput, Converter={StaticResource FalseToVisibleConverter}}"
|
||||
Margin="0 8 2 0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
x:Uid="LineEndingModeOnPaste"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Line ending on paste:" />
|
||||
<ComboBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind LineEndingStyles}"
|
||||
SelectedItem="{Binding LineEndingTranslation, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="enums:LineEndingStyle">
|
||||
<TextBlock Text="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock
|
||||
x:Uid="MoshPorts"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="0,8,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Mosh ports:"
|
||||
Visibility="{Binding UseMosh}" />
|
||||
|
||||
<!-- Row -->
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,8,0,0"
|
||||
VerticalAlignment="Center"
|
||||
BeforeTextChanging="Port_OnBeforeTextChanging"
|
||||
Text="{Binding MoshPortFrom, Mode=TwoWay}"
|
||||
Visibility="{Binding UseMosh}" />
|
||||
x:Name="UserTextBox"
|
||||
x:Uid="UserTextBox"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Width="140"
|
||||
PlaceholderText="user"
|
||||
Text="{Binding Username, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
FontSize="22"
|
||||
Text="@" />
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Margin="2,8,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text=":"
|
||||
Visibility="{Binding UseMosh}" />
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
x:Name="HostTextBox"
|
||||
x:Uid="HostTextBox"
|
||||
PlaceholderText="host"
|
||||
Text="{Binding Host, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Margin="0,8,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Margin="2,0,0,0"
|
||||
FontSize="22"
|
||||
Text=":" />
|
||||
<TextBox
|
||||
x:Uid="PortTextBox"
|
||||
Grid.Row="0"
|
||||
Grid.Column="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
BeforeTextChanging="Port_OnBeforeTextChanging"
|
||||
Text="{Binding MoshPortTo, Mode=TwoWay}"
|
||||
Visibility="{Binding UseMosh}" />
|
||||
PlaceholderText="port"
|
||||
Text="{Binding SshPort, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock
|
||||
x:Uid="SshInfoDialog_TabColor"
|
||||
Grid.Row="2"
|
||||
<!-- Row -->
|
||||
<TextBox
|
||||
x:Uid="IdentityFileTextBox"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="0,8,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Tab color:" />
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
Margin="0,8,0,0"
|
||||
IsReadOnly="False"
|
||||
PlaceholderText="Identity file path"
|
||||
Text="{Binding IdentityFile, Mode=TwoWay}"
|
||||
Margin="0,8,0,0" />
|
||||
<Button
|
||||
x:Uid="BrowseButton"
|
||||
Grid.Row="1"
|
||||
Grid.Column="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TabThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTabTheme, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock
|
||||
x:Uid="Theme"
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Margin="0,8,8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Theme:" />
|
||||
<ComboBox
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TerminalThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTerminalTheme, Mode=TwoWay}" />
|
||||
Click="BrowseButtonOnClick"
|
||||
Content="Browse"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
<!-- Row -->
|
||||
<CheckBox
|
||||
Grid.Row="4"
|
||||
x:Uid="UseMoshCheckBox"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="4"
|
||||
Margin="0,8,0,0"
|
||||
Content="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseConPty}"
|
||||
IsChecked="{Binding UseConPty, Mode=TwoWay}" />
|
||||
Grid.ColumnSpan="5"
|
||||
Content="Use Mosh"
|
||||
IsChecked="{Binding UseMosh, Mode=TwoWay}"
|
||||
Margin="0,8,0,0"/>
|
||||
|
||||
</Grid>
|
||||
</controls:Expander>
|
||||
<Grid
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
x:Uid="SaveShortcutButton"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="0,10,4,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="SaveLink_OnClick"
|
||||
Content="Save Shortcut" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextBox x:Name="CommandTextBox"
|
||||
x:Uid="CommandTextBox"
|
||||
PlaceholderText="SSH command (e.g. 'ssh username@host')"
|
||||
Text="{Binding Command, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Visibility="{Binding CommandInput, Converter={StaticResource TrueToVisibleConverter}}"
|
||||
Margin="0 8 2 0"/>
|
||||
|
||||
<controls:Expander
|
||||
x:Uid="AdvancedExpander"
|
||||
Margin="0,10,0,0"
|
||||
Padding="0,10,0,10"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Advanced"
|
||||
IsExpanded="False">
|
||||
<Grid Margin="5,20,5,20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Row -->
|
||||
<TextBlock
|
||||
x:Uid="MoshPorts"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Mosh ports:"
|
||||
Visibility="{Binding MoshVisible}"
|
||||
Margin="0,8,8,0" />
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
BeforeTextChanging="Port_OnBeforeTextChanging"
|
||||
Text="{Binding MoshPortFrom, Mode=TwoWay}"
|
||||
Visibility="{Binding MoshVisible}"
|
||||
Margin="0,8,0,0" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Text=":"
|
||||
Visibility="{Binding MoshVisible}"
|
||||
Margin="2,8,0,0" />
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
BeforeTextChanging="Port_OnBeforeTextChanging"
|
||||
Text="{Binding MoshPortTo, Mode=TwoWay}"
|
||||
Visibility="{Binding MoshVisible}"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
<!-- Row -->
|
||||
<TextBlock
|
||||
x:Uid="LineEndingModeOnPaste"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Line ending on paste:"
|
||||
Margin="0,8,8,0" />
|
||||
<ComboBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding LineEndingStyles}"
|
||||
SelectedItem="{Binding LineEndingTranslation, Mode=TwoWay}"
|
||||
Margin="0,8,0,0">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="enums:LineEndingStyle">
|
||||
<TextBlock
|
||||
Text="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<!-- Row -->
|
||||
<TextBlock
|
||||
x:Uid="SshInfoDialog_TabColor"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Tab color:"
|
||||
Margin="0,8,8,0" />
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
ItemsSource="{Binding TabThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTabTheme, Mode=TwoWay}"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
<!-- Row -->
|
||||
<TextBlock
|
||||
x:Uid="Theme"
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Theme:"
|
||||
Margin="0,8,8,0" />
|
||||
<ComboBox
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
ItemsSource="{Binding TerminalThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTerminalTheme, Mode=TwoWay}"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
<!-- Row -->
|
||||
<CheckBox
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="4"
|
||||
Content="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseConPty}"
|
||||
IsChecked="{Binding UseConPty, Mode=TwoWay}"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
</Grid>
|
||||
</controls:Expander>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
x:Uid="SaveShortcutButton"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="0,10,4,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="SaveLink_OnClick"
|
||||
Content="Save Shortcut" />
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
</ContentDialog>
|
||||
@@ -10,66 +10,49 @@ using Windows.UI.Xaml.Controls;
|
||||
using FluentTerminal.App.Services.Dialogs;
|
||||
using FluentTerminal.App.Utilities;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
using FluentTerminal.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace FluentTerminal.App.Dialogs
|
||||
{
|
||||
// ReSharper disable once RedundantExtendsListEntry
|
||||
public sealed partial class SshInfoDialog : ContentDialog, ISshConnectionInfoDialog
|
||||
{
|
||||
private const string ShortcutFileFormat = @"[{{000214A0-0000-0000-C000-000000000046}}]
|
||||
Prop3=19,0
|
||||
[InternetShortcut]
|
||||
IDList=
|
||||
URL={0}
|
||||
";
|
||||
|
||||
private readonly ISshHelperService _sshHelperService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IApplicationDataContainer _historyContainer;
|
||||
|
||||
public IEnumerable<LineEndingStyle> LineEndingStyles { get; } = (LineEndingStyle[])Enum.GetValues(typeof(LineEndingStyle));
|
||||
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
|
||||
|
||||
|
||||
public SshInfoDialog(ISettingsService settingsService, ISshHelperService sshHelperService,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
public SshInfoDialog(ISettingsService settingsService, IApplicationView applicationView,
|
||||
IFileSystemService fileSystemService, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
ApplicationDataContainers containers)
|
||||
{
|
||||
_sshHelperService = sshHelperService;
|
||||
_settingsService = settingsService;
|
||||
_applicationView = applicationView;
|
||||
_fileSystemService = fileSystemService;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_historyContainer = containers.HistoryContainer;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
PrimaryButtonText = I18N.Translate("OK");
|
||||
SecondaryButtonText = I18N.Translate("Cancel");
|
||||
|
||||
var currentTheme = settingsService.GetCurrentTheme();
|
||||
RequestedTheme = ContrastHelper.GetIdealThemeForBackgroundColor(currentTheme.Colors.Background);
|
||||
|
||||
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
|
||||
|
||||
TerminalThemes = new ObservableCollection<TerminalTheme>
|
||||
{
|
||||
new TerminalTheme
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Name = "Default"
|
||||
}
|
||||
};
|
||||
foreach (var theme in settingsService.GetThemes())
|
||||
{
|
||||
TerminalThemes.Add(theme);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupFocus()
|
||||
{
|
||||
SshProfileViewModel vm = (SshProfileViewModel)DataContext;
|
||||
SshConnectViewModel vm = (SshConnectViewModel) DataContext;
|
||||
|
||||
if (string.IsNullOrEmpty(vm.Username))
|
||||
if (vm.CommandInput)
|
||||
{
|
||||
CommandTextBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(vm.Username))
|
||||
{
|
||||
UserTextBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
@@ -85,12 +68,12 @@ URL={0}
|
||||
|
||||
private async void OnLoading(FrameworkElement sender, object args)
|
||||
{
|
||||
SshProfileViewModel vm = (SshProfileViewModel)DataContext;
|
||||
SshConnectViewModel vm = (SshConnectViewModel) DataContext;
|
||||
|
||||
if (!string.IsNullOrEmpty(vm.Username))
|
||||
return;
|
||||
|
||||
vm.Username = await _trayProcessCommunicationService.GetUserName();
|
||||
if (!vm.CommandInput && string.IsNullOrEmpty(vm.Username))
|
||||
{
|
||||
vm.Username = await _trayProcessCommunicationService.GetUserName();
|
||||
}
|
||||
|
||||
SetupFocus();
|
||||
}
|
||||
@@ -102,36 +85,39 @@ URL={0}
|
||||
openPicker.FileTypeFilter.Add("*");
|
||||
|
||||
StorageFile file = await openPicker.PickSingleFileAsync();
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
((SshProfileViewModel)DataContext).SetValidatedIdentityFile(file.Path);
|
||||
((SshConnectViewModel) DataContext).SetValidatedIdentityFile(file.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SaveLink_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SshProfileViewModel vm = (SshProfileViewModel) DataContext;
|
||||
SshConnectViewModel vm = (SshConnectViewModel) DataContext;
|
||||
|
||||
var validationResult = await vm.ValidateAsync();
|
||||
var link = await vm.GetUrlAsync();
|
||||
|
||||
if (validationResult != SshConnectionInfoValidationResult.Valid &&
|
||||
// We may ignore empty username for links
|
||||
validationResult != SshConnectionInfoValidationResult.UsernameEmpty)
|
||||
if (!link.Item1)
|
||||
{
|
||||
await new MessageDialog(validationResult.GetErrorString(Environment.NewLine),
|
||||
I18N.Translate("InvalidInput")).ShowAsync();
|
||||
await new MessageDialog(link.Item2, I18N.Translate("InvalidInput")).ShowAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string content = string.Format(ShortcutFileFormat, await _sshHelperService.ConvertToUriAsync(vm));
|
||||
var content = ProfileProviderViewModelBase.GetShortcutFileContent(link.Item2);
|
||||
|
||||
string fileName = string.IsNullOrEmpty(vm.Username) ? $"{vm.Host}.url" : $"{vm.Username}@{vm.Host}.url";
|
||||
FileSavePicker savePicker = new FileSavePicker {SuggestedStartLocation = PickerLocationId.Desktop};
|
||||
|
||||
FileSavePicker savePicker = new FileSavePicker {SuggestedFileName = fileName, SuggestedStartLocation = PickerLocationId.Desktop};
|
||||
if (!vm.CommandInput)
|
||||
{
|
||||
savePicker.SuggestedFileName = string.IsNullOrEmpty(vm.Username) ? $"{vm.Host}.url" : $"{vm.Username}@{vm.Host}.url";
|
||||
}
|
||||
|
||||
savePicker.FileTypeChoices.Add("Shortcut", new List<string> {".url"});
|
||||
|
||||
StorageFile file = await savePicker.PickSaveFileAsync();
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return;
|
||||
@@ -151,13 +137,13 @@ URL={0}
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
var result = await ((ISshConnectionInfo) DataContext).ValidateAsync();
|
||||
var error = await ((SshConnectViewModel)DataContext).AcceptChangesAsync();
|
||||
|
||||
if (result != SshConnectionInfoValidationResult.Valid)
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
args.Cancel = true;
|
||||
|
||||
await new MessageDialog(result.GetErrorString(Environment.NewLine), I18N.Translate("InvalidInput")).ShowAsync();
|
||||
await new MessageDialog(error, I18N.Translate("InvalidInput")).ShowAsync();
|
||||
|
||||
SetupFocus();
|
||||
}
|
||||
@@ -168,22 +154,26 @@ URL={0}
|
||||
private void Port_OnBeforeTextChanging(TextBox sender, TextBoxBeforeTextChangingEventArgs args) =>
|
||||
args.Cancel = string.IsNullOrEmpty(args.NewText) || args.NewText.Any(c => !char.IsDigit(c));
|
||||
|
||||
public async Task<ISshConnectionInfo> GetSshConnectionInfoAsync(ISshConnectionInfo input)
|
||||
public async Task<SshProfile> GetSshConnectionInfoAsync(SshProfile input = null)
|
||||
{
|
||||
DataContext = input;
|
||||
var vm = new SshConnectViewModel(_settingsService, _applicationView, _trayProcessCommunicationService,
|
||||
_fileSystemService, _historyContainer, input);
|
||||
|
||||
Focus(FocusState.Programmatic);
|
||||
DataContext = vm;
|
||||
|
||||
if (await ShowAsync() != ContentDialogResult.Primary)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var vm = (SshProfileViewModel) DataContext;
|
||||
var profile = (SshProfile) vm.Model;
|
||||
|
||||
await vm.AcceptChangesAsync();
|
||||
if (vm.CommandInput)
|
||||
{
|
||||
vm.SaveCommand(profile.Location, profile.Arguments);
|
||||
}
|
||||
|
||||
return vm;
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,6 @@
|
||||
<Compile Include="Services\ApplicationLanguageService.cs" />
|
||||
<Compile Include="Services\ClipboardService.cs" />
|
||||
<Compile Include="Services\FileSystemService.cs" />
|
||||
<Compile Include="Services\SshHelperService.cs" />
|
||||
<Compile Include="Services\StartupTaskService.cs" />
|
||||
<Compile Include="Services\SystemFontService.cs" />
|
||||
<Compile Include="Utilities\ColorExtensions.cs" />
|
||||
@@ -198,6 +197,9 @@
|
||||
<DependentUpon>TabBar.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\TabBarBackgroundBindingHelper.cs" />
|
||||
<Compile Include="Views\TemplateSelectors.xaml.cs">
|
||||
<DependentUpon>TemplateSelectors.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\TerminalKeybindTemplateSelector.cs" />
|
||||
<Compile Include="Views\TabThemeTemplateSelector.cs" />
|
||||
<Compile Include="Views\TerminalColorPicker.xaml.cs">
|
||||
@@ -457,6 +459,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\TemplateSelectors.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\XtermTerminalView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" IgnorableNamespaces="uap mp desktop rescap">
|
||||
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.5.1.0" />
|
||||
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.5.3.0" />
|
||||
<mp:PhoneIdentity PhoneProductId="f7225014-f405-4342-ad2e-ebf6ac60ea2c" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
|
||||
<Properties>
|
||||
<DisplayName>Fluent Terminal</DisplayName>
|
||||
@@ -52,6 +52,11 @@
|
||||
<uap:DisplayName>Mobile Shell Session</uap:DisplayName>
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="sshft">
|
||||
<uap:DisplayName>Fluent-specific SSH</uap:DisplayName>
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
// Resources:
|
||||
// https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04#section-3
|
||||
// https://man.openbsd.org/ssh
|
||||
public class SshHelperService : ISshHelperService
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const string SshUriScheme = "ssh";
|
||||
private const string MoshUriScheme = "mosh";
|
||||
private const string ThemeQueryStringParam = "theme";
|
||||
private const string TabQueryStringParam = "tab";
|
||||
|
||||
// Constant derived from https://man.openbsd.org/ssh
|
||||
private const string IdentityFileOptionName = "IdentityFile";
|
||||
|
||||
private static readonly string[] ValidMoshPortsNames = { "mosh_ports", "mosh-ports" };
|
||||
|
||||
private static readonly Regex MoshRangeRx =
|
||||
new Regex(@"^(?<from>\d{1,5})[:-](?<to>\d{1,5})$", RegexOptions.Compiled);
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Static
|
||||
|
||||
private static IEnumerable<Tuple<string, string>> ParseParams(string uriOpts, char separator) =>
|
||||
uriOpts.Split(separator).Select(ParseSshOptionFromUri).Where(p => p != null);
|
||||
|
||||
private static Tuple<string, string> ParseSshOptionFromUri(string option)
|
||||
{
|
||||
string[] nv = option.Split('=');
|
||||
|
||||
if (nv.Length != 2 || string.IsNullOrEmpty(nv[0]))
|
||||
{
|
||||
// For now simply ignore invalid options
|
||||
return null;
|
||||
//throw new FormatException($"Invalid SSH option '{option}'.");
|
||||
}
|
||||
|
||||
return Tuple.Create(HttpUtility.UrlDecode(nv[0]), HttpUtility.UrlDecode(nv[1]));
|
||||
}
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructor
|
||||
|
||||
public SshHelperService(ISettingsService settingsService, IDialogService dialogService,
|
||||
IFileSystemService fileSystemService, IApplicationView applicationView,
|
||||
IDefaultValueProvider defaultValueProvider,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_applicationView = applicationView;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
public bool IsSsh(Uri uri) =>
|
||||
SshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase) ||
|
||||
MoshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public ISshConnectionInfo ParseSsh(Uri uri)
|
||||
{
|
||||
SshProfileViewModel vm = new SshProfileViewModel(null, _settingsService, _dialogService,
|
||||
_fileSystemService, _applicationView, _defaultValueProvider, _trayProcessCommunicationService, true)
|
||||
{
|
||||
Host = uri.Host,
|
||||
UseMosh = MoshUriScheme.Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
|
||||
if (uri.Port >= 0)
|
||||
vm.SshPort = (ushort)uri.Port;
|
||||
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
string[] parts = uri.UserInfo.Split(';');
|
||||
|
||||
if (parts.Length > 2)
|
||||
throw new FormatException($"UserInfo part contains {parts.Length} elements.");
|
||||
|
||||
vm.Username = HttpUtility.UrlDecode(parts[0]);
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
// For now we are only interested in IdentityFile option
|
||||
Tuple<string, string> identityFileOption = ParseParams(parts[1], ',').FirstOrDefault(p =>
|
||||
string.Equals(p.Item1, IdentityFileOptionName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
vm.IdentityFile = identityFileOption?.Item2;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(uri.Query))
|
||||
{
|
||||
return vm;
|
||||
}
|
||||
|
||||
string queryString = uri.Query?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(queryString))
|
||||
{
|
||||
return vm;
|
||||
}
|
||||
|
||||
if (queryString.StartsWith('?'))
|
||||
{
|
||||
queryString = queryString.Substring(1);
|
||||
}
|
||||
|
||||
foreach (Tuple<string, string> param in ParseParams(queryString, '&'))
|
||||
{
|
||||
string paramName = param.Item1.ToLower();
|
||||
|
||||
if (ValidMoshPortsNames.Contains(paramName))
|
||||
{
|
||||
Match match = MoshRangeRx.Match(param.Item2);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
vm.MoshPortFrom = ushort.Parse(match.Groups["from"].Value);
|
||||
vm.MoshPortTo = ushort.Parse(match.Groups["to"].Value);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.Item1.Equals(ThemeQueryStringParam, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Guid.TryParse(param.Item2, out Guid themeId))
|
||||
{
|
||||
vm.TerminalThemeId = themeId;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (param.Item2.Equals(TabQueryStringParam, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (int.TryParse(param.Item2, out int tabThemeId))
|
||||
{
|
||||
vm.TabThemeId = tabThemeId;
|
||||
}
|
||||
}
|
||||
|
||||
// For now we are ignoring unknown query string arguments
|
||||
}
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
public async Task<SshProfile> GetSshProfileAsync(SshProfile profile)
|
||||
{
|
||||
SshProfileViewModel vm = new SshProfileViewModel(profile, _settingsService, _dialogService,
|
||||
_fileSystemService, _applicationView, _defaultValueProvider, _trayProcessCommunicationService, true);
|
||||
|
||||
vm.UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault;
|
||||
|
||||
vm = (SshProfileViewModel) await _dialogService.ShowSshConnectionInfoDialogAsync(vm);
|
||||
|
||||
return (SshProfile) vm?.Model;
|
||||
}
|
||||
|
||||
public Task<SshProfile> GetSavedSshProfileAsync() =>
|
||||
_dialogService.ShowSshProfileSelectionDialogAsync();
|
||||
|
||||
public async Task<string> ConvertToUriAsync(ISshConnectionInfo sshConnectionInfo)
|
||||
{
|
||||
var result = await sshConnectionInfo.ValidateAsync();
|
||||
|
||||
if (result != SshConnectionInfoValidationResult.Valid &&
|
||||
// We can ignore empty username here
|
||||
result != SshConnectionInfoValidationResult.UsernameEmpty)
|
||||
throw new ArgumentException(result.GetErrorString(), nameof(sshConnectionInfo));
|
||||
|
||||
SshProfileViewModel sshConnectionInfoVm = (SshProfileViewModel) sshConnectionInfo;
|
||||
|
||||
StringBuilder sb = new StringBuilder(sshConnectionInfoVm.UseMosh ? MoshUriScheme : SshUriScheme);
|
||||
|
||||
sb.Append("://");
|
||||
|
||||
bool containsUserInfo = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(sshConnectionInfoVm.Username))
|
||||
{
|
||||
sb.Append(HttpUtility.UrlEncode(sshConnectionInfoVm.Username));
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sshConnectionInfoVm.IdentityFile))
|
||||
{
|
||||
sb.Append(";");
|
||||
|
||||
if (!string.IsNullOrEmpty(sshConnectionInfoVm.IdentityFile))
|
||||
{
|
||||
sb.Append($"{IdentityFileOptionName}={HttpUtility.UrlEncode(sshConnectionInfoVm.IdentityFile)}");
|
||||
}
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (containsUserInfo)
|
||||
sb.Append("@");
|
||||
|
||||
sb.Append(sshConnectionInfoVm.Host);
|
||||
|
||||
if (sshConnectionInfoVm.SshPort != SshProfile.DefaultSshPort)
|
||||
{
|
||||
sb.Append(":");
|
||||
sb.Append(sshConnectionInfoVm.SshPort.ToString("#####"));
|
||||
}
|
||||
|
||||
sb.Append("/");
|
||||
|
||||
bool queryStringAdded = false;
|
||||
|
||||
if (sshConnectionInfoVm.UseMosh)
|
||||
{
|
||||
sb.Append(
|
||||
$"?{ValidMoshPortsNames[0]}={sshConnectionInfo.MoshPortFrom:#####}-{sshConnectionInfo.MoshPortTo:#####}");
|
||||
|
||||
queryStringAdded = true;
|
||||
}
|
||||
|
||||
if (!sshConnectionInfo.TerminalThemeId.Equals(Guid.Empty))
|
||||
{
|
||||
if (!queryStringAdded)
|
||||
{
|
||||
sb.Append("?");
|
||||
|
||||
queryStringAdded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("&");
|
||||
}
|
||||
|
||||
sb.Append($"{ThemeQueryStringParam}={sshConnectionInfo.TerminalThemeId}" );
|
||||
}
|
||||
|
||||
if (sshConnectionInfo.TabThemeId != 0)
|
||||
{
|
||||
if (!queryStringAdded)
|
||||
{
|
||||
sb.Append("?");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("&");
|
||||
}
|
||||
|
||||
sb.Append($"{TabQueryStringParam}={sshConnectionInfo.TabThemeId:##########}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -889,4 +889,31 @@ Fuzzy</comment>
|
||||
<data name="RTrimCopiedLines.Header" xml:space="preserve">
|
||||
<value>Nachfolgende Leerzeichen aus kopierten Zeilen entfernen</value>
|
||||
</data>
|
||||
<data name="QuickSshDefault.Header" xml:space="preserve">
|
||||
<value>Quick SSH standardmäßig benutzen</value>
|
||||
</data>
|
||||
<data name="InvalidCommand" xml:space="preserve">
|
||||
<value>Ungültiger Befehl</value>
|
||||
</data>
|
||||
<data name="LocationCantBeEmpty" xml:space="preserve">
|
||||
<value>Dateipfad darf nicht leer sein</value>
|
||||
</data>
|
||||
<data name="MuteTerminalBeeps.Header" xml:space="preserve">
|
||||
<value>Hinweistöne stummschalten</value>
|
||||
</data>
|
||||
<data name="UseCommandInput.Content" xml:space="preserve">
|
||||
<value>Quick SSH verwenden</value>
|
||||
</data>
|
||||
<data name="CommandTextBox.PlaceholderText" xml:space="preserve">
|
||||
<value>SSH-Befehl (z.b. 'ssh username@host')</value>
|
||||
</data>
|
||||
<data name="CommandLine.Header" xml:space="preserve">
|
||||
<value>Befehl</value>
|
||||
</data>
|
||||
<data name="UnsupportedCommand" xml:space="preserve">
|
||||
<value>Nicht unterstüzter Befehl:</value>
|
||||
</data>
|
||||
<data name="CommandArgumentsMandatory" xml:space="preserve">
|
||||
<value>Befehlsargument fehlen</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -927,4 +927,31 @@ Fuzzy</comment>
|
||||
<data name="RTrimCopiedLines.Header" xml:space="preserve">
|
||||
<value>Right trim copied lines</value>
|
||||
</data>
|
||||
<data name="QuickSshDefault.Header" xml:space="preserve">
|
||||
<value>Use quick SSH by default</value>
|
||||
</data>
|
||||
<data name="InvalidCommand" xml:space="preserve">
|
||||
<value>Invalid command.</value>
|
||||
</data>
|
||||
<data name="LocationCantBeEmpty" xml:space="preserve">
|
||||
<value>Location cannot be empty.</value>
|
||||
</data>
|
||||
<data name="MuteTerminalBeeps.Header" xml:space="preserve">
|
||||
<value>Mute terminal beeps</value>
|
||||
</data>
|
||||
<data name="UseCommandInput.Content" xml:space="preserve">
|
||||
<value>Use quick SSH</value>
|
||||
</data>
|
||||
<data name="CommandTextBox.PlaceholderText" xml:space="preserve">
|
||||
<value>SSH command (e.g. 'ssh username@host')</value>
|
||||
</data>
|
||||
<data name="CommandLine.Header" xml:space="preserve">
|
||||
<value>Command</value>
|
||||
</data>
|
||||
<data name="UnsupportedCommand" xml:space="preserve">
|
||||
<value>Unsupported command:</value>
|
||||
</data>
|
||||
<data name="CommandArgumentsMandatory" xml:space="preserve">
|
||||
<value>Command arguments are missing.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -880,4 +880,31 @@
|
||||
<data name="RTrimCopiedLines.Header" xml:space="preserve">
|
||||
<value>Right trim copied lines</value>
|
||||
</data>
|
||||
<data name="QuickSshDefault.Header" xml:space="preserve">
|
||||
<value>Use quick SSH by default</value>
|
||||
</data>
|
||||
<data name="InvalidCommand" xml:space="preserve">
|
||||
<value>Invalid command.</value>
|
||||
</data>
|
||||
<data name="LocationCantBeEmpty" xml:space="preserve">
|
||||
<value>Location cannot be empty.</value>
|
||||
</data>
|
||||
<data name="MuteTerminalBeeps.Header" xml:space="preserve">
|
||||
<value>Mute terminal beeps</value>
|
||||
</data>
|
||||
<data name="UseCommandInput.Content" xml:space="preserve">
|
||||
<value>Use quick SSH</value>
|
||||
</data>
|
||||
<data name="CommandTextBox.PlaceholderText" xml:space="preserve">
|
||||
<value>SSH command (e.g. 'ssh username@host')</value>
|
||||
</data>
|
||||
<data name="CommandLine.Header" xml:space="preserve">
|
||||
<value>Command</value>
|
||||
</data>
|
||||
<data name="UnsupportedCommand" xml:space="preserve">
|
||||
<value>Unsupported command:</value>
|
||||
</data>
|
||||
<data name="CommandArgumentsMandatory" xml:space="preserve">
|
||||
<value>Command arguments are missing.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -921,4 +921,31 @@ Fuzzy</comment>
|
||||
<data name="RTrimCopiedLines.Header" xml:space="preserve">
|
||||
<value>Right trim copied lines</value>
|
||||
</data>
|
||||
<data name="QuickSshDefault.Header" xml:space="preserve">
|
||||
<value>Use quick SSH by default</value>
|
||||
</data>
|
||||
<data name="InvalidCommand" xml:space="preserve">
|
||||
<value>Invalid command.</value>
|
||||
</data>
|
||||
<data name="LocationCantBeEmpty" xml:space="preserve">
|
||||
<value>Location cannot be empty.</value>
|
||||
</data>
|
||||
<data name="MuteTerminalBeeps.Header" xml:space="preserve">
|
||||
<value>Mute terminal beeps</value>
|
||||
</data>
|
||||
<data name="UseCommandInput.Content" xml:space="preserve">
|
||||
<value>Use quick SSH</value>
|
||||
</data>
|
||||
<data name="CommandTextBox.PlaceholderText" xml:space="preserve">
|
||||
<value>SSH command (e.g. 'ssh username@host')</value>
|
||||
</data>
|
||||
<data name="CommandLine.Header" xml:space="preserve">
|
||||
<value>Command</value>
|
||||
</data>
|
||||
<data name="UnsupportedCommand" xml:space="preserve">
|
||||
<value>Unsupported command:</value>
|
||||
</data>
|
||||
<data name="CommandArgumentsMandatory" xml:space="preserve">
|
||||
<value>Command arguments are missing.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -173,11 +173,23 @@
|
||||
Header="Use Windows username when opening SSH URLs that don't specify one"
|
||||
IsOn="{x:Bind ViewModel.AutoFallbackToWindowsUsernameInLinks, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
x:Uid="QuickSshDefault"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Use quick SSH by default"
|
||||
IsOn="{x:Bind ViewModel.UseQuickSshConnectByDefault, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
x:Uid="RTrimCopiedLines"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Right trim copied lines"
|
||||
IsOn="{x:Bind ViewModel.RTrimCopiedLines, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
x:Uid="MuteTerminalBeeps"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Mute terminal beeps"
|
||||
IsOn="{x:Bind ViewModel.MuteTerminalBeeps, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
@@ -4,90 +4,17 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:FluentTerminal.Models"
|
||||
xmlns:settings="using:FluentTerminal.App.ViewModels.Settings"
|
||||
xmlns:viewmodels="using:FluentTerminal.App.ViewModels"
|
||||
xmlns:views="using:FluentTerminal.App.Views"
|
||||
xmlns:enums="using:FluentTerminal.Models.Enums"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewmodels="using:FluentTerminal.App.ViewModels"
|
||||
xmlns:views="using:FluentTerminal.App.Views"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<views:TabThemeTemplateSelector x:Key="TabThemeTemplateSelector">
|
||||
<views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="{x:Bind Color}"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
</views:TabThemeTemplateSelector>
|
||||
<views:TerminalThemeTemplateSelector x:Key="TerminalThemeTemplateSelector">
|
||||
<views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock FontStyle="Italic" Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
</views:TerminalThemeTemplateSelector>
|
||||
<views:TerminalKeybindTemplateSelector x:Key="TerminalKeybindTemplateSelector">
|
||||
<views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<TextBlock x:Uid="None" FontStyle="Italic" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<views:KeyBindingsView
|
||||
Margin="0,6,0,0"
|
||||
ShowCommandName="False"
|
||||
ViewModel="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
</views:TerminalKeybindTemplateSelector>
|
||||
<views:NoValueTemplateSelector x:Key="NoValueTemplateSelector">
|
||||
<views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock x:Uid="None" FontStyle="Italic" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<views:NoValueTemplateSelector.CustomTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.CustomTemplate>
|
||||
</views:NoValueTemplateSelector>
|
||||
<views:BooleanTemplateSelector x:Key="UseConPtyTemplateSelector">
|
||||
<views:BooleanTemplateSelector.TrueTemplate>
|
||||
<DataTemplate x:DataType="x:Boolean">
|
||||
<TextBlock x:Uid="Yes" />
|
||||
</DataTemplate>
|
||||
</views:BooleanTemplateSelector.TrueTemplate>
|
||||
<views:BooleanTemplateSelector.FalseTemplate>
|
||||
<DataTemplate x:DataType="x:Boolean">
|
||||
<TextBlock x:Uid="No" />
|
||||
</DataTemplate>
|
||||
</views:BooleanTemplateSelector.FalseTemplate>
|
||||
</views:BooleanTemplateSelector>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<views:TemplateSelectors />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -147,13 +74,13 @@
|
||||
Command="{x:Bind ViewModel.SelectedShellProfile.DeleteCommand, Mode=OneWay}"
|
||||
Icon="Delete"
|
||||
Label="Delete"
|
||||
Visibility="{x:Bind ViewModel.SelectedShellProfile.PreInstalled, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
|
||||
Visibility="{x:Bind ViewModel.SelectedShellProfile.ProfileVm.PreInstalled, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
|
||||
<AppBarButton
|
||||
x:Uid="Restore"
|
||||
Command="{x:Bind ViewModel.SelectedShellProfile.RestoreDefaultsCommand, Mode=OneWay}"
|
||||
Icon="Undo"
|
||||
Label="Restore Defaults"
|
||||
Visibility="{x:Bind ViewModel.SelectedShellProfile.PreInstalled, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
Visibility="{x:Bind ViewModel.SelectedShellProfile.ProfileVm.PreInstalled, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
</CommandBar>
|
||||
<CommandBar
|
||||
Grid.Column="1"
|
||||
@@ -248,7 +175,7 @@
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Name"
|
||||
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ProfileVm.PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
@@ -257,15 +184,15 @@
|
||||
x:Uid="ShellLocation"
|
||||
Width="300"
|
||||
Header="Shell executable location"
|
||||
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ProfileVm.PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Command="{x:Bind BrowseForCustomShellCommand}"
|
||||
Command="{x:Bind ProfileVm.BrowseForCustomShellCommand}"
|
||||
Content="..."
|
||||
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ProfileVm.PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
|
||||
Style="{StaticResource ButtonRevealStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -276,11 +203,11 @@
|
||||
Header="Working directory"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind WorkingDirectory, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.WorkingDirectory, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Command="{x:Bind BrowseForWorkingDirectoryCommand}"
|
||||
Command="{x:Bind ProfileVm.BrowseForWorkingDirectoryCommand}"
|
||||
Content="..."
|
||||
Style="{StaticResource ButtonRevealStyle}" />
|
||||
</StackPanel>
|
||||
@@ -293,7 +220,7 @@
|
||||
Header="Arguments"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Arguments, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.Arguments, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<ComboBox
|
||||
x:Uid="TabColor"
|
||||
@@ -302,8 +229,8 @@
|
||||
Header="Tab color"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
ItemTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TabThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTabTheme, Mode=TwoWay}" />
|
||||
ItemsSource="{x:Bind ProfileVm.TabThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding ProfileVm.SelectedTabTheme, Mode=TwoWay}" />
|
||||
|
||||
<ComboBox
|
||||
x:Uid="ProfileTheme"
|
||||
@@ -312,21 +239,21 @@
|
||||
Header="Theme"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
ItemTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TerminalThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTerminalTheme, Mode=TwoWay}" />
|
||||
ItemsSource="{x:Bind ProfileVm.TerminalThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding ProfileVm.SelectedTerminalTheme, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Use ConPty"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsOn="{x:Bind UseConPty, Mode=TwoWay}" />
|
||||
IsOn="{x:Bind ProfileVm.UseConPty, Mode=TwoWay}" />
|
||||
|
||||
<ComboBox
|
||||
x:Uid="LineEndingModeOnPaste"
|
||||
x:Uid="LineEndingMode"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind LineEndingStyles}"
|
||||
SelectedItem="{Binding LineEndingTranslation, Mode=TwoWay}">
|
||||
ItemsSource="{x:Bind ProfileVm.LineEndingStyles}"
|
||||
SelectedItem="{Binding ProfileVm.LineEndingTranslation, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="enums:LineEndingStyle">
|
||||
<TextBlock Text="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}" />
|
||||
@@ -357,7 +284,7 @@
|
||||
x:Uid="ShellLocation"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Shell executable location"
|
||||
Text="{x:Bind Location, Mode=OneWay}" />
|
||||
Text="{x:Bind ProfileVm.Location, Mode=OneWay}" />
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
x:Uid="WorkingDir"
|
||||
@@ -365,7 +292,7 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Working directory">
|
||||
<ContentControl
|
||||
Content="{x:Bind WorkingDirectory, Mode=OneWay}"
|
||||
Content="{x:Bind ProfileVm.WorkingDirectory, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
@@ -377,7 +304,7 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Arguments">
|
||||
<ContentControl
|
||||
Content="{x:Bind Arguments, Mode=OneWay}"
|
||||
Content="{x:Bind ProfileVm.Arguments, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
@@ -390,7 +317,7 @@
|
||||
Header="Tab color">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind SelectedTabTheme, Mode=OneWay}"
|
||||
Content="{x:Bind ProfileVm.SelectedTabTheme, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
@@ -402,7 +329,7 @@
|
||||
Header="Theme">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind SelectedTerminalTheme, Mode=OneWay}"
|
||||
Content="{x:Bind ProfileVm.SelectedTerminalTheme, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
@@ -414,7 +341,7 @@
|
||||
Header="Use ConPty">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind UseConPty, Mode=OneWay}"
|
||||
Content="{x:Bind ProfileVm.UseConPty, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource UseConPtyTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
@@ -426,7 +353,7 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Line ending translation mode">
|
||||
<ContentControl
|
||||
Content="{x:Bind LineEndingTranslation, Mode=OneWay, Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}"
|
||||
Content="{x:Bind ProfileVm.LineEndingTranslation, Mode=OneWay, Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}"
|
||||
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
|
||||
@@ -6,90 +6,17 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:enums="using:FluentTerminal.Models.Enums"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:FluentTerminal.Models"
|
||||
xmlns:settings="using:FluentTerminal.App.ViewModels.Settings"
|
||||
xmlns:viewmodels="using:FluentTerminal.App.ViewModels"
|
||||
xmlns:views="using:FluentTerminal.App.Views"
|
||||
xmlns:profiles="using:FluentTerminal.App.ViewModels.Profiles"
|
||||
mc:Ignorable="d">
|
||||
|
||||
|
||||
<Page.Resources>
|
||||
<views:TabThemeTemplateSelector x:Key="TabThemeTemplateSelector">
|
||||
<views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="{x:Bind Color}"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
</views:TabThemeTemplateSelector>
|
||||
<views:TerminalThemeTemplateSelector x:Key="TerminalThemeTemplateSelector">
|
||||
<views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock FontStyle="Italic" Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
</views:TerminalThemeTemplateSelector>
|
||||
<views:TerminalKeybindTemplateSelector x:Key="TerminalKeybindTemplateSelector">
|
||||
<views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<TextBlock x:Uid="None" FontStyle="Italic" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<views:KeyBindingsView
|
||||
Margin="0,6,0,0"
|
||||
ShowCommandName="False"
|
||||
ViewModel="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
</views:TerminalKeybindTemplateSelector>
|
||||
<views:NoValueTemplateSelector x:Key="NoValueTemplateSelector">
|
||||
<views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock x:Uid="None" FontStyle="Italic" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<views:NoValueTemplateSelector.CustomTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.CustomTemplate>
|
||||
</views:NoValueTemplateSelector>
|
||||
<views:BooleanTemplateSelector x:Key="UseConPtyTemplateSelector">
|
||||
<views:BooleanTemplateSelector.TrueTemplate>
|
||||
<DataTemplate x:DataType="x:Boolean">
|
||||
<TextBlock x:Uid="Yes" />
|
||||
</DataTemplate>
|
||||
</views:BooleanTemplateSelector.TrueTemplate>
|
||||
<views:BooleanTemplateSelector.FalseTemplate>
|
||||
<DataTemplate x:DataType="x:Boolean">
|
||||
<TextBlock x:Uid="No" />
|
||||
</DataTemplate>
|
||||
</views:BooleanTemplateSelector.FalseTemplate>
|
||||
</views:BooleanTemplateSelector>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<views:TemplateSelectors />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Page.Resources>
|
||||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -233,10 +160,17 @@
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<CheckBox
|
||||
x:Uid="UseCommandInput"
|
||||
Content="Use quick SSH"
|
||||
IsChecked="{Binding ProfileVm.CommandInput, Mode=TwoWay}"
|
||||
Margin="{StaticResource ItemMargin}" />
|
||||
|
||||
<Grid
|
||||
Width="340"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left">
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="{Binding ProfileVm.CommandInput, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="90" />
|
||||
<ColumnDefinition Width="20" />
|
||||
@@ -251,7 +185,7 @@
|
||||
Header="Username"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Username, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.Username, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
@@ -268,7 +202,7 @@
|
||||
Header="Host"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Host, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.Host, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
@@ -285,19 +219,22 @@
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter},ConverterParameter=PortTextBox.PlaceholderText}"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind SshPort, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.SshPort, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}" Orientation="Horizontal">
|
||||
<StackPanel
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{Binding ProfileVm.CommandInput, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<TextBox
|
||||
Width="300"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=IdentityFileTextBox.PlaceholderText}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind IdentityFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.IdentityFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Command="{x:Bind BrowseForIdentityFileCommand}"
|
||||
Command="{x:Bind ProfileVm.BrowseForIdentityFileCommand}"
|
||||
Content="..."
|
||||
Style="{StaticResource ButtonRevealStyle}" />
|
||||
</StackPanel>
|
||||
@@ -306,37 +243,49 @@
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseMoshCheckBox.Content}"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsOn="{x:Bind UseMosh, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
IsOn="{x:Bind ProfileVm.UseMosh, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Visibility="{Binding ProfileVm.CommandInput, Converter={StaticResource FalseToVisibleConverter}}" />
|
||||
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
x:Uid="MoshPortRange"
|
||||
Text="Mosh port range"
|
||||
Visibility="{x:Bind ProfileVm.MoshVisible, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}"/>
|
||||
|
||||
<StackPanel
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{x:Bind UseMosh, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
|
||||
Visibility="{x:Bind ProfileVm.MoshVisible, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
|
||||
<TextBox
|
||||
x:Uid="MoshPortFrom"
|
||||
Width="120"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Mosh Port From"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind MoshPortFrom, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.MoshPortFrom, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock
|
||||
Width="8"
|
||||
Margin="3,50,0,0"
|
||||
Margin="3,26,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="22"
|
||||
Text=":" />
|
||||
<TextBox
|
||||
x:Uid="MoshPortTo"
|
||||
Width="120"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Mosh Port To"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind MoshPortTo, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Text="{x:Bind ProfileVm.MoshPortTo, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBox
|
||||
x:Uid="CommandTextBox"
|
||||
PlaceholderText="SSH command (e.g. 'ssh username@host')"
|
||||
Text="{Binding ProfileVm.Command, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Visibility="{Binding ProfileVm.CommandInput, Converter={StaticResource TrueToVisibleConverter}}"
|
||||
Margin="0 8 2 0" />
|
||||
|
||||
<ComboBox
|
||||
x:Uid="TabColor"
|
||||
Width="150"
|
||||
@@ -344,8 +293,8 @@
|
||||
Header="Tab color"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
ItemTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TabThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTabTheme, Mode=TwoWay}" />
|
||||
ItemsSource="{x:Bind ProfileVm.TabThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding ProfileVm.SelectedTabTheme, Mode=TwoWay}" />
|
||||
|
||||
<ComboBox
|
||||
x:Uid="ProfileTheme"
|
||||
@@ -354,20 +303,21 @@
|
||||
Header="Theme"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
ItemTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TerminalThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTerminalTheme, Mode=TwoWay}" />
|
||||
ItemsSource="{x:Bind ProfileVm.TerminalThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding ProfileVm.SelectedTerminalTheme, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseConPty}"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsOn="{x:Bind UseConPty, Mode=TwoWay}" />
|
||||
IsOn="{x:Bind ProfileVm.UseConPty, Mode=TwoWay}" />
|
||||
|
||||
<ComboBox
|
||||
x:Uid="LineEndingMode"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{x:Bind LineEndingStyles}"
|
||||
SelectedItem="{Binding LineEndingTranslation, Mode=TwoWay}">
|
||||
ItemsSource="{x:Bind ProfileVm.LineEndingStyles}"
|
||||
SelectedItem="{Binding ProfileVm.LineEndingTranslation, Mode=TwoWay}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="enums:LineEndingStyle">
|
||||
<TextBlock Text="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}" />
|
||||
@@ -388,143 +338,188 @@
|
||||
<views:KeyBindingsView ShowCommandName="False" ViewModel="{x:Bind KeyBindings}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
|
||||
<StackPanel Visibility="{Binding InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
<TextBlock Margin="0,30,0,0" Text="{x:Bind Converter={StaticResource I18NConverter},ConverterParameter=SshInfoDialog.Title}" />
|
||||
<StackPanel Margin="0,-20,0,0" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Username, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,23,4,0"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="@"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Host, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,23,4,0"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text=":"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind SshPort, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
x:Uid="CommandLine"
|
||||
DataContext="{Binding ProfileVm}"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=IdentityFileTextBox.PlaceholderText}">
|
||||
Header="Command"
|
||||
Visibility="{Binding CommandInput, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<ContentControl>
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate x:DataType="profiles:SshConnectViewModel">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{Binding Command, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
</ContentControl.ContentTemplate>
|
||||
</ContentControl>
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=SshInfoDialog.Title}"
|
||||
Visibility="{Binding CommandInput, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<ContentControl>
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate x:DataType="profiles:SshConnectViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{Binding Username, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,0,4,0"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="@"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{Binding Host, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,0,4,0"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text=":"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{Binding SshPort, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ContentControl.ContentTemplate>
|
||||
</ContentControl>
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=IdentityFileTextBox.PlaceholderText}"
|
||||
Visibility="{Binding CommandInput, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<ContentControl
|
||||
Content="{x:Bind IdentityFile, Mode=OneWay}"
|
||||
Content="{Binding IdentityFile, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseMoshCheckBox.Content}">
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseMoshCheckBox.Content}"
|
||||
Visibility="{Binding CommandInput, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind UseMosh, Mode=OneWay}"
|
||||
Content="{Binding UseMosh, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource UseConPtyTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<TextBlock
|
||||
x:Uid="MoshPortRange"
|
||||
Margin="0,30,0,0"
|
||||
Text="Mosh Port Range"
|
||||
Visibility="{x:Bind UseMosh, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}" />
|
||||
<StackPanel
|
||||
Margin="0,-20,0,0"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{x:Bind UseMosh, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind MoshPortFrom, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,23,3,0"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text=":"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind MoshPortTo, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=MoshPortRange.Text}"
|
||||
Visibility="{Binding MoshVisible, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<ContentControl>
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate x:DataType="profiles:SshConnectViewModel">
|
||||
<StackPanel
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{Binding MoshPortFrom, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,0,3,0"
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text=":"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="Light"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{Binding MoshPortTo, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ContentControl.ContentTemplate>
|
||||
</ContentControl>
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
x:Uid="TabColor"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Tab color">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind SelectedTabTheme, Mode=OneWay}"
|
||||
Content="{Binding SelectedTabTheme, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Theme">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind SelectedTerminalTheme, Mode=OneWay}"
|
||||
Content="{Binding SelectedTerminalTheme, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseConPty}">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind UseConPty, Mode=OneWay}"
|
||||
Content="{Binding UseConPty, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource UseConPtyTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
DataContext="{Binding ProfileVm}"
|
||||
x:Uid="LineEndingMode"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Line ending translation mode">
|
||||
<ContentControl
|
||||
Content="{x:Bind LineEndingTranslation, Mode=OneWay, Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}"
|
||||
Content="{Binding LineEndingTranslation, Mode=OneWay, Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}"
|
||||
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
@@ -536,7 +531,7 @@
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Keyboard shortcut">
|
||||
<ContentControl
|
||||
Content="{x:Bind KeyBindings, Mode=OneWay}"
|
||||
Content="{Binding KeyBindings, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TerminalKeybindTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<ResourceDictionary
|
||||
x:Class="FluentTerminal.App.Views.TemplateSelectors"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:views="using:FluentTerminal.App.Views"
|
||||
xmlns:models="using:FluentTerminal.Models"
|
||||
xmlns:settings="using:FluentTerminal.App.ViewModels.Settings">
|
||||
<views:TabThemeTemplateSelector x:Key="TabThemeTemplateSelector">
|
||||
<views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
<DataTemplate x:DataType="models:TabTheme">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="0,0,6,0"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="{x:Bind Color}"
|
||||
Glyph="" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</views:TabThemeTemplateSelector.ColoredTemplate>
|
||||
</views:TabThemeTemplateSelector>
|
||||
<views:TerminalThemeTemplateSelector x:Key="TerminalThemeTemplateSelector">
|
||||
<views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock FontStyle="Italic" Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
<DataTemplate x:DataType="models:TerminalTheme">
|
||||
<TextBlock Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
</views:TerminalThemeTemplateSelector>
|
||||
<views:TerminalKeybindTemplateSelector x:Key="TerminalKeybindTemplateSelector">
|
||||
<views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<TextBlock x:Uid="None" FontStyle="Italic" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<views:KeyBindingsView
|
||||
Margin="0,6,0,0"
|
||||
ShowCommandName="False"
|
||||
ViewModel="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
</views:TerminalKeybindTemplateSelector>
|
||||
<views:NoValueTemplateSelector x:Key="NoValueTemplateSelector">
|
||||
<views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock x:Uid="None" FontStyle="Italic" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<views:NoValueTemplateSelector.CustomTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.CustomTemplate>
|
||||
</views:NoValueTemplateSelector>
|
||||
<views:BooleanTemplateSelector x:Key="UseConPtyTemplateSelector">
|
||||
<views:BooleanTemplateSelector.TrueTemplate>
|
||||
<DataTemplate x:DataType="x:Boolean">
|
||||
<TextBlock x:Uid="Yes" />
|
||||
</DataTemplate>
|
||||
</views:BooleanTemplateSelector.TrueTemplate>
|
||||
<views:BooleanTemplateSelector.FalseTemplate>
|
||||
<DataTemplate x:DataType="x:Boolean">
|
||||
<TextBlock x:Uid="No" />
|
||||
</DataTemplate>
|
||||
</views:BooleanTemplateSelector.FalseTemplate>
|
||||
</views:BooleanTemplateSelector>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace FluentTerminal.App.Views
|
||||
{
|
||||
// Added due https://docs.microsoft.com/en-us/windows/uwp/data-binding/data-binding-in-depth#resource-dictionaries-with-xbind
|
||||
public partial class TemplateSelectors
|
||||
{
|
||||
public TemplateSelectors()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ namespace FluentTerminal.Models
|
||||
public bool ShowCustomTitleInTitlebar { get; set; }
|
||||
public bool UseMoshByDefault { get; set; }
|
||||
public bool AutoFallbackToWindowsUsernameInLinks { get; set; }
|
||||
public bool UseQuickSshConnectByDefault { get; set; }
|
||||
public bool RTrimCopiedLines { get; set; }
|
||||
public bool MuteTerminalBeeps { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models.Enums;
|
||||
|
||||
|
||||
namespace FluentTerminal.Models
|
||||
{
|
||||
public interface ISshConnectionInfo
|
||||
{
|
||||
string Host { get; set; }
|
||||
|
||||
ushort SshPort { get; set; }
|
||||
|
||||
string Username { get; set; }
|
||||
|
||||
string IdentityFile { get; set; }
|
||||
|
||||
LineEndingStyle LineEndingTranslation { get; set; }
|
||||
|
||||
bool UseMosh { get; set; }
|
||||
|
||||
ushort MoshPortFrom { get; set; }
|
||||
|
||||
ushort MoshPortTo { get; set; }
|
||||
|
||||
Guid TerminalThemeId { get; set; }
|
||||
|
||||
int TabThemeId { get; set; }
|
||||
|
||||
Task<SshConnectionInfoValidationResult> ValidateAsync();
|
||||
|
||||
bool UseConPty { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace FluentTerminal.Models.Requests
|
||||
{
|
||||
public class MuteTerminalRequest : IMessage
|
||||
{
|
||||
public const byte Identifier = 14;
|
||||
|
||||
byte IMessage.Identifier => Identifier;
|
||||
|
||||
public bool Mute { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace FluentTerminal.Models
|
||||
{
|
||||
public sealed class ShellLocation
|
||||
{
|
||||
public const string SSH = "SSH";
|
||||
public const string Mosh = "MOSH";
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,6 @@ namespace FluentTerminal.Models
|
||||
public int TabThemeId { get; set; }
|
||||
public LineEndingStyle LineEndingTranslation { get; set; }
|
||||
public Dictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>();
|
||||
|
||||
public bool UseConPty { get; set; }
|
||||
|
||||
public string TranslateLineEndings(string content)
|
||||
@@ -90,14 +89,5 @@ namespace FluentTerminal.Models
|
||||
}
|
||||
|
||||
public virtual ShellProfile Clone() => new ShellProfile(this);
|
||||
|
||||
/// <summary>
|
||||
/// Checks the profile for errors and returns string describing the found errors, if any.
|
||||
/// Returns null if no errors found.
|
||||
/// </summary>
|
||||
public virtual string ValidateAndGetErrors()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using FluentTerminal.Models.Enums;
|
||||
|
||||
namespace FluentTerminal.Models
|
||||
{
|
||||
public static class SshConnectionInfoExtensions
|
||||
{
|
||||
public static SshConnectionInfoValidationResult GetValidationResult(this ISshConnectionInfo sshConnectionInfo)
|
||||
{
|
||||
var result = SshConnectionInfoValidationResult.Valid;
|
||||
|
||||
if (string.IsNullOrEmpty(sshConnectionInfo.Username))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.UsernameEmpty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(sshConnectionInfo.Host))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.HostEmpty;
|
||||
}
|
||||
|
||||
if (sshConnectionInfo.SshPort < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (!sshConnectionInfo.UseMosh)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (sshConnectionInfo.MoshPortFrom < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (sshConnectionInfo.MoshPortFrom > sshConnectionInfo.MoshPortTo)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortRangeInvalid;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentTerminal.Models.Enums;
|
||||
|
||||
namespace FluentTerminal.Models
|
||||
{
|
||||
public static class SshConnectionInfoValidationResultExtensions
|
||||
{
|
||||
public static string GetErrorString(this SshConnectionInfoValidationResult result, string separator = "; ") =>
|
||||
string.Join(separator, result.GetErrors());
|
||||
|
||||
public static IEnumerable<string> GetErrors(this SshConnectionInfoValidationResult result)
|
||||
{
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var value in Enum.GetValues(typeof(SshConnectionInfoValidationResult))
|
||||
.Cast<SshConnectionInfoValidationResult>().Where(r => r != SshConnectionInfoValidationResult.Valid))
|
||||
{
|
||||
if ((value & result) == value)
|
||||
{
|
||||
yield return Resources.GetString($"{nameof(SshConnectionInfoValidationResult)}.{value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Metadata;
|
||||
|
||||
namespace FluentTerminal.Models
|
||||
{
|
||||
public class SshProfile : ShellProfile, ISshConnectionInfo
|
||||
public class SshProfile : ShellProfile
|
||||
{
|
||||
#region Constants
|
||||
|
||||
@@ -83,14 +82,6 @@ namespace FluentTerminal.Models
|
||||
|
||||
public override ShellProfile Clone() => new SshProfile(this);
|
||||
|
||||
public Task<SshConnectionInfoValidationResult> ValidateAsync()
|
||||
{
|
||||
return Task.FromResult(this.GetValidationResult());
|
||||
// Here we don't check if IdentityFile exists because this class isn't used in UI (if it's saved - it exists).
|
||||
}
|
||||
|
||||
public override string ValidateAndGetErrors() => this.GetValidationResult().GetErrorString();
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,7 @@
|
||||
<Compile Include="Services\WinPty\WinPtySession.cs" />
|
||||
<Compile Include="SystemTrayApplicationContext.cs" />
|
||||
<Compile Include="Utilities.cs" />
|
||||
<Compile Include="VolumeControl.cs" />
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
|
||||
@@ -90,6 +90,7 @@ namespace FluentTerminal.SystemTray
|
||||
Task.Run(() => container.Resolve<IUpdateService>().CheckForUpdate());
|
||||
|
||||
var settingsService = container.Resolve<ISettingsService>();
|
||||
Task.Run(() => Utilities.MuteTerminal(settingsService.GetApplicationSettings().MuteTerminalBeeps));
|
||||
if (settingsService.GetApplicationSettings().EnableTrayIcon)
|
||||
{
|
||||
Application.Run(container.Resolve<SystemTrayApplicationContext>());
|
||||
|
||||
@@ -117,6 +117,9 @@ namespace FluentTerminal.SystemTray.Services
|
||||
case CheckFileExistsRequest.Identifier:
|
||||
await HandleCheckFileExistsRequest(args);
|
||||
break;
|
||||
case MuteTerminalRequest.Identifier:
|
||||
await HandleMuteTerminalRequest(args);
|
||||
break;
|
||||
default:
|
||||
Logger.Instance.Error("Received unknown message type: {messageType}", messageType);
|
||||
break;
|
||||
@@ -235,6 +238,13 @@ namespace FluentTerminal.SystemTray.Services
|
||||
deferral.Complete();
|
||||
}
|
||||
|
||||
private async Task HandleMuteTerminalRequest(AppServiceRequestReceivedEventArgs args)
|
||||
{
|
||||
var messageContent = (string)args.Request.Message[MessageKeys.Content];
|
||||
var request = JsonConvert.DeserializeObject<MuteTerminalRequest>(messageContent);
|
||||
Utilities.MuteTerminal(request.Mute);
|
||||
}
|
||||
|
||||
private async Task HandleCheckFileExistsRequest(AppServiceRequestReceivedEventArgs args)
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
using System.Windows.Input;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models.Enums;
|
||||
|
||||
|
||||
@@ -500,11 +501,11 @@ namespace FluentTerminal.SystemTray
|
||||
|
||||
public static string ResolveLocation(string location)
|
||||
{
|
||||
switch (location)
|
||||
switch (location?.ToLower())
|
||||
{
|
||||
case ShellLocation.SSH:
|
||||
case Constants.SshCommandName:
|
||||
return GetSshPath();
|
||||
case ShellLocation.Mosh:
|
||||
case Constants.MoshCommandName:
|
||||
return GetMoshPath();
|
||||
default:
|
||||
return location;
|
||||
@@ -567,5 +568,49 @@ namespace FluentTerminal.SystemTray
|
||||
writer.Write(content);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void MuteTerminal(bool mute)
|
||||
{
|
||||
bool? deviceMuted = VolumeControl.GetDefaultAudioEndpointMute();
|
||||
if (deviceMuted != true)
|
||||
{
|
||||
VolumeControl.SetDefaultAudioEndpointMute(true);
|
||||
}
|
||||
|
||||
Process cmdProcess = new Process();
|
||||
cmdProcess.StartInfo.FileName = "cmd.exe";
|
||||
cmdProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
cmdProcess.StartInfo.Arguments = "/k \x07";
|
||||
cmdProcess.Start();
|
||||
|
||||
bool wasConhostMuted = false;
|
||||
int attempt = 0;
|
||||
while (wasConhostMuted == false && attempt++ < 2 && !cmdProcess.HasExited)
|
||||
{
|
||||
cmdProcess.WaitForExit(1500);
|
||||
|
||||
foreach (Process conhost in Process.GetProcessesByName("conhost"))
|
||||
{
|
||||
try
|
||||
{
|
||||
bool? isMuted = null;
|
||||
isMuted = VolumeControl.GetAudioSessionMute(conhost.Id);
|
||||
if (isMuted != null && isMuted != mute)
|
||||
{
|
||||
VolumeControl.SetAudioSessionMute(conhost.Id, mute);
|
||||
wasConhostMuted = true;
|
||||
Logger.Instance.Debug($"Mute state for conhost process with id={conhost.Id} was set to {mute}.");
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
cmdProcess.Kill();
|
||||
if (deviceMuted != true)
|
||||
{
|
||||
VolumeControl.SetDefaultAudioEndpointMute(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FluentTerminal.SystemTray
|
||||
{
|
||||
public static class VolumeControl
|
||||
{
|
||||
public static bool? GetAudioSessionMute(int processId)
|
||||
{
|
||||
ISimpleAudioVolume volume = GetAudioVolume(processId);
|
||||
if (volume == null)
|
||||
return null;
|
||||
|
||||
volume.GetMute(out var mute);
|
||||
Marshal.ReleaseComObject(volume);
|
||||
return mute;
|
||||
}
|
||||
|
||||
public static void SetAudioSessionMute(int processId, bool mute)
|
||||
{
|
||||
ISimpleAudioVolume volume = GetAudioVolume(processId);
|
||||
if (volume == null)
|
||||
return;
|
||||
|
||||
Guid guid = Guid.Empty;
|
||||
volume.SetMute(mute, ref guid);
|
||||
Marshal.ReleaseComObject(volume);
|
||||
}
|
||||
|
||||
public static bool? GetDefaultAudioEndpointMute()
|
||||
{
|
||||
IMMDevice speakers = GetDefaultDevice();
|
||||
|
||||
Guid IID_IAudioEndpointVolume = typeof(IAudioEndpointVolume).GUID;
|
||||
speakers.Activate(ref IID_IAudioEndpointVolume, 0, IntPtr.Zero, out var result);
|
||||
IAudioEndpointVolume audioEndpoint = (IAudioEndpointVolume)result;
|
||||
if (audioEndpoint == null)
|
||||
return null;
|
||||
|
||||
audioEndpoint.GetMute(out var mute);
|
||||
Marshal.ReleaseComObject(audioEndpoint);
|
||||
return mute;
|
||||
}
|
||||
|
||||
public static void SetDefaultAudioEndpointMute(bool mute)
|
||||
{
|
||||
IMMDevice speakers = GetDefaultDevice();
|
||||
|
||||
Guid IID_IAudioEndpointVolume = typeof(IAudioEndpointVolume).GUID;
|
||||
speakers.Activate(ref IID_IAudioEndpointVolume, 0, IntPtr.Zero, out var result);
|
||||
IAudioEndpointVolume audioEndpoint = (IAudioEndpointVolume)result;
|
||||
if (audioEndpoint == null)
|
||||
return;
|
||||
|
||||
Guid guid = Guid.Empty;
|
||||
audioEndpoint.SetMute(mute, ref guid);
|
||||
Marshal.ReleaseComObject(audioEndpoint);
|
||||
}
|
||||
|
||||
private static IMMDevice GetDefaultDevice()
|
||||
{
|
||||
IMMDeviceEnumerator deviceEnumerator = (IMMDeviceEnumerator)(new MMDeviceEnumerator());
|
||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
||||
Marshal.ReleaseComObject(deviceEnumerator);
|
||||
return device;
|
||||
}
|
||||
|
||||
private static ISimpleAudioVolume GetAudioVolume(int pid)
|
||||
{
|
||||
IMMDevice device = GetDefaultDevice();
|
||||
Guid IID_IAudioSessionManager2 = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref IID_IAudioSessionManager2, 0, IntPtr.Zero, out var result);
|
||||
IAudioSessionManager2 sessionManager = (IAudioSessionManager2)result;
|
||||
|
||||
sessionManager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
|
||||
ISimpleAudioVolume volumeControl = null;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var sessionControl2);
|
||||
sessionControl2.GetProcessId(out var processId);
|
||||
|
||||
if (processId == pid)
|
||||
{
|
||||
volumeControl = sessionControl2 as ISimpleAudioVolume;
|
||||
break;
|
||||
}
|
||||
Marshal.ReleaseComObject(sessionControl2);
|
||||
}
|
||||
Marshal.ReleaseComObject(sessionEnumerator);
|
||||
Marshal.ReleaseComObject(sessionManager);
|
||||
Marshal.ReleaseComObject(device);
|
||||
return volumeControl;
|
||||
}
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||
internal class MMDeviceEnumerator
|
||||
{
|
||||
}
|
||||
|
||||
internal enum EDataFlow
|
||||
{
|
||||
eRender,
|
||||
eCapture,
|
||||
eAll,
|
||||
EDataFlow_enum_count
|
||||
}
|
||||
|
||||
internal enum ERole
|
||||
{
|
||||
eConsole,
|
||||
eMultimedia,
|
||||
eCommunications,
|
||||
ERole_enum_count
|
||||
}
|
||||
|
||||
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IAudioEndpointVolume
|
||||
{
|
||||
int NoImp1();
|
||||
int NoImp2();
|
||||
int NoImp3();
|
||||
int NoImp4();
|
||||
int NoImp5();
|
||||
int NoImp6();
|
||||
int NoImp7();
|
||||
int NoImp8();
|
||||
int NoImp9();
|
||||
int NoImp10();
|
||||
int NoImp11();
|
||||
|
||||
[PreserveSig]
|
||||
int SetMute([MarshalAs(UnmanagedType.Bool)] Boolean bMute, ref Guid pguidEventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMute(out bool pbMute);
|
||||
}
|
||||
|
||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMMDeviceEnumerator
|
||||
{
|
||||
int NoImp1();
|
||||
|
||||
[PreserveSig]
|
||||
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice);
|
||||
}
|
||||
|
||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMMDevice
|
||||
{
|
||||
[PreserveSig]
|
||||
int Activate(ref Guid iid, int clsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||
}
|
||||
|
||||
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IAudioSessionManager2
|
||||
{
|
||||
int NoImp1();
|
||||
int NoImp2();
|
||||
|
||||
[PreserveSig]
|
||||
int GetSessionEnumerator(out IAudioSessionEnumerator SessionEnum);
|
||||
}
|
||||
|
||||
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IAudioSessionEnumerator
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetCount(out int SessionCount);
|
||||
|
||||
[PreserveSig]
|
||||
int GetSession(int SessionCount, out IAudioSessionControl2 Session);
|
||||
}
|
||||
|
||||
[Guid("87CE5498-68D6-44E5-9215-6DA47EF883D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface ISimpleAudioVolume
|
||||
{
|
||||
[PreserveSig]
|
||||
int SetMasterVolume(float fLevel, ref Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMasterVolume(out float pfLevel);
|
||||
|
||||
[PreserveSig]
|
||||
int SetMute(bool bMute, ref Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMute(out bool pbMute);
|
||||
}
|
||||
|
||||
[Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IAudioSessionControl2
|
||||
{
|
||||
// IAudioSessionControl
|
||||
int NoImp0();
|
||||
int NoImp1();
|
||||
int NoImp2();
|
||||
int NoImp3();
|
||||
int NoImp4();
|
||||
int NoImp5();
|
||||
int NoImp6();
|
||||
int NoImp7();
|
||||
int NoImp8();
|
||||
|
||||
// IAudioSessionControl2
|
||||
[PreserveSig]
|
||||
int GetSessionIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int GetSessionInstanceIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int GetProcessId(out int pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int IsSystemSoundsSession();
|
||||
|
||||
[PreserveSig]
|
||||
int SetDuckingPreference(bool optOut);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user