Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b08fb11a78 | ||
|
|
f5c91c10e3 | ||
|
|
223fafcf1c | ||
|
|
cbdf629b49 | ||
|
|
614e86f38a | ||
|
|
1519fc1f1a | ||
|
|
9eb31ec872 | ||
|
|
5c568e6c33 | ||
|
|
b07c56247a | ||
|
|
ad82c96d8c | ||
|
|
2bca8b345f | ||
|
|
6c2ee1bfff | ||
|
|
83ead9a1d4 | ||
|
|
3b78de25a4 | ||
|
|
e36704b2b1 | ||
|
|
1fd253ff6b | ||
|
|
3ed69a048b | ||
|
|
43ed08e0e2 | ||
|
|
e89721130b | ||
|
|
7cbcce3404 | ||
|
|
708152ee19 | ||
|
|
26317d9ed2 | ||
|
|
d4de37fd15 | ||
|
|
88fdd2fec5 | ||
|
|
3493bbcf17 | ||
|
|
7ffa71d254 | ||
|
|
aa21efb86b | ||
|
|
54548564dd | ||
|
|
1f3dcc3332 | ||
|
|
dfb2de91f9 | ||
|
|
33c2fa07b8 | ||
|
|
3bf35c4d5d | ||
|
|
912dddaa6c | ||
|
|
bad0f85f6b | ||
|
|
b46dcb193f | ||
|
|
2c0aa31be7 | ||
|
|
01a672130c | ||
|
|
a1a37d33f9 | ||
|
|
48216e342b | ||
|
|
1a442ff002 | ||
|
|
dbe4aad8ea | ||
|
|
8d9571f5d8 | ||
|
|
93939d08bb | ||
|
|
219e342315 | ||
|
|
e11556bd9a | ||
|
|
917cacb18b | ||
|
|
b154c7b5aa | ||
|
|
c8ca8e55df | ||
|
|
39cecde560 | ||
|
|
c9c029f821 | ||
|
|
ecd40f5c43 | ||
|
|
d14236d502 | ||
|
|
5eecbc6718 | ||
|
|
09e4381ea7 | ||
|
|
cc991c6761 | ||
|
|
defec12c04 | ||
|
|
ef1c5fcfc5 | ||
|
|
0d12bf6b4a | ||
|
|
2e432967af | ||
|
|
8e53a6909f | ||
|
|
38b3e4ec94 | ||
|
|
6c00c1f497 | ||
|
|
121b3d6dc1 | ||
|
|
d2eed2b4fc | ||
|
|
5decbe9b86 | ||
|
|
91ba648095 | ||
|
|
919b4930b9 | ||
|
|
fb3c76184c | ||
|
|
d72a64ce67 | ||
|
|
07248bd28d | ||
|
|
37c948c9ac | ||
|
|
98b6e99743 | ||
|
|
0ffdf236a1 | ||
|
|
3f78aff86a | ||
|
|
601cca612c | ||
|
|
901cbda824 | ||
|
|
afb50f1bdd | ||
|
|
08877785aa | ||
|
|
71282eb65d | ||
|
|
edfb890638 | ||
|
|
e88fb78e9d | ||
|
|
93e52f3209 | ||
|
|
3af29314e7 | ||
|
|
8b18773da9 | ||
|
|
8d710fe6d7 | ||
|
|
010f3fdd7e | ||
|
|
0749d9e17d | ||
|
|
8a1ebd89ad | ||
|
|
594ebdd36a | ||
|
|
90c299da83 | ||
|
|
02a695056a | ||
|
|
50a3a7c422 | ||
|
|
3f699f40e3 | ||
|
|
b87ec9f348 | ||
|
|
79dda1d4fd | ||
|
|
5467bafaed | ||
|
|
ac3190111d | ||
|
|
298b402d5d | ||
|
|
a7b734b3bf | ||
|
|
3dd95e994f | ||
|
|
96e32fcc96 | ||
|
|
04a50e8152 | ||
|
|
e89853b6b5 | ||
|
|
c6bb9c9e28 | ||
|
|
d027ab9a02 | ||
|
|
13321cbc1a | ||
|
|
9a5af6437f | ||
|
|
a1967edb1d | ||
|
|
5900c51e70 | ||
|
|
6209727037 | ||
|
|
df15cb2231 | ||
|
|
eb2a44d2de | ||
|
|
e4fff8f638 | ||
|
|
80ec667f8b | ||
|
|
112c8c7c6c | ||
|
|
45338d00b8 | ||
|
|
04ed5d85e5 | ||
|
|
96ac5cd49f | ||
|
|
c70061995f | ||
|
|
501c5e20d8 | ||
|
|
5cc7d813a8 | ||
|
|
a0462ef502 | ||
|
|
1b1e29cbfe | ||
|
|
94855b7487 | ||
|
|
8ec0d58065 | ||
|
|
4476fe3061 | ||
|
|
4dbc75f540 | ||
|
|
a82c2a59f5 | ||
|
|
00b250c1b2 | ||
|
|
4ec9f0a5a6 | ||
|
|
5f82724039 | ||
|
|
6c65258439 | ||
|
|
f23493e3c8 | ||
|
|
de07e277f1 | ||
|
|
5c52810149 | ||
|
|
05a5ad56ea | ||
|
|
c51ffaa6e9 | ||
|
|
288b661611 | ||
|
|
32ab18f5f9 | ||
|
|
fddbd12820 | ||
|
|
563a7071ea | ||
|
|
d2c89ccb08 | ||
|
|
b5e8ce7639 | ||
|
|
6829c4c7f3 | ||
|
|
09bd970673 | ||
|
|
af61c83949 | ||
|
|
5ed24f85cf | ||
|
|
763a071488 | ||
|
|
bdb4d57d62 | ||
|
|
1be5fecf41 | ||
|
|
14dfb8b26a | ||
|
|
7284ce7f52 | ||
|
|
ac9f3c2a0d | ||
|
|
9dfab2223f | ||
|
|
c15da646fc | ||
|
|
2a47555987 | ||
|
|
ccf75bd45f | ||
|
|
d9b0b86cd5 | ||
|
|
d157b68384 | ||
|
|
0733381cd0 | ||
|
|
1870209878 | ||
|
|
565b3c3f69 | ||
|
|
7cdda79d67 | ||
|
|
a0f57ab4a3 | ||
|
|
edee7628d0 | ||
|
|
d06ad45423 | ||
|
|
9cf6a61e0f | ||
|
|
0ea4722dc8 | ||
|
|
39960dd755 | ||
|
|
8e5d3dea0a | ||
|
|
973b1a4963 | ||
|
|
dbd8632076 | ||
|
|
fffa7ebcb9 | ||
|
|
ded043db1d | ||
|
|
61ad64519b | ||
|
|
ef7f909d26 | ||
|
|
f2618fa940 | ||
|
|
363f885aea | ||
|
|
ed4a5adb16 | ||
|
|
82a3532e90 | ||
|
|
15d713191c | ||
|
|
575022139a | ||
|
|
b9686ac4ec | ||
|
|
43e4f8290e | ||
|
|
93387ebc57 | ||
|
|
54a9d7ce0d | ||
|
|
44b306ac20 | ||
|
|
14e1e0e3d3 | ||
|
|
136ba3c54f | ||
|
|
1a0c51c6d4 | ||
|
|
363018a43f | ||
|
|
3dcfaa94e9 | ||
|
|
05104e450e | ||
|
|
37f20e70bb | ||
|
|
e5a39fbe0d | ||
|
|
5c2f3e6f95 | ||
|
|
b3e831e3e6 | ||
|
|
31ff544379 | ||
|
|
7c4f62d16b | ||
|
|
f4b4623dab | ||
|
|
a32a86e15f | ||
|
|
6b64d3985d | ||
|
|
ed9c9ad6a6 | ||
|
|
bece957ea9 | ||
|
|
4e83e66a85 | ||
|
|
57d86a9723 | ||
|
|
8b6aae7bda | ||
|
|
2d204e48ab | ||
|
|
38a7b71fa8 | ||
|
|
95c94e3820 | ||
|
|
1920fdf35c | ||
|
|
9f3c46c366 | ||
|
|
221a2c2c6f | ||
|
|
bde65fa865 | ||
|
|
d52860e90a | ||
|
|
b85be454ce | ||
|
|
1f3dd72214 | ||
|
|
c4fcd986c0 | ||
|
|
ea21d37089 | ||
|
|
2f133720cc | ||
|
|
250c90fa02 | ||
|
|
7e2c5d2f45 | ||
|
|
9fed3fb192 | ||
|
|
0d85f933c7 | ||
|
|
f235ddf0f1 | ||
|
|
887cd45b4c | ||
|
|
aac2f220d6 | ||
|
|
90f15cc1e3 | ||
|
|
292b66af4d | ||
|
|
b6d835984c | ||
|
|
369b5ddacf | ||
|
|
6be29f49d7 | ||
|
|
8131140cb0 | ||
|
|
304314230c | ||
|
|
0ce605f122 | ||
|
|
d4aadd7dd4 | ||
|
|
3cf3e0a330 | ||
|
|
ff2cfc36b6 | ||
|
|
b118c66959 | ||
|
|
131e3536f1 | ||
|
|
b47b544065 | ||
|
|
b13c40e7e8 | ||
|
|
4e47fcef72 | ||
|
|
8acf618fd4 | ||
|
|
49e09f3f8b | ||
|
|
9bf8284d9f | ||
|
|
0f8aac81de | ||
|
|
abec73b868 | ||
|
|
86486c99f2 | ||
|
|
d9b7052e87 | ||
|
|
0efe105321 | ||
|
|
ed0dad960d | ||
|
|
2f58ed63de | ||
|
|
cc7e069f11 | ||
|
|
94a0eac58f | ||
|
|
9af0556d6c | ||
|
|
b14ca2c4ed | ||
|
|
89c1a5a614 | ||
|
|
3e32f611e8 | ||
|
|
07e65ce2bf | ||
|
|
f2a48487f1 | ||
|
|
c9d0cc7368 | ||
|
|
eecc2e8243 | ||
|
|
5c1896cd3b | ||
|
|
e0cde7fe0f |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# Git will handle the files in whatever way it thinks is best.
|
||||
# Overwrites global core.autocrlf git setting
|
||||
* text=auto
|
||||
@@ -292,3 +292,5 @@ __pycache__/
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
!FluentTerminal.App/MoshExecutables/*
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
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\LibraryFolder\Background\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\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
|
||||
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
|
||||
{
|
||||
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowCreateKeyBindingDialog();
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var title = string.Empty;
|
||||
var content = _fixture.Create<string>();
|
||||
var buttons = _fixture.CreateMany<DialogButton>(2);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var title = _fixture.Create<string>();
|
||||
var content = string.Empty;
|
||||
var buttons = _fixture.CreateMany<DialogButton>(2);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
{
|
||||
var title = _fixture.Create<string>();
|
||||
var content = _fixture.Create<string>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var content = _fixture.Create<string>();
|
||||
var buttons = _fixture.CreateMany<DialogButton>(2);
|
||||
var messageDialog = new Mock<IMessageDialog>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
@@ -86,11 +86,24 @@ namespace FluentTerminal.App.Services.Test
|
||||
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
|
||||
{
|
||||
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
|
||||
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>);
|
||||
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowProfileSelectionDialogAsync();
|
||||
|
||||
shellProfileSelectionDialog.Verify(x => x.SelectProfile(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowInputDialogAsync_UsesIInputDialogSetTitle()
|
||||
{
|
||||
var title = "title";
|
||||
var inputDialog = new Mock<IInputDialog>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowInputDialogAsync(title);
|
||||
|
||||
inputDialog.Verify(x => x.SetTitle(title), Times.Once);
|
||||
inputDialog.Verify(x => x.GetInput(), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
@@ -11,13 +11,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.4.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="Moq" Version="4.8.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
<PackageReference Include="AutoFixture" Version="4.11.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
|
||||
<PackageReference Include="Moq" Version="4.13.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -31,4 +34,11 @@
|
||||
<ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" />
|
||||
</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>
|
||||
@@ -20,7 +20,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
[Fact]
|
||||
public void RegisterCommandHandler_HandlerIsNull_ThrowsArgumentNullException()
|
||||
{
|
||||
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
|
||||
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
|
||||
Action handler = null;
|
||||
var keyboardCommandService = new KeyboardCommandService();
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
[Fact]
|
||||
public void RegisterCommandHandler_CommandAlreadyRegisted_ThrowsInvalidOperationException()
|
||||
{
|
||||
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
|
||||
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);
|
||||
@@ -41,11 +41,46 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
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]
|
||||
public void SendCommand_CommandIsRegistered_HandlerGetsInvoked()
|
||||
{
|
||||
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
|
||||
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
|
||||
var handlerCalled = false;
|
||||
var keyboardCommandService = new KeyboardCommandService();
|
||||
keyboardCommandService.RegisterCommandHandler(command, () => handlerCalled = true);
|
||||
@@ -58,12 +93,23 @@ namespace FluentTerminal.App.Services.Test
|
||||
[Fact]
|
||||
public void SendCommand_CommandIsNotRegisted_KeyNotFoundExceptionIsThrown()
|
||||
{
|
||||
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
|
||||
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
|
||||
var keyboardCommandService = new KeyboardCommandService();
|
||||
|
||||
Action invoke = () => keyboardCommandService.SendCommand(command);
|
||||
|
||||
invoke.Should().Throw<KeyNotFoundException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendCommand_CommandIsToggleWindow_ShouldNotThrow()
|
||||
{
|
||||
var command = nameof(Command.ToggleWindow);
|
||||
var keyboardCommandService = new KeyboardCommandService();
|
||||
|
||||
Action invoke = () => keyboardCommandService.SendCommand(command);
|
||||
|
||||
invoke.Should().NotThrow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using AutoFixture;
|
||||
using FluentAssertions;
|
||||
using FluentTerminal.App.Services.Implementation;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using Moq;
|
||||
@@ -9,6 +10,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using FluentTerminal.Models.Messages;
|
||||
using GalaSoft.MvvmLight.Messaging;
|
||||
using Xunit;
|
||||
|
||||
namespace FluentTerminal.App.Services.Test
|
||||
@@ -158,7 +161,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
};
|
||||
var currentThemeChangedEventInvoked = false;
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
|
||||
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
|
||||
message => currentThemeChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveCurrentThemeId(currentThemeId);
|
||||
|
||||
@@ -205,7 +209,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
|
||||
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
|
||||
message => currentThemeChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveTheme(theme);
|
||||
|
||||
@@ -362,7 +367,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.TerminalOptionsChanged += (s, e) => terminalOptionsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<TerminalOptionsChangedMessage>(this,
|
||||
message => terminalOptionsChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveTerminalOptions(terminalOptions);
|
||||
|
||||
@@ -429,7 +435,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.ApplicationSettingsChanged += (s, e) => applicationSettingsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this,
|
||||
a => applicationSettingsChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveApplicationSettings(applicationSettings);
|
||||
|
||||
@@ -439,12 +446,12 @@ namespace FluentTerminal.App.Services.Test
|
||||
[Fact]
|
||||
public void GetKeyBindings_KeyBindingsInKeyBindingsContainer_ReturnsKeyBindingsFromKeyBindingsContainer()
|
||||
{
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(5);
|
||||
var commands = _fixture.CreateMany<Command>(5);
|
||||
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
|
||||
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
|
||||
foreach (var keyBinding in keyBindings)
|
||||
foreach (var command in commands)
|
||||
{
|
||||
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(keyBinding.Command.ToString(), null)).Returns(new Collection<KeyBinding> { keyBinding });
|
||||
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null)).Returns(new Collection<KeyBinding> { });
|
||||
}
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
@@ -456,24 +463,20 @@ namespace FluentTerminal.App.Services.Test
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
var result = settingsService.GetKeyBindings();
|
||||
var result = settingsService.GetCommandKeyBindings();
|
||||
|
||||
foreach (var keyBinding in keyBindings)
|
||||
foreach (var command in commands)
|
||||
{
|
||||
keyBindingsContainer.Verify(x => x.ReadValueFromJson<Collection<KeyBinding>>(keyBinding.Command.ToString(), null), Times.Once);
|
||||
keyBindingsContainer.Verify(x => x.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null), Times.Once);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetKeyBindings_KeyBindingsNotInKeyBindingsContainer_ReturnsKeyBindingsFromDefaultValueProvider()
|
||||
{
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(5);
|
||||
var commands = _fixture.CreateMany<Command>(5);
|
||||
var defaultValueProvider = new Mock<IDefaultValueProvider>();
|
||||
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
|
||||
foreach (var keyBinding in keyBindings)
|
||||
{
|
||||
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(keyBinding.Command.ToString(), null)).Returns(new Collection<KeyBinding> { keyBinding });
|
||||
}
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
KeyBindings = keyBindingsContainer.Object,
|
||||
@@ -484,14 +487,10 @@ namespace FluentTerminal.App.Services.Test
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
|
||||
|
||||
var result = settingsService.GetKeyBindings();
|
||||
var result = settingsService.GetCommandKeyBindings();
|
||||
|
||||
foreach (Command command in Enum.GetValues(typeof(Command)))
|
||||
{
|
||||
if (keyBindings.Select(x => x.Command).Contains(command))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
defaultValueProvider.Verify(x => x.GetDefaultKeyBindings(command), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -513,7 +512,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
settingsService.SaveKeyBindings(command, keyBindings);
|
||||
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
|
||||
|
||||
keyBindingsContainer.Verify(x => x.WriteValueAsJson(command.ToString(), keyBindings), Times.Once);
|
||||
}
|
||||
@@ -535,9 +534,10 @@ namespace FluentTerminal.App.Services.Test
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
|
||||
message => keyBindingsChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveKeyBindings(command, keyBindings);
|
||||
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
|
||||
|
||||
keyBindingsChangedEventInvoked.Should().BeTrue();
|
||||
}
|
||||
@@ -581,7 +581,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
|
||||
message => keyBindingsChangedEventInvoked = true);
|
||||
|
||||
settingsService.ResetKeyBindings();
|
||||
|
||||
@@ -656,7 +657,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
var result = settingsService.GetShellProfiles();
|
||||
|
||||
result.Should().BeEquivalentTo(shellProfiles);
|
||||
result.Select(p => p.EqualTo(shellProfiles.FirstOrDefault(x => x.Id == p.Id)).Should().BeTrue());
|
||||
shellProfilesContainer.Verify(x => x.GetAll(), Times.Once);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Collections;
|
||||
using Xunit;
|
||||
|
||||
namespace FluentTerminal.App.Services.Test
|
||||
@@ -28,24 +29,26 @@ namespace FluentTerminal.App.Services.Test
|
||||
{
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
{
|
||||
[Command.ToggleWindow] = keyBindings.ToList()
|
||||
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
|
||||
});
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
var response = _fixture.Create<CreateTerminalResponse>();
|
||||
var responseMessage = new Dictionary<string, string>
|
||||
var responseMessage = new ValueSet
|
||||
{
|
||||
[MessageKeys.Type] = _fixture.Create<string>(),
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(response)
|
||||
};
|
||||
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
|
||||
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<ValueSet>())).Returns(Task.FromResult(responseMessage));
|
||||
var terminalSize = _fixture.Create<TerminalSize>();
|
||||
var shellProfile = _fixture.Create<ShellProfile>();
|
||||
var sessionType = _fixture.Create<SessionType>();
|
||||
var id = _fixture.Create<byte>();
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile);
|
||||
var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
|
||||
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
@@ -53,13 +56,13 @@ namespace FluentTerminal.App.Services.Test
|
||||
[Fact]
|
||||
public async Task ResizeTerminal_Default_SendsResizeTerminalRequest()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var terminalSize = _fixture.Create<TerminalSize>();
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
{
|
||||
[Command.ToggleWindow] = keyBindings.ToList()
|
||||
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
|
||||
});
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
@@ -67,38 +70,38 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
await trayProcessCommunicationService.ResizeTerminal(terminalId, terminalSize);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(ResizeTerminalRequest))), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteText_Default_SendsWriteTextRequest()
|
||||
public async Task Write_Default_SendsWriteDataRequest()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var text = _fixture.Create<string>();
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var data = _fixture.Create<byte[]>();
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
{
|
||||
[Command.ToggleWindow] = keyBindings.ToList()
|
||||
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
|
||||
});
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
await trayProcessCommunicationService.WriteText(terminalId, text);
|
||||
await trayProcessCommunicationService.Write(terminalId, data);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteTextRequest))), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseTerminal_Default_SendsTerminalExitedRequest()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
{
|
||||
[Command.ToggleWindow] = keyBindings.ToList()
|
||||
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
|
||||
});
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
@@ -106,66 +109,59 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
await trayProcessCommunicationService.CloseTerminal(terminalId);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(TerminalExitedRequest))), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnMessageReceived_TerminalExitedRequest_InvokesTerminalExitedEvent()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var receivedTerminalId = 0;
|
||||
var terminalExitedEventCalled = false;
|
||||
var request = new TerminalExitedRequest
|
||||
var request = new TerminalExitedRequest(_fixture.Create<byte>(), _fixture.Create<int>());
|
||||
|
||||
var receivedExitStatus = (TerminalExitStatus) null;
|
||||
|
||||
var message = new ValueSet
|
||||
{
|
||||
TerminalId = terminalId
|
||||
};
|
||||
var message = new Dictionary<string, string>
|
||||
{
|
||||
[MessageKeys.Type] = nameof(TerminalExitedRequest),
|
||||
[MessageKeys.Type] = (byte) MessageIdentifiers.TerminalExitedRequest,
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
|
||||
};
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
{
|
||||
[Command.ToggleWindow] = keyBindings.ToList()
|
||||
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
|
||||
});
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
trayProcessCommunicationService.TerminalExited += (s, e) =>
|
||||
trayProcessCommunicationService.TerminalExited += (s, status) =>
|
||||
{
|
||||
terminalExitedEventCalled = true;
|
||||
receivedTerminalId = e;
|
||||
receivedExitStatus = status;
|
||||
};
|
||||
|
||||
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
|
||||
|
||||
terminalExitedEventCalled.Should().BeTrue();
|
||||
receivedTerminalId.Should().Be(terminalId);
|
||||
receivedExitStatus.Should().NotBeNull();
|
||||
receivedExitStatus.TerminalId.Should().Be(request.TerminalId);
|
||||
receivedExitStatus.ExitCode.Should().Be(request.ExitCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var output = _fixture.Create<string>();
|
||||
var receivedOutput = string.Empty;
|
||||
var request = new DisplayTerminalOutputRequest
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var output = _fixture.Create<byte[]>();
|
||||
var receivedOutput = default(byte[]);
|
||||
var message = new ValueSet
|
||||
{
|
||||
TerminalId = terminalId,
|
||||
Output = output
|
||||
};
|
||||
var message = new Dictionary<string, string>
|
||||
{
|
||||
[MessageKeys.Type] = nameof(DisplayTerminalOutputRequest),
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
|
||||
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
|
||||
[MessageKeys.TerminalId] = terminalId,
|
||||
[MessageKeys.Content] = output
|
||||
};
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
{
|
||||
[Command.ToggleWindow] = keyBindings.ToList()
|
||||
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
|
||||
});
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
@@ -177,7 +173,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
|
||||
|
||||
receivedOutput.Should().Be(output);
|
||||
receivedOutput.Should().BeEquivalentTo(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ namespace FluentTerminal.App.Services.Adapters
|
||||
_applicationDataContainer.Values.Remove(key);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_applicationDataContainer.Values.Clear();
|
||||
}
|
||||
|
||||
public IEnumerable<object> GetAll()
|
||||
{
|
||||
return _applicationDataContainer.Values.Select(x => x.Value);
|
||||
|
||||
@@ -7,5 +7,7 @@
|
||||
public IApplicationDataContainer Themes { get; set; }
|
||||
public IApplicationDataContainer KeyBindings { get; set; }
|
||||
public IApplicationDataContainer ShellProfiles { get; set; }
|
||||
public IApplicationDataContainer SshProfiles { get; set; }
|
||||
public IApplicationDataContainer HistoryContainer { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,11 @@
|
||||
public const string ThemesContainerName = "Themes";
|
||||
public const string KeyBindingsContainerName = "KeyBindings";
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface IAboutDialog
|
||||
{
|
||||
Task ShowAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ICustomCommandDialog
|
||||
{
|
||||
Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface IInputDialog
|
||||
{
|
||||
void SetTitle(string title);
|
||||
Task<string> GetInput();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ISshConnectionInfoDialog
|
||||
{
|
||||
Task<SshProfile> GetSshConnectionInfoAsync(SshProfile input = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ISshProfileSelectionDialog
|
||||
{
|
||||
SshProfile SelectedProfile { get; }
|
||||
|
||||
Task<SshProfile> SelectProfile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using Windows.UI.Xaml;
|
||||
|
||||
namespace FluentTerminal.App.Services.EventArgs
|
||||
{
|
||||
public class NewTabRequestedEventArgs : System.EventArgs
|
||||
{
|
||||
public DragEventArgs DragEventArgs { get; set; }
|
||||
public int Position { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.Services.EventArgs
|
||||
{
|
||||
public class NewWindowRequestedEventArgs : System.EventArgs
|
||||
{
|
||||
public Models.Enums.NewWindowAction Action { get; set; }
|
||||
public Guid ProfileId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
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,8 +6,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="PListNet" Version="2.0.3" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.6.0" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="PListNet" Version="3.0.0" />
|
||||
<PackageReference Include="RestSharp" Version="106.6.10" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IAppServiceConnection
|
||||
{
|
||||
event EventHandler<IDictionary<string, string>> MessageReceived;
|
||||
event EventHandler<ValueSet> MessageReceived;
|
||||
|
||||
Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message);
|
||||
Task<ValueSet> SendMessageAsync(ValueSet message);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
void Delete(string key);
|
||||
|
||||
void Clear();
|
||||
|
||||
IEnumerable<object> GetAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IApplicationLanguageService
|
||||
{
|
||||
IEnumerable<string> Languages { get; }
|
||||
|
||||
void SetLanguage(string language);
|
||||
|
||||
string GetCurrentLanguage();
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,16 @@ namespace FluentTerminal.App.Services
|
||||
public interface IApplicationView
|
||||
{
|
||||
event CloseRequestedHandler CloseRequested;
|
||||
event EventHandler Closed;
|
||||
|
||||
int Id { get; }
|
||||
string Title { get; set; }
|
||||
|
||||
Task RunOnDispatcherThread(Action action);
|
||||
Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true);
|
||||
|
||||
Task<bool> TryClose();
|
||||
|
||||
bool ToggleFullScreen();
|
||||
bool IsApiContractPresent(string api, ushort version);
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,20 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IDefaultValueProvider
|
||||
{
|
||||
IEnumerable<ShellProfile> GetPreinstalledShellProfiles();
|
||||
|
||||
Guid GetDefaultShellProfileId();
|
||||
|
||||
TerminalOptions GetDefaultTerminalOptions();
|
||||
ApplicationSettings GetDefaultApplicationSettings();
|
||||
|
||||
ICollection<KeyBinding> GetDefaultKeyBindings(Command command);
|
||||
|
||||
ApplicationSettings GetDefaultApplicationSettings();
|
||||
Guid GetDefaultShellProfileId();
|
||||
|
||||
IEnumerable<TabTheme> GetDefaultTabThemes();
|
||||
|
||||
TerminalOptions GetDefaultTerminalOptions();
|
||||
|
||||
Guid GetDefaultThemeId();
|
||||
|
||||
IEnumerable<ShellProfile> GetPreinstalledShellProfiles();
|
||||
|
||||
IEnumerable<TerminalTheme> GetPreInstalledThemes();
|
||||
}
|
||||
}
|
||||
@@ -16,5 +16,14 @@ namespace FluentTerminal.App.Services
|
||||
Task<ShellProfile> ShowProfileSelectionDialogAsync();
|
||||
|
||||
Task<KeyBinding> ShowCreateKeyBindingDialog();
|
||||
Task<string> ShowInputDialogAsync(string title);
|
||||
|
||||
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null);
|
||||
|
||||
Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null);
|
||||
|
||||
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
|
||||
|
||||
Task ShowAboutDialogAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IKeyboardCommandService
|
||||
{
|
||||
void RegisterCommandHandler(Command command, Action handler);
|
||||
void RegisterCommandHandler(string command, Action handler);
|
||||
|
||||
void SendCommand(Command command);
|
||||
void SendCommand(string command);
|
||||
void DeregisterCommandHandler(string command);
|
||||
void ClearCommandHandlers();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface INotificationService
|
||||
{
|
||||
void ShowNotification(string title, string content, string url = null);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -7,52 +6,37 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ISettingsService
|
||||
{
|
||||
event EventHandler<Guid> CurrentThemeChanged;
|
||||
|
||||
event EventHandler<TerminalOptions> TerminalOptionsChanged;
|
||||
|
||||
event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
|
||||
|
||||
event EventHandler KeyBindingsChanged;
|
||||
|
||||
Guid GetDefaultShellProfileId();
|
||||
|
||||
ShellProfile GetDefaultShellProfile();
|
||||
|
||||
void SaveDefaultShellProfileId(Guid id);
|
||||
|
||||
IEnumerable<ShellProfile> GetShellProfiles();
|
||||
|
||||
void SaveShellProfile(ShellProfile shellProfile);
|
||||
|
||||
void DeleteShellProfile(Guid id);
|
||||
|
||||
TerminalOptions GetTerminalOptions();
|
||||
|
||||
void SaveTerminalOptions(TerminalOptions terminalOptions);
|
||||
|
||||
ApplicationSettings GetApplicationSettings();
|
||||
|
||||
void SaveApplicationSettings(ApplicationSettings applicationSettings);
|
||||
|
||||
IDictionary<Command, ICollection<KeyBinding>> GetKeyBindings();
|
||||
|
||||
void SaveKeyBindings(Command command, ICollection<KeyBinding> keyBindings);
|
||||
|
||||
void ResetKeyBindings();
|
||||
|
||||
TerminalTheme GetCurrentTheme();
|
||||
|
||||
Guid GetCurrentThemeId();
|
||||
|
||||
TerminalTheme GetTheme(Guid id);
|
||||
|
||||
void SaveCurrentThemeId(Guid id);
|
||||
|
||||
IEnumerable<TerminalTheme> GetThemes();
|
||||
|
||||
void SaveTheme(TerminalTheme theme);
|
||||
|
||||
void DeleteSshProfile(Guid id);
|
||||
void DeleteTheme(Guid id);
|
||||
ApplicationSettings GetApplicationSettings();
|
||||
TerminalTheme GetCurrentTheme();
|
||||
Guid GetCurrentThemeId();
|
||||
ShellProfile GetDefaultShellProfile();
|
||||
SshProfile GetDefaultSshProfile();
|
||||
Guid GetDefaultShellProfileId();
|
||||
Guid GetDefaultSshProfileId();
|
||||
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
|
||||
IEnumerable<ShellProfile> GetShellProfiles();
|
||||
IEnumerable<SshProfile> GetSshProfiles();
|
||||
IEnumerable<TabTheme> GetTabThemes();
|
||||
TerminalOptions GetTerminalOptions();
|
||||
TerminalTheme GetTheme(Guid id);
|
||||
IEnumerable<TerminalTheme> GetThemes();
|
||||
void ResetKeyBindings();
|
||||
void SaveApplicationSettings(ApplicationSettings applicationSettings);
|
||||
void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings);
|
||||
void SaveCurrentThemeId(Guid id);
|
||||
void SaveDefaultShellProfileId(Guid id);
|
||||
void SaveDefaultSshProfileId(Guid id);
|
||||
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
|
||||
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
|
||||
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
|
||||
void SaveTerminalOptions(TerminalOptions terminalOptions);
|
||||
void SaveTheme(TerminalTheme theme, bool newTheme = false);
|
||||
ShellProfile GetShellProfile(Guid id);
|
||||
SshProfile GetSshProfile(Guid id);
|
||||
string ExportSettings();
|
||||
void ImportSettings(string serializedSettings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IShellProfileMigrationService
|
||||
{
|
||||
void Migrate(ShellProfile profile);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentTerminal.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ISystemFontService
|
||||
{
|
||||
IEnumerable<string> GetSystemFontFamilies();
|
||||
IEnumerable<FontInfo> GetSystemFontFamilies();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ITerminalView
|
||||
{
|
||||
event EventHandler<TerminalSize> TerminalSizeChanged;
|
||||
|
||||
event EventHandler<string> TerminalTitleChanged;
|
||||
|
||||
event EventHandler<Command> KeyboardCommandReceived;
|
||||
|
||||
Task ChangeTheme(TerminalColors theme);
|
||||
|
||||
Task ChangeOptions(TerminalOptions options);
|
||||
|
||||
Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings);
|
||||
|
||||
Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings);
|
||||
|
||||
Task ConnectToSocket(string url);
|
||||
|
||||
void Close();
|
||||
|
||||
Task FocusTerminal();
|
||||
|
||||
Task<string> GetSelection();
|
||||
|
||||
Task FindNext(string searchText);
|
||||
|
||||
Task FindPrevious(string searchText);
|
||||
|
||||
void FocusSearchTextBox();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,20 +8,41 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ITrayProcessCommunicationService
|
||||
{
|
||||
event EventHandler<int> TerminalExited;
|
||||
event EventHandler<TerminalExitStatus> TerminalExited;
|
||||
|
||||
void Initialize(IAppServiceConnection appServiceConnection);
|
||||
|
||||
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile);
|
||||
Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
|
||||
|
||||
Task ResizeTerminal(int id, TerminalSize size);
|
||||
Task<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause);
|
||||
|
||||
Task ResizeTerminal(byte id, TerminalSize size);
|
||||
|
||||
Task UpdateToggleWindowKeyBindings();
|
||||
|
||||
Task WriteText(int terminalId, string text);
|
||||
Task Write(byte terminalId, byte[] data);
|
||||
|
||||
void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
|
||||
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
|
||||
|
||||
Task CloseTerminal(int terminalId);
|
||||
void UnsubscribeFromTerminalOutput(byte terminalId);
|
||||
|
||||
Task CloseTerminal(byte terminalId);
|
||||
byte GetNextTerminalId();
|
||||
|
||||
Task<string> GetUserName();
|
||||
|
||||
Task SaveTextFileAsync(string path, string content);
|
||||
|
||||
Task<string> GetSshConfigDirAsync();
|
||||
|
||||
Task<string[]> GetFilesFromSshConfigDirAsync();
|
||||
|
||||
Task<bool> CheckFileExistsAsync(string path);
|
||||
|
||||
void MuteTerminal(bool mute);
|
||||
|
||||
void UpdateSettings(ApplicationSettings settings);
|
||||
|
||||
Task<string> GetCommandPathAsync(string command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IUpdateService
|
||||
{
|
||||
Task CheckForUpdate(bool notifyNoUpdate = false);
|
||||
Version GetCurrentVersion();
|
||||
Task<Version> GetLatestVersionAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Windows.Foundation.Metadata;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
@@ -9,17 +12,35 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
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
|
||||
{
|
||||
ConfirmClosingTabs = false,
|
||||
ConfirmClosingWindows = false,
|
||||
UnderlineSelectedTab = false,
|
||||
InactiveTabColorMode = InactiveTabColorMode.Background,
|
||||
NewTerminalLocation = NewTerminalLocation.Tab,
|
||||
TabsPosition = TabsPosition.Top,
|
||||
CopyOnSelect = false,
|
||||
MouseMiddleClickAction = MouseAction.None,
|
||||
MouseRightClickAction = MouseAction.ContextMenu,
|
||||
AlwaysShowTabs = false
|
||||
ShowNewOutputIndicator = false,
|
||||
EnableTrayIcon = true,
|
||||
ShowCustomTitleInTitlebar = true,
|
||||
UseMoshByDefault = true,
|
||||
AutoFallbackToWindowsUsernameInLinks = true,
|
||||
RTrimCopiedLines = true,
|
||||
MuteTerminalBeeps = true,
|
||||
EnableLogging = false,
|
||||
PrintableOutputOnly = true,
|
||||
LogDirectoryPath = logDirectoryPath
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,10 +53,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.ToggleWindow,
|
||||
Ctrl = false,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Command = nameof(Command.ToggleWindow),
|
||||
Key = (int)ExtendedVirtualKey.Scroll
|
||||
}
|
||||
};
|
||||
@@ -45,10 +63,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.NextTab,
|
||||
Command = nameof(Command.NextTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Key = (int)ExtendedVirtualKey.Tab
|
||||
}
|
||||
};
|
||||
@@ -58,23 +74,52 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.PreviousTab,
|
||||
Command = nameof(Command.PreviousTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.Tab
|
||||
}
|
||||
};
|
||||
|
||||
case Command.SwitchToTerm1:
|
||||
case Command.SwitchToTerm2:
|
||||
case Command.SwitchToTerm3:
|
||||
case Command.SwitchToTerm4:
|
||||
case Command.SwitchToTerm5:
|
||||
case Command.SwitchToTerm6:
|
||||
case Command.SwitchToTerm7:
|
||||
case Command.SwitchToTerm8:
|
||||
case Command.SwitchToTerm9:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = command.ToString(),
|
||||
Alt = true,
|
||||
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.NewTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.NewTab,
|
||||
Command = nameof(Command.NewTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.T
|
||||
}
|
||||
};
|
||||
@@ -84,23 +129,106 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.ConfigurableNewTab,
|
||||
Command = nameof(Command.ConfigurableNewTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.T
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewSshTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewSshTab),
|
||||
Ctrl = true,
|
||||
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,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.Q
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewCustomCommandWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewCustomCommandWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.Q
|
||||
}
|
||||
};
|
||||
|
||||
case Command.SavedSshNewTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.SavedSshNewTab),
|
||||
Ctrl = true,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.U
|
||||
}
|
||||
};
|
||||
|
||||
case Command.SavedSshNewWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.SavedSshNewWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.U
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
case Command.ChangeTabTitle:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ChangeTabTitle),
|
||||
Ctrl = true,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.R
|
||||
}
|
||||
};
|
||||
|
||||
case Command.CloseTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.CloseTab,
|
||||
Command = nameof(Command.CloseTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.W
|
||||
}
|
||||
};
|
||||
@@ -110,10 +238,21 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.NewWindow,
|
||||
Command = nameof(Command.NewWindow),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.N
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ConfigurableNewWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ConfigurableNewWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.N
|
||||
}
|
||||
};
|
||||
@@ -123,10 +262,9 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.ShowSettings,
|
||||
Command = nameof(Command.ShowSettings),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.Comma
|
||||
}
|
||||
};
|
||||
@@ -136,10 +274,9 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.Copy,
|
||||
Command = nameof(Command.Copy),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.C
|
||||
}
|
||||
};
|
||||
@@ -149,10 +286,22 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.Paste,
|
||||
Command = nameof(Command.Paste),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.V
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
case Command.PasteWithoutNewlines:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.PasteWithoutNewlines),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.V
|
||||
}
|
||||
};
|
||||
@@ -162,10 +311,9 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.Search,
|
||||
Command = nameof(Command.Search),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.F
|
||||
}
|
||||
};
|
||||
@@ -175,11 +323,14 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.ToggleFullScreen,
|
||||
Ctrl = false,
|
||||
Command = nameof(Command.ToggleFullScreen),
|
||||
Alt = true,
|
||||
Shift = false,
|
||||
Key = (int)ExtendedVirtualKey.Enter
|
||||
},
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ToggleFullScreen),
|
||||
Key = (int)ExtendedVirtualKey.F11
|
||||
}
|
||||
};
|
||||
|
||||
@@ -188,10 +339,9 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.SelectAll,
|
||||
Command = nameof(Command.SelectAll),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.A
|
||||
}
|
||||
};
|
||||
@@ -201,16 +351,16 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = Command.Clear,
|
||||
Command = nameof(Command.Clear),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.L
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw new InvalidOperationException($"Default keybindings for Command '{command}' are missing");
|
||||
}
|
||||
}
|
||||
|
||||
public Guid GetDefaultShellProfileId()
|
||||
@@ -218,6 +368,60 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return Guid.Parse("813f2298-210a-481a-bdbf-c17bc637a3e2");
|
||||
}
|
||||
|
||||
public IEnumerable<TabTheme> GetDefaultTabThemes()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new TabTheme
|
||||
{
|
||||
Id = 0,
|
||||
Name = I18N.Translate("TabTheme.None"),
|
||||
BackgroundOpacity = double.NaN,
|
||||
BackgroundPointerOverOpacity = double.NaN,
|
||||
BackgroundPressedOpacity = double.NaN,
|
||||
BackgroundSelectedOpacity = double.NaN,
|
||||
BackgroundSelectedPointerOverOpacity = double.NaN,
|
||||
BackgroundSelectedPressedOpacity = double.NaN
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 1,
|
||||
Name = I18N.Translate("TabTheme.Red"),
|
||||
Color = "#E81123"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 2,
|
||||
Name = I18N.Translate("TabTheme.Green"),
|
||||
Color = "#10893E"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 3,
|
||||
Name = I18N.Translate("TabTheme.Blue"),
|
||||
Color = "#0078D7"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 4,
|
||||
Name = I18N.Translate("TabTheme.Purple"),
|
||||
Color = "#881798"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 5,
|
||||
Name = I18N.Translate("TabTheme.Orange"),
|
||||
Color = "#FF8C00"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 6,
|
||||
Name = I18N.Translate("TabTheme.Teal"),
|
||||
Color = "#00B7C3"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public TerminalOptions GetDefaultTerminalOptions()
|
||||
{
|
||||
return new TerminalOptions
|
||||
@@ -228,8 +432,11 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
ScrollBarStyle = ScrollBarStyle.Hidden,
|
||||
FontFamily = "Consolas",
|
||||
FontSize = 13,
|
||||
BoldText = false,
|
||||
BackgroundOpacity = 0.8,
|
||||
ScrollBackLimit = 1000
|
||||
Padding = 12,
|
||||
ScrollBackLimit = 1000,
|
||||
WordSeparator = " ()[]{}:;|│!&*<>@"&squo;"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -246,28 +453,85 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Id = GetDefaultShellProfileId(),
|
||||
Name = "Powershell",
|
||||
MigrationVersion = ShellProfile.CurrentMigrationVersion,
|
||||
Arguments = string.Empty,
|
||||
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
|
||||
PreInstalled = true,
|
||||
WorkingDirectory = string.Empty
|
||||
WorkingDirectory = string.Empty,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = GetDefaultShellProfileId().ToString(),
|
||||
Ctrl=true,
|
||||
Alt=true,
|
||||
Shift=false,
|
||||
Meta=false,
|
||||
Key=(int)ExtendedVirtualKey.Number1
|
||||
}
|
||||
}
|
||||
},
|
||||
new ShellProfile
|
||||
{
|
||||
Id = Guid.Parse("ab942a61-7673-4755-9bd8-765aff91d9a3"),
|
||||
Name = "CMD",
|
||||
MigrationVersion = ShellProfile.CurrentMigrationVersion,
|
||||
Arguments = string.Empty,
|
||||
Location = @"C:\Windows\System32\cmd.exe",
|
||||
PreInstalled = true,
|
||||
WorkingDirectory = string.Empty
|
||||
WorkingDirectory = string.Empty,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = "ab942a61-7673-4755-9bd8-765aff91d9a3",
|
||||
Ctrl=true,
|
||||
Alt=true,
|
||||
Shift=false,
|
||||
Meta=false,
|
||||
Key=(int)ExtendedVirtualKey.Number2
|
||||
}
|
||||
}
|
||||
},
|
||||
new ShellProfile
|
||||
{
|
||||
Id= Guid.Parse("e5785ad6-584f-40cb-bdcd-d5b3b3953e7f"),
|
||||
Name = "WSL",
|
||||
MigrationVersion = ShellProfile.CurrentMigrationVersion,
|
||||
Arguments = string.Empty,
|
||||
Location = @"C:\windows\system32\wsl.exe",
|
||||
PreInstalled = true,
|
||||
WorkingDirectory = string.Empty
|
||||
WorkingDirectory = string.Empty,
|
||||
LineEndingTranslation = LineEndingStyle.ToLF,
|
||||
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = "e5785ad6-584f-40cb-bdcd-d5b3b3953e7f",
|
||||
Ctrl=true,
|
||||
Alt=true,
|
||||
Shift=false,
|
||||
Meta=false,
|
||||
Key=(int)ExtendedVirtualKey.Number3
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -418,18 +682,18 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Black = "#300A24",
|
||||
Red = "#CC0000",
|
||||
Green = "#4E9A06",
|
||||
Yellow = "#06989A",
|
||||
Yellow = "#C4A000",
|
||||
Blue = "#3465A4",
|
||||
Magenta = "#75507B",
|
||||
Cyan = "#C4A000",
|
||||
Cyan = "#06989A",
|
||||
White = "#D3D7CF",
|
||||
BrightBlack = "#554E53",
|
||||
BrightRed = "#EF2929",
|
||||
BrightGreen = "#8AE234",
|
||||
BrightYellow = "#34E2E2",
|
||||
BrightYellow = "#FCE94F",
|
||||
BrightBlue = "#729FCF",
|
||||
BrightMagenta = "#AD7FA8",
|
||||
BrightCyan = "#FCE94F",
|
||||
BrightCyan = "#34E2E2",
|
||||
BrightWhite = "#EEEEEE"
|
||||
}
|
||||
};
|
||||
@@ -437,4 +701,4 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return new[] { defaultXterm, powerShell, homebrew, tomorrow, ubuntu };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,27 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
private readonly Func<IShellProfileSelectionDialog> _shellProfileSelectionDialogFactory;
|
||||
private readonly Func<IMessageDialog> _messageDialogFactory;
|
||||
private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory;
|
||||
private readonly Func<IInputDialog> _inputDialogFactory;
|
||||
private readonly Func<ISshConnectionInfoDialog> _sshConnectionInfoDialogFactory;
|
||||
private readonly Func<ICustomCommandDialog> _customCommandDialogFactory;
|
||||
private readonly Func<ISshProfileSelectionDialog> _sshProfileSelectionDialogFactory;
|
||||
private readonly Func<IAboutDialog> _aboutDialogFactory;
|
||||
|
||||
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory, Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory)
|
||||
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
|
||||
Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
|
||||
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
|
||||
Func<ICustomCommandDialog> customCommandDialogFactory,
|
||||
Func<ISshProfileSelectionDialog> sshProfileSelectionDialogFactory,
|
||||
Func<IAboutDialog> aboutDialogFactory)
|
||||
{
|
||||
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
|
||||
_messageDialogFactory = messageDialogFactory;
|
||||
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
|
||||
_inputDialogFactory = inputDialogFactory;
|
||||
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
|
||||
_customCommandDialogFactory = customCommandDialogFactory;
|
||||
_sshProfileSelectionDialogFactory = sshProfileSelectionDialogFactory;
|
||||
_aboutDialogFactory = aboutDialogFactory;
|
||||
}
|
||||
|
||||
public Task<KeyBinding> ShowCreateKeyBindingDialog()
|
||||
@@ -54,11 +69,39 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return dialog.ShowAsync();
|
||||
}
|
||||
|
||||
public Task<string> ShowInputDialogAsync(string title)
|
||||
{
|
||||
var dialog = _inputDialogFactory.Invoke();
|
||||
dialog.SetTitle(title);
|
||||
|
||||
return dialog.GetInput();
|
||||
}
|
||||
|
||||
public Task<ShellProfile> ShowProfileSelectionDialogAsync()
|
||||
{
|
||||
var dialog = _shellProfileSelectionDialogFactory();
|
||||
|
||||
return dialog.SelectProfile();
|
||||
}
|
||||
|
||||
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) =>
|
||||
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
|
||||
|
||||
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
|
||||
_customCommandDialogFactory().GetCustomCommandAsync(input);
|
||||
|
||||
public Task<SshProfile> ShowSshProfileSelectionDialogAsync()
|
||||
{
|
||||
var dialog = _sshProfileSelectionDialogFactory();
|
||||
|
||||
return dialog.SelectProfile();
|
||||
}
|
||||
|
||||
public Task ShowAboutDialogAsync()
|
||||
{
|
||||
var dialog = _aboutDialogFactory();
|
||||
|
||||
return dialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
public const string RedComponent = "Red Component";
|
||||
}
|
||||
|
||||
public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
|
||||
public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
@@ -65,13 +65,13 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
|
||||
|
||||
return new TerminalTheme
|
||||
return Task.FromResult(new TerminalTheme
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(fileName),
|
||||
Colors = GetColors(node),
|
||||
Id = Guid.NewGuid(),
|
||||
PreInstalled = false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private TerminalColors GetColors(DictionaryNode themeDictionary)
|
||||
@@ -83,6 +83,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]),
|
||||
CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]),
|
||||
Selection = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor], Opacity30Percent),
|
||||
SelectionForeground = GetColorString(themeDictionary[ITermThemeKeys.SelectedTextColor]),
|
||||
SelectionBackground = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor]),
|
||||
Black = GetColorString(themeDictionary[ITermThemeKeys.Ansi0Color]),
|
||||
Red = GetColorString(themeDictionary[ITermThemeKeys.Ansi1Color]),
|
||||
Green = GetColorString(themeDictionary[ITermThemeKeys.Ansi2Color]),
|
||||
@@ -110,8 +112,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
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 color = $"#{alpha:X2}{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
|
||||
|
||||
if (alpha == byte.MaxValue)
|
||||
{
|
||||
return $"#{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public class KeyboardCommandService : IKeyboardCommandService
|
||||
{
|
||||
private readonly Dictionary<Command, Action> _commandHandlers = new Dictionary<Command, Action>();
|
||||
|
||||
public void RegisterCommandHandler(Command command, Action handler)
|
||||
{
|
||||
if (_commandHandlers.ContainsKey(command))
|
||||
{
|
||||
throw new InvalidOperationException("command already registered");
|
||||
}
|
||||
|
||||
_commandHandlers[command] = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
}
|
||||
|
||||
public void SendCommand(Command command)
|
||||
{
|
||||
if (_commandHandlers.TryGetValue(command, out Action handler))
|
||||
{
|
||||
handler.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public class KeyboardCommandService : IKeyboardCommandService
|
||||
{
|
||||
private readonly Dictionary<string, Action> _commandHandlers = new Dictionary<string, Action>();
|
||||
|
||||
public void RegisterCommandHandler(string command, Action handler)
|
||||
{
|
||||
if (_commandHandlers.ContainsKey(command))
|
||||
{
|
||||
throw new InvalidOperationException("command already registered");
|
||||
}
|
||||
|
||||
_commandHandlers[command] = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
}
|
||||
|
||||
public void DeregisterCommandHandler(string command)
|
||||
{
|
||||
if (_commandHandlers.ContainsKey(command))
|
||||
{
|
||||
_commandHandlers.Remove(command);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendCommand(string command)
|
||||
{
|
||||
if (_commandHandlers.TryGetValue(command, out Action handler))
|
||||
{
|
||||
handler.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// already registered by SystemTray's ToggleWindowService
|
||||
if (command == Command.ToggleWindow)
|
||||
if (command == nameof(Command.ToggleWindow))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException($"No handler registered for command: {command}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException($"No handler registered for command: {command}");
|
||||
}
|
||||
|
||||
public void ClearCommandHandlers()
|
||||
{
|
||||
_commandHandlers.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
using Windows.Data.Xml.Dom;
|
||||
using Windows.UI.Notifications;
|
||||
|
||||
namespace FluentTerminal.SystemTray.Services
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public class NotificationService
|
||||
public class NotificationService : INotificationService
|
||||
{
|
||||
public void ShowNotification(string title, string content, string url = null)
|
||||
{
|
||||
@@ -5,6 +5,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using FluentTerminal.Models.Messages;
|
||||
using GalaSoft.MvvmLight.Messaging;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
@@ -12,21 +14,15 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public const string CurrentThemeKey = "CurrentTheme";
|
||||
public const string DefaultShellProfileKey = "DefaultShellProfile";
|
||||
public const string DefaultSshProfileKey = "DefaultSshProfile";
|
||||
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private readonly IApplicationDataContainer _localSettings;
|
||||
private readonly IApplicationDataContainer _themes;
|
||||
private readonly IApplicationDataContainer _keyBindings;
|
||||
private readonly IApplicationDataContainer _shellProfiles;
|
||||
private readonly IApplicationDataContainer _localSettings;
|
||||
private readonly IApplicationDataContainer _roamingSettings;
|
||||
|
||||
public event EventHandler<Guid> CurrentThemeChanged;
|
||||
|
||||
public event EventHandler<TerminalOptions> TerminalOptionsChanged;
|
||||
|
||||
public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
|
||||
|
||||
public event EventHandler KeyBindingsChanged;
|
||||
private readonly IApplicationDataContainer _shellProfiles;
|
||||
private readonly IApplicationDataContainer _sshProfiles;
|
||||
private readonly IApplicationDataContainer _themes;
|
||||
|
||||
public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers)
|
||||
{
|
||||
@@ -37,18 +33,156 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_themes = containers.Themes;
|
||||
_keyBindings = containers.KeyBindings;
|
||||
_shellProfiles = containers.ShellProfiles;
|
||||
_sshProfiles = containers.SshProfiles;
|
||||
|
||||
|
||||
foreach (var theme in _defaultValueProvider.GetPreInstalledThemes())
|
||||
{
|
||||
_themes.WriteValueAsJson(theme.Id.ToString(), theme);
|
||||
if (GetTheme(theme.Id) == null)
|
||||
{
|
||||
_themes.WriteValueAsJson(theme.Id.ToString(), theme);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var shellProfile in _defaultValueProvider.GetPreinstalledShellProfiles())
|
||||
{
|
||||
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
|
||||
if (GetShellProfile(shellProfile.Id) == null)
|
||||
{
|
||||
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ExportSettings()
|
||||
{
|
||||
var config = new
|
||||
{
|
||||
App = GetApplicationSettings(),
|
||||
KeyBindings = GetCommandKeyBindings(),
|
||||
TerminalOptions = GetTerminalOptions(),
|
||||
Themes = new List<TerminalTheme>(),
|
||||
Profiles = new List<ShellProfile>(),
|
||||
SshProfiles = new List<SshProfile>()
|
||||
};
|
||||
|
||||
foreach (var theme in GetThemes().Where(x => !x.PreInstalled))
|
||||
{
|
||||
config.Themes.Add(theme);
|
||||
}
|
||||
|
||||
foreach (var profile in GetShellProfiles().Where(x => !x.PreInstalled))
|
||||
{
|
||||
config.Profiles.Add(profile);
|
||||
}
|
||||
|
||||
foreach (var profile in GetSshProfiles())
|
||||
{
|
||||
config.SshProfiles.Add(profile);
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(config);
|
||||
}
|
||||
|
||||
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()
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Can't create pre-installed profiles
|
||||
foreach (var profile in config.Profiles.Where(x => !x.PreInstalled))
|
||||
{
|
||||
var existingProfile = GetShellProfile(profile.Id);
|
||||
var isNew = existingProfile.EqualTo(default);
|
||||
|
||||
// You can only edit certain parts of preinstalled profiles
|
||||
if (!isNew && existingProfile.PreInstalled)
|
||||
{
|
||||
existingProfile.WorkingDirectory = profile.WorkingDirectory;
|
||||
existingProfile.Arguments = profile.Arguments;
|
||||
existingProfile.TabThemeId = profile.TabThemeId;
|
||||
existingProfile.TerminalThemeId = profile.TerminalThemeId;
|
||||
existingProfile.LineEndingTranslation = profile.LineEndingTranslation;
|
||||
existingProfile.KeyBindings = profile.KeyBindings;
|
||||
SaveShellProfile(profile, isNew);
|
||||
continue;
|
||||
}
|
||||
SaveShellProfile(profile, isNew);
|
||||
}
|
||||
|
||||
foreach (var profile in config.SshProfiles)
|
||||
{
|
||||
var existingProfile = GetSshProfile(profile.Id);
|
||||
var isNew = existingProfile.EqualTo(default(SshProfile));
|
||||
|
||||
SaveSshProfile(profile, isNew);
|
||||
}
|
||||
|
||||
SaveTerminalOptions(config.TerminalOptions);
|
||||
}
|
||||
|
||||
public void DeleteShellProfile(Guid id)
|
||||
{
|
||||
_shellProfiles.Delete(id.ToString());
|
||||
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
|
||||
}
|
||||
|
||||
public void DeleteSshProfile(Guid id)
|
||||
{
|
||||
_sshProfiles.Delete(id.ToString());
|
||||
Messenger.Default.Send(new SshProfileDeletedMessage(id));
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
|
||||
public void DeleteTheme(Guid id)
|
||||
{
|
||||
_themes.Delete(id.ToString());
|
||||
|
||||
foreach (var profile in GetShellProfiles())
|
||||
{
|
||||
if (profile.TerminalThemeId == id)
|
||||
{
|
||||
profile.TerminalThemeId = Guid.Empty;
|
||||
SaveShellProfile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
Messenger.Default.Send(new ThemeDeletedMessage(id));
|
||||
}
|
||||
|
||||
public ApplicationSettings GetApplicationSettings()
|
||||
{
|
||||
return _roamingSettings.ReadValueFromJson(nameof(ApplicationSettings), _defaultValueProvider.GetDefaultApplicationSettings());
|
||||
}
|
||||
|
||||
public TerminalTheme GetCurrentTheme()
|
||||
{
|
||||
var id = GetCurrentThemeId();
|
||||
@@ -71,96 +205,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _defaultValueProvider.GetDefaultThemeId();
|
||||
}
|
||||
|
||||
public void SaveCurrentThemeId(Guid id)
|
||||
{
|
||||
_roamingSettings.SetValue(CurrentThemeKey, id);
|
||||
|
||||
CurrentThemeChanged?.Invoke(this, id);
|
||||
}
|
||||
|
||||
public void SaveTheme(TerminalTheme theme)
|
||||
{
|
||||
_themes.WriteValueAsJson(theme.Id.ToString(), theme);
|
||||
|
||||
if (theme.Id == GetCurrentThemeId())
|
||||
{
|
||||
CurrentThemeChanged?.Invoke(this, theme.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteTheme(Guid id)
|
||||
{
|
||||
_themes.Delete(id.ToString());
|
||||
}
|
||||
|
||||
public IEnumerable<TerminalTheme> GetThemes()
|
||||
{
|
||||
return _themes.GetAll().Select(x => JsonConvert.DeserializeObject<TerminalTheme>((string)x)).ToList();
|
||||
}
|
||||
|
||||
public TerminalTheme GetTheme(Guid id)
|
||||
{
|
||||
return _themes.ReadValueFromJson(id.ToString(), default(TerminalTheme));
|
||||
}
|
||||
|
||||
public TerminalOptions GetTerminalOptions()
|
||||
{
|
||||
return _roamingSettings.ReadValueFromJson(nameof(TerminalOptions), _defaultValueProvider.GetDefaultTerminalOptions());
|
||||
}
|
||||
|
||||
public void SaveTerminalOptions(TerminalOptions terminalOptions)
|
||||
{
|
||||
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
|
||||
TerminalOptionsChanged?.Invoke(this, terminalOptions);
|
||||
}
|
||||
|
||||
public ApplicationSettings GetApplicationSettings()
|
||||
{
|
||||
return _roamingSettings.ReadValueFromJson(nameof(ApplicationSettings), _defaultValueProvider.GetDefaultApplicationSettings());
|
||||
}
|
||||
|
||||
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
|
||||
{
|
||||
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
|
||||
ApplicationSettingsChanged?.Invoke(this, applicationSettings);
|
||||
}
|
||||
|
||||
public IDictionary<Command, ICollection<KeyBinding>> GetKeyBindings()
|
||||
{
|
||||
var keyBindings = new Dictionary<Command, ICollection<KeyBinding>>();
|
||||
foreach (Command command in Enum.GetValues(typeof(Command)))
|
||||
{
|
||||
keyBindings.Add(command, _keyBindings.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null) ?? _defaultValueProvider.GetDefaultKeyBindings(command));
|
||||
}
|
||||
|
||||
return keyBindings;
|
||||
}
|
||||
|
||||
public void SaveKeyBindings(Command command, ICollection<KeyBinding> keyBindings)
|
||||
{
|
||||
_keyBindings.WriteValueAsJson(command.ToString(), keyBindings);
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void ResetKeyBindings()
|
||||
{
|
||||
foreach (Command command in Enum.GetValues(typeof(Command)))
|
||||
{
|
||||
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
|
||||
}
|
||||
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
|
||||
public Guid GetDefaultShellProfileId()
|
||||
{
|
||||
if (_localSettings.TryGetValue(DefaultShellProfileKey, out object value))
|
||||
{
|
||||
return (Guid)value;
|
||||
}
|
||||
return _defaultValueProvider.GetDefaultShellProfileId();
|
||||
}
|
||||
|
||||
public ShellProfile GetDefaultShellProfile()
|
||||
{
|
||||
var id = GetDefaultShellProfileId();
|
||||
@@ -173,10 +217,49 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
public void SaveDefaultShellProfileId(Guid id)
|
||||
public SshProfile GetDefaultSshProfile()
|
||||
{
|
||||
_localSettings.SetValue(DefaultShellProfileKey, id);
|
||||
var id = GetDefaultSshProfileId();
|
||||
var profile = _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public ShellProfile GetShellProfile(Guid id)
|
||||
{
|
||||
return _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
|
||||
}
|
||||
public SshProfile GetSshProfile(Guid id)
|
||||
{
|
||||
return _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
|
||||
}
|
||||
|
||||
public Guid GetDefaultShellProfileId()
|
||||
{
|
||||
if (_localSettings.TryGetValue(DefaultShellProfileKey, out object value))
|
||||
{
|
||||
return (Guid)value;
|
||||
}
|
||||
return _defaultValueProvider.GetDefaultShellProfileId();
|
||||
}
|
||||
public Guid GetDefaultSshProfileId()
|
||||
{
|
||||
if (_localSettings.TryGetValue(DefaultSshProfileKey, out object value))
|
||||
{
|
||||
return (Guid)value;
|
||||
}
|
||||
return System.Guid.Empty;
|
||||
}
|
||||
|
||||
public IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings()
|
||||
{
|
||||
var keyBindings = new Dictionary<string, ICollection<KeyBinding>>();
|
||||
|
||||
foreach (Command command in Enum.GetValues(typeof(Command)))
|
||||
{
|
||||
keyBindings.Add(command.ToString(), _keyBindings.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null) ?? _defaultValueProvider.GetDefaultKeyBindings(command));
|
||||
}
|
||||
return keyBindings;
|
||||
}
|
||||
|
||||
public IEnumerable<ShellProfile> GetShellProfiles()
|
||||
@@ -184,14 +267,131 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string)x)).ToList();
|
||||
}
|
||||
|
||||
public void SaveShellProfile(ShellProfile shellProfile)
|
||||
public IEnumerable<SshProfile> GetSshProfiles()
|
||||
{
|
||||
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
|
||||
if (_sshProfiles == null)
|
||||
{
|
||||
return new List<SshProfile>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return _sshProfiles.GetAll()
|
||||
.Select(x => JsonConvert.DeserializeObject<SshProfile>((string)x)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteShellProfile(Guid id)
|
||||
public IEnumerable<TabTheme> GetTabThemes()
|
||||
{
|
||||
_shellProfiles.Delete(id.ToString());
|
||||
return _defaultValueProvider.GetDefaultTabThemes();
|
||||
}
|
||||
|
||||
public TerminalOptions GetTerminalOptions()
|
||||
{
|
||||
return _roamingSettings.ReadValueFromJson(nameof(TerminalOptions), _defaultValueProvider.GetDefaultTerminalOptions());
|
||||
}
|
||||
|
||||
public TerminalTheme GetTheme(Guid id)
|
||||
{
|
||||
return _themes.ReadValueFromJson(id.ToString(), default(TerminalTheme));
|
||||
}
|
||||
|
||||
public IEnumerable<TerminalTheme> GetThemes()
|
||||
{
|
||||
return _themes.GetAll().Select(x => JsonConvert.DeserializeObject<TerminalTheme>((string)x)).ToList();
|
||||
}
|
||||
|
||||
public void ResetKeyBindings()
|
||||
{
|
||||
foreach (Command command in Enum.GetValues(typeof(Command)))
|
||||
{
|
||||
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
|
||||
}
|
||||
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
|
||||
{
|
||||
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
|
||||
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
|
||||
}
|
||||
|
||||
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
|
||||
{
|
||||
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
|
||||
}
|
||||
|
||||
public void SaveCurrentThemeId(Guid id)
|
||||
{
|
||||
_roamingSettings.SetValue(CurrentThemeKey, id);
|
||||
|
||||
Messenger.Default.Send(new CurrentThemeChangedMessage(id));
|
||||
}
|
||||
|
||||
public void SaveDefaultShellProfileId(Guid id)
|
||||
{
|
||||
_localSettings.SetValue(DefaultShellProfileKey, id);
|
||||
}
|
||||
public void SaveDefaultSshProfileId(Guid id)
|
||||
{
|
||||
_localSettings.SetValue(DefaultSshProfileKey, id);
|
||||
}
|
||||
|
||||
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
|
||||
{
|
||||
if (!Enum.TryParse<Command>(command, true, out var enumValue))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
|
||||
{
|
||||
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
|
||||
|
||||
// When saving the shell profile, we also need to update keybindings for everywhere.
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
|
||||
if (newShell)
|
||||
{
|
||||
Messenger.Default.Send(new ShellProfileAddedMessage(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.
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
|
||||
if (newShell)
|
||||
{
|
||||
Messenger.Default.Send(new SshProfileAddedMessage(sshProfile));
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveTerminalOptions(TerminalOptions terminalOptions)
|
||||
{
|
||||
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
|
||||
Messenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions));
|
||||
}
|
||||
|
||||
public void SaveTheme(TerminalTheme theme, bool newTheme = false)
|
||||
{
|
||||
_themes.WriteValueAsJson(theme.Id.ToString(), theme);
|
||||
|
||||
if (theme.Id == GetCurrentThemeId())
|
||||
{
|
||||
Messenger.Default.Send(new CurrentThemeChangedMessage(theme.Id));
|
||||
}
|
||||
|
||||
if (newTheme)
|
||||
{
|
||||
Messenger.Default.Send(new ThemeAddedMessage(theme));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
{
|
||||
private IEnumerable<IThemeParser> _parsers;
|
||||
private readonly IEnumerable<IThemeParser> _parsers;
|
||||
|
||||
public IEnumerable<string> SupportedFileTypes { get; }
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.App.Services.Exceptions;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Requests;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
@@ -14,27 +15,158 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private IAppServiceConnection _appServiceConnection;
|
||||
private Dictionary<int, Action<string>> _terminalOutputHandlers;
|
||||
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers;
|
||||
private byte _nextTerminalId = 0;
|
||||
|
||||
public event EventHandler<int> TerminalExited;
|
||||
public event EventHandler<TerminalExitStatus> TerminalExited;
|
||||
|
||||
public TrayProcessCommunicationService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
|
||||
_terminalOutputHandlers = new Dictionary<byte, Action<byte[]>>();
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile)
|
||||
public byte GetNextTerminalId()
|
||||
{
|
||||
return _nextTerminalId++;
|
||||
}
|
||||
|
||||
private string _userName;
|
||||
|
||||
public async Task<string> GetUserName()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_userName))
|
||||
{
|
||||
// Returning the username from cache
|
||||
return _userName;
|
||||
}
|
||||
|
||||
StringValueResponse response;
|
||||
|
||||
// No need to crash for username, so try/catch
|
||||
try
|
||||
{
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new GetUserNameRequest()));
|
||||
response = JsonConvert.DeserializeObject<StringValueResponse>((string)responseMessage[MessageKeys.Content]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Instance.Error(e, "Error while trying to get username.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.Instance.Debug("Received GetUserNameResponse: {@response}", response);
|
||||
|
||||
_userName = response.Value;
|
||||
|
||||
return _userName;
|
||||
}
|
||||
|
||||
public async Task SaveTextFileAsync(string path, string content)
|
||||
{
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new SaveTextFileRequest {Path = path, Content = content}));
|
||||
|
||||
var response = JsonConvert.DeserializeObject<CommonResponse>((string)responseMessage[MessageKeys.Content]);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error) ? "Failed to save the file." : response.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string _sshConfigDir;
|
||||
|
||||
public async Task<string> GetSshConfigDirAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_sshConfigDir))
|
||||
{
|
||||
var response = await GetSshConfigFolderAsync(false);
|
||||
|
||||
if (response?.Success ?? false)
|
||||
{
|
||||
_sshConfigDir = response.Path;
|
||||
}
|
||||
}
|
||||
|
||||
return _sshConfigDir;
|
||||
}
|
||||
|
||||
public async Task<string[]> GetFilesFromSshConfigDirAsync()
|
||||
{
|
||||
var response = await GetSshConfigFolderAsync(false);
|
||||
|
||||
if (response == null || !response.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_sshConfigDir))
|
||||
{
|
||||
_sshConfigDir = response.Path;
|
||||
}
|
||||
|
||||
return response.Files;
|
||||
}
|
||||
|
||||
private async Task<GetSshConfigFolderResponse> GetSshConfigFolderAsync(bool includeContent)
|
||||
{
|
||||
var responseMessage =
|
||||
await _appServiceConnection.SendMessageAsync(CreateMessage(new GetSshConfigFolderRequest
|
||||
{IncludeContent = includeContent}));
|
||||
|
||||
return JsonConvert.DeserializeObject<GetSshConfigFolderResponse>(
|
||||
(string) responseMessage[MessageKeys.Content]);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckFileExistsAsync(string path)
|
||||
{
|
||||
var responseMessage =
|
||||
await _appServiceConnection.SendMessageAsync(CreateMessage(new CheckFileExistsRequest {Path = path}));
|
||||
|
||||
return JsonConvert.DeserializeObject<CommonResponse>((string) responseMessage[MessageKeys.Content]).Success;
|
||||
}
|
||||
|
||||
public void MuteTerminal(bool mute)
|
||||
{
|
||||
_appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest { Mute = mute }));
|
||||
}
|
||||
|
||||
public void UpdateSettings(ApplicationSettings settings)
|
||||
{
|
||||
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
|
||||
{
|
||||
var request = new CreateTerminalRequest
|
||||
{
|
||||
Id = id,
|
||||
Size = size,
|
||||
Profile = shellProfile
|
||||
Profile = shellProfile,
|
||||
SessionType = sessionType
|
||||
};
|
||||
|
||||
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>((string)responseMessage[MessageKeys.Content]);
|
||||
|
||||
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause)
|
||||
{
|
||||
var request = new PauseTerminalOutputRequest
|
||||
{
|
||||
Id = id,
|
||||
Pause = pause
|
||||
};
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
|
||||
return JsonConvert.DeserializeObject<CreateTerminalResponse>(responseMessage[MessageKeys.Content]);
|
||||
return JsonConvert.DeserializeObject<PauseTerminalOutputResponse>((string)responseMessage[MessageKeys.Content]);
|
||||
}
|
||||
|
||||
public void Initialize(IAppServiceConnection appServiceConnection)
|
||||
@@ -43,33 +175,38 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_appServiceConnection.MessageReceived += OnMessageReceived;
|
||||
}
|
||||
|
||||
private void OnMessageReceived(object sender, IDictionary<string, string> e)
|
||||
private void OnMessageReceived(object sender, IDictionary<string, object> e)
|
||||
{
|
||||
var messageType = e[MessageKeys.Type];
|
||||
var messageType = (byte)e[MessageKeys.Type];
|
||||
var messageContent = e[MessageKeys.Content];
|
||||
|
||||
if (messageType == nameof(DisplayTerminalOutputRequest))
|
||||
switch (messageType)
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<DisplayTerminalOutputRequest>(messageContent);
|
||||
case Constants.TerminalBufferRequestIdentifier:
|
||||
var terminalId = (byte)e[MessageKeys.TerminalId];
|
||||
|
||||
if (_terminalOutputHandlers.ContainsKey(request.TerminalId))
|
||||
{
|
||||
_terminalOutputHandlers[request.TerminalId].Invoke(request.Output);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("output was not handled: " + request.Output);
|
||||
}
|
||||
}
|
||||
else if (messageType == nameof(TerminalExitedRequest))
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
|
||||
if (_terminalOutputHandlers.ContainsKey(terminalId))
|
||||
{
|
||||
_terminalOutputHandlers[terminalId].Invoke((byte[])messageContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId);
|
||||
}
|
||||
break;
|
||||
case (byte) MessageIdentifiers.TerminalExitedRequest:
|
||||
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent);
|
||||
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
|
||||
|
||||
TerminalExited?.Invoke(this, request.TerminalId);
|
||||
TerminalExited?.Invoke(this, request.ToStatus());
|
||||
break;
|
||||
default:
|
||||
Logger.Instance.Error("Received unknown message type: {messageType}", messageType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Task ResizeTerminal(int id, TerminalSize size)
|
||||
public Task ResizeTerminal(byte id, TerminalSize size)
|
||||
{
|
||||
var request = new ResizeTerminalRequest
|
||||
{
|
||||
@@ -80,14 +217,22 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
public void SubscribeForTerminalOutput(int terminalId, Action<string> callback)
|
||||
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
|
||||
{
|
||||
_terminalOutputHandlers[terminalId] = callback;
|
||||
}
|
||||
|
||||
public void UnsubscribeFromTerminalOutput(byte id)
|
||||
{
|
||||
if (_terminalOutputHandlers.ContainsKey(id))
|
||||
{
|
||||
_terminalOutputHandlers.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
public Task UpdateToggleWindowKeyBindings()
|
||||
{
|
||||
var keyBindings = _settingsService.GetKeyBindings()[Command.ToggleWindow];
|
||||
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
|
||||
|
||||
var request = new SetToggleWindowKeyBindingsRequest
|
||||
{
|
||||
@@ -97,32 +242,75 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
public Task WriteText(int id, string text)
|
||||
public Task Write(byte id, byte[] data)
|
||||
{
|
||||
var request = new WriteTextRequest
|
||||
var message = new ValueSet
|
||||
{
|
||||
TerminalId = id,
|
||||
Text = text
|
||||
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
|
||||
[MessageKeys.TerminalId] = id,
|
||||
[MessageKeys.Content] = data
|
||||
};
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(message);
|
||||
}
|
||||
|
||||
public Task CloseTerminal(byte terminalId)
|
||||
{
|
||||
var request = new TerminalExitedRequest(terminalId, -1);
|
||||
|
||||
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
public Task CloseTerminal(int terminalId)
|
||||
{
|
||||
var request = new TerminalExitedRequest
|
||||
{
|
||||
TerminalId = terminalId
|
||||
};
|
||||
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
|
||||
|
||||
public async Task<string> GetCommandPathAsync(string command)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
throw new ArgumentException("Input value is null or empty.", nameof(command));
|
||||
}
|
||||
|
||||
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 request = new GetCommandPathRequest {Command = command};
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
var response =
|
||||
JsonConvert.DeserializeObject<StringValueResponse>((string) responseMessage[MessageKeys.Content]);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
CommandPaths[commandLower] = response.Value;
|
||||
|
||||
return response.Value;
|
||||
}
|
||||
|
||||
CommandErrors[commandLower] = response.Error;
|
||||
|
||||
throw new Exception(response.Error);
|
||||
}
|
||||
|
||||
private IDictionary<string, string> CreateMessage(object content)
|
||||
private ValueSet CreateMessage(IMessage content)
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
return new ValueSet
|
||||
{
|
||||
[MessageKeys.Type] = content.GetType().Name,
|
||||
[MessageKeys.Type] = content.Identifier,
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Windows.ApplicationModel;
|
||||
using Newtonsoft.Json;
|
||||
using RestSharp;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public class UpdateService : IUpdateService
|
||||
{
|
||||
private const string apiEndpoint = "https://api.github.com";
|
||||
|
||||
private readonly INotificationService _notificationService;
|
||||
|
||||
public UpdateService(INotificationService notificationService)
|
||||
{
|
||||
_notificationService = notificationService;
|
||||
}
|
||||
|
||||
public async Task CheckForUpdate(bool notifyNoUpdate = false)
|
||||
{
|
||||
var latest = await GetLatestVersionAsync();
|
||||
if (latest > GetCurrentVersion())
|
||||
{
|
||||
_notificationService.ShowNotification("Update available",
|
||||
"Click to open the releases page.", "https://github.com/felixse/FluentTerminal/releases");
|
||||
}
|
||||
else if (notifyNoUpdate)
|
||||
{
|
||||
_notificationService.ShowNotification("No update available", "You're up to date!");
|
||||
}
|
||||
}
|
||||
|
||||
public Version GetCurrentVersion()
|
||||
{
|
||||
var currentVersion = Package.Current.Id.Version;
|
||||
return new Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build, currentVersion.Revision);
|
||||
}
|
||||
|
||||
public async Task<Version> GetLatestVersionAsync()
|
||||
{
|
||||
var restClient = new RestClient(apiEndpoint);
|
||||
var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET);
|
||||
|
||||
var restResponse = await restClient.ExecuteTaskAsync(restRequest);
|
||||
if (restResponse.IsSuccessful)
|
||||
{
|
||||
dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content);
|
||||
string tag = restResponseData[0].tag_name;
|
||||
var latestVersion = new Version(tag);
|
||||
return new Version(latestVersion.Major, latestVersion.Minor, latestVersion.Build, latestVersion.Revision);
|
||||
}
|
||||
return new Version(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public sealed class Logger
|
||||
{
|
||||
public enum LogLevel
|
||||
{
|
||||
Verbose = 0,
|
||||
Debug = 1,
|
||||
Information = 2,
|
||||
Warning = 3,
|
||||
Error = 4,
|
||||
Fatal = 5
|
||||
}
|
||||
|
||||
public class Configuration
|
||||
{
|
||||
public LogLevel LogLevel { get; set; } = LogLevel.Error;
|
||||
}
|
||||
|
||||
public static Logger Instance { get; } = new Logger();
|
||||
|
||||
private Logger()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Initialize(string filePath, Configuration configuration)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Async(a => a.File(filePath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7))
|
||||
.MinimumLevel.Is((LogEventLevel)configuration.LogLevel)
|
||||
.CreateLogger();
|
||||
|
||||
// Log file is not created during unhandled exception processing.
|
||||
// Force log file creation with the highest severity initialization record.
|
||||
Log.Fatal("Initialized");
|
||||
}
|
||||
|
||||
public void Debug(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Debug(text, propertyValues, propertyValues);
|
||||
}
|
||||
|
||||
public void Information(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Information(text, propertyValues);
|
||||
}
|
||||
|
||||
public void Warning(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Warning(text, propertyValues);
|
||||
}
|
||||
|
||||
public void Error(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Error(text, propertyValues);
|
||||
}
|
||||
|
||||
public void Error(Exception exception, string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Error(exception, text, propertyValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public class Terminal
|
||||
{
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private Func<Task<string>> _selectedTextCallback;
|
||||
private bool _closingFromUI;
|
||||
private bool _exited;
|
||||
private readonly bool _requireShellProcessStart;
|
||||
|
||||
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId();
|
||||
_requireShellProcessStart = !terminalId.HasValue;
|
||||
}
|
||||
|
||||
private void OnTerminalExited(object sender, TerminalExitStatus status)
|
||||
{
|
||||
if (status.TerminalId != Id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_exited = true;
|
||||
Exited?.Invoke(this, status.ExitCode);
|
||||
|
||||
if (_closingFromUI || status.ExitCode <= 0)
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
|
||||
_trayProcessCommunicationService.TerminalExited -= OnTerminalExited;
|
||||
}
|
||||
|
||||
public event EventHandler<int> Exited;
|
||||
|
||||
/// <summary>
|
||||
/// To be observed by both view and viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler Closed;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<string> KeyboardCommandReceived;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by view
|
||||
/// </summary>
|
||||
public event EventHandler<byte[]> OutputReceived;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<TerminalSize> SizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<string> TitleChanged;
|
||||
|
||||
public string FallbackTitle { get; private set; }
|
||||
|
||||
public byte Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// To be called by either view or viewmodel
|
||||
/// </summary>
|
||||
public async Task Close()
|
||||
{
|
||||
if (_exited)
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
_closingFromUI = true;
|
||||
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
|
||||
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by viewmodel
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<string> GetSelectedText()
|
||||
{
|
||||
return _selectedTextCallback?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view
|
||||
/// </summary>
|
||||
public void ProcessKeyboardCommand(string command)
|
||||
{
|
||||
KeyboardCommandReceived?.Invoke(this, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be set by view
|
||||
/// </summary>
|
||||
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
|
||||
{
|
||||
_selectedTextCallback = selectedTextCallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be called by view
|
||||
/// </summary>
|
||||
public async Task SetSize(TerminalSize size)
|
||||
{
|
||||
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
|
||||
SizeChanged?.Invoke(this, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view
|
||||
/// </summary>
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
title = FallbackTitle;
|
||||
}
|
||||
TitleChanged?.Invoke(this, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view when ready
|
||||
/// </summary>
|
||||
/// <param name="shellProfile"></param>
|
||||
/// <param name="size"></param>
|
||||
/// <param name="sessionType"></param>
|
||||
public async Task<TerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
|
||||
{
|
||||
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.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
FallbackTitle = response.ShellExecutableName;
|
||||
SetTitle(FallbackTitle);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
else
|
||||
{
|
||||
return await _trayProcessCommunicationService.PauseTerminalOutput(Id, false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task Write(byte[] data)
|
||||
{
|
||||
return _trayProcessCommunicationService.Write(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace FluentTerminal.App.Services.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Get strings in different languages.
|
||||
/// Author: Zuozishi
|
||||
/// </summary>
|
||||
public static class I18N
|
||||
{
|
||||
public static string Translate(string resource)
|
||||
{
|
||||
var resourceLoader = ResourceLoader.GetForCurrentView();
|
||||
return resourceLoader.GetString(resource.Replace('.', '/'));
|
||||
}
|
||||
|
||||
public static string TranslateWithFallback(string resource, string fallback)
|
||||
{
|
||||
var result = Translate(resource);
|
||||
|
||||
return string.IsNullOrEmpty(result) ? fallback : result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class EnvironmentVariableViewModel : ViewModelBase
|
||||
{
|
||||
private string _name;
|
||||
private string _value;
|
||||
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(ref _name, value);
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set => Set(ref _value, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fleck" Version="1.0.3" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public interface ISessionSuccessTracker
|
||||
{
|
||||
void SetSuccessfulSessionStart();
|
||||
|
||||
void SetOutputReceived();
|
||||
|
||||
void SetInvalid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public interface ITerminalView
|
||||
{
|
||||
Task ChangeTheme(TerminalTheme theme);
|
||||
Task ChangeKeyBindings();
|
||||
Task ChangeOptions(TerminalOptions options);
|
||||
Task Initialize(TerminalViewModel viewModel);
|
||||
void DisposalPrepare();
|
||||
Task FindNext(string searchText);
|
||||
Task FindPrevious(string searchText);
|
||||
Task FocusTerminal();
|
||||
Task<string> SerializeXtermState();
|
||||
}
|
||||
}
|
||||
@@ -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<in T> : ICommand
|
||||
{
|
||||
Task ExecuteAsync(T parameter);
|
||||
bool CanExecute(T parameter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Infrastructure
|
||||
{
|
||||
public interface IErrorHandler
|
||||
{
|
||||
void HandleError(Exception ex);
|
||||
}
|
||||
}
|
||||
@@ -1,271 +1,626 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.EventArgs;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDispatcherTimer _dispatcherTimer;
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private ApplicationSettings _applicationSettings;
|
||||
private string _background;
|
||||
private double _backgroundOpacity;
|
||||
private TerminalViewModel _selectedTerminal;
|
||||
private TabsPosition _tabsPosition;
|
||||
|
||||
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_dialogService = dialogService;
|
||||
_applicationView = applicationView;
|
||||
_dispatcherTimer = dispatcherTimer;
|
||||
_clipboardService = clipboardService;
|
||||
_keyboardCommandService = keyboardCommandService;
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.NewTab, () => AddTerminal(null, false));
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.ConfigurableNewTab, () => AddTerminal(null, true));
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.CloseTab, CloseCurrentTab);
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.NextTab, SelectNextTab);
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.PreviousTab, SelectPreviousTab);
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.NewWindow, NewWindow);
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.ShowSettings, ShowSettings);
|
||||
_keyboardCommandService.RegisterCommandHandler(Command.ToggleFullScreen, ToggleFullScreen);
|
||||
var currentTheme = _settingsService.GetCurrentTheme();
|
||||
var options = _settingsService.GetTerminalOptions();
|
||||
Background = currentTheme.Colors.Background;
|
||||
BackgroundOpacity = options.BackgroundOpacity;
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
TabsPosition = _applicationSettings.TabsPosition;
|
||||
|
||||
AddTerminalCommand = new RelayCommand(() => AddTerminal(null, false));
|
||||
ShowSettingsCommand = new RelayCommand(ShowSettings);
|
||||
|
||||
_applicationView.CloseRequested += OnCloseRequest;
|
||||
Terminals.CollectionChanged += OnTerminalsCollectionChanged;
|
||||
}
|
||||
|
||||
private void OnTerminalsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RaisePropertyChanged(nameof(ShowTabsOnTop));
|
||||
RaisePropertyChanged(nameof(ShowTabsOnBottom));
|
||||
}
|
||||
|
||||
public event EventHandler Closed;
|
||||
|
||||
public event EventHandler NewWindowRequested;
|
||||
|
||||
public event EventHandler ShowSettingsRequested;
|
||||
|
||||
public RelayCommand AddTerminalCommand { get; }
|
||||
|
||||
public bool ShowTabsOnTop
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Top && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
|
||||
}
|
||||
|
||||
public bool ShowTabsOnBottom
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Bottom && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
|
||||
}
|
||||
|
||||
public string Background
|
||||
{
|
||||
get => _background;
|
||||
set => Set(ref _background, value);
|
||||
}
|
||||
|
||||
public double BackgroundOpacity
|
||||
{
|
||||
get => _backgroundOpacity;
|
||||
set => Set(ref _backgroundOpacity, value);
|
||||
}
|
||||
|
||||
public TerminalViewModel SelectedTerminal
|
||||
{
|
||||
get => _selectedTerminal;
|
||||
set
|
||||
{
|
||||
if (SelectedTerminal != null)
|
||||
{
|
||||
_selectedTerminal.IsSelected = false;
|
||||
}
|
||||
if (Set(ref _selectedTerminal, value))
|
||||
{
|
||||
SelectedTerminal?.FocusTerminal();
|
||||
if (SelectedTerminal != null)
|
||||
{
|
||||
SelectedTerminal.IsSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand ShowSettingsCommand { get; }
|
||||
|
||||
public TabsPosition TabsPosition
|
||||
{
|
||||
get => _tabsPosition;
|
||||
set => Set(ref _tabsPosition, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
|
||||
|
||||
public Task AddTerminal(string startupDirectory, bool showProfileSelection)
|
||||
{
|
||||
return _applicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
ShellProfile profile = null;
|
||||
if (showProfileSelection)
|
||||
{
|
||||
profile = await _dialogService.ShowProfileSelectionDialogAsync();
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
profile = _settingsService.GetDefaultShellProfile();
|
||||
}
|
||||
|
||||
var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService,
|
||||
_applicationSettings, startupDirectory, profile, _applicationView, _dispatcherTimer, _clipboardService);
|
||||
terminal.Closed += OnTerminalCloseRequested;
|
||||
Terminals.Add(terminal);
|
||||
|
||||
SelectedTerminal = terminal;
|
||||
});
|
||||
}
|
||||
|
||||
public void CloseAllTerminals()
|
||||
{
|
||||
foreach (var terminal in Terminals)
|
||||
{
|
||||
terminal.CloseView();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseCurrentTab()
|
||||
{
|
||||
SelectedTerminal?.CloseCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void NewWindow()
|
||||
{
|
||||
NewWindowRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
_applicationSettings = e;
|
||||
TabsPosition = e.TabsPosition;
|
||||
RaisePropertyChanged(nameof(ShowTabsOnTop));
|
||||
RaisePropertyChanged(nameof(ShowTabsOnBottom));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnCloseRequest(object sender, CancelableEventArgs e)
|
||||
{
|
||||
if (_applicationSettings.ConfirmClosingWindows)
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this window?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(false);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
CloseAllTerminals();
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Cancelled = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseAllTerminals();
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCurrentThemeChanged(object sender, Guid e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
var currentTheme = _settingsService.GetTheme(e);
|
||||
Background = currentTheme.Colors.Background;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnTerminalCloseRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
terminal.CloseView();
|
||||
if (SelectedTerminal == terminal)
|
||||
{
|
||||
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
|
||||
}
|
||||
Terminals.Remove(terminal);
|
||||
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
_applicationView.TryClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
BackgroundOpacity = e.BackgroundOpacity;
|
||||
});
|
||||
}
|
||||
|
||||
private void SelectNextTab()
|
||||
{
|
||||
var currentIndex = Terminals.IndexOf(SelectedTerminal);
|
||||
var nextIndex = (currentIndex + 1) % Terminals.Count;
|
||||
SelectedTerminal = Terminals[nextIndex];
|
||||
}
|
||||
|
||||
private void SelectPreviousTab()
|
||||
{
|
||||
var currentIndex = Terminals.IndexOf(SelectedTerminal);
|
||||
var previousIndex = (currentIndex - 1 + Terminals.Count) % Terminals.Count;
|
||||
SelectedTerminal = Terminals[previousIndex];
|
||||
}
|
||||
|
||||
private void ShowSettings()
|
||||
{
|
||||
ShowSettingsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void ToggleFullScreen()
|
||||
{
|
||||
_applicationView.ToggleFullScreen();
|
||||
}
|
||||
}
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.EventArgs;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models.Messages;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDispatcherTimer _dispatcherTimer;
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private ApplicationSettings _applicationSettings;
|
||||
private string _background;
|
||||
private double _backgroundOpacity;
|
||||
private TerminalViewModel _selectedTerminal;
|
||||
private TabsPosition _tabsPosition;
|
||||
private string _windowTitle;
|
||||
|
||||
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
{
|
||||
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
|
||||
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
|
||||
MessengerInstance.Register<ShellProfileAddedMessage>(this, OnShellProfileAdded);
|
||||
MessengerInstance.Register<ShellProfileDeletedMessage>(this, OnShellProfileDeleted);
|
||||
MessengerInstance.Register<SshProfileAddedMessage>(this, OnSshProfileAdded);
|
||||
MessengerInstance.Register<SshProfileDeletedMessage>(this, OnSshProfileDeleted);
|
||||
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
|
||||
|
||||
_settingsService = settingsService;
|
||||
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_dialogService = dialogService;
|
||||
ApplicationView = applicationView;
|
||||
_dispatcherTimer = dispatcherTimer;
|
||||
_clipboardService = clipboardService;
|
||||
_keyboardCommandService = keyboardCommandService;
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), async () => await AddLocalTabAsync());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshTab), async () => await AddSshTabAsync());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewCustomCommandTab), async () => await AddCustomCommandTabAsync());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewTab), async () => await AddConfigurableTerminal());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), async () => await SelectedTerminal.EditTitle());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.SavedSshNewTab), async () => await AddSavedSshTerminalAsync());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.SavedSshNewWindow), () => NewWindow(NewWindowAction.ShowSshProfileSelection));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshWindow), () => NewWindow(NewWindowAction.ShowSshInfoDialog));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewCustomCommandWindow), () => NewWindow(NewWindowAction.ShowCustomCommandDialog));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.DuplicateTab), async () => await AddTerminalAsync(SelectedTerminal.ShellProfile.Clone()));
|
||||
|
||||
// Add all of the commands for switching to a tab of a given ID, if there's one open there
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var switchCmd = Command.SwitchToTerm1 + i;
|
||||
int tabNumber = i;
|
||||
// ReSharper disable once InconsistentNaming
|
||||
void handler() => SelectTabNumber(tabNumber);
|
||||
_keyboardCommandService.RegisterCommandHandler(switchCmd.ToString(), handler);
|
||||
}
|
||||
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NextTab), SelectNextTab);
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.PreviousTab), SelectPreviousTab);
|
||||
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), () => NewWindow(NewWindowAction.StartDefaultLocalTerminal));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewWindow), () => NewWindow(NewWindowAction.ShowProfileSelection));
|
||||
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ShowSettings), ShowSettings);
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ToggleFullScreen), ToggleFullScreen);
|
||||
|
||||
foreach (ShellProfile profile in _settingsService.GetShellProfiles())
|
||||
{
|
||||
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), async () => await AddLocalTabOrWindowAsync(profile.Id));
|
||||
}
|
||||
|
||||
foreach (SshProfile profile in _settingsService.GetSshProfiles())
|
||||
{
|
||||
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), async () => await AddSshTabOrWindowAsync(profile.Id));
|
||||
}
|
||||
|
||||
var currentTheme = _settingsService.GetCurrentTheme();
|
||||
var options = _settingsService.GetTerminalOptions();
|
||||
Background = currentTheme.Colors.Background;
|
||||
BackgroundOpacity = options.BackgroundOpacity;
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
TabsPosition = _applicationSettings.TabsPosition;
|
||||
|
||||
AddLocalShellCommand = new RelayCommand(async () => await AddLocalTabAsync());
|
||||
AddSshShellCommand = new RelayCommand(async () => await AddSshTabAsync());
|
||||
AddQuickShellCommand = new RelayCommand(async () => await AddCustomCommandTabAsync());
|
||||
ShowAboutCommand = new AsyncCommand(ShowAbout);
|
||||
ShowSettingsCommand = new RelayCommand(ShowSettings);
|
||||
|
||||
ApplicationView.CloseRequested += OnCloseRequest;
|
||||
ApplicationView.Closed += OnClosed;
|
||||
Terminals.CollectionChanged += OnTerminalsCollectionChanged;
|
||||
}
|
||||
|
||||
private void OnClosed(object sender, EventArgs e)
|
||||
{
|
||||
MessengerInstance.Unregister(this);
|
||||
|
||||
ApplicationView.CloseRequested -= OnCloseRequest;
|
||||
ApplicationView.Closed -= OnClosed;
|
||||
Terminals.CollectionChanged -= OnTerminalsCollectionChanged;
|
||||
|
||||
_keyboardCommandService.ClearCommandHandlers();
|
||||
|
||||
_applicationSettings = null;
|
||||
|
||||
AddLocalShellCommand = null;
|
||||
AddSshShellCommand = null;
|
||||
AddQuickShellCommand = null;
|
||||
ShowAboutCommand = null;
|
||||
ShowSettingsCommand = null;
|
||||
|
||||
Closed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void OnShellProfileDeleted(ShellProfileDeletedMessage message)
|
||||
{
|
||||
_keyboardCommandService.DeregisterCommandHandler(message.ProfileId.ToString());
|
||||
}
|
||||
|
||||
private void OnShellProfileAdded(ShellProfileAddedMessage message)
|
||||
{
|
||||
_keyboardCommandService.RegisterCommandHandler(message.ShellProfile.Id.ToString(),
|
||||
() => AddLocalTabOrWindowAsync(message.ShellProfile.Id));
|
||||
}
|
||||
|
||||
private void OnSshProfileAdded(SshProfileAddedMessage message)
|
||||
{
|
||||
_keyboardCommandService.RegisterCommandHandler(message.SshProfile.Id.ToString(),
|
||||
() => AddSshTabOrWindowAsync(message.SshProfile.Id));
|
||||
}
|
||||
private void OnSshProfileDeleted(SshProfileDeletedMessage message)
|
||||
{
|
||||
_keyboardCommandService.DeregisterCommandHandler(message.ProfileId.ToString());
|
||||
}
|
||||
|
||||
private void OnTerminalsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RaisePropertyChanged(nameof(ShowTabsOnTop));
|
||||
RaisePropertyChanged(nameof(ShowTabsOnBottom));
|
||||
}
|
||||
|
||||
public event EventHandler Closed;
|
||||
|
||||
public event EventHandler<NewWindowRequestedEventArgs> NewWindowRequested;
|
||||
|
||||
public event EventHandler ShowSettingsRequested;
|
||||
|
||||
public event EventHandler ActivatedMv;
|
||||
|
||||
public event EventHandler<TerminalViewModel> TabTearedOff;
|
||||
|
||||
public void TearOffTab(TerminalViewModel model)
|
||||
{
|
||||
TabTearedOff?.Invoke(this, model);
|
||||
}
|
||||
|
||||
public void FocusWindow()
|
||||
{
|
||||
ActivatedMv?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public RelayCommand AddLocalShellCommand { get; private set; }
|
||||
public RelayCommand AddSshShellCommand { get; private set; }
|
||||
public RelayCommand AddQuickShellCommand { get; private set; }
|
||||
|
||||
public string WindowTitle
|
||||
{
|
||||
get => _windowTitle;
|
||||
set
|
||||
{
|
||||
if (Set(ref _windowTitle, value))
|
||||
{
|
||||
ApplicationView.Title = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowTabsOnTop
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Top;
|
||||
}
|
||||
|
||||
public bool ShowTabsOnBottom
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Bottom;
|
||||
}
|
||||
|
||||
public string Background
|
||||
{
|
||||
get => _background;
|
||||
set => Set(ref _background, value);
|
||||
}
|
||||
|
||||
public double BackgroundOpacity
|
||||
{
|
||||
get => _backgroundOpacity;
|
||||
set => Set(ref _backgroundOpacity, value);
|
||||
}
|
||||
|
||||
public TerminalViewModel SelectedTerminal
|
||||
{
|
||||
get => _selectedTerminal;
|
||||
set
|
||||
{
|
||||
if (SelectedTerminal != null)
|
||||
{
|
||||
_selectedTerminal.IsSelected = false;
|
||||
}
|
||||
if (Set(ref _selectedTerminal, value))
|
||||
{
|
||||
SelectedTerminal?.FocusTerminal();
|
||||
if (SelectedTerminal != null)
|
||||
{
|
||||
SelectedTerminal.IsSelected = true;
|
||||
if (_applicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
WindowTitle = SelectedTerminal.TabTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowTitle = SelectedTerminal.ShellTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IAsyncCommand ShowAboutCommand { get; private set; }
|
||||
|
||||
public RelayCommand ShowSettingsCommand { get; private set; }
|
||||
|
||||
public TabsPosition TabsPosition
|
||||
{
|
||||
get => _tabsPosition;
|
||||
set => Set(ref _tabsPosition, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
|
||||
|
||||
public IApplicationView ApplicationView { get; }
|
||||
|
||||
public async Task AddConfigurableTerminal()
|
||||
{
|
||||
var profile = await _dialogService.ShowProfileSelectionDialogAsync();
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await AddTerminalAsync(profile);
|
||||
}
|
||||
|
||||
public async Task AddSshTabAsync()
|
||||
{
|
||||
SshProfile profile = await _dialogService.ShowSshConnectionInfoDialogAsync();
|
||||
if (profile == null)
|
||||
{
|
||||
// User selected "Cancel"
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await AddTerminalAsync(profile);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddCustomCommandTabAsync()
|
||||
{
|
||||
var profile = await _dialogService.ShowCustomCommandDialogAsync();
|
||||
if (profile == null)
|
||||
{
|
||||
// User selected "Cancel"
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await AddTerminalAsync(profile);
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddSshTabOrWindowAsync(Guid shellProfileId)
|
||||
{
|
||||
switch (_applicationSettings.NewTerminalLocation)
|
||||
{
|
||||
case NewTerminalLocation.Tab:
|
||||
var profile = _settingsService.GetSshProfile(shellProfileId);
|
||||
return AddTerminalAsync(profile);
|
||||
case NewTerminalLocation.Window:
|
||||
NewWindow(NewWindowAction.StartSshTerminal, shellProfileId);
|
||||
return Task.FromResult(0);
|
||||
default:
|
||||
throw new ArgumentException("unknown NewTerminalLocation");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddSavedSshTerminalAsync()
|
||||
{
|
||||
ShellProfile profile = await _dialogService.ShowSshProfileSelectionDialogAsync();
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
// User selected "Cancel"
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await AddTerminalAsync(profile);
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddLocalTabAsync()
|
||||
{
|
||||
var profile = _settingsService.GetDefaultShellProfile();
|
||||
return AddTerminalAsync(profile);
|
||||
}
|
||||
|
||||
public Task AddLocalTabOrWindowAsync(Guid shellProfileId)
|
||||
{
|
||||
switch (_applicationSettings.NewTerminalLocation)
|
||||
{
|
||||
case NewTerminalLocation.Tab:
|
||||
var profile = _settingsService.GetShellProfile(shellProfileId);
|
||||
return AddTerminalAsync(profile);
|
||||
case NewTerminalLocation.Window:
|
||||
NewWindow(NewWindowAction.StartLocalTerminal, shellProfileId);
|
||||
return Task.FromResult(0);
|
||||
default:
|
||||
throw new ArgumentException("unknown NewTerminalLocation");
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddTerminalAsync(ShellProfile profile, string terminalState, int position)
|
||||
{
|
||||
return ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService,
|
||||
_applicationSettings, profile, ApplicationView, _dispatcherTimer, _clipboardService, terminalState);
|
||||
|
||||
terminal.Closed += OnTerminalClosed;
|
||||
terminal.ShellTitleChanged += Terminal_ShellTitleChanged;
|
||||
terminal.CustomTitleChanged += Terminal_CustomTitleChanged;
|
||||
terminal.CloseLeftTabsRequested += Terminal_CloseLeftTabsRequested;
|
||||
terminal.CloseRightTabsRequested += Terminal_CloseRightTabsRequested;
|
||||
terminal.CloseOtherTabsRequested += Terminal_CloseOtherTabsRequested;
|
||||
terminal.DuplicateTabRequested += Terminal_DuplicateTabRequested;
|
||||
Terminals.Insert(Math.Min(position, Terminals.Count), terminal);
|
||||
|
||||
SelectedTerminal = terminal;
|
||||
});
|
||||
}
|
||||
|
||||
private async void Terminal_DuplicateTabRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
await AddTerminalAsync(terminal.ShellProfile.Clone());
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_CloseOtherTabsRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
Array.ForEach<TerminalViewModel>(Terminals.ToArray(),
|
||||
async t => {
|
||||
if (terminal != t)
|
||||
{
|
||||
Logger.Instance.Debug("Terminal with Id: {@id} closed.", t.Terminal.Id);
|
||||
await t.CloseCommand.ExecuteAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async void Terminal_CloseRightTabsRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
for (int i = Terminals.Count - 1; i > Terminals.IndexOf(terminal); --i)
|
||||
{
|
||||
var terminalToRemove = Terminals[i];
|
||||
Logger.Instance.Debug("Terminal with Id: {@id} closed.", terminalToRemove.Terminal.Id);
|
||||
await terminalToRemove.CloseCommand.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Terminal_CloseLeftTabsRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
for(int i = Terminals.IndexOf(terminal) - 1; i >= 0; --i)
|
||||
{
|
||||
var terminalToRemove = Terminals[i];
|
||||
Logger.Instance.Debug("Terminal with Id: {@id} closed.", terminalToRemove.Terminal.Id);
|
||||
await terminalToRemove.CloseCommand.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddTerminalAsync(ShellProfile profile)
|
||||
{
|
||||
return AddTerminalAsync(profile, "", Terminals.Count);
|
||||
}
|
||||
|
||||
public Task AddTerminalAsync(string terminalState, int position)
|
||||
{
|
||||
return AddTerminalAsync(new ShellProfile(), terminalState, position);
|
||||
}
|
||||
|
||||
private void Terminal_CustomTitleChanged(object sender, string e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
if (terminal.IsSelected && _applicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e))
|
||||
{
|
||||
WindowTitle = terminal.ShellTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowTitle = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_ShellTitleChanged(object sender, string e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal && terminal.IsSelected && !_applicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
WindowTitle = e;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CloseAllTerminals()
|
||||
{
|
||||
foreach (var terminal in Terminals)
|
||||
{
|
||||
terminal.Closed -= OnTerminalClosed;
|
||||
terminal.ShellTitleChanged -= Terminal_ShellTitleChanged;
|
||||
terminal.CustomTitleChanged -= Terminal_CustomTitleChanged;
|
||||
terminal.CloseLeftTabsRequested -= Terminal_CloseLeftTabsRequested;
|
||||
terminal.CloseRightTabsRequested -= Terminal_CloseRightTabsRequested;
|
||||
terminal.CloseOtherTabsRequested -= Terminal_CloseOtherTabsRequested;
|
||||
terminal.DuplicateTabRequested -= Terminal_DuplicateTabRequested;
|
||||
await terminal.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseCurrentTab()
|
||||
{
|
||||
SelectedTerminal?.CloseCommand.ExecuteAsync();
|
||||
}
|
||||
|
||||
private void NewWindow(NewWindowAction showSelection)
|
||||
{
|
||||
NewWindow(showSelection, Guid.Empty);
|
||||
}
|
||||
|
||||
private void NewWindow(NewWindowAction showSelection, Guid profileId)
|
||||
{
|
||||
var args = new NewWindowRequestedEventArgs
|
||||
{
|
||||
Action = showSelection,
|
||||
ProfileId = profileId
|
||||
};
|
||||
NewWindowRequested?.Invoke(this, args);
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
|
||||
{
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
_applicationSettings = message.ApplicationSettings;
|
||||
TabsPosition = message.ApplicationSettings.TabsPosition;
|
||||
RaisePropertyChanged(nameof(ShowTabsOnTop));
|
||||
RaisePropertyChanged(nameof(ShowTabsOnBottom));
|
||||
|
||||
if (message.ApplicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
WindowTitle = SelectedTerminal?.TabTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowTitle = SelectedTerminal?.ShellTitle;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnCloseRequest(object sender, CancelableEventArgs e)
|
||||
{
|
||||
if (_applicationSettings.ConfirmClosingWindows)
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmCloseWindow"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(false);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
await CloseAllTerminals();
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Cancelled = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await CloseAllTerminals();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
|
||||
{
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
var currentTheme = _settingsService.GetTheme(message.ThemeId);
|
||||
Background = currentTheme.Colors.Background;
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnTerminalClosed(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
if (SelectedTerminal == terminal)
|
||||
{
|
||||
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
|
||||
}
|
||||
Logger.Instance.Debug("Terminal with Id: {@id} closed.", terminal.Terminal.Id);
|
||||
|
||||
terminal.Closed -= OnTerminalClosed;
|
||||
terminal.ShellTitleChanged -= Terminal_ShellTitleChanged;
|
||||
terminal.CustomTitleChanged -= Terminal_CustomTitleChanged;
|
||||
terminal.CloseLeftTabsRequested -= Terminal_CloseLeftTabsRequested;
|
||||
terminal.CloseRightTabsRequested -= Terminal_CloseRightTabsRequested;
|
||||
terminal.CloseOtherTabsRequested -= Terminal_CloseOtherTabsRequested;
|
||||
terminal.DuplicateTabRequested -= Terminal_DuplicateTabRequested;
|
||||
|
||||
Terminals.Remove(terminal);
|
||||
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
|
||||
{
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
BackgroundOpacity = message.TerminalOptions.BackgroundOpacity;
|
||||
});
|
||||
}
|
||||
|
||||
private void SelectTabNumber(int tabNumber)
|
||||
{
|
||||
if (tabNumber < Terminals.Count)
|
||||
{
|
||||
SelectedTerminal = Terminals[tabNumber];
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectNextTab()
|
||||
{
|
||||
var currentIndex = Terminals.IndexOf(SelectedTerminal);
|
||||
var nextIndex = (currentIndex + 1) % Terminals.Count;
|
||||
SelectedTerminal = Terminals[nextIndex];
|
||||
}
|
||||
|
||||
private void SelectPreviousTab()
|
||||
{
|
||||
var currentIndex = Terminals.IndexOf(SelectedTerminal);
|
||||
var previousIndex = (currentIndex - 1 + Terminals.Count) % Terminals.Count;
|
||||
SelectedTerminal = Terminals[previousIndex];
|
||||
}
|
||||
|
||||
private Task ShowAbout()
|
||||
{
|
||||
return _dialogService.ShowAboutDialogAsync();
|
||||
}
|
||||
|
||||
private void ShowSettings()
|
||||
{
|
||||
ShowSettingsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void ToggleFullScreen()
|
||||
{
|
||||
ApplicationView.ToggleFullScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class OverlayViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDispatcherTimer _overlayTimer;
|
||||
private bool _showOverlay;
|
||||
private string _overlayContent;
|
||||
|
||||
public OverlayViewModel(IDispatcherTimer dispatcherTimer)
|
||||
{
|
||||
_overlayTimer = dispatcherTimer;
|
||||
_overlayTimer.Interval = new TimeSpan(0, 0, 2);
|
||||
}
|
||||
|
||||
public bool ShowOverlay
|
||||
{
|
||||
get => _showOverlay;
|
||||
set => Set(ref _showOverlay, value);
|
||||
}
|
||||
|
||||
public string OverlayContent
|
||||
{
|
||||
get => _overlayContent;
|
||||
set => Set(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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for common profiles (PowerShell, CMD, WSL).
|
||||
/// </summary>
|
||||
public class CommonProfileProviderViewModel : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
public bool PreInstalled { get; private set; }
|
||||
|
||||
private string _location;
|
||||
|
||||
public string Location
|
||||
{
|
||||
get => _location;
|
||||
set => Set(ref _location, value);
|
||||
}
|
||||
|
||||
private string _workingDirectory;
|
||||
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get => _workingDirectory;
|
||||
set => Set(ref _workingDirectory, value);
|
||||
}
|
||||
|
||||
private string _arguments;
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => _arguments;
|
||||
set => Set(ref _arguments, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForCustomShellCommand { get; }
|
||||
|
||||
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructor
|
||||
|
||||
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
|
||||
true, original)
|
||||
{
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
|
||||
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
|
||||
|
||||
Initialize(Model);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
private void Initialize(ShellProfile profile)
|
||||
{
|
||||
PreInstalled = profile.PreInstalled;
|
||||
Location = profile.Location;
|
||||
Arguments = profile.Arguments;
|
||||
WorkingDirectory = profile.WorkingDirectory;
|
||||
}
|
||||
|
||||
protected override void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
base.LoadFromProfile(profile);
|
||||
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected override async Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
await base.CopyToProfileAsync(profile);
|
||||
|
||||
profile.Location = _location;
|
||||
profile.Arguments = _arguments;
|
||||
profile.WorkingDirectory = _workingDirectory;
|
||||
}
|
||||
|
||||
public override async Task<string> ValidateAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_location))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
error = I18N.Translate("LocationCantBeEmpty");
|
||||
|
||||
return string.IsNullOrEmpty(error) ? "Location cannot be empty." : error;
|
||||
}
|
||||
|
||||
public override bool HasChanges()
|
||||
{
|
||||
return base.HasChanges() || !Model.Location.NullableEqualTo(_location) ||
|
||||
!Model.WorkingDirectory.NullableEqualTo(_workingDirectory) ||
|
||||
!Model.Arguments.NullableEqualTo(_arguments);
|
||||
}
|
||||
|
||||
private async Task BrowseForCustomShell()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
Location = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseForWorkingDirectory()
|
||||
{
|
||||
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
|
||||
if (directory != null)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
public class ExecutedCommand
|
||||
{
|
||||
public string Value { get; set; }
|
||||
|
||||
public bool IsProfile { get; set; }
|
||||
|
||||
public DateTime LastExecution { get; set; }
|
||||
|
||||
public int ExecutionCount { get; set; }
|
||||
|
||||
public ShellProfile ShellProfile { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Messages;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all profile view model classes. It contains properties shared by all other view models
|
||||
/// (theme-related properties, line-ending translation, and WinPTY/ConPTY selection).
|
||||
/// </summary>
|
||||
public abstract class ProfileProviderViewModelBase : ViewModelBase
|
||||
{
|
||||
#region Static
|
||||
|
||||
private static readonly LineEndingStyle[] LineEndingStylesArray =
|
||||
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Fields
|
||||
|
||||
protected readonly ISettingsService SettingsService;
|
||||
protected readonly IApplicationView ApplicationView;
|
||||
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; }
|
||||
|
||||
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
|
||||
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
|
||||
|
||||
private LineEndingStyle _lineEndingTranslation;
|
||||
|
||||
public LineEndingStyle LineEndingTranslation
|
||||
{
|
||||
get => _lineEndingTranslation;
|
||||
set => Set(ref _lineEndingTranslation, value);
|
||||
}
|
||||
|
||||
private TabTheme _selectedTabTheme;
|
||||
|
||||
public TabTheme SelectedTabTheme
|
||||
{
|
||||
get => _selectedTabTheme ?? (_selectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(_tabThemeId)));
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// Ignore attempt setting null.
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that it's from the list
|
||||
var theme = TabThemes.Contains(value)
|
||||
? value
|
||||
: TabThemes.FirstOrDefault(t => t.Id == value.Id) ?? TabThemes.First();
|
||||
|
||||
if (Set(ref _selectedTabTheme, theme))
|
||||
{
|
||||
_tabThemeId = theme.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _tabThemeId;
|
||||
|
||||
public int TabThemeId
|
||||
{
|
||||
get => _tabThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _tabThemeId, value))
|
||||
{
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalTheme _selectedTerminalTheme;
|
||||
|
||||
public TerminalTheme SelectedTerminalTheme
|
||||
{
|
||||
get => _selectedTerminalTheme ??
|
||||
(_selectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(_terminalThemeId)));
|
||||
set
|
||||
{
|
||||
TerminalTheme theme = value;
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
// How to handle attempt to setting null theme?
|
||||
// 1) Throw an exception:
|
||||
//throw new ArgumentNullException();
|
||||
// 2) Ignore the attempt:
|
||||
//return;
|
||||
// 3) Use default theme (probably the best):
|
||||
theme = TerminalThemes.First();
|
||||
}
|
||||
else if (!TerminalThemes.Contains(theme))
|
||||
{
|
||||
// Ensure that it's from the list
|
||||
theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(theme.Id)) ?? TerminalThemes.First();
|
||||
}
|
||||
|
||||
if (Set(ref _selectedTerminalTheme, theme))
|
||||
{
|
||||
_terminalThemeId = theme.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Guid _terminalThemeId;
|
||||
|
||||
public Guid TerminalThemeId
|
||||
{
|
||||
get => _terminalThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _terminalThemeId, value))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _useConPty;
|
||||
|
||||
public bool UseConPty
|
||||
{
|
||||
get => _useConPty;
|
||||
set => Set(ref _useConPty, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected ProfileProviderViewModelBase(ISettingsService settingsService, IApplicationView applicationView,
|
||||
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();
|
||||
|
||||
MessengerInstance.Register<ThemeAddedMessage>(this, OnThemeAdded);
|
||||
MessengerInstance.Register<ThemeDeletedMessage>(this, OnThemeDeleted);
|
||||
|
||||
Initialize(Model);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
private void OnThemeDeleted(ThemeDeletedMessage message)
|
||||
{
|
||||
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(message.ThemeId));
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
TerminalThemes.Remove(theme);
|
||||
|
||||
if (_terminalThemeId.Equals(message.ThemeId))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.First();
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
private void OnThemeAdded(ThemeAddedMessage message)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => TerminalThemes.Add(message.Theme), false);
|
||||
}
|
||||
|
||||
private void Initialize(ShellProfile profile)
|
||||
{
|
||||
LineEndingTranslation = profile.LineEndingTranslation;
|
||||
UseConPty = profile.UseConPty;
|
||||
TerminalThemeId = profile.TerminalThemeId;
|
||||
TabThemeId = profile.TabThemeId;
|
||||
}
|
||||
|
||||
protected virtual void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected virtual Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
profile.LineEndingTranslation = _lineEndingTranslation;
|
||||
profile.UseConPty = _useConPty;
|
||||
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.LineEndingTranslation != _lineEndingTranslation ||
|
||||
Model.UseConPty != _useConPty ||
|
||||
!Model.TerminalThemeId.Equals(_terminalThemeId) ||
|
||||
Model.TabThemeId != _tabThemeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies values from the view model to the underlying <see cref="ShellProfile"/>.
|
||||
/// </summary>
|
||||
/// <param name="acceptIfInvalid">Defines what will happen if the view model data is invalid (if
|
||||
/// <see cref="ValidateAsync"/> returns non-empty string). If this parameter is set to <c>true</c>, the
|
||||
/// data will be copied to the underlying <see cref="ShellProfile"/> anyway. If it's set to <c>false</c>,
|
||||
/// (default) the underlying <see cref="ShellProfile"/> won't be changed at all.</param>
|
||||
/// <returns><c>null</c> or empty string if the operation was successful (the data is valid), or an error
|
||||
/// message returned by <see cref="ValidateAsync"/> method.</returns>
|
||||
public async Task<string> AcceptChangesAsync(bool acceptIfInvalid = false)
|
||||
{
|
||||
var error = await ValidateAsync();
|
||||
|
||||
if (acceptIfInvalid || string.IsNullOrEmpty(error))
|
||||
{
|
||||
await CopyToProfileAsync(Model);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public void RejectChanges()
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => LoadFromProfile(Model), false);
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Links/shortcuts related
|
||||
|
||||
private const string UseConPtyQueryStringName = "conpty";
|
||||
private const string LineEndingQueryStringName = "lineending";
|
||||
private const string TerminalThemeIdQueryStringName = "theme";
|
||||
private const string TabThemeIdQueryStringName = "tab";
|
||||
|
||||
private const string ShortcutFileFormat = @"[{{000214A0-0000-0000-C000-000000000046}}]
|
||||
Prop3=19,0
|
||||
[InternetShortcut]
|
||||
IDList=
|
||||
URL={0}
|
||||
";
|
||||
|
||||
public static string GetShortcutFileContent(string url) => string.Format(ShortcutFileFormat, url);
|
||||
|
||||
public static IEnumerable<Tuple<string, string>> ParseParams(string uriOpts, char separator) =>
|
||||
uriOpts.Split(separator).Select(ParseSshOptionFromUri).Where(p => p != null);
|
||||
|
||||
private static Tuple<string, string> ParseSshOptionFromUri(string option)
|
||||
{
|
||||
string[] nv = option.Split('=');
|
||||
|
||||
if (nv.Length != 2 || string.IsNullOrEmpty(nv[0]))
|
||||
{
|
||||
// For now simply ignore invalid options
|
||||
return null;
|
||||
//throw new FormatException($"Invalid SSH option '{option}'.");
|
||||
}
|
||||
|
||||
return Tuple.Create(HttpUtility.UrlDecode(nv[0]), HttpUtility.UrlDecode(nv[1]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates URL.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Tuple"/> whose first item represents success. If the first item is <c>true</c>,
|
||||
/// the second contains the URL. If the first item is <c>false</c>, the second contains error message.</returns>
|
||||
public virtual Task<Tuple<bool, string>> GetUrlAsync() =>
|
||||
Task.FromResult(Tuple.Create(false, "Not supported."));
|
||||
|
||||
public string GetBaseQueryString()
|
||||
{
|
||||
var queryString =
|
||||
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
|
||||
|
||||
if (_tabThemeId != TabThemes.First().Id)
|
||||
{
|
||||
queryString += $"&{TabThemeIdQueryStringName}={_tabThemeId:##########}";
|
||||
}
|
||||
|
||||
if (!_terminalThemeId.Equals(TerminalThemes.First().Id))
|
||||
{
|
||||
queryString += $"&{TerminalThemeIdQueryStringName}={_terminalThemeId}";
|
||||
}
|
||||
|
||||
return queryString;
|
||||
}
|
||||
|
||||
public void LoadBaseFromQueryString(IList<Tuple<string, string>> queryStringParams)
|
||||
{
|
||||
if (queryStringParams == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
UseConPtyQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useConPty))
|
||||
{
|
||||
UseConPty = useConPty;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
LineEndingQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && Enum.TryParse(keyValue.Item2, true, out LineEndingStyle lineEndingTranslation))
|
||||
{
|
||||
LineEndingTranslation = lineEndingTranslation;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
TabThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && int.TryParse(keyValue.Item2, out int tabThemeId))
|
||||
{
|
||||
TabThemeId = tabThemeId;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
TerminalThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && Guid.TryParse(keyValue.Item2, out Guid terminalThemeId))
|
||||
{
|
||||
TerminalThemeId = terminalThemeId;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Links/shortcuts related
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
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 FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// View-model for rich-UI based SSH profiles.
|
||||
/// </summary>
|
||||
public class SshConnectViewModel : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Static
|
||||
|
||||
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 => Set(ref _host, value);
|
||||
}
|
||||
|
||||
private ushort _sshPort;
|
||||
|
||||
public ushort SshPort
|
||||
{
|
||||
get => _sshPort;
|
||||
set => Set(ref _sshPort, value);
|
||||
}
|
||||
|
||||
private string _username;
|
||||
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => Set(ref _username, value);
|
||||
}
|
||||
|
||||
private string _identityFile;
|
||||
|
||||
public string IdentityFile
|
||||
{
|
||||
get => _identityFile;
|
||||
set => Set(ref _identityFile, value);
|
||||
}
|
||||
|
||||
private bool _useMosh;
|
||||
|
||||
public bool UseMosh
|
||||
{
|
||||
get => _useMosh;
|
||||
set => Set(ref _useMosh, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortFrom;
|
||||
|
||||
public ushort MoshPortFrom
|
||||
{
|
||||
get => _moshPortFrom;
|
||||
set => Set(ref _moshPortFrom, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortTo;
|
||||
|
||||
public ushort MoshPortTo
|
||||
{
|
||||
get => _moshPortTo;
|
||||
set => Set(ref _moshPortTo, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForIdentityFileCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#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 })
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
Initialize((SshProfile)Model);
|
||||
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
|
||||
}
|
||||
|
||||
#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.GetUserName().ContinueWith(t =>
|
||||
{
|
||||
var username = t.Result;
|
||||
|
||||
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => Username = username, false);
|
||||
}
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
|
||||
IdentityFile = sshProfile.IdentityFile;
|
||||
UseMosh = sshProfile.UseMosh;
|
||||
MoshPortFrom = sshProfile.MoshPortFrom;
|
||||
MoshPortTo = sshProfile.MoshPortTo;
|
||||
}
|
||||
|
||||
private async Task BrowseForIdentityFile()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { "*" });
|
||||
if (file != null)
|
||||
{
|
||||
IdentityFile = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetArgumentsString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (_sshPort != SshProfile.DefaultSshPort)
|
||||
sb.Append($"-p {_sshPort:#####} ");
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
sb.Append($"-i \"{_identityFile}\" ");
|
||||
|
||||
sb.Append($"{_username}@{_host}");
|
||||
|
||||
if (_useMosh)
|
||||
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
protected override void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
base.LoadFromProfile(profile);
|
||||
|
||||
Initialize((SshProfile)profile);
|
||||
}
|
||||
|
||||
protected override async Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
await base.CopyToProfileAsync(profile);
|
||||
|
||||
SshProfile 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();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync();
|
||||
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
error = GetErrorString(result, Environment.NewLine);
|
||||
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
error = "Invalid input.";
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
|
||||
{
|
||||
var result = SshConnectionInfoValidationResult.Valid;
|
||||
|
||||
if (string.IsNullOrEmpty(_username))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.UsernameEmpty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_host))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.HostEmpty;
|
||||
}
|
||||
|
||||
if (_sshPort < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (!await CheckIdentityFileExistsAsync())
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
|
||||
}
|
||||
|
||||
if (!_useMosh)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (_moshPortFrom < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (_moshPortFrom > _moshPortTo)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortRangeInvalid;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> CheckIdentityFileExistsAsync()
|
||||
{
|
||||
var identityFile = _identityFile;
|
||||
|
||||
if (string.IsNullOrEmpty(identityFile) ||
|
||||
identityFile.Equals(_validatedIdentityFile, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Here we need to take into account that files from ssh config dir can be provided by name, without full path.
|
||||
string fullPath;
|
||||
|
||||
if (Path.IsPathRooted(identityFile))
|
||||
{
|
||||
fullPath = identityFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(sshConfigDir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
fullPath = Path.Combine(sshConfigDir, identityFile);
|
||||
}
|
||||
|
||||
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
|
||||
IdentityFile = fullPath;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetValidatedIdentityFile(string identityFile)
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
IdentityFile = identityFile;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region 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();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync();
|
||||
|
||||
if (result != SshConnectionInfoValidationResult.Valid &&
|
||||
// For links we can ignore missing username
|
||||
result != SshConnectionInfoValidationResult.UsernameEmpty)
|
||||
{
|
||||
|
||||
error = GetErrorString(result, Environment.NewLine);
|
||||
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
error = "Invalid input.";
|
||||
}
|
||||
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
|
||||
|
||||
sb.Append("://");
|
||||
|
||||
bool containsUserInfo = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
sb.Append(HttpUtility.UrlEncode(_username));
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
sb.Append(";");
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
sb.Append($"{IdentityFileOptionName}={HttpUtility.UrlEncode(_identityFile)}");
|
||||
}
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (containsUserInfo)
|
||||
sb.Append("@");
|
||||
|
||||
sb.Append(_host);
|
||||
|
||||
if (_sshPort != SshProfile.DefaultSshPort)
|
||||
{
|
||||
sb.Append($":{_sshPort:#####}");
|
||||
}
|
||||
|
||||
sb.Append("/");
|
||||
|
||||
bool queryStringAdded = false;
|
||||
|
||||
if (_useMosh)
|
||||
{
|
||||
sb.Append(
|
||||
$"?{ValidMoshPortsNames[0]}={_moshPortFrom:#####}-{_moshPortTo:#####}");
|
||||
|
||||
queryStringAdded = true;
|
||||
}
|
||||
|
||||
var terminalInfoQueryString = GetBaseQueryString();
|
||||
|
||||
if (!string.IsNullOrEmpty(terminalInfoQueryString))
|
||||
{
|
||||
sb.Append($"{(queryStringAdded ? "&" : "?")}{terminalInfoQueryString}");
|
||||
}
|
||||
|
||||
return Tuple.Create(true, sb.ToString());
|
||||
}
|
||||
|
||||
#endregion Links/shortcuts related
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
public class AboutPageViewModel : ViewModelBase
|
||||
{
|
||||
private const string BaseUrl = "https://github.com/felixse/FluentTerminal/releases/tag/";
|
||||
private readonly IUpdateService _updateService;
|
||||
private string _latestVersion;
|
||||
private readonly IApplicationView _applicationView;
|
||||
|
||||
public AboutPageViewModel(IUpdateService updateService, IApplicationView applicationView)
|
||||
{
|
||||
_updateService = updateService;
|
||||
_applicationView = applicationView;
|
||||
|
||||
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdate(true));
|
||||
}
|
||||
|
||||
public IAsyncCommand CheckForUpdatesCommand { get; }
|
||||
|
||||
public string CurrentVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
var version = _updateService.GetCurrentVersion();
|
||||
return ConvertVersionToString(version);
|
||||
}
|
||||
}
|
||||
|
||||
public string CurrentVersionReleaseNotesURL => BaseUrl + CurrentVersion;
|
||||
|
||||
public string LatestVersion
|
||||
{
|
||||
get => _latestVersion;
|
||||
set
|
||||
{
|
||||
if (Set(ref _latestVersion, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(LatestVersionFound));
|
||||
RaisePropertyChanged(nameof(LatestVersionLoading));
|
||||
RaisePropertyChanged(nameof(LatestVersionNotFound));
|
||||
RaisePropertyChanged(nameof(LatestVersionReleaseNotesURL));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool LatestVersionFound => !LatestVersionNotFound && !LatestVersionLoading;
|
||||
|
||||
public bool LatestVersionLoading => LatestVersion == null;
|
||||
|
||||
public bool LatestVersionNotFound => LatestVersion == "0.0.0.0";
|
||||
|
||||
public string LatestVersionReleaseNotesURL => BaseUrl + LatestVersion;
|
||||
|
||||
public Task OnNavigatedTo()
|
||||
{
|
||||
return CheckForUpdate(false);
|
||||
}
|
||||
|
||||
public async Task CheckForUpdate(bool notifyNoUpdate)
|
||||
{
|
||||
var version = ConvertVersionToString(await _updateService.GetLatestVersionAsync());
|
||||
await _applicationView.RunOnDispatcherThread(() => LatestVersion = version);
|
||||
await _updateService.CheckForUpdate(notifyNoUpdate);
|
||||
}
|
||||
|
||||
private string ConvertVersionToString(Version version)
|
||||
{
|
||||
return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
@@ -15,21 +18,48 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IStartupTaskService _startupTaskService;
|
||||
private bool _canEnableStartupTask;
|
||||
private bool _editingNewTerminalLocation;
|
||||
private bool _editingTabsPosition;
|
||||
private bool _startupTaskEnabled;
|
||||
private bool _shouldRestartForTrayMessage;
|
||||
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, IStartupTaskService startupTaskService)
|
||||
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
|
||||
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_startupTaskService = startupTaskService;
|
||||
_applicationLanguageService = applicationLanguageService;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
|
||||
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectory);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
|
||||
|
||||
public bool NeedsToRestart
|
||||
{
|
||||
get => _needsToRestart;
|
||||
set => Set(ref _needsToRestart, value);
|
||||
}
|
||||
|
||||
public string SelectedLanguage
|
||||
{
|
||||
get => _applicationLanguageService.GetCurrentLanguage();
|
||||
set
|
||||
{
|
||||
_applicationLanguageService.SetLanguage(value);
|
||||
NeedsToRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnNavigatedTo()
|
||||
@@ -38,14 +68,150 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
SetStartupTaskPropertiesForStatus(startupTaskStatus);
|
||||
}
|
||||
|
||||
public bool AlwaysShowTabs
|
||||
public bool ShowCustomTitleInTitlebar
|
||||
{
|
||||
get => _applicationSettings.AlwaysShowTabs;
|
||||
get => _applicationSettings.ShowCustomTitleInTitlebar;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.AlwaysShowTabs != value)
|
||||
if (_applicationSettings.ShowCustomTitleInTitlebar != value)
|
||||
{
|
||||
_applicationSettings.AlwaysShowTabs = value;
|
||||
_applicationSettings.ShowCustomTitleInTitlebar = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableTrayIcon
|
||||
{
|
||||
get => _applicationSettings.EnableTrayIcon;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.EnableTrayIcon != value)
|
||||
{
|
||||
_applicationSettings.EnableTrayIcon = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
|
||||
// Toggle message telling user to restart the program
|
||||
ShouldRestartForTrayMessage = !ShouldRestartForTrayMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldRestartForTrayMessage
|
||||
{
|
||||
get => _shouldRestartForTrayMessage;
|
||||
set => Set(ref _shouldRestartForTrayMessage, value);
|
||||
}
|
||||
|
||||
public bool ShowNewOutputIndicator
|
||||
{
|
||||
get => _applicationSettings.ShowNewOutputIndicator;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.ShowNewOutputIndicator != value)
|
||||
{
|
||||
_applicationSettings.ShowNewOutputIndicator = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseMoshByDefault
|
||||
{
|
||||
get => _applicationSettings.UseMoshByDefault;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.UseMoshByDefault != value)
|
||||
{
|
||||
_applicationSettings.UseMoshByDefault = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoFallbackToWindowsUsernameInLinks
|
||||
{
|
||||
get => _applicationSettings.AutoFallbackToWindowsUsernameInLinks;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks != value)
|
||||
{
|
||||
_applicationSettings.AutoFallbackToWindowsUsernameInLinks = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RTrimCopiedLines
|
||||
{
|
||||
get => _applicationSettings.RTrimCopiedLines;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.RTrimCopiedLines != value)
|
||||
{
|
||||
_applicationSettings.RTrimCopiedLines = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool MuteTerminalBeeps
|
||||
{
|
||||
get => _applicationSettings.MuteTerminalBeeps;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.MuteTerminalBeeps != value)
|
||||
{
|
||||
_applicationSettings.MuteTerminalBeeps = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
|
||||
_trayProcessCommunicationService.MuteTerminal(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool EnableLogging
|
||||
{
|
||||
get => _applicationSettings.EnableLogging;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.EnableLogging != value)
|
||||
{
|
||||
_applicationSettings.EnableLogging = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool PrintableOutputOnly
|
||||
{
|
||||
get => _applicationSettings.PrintableOutputOnly;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.PrintableOutputOnly != value)
|
||||
{
|
||||
_applicationSettings.PrintableOutputOnly = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LogDirectoryPath
|
||||
{
|
||||
get => _applicationSettings.LogDirectoryPath;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.LogDirectoryPath != value)
|
||||
{
|
||||
_applicationSettings.LogDirectoryPath = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
@@ -55,7 +221,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool BottomIsSelected
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Bottom;
|
||||
set => TabsPosition = TabsPosition.Bottom;
|
||||
set { if (value) TabsPosition = TabsPosition.Bottom; }
|
||||
}
|
||||
|
||||
public bool CanEnableStartupTask
|
||||
@@ -97,20 +263,18 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.NewTerminalLocation;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.NewTerminalLocation != value && !_editingNewTerminalLocation)
|
||||
if (_applicationSettings.NewTerminalLocation != value)
|
||||
{
|
||||
_editingNewTerminalLocation = true;
|
||||
_applicationSettings.NewTerminalLocation = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(WindowIsSelected));
|
||||
RaisePropertyChanged(nameof(TabIsSelected));
|
||||
_editingNewTerminalLocation = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
public IAsyncCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
public IAsyncCommand BrowseLogDirectoryCommand { get; }
|
||||
|
||||
public bool StartupTaskEnabled
|
||||
{
|
||||
@@ -135,7 +299,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool TabIsSelected
|
||||
{
|
||||
get => NewTerminalLocation == NewTerminalLocation.Tab;
|
||||
set => NewTerminalLocation = NewTerminalLocation.Tab;
|
||||
set { if (value) NewTerminalLocation = NewTerminalLocation.Tab; }
|
||||
}
|
||||
|
||||
public TabsPosition TabsPosition
|
||||
@@ -143,15 +307,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.TabsPosition;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.TabsPosition != value && !_editingTabsPosition)
|
||||
if (_applicationSettings.TabsPosition != value)
|
||||
{
|
||||
_editingTabsPosition = true;
|
||||
_applicationSettings.TabsPosition = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(TopIsSelected));
|
||||
RaisePropertyChanged(nameof(BottomIsSelected));
|
||||
_editingTabsPosition = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +319,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool TopIsSelected
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Top;
|
||||
set => TabsPosition = TabsPosition.Top;
|
||||
set { if (value) TabsPosition = TabsPosition.Top; }
|
||||
}
|
||||
|
||||
public bool UnderlineSelectedTab
|
||||
@@ -176,15 +336,41 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool BackgroundIsSelected
|
||||
{
|
||||
get => InactiveTabColorMode == InactiveTabColorMode.Background;
|
||||
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Background; }
|
||||
}
|
||||
|
||||
public bool UnderlinedIsSelected
|
||||
{
|
||||
get => InactiveTabColorMode == InactiveTabColorMode.Underlined;
|
||||
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Underlined; }
|
||||
}
|
||||
|
||||
public InactiveTabColorMode InactiveTabColorMode
|
||||
{
|
||||
get => _applicationSettings.InactiveTabColorMode;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.InactiveTabColorMode != value)
|
||||
{
|
||||
_applicationSettings.InactiveTabColorMode = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool WindowIsSelected
|
||||
{
|
||||
get => NewTerminalLocation == NewTerminalLocation.Window;
|
||||
set => NewTerminalLocation = NewTerminalLocation.Window;
|
||||
set { if (value) NewTerminalLocation = NewTerminalLocation.Window; }
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
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.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
@@ -192,8 +378,18 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
ConfirmClosingWindows = defaults.ConfirmClosingWindows;
|
||||
ConfirmClosingTabs = defaults.ConfirmClosingTabs;
|
||||
UnderlineSelectedTab = defaults.UnderlineSelectedTab;
|
||||
InactiveTabColorMode = defaults.InactiveTabColorMode;
|
||||
NewTerminalLocation = defaults.NewTerminalLocation;
|
||||
AlwaysShowTabs = defaults.AlwaysShowTabs;
|
||||
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
|
||||
EnableTrayIcon = defaults.EnableTrayIcon;
|
||||
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
|
||||
UseMoshByDefault = defaults.UseMoshByDefault;
|
||||
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
|
||||
RTrimCopiedLines = defaults.RTrimCopiedLines;
|
||||
MuteTerminalBeeps = defaults.MuteTerminalBeeps;
|
||||
EnableLogging = defaults.EnableLogging;
|
||||
PrintableOutputOnly = defaults.PrintableOutputOnly;
|
||||
LogDirectoryPath = defaults.LogDirectoryPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,13 +411,13 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
case StartupTaskStatus.DisabledByUser:
|
||||
StartupTaskEnabled = false;
|
||||
StartupTaskErrorMessage = "Disabled by user. Please reactivate it in the Startup tab of the Task Manager.";
|
||||
StartupTaskErrorMessage = I18N.Translate("DisabledByUser");
|
||||
CanEnableStartupTask = false;
|
||||
break;
|
||||
|
||||
case StartupTaskStatus.DisabledByPolicy:
|
||||
StartupTaskEnabled = false;
|
||||
StartupTaskErrorMessage = "Disabled by policy.";
|
||||
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy");
|
||||
CanEnableStartupTask = false;
|
||||
break;
|
||||
}
|
||||
@@ -236,10 +432,20 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
else
|
||||
{
|
||||
_startupTaskService.DisableStartupTask();
|
||||
await _startupTaskService.DisableStartupTask();
|
||||
status = await _startupTaskService.GetStatus();
|
||||
}
|
||||
SetStartupTaskPropertiesForStatus(status);
|
||||
}
|
||||
|
||||
private async Task BrowseLogDirectory()
|
||||
{
|
||||
var folder = await _fileSystemService.BrowseForDirectory();
|
||||
|
||||
if (folder != null)
|
||||
{
|
||||
LogDirectoryPath = folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -11,18 +12,21 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
public KeyBindingViewModel(KeyBinding keyBinding, IDialogService dialogService)
|
||||
public KeyBindingViewModel(KeyBinding keyBinding, IDialogService dialogService, KeyBindingsViewModel parent)
|
||||
{
|
||||
Model = keyBinding;
|
||||
Parent = parent;
|
||||
_dialogService = dialogService;
|
||||
EditCommand = new RelayCommand(async () => await Edit().ConfigureAwait(false));
|
||||
DeleteCommand = new RelayCommand(async () => await Delete().ConfigureAwait(false));
|
||||
EditCommand = new AsyncCommand(Edit);
|
||||
DeleteCommand = new AsyncCommand(Delete);
|
||||
}
|
||||
|
||||
public event EventHandler Deleted;
|
||||
|
||||
public event EventHandler Edited;
|
||||
|
||||
public KeyBindingsViewModel Parent { get; }
|
||||
|
||||
public bool Meta
|
||||
{
|
||||
get => Model.Meta;
|
||||
@@ -62,8 +66,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand DeleteCommand { get; }
|
||||
public RelayCommand EditCommand { get; }
|
||||
public IAsyncCommand DeleteCommand { get; }
|
||||
public IAsyncCommand EditCommand { get; }
|
||||
|
||||
public int Key
|
||||
{
|
||||
@@ -115,7 +119,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
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.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
@@ -13,32 +14,29 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
public class KeyBindingsPageViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private IDictionary<Command, ICollection<KeyBinding>> _keyBindings;
|
||||
|
||||
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
AddCommand = new RelayCommand<Command>(async command => await Add(command).ConfigureAwait(false));
|
||||
AddCommand = new RelayCommand<string>(async command => await Add(command).ConfigureAwait(false));
|
||||
|
||||
Initialize(_settingsService.GetKeyBindings());
|
||||
Initialize(_settingsService.GetCommandKeyBindings());
|
||||
}
|
||||
|
||||
public RelayCommand<Command> AddCommand { get; }
|
||||
public RelayCommand<string> AddCommand { get; }
|
||||
public ObservableCollection<KeyBindingsViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingsViewModel>();
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
private async Task Add(Command command)
|
||||
private async Task Add(string command)
|
||||
{
|
||||
var keyBinding = KeyBindings.FirstOrDefault(k => k.Command == command);
|
||||
await keyBinding?.Add();
|
||||
await keyBinding?.ShowAddKeyBindingDialog();
|
||||
}
|
||||
|
||||
private void ClearKeyBindings()
|
||||
@@ -51,22 +49,24 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
KeyBindings.Clear();
|
||||
}
|
||||
|
||||
private void Initialize(IDictionary<Command, ICollection<KeyBinding>> keyBindings)
|
||||
private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings)
|
||||
{
|
||||
_keyBindings = keyBindings;
|
||||
|
||||
ClearKeyBindings();
|
||||
|
||||
foreach (var value in Enum.GetValues(typeof(Command)))
|
||||
{
|
||||
var command = (Command)value;
|
||||
var viewModel = new KeyBindingsViewModel(command, _keyBindings[command], _dialogService);
|
||||
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, I18N.Translate($"{nameof(Command)}.{command}"), true);
|
||||
foreach (var keyBinding in keyBindings[command.ToString()])
|
||||
{
|
||||
viewModel.Add(keyBinding);
|
||||
}
|
||||
viewModel.Edited += OnEdited;
|
||||
KeyBindings.Add(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEdited(Command command, ICollection<KeyBinding> keyBindings)
|
||||
private void OnEdited(string command, ICollection<KeyBinding> keyBindings)
|
||||
{
|
||||
_settingsService.SaveKeyBindings(command, keyBindings);
|
||||
_trayProcessCommunicationService.UpdateToggleWindowKeyBindings();
|
||||
@@ -74,12 +74,12 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
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.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
_settingsService.ResetKeyBindings();
|
||||
Initialize(_settingsService.GetKeyBindings());
|
||||
Initialize(_settingsService.GetCommandKeyBindings());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
@@ -13,62 +12,73 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public class KeyBindingsViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly ICollection<KeyBinding> _keyBindings;
|
||||
private bool _editable;
|
||||
|
||||
public KeyBindingsViewModel(Command command, ICollection<KeyBinding> keyBindings, IDialogService dialogService)
|
||||
public KeyBindingsViewModel(string command, IDialogService dialogService, string commandName, bool editable)
|
||||
{
|
||||
Command = command;
|
||||
_keyBindings = keyBindings;
|
||||
_dialogService = dialogService;
|
||||
|
||||
CommandName = EnumHelper.GetEnumDescription(command);
|
||||
|
||||
foreach (var binding in keyBindings)
|
||||
{
|
||||
var viewModel = new KeyBindingViewModel(binding, _dialogService);
|
||||
viewModel.Deleted += ViewModel_Deleted;
|
||||
viewModel.Edited += ViewModel_Edited;
|
||||
KeyBindings.Add(viewModel);
|
||||
}
|
||||
Command = command;
|
||||
CommandName = commandName;
|
||||
Editable = editable;
|
||||
}
|
||||
|
||||
public delegate void EditedEvent(Command command, ICollection<KeyBinding> keyBindings);
|
||||
public delegate void EditedEvent(string command, ICollection<KeyBinding> keyBindings);
|
||||
|
||||
public event EditedEvent Edited;
|
||||
|
||||
public Command Command { get; }
|
||||
public string Command { get; }
|
||||
|
||||
public string CommandName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the "Edit" and "Delete" links appear next to the key binding list.
|
||||
/// </summary>
|
||||
public bool Editable
|
||||
{
|
||||
get => _editable;
|
||||
set => Set(ref _editable, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<KeyBindingViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingViewModel>();
|
||||
|
||||
public async Task Add()
|
||||
public async Task ShowAddKeyBindingDialog()
|
||||
{
|
||||
var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService);
|
||||
var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService, this);
|
||||
|
||||
if (await newKeyBinding.Edit().ConfigureAwait(true))
|
||||
{
|
||||
newKeyBinding.Deleted += ViewModel_Deleted;
|
||||
newKeyBinding.Edited += ViewModel_Edited;
|
||||
KeyBindings.Add(newKeyBinding);
|
||||
_keyBindings.Add(newKeyBinding.Model);
|
||||
Edited?.Invoke(Command, _keyBindings);
|
||||
Add(newKeyBinding.Model);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
KeyBindings.Clear();
|
||||
}
|
||||
|
||||
public void Add(KeyBinding keyBinding)
|
||||
{
|
||||
var viewModel = new KeyBindingViewModel(keyBinding, _dialogService, this);
|
||||
|
||||
viewModel.Deleted += ViewModel_Deleted;
|
||||
viewModel.Edited += ViewModel_Edited;
|
||||
KeyBindings.Add(viewModel);
|
||||
Edited?.Invoke(Command, KeyBindings.Select(x => x.Model).ToList());
|
||||
}
|
||||
|
||||
private void ViewModel_Deleted(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is KeyBindingViewModel keyBinding)
|
||||
{
|
||||
_keyBindings.Remove(keyBinding.Model);
|
||||
KeyBindings.Remove(keyBinding);
|
||||
Edited?.Invoke(Command, _keyBindings);
|
||||
Edited?.Invoke(Command, KeyBindings.Select(x => x.Model).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_Edited(object sender, EventArgs e)
|
||||
{
|
||||
Edited?.Invoke(Command, _keyBindings);
|
||||
Edited?.Invoke(Command, KeyBindings.Select(x => x.Model).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
@@ -16,8 +14,6 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private readonly ApplicationSettings _applicationSettings;
|
||||
private bool _editingMouseRightClickAction;
|
||||
private bool _editingMouseMiddleClickAction;
|
||||
|
||||
public MousePageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider)
|
||||
{
|
||||
@@ -48,16 +44,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.MouseRightClickAction;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.MouseRightClickAction != value && !_editingMouseRightClickAction)
|
||||
if (_applicationSettings.MouseRightClickAction != value)
|
||||
{
|
||||
_editingMouseRightClickAction = true;
|
||||
_applicationSettings.MouseRightClickAction = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(MouseRightClickNoneIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseRightClickContextMenuIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseRightClickPasteIsSelected));
|
||||
_editingMouseRightClickAction = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,19 +56,19 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool MouseRightClickNoneIsSelected
|
||||
{
|
||||
get => MouseRightClickAction == MouseAction.None;
|
||||
set => MouseRightClickAction = MouseAction.None;
|
||||
set { if (value) MouseRightClickAction = MouseAction.None; }
|
||||
}
|
||||
|
||||
public bool MouseRightClickContextMenuIsSelected
|
||||
{
|
||||
get => MouseRightClickAction == MouseAction.ContextMenu;
|
||||
set => MouseRightClickAction = MouseAction.ContextMenu;
|
||||
set { if (value) MouseRightClickAction = MouseAction.ContextMenu; }
|
||||
}
|
||||
|
||||
public bool MouseRightClickPasteIsSelected
|
||||
{
|
||||
get => MouseRightClickAction == MouseAction.Paste;
|
||||
set => MouseRightClickAction = MouseAction.Paste;
|
||||
set { if (value) MouseRightClickAction = MouseAction.Paste; }
|
||||
}
|
||||
|
||||
public MouseAction MouseMiddleClickAction
|
||||
@@ -85,16 +76,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.MouseMiddleClickAction;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.MouseMiddleClickAction != value && !_editingMouseMiddleClickAction)
|
||||
if (_applicationSettings.MouseMiddleClickAction != value)
|
||||
{
|
||||
_editingMouseMiddleClickAction = true;
|
||||
_applicationSettings.MouseMiddleClickAction = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(MouseMiddleClickNoneIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseMiddleClickContextMenuIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseMiddleClickPasteIsSelected));
|
||||
_editingMouseMiddleClickAction = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,26 +88,26 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool MouseMiddleClickNoneIsSelected
|
||||
{
|
||||
get => MouseMiddleClickAction == MouseAction.None;
|
||||
set => MouseMiddleClickAction = MouseAction.None;
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.None; }
|
||||
}
|
||||
|
||||
public bool MouseMiddleClickContextMenuIsSelected
|
||||
{
|
||||
get => MouseMiddleClickAction == MouseAction.ContextMenu;
|
||||
set => MouseMiddleClickAction = MouseAction.ContextMenu;
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.ContextMenu; }
|
||||
}
|
||||
|
||||
public bool MouseMiddleClickPasteIsSelected
|
||||
{
|
||||
get => MouseMiddleClickAction == MouseAction.Paste;
|
||||
set => MouseMiddleClickAction = MouseAction.Paste;
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.Paste; }
|
||||
}
|
||||
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
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.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
@@ -15,20 +16,23 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private ShellProfileViewModel _selectedShellProfile;
|
||||
private readonly IApplicationView _applicationView;
|
||||
|
||||
public ProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IFileSystemService fileSystemService)
|
||||
public ProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IFileSystemService fileSystemService, IApplicationView applicationView)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_fileSystemService = fileSystemService;
|
||||
_applicationView = applicationView;
|
||||
|
||||
CreateShellProfileCommand = new RelayCommand(CreateShellProfile);
|
||||
CloneCommand = new RelayCommand<ShellProfileViewModel>(Clone);
|
||||
|
||||
var defaultShellProfileId = _settingsService.GetDefaultShellProfileId();
|
||||
foreach (var shellProfile in _settingsService.GetShellProfiles())
|
||||
{
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService);
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider, false);
|
||||
viewModel.Deleted += OnShellProfileDeleted;
|
||||
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
|
||||
|
||||
@@ -40,6 +44,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
|
||||
SelectedShellProfile = ShellProfiles.First(p => p.IsDefault);
|
||||
|
||||
}
|
||||
|
||||
private void OnShellProfileSetAsDefault(object sender, EventArgs e)
|
||||
@@ -76,18 +81,36 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
public RelayCommand CreateShellProfileCommand { get; }
|
||||
|
||||
public RelayCommand<ShellProfileViewModel> CloneCommand { get; }
|
||||
|
||||
private void CreateShellProfile()
|
||||
{
|
||||
var shellProfile = new ShellProfile
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PreInstalled = false,
|
||||
Name = "New profile"
|
||||
Name = "New profile",
|
||||
KeyBindings = new List<KeyBinding>()
|
||||
};
|
||||
|
||||
_settingsService.SaveShellProfile(shellProfile);
|
||||
AddShellProfile(shellProfile);
|
||||
}
|
||||
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService);
|
||||
private void Clone(ShellProfileViewModel shellProfile)
|
||||
{
|
||||
var cloned = shellProfile.ProfileVm.Model.Clone();
|
||||
|
||||
cloned.Id = Guid.NewGuid();
|
||||
cloned.PreInstalled = false;
|
||||
cloned.Name = $"Copy of {shellProfile.Name}";
|
||||
cloned.KeyBindings = new List<KeyBinding>();
|
||||
|
||||
AddShellProfile(cloned);
|
||||
}
|
||||
|
||||
private void AddShellProfile(ShellProfile shellProfile)
|
||||
{
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider, true);
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
|
||||
viewModel.Deleted += OnShellProfileDeleted;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
public class SshProfilesPageViewModel: ViewModelBase
|
||||
{
|
||||
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 shellProfile)
|
||||
{
|
||||
if (SelectedSshProfile == shellProfile)
|
||||
{
|
||||
SelectedSshProfile = SshProfiles.First();
|
||||
}
|
||||
SshProfiles.Remove(shellProfile);
|
||||
_settingsService.DeleteSshProfile(shellProfile.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand CreateSshProfileCommand { get; }
|
||||
|
||||
public RelayCommand<SshProfileViewModel> 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
|
||||
};
|
||||
|
||||
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 => Set(ref _selectedShellProfile, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<SshProfileViewModel> SshProfiles { get; } = new ObservableCollection<SshProfileViewModel>();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
@@ -15,43 +17,41 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private bool _isEditingCursorStyle;
|
||||
private bool _isEditingScrollBarStyle;
|
||||
|
||||
public bool BarIsSelected
|
||||
{
|
||||
get => CursorStyle == CursorStyle.Bar;
|
||||
set => CursorStyle = CursorStyle.Bar;
|
||||
set { if (value) CursorStyle = CursorStyle.Bar; }
|
||||
}
|
||||
|
||||
public bool BlockIsSelected
|
||||
{
|
||||
get => CursorStyle == CursorStyle.Block;
|
||||
set => CursorStyle = CursorStyle.Block;
|
||||
set { if (value) CursorStyle = CursorStyle.Block; }
|
||||
}
|
||||
|
||||
public bool UnderlineIsSelected
|
||||
{
|
||||
get => CursorStyle == CursorStyle.Underline;
|
||||
set => CursorStyle = CursorStyle.Underline;
|
||||
set { if (value) CursorStyle = CursorStyle.Underline; }
|
||||
}
|
||||
|
||||
public bool HiddenIsSelected
|
||||
{
|
||||
get => ScrollBarStyle == ScrollBarStyle.Hidden;
|
||||
set => ScrollBarStyle = ScrollBarStyle.Hidden;
|
||||
set { if (value) ScrollBarStyle = ScrollBarStyle.Hidden; }
|
||||
}
|
||||
|
||||
public bool AutoHidingIsSelected
|
||||
{
|
||||
get => ScrollBarStyle == ScrollBarStyle.AutoHiding;
|
||||
set => ScrollBarStyle = ScrollBarStyle.AutoHiding;
|
||||
set { if (value) ScrollBarStyle = ScrollBarStyle.AutoHiding; }
|
||||
}
|
||||
|
||||
public bool VisibleIsSelected
|
||||
{
|
||||
get => ScrollBarStyle == ScrollBarStyle.Visible;
|
||||
set => ScrollBarStyle = ScrollBarStyle.Visible;
|
||||
set { if (value) ScrollBarStyle = ScrollBarStyle.Visible; }
|
||||
}
|
||||
|
||||
public bool CursorBlink
|
||||
@@ -96,12 +96,25 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public int Padding
|
||||
{
|
||||
get => _terminalOptions.Padding;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.Padding != value)
|
||||
{
|
||||
_terminalOptions.Padding = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ScrollBackLimit
|
||||
{
|
||||
get => _terminalOptions.ScrollBackLimit.ToString();
|
||||
set
|
||||
{
|
||||
var oldValue = _terminalOptions.ScrollBackLimit;
|
||||
if (uint.TryParse(value, out uint intValue))
|
||||
{
|
||||
if (intValue == uint.MinValue)
|
||||
@@ -122,7 +135,50 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> Fonts { get; }
|
||||
public string WordSeparator
|
||||
{
|
||||
get => WebViewSpecialCharEncoder.Decode(_terminalOptions.WordSeparator);
|
||||
set
|
||||
{
|
||||
value = WebViewSpecialCharEncoder.Encode(value);
|
||||
if (_terminalOptions.WordSeparator != value)
|
||||
{
|
||||
_terminalOptions.WordSeparator = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool BoldText
|
||||
{
|
||||
get => _terminalOptions.BoldText;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.BoldText != value)
|
||||
{
|
||||
_terminalOptions.BoldText = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowTextCopied
|
||||
{
|
||||
get => _terminalOptions.ShowTextCopied;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.ShowTextCopied != value)
|
||||
{
|
||||
_terminalOptions.ShowTextCopied = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<FontInfo> Fonts { get; }
|
||||
|
||||
public int FontSize
|
||||
{
|
||||
@@ -147,16 +203,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _terminalOptions.CursorStyle;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.CursorStyle != value && !_isEditingCursorStyle)
|
||||
if (_terminalOptions.CursorStyle != value)
|
||||
{
|
||||
_isEditingCursorStyle = true;
|
||||
_terminalOptions.CursorStyle = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(BlockIsSelected));
|
||||
RaisePropertyChanged(nameof(BarIsSelected));
|
||||
RaisePropertyChanged(nameof(UnderlineIsSelected));
|
||||
_isEditingCursorStyle = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,23 +217,18 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _terminalOptions.ScrollBarStyle;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.ScrollBarStyle != value && !_isEditingScrollBarStyle)
|
||||
if (_terminalOptions.ScrollBarStyle != value)
|
||||
{
|
||||
_isEditingScrollBarStyle = true;
|
||||
_terminalOptions.ScrollBarStyle = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(HiddenIsSelected));
|
||||
RaisePropertyChanged(nameof(AutoHidingIsSelected));
|
||||
RaisePropertyChanged(nameof(VisibleIsSelected));
|
||||
_isEditingScrollBarStyle = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
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.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
@@ -192,8 +238,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
ScrollBarStyle = defaults.ScrollBarStyle;
|
||||
FontFamily = defaults.FontFamily;
|
||||
FontSize = defaults.FontSize;
|
||||
BoldText = defaults.BoldText;
|
||||
BackgroundOpacity = defaults.BackgroundOpacity;
|
||||
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
|
||||
ShowTextCopied = defaults.ShowTextCopied;
|
||||
WordSeparator = defaults.WordSeparator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,8 +254,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
|
||||
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s);
|
||||
Sizes = Enumerable.Range(1, 72);
|
||||
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
|
||||
Sizes = Enumerable.Range(2, 72);
|
||||
|
||||
_terminalOptions = _settingsService.GetTerminalOptions();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models.Messages;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
@@ -30,16 +34,17 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
CreateThemeCommand = new RelayCommand(CreateTheme);
|
||||
ImportThemeCommand = new RelayCommand(ImportTheme);
|
||||
ImportThemeCommand = new AsyncCommand(ImportTheme);
|
||||
CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme);
|
||||
|
||||
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
|
||||
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
|
||||
|
||||
BackgroundOpacity = _settingsService.GetTerminalOptions().BackgroundOpacity;
|
||||
|
||||
var activeThemeId = _settingsService.GetCurrentThemeId();
|
||||
foreach (var theme in _settingsService.GetThemes())
|
||||
{
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService);
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService, false);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
|
||||
@@ -54,7 +59,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
|
||||
public RelayCommand CreateThemeCommand { get; }
|
||||
public RelayCommand ImportThemeCommand { get; }
|
||||
public IAsyncCommand ImportThemeCommand { get; }
|
||||
public RelayCommand<ThemeViewModel> CloneCommand { get; set; }
|
||||
|
||||
public double BackgroundOpacity
|
||||
{
|
||||
@@ -81,6 +87,18 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
public ObservableCollection<ThemeViewModel> Themes { get; } = new ObservableCollection<ThemeViewModel>();
|
||||
|
||||
private void CloneTheme(ThemeViewModel theme)
|
||||
{
|
||||
var cloned = new TerminalTheme(theme.Model)
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PreInstalled = false,
|
||||
Name = $"Copy of {theme.Name}"
|
||||
};
|
||||
|
||||
AddTheme(cloned);
|
||||
}
|
||||
|
||||
private void CreateTheme()
|
||||
{
|
||||
var defaultTheme = _settingsService.GetTheme(_defaultValueProvider.GetDefaultThemeId());
|
||||
@@ -92,16 +110,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
Colors = new TerminalColors(defaultTheme.Colors)
|
||||
};
|
||||
|
||||
_settingsService.SaveTheme(theme);
|
||||
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
Themes.Add(viewModel);
|
||||
SelectedTheme = viewModel;
|
||||
AddTheme(theme);
|
||||
}
|
||||
|
||||
private async void ImportTheme()
|
||||
private async Task ImportTheme()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(_themeParserFactory.SupportedFileTypes).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
@@ -110,7 +122,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
if (parser == null)
|
||||
{
|
||||
await _dialogService.ShowMessageDialogAsnyc("Import theme failed", "No suitable parser found", DialogButton.OK).ConfigureAwait(false);
|
||||
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,21 +130,27 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
var theme = await parser.Parse(file.Name, file.Content).ConfigureAwait(true);
|
||||
|
||||
_settingsService.SaveTheme(theme);
|
||||
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
Themes.Add(viewModel);
|
||||
SelectedTheme = viewModel;
|
||||
AddTheme(theme);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await _dialogService.ShowMessageDialogAsnyc("Import theme failed", exception.Message, DialogButton.OK).ConfigureAwait(false);
|
||||
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTheme(TerminalTheme theme)
|
||||
{
|
||||
_settingsService.SaveTheme(theme, true);
|
||||
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, true);
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
Themes.Add(viewModel);
|
||||
SelectedTheme = viewModel;
|
||||
}
|
||||
|
||||
private void OnThemeActivated(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is ThemeViewModel activatedTheme)
|
||||
@@ -165,9 +183,9 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
|
||||
{
|
||||
BackgroundOpacity = e.BackgroundOpacity;
|
||||
BackgroundOpacity = message.TerminalOptions.BackgroundOpacity;
|
||||
}
|
||||
|
||||
private void OnSelectedThemeBackgroundChanged(object sender, string e)
|
||||
|
||||
@@ -8,24 +8,29 @@ namespace FluentTerminal.App.ViewModels
|
||||
public class SettingsViewModel : ViewModelBase
|
||||
{
|
||||
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService, IFileSystemService fileSystemService, IStartupTaskService startupTaskService)
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
|
||||
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IApplicationView applicationView,
|
||||
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers)
|
||||
{
|
||||
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, defaultValueProvider, trayProcessCommunicationService);
|
||||
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService);
|
||||
Shell = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService);
|
||||
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, trayProcessCommunicationService);
|
||||
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService, fileSystemService);
|
||||
Profiles = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
|
||||
Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService);
|
||||
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService);
|
||||
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
|
||||
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService,
|
||||
applicationView, trayProcessCommunicationService, containers.HistoryContainer);
|
||||
}
|
||||
|
||||
public event EventHandler Closed;
|
||||
|
||||
public GeneralPageViewModel General { get; }
|
||||
public KeyBindingsPageViewModel KeyBindings { get; }
|
||||
public ProfilesPageViewModel Shell { get; }
|
||||
public ProfilesPageViewModel Profiles { get; }
|
||||
public TerminalPageViewModel Terminal { get; }
|
||||
public ThemesPageViewModel Themes { get; }
|
||||
public MousePageViewModel Mouse { get; }
|
||||
public SshProfilesPageViewModel SshProfiles { get; }
|
||||
|
||||
public void Close()
|
||||
{
|
||||
|
||||
@@ -1,88 +1,29 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class ShellProfileViewModel : ViewModelBase
|
||||
/// <summary>
|
||||
/// Extends <see cref="ProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
|
||||
/// </summary>
|
||||
public class ShellProfileViewModel : ProfileViewModelBase<CommonProfileProviderViewModel>
|
||||
{
|
||||
private readonly ShellProfile _shellProfile;
|
||||
private string _name;
|
||||
private string _arguments;
|
||||
private string _location;
|
||||
private string _workingDirectory;
|
||||
private string _fallbackName;
|
||||
private string _fallbackArguments;
|
||||
private string _fallbackLocation;
|
||||
private string _fallbackWorkingDirectory;
|
||||
#region Fields
|
||||
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
private bool _isDefault;
|
||||
private bool _inEditMode;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService)
|
||||
{
|
||||
_shellProfile = shellProfile;
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
Id = shellProfile.Id;
|
||||
Name = shellProfile.Name;
|
||||
Arguments = shellProfile.Arguments;
|
||||
Location = shellProfile.Location;
|
||||
WorkingDirectory = shellProfile.WorkingDirectory;
|
||||
|
||||
SetDefaultCommand = new RelayCommand(SetDefault);
|
||||
DeleteCommand = new RelayCommand(async () => await Delete().ConfigureAwait(false), CanDelete);
|
||||
EditCommand = new RelayCommand(Edit, CanEdit);
|
||||
CancelEditCommand = new RelayCommand(async () => await CancelEdit().ConfigureAwait(false));
|
||||
SaveChangesCommand = new RelayCommand(SaveChanges);
|
||||
BrowseForCustomShellCommand = new RelayCommand(async () => await BrowseForCustomShell().ConfigureAwait(false));
|
||||
BrowseForWorkingDirectoryCommand = new RelayCommand(async () => await BrowseForWorkingDirectory().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public event EventHandler Deleted;
|
||||
|
||||
public event EventHandler SetAsDefault;
|
||||
|
||||
public RelayCommand DeleteCommand { get; }
|
||||
public RelayCommand EditCommand { get; }
|
||||
public RelayCommand SaveChangesCommand { get; }
|
||||
public RelayCommand CancelEditCommand { get; }
|
||||
public RelayCommand SetDefaultCommand { get; }
|
||||
public RelayCommand BrowseForCustomShellCommand { get; }
|
||||
public RelayCommand BrowseForWorkingDirectoryCommand { get; }
|
||||
|
||||
public Guid Id { get; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(ref _name, value);
|
||||
}
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => _arguments;
|
||||
set => Set(ref _arguments, value);
|
||||
}
|
||||
|
||||
public string Location
|
||||
{
|
||||
get => _location;
|
||||
set => Set(ref _location, value);
|
||||
}
|
||||
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get => _workingDirectory;
|
||||
set => Set(ref _workingDirectory, value);
|
||||
}
|
||||
|
||||
public bool IsDefault
|
||||
{
|
||||
@@ -90,66 +31,72 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _isDefault, value);
|
||||
}
|
||||
|
||||
public bool InEditMode
|
||||
#endregion Properties
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler SetAsDefault;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Commands
|
||||
|
||||
public RelayCommand SetDefaultCommand { get; }
|
||||
|
||||
public IAsyncCommand 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)
|
||||
{
|
||||
get => _inEditMode;
|
||||
set => Set(ref _inEditMode, value);
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
|
||||
ProfileVm = new CommonProfileProviderViewModel(settingsService, applicationView, fileSystemService,
|
||||
shellProfile);
|
||||
|
||||
SetDefaultCommand = new RelayCommand(SetDefault);
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
|
||||
}
|
||||
|
||||
public void SaveChanges()
|
||||
#endregion Constrcutor
|
||||
|
||||
#region Methods
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
_shellProfile.Arguments = Arguments;
|
||||
_shellProfile.Location = Location;
|
||||
_shellProfile.Name = Name;
|
||||
_shellProfile.WorkingDirectory = WorkingDirectory;
|
||||
if (InEditMode || !ProfileVm.PreInstalled)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_settingsService.SaveShellProfile(_shellProfile);
|
||||
|
||||
InEditMode = false;
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmRestoreProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Arguments = _fallbackArguments;
|
||||
Location = _fallbackLocation;
|
||||
Name = _fallbackName;
|
||||
WorkingDirectory = _fallbackWorkingDirectory;
|
||||
var profile = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id.Equals(Id));
|
||||
|
||||
InEditMode = false;
|
||||
if (profile == null)
|
||||
{
|
||||
// Should not happen ever, but...
|
||||
return;
|
||||
}
|
||||
|
||||
ProfileVm.Model = profile;
|
||||
Initialize(profile);
|
||||
|
||||
SettingsService.SaveShellProfile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
private void Edit()
|
||||
protected override bool CanDelete()
|
||||
{
|
||||
_fallbackArguments = Arguments;
|
||||
_fallbackLocation = Location;
|
||||
_fallbackName = Name;
|
||||
_fallbackWorkingDirectory = WorkingDirectory;
|
||||
InEditMode = true;
|
||||
}
|
||||
|
||||
private bool CanDelete()
|
||||
{
|
||||
return !_shellProfile.PreInstalled;
|
||||
}
|
||||
|
||||
private bool CanEdit()
|
||||
{
|
||||
return !_shellProfile.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);
|
||||
}
|
||||
return !ProfileVm.PreInstalled;
|
||||
}
|
||||
|
||||
private void SetDefault()
|
||||
@@ -157,22 +104,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
SetAsDefault?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async Task BrowseForCustomShell()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
Location = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseForWorkingDirectory()
|
||||
{
|
||||
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
|
||||
if (directory != null)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
}
|
||||
}
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class ProfileViewModelBase<T> : ViewModelBase 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 => Set(ref _inEditMode, value);
|
||||
}
|
||||
|
||||
private string _name;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(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
|
||||
|
||||
public event EventHandler Deleted;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand CancelEditCommand { get; }
|
||||
|
||||
public IAsyncCommand DeleteCommand { get; }
|
||||
|
||||
public RelayCommand EditCommand { get; }
|
||||
|
||||
public IAsyncCommand SaveChangesCommand { get; }
|
||||
|
||||
public IAsyncCommand AddKeyboardShortcutCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected 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 AsyncCommand(Delete, CanDelete);
|
||||
EditCommand = new RelayCommand(Edit);
|
||||
CancelEditCommand = new AsyncCommand(CancelEdit);
|
||||
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
|
||||
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
// Loads view model properties from the input shellProfile
|
||||
protected void Initialize(ShellProfile shellProfile)
|
||||
{
|
||||
Id = shellProfile.Id;
|
||||
Name = shellProfile.Name;
|
||||
|
||||
KeyBindings.Clear();
|
||||
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool HasChanges()
|
||||
{
|
||||
return ProfileVm.HasChanges()
|
||||
|| !_nameBeforeEdit.NullableEqualTo(_name)
|
||||
|| !(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ?? false)
|
||||
|| !(_environmentVariablesBeforeEdit?.SequenceEqual(EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value)) ?? false);
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Deleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool CanDelete();
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
if (InEditMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bindingsBeforeEdit = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
_environmentVariablesBeforeEdit = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
|
||||
_nameBeforeEdit = _name;
|
||||
|
||||
KeyBindings.Editable = true;
|
||||
InEditMode = true;
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
if (IsNew)
|
||||
{
|
||||
await Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HasChanges())
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
// Cancelled, so rollback
|
||||
ProfileVm.RejectChanges();
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task SaveChangesAsync()
|
||||
{
|
||||
var error = await ProfileVm.AcceptChangesAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), error, DialogButton.OK);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnvironmentVariables.Select(x => x.Name).Any(x => string.IsNullOrWhiteSpace(x)))
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Task AddKeyboardShortcut()
|
||||
{
|
||||
return KeyBindings.ShowAddKeyBindingDialog();
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,89 +1,213 @@
|
||||
using Fleck;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models.Messages;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class TerminalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ManualResetEventSlim _connectedEvent;
|
||||
private readonly IDialogService _dialogService;
|
||||
#region Static
|
||||
|
||||
private static readonly Regex SshTitleRx = new Regex(@"^\w+@\S+:", RegexOptions.Compiled);
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Serialize terminal state
|
||||
private class TerminalState
|
||||
{
|
||||
public bool HasCustomTitle { get; set; }
|
||||
public string ShellTitle { get; set; }
|
||||
public string TabTitle { get; set; }
|
||||
public TerminalTheme TerminalTheme { get; set; }
|
||||
public TabTheme TabTheme { get; set; }
|
||||
public bool ShowSearchPanel { get; set; }
|
||||
public string SearchText { get; set; }
|
||||
public string XtermBufferState { get; set; }
|
||||
public byte TerminalId { get; set; }
|
||||
public ShellProfile ShellProfile { get; set; }
|
||||
}
|
||||
|
||||
public async Task<string> Serialize()
|
||||
{
|
||||
TerminalState state = new TerminalState
|
||||
{
|
||||
HasCustomTitle = _hasCustomTitle,
|
||||
ShellTitle = ShellTitle,
|
||||
TabTitle = TabTitle,
|
||||
TerminalTheme = TerminalTheme,
|
||||
TabTheme = TabTheme,
|
||||
ShowSearchPanel = ShowSearchPanel,
|
||||
SearchText = SearchText,
|
||||
XtermBufferState = await SerializeXtermState(),
|
||||
TerminalId = Terminal.Id,
|
||||
ShellProfile = ShellProfile
|
||||
};
|
||||
|
||||
return JsonConvert.SerializeObject(state);
|
||||
}
|
||||
|
||||
public void Restore(string data)
|
||||
{
|
||||
TerminalState state = JsonConvert.DeserializeObject<TerminalState>(data);
|
||||
if (state != null)
|
||||
{
|
||||
_hasCustomTitle = state.HasCustomTitle;
|
||||
ShellTitle = state.ShellTitle;
|
||||
TabTitle = state.TabTitle;
|
||||
TerminalTheme = state.TerminalTheme;
|
||||
TabTheme = state.TabTheme;
|
||||
ShowSearchPanel = state.ShowSearchPanel;
|
||||
SearchText = state.SearchText;
|
||||
XtermBufferState = state.XtermBufferState;
|
||||
_terminalId = state.TerminalId;
|
||||
ShellProfile = state.ShellProfile;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Serialize terminal state
|
||||
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private readonly IDispatcherTimer _resizeOverlayTimer;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ShellProfile _shellProfile;
|
||||
private readonly string _startupDirectory;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private bool _isSelected;
|
||||
private string _resizeOverlayContent;
|
||||
private bool _isHovered;
|
||||
private bool _hasNewOutput;
|
||||
private bool _hasExitedWithError;
|
||||
private string _searchText;
|
||||
private bool _showResizeOverlay;
|
||||
private bool _showSearchPanel;
|
||||
private int _terminalId;
|
||||
private ITerminalView _terminalView;
|
||||
private string _title;
|
||||
private IWebSocketConnection _webSocket;
|
||||
private TabTheme _tabTheme;
|
||||
private TerminalTheme _terminalTheme;
|
||||
private TerminalOptions _terminalOptions;
|
||||
private string _tabTitle;
|
||||
private string _shellTitle;
|
||||
private bool _hasCustomTitle;
|
||||
private byte? _terminalId;
|
||||
|
||||
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
|
||||
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, string terminalState = null)
|
||||
{
|
||||
Title = DefaultTitle;
|
||||
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
|
||||
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
|
||||
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
|
||||
|
||||
_connectedEvent = new ManualResetEventSlim(false);
|
||||
SettingsService = settingsService;
|
||||
|
||||
_settingsService = settingsService;
|
||||
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
|
||||
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
_settingsService.KeyBindingsChanged += OnKeyBindingsChanged;
|
||||
_terminalOptions = SettingsService.GetTerminalOptions();
|
||||
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
TrayProcessCommunicationService = trayProcessCommunicationService;
|
||||
|
||||
_dialogService = dialogService;
|
||||
DialogService = dialogService;
|
||||
_keyboardCommandService = keyboardCommandService;
|
||||
ApplicationSettings = applicationSettings;
|
||||
_startupDirectory = startupDirectory;
|
||||
_shellProfile = shellProfile;
|
||||
_applicationView = applicationView;
|
||||
_clipboardService = clipboardService;
|
||||
ApplicationView = applicationView;
|
||||
ClipboardService = clipboardService;
|
||||
|
||||
_resizeOverlayTimer = dispatcherTimer;
|
||||
_resizeOverlayTimer.Interval = new TimeSpan(0, 0, 2);
|
||||
_resizeOverlayTimer.Tick += OnResizeOverlayTimerFinished;
|
||||
ShellProfile = shellProfile;
|
||||
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? SettingsService.GetCurrentTheme() : SettingsService.GetTheme(shellProfile.TerminalThemeId);
|
||||
|
||||
TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
|
||||
|
||||
CloseCommand = new AsyncCommand(CloseTab, CanExecuteCommand);
|
||||
CloseLeftTabsCommand = new RelayCommand(CloseLeftTabs, CanExecuteCommand);
|
||||
CloseRightTabsCommand = new RelayCommand(CloseRightTabs, CanExecuteCommand);
|
||||
CloseOtherTabsCommand = new RelayCommand(CloseOtherTabs, CanExecuteCommand);
|
||||
FindNextCommand = new RelayCommand(FindNext, CanExecuteCommand);
|
||||
FindPreviousCommand = new RelayCommand(FindPrevious, CanExecuteCommand);
|
||||
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel, CanExecuteCommand);
|
||||
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme, CanExecuteCommand);
|
||||
EditTitleCommand = new AsyncCommand(EditTitle, CanExecuteCommand);
|
||||
DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand);
|
||||
|
||||
if (!String.IsNullOrEmpty(terminalState))
|
||||
{
|
||||
Restore(terminalState);
|
||||
}
|
||||
|
||||
Terminal = new Terminal(TrayProcessCommunicationService, _terminalId);
|
||||
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
|
||||
Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
Terminal.SizeChanged += Terminal_SizeChanged;
|
||||
Terminal.TitleChanged += Terminal_TitleChanged;
|
||||
Terminal.Exited += Terminal_Exited;
|
||||
Terminal.Closed += Terminal_Closed;
|
||||
|
||||
Overlay = new OverlayViewModel(dispatcherTimer);
|
||||
|
||||
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
|
||||
FindNextCommand = new RelayCommand(async () => await FindNext().ConfigureAwait(false));
|
||||
FindPreviousCommand = new RelayCommand(async () => await FindPrevious().ConfigureAwait(false));
|
||||
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
|
||||
}
|
||||
|
||||
private bool _disposalRequested;
|
||||
|
||||
public void DisposalPrepare()
|
||||
{
|
||||
_disposalRequested = true;
|
||||
CloseCommand = null;
|
||||
TerminalView.DisposalPrepare();
|
||||
TerminalView = null;
|
||||
Terminal = null;
|
||||
Overlay = null;
|
||||
}
|
||||
|
||||
public event EventHandler Activated;
|
||||
public event EventHandler Closed;
|
||||
public event EventHandler<string> FindNextRequested;
|
||||
public event EventHandler<string> FindPreviousRequested;
|
||||
public event EventHandler SearchStarted;
|
||||
public event EventHandler<TerminalTheme> ThemeChanged;
|
||||
public event EventHandler<string> ShellTitleChanged;
|
||||
public event EventHandler<string> CustomTitleChanged;
|
||||
public event EventHandler CloseLeftTabsRequested;
|
||||
public event EventHandler CloseRightTabsRequested;
|
||||
public event EventHandler CloseOtherTabsRequested;
|
||||
public event EventHandler DuplicateTabRequested;
|
||||
|
||||
public ApplicationSettings ApplicationSettings { get; private set; }
|
||||
|
||||
public event EventHandler Closed;
|
||||
public IApplicationView ApplicationView { get; }
|
||||
|
||||
public RelayCommand CloseCommand { get; }
|
||||
public TabTheme BackgroundTabTheme
|
||||
{
|
||||
// The effective background theme depends on whether it is selected (use the theme), or if it is inactive
|
||||
// (if we're set to underline inactive tabs, use the null theme).
|
||||
get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ?
|
||||
_tabTheme :
|
||||
SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
|
||||
}
|
||||
|
||||
public IClipboardService ClipboardService { get; }
|
||||
|
||||
public string XtermBufferState { get; private set; }
|
||||
|
||||
public AsyncCommand CloseCommand { get; private set; }
|
||||
|
||||
public RelayCommand CloseRightTabsCommand { get; }
|
||||
|
||||
public RelayCommand CloseLeftTabsCommand { get; }
|
||||
|
||||
public RelayCommand CloseOtherTabsCommand { get; }
|
||||
|
||||
public RelayCommand CloseSearchPanelCommand { get; }
|
||||
|
||||
public string DefaultTitle { get; private set; } = string.Empty;
|
||||
public IDialogService DialogService { get; }
|
||||
|
||||
public IAsyncCommand EditTitleCommand { get; }
|
||||
|
||||
public RelayCommand FindNextCommand { get; }
|
||||
|
||||
public RelayCommand FindPreviousCommand { get; }
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
public RelayCommand DuplicateTabCommand { get; }
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
@@ -94,19 +218,51 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
_applicationView.Title = Title;
|
||||
HasNewOutput = false;
|
||||
}
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
RaisePropertyChanged(nameof(ShowCloseButton));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsUnderlined => IsSelected && ApplicationSettings.UnderlineSelectedTab;
|
||||
|
||||
public string ResizeOverlayContent
|
||||
public bool IsHovered
|
||||
{
|
||||
get => _resizeOverlayContent;
|
||||
set => Set(ref _resizeOverlayContent, value);
|
||||
get => _isHovered;
|
||||
set
|
||||
{
|
||||
if (Set(ref _isHovered, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(ShowCloseButton));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowCloseButton
|
||||
{
|
||||
get => IsHovered || IsSelected;
|
||||
}
|
||||
|
||||
public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) ||
|
||||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null);
|
||||
|
||||
public bool HasNewOutput
|
||||
{
|
||||
get => _hasNewOutput;
|
||||
set => Set(ref _hasNewOutput, value);
|
||||
}
|
||||
|
||||
public bool HasExitedWithError
|
||||
{
|
||||
get => _hasExitedWithError;
|
||||
set
|
||||
{
|
||||
if (Set(ref _hasExitedWithError, value) && value)
|
||||
{
|
||||
HasNewOutput = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SearchText
|
||||
@@ -115,22 +271,11 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _searchText, value);
|
||||
}
|
||||
|
||||
public bool ShowResizeOverlay
|
||||
{
|
||||
get => _showResizeOverlay;
|
||||
set
|
||||
{
|
||||
Set(ref _showResizeOverlay, value);
|
||||
if (value)
|
||||
{
|
||||
if (_resizeOverlayTimer.IsEnabled)
|
||||
{
|
||||
_resizeOverlayTimer.Stop();
|
||||
}
|
||||
_resizeOverlayTimer.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
public RelayCommand<string> SelectTabThemeCommand { get; }
|
||||
|
||||
public ISettingsService SettingsService { get; }
|
||||
|
||||
public ShellProfile ShellProfile { get; private set; }
|
||||
|
||||
public bool ShowSearchPanel
|
||||
{
|
||||
@@ -138,210 +283,313 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _showSearchPanel, value);
|
||||
}
|
||||
|
||||
public string Title
|
||||
public TabTheme TabTheme
|
||||
{
|
||||
get => _title;
|
||||
get => _tabTheme;
|
||||
set
|
||||
{
|
||||
value = string.IsNullOrWhiteSpace(value) ? DefaultTitle : value;
|
||||
Set(ref _tabTheme, value);
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
}
|
||||
}
|
||||
|
||||
if (Set(ref _title, value) && IsSelected)
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public Terminal Terminal { get; private set; }
|
||||
|
||||
public OverlayViewModel Overlay { get; private set; }
|
||||
|
||||
public TerminalTheme TerminalTheme
|
||||
{
|
||||
get => _terminalTheme;
|
||||
set => Set(ref _terminalTheme, value);
|
||||
}
|
||||
|
||||
public bool Initialized { get; set; }
|
||||
|
||||
public string TabTitle
|
||||
{
|
||||
get => _tabTitle;
|
||||
set
|
||||
{
|
||||
var title = value?.Trim() ?? string.Empty;
|
||||
|
||||
if (ShellProfile is SshProfile)
|
||||
{
|
||||
_applicationView.Title = Title;
|
||||
// For SshProfile we are adjusting title.
|
||||
if (title.Equals("ssh", StringComparison.OrdinalIgnoreCase) ||
|
||||
title.EndsWith("\\ssh.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
title = $"[ssh] {I18N.Translate("Authenticate")}";
|
||||
}
|
||||
else if (title.Equals("mosh", StringComparison.OrdinalIgnoreCase) ||
|
||||
title.EndsWith("\\mosh.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
title = $"[mosh] {I18N.Translate("Authenticate")}";
|
||||
}
|
||||
else if (SshTitleRx.IsMatch(title))
|
||||
{
|
||||
title = $"[ssh] {title}";
|
||||
}
|
||||
}
|
||||
|
||||
if (Set(ref _tabTitle, title))
|
||||
{
|
||||
CustomTitleChanged?.Invoke(this, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CloseView()
|
||||
public string ShellTitle
|
||||
{
|
||||
_terminalView.Close();
|
||||
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
|
||||
get => _shellTitle;
|
||||
set
|
||||
{
|
||||
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
|
||||
{
|
||||
TabTitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task FocusTerminal()
|
||||
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
|
||||
|
||||
public Task Close()
|
||||
{
|
||||
return _terminalView?.FocusTerminal();
|
||||
}
|
||||
MessengerInstance.Unregister(this);
|
||||
|
||||
public async Task OnViewIsReady(ITerminalView terminalView)
|
||||
{
|
||||
_terminalView = terminalView;
|
||||
|
||||
var options = _settingsService.GetTerminalOptions();
|
||||
var theme = _settingsService.GetCurrentTheme();
|
||||
var keyBindings = _settingsService.GetKeyBindings();
|
||||
|
||||
var size = await _terminalView.CreateTerminal(options, theme.Colors, FlattenKeyBindings(keyBindings)).ConfigureAwait(true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_startupDirectory))
|
||||
{
|
||||
_shellProfile.WorkingDirectory = _startupDirectory;
|
||||
}
|
||||
|
||||
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile).ConfigureAwait(true);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_terminalId = response.Id;
|
||||
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
|
||||
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
|
||||
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
|
||||
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
|
||||
{
|
||||
_connectedEvent.Wait();
|
||||
_webSocket.Send(t);
|
||||
});
|
||||
|
||||
DefaultTitle = response.ShellExecutableName;
|
||||
Title = DefaultTitle;
|
||||
|
||||
var webSocketUrl = "ws://127.0.0.1:" + response.Id;
|
||||
var webSocketServer = new WebSocketServer(webSocketUrl);
|
||||
webSocketServer.Start(socket =>
|
||||
{
|
||||
_webSocket = socket;
|
||||
socket.OnOpen = () => _connectedEvent.Set();
|
||||
socket.OnMessage = message => _trayProcessCommunicationService.WriteText(_terminalId, message);
|
||||
});
|
||||
|
||||
await _terminalView.ConnectToSocket(webSocketUrl).ConfigureAwait(true);
|
||||
Initialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _dialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
await FocusTerminal().ConfigureAwait(true);
|
||||
return Terminal.Close();
|
||||
}
|
||||
|
||||
public void CopyText(string text)
|
||||
{
|
||||
_clipboardService.SetText(text);
|
||||
ClipboardService.SetText(text);
|
||||
if (_terminalOptions.ShowTextCopied)
|
||||
{
|
||||
Overlay.Show(I18N.Translate("TextCopied"));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EditTitle()
|
||||
{
|
||||
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"));
|
||||
if (result != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
{
|
||||
_hasCustomTitle = false;
|
||||
TabTitle = ShellTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
_hasCustomTitle = true;
|
||||
TabTitle = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void FocusTerminal()
|
||||
{
|
||||
Activated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void CloseSearchPanel()
|
||||
{
|
||||
SearchText = string.Empty;
|
||||
ShowSearchPanel = false;
|
||||
_terminalView.FocusTerminal();
|
||||
FocusTerminal();
|
||||
}
|
||||
|
||||
private Task FindNext()
|
||||
private bool CanExecuteCommand<T>(T a)
|
||||
{
|
||||
return _terminalView.FindNext(SearchText);
|
||||
return CanExecuteCommand();
|
||||
}
|
||||
|
||||
private Task FindPrevious()
|
||||
private bool CanExecuteCommand()
|
||||
{
|
||||
return _terminalView.FindPrevious(SearchText);
|
||||
return Initialized && !_disposalRequested;
|
||||
}
|
||||
|
||||
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<Command, ICollection<KeyBinding>> keyBindings)
|
||||
private async Task CloseTab()
|
||||
{
|
||||
return keyBindings.Values.SelectMany(k => k);
|
||||
await TryClose().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
private void CloseLeftTabs()
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
CloseLeftTabsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void CloseRightTabs()
|
||||
{
|
||||
CloseRightTabsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void CloseOtherTabs()
|
||||
{
|
||||
CloseOtherTabsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void FindNext()
|
||||
{
|
||||
FindNextRequested?.Invoke(this, SearchText);
|
||||
}
|
||||
|
||||
public ITerminalView TerminalView { get; set; }
|
||||
|
||||
public async Task<string> SerializeXtermState()
|
||||
{
|
||||
return await TerminalView?.SerializeXtermState();
|
||||
}
|
||||
|
||||
private void FindPrevious()
|
||||
{
|
||||
FindPreviousRequested?.Invoke(this, SearchText);
|
||||
}
|
||||
|
||||
private void DuplicateTab()
|
||||
{
|
||||
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
|
||||
{
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
ApplicationSettings = e;
|
||||
ApplicationSettings = message.ApplicationSettings;
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnCurrentThemeChanged(object sender, Guid e)
|
||||
private async void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(async () =>
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
var currentTheme = _settingsService.GetTheme(e);
|
||||
await _terminalView.ChangeTheme(currentTheme.Colors).ConfigureAwait(true);
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnKeyBindingsChanged(object sender, EventArgs e)
|
||||
{
|
||||
var keyBindings = _settingsService.GetKeyBindings();
|
||||
await _applicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
await _terminalView.ChangeKeyBindings(FlattenKeyBindings(keyBindings)).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnKeyboardCommandReceived(object sender, Command e)
|
||||
{
|
||||
if (e == Command.Copy)
|
||||
{
|
||||
var selection = await _terminalView.GetSelection().ConfigureAwait(true);
|
||||
_clipboardService.SetText(selection);
|
||||
}
|
||||
else if (e == Command.Paste)
|
||||
{
|
||||
var content = await _clipboardService.GetText().ConfigureAwait(true);
|
||||
if (content != null)
|
||||
// only change theme if not overwritten by profile
|
||||
if (ShellProfile.TerminalThemeId == Guid.Empty)
|
||||
{
|
||||
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
|
||||
var currentTheme = SettingsService.GetTheme(message.ThemeId);
|
||||
TerminalTheme = currentTheme;
|
||||
ThemeChanged?.Invoke(this, currentTheme);
|
||||
}
|
||||
}
|
||||
else if (e == Command.Search)
|
||||
{
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
{
|
||||
_terminalView.FocusSearchTextBox();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_keyboardCommandService.SendCommand(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResizeOverlayTimerFinished(object sender, object e)
|
||||
{
|
||||
_resizeOverlayTimer.Stop();
|
||||
ShowResizeOverlay = false;
|
||||
}
|
||||
|
||||
private void OnTerminalExited(object sender, int e)
|
||||
{
|
||||
if (e == _terminalId)
|
||||
{
|
||||
_applicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
await _terminalView.ChangeOptions(e).ConfigureAwait(true);
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnTerminalSizeChanged(object sender, TerminalSize e)
|
||||
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
|
||||
ShowResizeOverlay = true;
|
||||
await _trayProcessCommunicationService.ResizeTerminal(_terminalId, e).ConfigureAwait(true);
|
||||
_terminalOptions = message.TerminalOptions;
|
||||
}
|
||||
|
||||
private void OnTerminalTitleChanged(object sender, string e)
|
||||
private void SelectTabTheme(string id)
|
||||
{
|
||||
Title = e;
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
|
||||
}
|
||||
|
||||
private void Terminal_Exited(object sender, int exitCode)
|
||||
{
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker && exitCode != 0)
|
||||
{
|
||||
tracker.SetInvalid();
|
||||
}
|
||||
|
||||
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
|
||||
}
|
||||
|
||||
private void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
|
||||
Terminal.KeyboardCommandReceived -= Terminal_KeyboardCommandReceived;
|
||||
Terminal.OutputReceived -= Terminal_OutputReceived;
|
||||
Terminal.SizeChanged -= Terminal_SizeChanged;
|
||||
Terminal.TitleChanged -= Terminal_TitleChanged;
|
||||
Terminal.Exited -= Terminal_Exited;
|
||||
Terminal.Closed -= Terminal_Closed;
|
||||
}
|
||||
|
||||
private async void Terminal_KeyboardCommandReceived(object sender, string e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case nameof(Command.Copy):
|
||||
{
|
||||
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
|
||||
CopyText(selection);
|
||||
break;
|
||||
}
|
||||
case nameof(Command.Paste):
|
||||
{
|
||||
string content = await ClipboardService.GetText().ConfigureAwait(true);
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.TranslateLineEndings(content);
|
||||
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case nameof(Command.PasteWithoutNewlines):
|
||||
{
|
||||
string content = await ClipboardService.GetText().ConfigureAwait(true);
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
|
||||
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case nameof(Command.Search):
|
||||
{
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
{
|
||||
SearchStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
_keyboardCommandService.SendCommand(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
|
||||
{
|
||||
tracker.SetOutputReceived();
|
||||
}
|
||||
|
||||
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_SizeChanged(object sender, TerminalSize e)
|
||||
{
|
||||
Overlay.Show($"{e.Columns} x {e.Rows}");
|
||||
}
|
||||
|
||||
private void Terminal_TitleChanged(object sender, string e)
|
||||
{
|
||||
ShellTitle = e;
|
||||
ShellTitleChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private async Task TryClose()
|
||||
{
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
|
||||
{
|
||||
tracker.SetInvalid();
|
||||
}
|
||||
|
||||
if (ApplicationSettings.ConfirmClosingTabs)
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), String.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.Cancel)
|
||||
{
|
||||
@@ -349,8 +597,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
await Close().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,58 +29,57 @@ namespace FluentTerminal.App.ViewModels
|
||||
private string _cursor;
|
||||
private string _cursorAccent;
|
||||
private string _cyan;
|
||||
private string _fallBackAuthor;
|
||||
private TerminalColors _fallBackColors;
|
||||
private string _fallBackName;
|
||||
private TerminalTheme _fallbackTheme;
|
||||
private string _foreground;
|
||||
private string _green;
|
||||
private bool _inEditMode;
|
||||
private bool _isActive;
|
||||
private string _magenta;
|
||||
private string _name;
|
||||
private bool _isNew;
|
||||
private string _red;
|
||||
private string _selection;
|
||||
private readonly TerminalTheme _theme;
|
||||
private string _white;
|
||||
private string _yellow;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
public event EventHandler<string> BackgroundChanged;
|
||||
|
||||
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService)
|
||||
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, bool isNew)
|
||||
{
|
||||
_theme = theme;
|
||||
Model = theme;
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_isNew = isNew;
|
||||
|
||||
Name = _theme.Name;
|
||||
Author = _theme.Author;
|
||||
Id = _theme.Id;
|
||||
Name = Model.Name;
|
||||
Author = Model.Author;
|
||||
Id = Model.Id;
|
||||
|
||||
Black = _theme.Colors.Black;
|
||||
Red = _theme.Colors.Red;
|
||||
Green = _theme.Colors.Green;
|
||||
Yellow = _theme.Colors.Yellow;
|
||||
Blue = _theme.Colors.Blue;
|
||||
Magenta = _theme.Colors.Magenta;
|
||||
Cyan = _theme.Colors.Cyan;
|
||||
White = _theme.Colors.White;
|
||||
Black = Model.Colors.Black;
|
||||
Red = Model.Colors.Red;
|
||||
Green = Model.Colors.Green;
|
||||
Yellow = Model.Colors.Yellow;
|
||||
Blue = Model.Colors.Blue;
|
||||
Magenta = Model.Colors.Magenta;
|
||||
Cyan = Model.Colors.Cyan;
|
||||
White = Model.Colors.White;
|
||||
|
||||
BrightBlack = _theme.Colors.BrightBlack;
|
||||
BrightRed = _theme.Colors.BrightRed;
|
||||
BrightGreen = _theme.Colors.BrightGreen;
|
||||
BrightYellow = _theme.Colors.BrightYellow;
|
||||
BrightBlue = _theme.Colors.BrightBlue;
|
||||
BrightMagenta = _theme.Colors.BrightMagenta;
|
||||
BrightCyan = _theme.Colors.BrightCyan;
|
||||
BrightWhite = _theme.Colors.BrightWhite;
|
||||
BrightBlack = Model.Colors.BrightBlack;
|
||||
BrightRed = Model.Colors.BrightRed;
|
||||
BrightGreen = Model.Colors.BrightGreen;
|
||||
BrightYellow = Model.Colors.BrightYellow;
|
||||
BrightBlue = Model.Colors.BrightBlue;
|
||||
BrightMagenta = Model.Colors.BrightMagenta;
|
||||
BrightCyan = Model.Colors.BrightCyan;
|
||||
BrightWhite = Model.Colors.BrightWhite;
|
||||
|
||||
Background = _theme.Colors.Background;
|
||||
Foreground = _theme.Colors.Foreground;
|
||||
Cursor = _theme.Colors.Cursor;
|
||||
CursorAccent = _theme.Colors.CursorAccent;
|
||||
Selection = _theme.Colors.Selection;
|
||||
Background = Model.Colors.Background;
|
||||
Foreground = Model.Colors.Foreground;
|
||||
Cursor = Model.Colors.Cursor;
|
||||
CursorAccent = Model.Colors.CursorAccent;
|
||||
Selection = Model.Colors.Selection;
|
||||
|
||||
SetActiveCommand = new RelayCommand(SetActive);
|
||||
DeleteCommand = new RelayCommand(async () => await Delete().ConfigureAwait(false), NotPreInstalled);
|
||||
@@ -94,6 +93,8 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public event EventHandler Deleted;
|
||||
|
||||
public TerminalTheme Model { get; }
|
||||
|
||||
public string Author
|
||||
{
|
||||
get => _author;
|
||||
@@ -268,83 +269,130 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public void SaveChanges()
|
||||
{
|
||||
_theme.Name = Name;
|
||||
_theme.Author = Author;
|
||||
Model.Name = Name;
|
||||
Model.Author = Author;
|
||||
|
||||
_theme.Colors.Black = Black;
|
||||
_theme.Colors.Red = Red;
|
||||
_theme.Colors.Green = Green;
|
||||
_theme.Colors.Yellow = Yellow;
|
||||
_theme.Colors.Blue = Blue;
|
||||
_theme.Colors.Magenta = Magenta;
|
||||
_theme.Colors.Cyan = Cyan;
|
||||
_theme.Colors.White = White;
|
||||
Model.Colors.Black = Black;
|
||||
Model.Colors.Red = Red;
|
||||
Model.Colors.Green = Green;
|
||||
Model.Colors.Yellow = Yellow;
|
||||
Model.Colors.Blue = Blue;
|
||||
Model.Colors.Magenta = Magenta;
|
||||
Model.Colors.Cyan = Cyan;
|
||||
Model.Colors.White = White;
|
||||
|
||||
_theme.Colors.BrightBlack = BrightBlack;
|
||||
_theme.Colors.BrightRed = BrightRed;
|
||||
_theme.Colors.BrightGreen = BrightGreen;
|
||||
_theme.Colors.BrightYellow = BrightYellow;
|
||||
_theme.Colors.BrightBlue = BrightBlue;
|
||||
_theme.Colors.BrightMagenta = BrightMagenta;
|
||||
_theme.Colors.BrightCyan = BrightCyan;
|
||||
_theme.Colors.BrightWhite = BrightWhite;
|
||||
Model.Colors.BrightBlack = BrightBlack;
|
||||
Model.Colors.BrightRed = BrightRed;
|
||||
Model.Colors.BrightGreen = BrightGreen;
|
||||
Model.Colors.BrightYellow = BrightYellow;
|
||||
Model.Colors.BrightBlue = BrightBlue;
|
||||
Model.Colors.BrightMagenta = BrightMagenta;
|
||||
Model.Colors.BrightCyan = BrightCyan;
|
||||
Model.Colors.BrightWhite = BrightWhite;
|
||||
|
||||
_theme.Colors.Background = Background;
|
||||
_theme.Colors.Foreground = Foreground;
|
||||
_theme.Colors.Cursor = Cursor;
|
||||
_theme.Colors.CursorAccent = CursorAccent;
|
||||
_theme.Colors.Selection = Selection;
|
||||
Model.Colors.Background = Background;
|
||||
Model.Colors.Foreground = Foreground;
|
||||
Model.Colors.Cursor = Cursor;
|
||||
Model.Colors.CursorAccent = CursorAccent;
|
||||
Model.Colors.Selection = Selection;
|
||||
|
||||
_settingsService.SaveTheme(_theme);
|
||||
_settingsService.SaveTheme(Model);
|
||||
|
||||
InEditMode = false;
|
||||
_isNew = false;
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
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)
|
||||
if (_isNew)
|
||||
{
|
||||
Black = _fallBackColors.Black;
|
||||
Red = _fallBackColors.Red;
|
||||
Green = _fallBackColors.Green;
|
||||
Yellow = _fallBackColors.Yellow;
|
||||
Blue = _fallBackColors.Blue;
|
||||
Magenta = _fallBackColors.Magenta;
|
||||
Cyan = _fallBackColors.Cyan;
|
||||
White = _fallBackColors.White;
|
||||
await Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
TerminalTheme changedTheme = new TerminalTheme()
|
||||
{
|
||||
Name = Name,
|
||||
Author = Author,
|
||||
Colors = new TerminalColors()
|
||||
{
|
||||
Black = Black,
|
||||
Red = Red,
|
||||
Green = Green,
|
||||
Yellow = Yellow,
|
||||
Blue = Blue,
|
||||
Magenta = Magenta,
|
||||
Cyan = Cyan,
|
||||
White = White,
|
||||
|
||||
BrightBlack = _fallBackColors.BrightBlack;
|
||||
BrightRed = _fallBackColors.BrightRed;
|
||||
BrightGreen = _fallBackColors.BrightGreen;
|
||||
BrightYellow = _fallBackColors.BrightYellow;
|
||||
BrightBlue = _fallBackColors.BrightBlue;
|
||||
BrightMagenta = _fallBackColors.BrightMagenta;
|
||||
BrightCyan = _fallBackColors.BrightCyan;
|
||||
BrightWhite = _fallBackColors.BrightWhite;
|
||||
BrightBlack = BrightBlack,
|
||||
BrightRed = BrightRed,
|
||||
BrightGreen = BrightGreen,
|
||||
BrightYellow = BrightYellow,
|
||||
BrightBlue = BrightBlue,
|
||||
BrightMagenta = BrightMagenta,
|
||||
BrightCyan = BrightCyan,
|
||||
BrightWhite = BrightWhite,
|
||||
|
||||
Background = _fallBackColors.Background;
|
||||
Foreground = _fallBackColors.Foreground;
|
||||
Cursor = _fallBackColors.Cursor;
|
||||
CursorAccent = _fallBackColors.CursorAccent;
|
||||
Selection = _fallBackColors.Selection;
|
||||
Background = Background,
|
||||
Foreground = Foreground,
|
||||
Cursor = Cursor,
|
||||
CursorAccent = CursorAccent,
|
||||
Selection = Selection
|
||||
}
|
||||
};
|
||||
|
||||
Name = _fallBackName;
|
||||
Author = _fallBackAuthor;
|
||||
if (!_fallbackTheme.Equals(changedTheme))
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
InEditMode = false;
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Black = _fallbackTheme.Colors.Black;
|
||||
Red = _fallbackTheme.Colors.Red;
|
||||
Green = _fallbackTheme.Colors.Green;
|
||||
Yellow = _fallbackTheme.Colors.Yellow;
|
||||
Blue = _fallbackTheme.Colors.Blue;
|
||||
Magenta = _fallbackTheme.Colors.Magenta;
|
||||
Cyan = _fallbackTheme.Colors.Cyan;
|
||||
White = _fallbackTheme.Colors.White;
|
||||
|
||||
BrightBlack = _fallbackTheme.Colors.BrightBlack;
|
||||
BrightRed = _fallbackTheme.Colors.BrightRed;
|
||||
BrightGreen = _fallbackTheme.Colors.BrightGreen;
|
||||
BrightYellow = _fallbackTheme.Colors.BrightYellow;
|
||||
BrightBlue = _fallbackTheme.Colors.BrightBlue;
|
||||
BrightMagenta = _fallbackTheme.Colors.BrightMagenta;
|
||||
BrightCyan = _fallbackTheme.Colors.BrightCyan;
|
||||
BrightWhite = _fallbackTheme.Colors.BrightWhite;
|
||||
|
||||
Background = _fallbackTheme.Colors.Background;
|
||||
Foreground = _fallbackTheme.Colors.Foreground;
|
||||
Cursor = _fallbackTheme.Colors.Cursor;
|
||||
CursorAccent = _fallbackTheme.Colors.CursorAccent;
|
||||
Selection = _fallbackTheme.Colors.Selection;
|
||||
|
||||
Name = _fallbackTheme.Name;
|
||||
Author = _fallbackTheme.Author;
|
||||
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool NotPreInstalled()
|
||||
{
|
||||
return !_theme.PreInstalled;
|
||||
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);
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteTheme"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
@@ -354,9 +402,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
_fallBackColors = new TerminalColors(_theme.Colors);
|
||||
_fallBackName = Name;
|
||||
_fallBackAuthor = Author;
|
||||
_fallbackTheme = new TerminalTheme(Model);
|
||||
InEditMode = true;
|
||||
}
|
||||
|
||||
@@ -367,7 +413,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private Task Export()
|
||||
{
|
||||
var content = JsonConvert.SerializeObject(_theme, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
|
||||
var content = JsonConvert.SerializeObject(Model, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
|
||||
return _fileSystemService.SaveTextFile(Name, "Fluent Terminal Theme", ".flutecolors", content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Utilities
|
||||
{
|
||||
public static class TaskUtilities
|
||||
{
|
||||
#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
|
||||
public static async void FireAndForgetSafeAsync(this Task task, IErrorHandler handler = null)
|
||||
#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
handler?.HandleError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace FluentTerminal.App.ViewModels.Utilities
|
||||
{
|
||||
public static class WebViewSpecialCharEncoder
|
||||
{
|
||||
public static string Encode(string value)
|
||||
{
|
||||
return value.Replace("\"", """).Replace("'", "&squo;").Replace("\\", "\");
|
||||
}
|
||||
|
||||
public static string Decode(string value)
|
||||
{
|
||||
return value.Replace(""", "\"").Replace("&squo;", "'").Replace("\", "\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace FluentTerminal.App.Adapters
|
||||
{
|
||||
private readonly AppServiceConnection _appServiceConnection;
|
||||
|
||||
public event EventHandler<IDictionary<string, string>> MessageReceived;
|
||||
public event EventHandler<ValueSet> MessageReceived;
|
||||
|
||||
public AppServiceConnectionAdapter(AppServiceConnection appServiceConnection)
|
||||
{
|
||||
@@ -22,35 +22,16 @@ namespace FluentTerminal.App.Adapters
|
||||
|
||||
private void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
|
||||
{
|
||||
var messageType = (string)args.Request.Message[MessageKeys.Type];
|
||||
var messageContent = (string)args.Request.Message[MessageKeys.Content];
|
||||
|
||||
MessageReceived?.Invoke(this, new Dictionary<string, string>
|
||||
{
|
||||
[MessageKeys.Type] = messageType,
|
||||
[MessageKeys.Content] = messageContent
|
||||
});
|
||||
MessageReceived?.Invoke(this, args.Request.Message);
|
||||
}
|
||||
|
||||
public async Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message)
|
||||
public async Task<ValueSet> SendMessageAsync(ValueSet message)
|
||||
{
|
||||
var valueSet = new ValueSet();
|
||||
|
||||
foreach (var pair in message)
|
||||
{
|
||||
valueSet.Add(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
var response = await _appServiceConnection.SendMessageAsync(valueSet).AsTask();
|
||||
var response = await _appServiceConnection.SendMessageAsync(message).AsTask();
|
||||
|
||||
if (response.Status == AppServiceResponseStatus.Success)
|
||||
{
|
||||
var responseDict = new Dictionary<string, string>();
|
||||
foreach (var pair in response.Message)
|
||||
{
|
||||
responseDict.Add(pair.Key, (string)pair.Value);
|
||||
}
|
||||
return responseDict;
|
||||
return response.Message;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using FluentTerminal.App.Services.EventArgs;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.Core;
|
||||
using Windows.Foundation.Metadata;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Core.Preview;
|
||||
using Windows.UI.ViewManagement;
|
||||
@@ -13,30 +14,62 @@ namespace FluentTerminal.App.Adapters
|
||||
{
|
||||
private readonly ApplicationView _applicationView;
|
||||
private readonly CoreDispatcher _dispatcher;
|
||||
private bool _closed;
|
||||
|
||||
public event CloseRequestedHandler CloseRequested;
|
||||
public event EventHandler Closed;
|
||||
|
||||
public ApplicationViewAdapter()
|
||||
{
|
||||
_applicationView = ApplicationView.GetForCurrentView();
|
||||
_applicationView.Consolidated += _applicationView_Consolidated;
|
||||
_dispatcher = CoreApplication.GetCurrentView().Dispatcher;
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequest;
|
||||
|
||||
Logger.Instance.Debug("Created ApplicationViewAdapter for ApplicationView with Id: {Id}", _applicationView.Id);
|
||||
}
|
||||
|
||||
private void _applicationView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
|
||||
{
|
||||
_applicationView.Consolidated -= _applicationView_Consolidated;
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public int Id => _applicationView.Id;
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => _applicationView.Title;
|
||||
set => _applicationView.Title = value;
|
||||
set => _applicationView.Title = value ?? string.Empty;
|
||||
}
|
||||
|
||||
public Task RunOnDispatcherThread(Action action)
|
||||
public bool IsApiContractPresent(string api, ushort version)
|
||||
{
|
||||
return ApiInformation.IsApiContractPresent(api, version);
|
||||
}
|
||||
|
||||
public Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true)
|
||||
{
|
||||
if (!enforceNewSchedule && _dispatcher.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
|
||||
}
|
||||
|
||||
public Task<bool> TryClose()
|
||||
public async Task<bool> TryClose()
|
||||
{
|
||||
return _applicationView.TryConsolidateAsync().AsTask();
|
||||
if (_closed)
|
||||
{
|
||||
Logger.Instance.Debug("ApplicationViewAdapter.TryClose was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
|
||||
return true;
|
||||
}
|
||||
Logger.Instance.Debug("TryClose ApplicationView with Id: {Id}", _applicationView.Id);
|
||||
_closed = await _applicationView.TryConsolidateAsync().AsTask();
|
||||
return _closed;
|
||||
}
|
||||
|
||||
public bool ToggleFullScreen()
|
||||
@@ -57,10 +90,19 @@ namespace FluentTerminal.App.Adapters
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
var args = new CancelableEventArgs();
|
||||
await CloseRequested?.Invoke(this, args);
|
||||
|
||||
if (CloseRequested != null)
|
||||
{
|
||||
await CloseRequested.Invoke(this, args);
|
||||
}
|
||||
|
||||
e.Handled = args.Cancelled;
|
||||
|
||||
if (!e.Handled)
|
||||
{
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
|
||||
}
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,11 @@ namespace FluentTerminal.App.Adapters
|
||||
public DispatcherTimerAdapter()
|
||||
{
|
||||
_dispatcherTimer = new DispatcherTimer();
|
||||
_dispatcherTimer.Tick += OnDispatcherTimerTick;
|
||||
}
|
||||
|
||||
private void OnDispatcherTimerTick(object sender, object e)
|
||||
{
|
||||
Tick?.Invoke(this, null);
|
||||
Tick?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public TimeSpan Interval
|
||||
@@ -31,12 +30,14 @@ namespace FluentTerminal.App.Adapters
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_dispatcherTimer.Tick += OnDispatcherTimerTick;
|
||||
_dispatcherTimer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_dispatcherTimer.Stop();
|
||||
_dispatcherTimer.Tick -= OnDispatcherTimerTick;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Dialogs;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Popups;
|
||||
@@ -29,14 +30,19 @@ namespace FluentTerminal.App.Adapters
|
||||
|
||||
public void AddButton(DialogButton button)
|
||||
{
|
||||
_messageDialog.Commands.Add(new UICommand(button.ToString()));
|
||||
var command = new UICommand
|
||||
{
|
||||
Label = I18N.Translate(button.ToString()),
|
||||
Id = button
|
||||
};
|
||||
_messageDialog.Commands.Add(command);
|
||||
}
|
||||
|
||||
public async Task<DialogButton> ShowAsync()
|
||||
{
|
||||
var result = await _messageDialog.ShowAsync();
|
||||
|
||||
return Enum.Parse<DialogButton>(result.Label);
|
||||
return (DialogButton)result.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,13 @@
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<ResourceDictionary Source="Styles\Custom.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<LinearGradientBrush x:Key="MissingColorSelectionBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Offset="0" Color="Black" />
|
||||
<GradientStop Offset="1.0" Color="White" />
|
||||
</LinearGradientBrush>
|
||||
<converters:IntToVisibilityConverter
|
||||
x:Key="OneToVisibleConverter"
|
||||
Else="Collapsed"
|
||||
@@ -21,6 +26,8 @@
|
||||
<converters:NegateConverter x:Key="NegateConverter" />
|
||||
<converters:FalseToVisibleConverter x:Key="FalseToVisibleConverter" />
|
||||
<converters:TrueToVisibleConverter x:Key="TrueToVisibleConverter" />
|
||||
<converters:TabColorFallbackConverter x:Key="TabColorFallbackConverter" />
|
||||
<converters:TextMiddleEllipsisConverter x:Key="TextMiddleEllipsisConverter" />
|
||||
<converters:IntToVisibilityConverter
|
||||
x:Key="ZeroToCollapsedConverter"
|
||||
Else="Visible"
|
||||
@@ -30,6 +37,10 @@
|
||||
<converters:BackgroundToApplicationThemeConverter x:Key="BackgroundToApplicationThemeConverter" />
|
||||
<converters:StringToColorConverter x:Key="StringToColorConverter" />
|
||||
<converters:EnumValueToVisibiltyConverter x:Key="EnumValueToVisibiltyConverter" />
|
||||
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
|
||||
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
|
||||
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
|
||||
<converters:I18NConverter x:Key="I18NConverter" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
Regular → Executable
+811
-303
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Loaded 100 of 447 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user