Compare commits

..
Author SHA1 Message Date
FelixSe 7bbed15874 Merge branch 'master' into Dev/NativeRenderer 2019-04-07 21:09:22 +02:00
FelixSe 997cb9814e fixed wrong thread event, made canvas resize 2019-04-03 23:03:24 +02:00
Andrew Kralovec 6524d14687 Native Renderer Collabate PR (#284)
* Load terminal options and add the inital scroll viewer

* Current Virtual Cavnas, problem with event handler calling the canvas on draw
2019-04-03 07:26:07 +02:00
FelixSe 7876984327 proof of concepts for vtnetcore, tterm inspired 2019-03-31 21:58:02 +02:00
474 changed files with 12676 additions and 54630 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
# Git will handle the files in whatever way it thinks is best.
# Overwrites global core.autocrlf git setting
* text=auto
-23
View File
@@ -1,23 +0,0 @@
name: CI
on: [push]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- name: Build client
working-directory: ./FluentTerminal.Client
run: |
yarn
yarn build
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v1.0.2
- name: MSBuild
run: msbuild FluentTerminal.sln -t:build -p:Configuration=Debug -p:Platform=x64 -m -restore
-2
View File
@@ -292,5 +292,3 @@ __pycache__/
*.btm.cs *.btm.cs
*.odx.cs *.odx.cs
*.xsd.cs *.xsd.cs
!FluentTerminal.App/MoshExecutables/*
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

@@ -1,4 +1,3 @@
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%1\"" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
@@ -1,9 +0,0 @@
copy /y FluentTerminal.ico "%LOCALAPPDATA%\Microsoft\WindowsApps"
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
reg add "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
@@ -1,2 +0,0 @@
If you want a an icon for the context menu entries, download the .ico file and put in the same directory as the Install_with_icon.bat file then run it.
The same Uninstall.bat works for both Install.bat files.
@@ -1,5 +1,3 @@
reg delete "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here" /f reg delete "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here" /f reg delete "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here" /f reg delete "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here" /f
del %LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Services.Test
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog() public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
{ {
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>(); var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
var dialogService = new DialogService(Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>); var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowCreateKeyBindingDialog(); dialogService.ShowCreateKeyBindingDialog();
@@ -36,9 +36,9 @@ namespace FluentTerminal.App.Services.Test
var title = string.Empty; var title = string.Empty;
var content = _fixture.Create<string>(); var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2); var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>); var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1)); Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("title"); showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("title");
} }
@@ -49,9 +49,9 @@ namespace FluentTerminal.App.Services.Test
var title = _fixture.Create<string>(); var title = _fixture.Create<string>();
var content = string.Empty; var content = string.Empty;
var buttons = _fixture.CreateMany<DialogButton>(2); var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>); var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1)); Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("content"); showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("content");
} }
@@ -61,9 +61,9 @@ namespace FluentTerminal.App.Services.Test
{ {
var title = _fixture.Create<string>(); var title = _fixture.Create<string>();
var content = _fixture.Create<string>(); var content = _fixture.Create<string>();
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>); var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content); Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
showMessageDialogAsnyc.Should().Throw<ArgumentException>().And.ParamName.Should().Be("buttons"); showMessageDialogAsnyc.Should().Throw<ArgumentException>().And.ParamName.Should().Be("buttons");
} }
@@ -75,19 +75,30 @@ namespace FluentTerminal.App.Services.Test
var content = _fixture.Create<string>(); var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2); var buttons = _fixture.CreateMany<DialogButton>(2);
var messageDialog = new Mock<IMessageDialog>(); var messageDialog = new Mock<IMessageDialog>();
var dialogService = new DialogService(() => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>); var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1)); dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
messageDialog.Verify(x => x.ShowAsync(), Times.Once); messageDialog.Verify(x => x.ShowAsync(), Times.Once);
} }
[Fact]
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
{
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowProfileSelectionDialogAsync();
shellProfileSelectionDialog.Verify(x => x.SelectProfile(), Times.Once);
}
[Fact] [Fact]
public void ShowInputDialogAsync_UsesIInputDialogSetTitle() public void ShowInputDialogAsync_UsesIInputDialogSetTitle()
{ {
var title = "title"; var title = "title";
var inputDialog = new Mock<IInputDialog>(); var inputDialog = new Mock<IInputDialog>();
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>); var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowInputDialogAsync(title); dialogService.ShowInputDialogAsync(title);
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
@@ -11,13 +11,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture" Version="4.8.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" /> <PackageReference Include="FluentAssertions" Version="5.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.10.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
@@ -34,11 +34,4 @@
<ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" /> <ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.FoundationContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project> </Project>
@@ -41,41 +41,6 @@ namespace FluentTerminal.App.Services.Test
invoke.Should().Throw<InvalidOperationException>(); invoke.Should().Throw<InvalidOperationException>();
} }
[Fact]
public void DeregisterCommandHandler_CommandIsNull_ThrowsArgumentNullException()
{
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(null);
invoke.Should().Throw<ArgumentNullException>();
}
[Fact]
public void DeregisterCommandHandler_CommandRegistered_HandlerGetsRemoved()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var handler = _fixture.Create<Action>();
var keyboardCommandService = new KeyboardCommandService();
keyboardCommandService.RegisterCommandHandler(command, handler);
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact]
public void DeregisterCommandHandler_CommandNotRegistered_ShouldNotThrow()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact] [Fact]
public void SendCommand_CommandIsRegistered_HandlerGetsInvoked() public void SendCommand_CommandIsRegistered_HandlerGetsInvoked()
@@ -1,6 +1,7 @@
using AutoFixture; using AutoFixture;
using FluentAssertions; using FluentAssertions;
using FluentTerminal.App.Services.Implementation; using FluentTerminal.App.Services.Implementation;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models; using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using Moq; using Moq;
@@ -9,9 +10,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using FluentTerminal.Models.Messages;
using Xunit; using Xunit;
using Microsoft.Toolkit.Mvvm.Messaging;
namespace FluentTerminal.App.Services.Test namespace FluentTerminal.App.Services.Test
{ {
@@ -37,8 +36,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
new SettingsService(defaultValueProvider.Object, applicationDataContainers); new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -60,7 +58,6 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
ShellProfiles = shellProfilesContainer.Object, ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -89,7 +86,6 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>() Themes = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -115,7 +111,6 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>() Themes = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -139,7 +134,6 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>() Themes = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -161,13 +155,11 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>() Themes = Mock.Of<IApplicationDataContainer>()
}; };
var currentThemeChangedEventInvoked = false; var currentThemeChangedEventInvoked = false;
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
WeakReferenceMessenger.Default.Register<CurrentThemeChangedMessage>(this, settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
(r, m) => currentThemeChangedEventInvoked = true);
settingsService.SaveCurrentThemeId(currentThemeId); settingsService.SaveCurrentThemeId(currentThemeId);
@@ -186,8 +178,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -212,12 +203,10 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = roamingSettings.Object, RoamingSettings = roamingSettings.Object,
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
WeakReferenceMessenger.Default.Register<CurrentThemeChangedMessage>(this, settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
(r, m) => currentThemeChangedEventInvoked = true);
settingsService.SaveTheme(theme); settingsService.SaveTheme(theme);
@@ -236,8 +225,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -259,8 +247,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -283,8 +270,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -307,8 +293,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -331,8 +316,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -354,8 +338,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -377,12 +360,10 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
WeakReferenceMessenger.Default.Register<TerminalOptionsChangedMessage>(this, settingsService.TerminalOptionsChanged += (s, e) => terminalOptionsChangedEventInvoked = true;
(r, m) => terminalOptionsChangedEventInvoked = true);
settingsService.SaveTerminalOptions(terminalOptions); settingsService.SaveTerminalOptions(terminalOptions);
@@ -402,8 +383,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -425,8 +405,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -448,11 +427,10 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
WeakReferenceMessenger.Default.Register<ApplicationSettingsChangedMessage>(this, (r, m) => applicationSettingsChangedEventInvoked = true); settingsService.ApplicationSettingsChanged += (s, e) => applicationSettingsChangedEventInvoked = true;
settingsService.SaveApplicationSettings(applicationSettings); settingsService.SaveApplicationSettings(applicationSettings);
@@ -475,8 +453,7 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -494,14 +471,17 @@ namespace FluentTerminal.App.Services.Test
var commands = _fixture.CreateMany<Command>(5); var commands = _fixture.CreateMany<Command>(5);
var defaultValueProvider = new Mock<IDefaultValueProvider>(); var defaultValueProvider = new Mock<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>(); var keyBindingsContainer = new Mock<IApplicationDataContainer>();
foreach (var command in commands)
{
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(EnumHelper.GetEnumDescription(command), null)).Returns(new Collection<KeyBinding> { });
}
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
KeyBindings = keyBindingsContainer.Object, KeyBindings = keyBindingsContainer.Object,
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -526,8 +506,7 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -550,12 +529,10 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
WeakReferenceMessenger.Default.Register<KeyBindingsChangedMessage>(this, settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
(r, m) => keyBindingsChangedEventInvoked = true);
settingsService.SaveKeyBindings(command.ToString(), keyBindings); settingsService.SaveKeyBindings(command.ToString(), keyBindings);
@@ -573,8 +550,7 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -599,12 +575,10 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(), Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>()
SshProfiles = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
WeakReferenceMessenger.Default.Register<KeyBindingsChangedMessage>(this, settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
(r, m) => keyBindingsChangedEventInvoked = true);
settingsService.ResetKeyBindings(); settingsService.ResetKeyBindings();
@@ -624,7 +598,6 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>() Themes = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -650,7 +623,6 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>() Themes = Mock.Of<IApplicationDataContainer>()
}; };
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers); var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -672,7 +644,6 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
ShellProfiles = shellProfilesContainer.Object, ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -682,7 +653,7 @@ namespace FluentTerminal.App.Services.Test
var result = settingsService.GetShellProfiles(); var result = settingsService.GetShellProfiles();
result.Select(p => p.EqualTo(shellProfiles.FirstOrDefault(x => x.Id == p.Id)).Should().BeTrue()); result.Should().BeEquivalentTo(shellProfiles);
shellProfilesContainer.Verify(x => x.GetAll(), Times.Once); shellProfilesContainer.Verify(x => x.GetAll(), Times.Once);
} }
@@ -695,7 +666,6 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
ShellProfiles = shellProfilesContainer.Object, ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -717,7 +687,6 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
ShellProfiles = shellProfilesContainer.Object, ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -742,7 +711,6 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
ShellProfiles = shellProfilesContainer.Object, ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(), RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = localSettings.Object, LocalSettings = localSettings.Object,
@@ -771,7 +739,6 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
ShellProfiles = Mock.Of<IApplicationDataContainer>(), ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(), KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(), LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = roamingSettings.Object, RoamingSettings = roamingSettings.Object,
@@ -10,7 +10,6 @@ using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Foundation.Collections;
using Xunit; using Xunit;
namespace FluentTerminal.App.Services.Test namespace FluentTerminal.App.Services.Test
@@ -35,20 +34,20 @@ namespace FluentTerminal.App.Services.Test
}); });
var appServiceConnection = new Mock<IAppServiceConnection>(); var appServiceConnection = new Mock<IAppServiceConnection>();
var response = _fixture.Create<CreateTerminalResponse>(); var response = _fixture.Create<CreateTerminalResponse>();
var responseMessage = new ValueSet var responseMessage = new Dictionary<string, string>
{ {
[MessageKeys.Type] = _fixture.Create<string>(), [MessageKeys.Type] = _fixture.Create<string>(),
[MessageKeys.Content] = JsonConvert.SerializeObject(response) [MessageKeys.Content] = JsonConvert.SerializeObject(response)
}; };
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<ValueSet>())).Returns(Task.FromResult(responseMessage)); appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
var terminalSize = _fixture.Create<TerminalSize>(); var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>(); var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>(); var sessionType = _fixture.Create<SessionType>();
var id = _fixture.Create<byte>(); var id = _fixture.Create<int>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object); var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object); trayProcessCommunicationService.Initialize(appServiceConnection.Object);
var result = await trayProcessCommunicationService.CreateTerminalAsync(id, terminalSize, shellProfile, sessionType); var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
result.Should().BeEquivalentTo(response); result.Should().BeEquivalentTo(response);
} }
@@ -56,7 +55,7 @@ namespace FluentTerminal.App.Services.Test
[Fact] [Fact]
public async Task ResizeTerminal_Default_SendsResizeTerminalRequest() public async Task ResizeTerminal_Default_SendsResizeTerminalRequest()
{ {
var terminalId = _fixture.Create<byte>(); var terminalId = _fixture.Create<int>();
var terminalSize = _fixture.Create<TerminalSize>(); var terminalSize = _fixture.Create<TerminalSize>();
var settingsService = new Mock<ISettingsService>(); var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3); var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -68,15 +67,15 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object); var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object); trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.ResizeTerminalAsync(terminalId, terminalSize); await trayProcessCommunicationService.ResizeTerminal(terminalId, terminalSize);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once); appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(ResizeTerminalRequest))), Times.Once);
} }
[Fact] [Fact]
public async Task Write_Default_SendsWriteDataRequest() public async Task Write_Default_SendsWriteDataRequest()
{ {
var terminalId = _fixture.Create<byte>(); var terminalId = _fixture.Create<int>();
var data = _fixture.Create<byte[]>(); var data = _fixture.Create<byte[]>();
var settingsService = new Mock<ISettingsService>(); var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3); var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -88,15 +87,15 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object); var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object); trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.WriteAsync(terminalId, data); await trayProcessCommunicationService.Write(terminalId, data);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once); appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteDataRequest))), Times.Once);
} }
[Fact] [Fact]
public async Task CloseTerminal_Default_SendsTerminalExitedRequest() public async Task CloseTerminal_Default_SendsTerminalExitedRequest()
{ {
var terminalId = _fixture.Create<byte>(); var terminalId = _fixture.Create<int>();
var settingsService = new Mock<ISettingsService>(); var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3); var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>> settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
@@ -107,21 +106,24 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object); var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object); trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.CloseTerminalAsync(terminalId); await trayProcessCommunicationService.CloseTerminal(terminalId);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once); appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(TerminalExitedRequest))), Times.Once);
} }
[Fact] [Fact]
public void OnMessageReceived_TerminalExitedRequest_InvokesTerminalExitedEvent() public void OnMessageReceived_TerminalExitedRequest_InvokesTerminalExitedEvent()
{ {
var request = new TerminalExitedRequest(_fixture.Create<byte>(), _fixture.Create<int>()); var terminalId = _fixture.Create<int>();
var receivedTerminalId = 0;
var receivedExitStatus = (TerminalExitStatus) null; var terminalExitedEventCalled = false;
var request = new TerminalExitedRequest
var message = new ValueSet
{ {
[MessageKeys.Type] = (byte) MessageIdentifiers.TerminalExitedRequest, TerminalId = terminalId
};
var message = new Dictionary<string, string>
{
[MessageKeys.Type] = nameof(TerminalExitedRequest),
[MessageKeys.Content] = JsonConvert.SerializeObject(request) [MessageKeys.Content] = JsonConvert.SerializeObject(request)
}; };
var settingsService = new Mock<ISettingsService>(); var settingsService = new Mock<ISettingsService>();
@@ -133,29 +135,33 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object); var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
var appServiceConnection = new Mock<IAppServiceConnection>(); var appServiceConnection = new Mock<IAppServiceConnection>();
trayProcessCommunicationService.Initialize(appServiceConnection.Object); trayProcessCommunicationService.Initialize(appServiceConnection.Object);
trayProcessCommunicationService.TerminalExited += (s, status) => trayProcessCommunicationService.TerminalExited += (s, e) =>
{ {
receivedExitStatus = status; terminalExitedEventCalled = true;
receivedTerminalId = e;
}; };
appServiceConnection.Raise(x => x.MessageReceived += null, null, message); appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
receivedExitStatus.Should().NotBeNull(); terminalExitedEventCalled.Should().BeTrue();
receivedExitStatus.TerminalId.Should().Be(request.TerminalId); receivedTerminalId.Should().Be(terminalId);
receivedExitStatus.ExitCode.Should().Be(request.ExitCode);
} }
[Fact] [Fact]
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler() public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
{ {
var terminalId = _fixture.Create<byte>(); var terminalId = _fixture.Create<int>();
var output = _fixture.Create<byte[]>(); var output = _fixture.Create<byte[]>();
var receivedOutput = default(byte[]); var receivedOutput = default(byte[]);
var message = new ValueSet var request = new DisplayTerminalOutputRequest
{ {
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier, TerminalId = terminalId,
[MessageKeys.TerminalId] = terminalId, Output = output
[MessageKeys.Content] = output };
var message = new Dictionary<string, string>
{
[MessageKeys.Type] = nameof(DisplayTerminalOutputRequest),
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
}; };
var settingsService = new Mock<ISettingsService>(); var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3); var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Windows.Storage; using Windows.Storage;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App.Services.Adapters namespace FluentTerminal.App.Services.Adapters
{ {
@@ -21,11 +20,6 @@ namespace FluentTerminal.App.Services.Adapters
_applicationDataContainer.Values.Remove(key); _applicationDataContainer.Values.Remove(key);
} }
public void Clear()
{
_applicationDataContainer.Values.Clear();
}
public IEnumerable<object> GetAll() public IEnumerable<object> GetAll()
{ {
return _applicationDataContainer.Values.Select(x => x.Value); return _applicationDataContainer.Values.Select(x => x.Value);
@@ -57,8 +51,7 @@ namespace FluentTerminal.App.Services.Adapters
public void WriteValueAsJson<T>(string name, T value) public void WriteValueAsJson<T>(string name, T value)
{ {
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value, _applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value);
PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
} }
} }
} }
@@ -7,7 +7,5 @@
public IApplicationDataContainer Themes { get; set; } public IApplicationDataContainer Themes { get; set; }
public IApplicationDataContainer KeyBindings { get; set; } public IApplicationDataContainer KeyBindings { get; set; }
public IApplicationDataContainer ShellProfiles { get; set; } public IApplicationDataContainer ShellProfiles { get; set; }
public IApplicationDataContainer SshProfiles { get; set; }
public IApplicationDataContainer HistoryContainer { get; set; }
} }
} }
-6
View File
@@ -5,11 +5,5 @@
public const string ThemesContainerName = "Themes"; public const string ThemesContainerName = "Themes";
public const string KeyBindingsContainerName = "KeyBindings"; public const string KeyBindingsContainerName = "KeyBindings";
public const string ShellProfilesContainerName = "ShellProfiles"; 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";
public const string TerminalViewModelStateId = "TerminalViewModelState";
} }
} }
@@ -1,9 +0,0 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IAboutDialog
{
Task ShowAsync();
}
}
@@ -1,10 +0,0 @@
using System.Threading.Tasks;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ICustomCommandDialog
{
Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null);
}
}
@@ -0,0 +1,12 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IShellProfileSelectionDialog
{
ShellProfile SelectedProfile { get; }
Task<ShellProfile> SelectProfile();
}
}
@@ -0,0 +1,11 @@
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshConnectionInfo
{
string Host { get; set; }
ushort Port { get; set; }
string Username { get; set; }
}
}
@@ -1,10 +1,9 @@
using FluentTerminal.Models; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs namespace FluentTerminal.App.Services.Dialogs
{ {
public interface ISshConnectionInfoDialog public interface ISshConnectionInfoDialog
{ {
Task<SshProfile> GetSshConnectionInfoAsync(SshProfile input = null); Task<ISshConnectionInfo> GetSshConnectionInfoAsync();
} }
} }
@@ -1,10 +0,0 @@
using Windows.UI.Xaml;
namespace FluentTerminal.App.Services.EventArgs
{
public class NewTabRequestedEventArgs : System.EventArgs
{
public DragEventArgs DragEventArgs { get; set; }
public int Position { get; set; }
}
}
@@ -1,13 +1,7 @@
using System; namespace FluentTerminal.App.Services.EventArgs
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.EventArgs
{ {
public class NewWindowRequestedEventArgs : System.EventArgs public class NewWindowRequestedEventArgs : System.EventArgs
{ {
public ShellProfile Profile { get; } public bool ShowProfileSelection { get; set; }
public NewWindowRequestedEventArgs(ShellProfile profile) =>
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
} }
} }
@@ -1,23 +0,0 @@
using System;
namespace FluentTerminal.App.Services.Exceptions
{
public class ReadTextFileException : Exception
{
public ReadTextFileException() : base()
{
}
public ReadTextFileException(string message) : base(message)
{
}
public ReadTextFileException(string message, Exception innerException) : base(message, innerException)
{
}
protected ReadTextFileException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
@@ -1,23 +0,0 @@
using System;
namespace FluentTerminal.App.Services.Exceptions
{
public class SaveTextFileException : Exception
{
public SaveTextFileException() : base()
{
}
public SaveTextFileException(string message) : base(message)
{
}
public SaveTextFileException(string message, Exception innerException) : base(message, innerException)
{
}
protected SaveTextFileException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
@@ -6,13 +6,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" /> <PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Microsoft.Toolkit.Mvvm" Version="7.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="PListNet" Version="2.0.3" />
<PackageReference Include="PListNet" Version="3.2.0" /> <PackageReference Include="RestSharp" Version="106.6.9" />
<PackageReference Include="RestSharp" Version="106.12.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.3.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" /> <PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
</ItemGroup> </ItemGroup>
@@ -1,13 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Foundation.Collections;
namespace FluentTerminal.App.Services namespace FluentTerminal.App.Services
{ {
public interface IAppServiceConnection public interface IAppServiceConnection
{ {
event EventHandler<ValueSet> MessageReceived; event EventHandler<IDictionary<string, string>> MessageReceived;
Task<ValueSet> SendMessageAsync(ValueSet message); Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message);
} }
} }
@@ -14,8 +14,6 @@ namespace FluentTerminal.App.Services
void Delete(string key); void Delete(string key);
void Clear();
IEnumerable<object> GetAll(); IEnumerable<object> GetAll();
} }
} }
@@ -1,13 +0,0 @@
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface IApplicationLanguageService
{
IEnumerable<string> Languages { get; }
void SetLanguage(string language);
string GetCurrentLanguage();
}
}
@@ -1,7 +1,6 @@
using FluentTerminal.App.Services.EventArgs; using FluentTerminal.App.Services.EventArgs;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.UI.Core;
namespace FluentTerminal.App.Services namespace FluentTerminal.App.Services
{ {
@@ -15,13 +14,9 @@ namespace FluentTerminal.App.Services
int Id { get; } int Id { get; }
string Title { get; set; } string Title { get; set; }
Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal, Task RunOnDispatcherThread(Action action);
bool enforceNewSchedule = false);
Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal, Task<bool> TryClose();
bool enforceNewSchedule = false);
Task<bool> TryCloseAsync();
bool ToggleFullScreen(); bool ToggleFullScreen();
bool IsApiContractPresent(string api, ushort version); bool IsApiContractPresent(string api, ushort version);
@@ -4,7 +4,7 @@ namespace FluentTerminal.App.Services
{ {
public interface IClipboardService public interface IClipboardService
{ {
Task<string> GetTextAsync(); Task<string> GetText();
void SetText(string text); void SetText(string text);
} }
@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface ICommandHistoryService
{
/// <summary>
/// Returns the most recent <paramref name="top"/> commands from history.
/// </summary>
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
/// <param name="top">Maximal number of items to return.</param>
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
/// already have them loaded.</param>
List<ExecutedCommand> GetHistoryRecentFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profilesProvider = null);
/// <summary>
/// Returns the most used <paramref name="top"/> commands from history.
/// </summary>
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
/// <param name="top">Maximal number of items to return.</param>
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
/// already have them loaded.</param>
List<ExecutedCommand> GetHistoryMostUsedFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profilesProvider = null);
void MarkUsed(ShellProfile profile);
void Delete(ExecutedCommand executedCommand);
}
}
@@ -1,27 +1,24 @@
using FluentTerminal.Models; using FluentTerminal.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentTerminal.App.Services.Dialogs;
namespace FluentTerminal.App.Services namespace FluentTerminal.App.Services
{ {
public enum DialogButton public enum DialogButton
{ {
// ReSharper disable once InconsistentNaming
OK, OK,
Cancel Cancel
} }
public interface IDialogService public interface IDialogService
{ {
Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons); Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons);
Task<ShellProfile> ShowProfileSelectionDialogAsync();
Task<KeyBinding> ShowCreateKeyBindingDialog(); Task<KeyBinding> ShowCreateKeyBindingDialog();
Task<string> ShowInputDialogAsync(string title); Task<string> ShowInputDialogAsync(string title);
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null); Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync();
Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null);
Task ShowAboutDialogAsync();
} }
} }
@@ -0,0 +1,16 @@
using System;
namespace FluentTerminal.App.Services
{
public interface IDispatcherTimer
{
void Start();
void Stop();
TimeSpan Interval { get; set; }
bool IsEnabled { get; }
event EventHandler<object> Tick;
}
}
@@ -6,12 +6,10 @@ namespace FluentTerminal.App.Services
{ {
public interface IFileSystemService public interface IFileSystemService
{ {
Task<File> OpenFileAsync(IEnumerable<string> fileTypes); Task<File> OpenFile(IEnumerable<string> fileTypes);
Task<string> BrowseForDirectoryAsync(); Task<string> BrowseForDirectory();
Task SaveTextFileAsync(string name, string fileTypeDescription, string fileType, string content); Task SaveTextFile(string name, string fileTypeDescription, string fileType, string content);
Task<ImageFile> SaveImageInRoamingAsync(ImageFile imageFile);
} }
} }
@@ -1,19 +0,0 @@
using FluentTerminal.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IImageFileSystemService
{
Task<ImageFile> ImportTemporaryImageFileAsync(IEnumerable<string> fileTypes);
Task RemoveTemporaryBackgroundThemeImageAsync();
Task RemoveImportedImageAsync(string fileName);
string EncodeImage(ImageFile imageFile);
Task<ImageFile> ImportThemeImageAsync(ImageFile backgroundImage, string encodedImage);
}
}
@@ -8,6 +8,5 @@ namespace FluentTerminal.App.Services
void SendCommand(string command); void SendCommand(string command);
void DeregisterCommandHandler(string command); void DeregisterCommandHandler(string command);
void ClearCommandHandlers();
} }
} }
@@ -6,8 +6,16 @@ namespace FluentTerminal.App.Services
{ {
public interface ISettingsService public interface ISettingsService
{ {
event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
event EventHandler<Guid> CurrentThemeChanged;
event EventHandler KeyBindingsChanged;
event EventHandler<ShellProfile> ShellProfileAdded;
event EventHandler<Guid> ShellProfileDeleted;
event EventHandler<TerminalOptions> TerminalOptionsChanged;
event EventHandler<Guid> ThemeDeleted;
event EventHandler<TerminalTheme> ThemeAdded;
void DeleteShellProfile(Guid id); void DeleteShellProfile(Guid id);
void DeleteSshProfile(Guid id);
void DeleteTheme(Guid id); void DeleteTheme(Guid id);
ApplicationSettings GetApplicationSettings(); ApplicationSettings GetApplicationSettings();
TerminalTheme GetCurrentTheme(); TerminalTheme GetCurrentTheme();
@@ -16,28 +24,18 @@ namespace FluentTerminal.App.Services
Guid GetDefaultShellProfileId(); Guid GetDefaultShellProfileId();
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings(); IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
IEnumerable<ShellProfile> GetShellProfiles(); IEnumerable<ShellProfile> GetShellProfiles();
IEnumerable<SshProfile> GetSshProfiles();
/// <summary>
/// Returns union of <see cref="GetShellProfiles"/> and <see cref="GetSshProfiles"/>.
/// </summary>
IEnumerable<ShellProfile> GetAllProfiles();
IEnumerable<TabTheme> GetTabThemes(); IEnumerable<TabTheme> GetTabThemes();
TerminalOptions GetTerminalOptions(); TerminalOptions GetTerminalOptions();
TerminalTheme GetTheme(Guid id); TerminalTheme GetTheme(Guid id);
IEnumerable<TerminalTheme> GetThemes(); IEnumerable<TerminalTheme> GetThemes();
void ResetKeyBindings(); void ResetKeyBindings();
void SaveApplicationSettings(ApplicationSettings applicationSettings); void SaveApplicationSettings(ApplicationSettings applicationSettings);
void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id); void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id); void SaveDefaultShellProfileId(Guid id);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings); void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false); void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
void SaveTerminalOptions(TerminalOptions terminalOptions); void SaveTerminalOptions(TerminalOptions terminalOptions);
void SaveTheme(TerminalTheme theme, bool newTheme = false); void SaveTheme(TerminalTheme theme, bool newTheme = false);
ShellProfile GetShellProfile(Guid id); ShellProfile GetShellProfile(Guid id);
SshProfile GetSshProfile(Guid id);
string ExportSettings();
void ImportSettings(string serializedSettings);
} }
} }
@@ -1,9 +0,0 @@
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface IShellProfileMigrationService
{
void Migrate(ShellProfile profile);
}
}
@@ -5,8 +5,8 @@ namespace FluentTerminal.App.Services
{ {
public interface IStartupTaskService public interface IStartupTaskService
{ {
Task DisableStartupTaskAsync(); Task DisableStartupTask();
Task<StartupTaskStatus> EnableStartupTaskAsync(); Task<StartupTaskStatus> EnableStartupTask();
Task<StartupTaskStatus> GetStatusAsync(); Task<StartupTaskStatus> GetStatus();
} }
} }
@@ -10,7 +10,5 @@ namespace FluentTerminal.App.Services
IEnumerable<string> SupportedFileTypes { get; } IEnumerable<string> SupportedFileTypes { get; }
Task<TerminalTheme> Parse(string fileName, Stream fileContent); Task<TerminalTheme> Parse(string fileName, Stream fileContent);
Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent);
} }
} }
@@ -8,47 +8,22 @@ namespace FluentTerminal.App.Services
{ {
public interface ITrayProcessCommunicationService public interface ITrayProcessCommunicationService
{ {
event EventHandler<TerminalExitStatus> TerminalExited; event EventHandler<int> TerminalExited;
void Initialize(IAppServiceConnection appServiceConnection); void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile, Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
SessionType sessionType);
Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause); Task ResizeTerminal(int id, TerminalSize size);
Task ResizeTerminalAsync(byte id, TerminalSize size); Task UpdateToggleWindowKeyBindings();
Task UpdateToggleWindowKeyBindingsAsync(); Task Write(int terminalId, byte[] data);
Task WriteAsync(byte terminalId, byte[] data); void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback);
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback); Task CloseTerminal(int terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
void UnsubscribeFromTerminalOutput(byte terminalId); int GetNextTerminalId();
Task CloseTerminalAsync(byte terminalId);
byte GetNextTerminalId();
Task<string> GetUserNameAsync();
Task SaveTextFileAsync(string path, string content);
Task<string> ReadTextFileAsync(string path);
Task<string> GetSshConfigDirAsync();
Task<string[]> GetFilesFromSshConfigDirAsync();
Task<bool> CheckFileExistsAsync(string path);
Task MuteTerminalAsync(bool mute);
void UpdateSettings(ApplicationSettings settings);
Task<string> GetCommandPathAsync(string command);
Task QuitApplicationAsync();
} }
} }
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services
{ {
public interface IUpdateService public interface IUpdateService
{ {
Task CheckForUpdateAsync(bool notifyNoUpdate = false); Task CheckForUpdate(bool notifyNoUpdate = false);
Version GetCurrentVersion(); Version GetCurrentVersion();
Task<Version> GetLatestVersionAsync(); Task<Version> GetLatestVersionAsync();
} }
@@ -2,8 +2,6 @@
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Windows.Foundation.Metadata;
using Windows.Storage;
namespace FluentTerminal.App.Services.Implementation namespace FluentTerminal.App.Services.Implementation
{ {
@@ -11,14 +9,6 @@ namespace FluentTerminal.App.Services.Implementation
{ {
public ApplicationSettings GetDefaultApplicationSettings() public ApplicationSettings GetDefaultApplicationSettings()
{ {
var logDirectoryPath = string.Empty;
try
{
// Accessing ApplicationData.Current during run of unit tests causes
// "System.InvalidOperationException : The process has no package identity" exception.
logDirectoryPath = ApplicationData.Current.LocalCacheFolder.Path;
} catch (InvalidOperationException) { }
return new ApplicationSettings return new ApplicationSettings
{ {
ConfirmClosingTabs = false, ConfirmClosingTabs = false,
@@ -26,23 +16,15 @@ namespace FluentTerminal.App.Services.Implementation
UnderlineSelectedTab = false, UnderlineSelectedTab = false,
InactiveTabColorMode = InactiveTabColorMode.Background, InactiveTabColorMode = InactiveTabColorMode.Background,
NewTerminalLocation = NewTerminalLocation.Tab, NewTerminalLocation = NewTerminalLocation.Tab,
TabWindowCascadingAppMenu = true,
TabsPosition = TabsPosition.Top, TabsPosition = TabsPosition.Top,
CopyOnSelect = false, CopyOnSelect = false,
MouseMiddleClickAction = MouseAction.None, MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu, MouseRightClickAction = MouseAction.ContextMenu,
AlwaysShowTabs = false,
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false, ShowNewOutputIndicator = false,
EnableTrayIcon = true, EnableTrayIcon = true,
ShowCustomTitleInTitlebar = true, ShowCustomTitleInTitlebar = true
UseMoshByDefault = true,
AutoFallbackToWindowsUsernameInLinks = true,
RTrimCopiedLines = true,
MuteTerminalBeeps = true,
EnableLogging = false,
PrintableOutputOnly = true,
LogDirectoryPath = logDirectoryPath,
UseConPty = true,
ShowTextCopied = false
}; };
} }
@@ -56,6 +38,9 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding new KeyBinding
{ {
Command = nameof(Command.ToggleWindow), Command = nameof(Command.ToggleWindow),
Ctrl = false,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Scroll Key = (int)ExtendedVirtualKey.Scroll
} }
}; };
@@ -67,6 +52,8 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.NextTab), Command = nameof(Command.NextTab),
Ctrl = true, Ctrl = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Tab Key = (int)ExtendedVirtualKey.Tab
} }
}; };
@@ -78,6 +65,7 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.PreviousTab), Command = nameof(Command.PreviousTab),
Ctrl = true, Ctrl = true,
Alt = false,
Shift = true, Shift = true,
Key = (int)ExtendedVirtualKey.Tab Key = (int)ExtendedVirtualKey.Tab
} }
@@ -97,35 +85,13 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding new KeyBinding
{ {
Command = command.ToString(), Command = command.ToString(),
Alt = true, Ctrl = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Number1 + (command - Command.SwitchToTerm1) Key = (int)ExtendedVirtualKey.Number1 + (command - Command.SwitchToTerm1)
} }
}; };
case Command.DuplicateTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.DuplicateTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.D
}
};
case Command.ReconnectTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ReconnectTab),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.R
}
};
case Command.NewTab: case Command.NewTab:
return new List<KeyBinding> return new List<KeyBinding>
{ {
@@ -133,59 +99,38 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.NewTab), Command = nameof(Command.NewTab),
Ctrl = true, Ctrl = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.ConfigurableNewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ConfigurableNewTab),
Ctrl = true,
Alt = false,
Shift = true, Shift = true,
Key = (int)ExtendedVirtualKey.T Key = (int)ExtendedVirtualKey.T
} }
}; };
case Command.NewSshTab: case Command.NewRemoteTab:
return new List<KeyBinding> return new List<KeyBinding>
{ {
new KeyBinding new KeyBinding
{ {
Command = nameof(Command.NewSshTab), Command = nameof(Command.NewRemoteTab),
Ctrl = true, Ctrl = false,
Shift = true,
Key = (int)ExtendedVirtualKey.Y
}
};
case Command.NewSshWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewSshWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.Y
}
};
case Command.NewCustomCommandTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewCustomCommandTab),
Ctrl = true,
Alt = true, Alt = true,
Shift = false,
Key = (int)ExtendedVirtualKey.T Key = (int)ExtendedVirtualKey.T
} }
}; };
case Command.NewCustomCommandWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewCustomCommandWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ChangeTabTitle: case Command.ChangeTabTitle:
return new List<KeyBinding> return new List<KeyBinding>
{ {
@@ -193,6 +138,7 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.ChangeTabTitle), Command = nameof(Command.ChangeTabTitle),
Ctrl = true, Ctrl = true,
Alt = false,
Shift = true, Shift = true,
Key = (int)ExtendedVirtualKey.R Key = (int)ExtendedVirtualKey.R
} }
@@ -205,7 +151,8 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.CloseTab), Command = nameof(Command.CloseTab),
Ctrl = true, Ctrl = true,
Shift = true, Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.W Key = (int)ExtendedVirtualKey.W
} }
}; };
@@ -217,6 +164,20 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.NewWindow), Command = nameof(Command.NewWindow),
Ctrl = true, Ctrl = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ConfigurableNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ConfigurableNewWindow),
Ctrl = true,
Alt = false,
Shift = true, Shift = true,
Key = (int)ExtendedVirtualKey.N Key = (int)ExtendedVirtualKey.N
} }
@@ -229,7 +190,8 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.ShowSettings), Command = nameof(Command.ShowSettings),
Ctrl = true, Ctrl = true,
Shift = true, Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Comma Key = (int)ExtendedVirtualKey.Comma
} }
}; };
@@ -241,7 +203,8 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.Copy), Command = nameof(Command.Copy),
Ctrl = true, Ctrl = true,
Shift = true, Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.C Key = (int)ExtendedVirtualKey.C
} }
}; };
@@ -253,7 +216,8 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.Paste), Command = nameof(Command.Paste),
Ctrl = true, Ctrl = true,
Shift = true, Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.V Key = (int)ExtendedVirtualKey.V
} }
}; };
@@ -266,7 +230,8 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.PasteWithoutNewlines), Command = nameof(Command.PasteWithoutNewlines),
Ctrl = true, Ctrl = true,
Alt = true, Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.V Key = (int)ExtendedVirtualKey.V
} }
}; };
@@ -278,34 +243,22 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.Search), Command = nameof(Command.Search),
Ctrl = true, Ctrl = true,
Shift = true, Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.F Key = (int)ExtendedVirtualKey.F
} }
}; };
case Command.CloseSearch:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.CloseSearch),
Key = (int)ExtendedVirtualKey.Escape
}
};
case Command.ToggleFullScreen: case Command.ToggleFullScreen:
return new List<KeyBinding> return new List<KeyBinding>
{ {
new KeyBinding new KeyBinding
{ {
Command = nameof(Command.ToggleFullScreen), Command = nameof(Command.ToggleFullScreen),
Ctrl = false,
Alt = true, Alt = true,
Shift = false,
Key = (int)ExtendedVirtualKey.Enter Key = (int)ExtendedVirtualKey.Enter
},
new KeyBinding
{
Command = nameof(Command.ToggleFullScreen),
Key = (int)ExtendedVirtualKey.F11
} }
}; };
@@ -316,7 +269,8 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.SelectAll), Command = nameof(Command.SelectAll),
Ctrl = true, Ctrl = true,
Shift = true, Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.A Key = (int)ExtendedVirtualKey.A
} }
}; };
@@ -328,43 +282,14 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Command = nameof(Command.Clear), Command = nameof(Command.Clear),
Ctrl = true, Ctrl = true,
Alt = true, Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.L Key = (int)ExtendedVirtualKey.L
} }
}; };
case Command.IncreaseFontSize:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.IncreaseFontSize),
Ctrl = true,
Key = (int)ExtendedVirtualKey.Plus
}
};
case Command.DecreaseFontSize:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.DecreaseFontSize),
Ctrl = true,
Key = (int)ExtendedVirtualKey.Minus
}
};
case Command.ResetFontSize:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ResetFontSize),
Ctrl = true,
Key = (int)ExtendedVirtualKey.Number0
}
};
default:
throw new InvalidOperationException($"Default keybindings for Command '{command}' are missing");
} }
return null;
} }
public Guid GetDefaultShellProfileId() public Guid GetDefaultShellProfileId()
@@ -379,7 +304,7 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme new TabTheme
{ {
Id = 0, Id = 0,
Name = "TabTheme.None", Name = "None",
BackgroundOpacity = double.NaN, BackgroundOpacity = double.NaN,
BackgroundPointerOverOpacity = double.NaN, BackgroundPointerOverOpacity = double.NaN,
BackgroundPressedOpacity = double.NaN, BackgroundPressedOpacity = double.NaN,
@@ -390,42 +315,41 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme new TabTheme
{ {
Id = 1, Id = 1,
Name = "TabTheme.Red", Name = "Red",
Color = "#E81123" Color = "#E81123"
}, },
new TabTheme new TabTheme
{ {
Id = 2, Id = 2,
Name = "TabTheme.Green", Name = "Green",
Color = "#10893E" Color = "#10893E"
}, },
new TabTheme new TabTheme
{ {
Id = 3, Id = 3,
Name = "TabTheme.Blue", Name = "Blue",
Color = "#0078D7" Color = "#0078D7"
}, },
new TabTheme new TabTheme
{ {
Id = 4, Id = 4,
Name = "TabTheme.Purple", Name = "Purple",
Color = "#881798" Color = "#881798"
}, },
new TabTheme new TabTheme
{ {
Id = 5, Id = 5,
Name = "TabTheme.Orange", Name = "Orange",
Color = "#FF8C00" Color = "#FF8C00"
}, },
new TabTheme new TabTheme
{ {
Id = 6, Id = 6,
Name = "TabTheme.Teal", Name = "Teal",
Color = "#00B7C3" Color = "#00B7C3"
} }
}; };
} }
public TerminalOptions GetDefaultTerminalOptions() public TerminalOptions GetDefaultTerminalOptions()
{ {
return new TerminalOptions return new TerminalOptions
@@ -436,12 +360,10 @@ namespace FluentTerminal.App.Services.Implementation
ScrollBarStyle = ScrollBarStyle.Hidden, ScrollBarStyle = ScrollBarStyle.Hidden,
FontFamily = "Consolas", FontFamily = "Consolas",
FontSize = 13, FontSize = 13,
FontWeight = 400, BoldText = false,
BackgroundOpacity = 0.8, BackgroundOpacity = 0.8,
Padding = 12, Padding = 12,
ScrollBackLimit = 1000, ScrollBackLimit = 1000
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;",
UseAcrylicBackground = true
}; };
} }
@@ -458,17 +380,11 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Id = GetDefaultShellProfileId(), Id = GetDefaultShellProfileId(),
Name = "Powershell", Name = "Powershell",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty, Arguments = string.Empty,
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe", Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
PreInstalled = true, PreInstalled = true,
WorkingDirectory = string.Empty, WorkingDirectory = string.Empty,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+ LineEndingTranslation = LineEndingStyle.DoNotModify,
UseBuffer = false,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new [] KeyBindings = new []
{ {
new KeyBinding new KeyBinding
@@ -486,17 +402,11 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Id = Guid.Parse("ab942a61-7673-4755-9bd8-765aff91d9a3"), Id = Guid.Parse("ab942a61-7673-4755-9bd8-765aff91d9a3"),
Name = "CMD", Name = "CMD",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty, Arguments = string.Empty,
Location = @"C:\Windows\System32\cmd.exe", Location = @"C:\Windows\System32\cmd.exe",
PreInstalled = true, PreInstalled = true,
WorkingDirectory = string.Empty, WorkingDirectory = string.Empty,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+ LineEndingTranslation = LineEndingStyle.DoNotModify,
UseBuffer = true,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new [] KeyBindings = new []
{ {
new KeyBinding new KeyBinding
@@ -514,17 +424,11 @@ namespace FluentTerminal.App.Services.Implementation
{ {
Id= Guid.Parse("e5785ad6-584f-40cb-bdcd-d5b3b3953e7f"), Id= Guid.Parse("e5785ad6-584f-40cb-bdcd-d5b3b3953e7f"),
Name = "WSL", Name = "WSL",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty, Arguments = string.Empty,
Location = @"C:\windows\system32\wsl.exe", Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true, PreInstalled = true,
WorkingDirectory = string.Empty, WorkingDirectory = string.Empty,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+ LineEndingTranslation = LineEndingStyle.DoNotModify,
UseBuffer = true, //TODO: Set to false if the buffer causes issues with WSL.
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new [] KeyBindings = new []
{ {
new KeyBinding new KeyBinding
@@ -7,24 +7,21 @@ namespace FluentTerminal.App.Services.Implementation
{ {
public class DialogService : IDialogService public class DialogService : IDialogService
{ {
private readonly Func<IShellProfileSelectionDialog> _shellProfileSelectionDialogFactory;
private readonly Func<IMessageDialog> _messageDialogFactory; private readonly Func<IMessageDialog> _messageDialogFactory;
private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory; private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory;
private readonly Func<IInputDialog> _inputDialogFactory; private readonly Func<IInputDialog> _inputDialogFactory;
private readonly Func<ISshConnectionInfoDialog> _sshConnectionInfoDialogFactory; private readonly Func<ISshConnectionInfoDialog> _sshConnectionInfoDialogFactory;
private readonly Func<ICustomCommandDialog> _customCommandDialogFactory;
private readonly Func<IAboutDialog> _aboutDialogFactory;
public DialogService(Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory, public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory, Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
Func<ICustomCommandDialog> customCommandDialogFactory, Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory)
Func<IAboutDialog> aboutDialogFactory)
{ {
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
_messageDialogFactory = messageDialogFactory; _messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory; _createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory; _inputDialogFactory = inputDialogFactory;
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory; _sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
_customCommandDialogFactory = customCommandDialogFactory;
_aboutDialogFactory = aboutDialogFactory;
} }
public Task<KeyBinding> ShowCreateKeyBindingDialog() public Task<KeyBinding> ShowCreateKeyBindingDialog()
@@ -34,7 +31,7 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.CreateKeyBinding(); return dialog.CreateKeyBinding();
} }
public Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons) public Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons)
{ {
if (string.IsNullOrWhiteSpace(title)) if (string.IsNullOrWhiteSpace(title))
{ {
@@ -71,17 +68,14 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.GetInput(); return dialog.GetInput();
} }
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) => public Task<ShellProfile> ShowProfileSelectionDialogAsync()
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
_customCommandDialogFactory().GetCustomCommandAsync(input);
public Task ShowAboutDialogAsync()
{ {
var dialog = _aboutDialogFactory(); var dialog = _shellProfileSelectionDialogFactory();
return dialog.ShowAsync(); return dialog.SelectProfile();
} }
public Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync() =>
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync();
} }
} }
@@ -9,20 +9,9 @@ namespace FluentTerminal.App.Services.Implementation
{ {
public class FluentTerminalThemeParser : IThemeParser public class FluentTerminalThemeParser : IThemeParser
{ {
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".flutecolors" }; public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".flutecolors" };
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent) public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
{
return DeserializeThemeAsync<ExportedTerminalTheme>(fileName, fileContent);
}
public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
{
return DeserializeThemeAsync<TerminalTheme>(fileName, fileContent);
}
private async Task<T> DeserializeThemeAsync<T>(string fileName, Stream fileContent)
where T : TerminalTheme
{ {
if (string.IsNullOrWhiteSpace(fileName)) if (string.IsNullOrWhiteSpace(fileName))
{ {
@@ -36,8 +25,8 @@ namespace FluentTerminal.App.Services.Implementation
using (var streamReader = new StreamReader(fileContent)) using (var streamReader = new StreamReader(fileContent))
{ {
var content = await streamReader.ReadToEndAsync().ConfigureAwait(false); var content = await streamReader.ReadToEndAsync();
var theme = JsonConvert.DeserializeObject<T>(content); var theme = JsonConvert.DeserializeObject<TerminalTheme>(content);
theme.PreInstalled = false; theme.PreInstalled = false;
theme.Id = Guid.NewGuid(); theme.Id = Guid.NewGuid();
@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
// ReSharper disable InconsistentNaming
namespace FluentTerminal.App.Services.Implementation namespace FluentTerminal.App.Services.Implementation
{ {
@@ -15,7 +14,7 @@ namespace FluentTerminal.App.Services.Implementation
{ {
private const byte Opacity30Percent = 77; private const byte Opacity30Percent = 77;
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".itermcolors" }; public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".itermcolors" };
private static class ITermThemeKeys private static class ITermThemeKeys
{ {
@@ -37,7 +36,7 @@ namespace FluentTerminal.App.Services.Implementation
public const string Ansi15Color = "Ansi 15 Color"; public const string Ansi15Color = "Ansi 15 Color";
public const string BackgroundColor = "Background Color"; public const string BackgroundColor = "Background Color";
//public const string BoldColor = "Bold Color"; public const string BoldColor = "Bold Color";
public const string CursorColor = "Cursor Color"; public const string CursorColor = "Cursor Color";
public const string CursorTextColor = "Cursor Text Color"; public const string CursorTextColor = "Cursor Text Color";
public const string ForegroundColor = "Foreground Color"; public const string ForegroundColor = "Foreground Color";
@@ -52,7 +51,7 @@ namespace FluentTerminal.App.Services.Implementation
public const string RedComponent = "Red Component"; public const string RedComponent = "Red Component";
} }
public Task<TerminalTheme> Parse(string fileName, Stream fileContent) public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
{ {
if (string.IsNullOrWhiteSpace(fileName)) if (string.IsNullOrWhiteSpace(fileName))
{ {
@@ -66,13 +65,13 @@ namespace FluentTerminal.App.Services.Implementation
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary."); var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
return Task.FromResult(new TerminalTheme return new TerminalTheme
{ {
Name = Path.GetFileNameWithoutExtension(fileName), Name = Path.GetFileNameWithoutExtension(fileName),
Colors = GetColors(node), Colors = GetColors(node),
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
PreInstalled = false PreInstalled = false
}); };
} }
private TerminalColors GetColors(DictionaryNode themeDictionary) private TerminalColors GetColors(DictionaryNode themeDictionary)
@@ -84,8 +83,6 @@ namespace FluentTerminal.App.Services.Implementation
Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]), Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]),
CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]), CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]),
Selection = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor], Opacity30Percent), Selection = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor], Opacity30Percent),
SelectionForeground = GetColorString(themeDictionary[ITermThemeKeys.SelectedTextColor]),
SelectionBackground = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor]),
Black = GetColorString(themeDictionary[ITermThemeKeys.Ansi0Color]), Black = GetColorString(themeDictionary[ITermThemeKeys.Ansi0Color]),
Red = GetColorString(themeDictionary[ITermThemeKeys.Ansi1Color]), Red = GetColorString(themeDictionary[ITermThemeKeys.Ansi1Color]),
Green = GetColorString(themeDictionary[ITermThemeKeys.Ansi2Color]), Green = GetColorString(themeDictionary[ITermThemeKeys.Ansi2Color]),
@@ -113,6 +110,8 @@ namespace FluentTerminal.App.Services.Implementation
var green = dictionaryNode[ITermThemeColorKeys.GreenComponent] as RealNode ?? throw new ParseThemeException("Green node value was not a real number"); var green = dictionaryNode[ITermThemeColorKeys.GreenComponent] as RealNode ?? throw new ParseThemeException("Green node value was not a real number");
var blue = dictionaryNode[ITermThemeColorKeys.BlueComponent] as RealNode ?? throw new ParseThemeException("Blue node value was not a real number"); var blue = dictionaryNode[ITermThemeColorKeys.BlueComponent] as RealNode ?? throw new ParseThemeException("Blue node value was not a real number");
var color = $"#{alpha:X2}{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
if (alpha == byte.MaxValue) if (alpha == byte.MaxValue)
{ {
return $"#{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}"; return $"#{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
@@ -134,30 +133,5 @@ namespace FluentTerminal.App.Services.Implementation
{ {
return (alpha / 256.0).ToString(CultureInfo.InvariantCulture); return (alpha / 256.0).ToString(CultureInfo.InvariantCulture);
} }
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent)
{
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentNullException(nameof(fileName));
}
if (fileContent == null)
{
throw new ArgumentNullException(nameof(fileContent));
}
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
var terminalTheme = new TerminalTheme
{
Name = Path.GetFileNameWithoutExtension(fileName),
Colors = GetColors(node),
Id = Guid.NewGuid(),
PreInstalled = false
};
return Task.FromResult(new ExportedTerminalTheme(terminalTheme, string.Empty));
}
} }
} }
@@ -42,10 +42,5 @@ namespace FluentTerminal.App.Services.Implementation
throw new KeyNotFoundException($"No handler registered for command: {command}"); throw new KeyNotFoundException($"No handler registered for command: {command}");
} }
public void ClearCommandHandlers()
{
_commandHandlers.Clear();
}
} }
} }
@@ -1,94 +0,0 @@
using System;
using System.Text.RegularExpressions;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Implementation
{
// The purpose of this class is to ensure backward compatibility of mosh profiles and command history
// TODO: This class should be probably deleted in few months or so
public static class MoshBackwardCompatibility
{
private static readonly string MoshCommandExe = $"{Constants.MoshCommandName}.exe";
private static readonly Regex OldFormatRx = new Regex(@".+\s(?<ports>\d{1,5}:\d{1,5})$", RegexOptions.Compiled);
private static readonly Regex TargetRx =
new Regex(@"(^|\s+)(?<user>[^\s@;]+)@(?<host>[^\s@:]+)(:(?<port>\d{1,5}))?\s+", RegexOptions.Compiled);
private static bool IsMoshProfile(ShellProfile profile) =>
Constants.MoshCommandName.Equals(profile.Location, StringComparison.OrdinalIgnoreCase) ||
MoshCommandExe.Equals(profile.Location, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Checks if the profile contains obsolete mosh syntax, and changes it if it does.
/// </summary>
/// <param name="profile">The profile. Note that the method doesn't change the input profile at all, but creates and
/// returns its clone if there's something to change.</param>
/// <remarks>
/// The method actually checks if <see cref="ShellProfile.Arguments"/> of the input <paramref name="profile"/> is in
/// obsolete <c>mosh</c> format <c>[ssh_arguments] [target] [mosh_port_range]</c>, and if it is changes it to the new
/// format <c>--ssh="[ssh_arguments]" -p [mosh_ports] [target]</c>.
/// </remarks>
/// <returns>
/// If the method detected that the input profile is obsolete mosh profile, it will create its clone with the updated
/// command line arguments, and return it. Otherwise it will simply return unchanged input profile (the same reference
/// as the input profile). It allows us to simply check if anything is changed by comparing the input and output
/// profiles for reference equality.
/// </returns>
public static ShellProfile FixProfile(ShellProfile profile)
{
// If there aren't any arguments, or if the profile isn't a mosh profile, there's nothing to do. Just return.
if (string.IsNullOrWhiteSpace(profile.Arguments) || !IsMoshProfile(profile)) return profile;
var arguments = profile.Arguments.Trim();
var moshPortsMatch = OldFormatRx.Match(arguments);
if (!moshPortsMatch.Success) return profile;// Arguments don't end with mosh ports (i.e. 60000:61000), so it isn't an obsolete format.
var targetMatches = TargetRx.Matches(arguments);
if (targetMatches.Count != 1) return profile;// Target part (user@host:port) not found or found multiple times. Cannot reliably determine the target, so don't change.
var targetMatch = targetMatches[0];
// Removing [mosh_ports] from the original arguments
arguments = arguments.Substring(0, arguments.Length - moshPortsMatch.Groups["ports"].Length).Trim();
var newProfile = profile.Clone();
// Removing [target] from the original arguments, thus getting "pure" ssh arguments
var sshArguments = targetMatch.Index > 0 ? arguments.Substring(0, targetMatch.Index).Trim() : string.Empty;
if (targetMatch.Index + targetMatch.Length < arguments.Length)
{
sshArguments += $" {arguments.Substring(targetMatch.Index + targetMatch.Length).Trim()}";
}
// If the ssh port is defined in the original target (i.e. user@host:port), which was allowed in the obsolete mosh, extracting the port part into a separate ssh argument
if (targetMatch.Groups["port"].Success && uint.TryParse(targetMatch.Groups["port"].Value, out var port) &&
port != SshProfile.DefaultSshPort)
{
sshArguments += $" -p {targetMatch.Groups["port"].Value}";
}
// Replacing any double quotes in ssh arguments with single quotes, since ssh arguments will be double-quoted as a whole
sshArguments = sshArguments.Replace("\"", "'").Trim();
arguments = $"-p {moshPortsMatch.Groups["ports"].Value}";
if (!string.IsNullOrEmpty(sshArguments))
{
// If there were some ssh arguments, add them as defined in the new format
arguments += $" --ssh=\"ssh {sshArguments}\"";
}
// Finally append the target to the arguments
arguments += $" {targetMatch.Groups["user"]}@{targetMatch.Groups["host"]}";
newProfile.Arguments = arguments;
return newProfile;
}
}
}
@@ -5,9 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models.Messages;
using Microsoft.Toolkit.Mvvm.Messaging;
namespace FluentTerminal.App.Services.Implementation namespace FluentTerminal.App.Services.Implementation
{ {
@@ -15,14 +12,12 @@ namespace FluentTerminal.App.Services.Implementation
{ {
public const string CurrentThemeKey = "CurrentTheme"; public const string CurrentThemeKey = "CurrentTheme";
public const string DefaultShellProfileKey = "DefaultShellProfile"; public const string DefaultShellProfileKey = "DefaultShellProfile";
public const string DefaultSshProfileKey = "DefaultSshProfile";
private readonly IDefaultValueProvider _defaultValueProvider; private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IApplicationDataContainer _keyBindings; private readonly IApplicationDataContainer _keyBindings;
private readonly IApplicationDataContainer _localSettings; private readonly IApplicationDataContainer _localSettings;
private readonly IApplicationDataContainer _roamingSettings; private readonly IApplicationDataContainer _roamingSettings;
private readonly IApplicationDataContainer _shellProfiles; private readonly IApplicationDataContainer _shellProfiles;
private readonly IApplicationDataContainer _sshProfiles;
private readonly IApplicationDataContainer _themes; private readonly IApplicationDataContainer _themes;
public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers) public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers)
@@ -34,8 +29,6 @@ namespace FluentTerminal.App.Services.Implementation
_themes = containers.Themes; _themes = containers.Themes;
_keyBindings = containers.KeyBindings; _keyBindings = containers.KeyBindings;
_shellProfiles = containers.ShellProfiles; _shellProfiles = containers.ShellProfiles;
_sshProfiles = containers.SshProfiles;
foreach (var theme in _defaultValueProvider.GetPreInstalledThemes()) foreach (var theme in _defaultValueProvider.GetPreInstalledThemes())
{ {
@@ -54,132 +47,25 @@ namespace FluentTerminal.App.Services.Implementation
} }
} }
public string ExportSettings() public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = GetCommandKeyBindings(),
TerminalOptions = GetTerminalOptions(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>(),
DefaultSettings = GetDefaultSettings(),
};
foreach (var theme in GetThemes().Where(x => !x.PreInstalled)) public event EventHandler<TerminalTheme> ThemeAdded;
{ public event EventHandler<Guid> CurrentThemeChanged;
config.Themes.Add(theme); public event EventHandler<Guid> ThemeDeleted;
}
foreach (var profile in GetShellProfiles()) public event EventHandler KeyBindingsChanged;
{
config.Profiles.Add(profile);
}
foreach (var profile in GetSshProfiles()) public event EventHandler<ShellProfile> ShellProfileAdded;
{ public event EventHandler<Guid> ShellProfileDeleted;
config.SshProfiles.Add(profile);
}
return JsonConvert.SerializeObject(config, PreserveDictionaryKeyCaseContractResolver.SerializerSettings); public event EventHandler<TerminalOptions> TerminalOptionsChanged;
}
private Dictionary<string, string> GetDefaultSettings()
{
return new Dictionary<string, string>
{
[DefaultShellProfileKey] = GetDefaultShellProfileId().ToString(),
[CurrentThemeKey] = GetCurrentThemeId().ToString(),
};
}
public void ImportSettings(string serializedSettings)
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = new Dictionary<string, ICollection<KeyBinding>>(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>(),
TerminalOptions = GetTerminalOptions(),
DefaultSettings = new Dictionary<string, string>(),
};
JsonConvert.PopulateObject(serializedSettings, config);
SaveApplicationSettings(config.App);
// Since we set each command sepaartely, we don't need all existing settings
foreach (var pair in config.KeyBindings)
{
SaveKeyBindings(pair.Key, pair.Value);
}
// Can't create/modify pre-installed themes
foreach (var theme in config.Themes.Where(x => !x.PreInstalled))
{
var existingTheme = GetTheme(theme.Id);
if (existingTheme?.PreInstalled == true)
{
continue;
}
SaveTheme(theme, existingTheme != null);
}
foreach (var profile in config.Profiles)
{
var existingProfile = GetShellProfile(profile.Id);
var isNew = existingProfile == default;
if (!isNew && existingProfile.PreInstalled)
{
// those cannot be edited
profile.Name = existingProfile.Name;
profile.Location = existingProfile.Location;
SaveShellProfile(profile, false);
continue;
}
SaveShellProfile(profile, isNew);
}
foreach (var profile in config.SshProfiles)
{
var existingProfile = GetSshProfile(profile.Id);
var isNew = existingProfile == default;
SaveSshProfile(profile, isNew);
}
SaveTerminalOptions(config.TerminalOptions);
if (config.DefaultSettings.ContainsKey(DefaultShellProfileKey))
{
SaveDefaultShellProfileId(new Guid(config.DefaultSettings[DefaultShellProfileKey]));
}
if (config.DefaultSettings.ContainsKey(CurrentThemeKey))
{
SaveCurrentThemeId(new Guid(config.DefaultSettings[CurrentThemeKey]));
}
}
public void DeleteShellProfile(Guid id) public void DeleteShellProfile(Guid id)
{ {
_shellProfiles.Delete(id.ToString()); _shellProfiles.Delete(id.ToString());
WeakReferenceMessenger.Default.Send(new ShellProfileDeletedMessage(id)); ShellProfileDeleted?.Invoke(this, id);
} }
public void DeleteSshProfile(Guid id)
{
_sshProfiles.Delete(id.ToString());
WeakReferenceMessenger.Default.Send(new ShellProfileDeletedMessage(id));
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage());
}
public void DeleteTheme(Guid id) public void DeleteTheme(Guid id)
{ {
_themes.Delete(id.ToString()); _themes.Delete(id.ToString());
@@ -193,7 +79,7 @@ namespace FluentTerminal.App.Services.Implementation
} }
} }
WeakReferenceMessenger.Default.Send(new ThemeDeletedMessage(id)); ThemeDeleted?.Invoke(this, id);
} }
public ApplicationSettings GetApplicationSettings() public ApplicationSettings GetApplicationSettings()
@@ -240,10 +126,6 @@ namespace FluentTerminal.App.Services.Implementation
{ {
return _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile)); return _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
} }
public SshProfile GetSshProfile(Guid id)
{
return _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
}
public Guid GetDefaultShellProfileId() public Guid GetDefaultShellProfileId()
{ {
@@ -267,43 +149,7 @@ namespace FluentTerminal.App.Services.Implementation
public IEnumerable<ShellProfile> GetShellProfiles() public IEnumerable<ShellProfile> GetShellProfiles()
{ {
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string) x)) return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string)x)).ToList();
.Select(MoshBackwardCompatibilityFixProfile);
}
public IEnumerable<SshProfile> GetSshProfiles()
{
return _sshProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x))
.Select(MoshBackwardCompatibilityFixProfile).Cast<SshProfile>();
}
public IEnumerable<ShellProfile> GetAllProfiles()
{
return GetShellProfiles().Union(GetSshProfiles());
}
private ShellProfile MoshBackwardCompatibilityFixProfile(ShellProfile profile)
{
var fixedProfile = MoshBackwardCompatibility.FixProfile(profile);
if (ReferenceEquals(fixedProfile, profile))
{
// Nothing changed
return fixedProfile;
}
if (fixedProfile is SshProfile sshProfile)
{
DeleteSshProfile(profile.Id);
SaveSshProfile(sshProfile);
}
else
{
DeleteShellProfile(profile.Id);
SaveShellProfile(fixedProfile);
}
return fixedProfile;
} }
public IEnumerable<TabTheme> GetTabThemes() public IEnumerable<TabTheme> GetTabThemes()
@@ -333,43 +179,36 @@ namespace FluentTerminal.App.Services.Implementation
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command)); _keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
} }
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage()); KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
} }
public void SaveApplicationSettings(ApplicationSettings applicationSettings) public void SaveApplicationSettings(ApplicationSettings applicationSettings)
{ {
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings); _roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
WeakReferenceMessenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone())); ApplicationSettingsChanged?.Invoke(this, applicationSettings);
}
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
{
WeakReferenceMessenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
} }
public void SaveCurrentThemeId(Guid id) public void SaveCurrentThemeId(Guid id)
{ {
_roamingSettings.SetValue(CurrentThemeKey, id); _roamingSettings.SetValue(CurrentThemeKey, id);
WeakReferenceMessenger.Default.Send(new CurrentThemeChangedMessage(id)); CurrentThemeChanged?.Invoke(this, id);
} }
public void SaveDefaultShellProfileId(Guid id) public void SaveDefaultShellProfileId(Guid id)
{ {
_localSettings.SetValue(DefaultShellProfileKey, id); _localSettings.SetValue(DefaultShellProfileKey, id);
WeakReferenceMessenger.Default.Send(new DefaultShellProfileChangedMessage(id));
} }
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings) public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
{ {
if (!Enum.TryParse<Command>(command, true, out var enumValue)) if (!Enum.IsDefined(typeof(Command), command))
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings); _keyBindings.WriteValueAsJson(command.ToString(), keyBindings);
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage()); KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
} }
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false) public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
@@ -377,39 +216,18 @@ namespace FluentTerminal.App.Services.Implementation
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile); _shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
// When saving the shell profile, we also need to update keybindings for everywhere. // When saving the shell profile, we also need to update keybindings for everywhere.
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage()); KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
if (newShell) if (newShell)
{ {
WeakReferenceMessenger.Default.Send(new ShellProfileAddedMessage(shellProfile)); ShellProfileAdded?.Invoke(this, shellProfile);
}
else
{
WeakReferenceMessenger.Default.Send(new ShellProfileChangedMessage(shellProfile));
}
}
public void SaveSshProfile(SshProfile sshProfile, bool newShell = false)
{
_sshProfiles.WriteValueAsJson(sshProfile.Id.ToString(), sshProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
WeakReferenceMessenger.Default.Send(new ShellProfileAddedMessage(sshProfile));
}
else
{
WeakReferenceMessenger.Default.Send(new ShellProfileChangedMessage(sshProfile));
} }
} }
public void SaveTerminalOptions(TerminalOptions terminalOptions) public void SaveTerminalOptions(TerminalOptions terminalOptions)
{ {
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions); _roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
WeakReferenceMessenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions)); TerminalOptionsChanged?.Invoke(this, terminalOptions);
} }
public void SaveTheme(TerminalTheme theme, bool newTheme = false) public void SaveTheme(TerminalTheme theme, bool newTheme = false)
@@ -418,12 +236,12 @@ namespace FluentTerminal.App.Services.Implementation
if (theme.Id == GetCurrentThemeId()) if (theme.Id == GetCurrentThemeId())
{ {
WeakReferenceMessenger.Default.Send(new CurrentThemeChangedMessage(theme.Id)); CurrentThemeChanged?.Invoke(this, theme.Id);
} }
if (newTheme) if (newTheme)
{ {
WeakReferenceMessenger.Default.Send(new ThemeAddedMessage(theme)); ThemeAdded?.Invoke(this, theme);
} }
} }
} }
@@ -1,32 +0,0 @@
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services.Implementation
{
public class ShellProfileMigrationService : IShellProfileMigrationService
{
private readonly Dictionary<int, Action<ShellProfile>> _migrationSteps = new Dictionary<int, Action<ShellProfile>>
{
[1] = new Action<ShellProfile>(profile =>
{
profile.EnvironmentVariables["TERM"] = "xterm-256color";
})
};
public void Migrate(ShellProfile profile)
{
while(profile.MigrationVersion < ShellProfile.CurrentMigrationVersion)
{
ApplyMigrationStep(profile, profile.MigrationVersion + 1);
}
}
private void ApplyMigrationStep(ShellProfile profile, int targetVersion)
{
_migrationSteps[targetVersion](profile);
profile.MigrationVersion = targetVersion;
}
}
}
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services.Implementation
{ {
public class ThemeParserFactory : IThemeParserFactory public class ThemeParserFactory : IThemeParserFactory
{ {
private readonly IEnumerable<IThemeParser> _parsers; private IEnumerable<IThemeParser> _parsers;
public IEnumerable<string> SupportedFileTypes { get; } public IEnumerable<string> SupportedFileTypes { get; }
@@ -1,5 +1,4 @@
using FluentTerminal.App.Services.Exceptions; using FluentTerminal.Models;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Requests; using FluentTerminal.Models.Requests;
using FluentTerminal.Models.Responses; using FluentTerminal.Models.Responses;
@@ -7,150 +6,42 @@ using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Foundation.Collections;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App.Services.Implementation namespace FluentTerminal.App.Services.Implementation
{ {
public class TrayProcessCommunicationService : ITrayProcessCommunicationService public class TrayProcessCommunicationService : ITrayProcessCommunicationService
{ {
#region Static
private static string _userName;
private static string _sshConfigDir;
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
private static ValueSet CreateMessage(IMessage content)
{
return new ValueSet
{
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(content,
PreserveDictionaryKeyCaseContractResolver.SerializerSettings)
};
}
#endregion Static
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection; private IAppServiceConnection _appServiceConnection;
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers; private readonly Dictionary<int, Action<byte[]>> _terminalOutputHandlers;
// ReSharper disable once RedundantDefaultMemberInitializer private int _nextTerminalId = 0;
private byte _nextTerminalId = 0;
public event EventHandler<TerminalExitStatus> TerminalExited; public event EventHandler<int> TerminalExited;
public TrayProcessCommunicationService(ISettingsService settingsService) public TrayProcessCommunicationService(ISettingsService settingsService)
{ {
_settingsService = settingsService; _settingsService = settingsService;
_terminalOutputHandlers = new Dictionary<byte, Action<byte[]>>(); _terminalOutputHandlers = new Dictionary<int, Action<byte[]>>();
} }
public byte GetNextTerminalId() public int GetNextTerminalId()
{ {
return _nextTerminalId++; return _nextTerminalId++;
} }
public async Task<string> GetUserNameAsync() public async Task<GetAvailablePortResponse> GetAvailablePort()
{ {
if (!string.IsNullOrEmpty(_userName)) var request = new GetAvailablePortRequest();
{
// Returning the username from cache
return _userName;
}
var response = await GetResponseAsync<StringValueResponse>(new GetUserNameRequest()).ConfigureAwait(false); var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
if (response.Success) Logger.Instance.Debug("Received GetAvailablePortResponse: {@response}", response);
{
_userName = response.Value;
}
return _userName; return response;
} }
public async Task SaveTextFileAsync(string path, string content) public async Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
{
var response =
await GetResponseAsync<CommonResponse>(new SaveTextFileRequest {Path = path, Content = content})
.ConfigureAwait(false);
if (!response.Success)
{
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error)
? "Failed to save the file."
: response.Error);
}
}
public async Task<string> ReadTextFileAsync(string path)
{
var response = await GetResponseAsync<StringValueResponse>(new ReadTextFileRequest { Path = path }).ConfigureAwait(false);
if (!response.Success)
{
throw new ReadTextFileException(string.IsNullOrWhiteSpace(response.Error) ? "Failed to read the file." : response.Error);
}
return response.Value;
}
public async Task<string> GetSshConfigDirAsync()
{
if (!string.IsNullOrEmpty(_sshConfigDir))
{
return _sshConfigDir;
}
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{ IncludeContent = false }).ConfigureAwait(false);
if (response.Success)
{
_sshConfigDir = response.Path;
}
return _sshConfigDir;
}
public async Task<string[]> GetFilesFromSshConfigDirAsync()
{
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{IncludeContent = true}).ConfigureAwait(false);
if (response.Success)
{
_sshConfigDir = response.Path;
return response.Files;
}
return null;
}
public async Task<bool> CheckFileExistsAsync(string path)
{
var response = await GetResponseAsync<CommonResponse>(new CheckFileExistsRequest {Path = path})
.ConfigureAwait(false);
return response.Success;
}
public Task MuteTerminalAsync(bool mute)
{
return _appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest {Mute = mute}));
}
public void UpdateSettings(ApplicationSettings settings)
{
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
}
public async Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType)
{ {
var request = new CreateTerminalRequest var request = new CreateTerminalRequest
{ {
@@ -162,151 +53,106 @@ namespace FluentTerminal.App.Services.Implementation
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request); Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
var response = await GetResponseAsync<CreateTerminalResponse>(request).ConfigureAwait(false); var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>(responseMessage[MessageKeys.Content]);
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response); Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
return response; return response;
} }
public Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause)
{
return GetResponseAsync<PauseTerminalOutputResponse>(
new PauseTerminalOutputRequest {Id = id, Pause = pause});
}
public void Initialize(IAppServiceConnection appServiceConnection) public void Initialize(IAppServiceConnection appServiceConnection)
{ {
_appServiceConnection = appServiceConnection; _appServiceConnection = appServiceConnection;
_appServiceConnection.MessageReceived += OnMessageReceived; _appServiceConnection.MessageReceived += OnMessageReceived;
} }
private void OnMessageReceived(object sender, IDictionary<string, object> e) private void OnMessageReceived(object sender, IDictionary<string, string> e)
{ {
var messageType = (byte)e[MessageKeys.Type]; var messageType = e[MessageKeys.Type];
var messageContent = e[MessageKeys.Content]; var messageContent = e[MessageKeys.Content];
switch (messageType) if (messageType == nameof(DisplayTerminalOutputRequest))
{ {
case Constants.TerminalBufferRequestIdentifier: var request = JsonConvert.DeserializeObject<DisplayTerminalOutputRequest>(messageContent);
var terminalId = (byte)e[MessageKeys.TerminalId];
if (_terminalOutputHandlers.ContainsKey(terminalId)) if (_terminalOutputHandlers.ContainsKey(request.TerminalId))
{ {
_terminalOutputHandlers[terminalId].Invoke((byte[])messageContent); _terminalOutputHandlers[request.TerminalId].Invoke(request.Output);
} }
else else
{ {
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId); Logger.Instance.Error("Received output for unknown terminal Id {id}", request.TerminalId);
} }
break; }
case (byte) MessageIdentifiers.TerminalExitedRequest: else if (messageType == nameof(TerminalExitedRequest))
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent); {
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request); var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
TerminalExited?.Invoke(this, request.ToStatus()); Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
break;
default: TerminalExited?.Invoke(this, request.TerminalId);
Logger.Instance.Error("Received unknown message type: {messageType}", messageType);
break;
} }
} }
public Task ResizeTerminalAsync(byte id, TerminalSize size) public Task ResizeTerminal(int id, TerminalSize size)
{ {
return _appServiceConnection.SendMessageAsync(CreateMessage(new ResizeTerminalRequest var request = new ResizeTerminalRequest
{TerminalId = id, NewSize = size})); {
TerminalId = id,
NewSize = size
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
} }
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback) public void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback)
{ {
_terminalOutputHandlers[terminalId] = callback; _terminalOutputHandlers[terminalId] = callback;
} }
public void UnsubscribeFromTerminalOutput(byte id) public Task UpdateToggleWindowKeyBindings()
{
if (_terminalOutputHandlers.ContainsKey(id))
{
_terminalOutputHandlers.Remove(id);
}
}
public Task UpdateToggleWindowKeyBindingsAsync()
{ {
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)]; var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
return _appServiceConnection.SendMessageAsync(CreateMessage(new SetToggleWindowKeyBindingsRequest var request = new SetToggleWindowKeyBindingsRequest
{KeyBindings = keyBindings}));
}
public Task WriteAsync(byte id, byte[] data)
{
var message = new ValueSet
{ {
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier, KeyBindings = keyBindings
[MessageKeys.TerminalId] = id,
[MessageKeys.Content] = data
}; };
return _appServiceConnection.SendMessageAsync(message); return _appServiceConnection.SendMessageAsync(CreateMessage(request));
} }
public Task CloseTerminalAsync(byte terminalId) public Task Write(int id, byte[] data)
{ {
var request = new TerminalExitedRequest(terminalId, -1); var request = new WriteDataRequest
{
TerminalId = id,
Data = data
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public Task CloseTerminal(int terminalId)
{
var request = new TerminalExitedRequest
{
TerminalId = terminalId
};
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request); Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
return _appServiceConnection.SendMessageAsync(CreateMessage(request)); return _appServiceConnection.SendMessageAsync(CreateMessage(request));
} }
public async Task<string> GetCommandPathAsync(string command) private IDictionary<string, string> CreateMessage(object content)
{ {
if (string.IsNullOrWhiteSpace(command)) return new Dictionary<string, string>
{ {
throw new ArgumentException("Input value is null or empty.", nameof(command)); [MessageKeys.Type] = content.GetType().Name,
} [MessageKeys.Content] = JsonConvert.SerializeObject(content)
};
command = command.Trim();
var commandLower = command.ToLowerInvariant();
if (CommandPaths.TryGetValue(commandLower, out var path))
{
return path;
}
if (CommandErrors.TryGetValue(commandLower, out var error))
{
throw new Exception(error);
}
var response = await GetResponseAsync<StringValueResponse>(new GetCommandPathRequest {Command = command})
.ConfigureAwait(false);
if (response.Success)
{
CommandPaths[commandLower] = response.Value;
return response.Value;
}
CommandErrors[commandLower] = response.Error;
throw new Exception(response.Error);
}
public Task QuitApplicationAsync()
{
return _appServiceConnection.SendMessageAsync(CreateMessage(new QuitApplicationRequest()));
}
private async Task<T> GetResponseAsync<T>(IMessage request)
{
var messageResponse =
await _appServiceConnection.SendMessageAsync(CreateMessage(request)).ConfigureAwait(false);
return JsonConvert.DeserializeObject<T>((string)messageResponse[MessageKeys.Content]);
} }
} }
} }
@@ -17,9 +17,9 @@ namespace FluentTerminal.App.Services.Implementation
_notificationService = notificationService; _notificationService = notificationService;
} }
public async Task CheckForUpdateAsync(bool notifyNoUpdate = false) public async Task CheckForUpdate(bool notifyNoUpdate = false)
{ {
var latest = await GetLatestVersionAsync().ConfigureAwait(false); var latest = await GetLatestVersionAsync();
if (latest > GetCurrentVersion()) if (latest > GetCurrentVersion())
{ {
_notificationService.ShowNotification("Update available", _notificationService.ShowNotification("Update available",
@@ -42,7 +42,7 @@ namespace FluentTerminal.App.Services.Implementation
var restClient = new RestClient(apiEndpoint); var restClient = new RestClient(apiEndpoint);
var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET); var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET);
var restResponse = await restClient.ExecuteAsync(restRequest).ConfigureAwait(false); var restResponse = await restClient.ExecuteTaskAsync(restRequest);
if (restResponse.IsSuccessful) if (restResponse.IsSuccessful)
{ {
dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content); dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content);
+1 -3
View File
@@ -35,9 +35,7 @@ namespace FluentTerminal.App.Services
.MinimumLevel.Is((LogEventLevel)configuration.LogLevel) .MinimumLevel.Is((LogEventLevel)configuration.LogLevel)
.CreateLogger(); .CreateLogger();
// Log file is not created during unhandled exception processing. Log.Information("Initialized");
// Force log file creation with the highest severity initialization record.
Log.Fatal("Initialized");
} }
public void Debug(string text, params object[] propertyValues) public void Debug(string text, params object[] propertyValues)
+143 -187
View File
@@ -1,188 +1,144 @@
using FluentTerminal.Models; using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses; using FluentTerminal.Models.Responses;
using System; using System;
using System.Text; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
namespace FluentTerminal.App.Services {
{ public class Terminal
public class Terminal {
{ private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService; private Func<Task<string>> _selectedTextCallback;
private Func<Task<string>> _selectedTextCallback;
private bool _closingFromUi; public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
private bool _exited; {
private readonly bool _requireShellProcessStart; _trayProcessCommunicationService = trayProcessCommunicationService;
private string _fallbackTitle; _trayProcessCommunicationService.TerminalExited += OnTerminalExited;
Id = _trayProcessCommunicationService.GetNextTerminalId();
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null) }
{
_trayProcessCommunicationService = trayProcessCommunicationService; private void OnTerminalExited(object sender, int e)
_trayProcessCommunicationService.TerminalExited += OnTerminalExited; {
Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId(); if (e == Id)
_requireShellProcessStart = !terminalId.HasValue; {
} Closed?.Invoke(this, System.EventArgs.Empty);
}
public void Reconnect() }
{
_exited = false; /// <summary>
_trayProcessCommunicationService.TerminalExited += OnTerminalExited; /// To be observed by both view and viewmodel
} /// </summary>
public event EventHandler Closed;
private void OnTerminalExited(object sender, TerminalExitStatus status)
{ /// <summary>
if (status.TerminalId != Id) /// to be observed by viewmodel
{ /// </summary>
return; public event EventHandler<string> KeyboardCommandReceived;
}
/// <summary>
_exited = true; /// to be observed by view
Exited?.Invoke(this, status.ExitCode); /// </summary>
public event EventHandler<byte[]> OutputReceived;
if (_closingFromUi || status.ExitCode <= 0)
{ /// <summary>
Closed?.Invoke(this, System.EventArgs.Empty); /// to be observed by viewmodel
} /// </summary>
public event EventHandler<TerminalSize> SizeChanged;
_trayProcessCommunicationService.TerminalExited -= OnTerminalExited;
} /// <summary>
/// to be observed by viewmodel
public event EventHandler<int> Exited; /// </summary>
public event EventHandler<string> TitleChanged;
/// <summary>
/// To be observed by both view and viewmodel public string FallbackTitle { get; private set; }
/// </summary>
public event EventHandler Closed; public int Id { get; }
/// <summary> /// <summary>
/// to be observed by viewmodel /// To be called by either view or viewmodel
/// </summary> /// </summary>
public event EventHandler<string> KeyboardCommandReceived; public async Task Close()
{
/// <summary> await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
/// to be observed by view }
/// </summary>
public event EventHandler<byte[]> OutputReceived; /// <summary>
/// to be called by viewmodel
/// <summary> /// </summary>
/// to be observed by viewmodel /// <returns></returns>
/// </summary> public Task<string> GetSelectedText()
public event EventHandler<TerminalSize> SizeChanged; {
return _selectedTextCallback();
/// <summary> }
/// to be observed by viewmodel
/// </summary> /// <summary>
public event EventHandler<string> TitleChanged; /// to be called by view
/// </summary>
public byte Id { get; } public void ProcessKeyboardCommand(string command)
{
public ShellProfile Profile { get; private set; } KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be called by either view or viewmodel /// <summary>
/// </summary> /// To be set by view
public Task CloseAsync() /// </summary>
{ public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
if (_exited) {
{ _selectedTextCallback = selectedTextCallback;
Closed?.Invoke(this, System.EventArgs.Empty); }
return Task.CompletedTask;
} /// <summary>
_closingFromUi = true; /// To be called by view
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id); /// </summary>
return _trayProcessCommunicationService.CloseTerminalAsync(Id); public async Task SetSize(TerminalSize size)
} {
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
/// <summary> SizeChanged?.Invoke(this, size);
/// to be called by viewmodel }
/// </summary>
/// <returns></returns> /// <summary>
public Task<string> GetSelectedText() /// to be called by view
{ /// </summary>
return _selectedTextCallback?.Invoke(); public void SetTitle(string title)
} {
if (string.IsNullOrWhiteSpace(title))
/// <summary> {
/// to be called by view title = FallbackTitle;
/// </summary> }
public void ProcessKeyboardCommand(string command) TitleChanged?.Invoke(this, title);
{ }
KeyboardCommandReceived?.Invoke(this, command);
} /// <summary>
/// to be called by view when ready
/// <summary> /// </summary>
/// To be set by view /// <param name="shellProfile"></param>
/// </summary> /// <param name="size"></param>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback) /// <param name="sessionType"></param>
{ public async Task<CreateTerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
_selectedTextCallback = selectedTextCallback; {
} _trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
/// <summary>
/// To be called by view if (response.Success)
/// </summary> {
public async Task SetSizeAsync(TerminalSize size) FallbackTitle = response.ShellExecutableName;
{ SetTitle(FallbackTitle);
await _trayProcessCommunicationService.ResizeTerminalAsync(Id, size).ConfigureAwait(false); }
SizeChanged?.Invoke(this, size); return response;
} }
/// <summary> public Task Write(byte[] data)
/// to be called by view {
/// </summary> return _trayProcessCommunicationService.Write(Id, data);
public void SetTitle(string title) }
{
if (string.IsNullOrWhiteSpace(title)) /// <summary>
{ /// Tells the ViewModel that this view failed to launch and can be closed.
title = _fallbackTitle; /// </summary>
} public void ReportLauchFailed()
TitleChanged?.Invoke(this, title); {
} Closed?.Invoke(this, System.EventArgs.Empty);
}
/// <summary> }
/// To be called by view when ready
/// </summary>
public async Task<TerminalResponse> StartShellProcessAsync(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
{
Profile = shellProfile;
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
{
OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState));
}
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
if (_requireShellProcessStart)
{
var response = await _trayProcessCommunicationService
.CreateTerminalAsync(Id, size, shellProfile, sessionType).ConfigureAwait(false);
if (response.Success)
{
_fallbackTitle = response.Name;
SetTitle(_fallbackTitle);
}
return response;
}
else
{
return await _trayProcessCommunicationService.PauseTerminalOutputAsync(Id, false);
}
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.WriteAsync(Id, data);
}
/// <summary>
/// Tells the ViewModel that this view failed to launch and can be closed.
/// </summary>
public void ReportLauchFailed()
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
} }
@@ -1,25 +0,0 @@
using Windows.ApplicationModel.Resources;
namespace FluentTerminal.App.Services.Utilities
{
/// <summary>
/// Get strings in different languages.
/// Author: Zuozishi
/// </summary>
public static class I18N
{
private static readonly ResourceLoader ResourceLoader = ResourceLoader.GetForViewIndependentUse();
public static string Translate(string resource)
{
return ResourceLoader.GetString(resource.Replace('.', '/'));
}
public static string TranslateWithFallback(string resource, string fallback)
{
var result = Translate(resource);
return string.IsNullOrEmpty(result) ? fallback : result;
}
}
}
@@ -1,21 +0,0 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace FluentTerminal.App.Services.Utilities
{
public class PreserveDictionaryKeyCaseContractResolver : DefaultContractResolver
{
public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
{ ContractResolver = new PreserveDictionaryKeyCaseContractResolver() };
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
contract.DictionaryKeyResolver = key => key;
return contract;
}
}
}
@@ -5,7 +5,7 @@ using System.Reflection;
namespace FluentTerminal.App.Services.Utilities namespace FluentTerminal.App.Services.Utilities
{ {
public class TerminalThemeContractResolver : PreserveDictionaryKeyCaseContractResolver public class TerminalThemeContractResolver : DefaultContractResolver
{ {
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{ {
@@ -24,4 +24,4 @@ namespace FluentTerminal.App.Services.Utilities
return property; return property;
} }
} }
} }
@@ -1,101 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
internal class DelayedHistorySaver : ISessionSuccessTracker
{
private const int DelayMilliseconds = 5000;
private readonly Action _saveAction;
private readonly object _lock = new object();
private CancellationTokenSource _cts;
private bool _sessionStarted;
private bool _outputReceived;
private bool _toSave = true;
internal DelayedHistorySaver(Action saveAction)
{
_saveAction = saveAction;
}
public void SetSuccessfulSessionStart()
{
lock (_lock)
{
if (!_toSave || _sessionStarted)
{
return;
}
_sessionStarted = true;
if (_outputReceived)
{
InitiateSave();
}
}
}
public void SetOutputReceived()
{
lock (_lock)
{
if (!_toSave || _outputReceived)
{
return;
}
_outputReceived = true;
if (_sessionStarted)
{
InitiateSave();
}
}
}
public void SetInvalid()
{
lock (_lock)
{
if (!_toSave || !(_cts is CancellationTokenSource cts))
{
return;
}
_toSave = false;
cts.Cancel();
cts.Dispose();
_cts = null;
}
}
private void InitiateSave()
{
_cts = new CancellationTokenSource();
Task.Delay(DelayMilliseconds, _cts.Token).ContinueWith(t =>
{
lock (_lock)
{
if (_toSave)
{
_saveAction.Invoke();
}
_toSave = false;
_cts?.Dispose();
_cts = null;
}
});
}
}
}
@@ -1,25 +0,0 @@
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
namespace FluentTerminal.App.ViewModels
{
public class EnvironmentVariableViewModel : ObservableObject
{
private string _name;
private string _value;
public Guid Id { get; } = Guid.NewGuid();
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public string Value
{
get => _value;
set => SetProperty(ref _value, value);
}
}
}
@@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Toolkit.Mvvm" Version="7.0.2" /> <PackageReference Include="Fleck" Version="1.0.3" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -13,11 +14,4 @@
<ProjectReference Include="..\FluentTerminal.Models\FluentTerminal.Models.csproj" /> <ProjectReference Include="..\FluentTerminal.Models\FluentTerminal.Models.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.UniversalApiContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.UniversalApiContract\5.0.0.0\Windows.Foundation.UniversalApiContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project> </Project>
@@ -1,11 +0,0 @@
namespace FluentTerminal.App.ViewModels
{
public interface ISessionSuccessTracker
{
void SetSuccessfulSessionStart();
void SetOutputReceived();
void SetInvalid();
}
}
@@ -1,22 +0,0 @@
using System;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public interface ITerminalView : IDisposable
{
Task ChangeFontSize(int fontSize);
Task ChangeThemeAsync(TerminalTheme theme);
Task ChangeKeyBindingsAsync();
Task ChangeOptionsAsync(TerminalOptions options);
Task InitializeAsync(TerminalViewModel viewModel);
Task ReconnectAsync();
void DisposalPrepare();
Task FindNextAsync(SearchRequest request);
Task FindPreviousAsync(SearchRequest request);
Task FocusTerminalAsync();
Task<string> SerializeXtermStateAsync();
void Paste(string text);
}
}
@@ -0,0 +1,124 @@
using FluentTerminal.App.ViewModels.Utilities;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Infrastructure
{
public class AsyncCommand : IAsyncCommand
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(
Func<Task> execute,
Func<bool> canExecute = null,
IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute()
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async Task ExecuteAsync()
{
if (CanExecute())
{
try
{
_isExecuting = true;
await _execute();
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute();
}
void ICommand.Execute(object parameter)
{
ExecuteAsync().FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
public class AsyncCommand<T> : IAsyncCommand<T>
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<T, Task> _execute;
private readonly Func<T, bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(Func<T, Task> execute, Func<T, bool> canExecute = null, IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute(T parameter)
{
return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
}
public async Task ExecuteAsync(T parameter)
{
if (CanExecute(parameter))
{
try
{
_isExecuting = true;
await _execute(parameter);
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute((T)parameter);
}
void ICommand.Execute(object parameter)
{
ExecuteAsync((T)parameter).FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
}
@@ -0,0 +1,20 @@
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Infrastructure
{
/// <summary>
/// based on: https://github.com/johnthiriet/AsyncVoid
/// </summary>
public interface IAsyncCommand : ICommand
{
Task ExecuteAsync();
bool CanExecute();
}
public interface IAsyncCommand<T> : ICommand
{
Task ExecuteAsync(T parameter);
bool CanExecute(T parameter);
}
}
File diff suppressed because it is too large. Load diff
@@ -1,41 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace FluentTerminal.App.ViewModels.Menu
{
public class ExpandableMenuItemViewModel : MenuItemViewModelBase
{
public ObservableCollection<MenuItemViewModelBase> SubItems { get; }
public ExpandableMenuItemViewModel(string text, IEnumerable<MenuItemViewModelBase> subItems = null,
string description = null, Mdl2Icon icon = null) : base(text, description, icon)
{
if (subItems == null)
{
SubItems = new ObservableCollection<MenuItemViewModelBase>();
}
else if (subItems is ObservableCollection<MenuItemViewModelBase> observableSubItems)
{
SubItems = observableSubItems;
}
else
{
SubItems = new ObservableCollection<MenuItemViewModelBase>(subItems);
}
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is ExpandableMenuItemViewModel otherExpandable) ||
SubItems.Count != otherExpandable.SubItems.Count)
{
return false;
}
return !SubItems.Where((t, i) => !t.EquivalentTo(otherExpandable.SubItems[i])).Any();
}
}
}
@@ -1,46 +0,0 @@
namespace FluentTerminal.App.ViewModels.Menu
{
public sealed class Mdl2Icon
{
public string Color { get; }
public string Glyph { get; }
private Mdl2Icon(string glyph, string color = null)
{
Glyph = glyph;
Color = color;
}
public static Mdl2Icon Link(string color = null) => new Mdl2Icon("\uE71B", color);
public static Mdl2Icon Copy(string color = null) => new Mdl2Icon("\uE8C8", color);
public static Mdl2Icon Paste(string color = null) => new Mdl2Icon("\uE77F", color);
public static Mdl2Icon Edit(string color = null) => new Mdl2Icon("\uE70F", color);
public static Mdl2Icon Search(string color = null) => new Mdl2Icon("\uE721", color);
public static Mdl2Icon Add(string color = null) => new Mdl2Icon("\uE710", color);
public static Mdl2Icon Cancel(string color = null) => new Mdl2Icon("\uE711", color);
public static Mdl2Icon PaginationDotOutline10(string color = null) => new Mdl2Icon("\uF126", color);
public static Mdl2Icon PaginationDotSolid10(string color = null) => new Mdl2Icon("\uF127", color);
public static Mdl2Icon NewWindow(string color = null) => new Mdl2Icon("\uE78B", color);
public static Mdl2Icon History(string color = null) => new Mdl2Icon("\uE81C", color);
public static Mdl2Icon Settings(string color = null) => new Mdl2Icon("\uE713", color);
public static Mdl2Icon Info(string color = null) => new Mdl2Icon("\uE946", color);
public static Mdl2Icon FavoriteStar(string color = null) => new Mdl2Icon("\uE734", color);
public static Mdl2Icon Globe(string color = null) => new Mdl2Icon("\uE774", color);
public static Mdl2Icon Play(string color = null) => new Mdl2Icon("\uE768", color);
}
}
@@ -1,159 +0,0 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using Windows.System;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemKeyBindingViewModel : ObservableObject
{
private int _key;
public int Key
{
get => _key;
set => SetProperty(ref _key, value);
}
private int _keyModifiers;
// Corresponds to https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers
public int KeyModifiers
{
get => _keyModifiers;
private set => SetProperty(ref _keyModifiers, value);
}
private bool _ctrl;
public bool Ctrl
{
get => _ctrl;
set
{
if (SetProperty(ref _ctrl, value))
{
SetKeyModifiers();
}
}
}
private bool _alt;
public bool Alt
{
get => _alt;
set
{
if (SetProperty(ref _alt, value))
{
SetKeyModifiers();
}
}
}
private bool _shift;
public bool Shift
{
get => _shift;
set
{
if (SetProperty(ref _shift, value))
{
SetKeyModifiers();
}
}
}
private bool _windows;
public bool Windows
{
get => _windows;
set
{
if (SetProperty(ref _windows, value))
{
SetKeyModifiers();
}
}
}
public bool IsExtendedVirtualKey => !Enum.IsDefined(typeof(VirtualKey), Key);
public string GetOverrideText()
{
var segments = new List<string>();
if (Ctrl) segments.Add("Ctrl");
if (Alt) segments.Add("Alt");
if (Windows) segments.Add("Windows");
if (Shift) segments.Add("Shift");
segments.Add(EnumHelper.GetEnumDescription((ExtendedVirtualKey)Key));
return string.Join("+", segments);
}
public MenuItemKeyBindingViewModel(int key = 0, bool ctrl = false, bool alt = false, bool shift = false,
bool windows = false)
{
_key = key;
_ctrl = ctrl;
_alt = alt;
_shift = shift;
_windows = windows;
SetKeyModifiers();
}
public MenuItemKeyBindingViewModel(KeyBinding keyBinding) : this(keyBinding.Key, keyBinding.Ctrl,
keyBinding.Alt, keyBinding.Shift, keyBinding.Meta)
{
}
private void SetKeyModifiers()
{
// Based on VirtualKeyModifiers enum values (https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers)
var result = 0;
if (_ctrl)
{
result |= 1;
}
if (_alt)
{
result |= 2;
}
if (_shift)
{
result |= 4;
}
if (_windows)
{
result |= 8;
}
KeyModifiers = result;
}
public bool EquivalentTo(MenuItemKeyBindingViewModel other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
return _key == other._key && _keyModifiers == other._keyModifiers;
}
}
}
@@ -1,46 +0,0 @@
using System;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemViewModel : MenuItemViewModelBase
{
#region Properties
private MenuItemKeyBindingViewModel _keyBinding;
public MenuItemKeyBindingViewModel KeyBinding
{
get => _keyBinding;
set => SetProperty(ref _keyBinding, value);
}
public ICommand Command { get; }
#endregion Properties
#region Constructors
public MenuItemViewModel(string text, ICommand command, string description = null, Mdl2Icon icon = null,
MenuItemKeyBindingViewModel keyBinding = null) : base(text, description, icon)
{
_keyBinding = keyBinding;
Command = command ?? throw new ArgumentNullException(nameof(command));
}
#endregion Constructors
#region Methods
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is MenuItemViewModel menuItem)) return false;
return _keyBinding?.EquivalentTo(menuItem._keyBinding) ?? menuItem._keyBinding == null;
}
#endregion Methods
}
}
@@ -1,61 +0,0 @@
using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.ComponentModel;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemViewModelBase : ObservableObject
{
#region Properties
private string _text;
public string Text
{
get => _text;
set => SetProperty(ref _text, value);
}
private string _description;
public string Description
{
get => _description;
set => SetProperty(ref _description, value);
}
private Mdl2Icon _icon;
public Mdl2Icon Icon
{
get => _icon;
set => SetProperty(ref _icon, value);
}
#endregion Properties
#region Constructor
protected MenuItemViewModelBase(string text, string description, Mdl2Icon icon)
{
_text = text;
_description = description;
_icon = icon;
}
#endregion Constructor
#region Methods
public virtual bool EquivalentTo(MenuItemViewModelBase other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
return _text.NullableEqualTo(other._text) && _description.NullableEqualTo(other._description) &&
(_icon?.Equals(other._icon) ?? other._icon == null);
}
#endregion Methods
}
}
@@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Toolkit.Mvvm.ComponentModel;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuViewModel : ObservableObject
{
public ObservableCollection<MenuItemViewModelBase> Items { get; }
public MenuViewModel(IEnumerable<MenuItemViewModelBase> items = null)
{
if (items == null)
{
Items = new ObservableCollection<MenuItemViewModelBase>();
}
else if (items is ObservableCollection<MenuItemViewModelBase> observableItems)
{
Items = observableItems;
}
else
{
Items = new ObservableCollection<MenuItemViewModelBase>(items);
}
}
public bool EquivalentTo(MenuViewModel other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
if (Items.Count != other.Items.Count) return false;
return !Items.Where((t, i) => !t.EquivalentTo(other.Items[i])).Any();
}
}
}
@@ -1,28 +0,0 @@
using System;
namespace FluentTerminal.App.ViewModels.Menu
{
public class RadioMenuItemViewModel : MenuItemViewModelBase
{
public object BindingSource { get; }
public string BindingPath { get; }
public string GroupName { get; }
public RadioMenuItemViewModel(string text, string groupName, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null)
: base(text, description, icon)
{
GroupName = groupName ?? throw new ArgumentNullException(nameof(groupName));
BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource));
BindingPath = bindingPath;
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is RadioMenuItemViewModel radioMenuItem)) return false;
return GroupName == radioMenuItem.GroupName && BindingSource == radioMenuItem.BindingSource && BindingPath == radioMenuItem.BindingPath;
}
}
}
@@ -1,15 +0,0 @@
namespace FluentTerminal.App.ViewModels.Menu
{
public class SeparatorMenuItemViewModel : MenuItemViewModelBase
{
public SeparatorMenuItemViewModel()
: base(string.Empty, string.Empty, null)
{
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
return other is SeparatorMenuItemViewModel;
}
}
}
@@ -1,26 +0,0 @@
using System;
namespace FluentTerminal.App.ViewModels.Menu
{
public class ToggleMenuItemViewModel : MenuItemViewModelBase
{
public object BindingSource { get; }
public string BindingPath { get; }
public ToggleMenuItemViewModel(string text, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null)
: base(text, description, icon)
{
BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource));
BindingPath = bindingPath;
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is ToggleMenuItemViewModel toggleMenuItem)) return false;
return BindingSource == toggleMenuItem.BindingSource && BindingPath == toggleMenuItem.BindingPath;
}
}
}
@@ -1,53 +0,0 @@
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.ViewModels
{
public class OverlayViewModel : ObservableObject
{
private readonly DispatcherTimer _overlayTimer;
private bool _showOverlay;
private string _overlayContent;
// Important! The constructor has to be called from the UI thread.
public OverlayViewModel()
{
_overlayTimer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 2)};
}
public bool ShowOverlay
{
get => _showOverlay;
set => SetProperty(ref _showOverlay, value);
}
public string OverlayContent
{
get => _overlayContent;
set => SetProperty(ref _overlayContent, value);
}
public void Show(string message)
{
OverlayContent = message;
ShowOverlay = true;
if (_overlayTimer.IsEnabled)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
}
_overlayTimer.Start();
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
}
private void OnResizeOverlayTimerFinished(object sender, object e)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
ShowOverlay = false;
}
}
}
@@ -1,258 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
/// </summary>
public abstract class ProfileViewModelBase<T> : ObservableObject where T : ProfileProviderViewModelBase
{
#region Fields
protected readonly IDialogService DialogService;
protected readonly ISettingsService SettingsService;
private List<KeyBinding> _bindingsBeforeEdit;
private Dictionary<string, string> _environmentVariablesBeforeEdit;
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 => SetProperty(ref _inEditMode, value);
}
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<EnvironmentVariableViewModel> EnvironmentVariables { get; } = new ObservableCollection<EnvironmentVariableViewModel>();
public T ProfileVm { get; protected set; }
#endregion Properties
#region Events
// Needs to be triggered from UI thread
public event EventHandler Deleted;
#endregion Events
#region Commands
public ICommand CancelEditCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand EditCommand { get; }
public ICommand SaveChangesCommand { get; }
public ICommand AddKeyboardShortcutCommand { get; }
#endregion Commands
#region Constructor
protected ProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
{
SettingsService = settingsService;
DialogService = dialogService;
IsNew = isNew;
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), dialogService, string.Empty, false);
foreach (var environmentVariable in shellProfile.EnvironmentVariables)
{
EnvironmentVariables.Add(new EnvironmentVariableViewModel
{
Name = environmentVariable.Key,
Value = environmentVariable.Value
});
}
Initialize(shellProfile);
DeleteCommand = new AsyncRelayCommand(DeleteAsync, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncRelayCommand(CancelEditAsync);
SaveChangesCommand = new AsyncRelayCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncRelayCommand(AddKeyboardShortcutAsync);
}
#endregion Constructor
#region Methods
// Requires UI thread
// 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)
|| !(_environmentVariablesBeforeEdit?.SequenceEqual(EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value)) ?? false);
}
// Requires UI thread
private async Task DeleteAsync()
{
// ConfigureAwait(true) because we need to trigger Deleted event from the calling (UI) thread.
var result = await DialogService.ShowMessageDialogAsync(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();
_environmentVariablesBeforeEdit = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
_nameBeforeEdit = _name;
KeyBindings.Editable = true;
InEditMode = true;
}
// Requires UI thread
private async Task CancelEditAsync()
{
if (IsNew)
{
await DeleteAsync().ConfigureAwait(false);
return;
}
if (!HasChanges())
{
KeyBindings.Editable = false;
InEditMode = false;
return;
}
// ConfigureAwait(true) because we're setting some view-model properties afterwards
if (await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true) == DialogButton.OK)
{
// Cancelled, so rollback
// ConfigureAwait(true) because we're setting some view-model properties afterwards
await ProfileVm.RejectChangesAsync().ConfigureAwait(true);
KeyBindings.Editable = false;
InEditMode = false;
}
}
// Requires UI thread
private async Task SaveChangesAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var error = await ProfileVm.AcceptChangesAsync().ConfigureAwait(true);
if (!string.IsNullOrEmpty(error))
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"), error, DialogButton.OK)
.ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Any(string.IsNullOrWhiteSpace))
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK).ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK).ConfigureAwait(false);
return;
}
var profile = ProfileVm.Model;
profile.Id = Id;
profile.Name = _name;
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
profile.EnvironmentVariables = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
if (profile is SshProfile sshProfile)
{
SettingsService.SaveSshProfile(sshProfile, IsNew);
}
else
{
SettingsService.SaveShellProfile(profile, IsNew);
}
KeyBindings.Editable = false;
InEditMode = false;
IsNew = false;
}
private Task AddKeyboardShortcutAsync()
{
return KeyBindings.ShowAddKeyBindingDialogAsync();
}
#endregion Methods
}
}
@@ -1,151 +0,0 @@
using System.Threading.Tasks;
using System.Windows.Input;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.Input;
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 => SetProperty(ref _location, value);
}
private string _workingDirectory;
public string WorkingDirectory
{
get => _workingDirectory;
set => SetProperty(ref _workingDirectory, value);
}
private string _arguments;
public string Arguments
{
get => _arguments;
set => SetProperty(ref _arguments, value);
}
#endregion Properties
#region Commands
public ICommand BrowseForCustomShellCommand { get; }
public ICommand BrowseForWorkingDirectoryCommand { get; }
#endregion Commands
#region Constructor
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
true, original)
{
_fileSystemService = fileSystemService;
BrowseForCustomShellCommand = new AsyncRelayCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncRelayCommand(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 async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
profile.Location = _location;
profile.Arguments = _arguments;
profile.WorkingDirectory = _workingDirectory;
}
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync().ConfigureAwait(false);
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);
}
// Requires UI thread
private async Task BrowseForCustomShell()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var file = await _fileSystemService.OpenFileAsync(new[] { ".exe" }).ConfigureAwait(true);
if (file != null)
{
Location = file.Path;
}
}
// Requires UI thread
private async Task BrowseForWorkingDirectory()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var directory = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (directory != null)
{
WorkingDirectory = directory;
}
}
#endregion Methods
}
}
@@ -1,386 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Windows.UI.Core;
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using FluentTerminal.Models.Messages;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Messaging;
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 : ObservableObject,
IRecipient<ThemeAddedMessage>,
IRecipient<ThemeDeletedMessage>
{
#region Fields
protected readonly ISettingsService SettingsService;
protected readonly IApplicationView ApplicationView;
private readonly bool _strictProfileType;
#endregion Fields
#region Properties
private ShellProfile _model;
public ShellProfile Model
{
get => _model;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (ReferenceEquals(_model, value))
{
return;
}
if (_strictProfileType && _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; }
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 (SetProperty(ref _selectedTabTheme, theme))
{
_tabThemeId = theme.Id;
}
}
}
private int _tabThemeId;
public int TabThemeId
{
get => _tabThemeId;
set
{
if (SetProperty(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)
{
return;
}
else if (!TerminalThemes.Contains(theme))
{
// Ensure that it's from the list
theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(theme.Id)) ?? TerminalThemes.First();
}
if (SetProperty(ref _selectedTerminalTheme, theme))
{
_terminalThemeId = theme.Id;
}
}
}
private Guid _terminalThemeId;
public Guid TerminalThemeId
{
get => _terminalThemeId;
set
{
if (SetProperty(ref _terminalThemeId, value))
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
}
}
}
private bool _useConPty;
public bool UseConPty
{
get => _useConPty;
set => SetProperty(ref _useConPty, value);
}
private bool _useBuffer;
public bool UseBuffer
{
get => _useBuffer;
set => SetProperty(ref _useBuffer, value);
}
#endregion Properties
#region Constructor
protected ProfileProviderViewModelBase(ISettingsService settingsService, IApplicationView applicationView,
bool strictProfileType, ShellProfile original = null)
{
SettingsService = settingsService;
ApplicationView = applicationView;
_strictProfileType = strictProfileType;
_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();
Initialize(Model);
WeakReferenceMessenger.Default.RegisterAll(this);
}
#endregion Constructor
#region Methods
public void Receive(ThemeDeletedMessage message)
{
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(message.ThemeId));
if (theme == null)
{
return;
}
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
TerminalThemes.Remove(theme);
if (_terminalThemeId.Equals(message.ThemeId))
{
SelectedTerminalTheme = TerminalThemes.First();
}
}, CoreDispatcherPriority.Low, true);
}
public void Receive(ThemeAddedMessage message)
{
ApplicationView.ExecuteOnUiThreadAsync(() => TerminalThemes.Add(message.Theme), CoreDispatcherPriority.Low);
}
private void Initialize(ShellProfile profile)
{
UseConPty = profile.UseConPty;
UseBuffer = profile.UseBuffer;
TerminalThemeId = profile.TerminalThemeId;
TabThemeId = profile.TabThemeId;
}
protected virtual void LoadFromProfile(ShellProfile profile)
{
Initialize(profile);
}
protected virtual Task CopyToProfileAsync(ShellProfile profile)
{
profile.UseConPty = _useConPty;
profile.UseBuffer = _useBuffer;
profile.TerminalThemeId = _terminalThemeId;
profile.TabThemeId = _tabThemeId;
return Task.CompletedTask;
}
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.UseConPty != _useConPty ||
Model.UseBuffer != _useBuffer ||
!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().ConfigureAwait(false);
if (acceptIfInvalid || string.IsNullOrEmpty(error))
{
await CopyToProfileAsync(Model).ConfigureAwait(false);
}
return error;
}
public Task RejectChangesAsync()
{
return ApplicationView.ExecuteOnUiThreadAsync(() => LoadFromProfile(Model));
}
#endregion Methods
#region Links/shortcuts related
private const string UseConPtyQueryStringName = "conpty";
private const string UseBufferQueryStringName = "buffer";
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}&{UseBufferQueryStringName}={_useBuffer}";
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 =>
UseBufferQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useBuffer))
{
UseConPty = useBuffer;
}
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
}
}
@@ -1,537 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Windows.Input;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels.Profiles
{
/// <summary>
/// View-model for rich-UI based SSH profiles.
/// </summary>
public class SshConnectViewModel : ProfileProviderViewModelBase
{
#region Static
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;
// 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 => SetProperty(ref _host, value);
}
private ushort _sshPort;
public ushort SshPort
{
get => _sshPort;
set => SetProperty(ref _sshPort, value);
}
private string _username;
public string Username
{
get => _username;
set => SetProperty(ref _username, value);
}
private string _identityFile;
public string IdentityFile
{
get => _identityFile;
set => SetProperty(ref _identityFile, value);
}
private bool _useMosh;
public bool UseMosh
{
get => _useMosh;
set => SetProperty(ref _useMosh, value);
}
private ushort _moshPortFrom;
public ushort MoshPortFrom
{
get => _moshPortFrom;
set => SetProperty(ref _moshPortFrom, value);
}
private ushort _moshPortTo;
public ushort MoshPortTo
{
get => _moshPortTo;
set => SetProperty(ref _moshPortTo, value);
}
#endregion Properties
#region Commands
public ICommand BrowseForIdentityFileCommand { get; }
#endregion Commands
#region Constructor
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
SshProfile original = null) : base(settingsService, applicationView, true,
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault,
RequestConPty = settingsService.GetApplicationSettings().UseConPty
})
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
Initialize((SshProfile)Model);
BrowseForIdentityFileCommand = new AsyncRelayCommand(BrowseForIdentityFileAsync);
}
#endregion Constructor
#region Methods
// Fills the view model properties from the input sshProfile
private void Initialize(SshProfile sshProfile)
{
Host = sshProfile.Host;
SshPort = sshProfile.SshPort;
Username = sshProfile.Username;
if (string.IsNullOrEmpty(Username))
{
_trayProcessCommunicationService.GetUserNameAsync().ContinueWith(t =>
{
var username = t.Result;
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
{
ApplicationView.ExecuteOnUiThreadAsync(() => Username = username);
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
}
IdentityFile = sshProfile.IdentityFile;
UseMosh = sshProfile.UseMosh;
MoshPortFrom = sshProfile.MoshPortFrom;
MoshPortTo = sshProfile.MoshPortTo;
}
// Requires UI thread
private async Task BrowseForIdentityFileAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var file = await _fileSystemService.OpenFileAsync(new[] { "*" }).ConfigureAwait(true);
if (file != null)
{
IdentityFile = file.Path;
}
}
private string GetArgumentsString()
{
var sshArguments = GetSshArguments();
if (!_useMosh)
{
return string.IsNullOrWhiteSpace(sshArguments) ? $"{_username}@{_host}" : $"{sshArguments} {_username}@{_host}";
}
return string.IsNullOrEmpty(sshArguments)
? $"-p {_moshPortFrom}:{_moshPortTo} {_username}@{_host}"
: $"-p {_moshPortFrom}:{_moshPortTo} --ssh=\"ssh {sshArguments}\" {_username}@{_host}";
}
private string GetSshArguments()
{
if (string.IsNullOrEmpty(_identityFile))
{
return _sshPort == SshProfile.DefaultSshPort ? null : $"-p {_sshPort:#####}";
}
return _sshPort == SshProfile.DefaultSshPort
? $"-i \"{_identityFile}\""
: $"-p {_sshPort:#####} -i \"{_identityFile}\"";
}
protected override void LoadFromProfile(ShellProfile profile)
{
base.LoadFromProfile(profile);
Initialize((SshProfile)profile);
}
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
var sshProfile = (SshProfile) profile;
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;
}
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
return error;
}
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
if (result == SshConnectionInfoValidationResult.Valid)
{
return null;
}
error = GetErrorString(result, Environment.NewLine);
if (string.IsNullOrEmpty(error))
{
error = "Invalid input.";
}
return error;
}
public override bool HasChanges()
{
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;
}
private 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().ConfigureAwait(false))
{
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
}
if (!_useMosh)
{
return result;
}
if (_moshPortFrom < 1)
{
result |= SshConnectionInfoValidationResult.MoshPortZeroOrNegative;
}
if (_moshPortFrom > _moshPortTo)
{
result |= SshConnectionInfoValidationResult.MoshPortRangeInvalid;
}
return result;
}
// Requires UI thread
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
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync().ConfigureAwait(true);
if (string.IsNullOrEmpty(sshConfigDir))
{
return false;
}
fullPath = Path.Combine(sshConfigDir, identityFile);
}
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath).ConfigureAwait(true))
{
_validatedIdentityFile = identityFile;
IdentityFile = fullPath;
return true;
}
return false;
}
public void SetValidatedIdentityFile(string identityFile)
{
_validatedIdentityFile = identityFile;
IdentityFile = identityFile;
}
#endregion Methods
#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";
// 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) =>
SshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase) ||
MoshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
public static SshConnectViewModel ParseUri(Uri uri, ISettingsService settingsService,
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
{
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
fileSystemService)
{
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;
}
public override async Task<Tuple<bool, string>> GetUrlAsync()
{
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
return Tuple.Create(false, error);
}
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
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);
}
var 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());
}
#endregion Links/shortcuts related
}
}
@@ -1,16 +0,0 @@
using System;
namespace FluentTerminal.App.ViewModels.Profiles
{
[Flags]
public enum SshConnectionInfoValidationResult
{
Valid = 0,
UsernameEmpty = 1,
HostEmpty = 2,
SshPortZeroOrNegative = 4,
MoshPortZeroOrNegative = 8,
MoshPortRangeInvalid = 16,
IdentityFileDoesNotExist = 32
}
}
@@ -1,34 +1,36 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using Microsoft.Toolkit.Mvvm.ComponentModel; using FluentTerminal.App.ViewModels.Infrastructure;
using Microsoft.Toolkit.Mvvm.Input; using GalaSoft.MvvmLight;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class AboutPageViewModel : ObservableObject public class AboutPageViewModel : ViewModelBase
{ {
private const string BaseUrl = "https://github.com/felixse/FluentTerminal/releases/tag/"; private const string BaseUrl = "https://github.com/felixse/FluentTerminal/releases/tag/";
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;
private string _latestVersion; private string _latestVersion;
private readonly IApplicationView _applicationView; private readonly IApplicationView _applicationView;
public AboutPageViewModel(IUpdateService updateService, IApplicationView applicationView) public AboutPageViewModel(ISettingsService settingsService, IUpdateService updateService, IApplicationView applicationView)
{ {
_settingsService = settingsService;
_updateService = updateService; _updateService = updateService;
_applicationView = applicationView; _applicationView = applicationView;
CheckForUpdatesCommand = new AsyncRelayCommand(() => CheckForUpdateAsync(true)); CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdate(true));
} }
public ICommand CheckForUpdatesCommand { get; } public IAsyncCommand CheckForUpdatesCommand { get; }
public string CurrentVersion public string CurrentVersion
{ {
get get
{ {
var version = _updateService.GetCurrentVersion(); var version = _updateService.GetCurrentVersion();
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; return ConvertVersionToString(version);
} }
} }
@@ -39,12 +41,12 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _latestVersion; get => _latestVersion;
set set
{ {
if (SetProperty(ref _latestVersion, value)) if (Set(ref _latestVersion, value))
{ {
OnPropertyChanged(nameof(LatestVersionFound)); RaisePropertyChanged(nameof(LatestVersionFound));
OnPropertyChanged(nameof(LatestVersionLoading)); RaisePropertyChanged(nameof(LatestVersionLoading));
OnPropertyChanged(nameof(LatestVersionNotFound)); RaisePropertyChanged(nameof(LatestVersionNotFound));
OnPropertyChanged(nameof(LatestVersionReleaseNotesURL)); RaisePropertyChanged(nameof(LatestVersionReleaseNotesURL));
} }
} }
} }
@@ -59,16 +61,19 @@ namespace FluentTerminal.App.ViewModels.Settings
public Task OnNavigatedTo() public Task OnNavigatedTo()
{ {
return CheckForUpdateAsync(false); return CheckForUpdate(false);
} }
private async Task CheckForUpdateAsync(bool notifyNoUpdate) public async Task CheckForUpdate(bool notifyNoUpdate)
{ {
var version = await _updateService.GetLatestVersionAsync().ConfigureAwait(false); var version = ConvertVersionToString(await _updateService.GetLatestVersionAsync());
await _applicationView.ExecuteOnUiThreadAsync(() => await _applicationView.RunOnDispatcherThread(() => LatestVersion = version);
LatestVersion = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}") await _updateService.CheckForUpdate(notifyNoUpdate);
.ConfigureAwait(false); }
await _updateService.CheckForUpdateAsync(notifyNoUpdate).ConfigureAwait(false);
private string ConvertVersionToString(Version version)
{
return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
} }
} }
} }
@@ -1,16 +1,13 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models; using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using Microsoft.Toolkit.Mvvm.ComponentModel; using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.Input; using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class GeneralPageViewModel : ObservableObject public class GeneralPageViewModel : ViewModelBase
{ {
private readonly ApplicationSettings _applicationSettings; private readonly ApplicationSettings _applicationSettings;
private readonly IDefaultValueProvider _defaultValueProvider; private readonly IDefaultValueProvider _defaultValueProvider;
@@ -18,56 +15,57 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly IStartupTaskService _startupTaskService; private readonly IStartupTaskService _startupTaskService;
private bool _canEnableStartupTask; private bool _canEnableStartupTask;
private bool _editingNewTerminalLocation;
private bool _editingTabsPosition;
private bool _editingInactiveTabColorMode;
private bool _startupTaskEnabled; private bool _startupTaskEnabled;
private bool _shouldRestartForTrayMessage; private bool _shouldRestartForTrayMessage;
private string _startupTaskErrorMessage; private string _startupTaskErrorMessage;
private bool _needsToRestart;
private readonly IApplicationLanguageService _applicationLanguageService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IFileSystemService _fileSystemService;
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IStartupTaskService startupTaskService)
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService)
{ {
_settingsService = settingsService; _settingsService = settingsService;
_dialogService = dialogService; _dialogService = dialogService;
_defaultValueProvider = defaultValueProvider; _defaultValueProvider = defaultValueProvider;
_startupTaskService = startupTaskService; _startupTaskService = startupTaskService;
_applicationLanguageService = applicationLanguageService;
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
_applicationSettings = _settingsService.GetApplicationSettings(); _applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync); RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
BrowseLogDirectoryCommand = new AsyncRelayCommand(BrowseLogDirectoryAsync);
} }
public IEnumerable<string> Languages => _applicationLanguageService.Languages; public async Task OnNavigatedTo()
public bool NeedsToRestart
{ {
get => _needsToRestart; var startupTaskStatus = await _startupTaskService.GetStatus();
set => SetProperty(ref _needsToRestart, value); SetStartupTaskPropertiesForStatus(startupTaskStatus);
} }
public string SelectedLanguage public bool AlwaysShowTabs
{ {
get => _applicationLanguageService.GetCurrentLanguage(); get => _applicationSettings.AlwaysShowTabs;
set set
{ {
_applicationLanguageService.SetLanguage(value); if (_applicationSettings.AlwaysShowTabs != value)
NeedsToRestart = true; {
_applicationSettings.AlwaysShowTabs = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
} }
} }
// Requires UI thread public bool AlwaysUseWinPty
public async Task OnNavigatedToAsync()
{ {
// ConfigureAwait(true) because we want to execute SetStartupTaskPropertiesForStatus from the calling (UI) thread. get => _applicationSettings.AlwaysUseWinPty;
var startupTaskStatus = await _startupTaskService.GetStatusAsync().ConfigureAwait(true); set
SetStartupTaskPropertiesForStatus(startupTaskStatus); {
if (_applicationSettings.AlwaysUseWinPty != value)
{
_applicationSettings.AlwaysUseWinPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
} }
public bool ShowCustomTitleInTitlebar public bool ShowCustomTitleInTitlebar
@@ -79,7 +77,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_applicationSettings.ShowCustomTitleInTitlebar = value; _applicationSettings.ShowCustomTitleInTitlebar = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -93,7 +91,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_applicationSettings.EnableTrayIcon = value; _applicationSettings.EnableTrayIcon = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
// Toggle message telling user to restart the program // Toggle message telling user to restart the program
ShouldRestartForTrayMessage = !ShouldRestartForTrayMessage; ShouldRestartForTrayMessage = !ShouldRestartForTrayMessage;
@@ -104,7 +102,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool ShouldRestartForTrayMessage public bool ShouldRestartForTrayMessage
{ {
get => _shouldRestartForTrayMessage; get => _shouldRestartForTrayMessage;
set => SetProperty(ref _shouldRestartForTrayMessage, value); set => Set(ref _shouldRestartForTrayMessage, value);
} }
public bool ShowNewOutputIndicator public bool ShowNewOutputIndicator
@@ -116,120 +114,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_applicationSettings.ShowNewOutputIndicator = value; _applicationSettings.ShowNewOutputIndicator = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
}
}
}
public bool UseMoshByDefault
{
get => _applicationSettings.UseMoshByDefault;
set
{
if (_applicationSettings.UseMoshByDefault != value)
{
_applicationSettings.UseMoshByDefault = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public bool AutoFallbackToWindowsUsernameInLinks
{
get => _applicationSettings.AutoFallbackToWindowsUsernameInLinks;
set
{
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks != value)
{
_applicationSettings.AutoFallbackToWindowsUsernameInLinks = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public bool RTrimCopiedLines
{
get => _applicationSettings.RTrimCopiedLines;
set
{
if (_applicationSettings.RTrimCopiedLines != value)
{
_applicationSettings.RTrimCopiedLines = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public bool MuteTerminalBeeps
{
get => _applicationSettings.MuteTerminalBeeps;
set
{
if (_applicationSettings.MuteTerminalBeeps != value)
{
_applicationSettings.MuteTerminalBeeps = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
_trayProcessCommunicationService.MuteTerminalAsync(value);
}
}
}
public bool EnableLogging
{
get => _applicationSettings.EnableLogging;
set
{
if (_applicationSettings.EnableLogging != value)
{
_applicationSettings.EnableLogging = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public bool PrintableOutputOnly
{
get => _applicationSettings.PrintableOutputOnly;
set
{
if (_applicationSettings.PrintableOutputOnly != value)
{
_applicationSettings.PrintableOutputOnly = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public string LogDirectoryPath
{
get => _applicationSettings.LogDirectoryPath;
set
{
if (_applicationSettings.LogDirectoryPath != value)
{
_applicationSettings.LogDirectoryPath = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public bool UseConPty
{
get => _applicationSettings.UseConPty;
set
{
if (_applicationSettings.UseConPty != value)
{
_applicationSettings.UseConPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
} }
} }
} }
@@ -237,13 +122,13 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool BottomIsSelected public bool BottomIsSelected
{ {
get => TabsPosition == TabsPosition.Bottom; get => TabsPosition == TabsPosition.Bottom;
set { if (value) TabsPosition = TabsPosition.Bottom; } set => TabsPosition = TabsPosition.Bottom;
} }
public bool CanEnableStartupTask public bool CanEnableStartupTask
{ {
get => _canEnableStartupTask; get => _canEnableStartupTask;
set => SetProperty(ref _canEnableStartupTask, value); set => Set(ref _canEnableStartupTask, value);
} }
public bool ConfirmClosingTabs public bool ConfirmClosingTabs
@@ -255,7 +140,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_applicationSettings.ConfirmClosingTabs = value; _applicationSettings.ConfirmClosingTabs = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -269,7 +154,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_applicationSettings.ConfirmClosingWindows = value; _applicationSettings.ConfirmClosingWindows = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -279,43 +164,31 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.NewTerminalLocation; get => _applicationSettings.NewTerminalLocation;
set set
{ {
if (_applicationSettings.NewTerminalLocation != value) if (_applicationSettings.NewTerminalLocation != value && !_editingNewTerminalLocation)
{ {
_editingNewTerminalLocation = true;
_applicationSettings.NewTerminalLocation = value; _applicationSettings.NewTerminalLocation = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
OnPropertyChanged(nameof(TabIsSelected)); RaisePropertyChanged(nameof(WindowIsSelected));
RaisePropertyChanged(nameof(TabIsSelected));
_editingNewTerminalLocation = false;
} }
} }
} }
public bool TabWindowCascadingAppMenu public RelayCommand RestoreDefaultsCommand { get; }
{
get => _applicationSettings.TabWindowCascadingAppMenu;
set
{
if (_applicationSettings.TabWindowCascadingAppMenu != value)
{
_applicationSettings.TabWindowCascadingAppMenu = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public ICommand RestoreDefaultsCommand { get; }
public ICommand BrowseLogDirectoryCommand { get; }
public bool StartupTaskEnabled public bool StartupTaskEnabled
{ {
get => _startupTaskEnabled; get => _startupTaskEnabled;
set set
{ {
if (SetProperty(ref _startupTaskEnabled, value)) if (_startupTaskEnabled != value)
{ {
// ReSharper disable once AssignmentIsFullyDiscarded _startupTaskEnabled = value;
_ = SetStartupTaskStateAsync(value); RaisePropertyChanged(nameof(StartupTaskEnabled));
SetStartupTaskState(value);
} }
} }
} }
@@ -323,13 +196,13 @@ namespace FluentTerminal.App.ViewModels.Settings
public string StartupTaskErrorMessage public string StartupTaskErrorMessage
{ {
get => _startupTaskErrorMessage; get => _startupTaskErrorMessage;
set => SetProperty(ref _startupTaskErrorMessage, value); set => Set(ref _startupTaskErrorMessage, value);
} }
public bool TabIsSelected public bool TabIsSelected
{ {
get => NewTerminalLocation == NewTerminalLocation.Tab; get => NewTerminalLocation == NewTerminalLocation.Tab;
set => NewTerminalLocation = value ? NewTerminalLocation.Tab : NewTerminalLocation.Window; set => NewTerminalLocation = NewTerminalLocation.Tab;
} }
public TabsPosition TabsPosition public TabsPosition TabsPosition
@@ -337,11 +210,15 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.TabsPosition; get => _applicationSettings.TabsPosition;
set set
{ {
if (_applicationSettings.TabsPosition != value) if (_applicationSettings.TabsPosition != value && !_editingTabsPosition)
{ {
_editingTabsPosition = true;
_applicationSettings.TabsPosition = value; _applicationSettings.TabsPosition = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
RaisePropertyChanged(nameof(TopIsSelected));
RaisePropertyChanged(nameof(BottomIsSelected));
_editingTabsPosition = false;
} }
} }
} }
@@ -349,7 +226,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool TopIsSelected public bool TopIsSelected
{ {
get => TabsPosition == TabsPosition.Top; get => TabsPosition == TabsPosition.Top;
set { if (value) TabsPosition = TabsPosition.Top; } set => TabsPosition = TabsPosition.Top;
} }
public bool UnderlineSelectedTab public bool UnderlineSelectedTab
@@ -361,7 +238,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_applicationSettings.UnderlineSelectedTab = value; _applicationSettings.UnderlineSelectedTab = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -369,13 +246,13 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool BackgroundIsSelected public bool BackgroundIsSelected
{ {
get => InactiveTabColorMode == InactiveTabColorMode.Background; get => InactiveTabColorMode == InactiveTabColorMode.Background;
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Background; } set => InactiveTabColorMode = InactiveTabColorMode.Background;
} }
public bool UnderlinedIsSelected public bool UnderlinedIsSelected
{ {
get => InactiveTabColorMode == InactiveTabColorMode.Underlined; get => InactiveTabColorMode == InactiveTabColorMode.Underlined;
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Underlined; } set => InactiveTabColorMode = InactiveTabColorMode.Underlined;
} }
public InactiveTabColorMode InactiveTabColorMode public InactiveTabColorMode InactiveTabColorMode
@@ -383,36 +260,28 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.InactiveTabColorMode; get => _applicationSettings.InactiveTabColorMode;
set set
{ {
if (_applicationSettings.InactiveTabColorMode != value) if (_applicationSettings.InactiveTabColorMode != value && !_editingInactiveTabColorMode)
{ {
_editingInactiveTabColorMode = true;
_applicationSettings.InactiveTabColorMode = value; _applicationSettings.InactiveTabColorMode = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
RaisePropertyChanged(nameof(BackgroundIsSelected));
RaisePropertyChanged(nameof(UnderlinedIsSelected));
_editingInactiveTabColorMode = false;
} }
} }
} }
public bool ShowTextCopied public bool WindowIsSelected
{ {
get => _applicationSettings.ShowTextCopied; get => NewTerminalLocation == NewTerminalLocation.Window;
set set => NewTerminalLocation = NewTerminalLocation.Window;
{
if (_applicationSettings.ShowTextCopied != value)
{
_applicationSettings.ShowTextCopied = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
} }
// Requires UI thread private async Task RestoreDefaults()
private async Task RestoreDefaultsAsync()
{ {
// ConfigureAwait(true) because we're setting some view-model properties afterwards var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the general settings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK) if (result == DialogButton.OK)
{ {
@@ -422,22 +291,14 @@ namespace FluentTerminal.App.ViewModels.Settings
UnderlineSelectedTab = defaults.UnderlineSelectedTab; UnderlineSelectedTab = defaults.UnderlineSelectedTab;
InactiveTabColorMode = defaults.InactiveTabColorMode; InactiveTabColorMode = defaults.InactiveTabColorMode;
NewTerminalLocation = defaults.NewTerminalLocation; NewTerminalLocation = defaults.NewTerminalLocation;
AlwaysShowTabs = defaults.AlwaysShowTabs;
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator; ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon; EnableTrayIcon = defaults.EnableTrayIcon;
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar; ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
UseMoshByDefault = defaults.UseMoshByDefault;
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
RTrimCopiedLines = defaults.RTrimCopiedLines;
MuteTerminalBeeps = defaults.MuteTerminalBeeps;
EnableLogging = defaults.EnableLogging;
PrintableOutputOnly = defaults.PrintableOutputOnly;
LogDirectoryPath = defaults.LogDirectoryPath;
UseConPty = defaults.UseConPty;
ShowTextCopied = defaults.ShowTextCopied;
} }
} }
// Requires UI thread
private void SetStartupTaskPropertiesForStatus(StartupTaskStatus startupTaskStatus) private void SetStartupTaskPropertiesForStatus(StartupTaskStatus startupTaskStatus)
{ {
switch (startupTaskStatus) switch (startupTaskStatus)
@@ -456,53 +317,31 @@ namespace FluentTerminal.App.ViewModels.Settings
case StartupTaskStatus.DisabledByUser: case StartupTaskStatus.DisabledByUser:
StartupTaskEnabled = false; StartupTaskEnabled = false;
StartupTaskErrorMessage = I18N.Translate("DisabledByUser"); StartupTaskErrorMessage = "Disabled by user. Please reactivate it in the Startup tab of the Task Manager.";
CanEnableStartupTask = false; CanEnableStartupTask = false;
break; break;
case StartupTaskStatus.DisabledByPolicy: case StartupTaskStatus.DisabledByPolicy:
StartupTaskEnabled = false; StartupTaskEnabled = false;
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy"); StartupTaskErrorMessage = "Disabled by policy.";
CanEnableStartupTask = false;
break;
case StartupTaskStatus.EnabledByPolicy:
StartupTaskEnabled = true;
StartupTaskErrorMessage = I18N.TranslateWithFallback("EnabledByPolicy", "Enabled by policy.");
CanEnableStartupTask = false; CanEnableStartupTask = false;
break; break;
} }
} }
// Requires UI thread private async Task SetStartupTaskState(bool enabled)
private async Task SetStartupTaskStateAsync(bool enabled)
{ {
StartupTaskStatus status; StartupTaskStatus status;
if (enabled) if (enabled)
{ {
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread. status = await _startupTaskService.EnableStartupTask();
status = await _startupTaskService.EnableStartupTaskAsync().ConfigureAwait(true);
} }
else else
{ {
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread. _startupTaskService.DisableStartupTask();
await _startupTaskService.DisableStartupTaskAsync().ConfigureAwait(true); status = await _startupTaskService.GetStatus();
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
status = await _startupTaskService.GetStatusAsync().ConfigureAwait(true);
} }
SetStartupTaskPropertiesForStatus(status); SetStartupTaskPropertiesForStatus(status);
} }
// Requires UI thread
private async Task BrowseLogDirectoryAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var folder = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (folder != null)
{
LogDirectoryPath = folder;
}
}
} }
} }
@@ -1,15 +1,13 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities; using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models; using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.ComponentModel; using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.Input;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class KeyBindingViewModel : ObservableObject public class KeyBindingViewModel : ViewModelBase
{ {
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
@@ -18,11 +16,10 @@ namespace FluentTerminal.App.ViewModels.Settings
Model = keyBinding; Model = keyBinding;
Parent = parent; Parent = parent;
_dialogService = dialogService; _dialogService = dialogService;
EditCommand = new AsyncRelayCommand(EditAsync); EditCommand = new AsyncCommand(Edit);
DeleteCommand = new AsyncRelayCommand(DeleteAsync); DeleteCommand = new AsyncCommand(Delete);
} }
// Needs to be triggered from the UI thread
public event EventHandler Deleted; public event EventHandler Deleted;
public event EventHandler Edited; public event EventHandler Edited;
@@ -37,7 +34,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Meta != value) if (Model.Meta != value)
{ {
Model.Meta = value; Model.Meta = value;
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -50,7 +47,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Alt != value) if (Model.Alt != value)
{ {
Model.Alt = value; Model.Alt = value;
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -63,13 +60,13 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Ctrl != value) if (Model.Ctrl != value)
{ {
Model.Ctrl = value; Model.Ctrl = value;
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
public ICommand DeleteCommand { get; } public IAsyncCommand DeleteCommand { get; }
public ICommand EditCommand { get; } public IAsyncCommand EditCommand { get; }
public int Key public int Key
{ {
@@ -79,7 +76,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Key != value) if (Model.Key != value)
{ {
Model.Key = value; Model.Key = value;
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -94,15 +91,13 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Shift != value) if (Model.Shift != value)
{ {
Model.Shift = value; Model.Shift = value;
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
// Requires UI thread public async Task<bool> Edit()
public async Task<bool> EditAsync()
{ {
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var keyBinding = await _dialogService.ShowCreateKeyBindingDialog().ConfigureAwait(true); var keyBinding = await _dialogService.ShowCreateKeyBindingDialog().ConfigureAwait(true);
if (keyBinding != null) if (keyBinding != null)
@@ -121,12 +116,9 @@ namespace FluentTerminal.App.ViewModels.Settings
return false; return false;
} }
// Requires UI thread private async Task Delete()
private async Task DeleteAsync()
{ {
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread. var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this keybinding?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK) if (result == DialogButton.OK)
{ {
@@ -2,44 +2,46 @@
using FluentTerminal.App.Services.Utilities; using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models; using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using Microsoft.Toolkit.Mvvm.ComponentModel; using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.Input; using GalaSoft.MvvmLight.Command;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class KeyBindingsPageViewModel : ObservableObject public class KeyBindingsPageViewModel : ViewModelBase
{ {
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService; private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private IDictionary<string, ICollection<KeyBinding>> _keyBindings;
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, ITrayProcessCommunicationService trayProcessCommunicationService) public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, ITrayProcessCommunicationService trayProcessCommunicationService)
{ {
_settingsService = settingsService; _settingsService = settingsService;
_dialogService = dialogService; _dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_trayProcessCommunicationService = trayProcessCommunicationService; _trayProcessCommunicationService = trayProcessCommunicationService;
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync); RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
AddCommand = new AsyncRelayCommand<string>(AddAsync); AddCommand = new RelayCommand<string>(async command => await Add(command).ConfigureAwait(false));
Initialize(_settingsService.GetCommandKeyBindings()); Initialize(_settingsService.GetCommandKeyBindings());
} }
public ICommand AddCommand { get; } public RelayCommand<string> AddCommand { get; }
public ObservableCollection<KeyBindingsViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingsViewModel>(); public ObservableCollection<KeyBindingsViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingsViewModel>();
public ICommand RestoreDefaultsCommand { get; } public RelayCommand RestoreDefaultsCommand { get; }
private Task AddAsync(string command) private async Task Add(string command)
{ {
return KeyBindings.First(k => k.Command == command).ShowAddKeyBindingDialogAsync(); var keyBinding = KeyBindings.FirstOrDefault(k => k.Command == command);
await keyBinding?.ShowAddKeyBindingDialog();
} }
// Requires UI thread
private void ClearKeyBindings() private void ClearKeyBindings()
{ {
foreach (var keyBinding in KeyBindings) foreach (var keyBinding in KeyBindings)
@@ -50,16 +52,17 @@ namespace FluentTerminal.App.ViewModels.Settings
KeyBindings.Clear(); KeyBindings.Clear();
} }
// Requires UI thread
private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings) private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings)
{ {
_keyBindings = keyBindings;
ClearKeyBindings(); ClearKeyBindings();
foreach (var value in Enum.GetValues(typeof(Command))) foreach (var value in Enum.GetValues(typeof(Command)))
{ {
var command = (Command)value; var command = (Command)value;
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, I18N.Translate($"{nameof(Command)}.{command}"), true); var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, EnumHelper.GetEnumDescription(command), true);
foreach (var keyBinding in keyBindings[command.ToString()]) foreach (var keyBinding in _keyBindings[command.ToString()])
{ {
viewModel.Add(keyBinding); viewModel.Add(keyBinding);
} }
@@ -71,15 +74,12 @@ namespace FluentTerminal.App.ViewModels.Settings
private void OnEdited(string command, ICollection<KeyBinding> keyBindings) private void OnEdited(string command, ICollection<KeyBinding> keyBindings)
{ {
_settingsService.SaveKeyBindings(command, keyBindings); _settingsService.SaveKeyBindings(command, keyBindings);
_trayProcessCommunicationService.UpdateToggleWindowKeyBindingsAsync(); _trayProcessCommunicationService.UpdateToggleWindowKeyBindings();
} }
// Requires UI thread private async Task RestoreDefaults()
private async Task RestoreDefaultsAsync()
{ {
// ConfigureAwait(true) because we need to execute Initialize in the calling (UI) thread. var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the default keybindings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK) if (result == DialogButton.OK)
{ {
@@ -1,6 +1,6 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.Models; using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.ComponentModel; using GalaSoft.MvvmLight;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class KeyBindingsViewModel : ObservableObject public class KeyBindingsViewModel : ViewModelBase
{ {
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private bool _editable; private bool _editable;
@@ -37,30 +37,26 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool Editable public bool Editable
{ {
get => _editable; get => _editable;
set => SetProperty(ref _editable, value); set => Set(ref _editable, value);
} }
public ObservableCollection<KeyBindingViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingViewModel>(); public ObservableCollection<KeyBindingViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingViewModel>();
// Requires UI thread public async Task ShowAddKeyBindingDialog()
public async Task ShowAddKeyBindingDialogAsync()
{ {
var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService, this); var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService, this);
// ConfigureAwait(true) because we need to execute Add method in the calling (UI) thread. if (await newKeyBinding.Edit().ConfigureAwait(true))
if (await newKeyBinding.EditAsync().ConfigureAwait(true))
{ {
Add(newKeyBinding.Model); Add(newKeyBinding.Model);
} }
} }
// Requires UI thread
public void Clear() public void Clear()
{ {
KeyBindings.Clear(); KeyBindings.Clear();
} }
// Requires UI thread
public void Add(KeyBinding keyBinding) public void Add(KeyBinding keyBinding)
{ {
var viewModel = new KeyBindingViewModel(keyBinding, _dialogService, this); var viewModel = new KeyBindingViewModel(keyBinding, _dialogService, this);
@@ -1,20 +1,23 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models; using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using Microsoft.Toolkit.Mvvm.ComponentModel; using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.Input; using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class MousePageViewModel : ObservableObject public class MousePageViewModel : ViewModelBase
{ {
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IDefaultValueProvider _defaultValueProvider; private readonly IDefaultValueProvider _defaultValueProvider;
private readonly ApplicationSettings _applicationSettings; private readonly ApplicationSettings _applicationSettings;
private bool _editingMouseRightClickAction;
private bool _editingMouseMiddleClickAction;
public MousePageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider) public MousePageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider)
{ {
@@ -23,7 +26,7 @@ namespace FluentTerminal.App.ViewModels.Settings
_defaultValueProvider = defaultValueProvider; _defaultValueProvider = defaultValueProvider;
_applicationSettings = _settingsService.GetApplicationSettings(); _applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync); RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
} }
public bool CopyOnSelect public bool CopyOnSelect
@@ -35,7 +38,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_applicationSettings.CopyOnSelect = value; _applicationSettings.CopyOnSelect = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -45,11 +48,16 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.MouseRightClickAction; get => _applicationSettings.MouseRightClickAction;
set set
{ {
if (_applicationSettings.MouseRightClickAction != value) if (_applicationSettings.MouseRightClickAction != value && !_editingMouseRightClickAction)
{ {
_editingMouseRightClickAction = true;
_applicationSettings.MouseRightClickAction = value; _applicationSettings.MouseRightClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
RaisePropertyChanged(nameof(MouseRightClickNoneIsSelected));
RaisePropertyChanged(nameof(MouseRightClickContextMenuIsSelected));
RaisePropertyChanged(nameof(MouseRightClickPasteIsSelected));
_editingMouseRightClickAction = false;
} }
} }
} }
@@ -57,25 +65,19 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool MouseRightClickNoneIsSelected public bool MouseRightClickNoneIsSelected
{ {
get => MouseRightClickAction == MouseAction.None; get => MouseRightClickAction == MouseAction.None;
set { if (value) MouseRightClickAction = MouseAction.None; } set => MouseRightClickAction = MouseAction.None;
} }
public bool MouseRightClickContextMenuIsSelected public bool MouseRightClickContextMenuIsSelected
{ {
get => MouseRightClickAction == MouseAction.ContextMenu; get => MouseRightClickAction == MouseAction.ContextMenu;
set { if (value) MouseRightClickAction = MouseAction.ContextMenu; } set => MouseRightClickAction = MouseAction.ContextMenu;
} }
public bool MouseRightClickPasteIsSelected public bool MouseRightClickPasteIsSelected
{ {
get => MouseRightClickAction == MouseAction.Paste; get => MouseRightClickAction == MouseAction.Paste;
set { if (value) MouseRightClickAction = MouseAction.Paste; } set => MouseRightClickAction = MouseAction.Paste;
}
public bool MouseRightClickCopySelectionOrPasteIsSelected
{
get => MouseRightClickAction == MouseAction.CopySelectionOrPaste;
set { if (value) MouseRightClickAction = MouseAction.CopySelectionOrPaste; }
} }
public MouseAction MouseMiddleClickAction public MouseAction MouseMiddleClickAction
@@ -83,11 +85,16 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.MouseMiddleClickAction; get => _applicationSettings.MouseMiddleClickAction;
set set
{ {
if (_applicationSettings.MouseMiddleClickAction != value) if (_applicationSettings.MouseMiddleClickAction != value && !_editingMouseMiddleClickAction)
{ {
_editingMouseMiddleClickAction = true;
_applicationSettings.MouseMiddleClickAction = value; _applicationSettings.MouseMiddleClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings); _settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged(); RaisePropertyChanged();
RaisePropertyChanged(nameof(MouseMiddleClickNoneIsSelected));
RaisePropertyChanged(nameof(MouseMiddleClickContextMenuIsSelected));
RaisePropertyChanged(nameof(MouseMiddleClickPasteIsSelected));
_editingMouseMiddleClickAction = false;
} }
} }
} }
@@ -95,36 +102,26 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool MouseMiddleClickNoneIsSelected public bool MouseMiddleClickNoneIsSelected
{ {
get => MouseMiddleClickAction == MouseAction.None; get => MouseMiddleClickAction == MouseAction.None;
set { if (value) MouseMiddleClickAction = MouseAction.None; } set => MouseMiddleClickAction = MouseAction.None;
} }
public bool MouseMiddleClickContextMenuIsSelected public bool MouseMiddleClickContextMenuIsSelected
{ {
get => MouseMiddleClickAction == MouseAction.ContextMenu; get => MouseMiddleClickAction == MouseAction.ContextMenu;
set { if (value) MouseMiddleClickAction = MouseAction.ContextMenu; } set => MouseMiddleClickAction = MouseAction.ContextMenu;
} }
public bool MouseMiddleClickPasteIsSelected public bool MouseMiddleClickPasteIsSelected
{ {
get => MouseMiddleClickAction == MouseAction.Paste; get => MouseMiddleClickAction == MouseAction.Paste;
set { if (value) MouseMiddleClickAction = MouseAction.Paste; } set => MouseMiddleClickAction = MouseAction.Paste;
} }
public bool MouseMiddleClickCopySelectionOrPasteIsSelected public RelayCommand RestoreDefaultsCommand { get; }
{
get => MouseMiddleClickAction == MouseAction.CopySelectionOrPaste;
set { if (value) MouseMiddleClickAction = MouseAction.CopySelectionOrPaste; }
}
public ICommand RestoreDefaultsCommand { get; } private async Task RestoreDefaults()
// Requires UI thread
private async Task RestoreDefaultsAsync()
{ {
// ConfigureAwait(true) because we're setting some view-model properties afterwards. var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the mouse settings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK) if (result == DialogButton.OK)
{ {
@@ -1,16 +1,15 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.Models; using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class ProfilesPageViewModel : ObservableObject public class ProfilesPageViewModel : ViewModelBase
{ {
private readonly IDefaultValueProvider _defaultValueProvider; private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
@@ -80,9 +79,9 @@ namespace FluentTerminal.App.ViewModels.Settings
} }
} }
public ICommand CreateShellProfileCommand { get; } public RelayCommand CreateShellProfileCommand { get; }
public ICommand CloneCommand { get; } public RelayCommand<ShellProfileViewModel> CloneCommand { get; }
private void CreateShellProfile() private void CreateShellProfile()
{ {
@@ -91,8 +90,7 @@ namespace FluentTerminal.App.ViewModels.Settings
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
PreInstalled = false, PreInstalled = false,
Name = "New profile", Name = "New profile",
KeyBindings = new List<KeyBinding>(), KeyBindings = new List<KeyBinding>()
UseConPty = _settingsService.GetApplicationSettings().UseConPty
}; };
AddShellProfile(shellProfile); AddShellProfile(shellProfile);
@@ -100,18 +98,20 @@ namespace FluentTerminal.App.ViewModels.Settings
private void Clone(ShellProfileViewModel shellProfile) private void Clone(ShellProfileViewModel shellProfile)
{ {
var cloned = shellProfile.ProfileVm.Model.Clone(); var cloned = new ShellProfile(shellProfile.Model)
{
cloned.Id = Guid.NewGuid(); Id = Guid.NewGuid(),
cloned.PreInstalled = false; PreInstalled = false,
cloned.Name = $"Copy of {shellProfile.Name}"; Name = $"Copy of {shellProfile.Name}"
cloned.KeyBindings = new List<KeyBinding>(); };
AddShellProfile(cloned); AddShellProfile(cloned);
} }
private void AddShellProfile(ShellProfile shellProfile) private void AddShellProfile(ShellProfile shellProfile)
{ {
_settingsService.SaveShellProfile(shellProfile, true);
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider, true); var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider, true);
viewModel.EditCommand.Execute(null); viewModel.EditCommand.Execute(null);
viewModel.SetAsDefault += OnShellProfileSetAsDefault; viewModel.SetAsDefault += OnShellProfileSetAsDefault;
@@ -123,7 +123,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public ShellProfileViewModel SelectedShellProfile public ShellProfileViewModel SelectedShellProfile
{ {
get => _selectedShellProfile; get => _selectedShellProfile;
set => SetProperty(ref _selectedShellProfile, value); set => Set(ref _selectedShellProfile, value);
} }
public ObservableCollection<ShellProfileViewModel> ShellProfiles { get; } = new ObservableCollection<ShellProfileViewModel>(); public ObservableCollection<ShellProfileViewModel> ShellProfiles { get; } = new ObservableCollection<ShellProfileViewModel>();
@@ -1,121 +0,0 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class SshProfilesPageViewModel: ObservableObject
{
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private SshProfileViewModel _selectedShellProfile;
private readonly IApplicationView _applicationView;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IApplicationDataContainer _historyContainer;
public SshProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService,
IFileSystemService fileSystemService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService,
IApplicationDataContainer historyContainer)
{
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_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, _trayProcessCommunicationService, historyContainer, false);
viewModel.Deleted += OnSshProfileDeleted;
SshProfiles.Add(viewModel);
}
if (SshProfiles.Count == 0)
{
CreateSshProfile();
}
SelectedSshProfile = SshProfiles.First();
}
private void OnSshProfileDeleted(object sender, EventArgs e)
{
if (sender is SshProfileViewModel sshProfile)
{
if (SelectedSshProfile == sshProfile)
{
SelectedSshProfile = SshProfiles.First();
}
SshProfiles.Remove(sshProfile);
_settingsService.DeleteSshProfile(sshProfile.Id);
}
}
public ICommand CreateSshProfileCommand { get; }
public ICommand CloneCommand { get; }
public void CreateSshProfile()
{
var shellProfile = new SshProfile
{
Id = Guid.NewGuid(),
PreInstalled = false,
Name = "New SSH profile",
KeyBindings = new List<KeyBinding>(),
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault,
RequestConPty = _settingsService.GetApplicationSettings().UseConPty
};
AddSshProfile(shellProfile);
}
private void Clone(SshProfileViewModel shellProfile)
{
var cloned = (SshProfile) shellProfile.ProfileVm.Model.Clone();
cloned.Id = Guid.NewGuid();
cloned.PreInstalled = false;
cloned.Name = $"Copy of {shellProfile.Name}";
cloned.KeyBindings = new List<KeyBinding>();
foreach (KeyBinding keyBinding in cloned.KeyBindings)
{
keyBinding.Command = cloned.Id.ToString();
}
AddSshProfile(cloned);
}
private void AddSshProfile(SshProfile sshProfile)
{
var viewModel = new SshProfileViewModel(sshProfile, _settingsService, _dialogService, _fileSystemService,
_applicationView, _trayProcessCommunicationService, _historyContainer, true);
viewModel.EditCommand.Execute(null);
viewModel.Deleted += OnSshProfileDeleted;
SshProfiles.Add(viewModel);
SelectedSshProfile = viewModel;
}
public SshProfileViewModel SelectedSshProfile
{
get => _selectedShellProfile;
set => SetProperty(ref _selectedShellProfile, value);
}
public ObservableCollection<SshProfileViewModel> SshProfiles { get; } = new ObservableCollection<SshProfileViewModel>();
}
}
@@ -1,58 +1,57 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models; using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using Microsoft.Toolkit.Mvvm.ComponentModel; using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.Input; using GalaSoft.MvvmLight.Command;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class TerminalPageViewModel : ObservableObject public class TerminalPageViewModel : ViewModelBase
{ {
private readonly TerminalOptions _terminalOptions; private readonly TerminalOptions _terminalOptions;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IDefaultValueProvider _defaultValueProvider; private readonly IDefaultValueProvider _defaultValueProvider;
private bool _isEditingCursorStyle;
private bool _isEditingScrollBarStyle;
public bool BarIsSelected public bool BarIsSelected
{ {
get => CursorStyle == CursorStyle.Bar; get => CursorStyle == CursorStyle.Bar;
set { if (value) CursorStyle = CursorStyle.Bar; } set => CursorStyle = CursorStyle.Bar;
} }
public bool BlockIsSelected public bool BlockIsSelected
{ {
get => CursorStyle == CursorStyle.Block; get => CursorStyle == CursorStyle.Block;
set { if (value) CursorStyle = CursorStyle.Block; } set => CursorStyle = CursorStyle.Block;
} }
public bool UnderlineIsSelected public bool UnderlineIsSelected
{ {
get => CursorStyle == CursorStyle.Underline; get => CursorStyle == CursorStyle.Underline;
set { if (value) CursorStyle = CursorStyle.Underline; } set => CursorStyle = CursorStyle.Underline;
} }
public bool HiddenIsSelected public bool HiddenIsSelected
{ {
get => ScrollBarStyle == ScrollBarStyle.Hidden; get => ScrollBarStyle == ScrollBarStyle.Hidden;
set { if (value) ScrollBarStyle = ScrollBarStyle.Hidden; } set => ScrollBarStyle = ScrollBarStyle.Hidden;
} }
public bool AutoHidingIsSelected public bool AutoHidingIsSelected
{ {
get => ScrollBarStyle == ScrollBarStyle.AutoHiding; get => ScrollBarStyle == ScrollBarStyle.AutoHiding;
set { if (value) ScrollBarStyle = ScrollBarStyle.AutoHiding; } set => ScrollBarStyle = ScrollBarStyle.AutoHiding;
} }
public bool VisibleIsSelected public bool VisibleIsSelected
{ {
get => ScrollBarStyle == ScrollBarStyle.Visible; get => ScrollBarStyle == ScrollBarStyle.Visible;
set { if (value) ScrollBarStyle = ScrollBarStyle.Visible; } set => ScrollBarStyle = ScrollBarStyle.Visible;
} }
public bool CursorBlink public bool CursorBlink
@@ -64,21 +63,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_terminalOptions.CursorBlink = value; _terminalOptions.CursorBlink = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
}
}
}
public bool UseAcrylicBackground
{
get => _terminalOptions.UseAcrylicBackground;
set
{
if (_terminalOptions.UseAcrylicBackground != value)
{
_terminalOptions.UseAcrylicBackground = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged();
} }
} }
} }
@@ -92,21 +77,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_terminalOptions.FontFamily = value; _terminalOptions.FontFamily = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
}
}
}
public int FontWeight
{
get => _terminalOptions.FontWeight;
set
{
if (_terminalOptions.FontWeight != value)
{
_terminalOptions.FontWeight = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged();
} }
} }
} }
@@ -120,7 +91,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_terminalOptions.BackgroundOpacity = value; _terminalOptions.BackgroundOpacity = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -134,7 +105,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_terminalOptions.Padding = value; _terminalOptions.Padding = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -144,6 +115,7 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.ScrollBackLimit.ToString(); get => _terminalOptions.ScrollBackLimit.ToString();
set set
{ {
var oldValue = _terminalOptions.ScrollBackLimit;
if (uint.TryParse(value, out uint intValue)) if (uint.TryParse(value, out uint intValue))
{ {
if (intValue == uint.MinValue) if (intValue == uint.MinValue)
@@ -154,27 +126,26 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_terminalOptions.ScrollBackLimit = intValue; _terminalOptions.ScrollBackLimit = intValue;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
else else
{ {
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
public string WordSeparator public bool BoldText
{ {
get => WebViewSpecialCharEncoder.Decode(_terminalOptions.WordSeparator); get => _terminalOptions.BoldText;
set set
{ {
value = WebViewSpecialCharEncoder.Encode(value); if (_terminalOptions.BoldText != value)
if (_terminalOptions.WordSeparator != value)
{ {
_terminalOptions.WordSeparator = value; _terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
@@ -190,25 +161,30 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_terminalOptions.FontSize = value; _terminalOptions.FontSize = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
} }
} }
} }
public IEnumerable<int> Sizes { get; } public IEnumerable<int> Sizes { get; }
public ICommand RestoreDefaultsCommand { get; } public RelayCommand RestoreDefaultsCommand { get; }
private CursorStyle CursorStyle private CursorStyle CursorStyle
{ {
get => _terminalOptions.CursorStyle; get => _terminalOptions.CursorStyle;
set set
{ {
if (_terminalOptions.CursorStyle != value) if (_terminalOptions.CursorStyle != value && !_isEditingCursorStyle)
{ {
_isEditingCursorStyle = true;
_terminalOptions.CursorStyle = value; _terminalOptions.CursorStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
RaisePropertyChanged(nameof(BlockIsSelected));
RaisePropertyChanged(nameof(BarIsSelected));
RaisePropertyChanged(nameof(UnderlineIsSelected));
_isEditingCursorStyle = false;
} }
} }
} }
@@ -218,22 +194,23 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.ScrollBarStyle; get => _terminalOptions.ScrollBarStyle;
set set
{ {
if (_terminalOptions.ScrollBarStyle != value) if (_terminalOptions.ScrollBarStyle != value && !_isEditingScrollBarStyle)
{ {
_isEditingScrollBarStyle = true;
_terminalOptions.ScrollBarStyle = value; _terminalOptions.ScrollBarStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions); _settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged(); RaisePropertyChanged();
RaisePropertyChanged(nameof(HiddenIsSelected));
RaisePropertyChanged(nameof(AutoHidingIsSelected));
RaisePropertyChanged(nameof(VisibleIsSelected));
_isEditingScrollBarStyle = false;
} }
} }
} }
// Requires UI thread private async Task RestoreDefaults()
private async Task RestoreDefaultsAsync()
{ {
// ConfigureAwait(true) because we're setting some view-model properties afterwards. var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the default terminal options?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK) if (result == DialogButton.OK)
{ {
@@ -243,10 +220,9 @@ namespace FluentTerminal.App.ViewModels.Settings
ScrollBarStyle = defaults.ScrollBarStyle; ScrollBarStyle = defaults.ScrollBarStyle;
FontFamily = defaults.FontFamily; FontFamily = defaults.FontFamily;
FontSize = defaults.FontSize; FontSize = defaults.FontSize;
FontWeight = defaults.FontWeight; BoldText = defaults.BoldText;
BackgroundOpacity = defaults.BackgroundOpacity; BackgroundOpacity = defaults.BackgroundOpacity;
ScrollBackLimit = defaults.ScrollBackLimit.ToString(); ScrollBackLimit = defaults.ScrollBackLimit.ToString();
WordSeparator = defaults.WordSeparator;
} }
} }
@@ -256,10 +232,10 @@ namespace FluentTerminal.App.ViewModels.Settings
_dialogService = dialogService; _dialogService = dialogService;
_defaultValueProvider = defaultValueProvider; _defaultValueProvider = defaultValueProvider;
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync); RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name); Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
Sizes = Enumerable.Range(2, 72); Sizes = Enumerable.Range(1, 72);
_terminalOptions = _settingsService.GetTerminalOptions(); _terminalOptions = _settingsService.GetTerminalOptions();
} }
@@ -1,20 +1,14 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models; using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.Models.Messages;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.Toolkit.Mvvm.Messaging;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings namespace FluentTerminal.App.ViewModels.Settings
{ {
public class ThemesPageViewModel : ObservableObject, public class ThemesPageViewModel : ViewModelBase
IRecipient<TerminalOptionsChangedMessage>
{ {
private readonly IDefaultValueProvider _defaultValueProvider; private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
@@ -23,36 +17,30 @@ namespace FluentTerminal.App.ViewModels.Settings
private double _backgroundOpacity; private double _backgroundOpacity;
private readonly IThemeParserFactory _themeParserFactory; private readonly IThemeParserFactory _themeParserFactory;
private readonly IFileSystemService _fileSystemService; private readonly IFileSystemService _fileSystemService;
private readonly IImageFileSystemService _imageFileSystemService;
public event EventHandler<string> SelectedThemeBackgroundColorChanged; public event EventHandler<string> SelectedThemeBackgroundColorChanged;
public event EventHandler<ImageFile> SelectedThemeBackgroundImageChanged;
public event EventHandler<ThemeViewModel> SelectedThemeChanged;
public ThemesPageViewModel(ISettingsService settingsService, public ThemesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
IDialogService dialogService, IThemeParserFactory themeParserFactory, IFileSystemService fileSystemService)
IDefaultValueProvider defaultValueProvider,
IThemeParserFactory themeParserFactory,
IFileSystemService fileSystemService,
IImageFileSystemService imageFileSystemService)
{ {
_settingsService = settingsService; _settingsService = settingsService;
_dialogService = dialogService; _dialogService = dialogService;
_defaultValueProvider = defaultValueProvider; _defaultValueProvider = defaultValueProvider;
_themeParserFactory = themeParserFactory; _themeParserFactory = themeParserFactory;
_fileSystemService = fileSystemService; _fileSystemService = fileSystemService;
_imageFileSystemService = imageFileSystemService;
CreateThemeCommand = new RelayCommand(CreateTheme); CreateThemeCommand = new RelayCommand(CreateTheme);
ImportThemeCommand = new AsyncRelayCommand(ImportThemeAsync); ImportThemeCommand = new RelayCommand(ImportTheme);
CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme); CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme);
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
BackgroundOpacity = _settingsService.GetTerminalOptions().BackgroundOpacity; BackgroundOpacity = _settingsService.GetTerminalOptions().BackgroundOpacity;
var activeThemeId = _settingsService.GetCurrentThemeId(); var activeThemeId = _settingsService.GetCurrentThemeId();
foreach (var theme in _settingsService.GetThemes()) foreach (var theme in _settingsService.GetThemes())
{ {
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, false); var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService, false);
viewModel.Activated += OnThemeActivated; viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted; viewModel.Deleted += OnThemeDeleted;
@@ -64,18 +52,16 @@ namespace FluentTerminal.App.ViewModels.Settings
} }
SelectedTheme = Themes.First(t => t.IsActive); SelectedTheme = Themes.First(t => t.IsActive);
WeakReferenceMessenger.Default.RegisterAll(this);
} }
public ICommand CreateThemeCommand { get; } public RelayCommand CreateThemeCommand { get; }
public ICommand ImportThemeCommand { get; } public RelayCommand ImportThemeCommand { get; }
public ICommand CloneCommand { get; set; } public RelayCommand<ThemeViewModel> CloneCommand { get; set; }
public double BackgroundOpacity public double BackgroundOpacity
{ {
get => _backgroundOpacity; get => _backgroundOpacity;
set => SetProperty(ref _backgroundOpacity, value); set => Set(ref _backgroundOpacity, value);
} }
public ThemeViewModel SelectedTheme public ThemeViewModel SelectedTheme
@@ -86,17 +72,11 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_selectedTheme != null) if (_selectedTheme != null)
{ {
_selectedTheme.BackgroundChanged -= OnSelectedThemeBackgroundChanged; _selectedTheme.BackgroundChanged -= OnSelectedThemeBackgroundChanged;
_selectedTheme.BackgroundImageChanged -= OnSelectedThemeBackgroundImageChanged;
} }
Set(ref _selectedTheme, value);
SetProperty(ref _selectedTheme, value);
SelectedThemeChanged?.Invoke(this, _selectedTheme);
if (value != null) if (value != null)
{ {
_selectedTheme.BackgroundOpacity = BackgroundOpacity;
value.BackgroundChanged += OnSelectedThemeBackgroundChanged; value.BackgroundChanged += OnSelectedThemeBackgroundChanged;
value.BackgroundImageChanged += OnSelectedThemeBackgroundImageChanged;
} }
} }
} }
@@ -129,49 +109,28 @@ namespace FluentTerminal.App.ViewModels.Settings
AddTheme(theme); AddTheme(theme);
} }
// Requires UI thread private async void ImportTheme()
private async Task ImportThemeAsync()
{ {
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread. var file = await _fileSystemService.OpenFile(_themeParserFactory.SupportedFileTypes).ConfigureAwait(true);
var file = await _fileSystemService.OpenFileAsync(_themeParserFactory.SupportedFileTypes)
.ConfigureAwait(true);
if (file != null) if (file != null)
{ {
var parser = _themeParserFactory.GetParser(file.FileType); var parser = _themeParserFactory.GetParser(file.FileType);
if (parser == null) if (parser == null)
{ {
await _dialogService.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"), await _dialogService.ShowMessageDialogAsnyc("Import theme failed", "No suitable parser found", DialogButton.OK).ConfigureAwait(false);
I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
return; return;
} }
try try
{ {
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread. var theme = await parser.Parse(file.Name, file.Content).ConfigureAwait(true);
var exportedTheme = await parser.Import(file.Name, file.Content).ConfigureAwait(true);
if (!string.IsNullOrWhiteSpace(exportedTheme.EncodedImage)) AddTheme(theme);
{
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
var importedImage = await _imageFileSystemService
.ImportThemeImageAsync(exportedTheme.BackgroundImage, exportedTheme.EncodedImage)
.ConfigureAwait(true);
exportedTheme.BackgroundImage = importedImage;
}
var terminalTheme = new TerminalTheme(exportedTheme);
AddTheme(terminalTheme);
} }
catch (Exception exception) catch (Exception exception)
{ {
await _dialogService await _dialogService.ShowMessageDialogAsnyc("Import theme failed", exception.Message, DialogButton.OK).ConfigureAwait(false);
.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK)
.ConfigureAwait(false);
} }
} }
} }
@@ -180,7 +139,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{ {
_settingsService.SaveTheme(theme, true); _settingsService.SaveTheme(theme, true);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, true); var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, true);
viewModel.EditCommand.Execute(null); viewModel.EditCommand.Execute(null);
viewModel.Activated += OnThemeActivated; viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted; viewModel.Deleted += OnThemeDeleted;
@@ -220,19 +179,14 @@ namespace FluentTerminal.App.ViewModels.Settings
} }
} }
public void Receive(TerminalOptionsChangedMessage message) private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{ {
BackgroundOpacity = message.TerminalOptions.BackgroundOpacity; BackgroundOpacity = e.BackgroundOpacity;
} }
private void OnSelectedThemeBackgroundChanged(object sender, string e) private void OnSelectedThemeBackgroundChanged(object sender, string e)
{ {
SelectedThemeBackgroundColorChanged?.Invoke(this, e); SelectedThemeBackgroundColorChanged?.Invoke(this, e);
} }
private void OnSelectedThemeBackgroundImageChanged(object sender, ImageFile e)
{
SelectedThemeBackgroundImageChanged?.Invoke(this, e);
}
} }
} }
@@ -1,37 +1,40 @@
using FluentTerminal.App.Services; using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Settings; using FluentTerminal.App.ViewModels.Settings;
using Microsoft.Toolkit.Mvvm.ComponentModel; using GalaSoft.MvvmLight;
using System; using System;
namespace FluentTerminal.App.ViewModels namespace FluentTerminal.App.ViewModels
{ {
public class SettingsViewModel : ObservableObject public class SettingsViewModel : ViewModelBase
{ {
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService, public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService, ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IApplicationView applicationView, IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView applicationView)
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers,
IImageFileSystemService imageFileSystemService)
{ {
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, trayProcessCommunicationService); About = new AboutPageViewModel(settingsService, updateService, applicationView);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService, fileSystemService); KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, defaultValueProvider, trayProcessCommunicationService);
Profiles = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView); General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService);
Shell = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService); Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService);
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService, imageFileSystemService); Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService);
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider); Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService,
applicationView, trayProcessCommunicationService, containers.HistoryContainer);
} }
public event EventHandler Closed; public event EventHandler Closed;
public event EventHandler AboutPageRequested;
public GeneralPageViewModel General { get; } public GeneralPageViewModel General { get; }
public KeyBindingsPageViewModel KeyBindings { get; } public KeyBindingsPageViewModel KeyBindings { get; }
public ProfilesPageViewModel Profiles { get; } public ProfilesPageViewModel Shell { get; }
public TerminalPageViewModel Terminal { get; } public TerminalPageViewModel Terminal { get; }
public ThemesPageViewModel Themes { get; } public ThemesPageViewModel Themes { get; }
public MousePageViewModel Mouse { get; } public MousePageViewModel Mouse { get; }
public SshProfilesPageViewModel SshProfiles { get; } public AboutPageViewModel About { get; }
public void NavigateToAboutPage()
{
AboutPageRequested?.Invoke(this, EventArgs.Empty);
}
public void Close() public void Close()
{ {
@@ -1,111 +1,376 @@
using FluentTerminal.App.Services; 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;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System; using System;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentTerminal.App.ViewModels.Profiles;
using System.Windows.Input;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels namespace FluentTerminal.App.ViewModels
{ {
/// <summary> public class ShellProfileViewModel : ViewModelBase
/// Extends <see cref="ProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
/// </summary>
public class ShellProfileViewModel : ProfileViewModelBase<CommonProfileProviderViewModel>
{ {
#region Fields private readonly IDialogService _dialogService;
private readonly IFileSystemService _fileSystemService;
private readonly ISettingsService _settingsService;
private string _arguments;
private ShellProfile _fallbackProfile;
private bool _inEditMode;
private bool _isDefault;
private string _location;
private LineEndingStyle _lineEndingStyle;
private string _name;
private bool _isNew;
private TabTheme _selectedTabTheme;
private TerminalTheme _selectedTerminalTheme;
private string _workingDirectory;
private readonly IApplicationView _applicationView;
private readonly IDefaultValueProvider _defaultValueProvider; private readonly IDefaultValueProvider _defaultValueProvider;
#endregion Fields public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider, Boolean isNew)
{
Model = shellProfile;
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_defaultValueProvider = defaultValueProvider;
_isNew = isNew;
#region Properties _settingsService.ThemeAdded += OnThemeAdded;
_settingsService.ThemeDeleted += OnThemeDeleted;
private bool _isDefault; TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
TerminalThemes = new ObservableCollection<TerminalTheme>();
TerminalThemes.Add(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);
InitializeViewModelProperties(shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
SaveChangesCommand = new RelayCommand(SaveChanges);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
}
private void InitializeViewModelProperties(ShellProfile shellProfile)
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == shellProfile.TerminalThemeId);
Id = shellProfile.Id;
Name = shellProfile.Name;
Arguments = shellProfile.Arguments;
Location = shellProfile.Location;
WorkingDirectory = shellProfile.WorkingDirectory;
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == shellProfile.TabThemeId);
PreInstalled = shellProfile.PreInstalled;
LineEndingStyle = shellProfile.LineEndingTranslation;
KeyBindings.Clear();
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
}
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 event EventHandler Deleted;
public event EventHandler SetAsDefault;
public IAsyncCommand BrowseForCustomShellCommand { get; }
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
public IAsyncCommand CancelEditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public RelayCommand SaveChangesCommand { get; }
public RelayCommand SetDefaultCommand { get; }
public IAsyncCommand RestoreDefaultsCommand { get; }
public IAsyncCommand AddKeyboardShortcutCommand { get; }
public ObservableCollection<TabTheme> TabThemes { get; }
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ShellProfile Model { get; private set; }
public bool PreInstalled { get; private set; }
public string Arguments
{
get => _arguments;
set => Set(ref _arguments, value);
}
public Guid Id { get; private set; }
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
}
public bool IsDefault public bool IsDefault
{ {
get => _isDefault; get => _isDefault;
set => SetProperty(ref _isDefault, value); set => Set(ref _isDefault, value);
} }
#endregion Properties public string Location
#region Events
public event EventHandler SetAsDefault;
#endregion Events
#region Commands
public ICommand SetDefaultCommand { get; }
public ICommand RestoreDefaultsCommand { get; }
#endregion Commands
#region Constrcutor
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService,
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
IDefaultValueProvider defaultValueProvider, bool isNew) : base(shellProfile, settingsService, dialogService,
isNew)
{ {
_defaultValueProvider = defaultValueProvider; get => _location;
set => Set(ref _location, value);
ProfileVm = new CommonProfileProviderViewModel(settingsService, applicationView, fileSystemService,
shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync);
} }
#endregion Constrcutor public string Name
#region Methods
// Requires UI thread
private async Task RestoreDefaultsAsync()
{ {
if (InEditMode || !ProfileVm.PreInstalled) get => _name;
set => Set(ref _name, value);
}
public TabTheme SelectedTabTheme
{
get => _selectedTabTheme;
set
{
if (value != null)
{
Set(ref _selectedTabTheme, value);
}
}
}
public TerminalTheme SelectedTerminalTheme
{
get => _selectedTerminalTheme;
set
{
if (value != null)
{
Set(ref _selectedTerminalTheme, value);
}
}
}
public string WorkingDirectory
{
get => _workingDirectory;
set => Set(ref _workingDirectory, value);
}
public LineEndingStyle LineEndingStyle
{
get => _lineEndingStyle;
set => Set(ref _lineEndingStyle, value);
}
public bool DoNotModifyIsSelected
{
get => LineEndingStyle == LineEndingStyle.DoNotModify;
set { if (value) LineEndingStyle = LineEndingStyle.DoNotModify; }
}
public bool ToCRLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCRLF;
set { if (value) LineEndingStyle = LineEndingStyle.ToCRLF; }
}
public bool ToLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToLF;
set { if (value) LineEndingStyle = LineEndingStyle.ToLF; }
}
public bool ToCRIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCR;
set { if (value) LineEndingStyle = LineEndingStyle.ToCR; }
}
public void SaveChanges()
{
Model.Arguments = Arguments;
Model.Location = Location;
Model.Name = Name;
Model.WorkingDirectory = WorkingDirectory;
Model.TabThemeId = SelectedTabTheme.Id;
Model.TerminalThemeId = SelectedTerminalTheme.Id;
Model.LineEndingTranslation = _lineEndingStyle;
Model.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
_settingsService.SaveShellProfile(Model);
KeyBindings.Editable = false;
InEditMode = false;
_isNew = false;
}
private async Task RestoreDefaults()
{
if (InEditMode || !PreInstalled)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
// ConfigureAwait(true) because we need to execute Initialize method in the calling (UI) thread. var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore this profile?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK) if (result == DialogButton.OK)
{ {
var profile = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id.Equals(Id)); Model = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id == Model.Id);
InitializeViewModelProperties(Model);
if (profile == null) _settingsService.SaveShellProfile(Model);
{
// Should not happen ever, but...
return;
}
ProfileVm.Model = profile;
Initialize(profile);
SettingsService.SaveShellProfile(profile);
} }
} }
protected override bool CanDelete() public Task AddKeyboardShortcut()
{ {
return !ProfileVm.PreInstalled; 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 = new ShellProfile
{
Arguments = Arguments,
Location = Location,
Name = Name,
WorkingDirectory = WorkingDirectory,
TabThemeId = SelectedTabTheme.Id,
TerminalThemeId = SelectedTerminalTheme.Id,
LineEndingTranslation = _lineEndingStyle,
KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList()
};
if (!_fallbackProfile.Equals(changedProfile))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Arguments = _fallbackProfile.Arguments;
Location = _fallbackProfile.Location;
Name = _fallbackProfile.Name;
WorkingDirectory = _fallbackProfile.WorkingDirectory;
LineEndingStyle = _fallbackProfile.LineEndingTranslation;
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TerminalThemeId);
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TabThemeId);
KeyBindings.KeyBindings.Clear();
foreach (var keyBinding in Model.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
}
}
private bool CanDelete()
{
return !Model.PreInstalled;
}
private async Task Delete()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this theme?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Deleted?.Invoke(this, EventArgs.Empty);
}
}
private void Edit()
{
// todo write copy ctor
_fallbackProfile = new ShellProfile
{
Arguments = Model.Arguments,
LineEndingTranslation = Model.LineEndingTranslation,
Location = Model.Location,
Name = Model.Name,
WorkingDirectory = Model.WorkingDirectory,
TabThemeId = Model.TabThemeId,
TerminalThemeId = Model.TerminalThemeId
};
KeyBindings.Editable = true;
InEditMode = true;
} }
private void SetDefault() private void SetDefault()
{ {
SetAsDefault?.Invoke(this, EventArgs.Empty); SetAsDefault?.Invoke(this, EventArgs.Empty);
} }
#endregion Methods
} }
} }
@@ -0,0 +1,32 @@
using FluentTerminal.App.Services.Dialogs;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels
{
public class SshConnectionInfoViewModel : ViewModelBase, ISshConnectionInfo
{
private string _host = string.Empty;
public string Host
{
get => _host;
set => Set(ref _host, value);
}
private ushort _port = 22;
public ushort Port
{
get => _port;
set => Set(ref _port, value);
}
private string _username = string.Empty;
public string Username
{
get => _username;
set => Set(ref _username, value);
}
}
}
@@ -1,36 +0,0 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Extends <see cref="ProfileViewModelBase{T}"/>, and doesn't implement any additional logic because in
/// case of SSH profiles no additional logic is needed.
/// </summary>
public class SshProfileViewModel : ProfileViewModelBase<SshConnectViewModel>
{
#region Constructor
public SshProfileViewModel(SshProfile sshProfile, ISettingsService settingsService,
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService,
IApplicationDataContainer historyContainer, bool isNew) : base(sshProfile, settingsService, dialogService,
isNew)
{
ProfileVm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
fileSystemService, sshProfile);
}
#endregion Constructor
#region Methods
protected override bool CanDelete()
{
return true;
}
#endregion Methods
}
}
Loaded 100 of 474 files, more files were not shown because too many files have changed in this diff. Show more