Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68208ff0ec | ||
|
|
2d7d1810a9 | ||
|
|
cbc90cc94c | ||
|
|
0d48fac129 | ||
|
|
099f7c701d | ||
|
|
2057c6d189 | ||
|
|
56ed197b02 | ||
|
|
5642e755af | ||
|
|
981eb9b362 | ||
|
|
dd6917e1f7 | ||
|
|
e9769a776d | ||
|
|
1068ca98e8 | ||
|
|
c2c93878c7 | ||
|
|
cded0ad97a | ||
|
|
fb299121a3 | ||
|
|
cce7b6e0f1 | ||
|
|
c169dd496b | ||
|
|
70d117cb64 | ||
|
|
39e8f2ba7a | ||
|
|
0a8c186a98 | ||
|
|
ea23b37947 | ||
|
|
ea50883d1c | ||
|
|
1c08a6d486 | ||
|
|
9f75007e21 | ||
|
|
38e1c4f68f | ||
|
|
57cfa6e99a | ||
|
|
1e7831c9a5 | ||
|
|
16730d2810 | ||
|
|
4ca061baa9 | ||
|
|
b753dcbf4d | ||
|
|
7dc2c85d62 | ||
|
|
5e76cf9967 | ||
|
|
8999ded059 | ||
|
|
6d83e357d6 | ||
|
|
05c385342c | ||
|
|
0dc1a1cd5d | ||
|
|
e1fc980779 | ||
|
|
ccd8c1eaa4 | ||
|
|
ad1a933207 | ||
|
|
8d7c57d6fe | ||
|
|
8d039f59ef | ||
|
|
cb8d3cabc7 | ||
|
|
579def4f63 | ||
|
|
5d54d448b0 | ||
|
|
dec492e39a | ||
|
|
20540937fe | ||
|
|
3dd29d7bb7 | ||
|
|
90ac2b0c7c | ||
|
|
e459255b23 | ||
|
|
9165716899 | ||
|
|
49cf63c6b8 | ||
|
|
d0b5f99e28 | ||
|
|
e1b28d2f83 | ||
|
|
24a2a23b23 | ||
|
|
25536e9bcb | ||
|
|
2f984fc411 | ||
|
|
596eee712b | ||
|
|
7b18037862 | ||
|
|
41cffb1a1c | ||
|
|
6ddfba93d9 | ||
|
|
6a1e12c83a | ||
|
|
e24261ab02 | ||
|
|
60d294b20f | ||
|
|
1ee051a106 | ||
|
|
ea1a167dd3 | ||
|
|
1a91a6b22e | ||
|
|
941c525409 | ||
|
|
d7f741c2ad | ||
|
|
ab73b28582 | ||
|
|
95ccf3a984 | ||
|
|
93eccc3470 | ||
|
|
f86f20321d | ||
|
|
475e2da999 | ||
|
|
985cac99e6 | ||
|
|
832b9bdf97 | ||
|
|
264502e1e6 | ||
|
|
6015dbda2f | ||
|
|
d879ee02fc | ||
|
|
27c55c6f09 | ||
|
|
395e380a3f | ||
|
|
ae6e61d6fa | ||
|
|
5ae7151916 | ||
|
|
0e2821eeca | ||
|
|
3ef374f79d | ||
|
|
9413cdc500 | ||
|
|
80d57342f9 | ||
|
|
089ec5dc54 | ||
|
|
a9ec669a27 | ||
|
|
7ed503390d | ||
|
|
33bc06a366 | ||
|
|
9c6a7ea2e2 | ||
|
|
3126d7b672 | ||
|
|
b5be5e4995 | ||
|
|
6a0b4b13f2 | ||
|
|
d3ad7b3797 | ||
|
|
6ae99e641b | ||
|
|
d1bf35607e | ||
|
|
603bcdffa4 | ||
|
|
71f2d56301 | ||
|
|
7f816d96cc | ||
|
|
5a2e2a5292 | ||
|
|
73f0c3e313 | ||
|
|
ff94887899 | ||
|
|
a8a357dc4b | ||
|
|
21d76a5245 | ||
|
|
0627495113 | ||
|
|
3397760955 | ||
|
|
ec9169306f | ||
|
|
1e48cb85d2 | ||
|
|
055e265f2f | ||
|
|
f692453ee2 | ||
|
|
272cc36788 | ||
|
|
5948ee6a4c | ||
|
|
47fc720df0 | ||
|
|
8008ba1298 | ||
|
|
7251beb2f1 | ||
|
|
79c48e4852 | ||
|
|
a709220e3a | ||
|
|
f5d23bce49 | ||
|
|
be4d1d5cab | ||
|
|
8dad2eb81b | ||
|
|
8019709534 | ||
|
|
83b9b0d457 | ||
|
|
e559c3ee38 | ||
|
|
a829c2e135 | ||
|
|
173baf899d | ||
|
|
ad2f45f062 | ||
|
|
85d04eab4b | ||
|
|
ec8ad133d1 | ||
|
|
c8b4d2ed8f | ||
|
|
b377ac8e38 | ||
|
|
12e0d5496d | ||
|
|
8593b5ac5f | ||
|
|
122dfccc00 | ||
|
|
7e95f151ed | ||
|
|
087c9aacbb | ||
|
|
5bea17da29 | ||
|
|
af25086090 | ||
|
|
80673696e9 | ||
|
|
1668908489 | ||
|
|
18164f1075 | ||
|
|
c461cdc37e | ||
|
|
bd843c77b2 | ||
|
|
9f3ba75ce6 | ||
|
|
a3fbb17615 | ||
|
|
eb8c4003a3 | ||
|
|
a4559ae168 | ||
|
|
03f0adbdd6 | ||
|
|
17c0a3b438 | ||
|
|
e1652d99dc | ||
|
|
1c029fc70e | ||
|
|
7ab42ec1de | ||
|
|
4b21a5abb6 | ||
|
|
b78e775285 | ||
|
|
a872d126ce | ||
|
|
5bc679d8e6 | ||
|
|
ae2b4a481d | ||
|
|
d90c403060 | ||
|
|
3a8fcaf099 | ||
|
|
473732b5ca | ||
|
|
8fa971c4e5 | ||
|
|
fd48f021e0 | ||
|
|
6868bfa7ca | ||
|
|
dcde49c7b3 | ||
|
|
21631f5cca | ||
|
|
89a75ba782 | ||
|
|
7893e804a4 | ||
|
|
f19d457209 | ||
|
|
2d94753690 | ||
|
|
37b4fd283a | ||
|
|
46b8bb037a | ||
|
|
073b15c5f8 | ||
|
|
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 |
No files matched your search
@@ -0,0 +1,23 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Build client
|
||||
working-directory: ./FluentTerminal.Client
|
||||
run: |
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: warrenbuckley/Setup-MSBuild@v1
|
||||
|
||||
- name: MSBuild
|
||||
run: msbuild FluentTerminal.sln -t:build -p:Configuration=Debug -p:Platform=x64 -m -restore
|
||||
File renamed without changes.
BIN
Binary file not shown.
File renamed without changes.
BIN
Binary file not shown.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
@@ -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, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowCreateKeyBindingDialog();
|
||||
|
||||
@@ -36,9 +36,9 @@ 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>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("title");
|
||||
}
|
||||
@@ -49,9 +49,9 @@ 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>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("content");
|
||||
}
|
||||
@@ -61,9 +61,9 @@ 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>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content);
|
||||
|
||||
showMessageDialogAsnyc.Should().Throw<ArgumentException>().And.ParamName.Should().Be("buttons");
|
||||
}
|
||||
@@ -75,30 +75,19 @@ 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>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
|
||||
var dialogService = new DialogService(() => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
messageDialog.Verify(x => x.ShowAsync(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
|
||||
{
|
||||
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
|
||||
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
|
||||
|
||||
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<ISshProfileSelectionDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowInputDialogAsync(title);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
@@ -11,13 +11,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.10.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.7.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.12.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="AutoFixture" Version="4.11.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="Moq" Version="4.14.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -10,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
|
||||
@@ -36,7 +38,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
|
||||
new SettingsService(defaultValueProvider.Object, applicationDataContainers);
|
||||
@@ -58,6 +61,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
ShellProfiles = shellProfilesContainer.Object,
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
@@ -86,6 +90,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
@@ -111,6 +116,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
|
||||
@@ -134,6 +140,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
@@ -155,11 +162,13 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
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);
|
||||
|
||||
@@ -178,7 +187,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -203,10 +213,12 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = roamingSettings.Object,
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = 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);
|
||||
|
||||
@@ -225,7 +237,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -247,7 +260,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -270,7 +284,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -293,7 +308,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -316,7 +332,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -338,7 +355,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -360,10 +378,12 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.TerminalOptionsChanged += (s, e) => terminalOptionsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<TerminalOptionsChangedMessage>(this,
|
||||
message => terminalOptionsChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveTerminalOptions(terminalOptions);
|
||||
|
||||
@@ -383,7 +403,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -405,7 +426,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -427,10 +449,12 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.ApplicationSettingsChanged += (s, e) => applicationSettingsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this,
|
||||
a => applicationSettingsChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveApplicationSettings(applicationSettings);
|
||||
|
||||
@@ -453,7 +477,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -477,7 +502,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
|
||||
|
||||
@@ -502,7 +528,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
|
||||
@@ -525,10 +552,12 @@ namespace FluentTerminal.App.Services.Test
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
|
||||
message => keyBindingsChangedEventInvoked = true);
|
||||
|
||||
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
|
||||
|
||||
@@ -546,7 +575,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
|
||||
|
||||
@@ -571,10 +601,12 @@ namespace FluentTerminal.App.Services.Test
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
|
||||
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
|
||||
message => keyBindingsChangedEventInvoked = true);
|
||||
|
||||
settingsService.ResetKeyBindings();
|
||||
|
||||
@@ -594,6 +626,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
|
||||
@@ -619,6 +652,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
Themes = Mock.Of<IApplicationDataContainer>()
|
||||
};
|
||||
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
|
||||
@@ -640,6 +674,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
ShellProfiles = shellProfilesContainer.Object,
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
@@ -649,7 +684,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);
|
||||
}
|
||||
|
||||
@@ -662,6 +697,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
ShellProfiles = shellProfilesContainer.Object,
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
@@ -683,6 +719,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
ShellProfiles = shellProfilesContainer.Object,
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
@@ -707,6 +744,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
ShellProfiles = shellProfilesContainer.Object,
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = localSettings.Object,
|
||||
@@ -735,6 +773,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
SshProfiles = Mock.Of<IApplicationDataContainer>(),
|
||||
KeyBindings = Mock.Of<IApplicationDataContainer>(),
|
||||
LocalSettings = Mock.Of<IApplicationDataContainer>(),
|
||||
RoamingSettings = roamingSettings.Object,
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
|
||||
var result = await trayProcessCommunicationService.CreateTerminalAsync(id, terminalSize, shellProfile, sessionType);
|
||||
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
@@ -68,9 +68,9 @@ namespace FluentTerminal.App.Services.Test
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
await trayProcessCommunicationService.ResizeTerminal(terminalId, terminalSize);
|
||||
await trayProcessCommunicationService.ResizeTerminalAsync(terminalId, terminalSize);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == ResizeTerminalRequest.Identifier)), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -88,7 +88,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
await trayProcessCommunicationService.Write(terminalId, data);
|
||||
await trayProcessCommunicationService.WriteAsync(terminalId, data);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
|
||||
}
|
||||
@@ -107,9 +107,9 @@ namespace FluentTerminal.App.Services.Test
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
await trayProcessCommunicationService.CloseTerminal(terminalId);
|
||||
await trayProcessCommunicationService.CloseTerminalAsync(terminalId);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == TerminalExitedRequest.Identifier)), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -121,7 +121,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
var message = new ValueSet
|
||||
{
|
||||
[MessageKeys.Type] = TerminalExitedRequest.Identifier,
|
||||
[MessageKeys.Type] = (byte) MessageIdentifiers.TerminalExitedRequest,
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
|
||||
};
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Windows.Storage;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
|
||||
namespace FluentTerminal.App.Services.Adapters
|
||||
{
|
||||
@@ -20,6 +21,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);
|
||||
@@ -51,7 +57,8 @@ namespace FluentTerminal.App.Services.Adapters
|
||||
|
||||
public void WriteValueAsJson<T>(string name, T value)
|
||||
{
|
||||
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value);
|
||||
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value,
|
||||
PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,6 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface IShellProfileSelectionDialog
|
||||
{
|
||||
ShellProfile SelectedProfile { get; }
|
||||
|
||||
Task<ShellProfile> SelectProfile();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services.EventArgs
|
||||
{
|
||||
public class NewWindowRequestedEventArgs : System.EventArgs
|
||||
{
|
||||
public Models.Enums.NewWindowAction Action { get; set; }
|
||||
public Guid ProfileId { get; set; }
|
||||
public ShellProfile Profile { get; }
|
||||
|
||||
public NewWindowRequestedEventArgs(ShellProfile profile) =>
|
||||
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.Services.Exceptions
|
||||
{
|
||||
public class ReadTextFileException : Exception
|
||||
{
|
||||
public ReadTextFileException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public ReadTextFileException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ReadTextFileException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected ReadTextFileException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,12 +6,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="PListNet" Version="3.0.0" />
|
||||
<PackageReference Include="RestSharp" Version="106.6.9" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="MvvmLightLibsStd10" Version="5.4.1.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="PListNet" Version="3.2.0" />
|
||||
<PackageReference Include="RestSharp" Version="106.11.4" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
||||
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
void Delete(string key);
|
||||
|
||||
void Clear();
|
||||
|
||||
IEnumerable<object> GetAll();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentTerminal.App.Services.EventArgs;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Core;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
@@ -14,9 +15,13 @@ namespace FluentTerminal.App.Services
|
||||
int Id { get; }
|
||||
string Title { get; set; }
|
||||
|
||||
Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true);
|
||||
Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
|
||||
bool enforceNewSchedule = false);
|
||||
|
||||
Task<bool> TryClose();
|
||||
Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
|
||||
bool enforceNewSchedule = false);
|
||||
|
||||
Task<bool> TryCloseAsync();
|
||||
|
||||
bool ToggleFullScreen();
|
||||
bool IsApiContractPresent(string api, ushort version);
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IClipboardService
|
||||
{
|
||||
Task<string> GetText();
|
||||
Task<string> GetTextAsync();
|
||||
|
||||
void SetText(string text);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ICommandHistoryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the most recent <paramref name="top"/> commands from history.
|
||||
/// </summary>
|
||||
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
|
||||
/// <param name="top">Maximal number of items to return.</param>
|
||||
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
|
||||
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
|
||||
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
|
||||
/// already have them loaded.</param>
|
||||
List<ExecutedCommand> GetHistoryRecentFirst(bool includeProfiles = false, int top = int.MaxValue,
|
||||
Func<List<ShellProfile>> profilesProvider = null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the most used <paramref name="top"/> commands from history.
|
||||
/// </summary>
|
||||
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
|
||||
/// <param name="top">Maximal number of items to return.</param>
|
||||
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
|
||||
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
|
||||
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
|
||||
/// already have them loaded.</param>
|
||||
List<ExecutedCommand> GetHistoryMostUsedFirst(bool includeProfiles = false, int top = int.MaxValue,
|
||||
Func<List<ShellProfile>> profilesProvider = null);
|
||||
|
||||
void MarkUsed(ShellProfile profile);
|
||||
|
||||
void Delete(ExecutedCommand executedCommand);
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,23 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public enum DialogButton
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
OK,
|
||||
Cancel
|
||||
}
|
||||
|
||||
public interface IDialogService
|
||||
{
|
||||
Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons);
|
||||
|
||||
Task<ShellProfile> ShowProfileSelectionDialogAsync();
|
||||
Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons);
|
||||
|
||||
Task<KeyBinding> ShowCreateKeyBindingDialog();
|
||||
|
||||
Task<string> ShowInputDialogAsync(string title);
|
||||
|
||||
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null);
|
||||
|
||||
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
|
||||
Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null);
|
||||
|
||||
Task ShowAboutDialogAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IDispatcherTimer
|
||||
{
|
||||
void Start();
|
||||
|
||||
void Stop();
|
||||
|
||||
TimeSpan Interval { get; set; }
|
||||
bool IsEnabled { get; }
|
||||
|
||||
event EventHandler<object> Tick;
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IFileSystemService
|
||||
{
|
||||
Task<File> OpenFile(IEnumerable<string> fileTypes);
|
||||
Task<File> OpenFileAsync(IEnumerable<string> fileTypes);
|
||||
|
||||
Task<string> BrowseForDirectory();
|
||||
Task<string> BrowseForDirectoryAsync();
|
||||
|
||||
Task SaveTextFile(string name, string fileTypeDescription, string fileType, string content);
|
||||
Task SaveTextFileAsync(string name, string fileTypeDescription, string fileType, string content);
|
||||
|
||||
Task<ImageFile> SaveImageInRoamingAsync(ImageFile imageFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentTerminal.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IImageFileSystemService
|
||||
{
|
||||
Task<ImageFile> ImportTemporaryImageFileAsync(IEnumerable<string> fileTypes);
|
||||
|
||||
Task RemoveTemporaryBackgroundThemeImageAsync();
|
||||
|
||||
Task RemoveImportedImageAsync(string fileName);
|
||||
|
||||
string EncodeImage(ImageFile imageFile);
|
||||
|
||||
Task<ImageFile> ImportThemeImageAsync(ImageFile backgroundImage, string encodedImage);
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,6 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
void SendCommand(string command);
|
||||
void DeregisterCommandHandler(string command);
|
||||
void ClearCommandHandlers();
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,6 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ISettingsService
|
||||
{
|
||||
event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
|
||||
event EventHandler<Guid> CurrentThemeChanged;
|
||||
event EventHandler KeyBindingsChanged;
|
||||
event EventHandler<ShellProfile> ShellProfileAdded;
|
||||
event EventHandler<Guid> ShellProfileDeleted;
|
||||
event EventHandler<SshProfile> SshProfileAdded;
|
||||
event EventHandler<Guid> SshProfileDeleted;
|
||||
event EventHandler<TerminalOptions> TerminalOptionsChanged;
|
||||
event EventHandler<Guid> ThemeDeleted;
|
||||
event EventHandler<TerminalTheme> ThemeAdded;
|
||||
|
||||
void DeleteShellProfile(Guid id);
|
||||
void DeleteSshProfile(Guid id);
|
||||
void DeleteTheme(Guid id);
|
||||
@@ -24,24 +13,26 @@ namespace FluentTerminal.App.Services
|
||||
TerminalTheme GetCurrentTheme();
|
||||
Guid GetCurrentThemeId();
|
||||
ShellProfile GetDefaultShellProfile();
|
||||
SshProfile GetDefaultSshProfile();
|
||||
Guid GetDefaultShellProfileId();
|
||||
Guid GetDefaultSshProfileId();
|
||||
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
|
||||
IEnumerable<ShellProfile> GetShellProfiles();
|
||||
IEnumerable<SshProfile> GetSshProfiles();
|
||||
/// <summary>
|
||||
/// Returns union of <see cref="GetShellProfiles"/> and <see cref="GetSshProfiles"/>.
|
||||
/// </summary>
|
||||
IEnumerable<ShellProfile> GetAllProfiles();
|
||||
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 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);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IShellProfileMigrationService
|
||||
{
|
||||
void Migrate(ShellProfile profile);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IStartupTaskService
|
||||
{
|
||||
Task DisableStartupTask();
|
||||
Task<StartupTaskStatus> EnableStartupTask();
|
||||
Task<StartupTaskStatus> GetStatus();
|
||||
Task DisableStartupTaskAsync();
|
||||
Task<StartupTaskStatus> EnableStartupTaskAsync();
|
||||
Task<StartupTaskStatus> GetStatusAsync();
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,7 @@ namespace FluentTerminal.App.Services
|
||||
IEnumerable<string> SupportedFileTypes { get; }
|
||||
|
||||
Task<TerminalTheme> Parse(string fileName, Stream fileContent);
|
||||
|
||||
Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent);
|
||||
}
|
||||
}
|
||||
@@ -12,30 +12,43 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
void Initialize(IAppServiceConnection appServiceConnection);
|
||||
|
||||
Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
|
||||
Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
|
||||
SessionType sessionType);
|
||||
|
||||
Task ResizeTerminal(byte id, TerminalSize size);
|
||||
Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause);
|
||||
|
||||
Task UpdateToggleWindowKeyBindings();
|
||||
Task ResizeTerminalAsync(byte id, TerminalSize size);
|
||||
|
||||
Task Write(byte terminalId, byte[] data);
|
||||
Task UpdateToggleWindowKeyBindingsAsync();
|
||||
|
||||
Task WriteAsync(byte terminalId, byte[] data);
|
||||
|
||||
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
|
||||
|
||||
Task CloseTerminal(byte terminalId);
|
||||
Task<GetAvailablePortResponse> GetAvailablePort();
|
||||
void UnsubscribeFromTerminalOutput(byte terminalId);
|
||||
|
||||
Task CloseTerminalAsync(byte terminalId);
|
||||
|
||||
byte GetNextTerminalId();
|
||||
|
||||
Task<string> GetUserName();
|
||||
Task<string> GetUserNameAsync();
|
||||
|
||||
Task SaveTextFileAsync(string path, string content);
|
||||
|
||||
Task<string> ReadTextFileAsync(string path);
|
||||
|
||||
Task<string> GetSshConfigDirAsync();
|
||||
|
||||
Task<string[]> GetFilesFromSshConfigDirAsync();
|
||||
|
||||
Task<bool> CheckFileExistsAsync(string path);
|
||||
|
||||
void MuteTerminal(bool mute);
|
||||
Task MuteTerminalAsync(bool mute);
|
||||
|
||||
void UpdateSettings(ApplicationSettings settings);
|
||||
|
||||
Task<string> GetCommandPathAsync(string command);
|
||||
|
||||
Task QuitApplicationAsync();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IUpdateService
|
||||
{
|
||||
Task CheckForUpdate(bool notifyNoUpdate = false);
|
||||
Task CheckForUpdateAsync(bool notifyNoUpdate = false);
|
||||
Version GetCurrentVersion();
|
||||
Task<Version> GetLatestVersionAsync();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
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
|
||||
{
|
||||
@@ -11,6 +11,14 @@ 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,
|
||||
@@ -18,19 +26,23 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
UnderlineSelectedTab = false,
|
||||
InactiveTabColorMode = InactiveTabColorMode.Background,
|
||||
NewTerminalLocation = NewTerminalLocation.Tab,
|
||||
TabWindowCascadingAppMenu = true,
|
||||
TabsPosition = TabsPosition.Top,
|
||||
CopyOnSelect = false,
|
||||
MouseMiddleClickAction = MouseAction.None,
|
||||
MouseRightClickAction = MouseAction.ContextMenu,
|
||||
AlwaysShowTabs = false,
|
||||
ShowNewOutputIndicator = false,
|
||||
EnableTrayIcon = true,
|
||||
ShowCustomTitleInTitlebar = true,
|
||||
UseMoshByDefault = true,
|
||||
AutoFallbackToWindowsUsernameInLinks = true,
|
||||
UseQuickSshConnectByDefault = false,
|
||||
RTrimCopiedLines = true,
|
||||
MuteTerminalBeeps = true
|
||||
MuteTerminalBeeps = true,
|
||||
EnableLogging = false,
|
||||
PrintableOutputOnly = true,
|
||||
LogDirectoryPath = logDirectoryPath,
|
||||
UseConPty = true,
|
||||
ShowTextCopied = false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,6 +102,30 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
};
|
||||
|
||||
case Command.DuplicateTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.DuplicateTab),
|
||||
Ctrl = true,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.D
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ReconnectTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ReconnectTab),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.R
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -102,18 +138,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ConfigurableNewTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ConfigurableNewTab),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.T
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewSshTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -138,31 +162,30 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
};
|
||||
|
||||
case Command.SavedSshNewTab:
|
||||
case Command.NewCustomCommandTab:
|
||||
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),
|
||||
Command = nameof(Command.NewCustomCommandTab),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.U
|
||||
Key = (int)ExtendedVirtualKey.T
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
case Command.NewCustomCommandWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewCustomCommandWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.N
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ChangeTabTitle:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -199,18 +222,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ConfigurableNewWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ConfigurableNewWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.N
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ShowSettings:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -311,10 +322,39 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Key = (int)ExtendedVirtualKey.L
|
||||
}
|
||||
};
|
||||
|
||||
case Command.IncreaseFontSize:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.IncreaseFontSize),
|
||||
Ctrl = true,
|
||||
Key = (int)ExtendedVirtualKey.Plus
|
||||
}
|
||||
};
|
||||
case Command.DecreaseFontSize:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.DecreaseFontSize),
|
||||
Ctrl = true,
|
||||
Key = (int)ExtendedVirtualKey.Minus
|
||||
}
|
||||
};
|
||||
case Command.ResetFontSize:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ResetFontSize),
|
||||
Ctrl = true,
|
||||
Key = (int)ExtendedVirtualKey.Number0
|
||||
}
|
||||
};
|
||||
default:
|
||||
throw new InvalidOperationException($"Default keybindings for Command '{command}' are missing");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Guid GetDefaultShellProfileId()
|
||||
@@ -329,7 +369,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
new TabTheme
|
||||
{
|
||||
Id = 0,
|
||||
Name = I18N.Translate("TabTheme.None"),
|
||||
Name = "TabTheme.None",
|
||||
BackgroundOpacity = double.NaN,
|
||||
BackgroundPointerOverOpacity = double.NaN,
|
||||
BackgroundPressedOpacity = double.NaN,
|
||||
@@ -340,37 +380,37 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
new TabTheme
|
||||
{
|
||||
Id = 1,
|
||||
Name = I18N.Translate("TabTheme.Red"),
|
||||
Name = "TabTheme.Red",
|
||||
Color = "#E81123"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 2,
|
||||
Name = I18N.Translate("TabTheme.Green"),
|
||||
Name = "TabTheme.Green",
|
||||
Color = "#10893E"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 3,
|
||||
Name = I18N.Translate("TabTheme.Blue"),
|
||||
Name = "TabTheme.Blue",
|
||||
Color = "#0078D7"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 4,
|
||||
Name = I18N.Translate("TabTheme.Purple"),
|
||||
Name = "TabTheme.Purple",
|
||||
Color = "#881798"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 5,
|
||||
Name = I18N.Translate("TabTheme.Orange"),
|
||||
Name = "TabTheme.Orange",
|
||||
Color = "#FF8C00"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 6,
|
||||
Name = I18N.Translate("TabTheme.Teal"),
|
||||
Name = "TabTheme.Teal",
|
||||
Color = "#00B7C3"
|
||||
}
|
||||
};
|
||||
@@ -386,10 +426,12 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
ScrollBarStyle = ScrollBarStyle.Hidden,
|
||||
FontFamily = "Consolas",
|
||||
FontSize = 13,
|
||||
BoldText = false,
|
||||
FontWeight = 400,
|
||||
BackgroundOpacity = 0.8,
|
||||
Padding = 12,
|
||||
ScrollBackLimit = 1000
|
||||
ScrollBackLimit = 1000,
|
||||
WordSeparator = " ()[]{}:;|│!&*<>@"&squo;",
|
||||
UseAcrylicBackground = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -406,12 +448,17 @@ 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,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+
|
||||
UseBuffer = false,
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -429,12 +476,17 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
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,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
|
||||
UseBuffer = true,
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -452,12 +504,17 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
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,
|
||||
LineEndingTranslation = LineEndingStyle.ToLF,
|
||||
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
|
||||
UseBuffer = true, //TODO: Set to false if the buffer causes issues with WSL.
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
|
||||
@@ -7,24 +7,24 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public class DialogService : IDialogService
|
||||
{
|
||||
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<ISshProfileSelectionDialog> _sshProfileSelectionDialogFactory;
|
||||
private readonly Func<ICustomCommandDialog> _customCommandDialogFactory;
|
||||
private readonly Func<IAboutDialog> _aboutDialogFactory;
|
||||
|
||||
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
|
||||
Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
|
||||
public DialogService(Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
|
||||
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
|
||||
Func<ISshProfileSelectionDialog> sshProfileSelectionDialogFactory)
|
||||
Func<ICustomCommandDialog> customCommandDialogFactory,
|
||||
Func<IAboutDialog> aboutDialogFactory)
|
||||
{
|
||||
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
|
||||
_messageDialogFactory = messageDialogFactory;
|
||||
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
|
||||
_inputDialogFactory = inputDialogFactory;
|
||||
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
|
||||
_sshProfileSelectionDialogFactory = sshProfileSelectionDialogFactory;
|
||||
_customCommandDialogFactory = customCommandDialogFactory;
|
||||
_aboutDialogFactory = aboutDialogFactory;
|
||||
}
|
||||
|
||||
public Task<KeyBinding> ShowCreateKeyBindingDialog()
|
||||
@@ -34,7 +34,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return dialog.CreateKeyBinding();
|
||||
}
|
||||
|
||||
public Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons)
|
||||
public Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
@@ -71,21 +71,17 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
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<SshProfile> ShowSshProfileSelectionDialogAsync()
|
||||
{
|
||||
var dialog = _sshProfileSelectionDialogFactory();
|
||||
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
|
||||
_customCommandDialogFactory().GetCustomCommandAsync(input);
|
||||
|
||||
return dialog.SelectProfile();
|
||||
public Task ShowAboutDialogAsync()
|
||||
{
|
||||
var dialog = _aboutDialogFactory();
|
||||
|
||||
return dialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,20 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public class FluentTerminalThemeParser : IThemeParser
|
||||
{
|
||||
public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".flutecolors" };
|
||||
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".flutecolors" };
|
||||
|
||||
public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
|
||||
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent)
|
||||
{
|
||||
return DeserializeThemeAsync<ExportedTerminalTheme>(fileName, fileContent);
|
||||
}
|
||||
|
||||
public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
|
||||
{
|
||||
return DeserializeThemeAsync<TerminalTheme>(fileName, fileContent);
|
||||
}
|
||||
|
||||
private async Task<T> DeserializeThemeAsync<T>(string fileName, Stream fileContent)
|
||||
where T : TerminalTheme
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
@@ -25,8 +36,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
using (var streamReader = new StreamReader(fileContent))
|
||||
{
|
||||
var content = await streamReader.ReadToEndAsync();
|
||||
var theme = JsonConvert.DeserializeObject<TerminalTheme>(content);
|
||||
var content = await streamReader.ReadToEndAsync().ConfigureAwait(false);
|
||||
var theme = JsonConvert.DeserializeObject<T>(content);
|
||||
|
||||
theme.PreInstalled = false;
|
||||
theme.Id = Guid.NewGuid();
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
@@ -14,7 +15,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
private const byte Opacity30Percent = 77;
|
||||
|
||||
public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".itermcolors" };
|
||||
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".itermcolors" };
|
||||
|
||||
private static class ITermThemeKeys
|
||||
{
|
||||
@@ -36,7 +37,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
public const string Ansi15Color = "Ansi 15 Color";
|
||||
|
||||
public const string BackgroundColor = "Background Color";
|
||||
public const string BoldColor = "Bold Color";
|
||||
//public const string BoldColor = "Bold Color";
|
||||
public const string CursorColor = "Cursor Color";
|
||||
public const string CursorTextColor = "Cursor Text Color";
|
||||
public const string ForegroundColor = "Foreground Color";
|
||||
@@ -51,7 +52,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 +66,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 +84,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 +113,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}";
|
||||
@@ -133,5 +134,30 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
return (alpha / 256.0).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileName));
|
||||
}
|
||||
|
||||
if (fileContent == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileContent));
|
||||
}
|
||||
|
||||
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
|
||||
|
||||
var terminalTheme = new TerminalTheme
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(fileName),
|
||||
Colors = GetColors(node),
|
||||
Id = Guid.NewGuid(),
|
||||
PreInstalled = false
|
||||
};
|
||||
|
||||
return Task.FromResult(new ExportedTerminalTheme(terminalTheme, string.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,5 +42,10 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
throw new KeyNotFoundException($"No handler registered for command: {command}");
|
||||
}
|
||||
|
||||
public void ClearCommandHandlers()
|
||||
{
|
||||
_commandHandlers.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
// The purpose of this class is to ensure backward compatibility of mosh profiles and command history
|
||||
// TODO: This class should be probably deleted in few months or so
|
||||
public static class MoshBackwardCompatibility
|
||||
{
|
||||
private static readonly string MoshCommandExe = $"{Constants.MoshCommandName}.exe";
|
||||
|
||||
private static readonly Regex OldFormatRx = new Regex(@".+\s(?<ports>\d{1,5}:\d{1,5})$", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex TargetRx =
|
||||
new Regex(@"(^|\s+)(?<user>[^\s@;]+)@(?<host>[^\s@:]+)(:(?<port>\d{1,5}))?\s+", RegexOptions.Compiled);
|
||||
|
||||
private static bool IsMoshProfile(ShellProfile profile) =>
|
||||
Constants.MoshCommandName.Equals(profile.Location, StringComparison.OrdinalIgnoreCase) ||
|
||||
MoshCommandExe.Equals(profile.Location, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the profile contains obsolete mosh syntax, and changes it if it does.
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile. Note that the method doesn't change the input profile at all, but creates and
|
||||
/// returns its clone if there's something to change.</param>
|
||||
/// <remarks>
|
||||
/// The method actually checks if <see cref="ShellProfile.Arguments"/> of the input <paramref name="profile"/> is in
|
||||
/// obsolete <c>mosh</c> format <c>[ssh_arguments] [target] [mosh_port_range]</c>, and if it is changes it to the new
|
||||
/// format <c>--ssh="[ssh_arguments]" -p [mosh_ports] [target]</c>.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// If the method detected that the input profile is obsolete mosh profile, it will create its clone with the updated
|
||||
/// command line arguments, and return it. Otherwise it will simply return unchanged input profile (the same reference
|
||||
/// as the input profile). It allows us to simply check if anything is changed by comparing the input and output
|
||||
/// profiles for reference equality.
|
||||
/// </returns>
|
||||
public static ShellProfile FixProfile(ShellProfile profile)
|
||||
{
|
||||
// If there aren't any arguments, or if the profile isn't a mosh profile, there's nothing to do. Just return.
|
||||
if (string.IsNullOrWhiteSpace(profile.Arguments) || !IsMoshProfile(profile)) return profile;
|
||||
|
||||
var arguments = profile.Arguments.Trim();
|
||||
|
||||
var moshPortsMatch = OldFormatRx.Match(arguments);
|
||||
|
||||
if (!moshPortsMatch.Success) return profile;// Arguments don't end with mosh ports (i.e. 60000:61000), so it isn't an obsolete format.
|
||||
|
||||
var targetMatches = TargetRx.Matches(arguments);
|
||||
|
||||
if (targetMatches.Count != 1) return profile;// Target part (user@host:port) not found or found multiple times. Cannot reliably determine the target, so don't change.
|
||||
|
||||
var targetMatch = targetMatches[0];
|
||||
|
||||
// Removing [mosh_ports] from the original arguments
|
||||
arguments = arguments.Substring(0, arguments.Length - moshPortsMatch.Groups["ports"].Length).Trim();
|
||||
|
||||
var newProfile = profile.Clone();
|
||||
|
||||
// Removing [target] from the original arguments, thus getting "pure" ssh arguments
|
||||
var sshArguments = targetMatch.Index > 0 ? arguments.Substring(0, targetMatch.Index).Trim() : string.Empty;
|
||||
|
||||
if (targetMatch.Index + targetMatch.Length < arguments.Length)
|
||||
{
|
||||
sshArguments += $" {arguments.Substring(targetMatch.Index + targetMatch.Length).Trim()}";
|
||||
}
|
||||
|
||||
// If the ssh port is defined in the original target (i.e. user@host:port), which was allowed in the obsolete mosh, extracting the port part into a separate ssh argument
|
||||
if (targetMatch.Groups["port"].Success && uint.TryParse(targetMatch.Groups["port"].Value, out var port) &&
|
||||
port != SshProfile.DefaultSshPort)
|
||||
{
|
||||
sshArguments += $" -p {targetMatch.Groups["port"].Value}";
|
||||
}
|
||||
|
||||
// Replacing any double quotes in ssh arguments with single quotes, since ssh arguments will be double-quoted as a whole
|
||||
sshArguments = sshArguments.Replace("\"", "'").Trim();
|
||||
|
||||
arguments = $"-p {moshPortsMatch.Groups["ports"].Value}";
|
||||
|
||||
if (!string.IsNullOrEmpty(sshArguments))
|
||||
{
|
||||
// If there were some ssh arguments, add them as defined in the new format
|
||||
arguments += $" --ssh=\"ssh {sshArguments}\"";
|
||||
}
|
||||
|
||||
// Finally append the target to the arguments
|
||||
arguments += $" {targetMatch.Groups["user"]}@{targetMatch.Groups["host"]}";
|
||||
|
||||
newProfile.Arguments = arguments;
|
||||
|
||||
return newProfile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models.Messages;
|
||||
using GalaSoft.MvvmLight.Messaging;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
@@ -60,7 +63,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
TerminalOptions = GetTerminalOptions(),
|
||||
Themes = new List<TerminalTheme>(),
|
||||
Profiles = new List<ShellProfile>(),
|
||||
SshProfiles = new List<SshProfile>()
|
||||
SshProfiles = new List<SshProfile>(),
|
||||
DefaultSettings = GetDefaultSettings(),
|
||||
};
|
||||
|
||||
foreach (var theme in GetThemes().Where(x => !x.PreInstalled))
|
||||
@@ -68,7 +72,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
config.Themes.Add(theme);
|
||||
}
|
||||
|
||||
foreach (var profile in GetShellProfiles().Where(x => !x.PreInstalled))
|
||||
foreach (var profile in GetShellProfiles())
|
||||
{
|
||||
config.Profiles.Add(profile);
|
||||
}
|
||||
@@ -78,7 +82,16 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
config.SshProfiles.Add(profile);
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(config);
|
||||
return JsonConvert.SerializeObject(config, PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> GetDefaultSettings()
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
[DefaultShellProfileKey] = GetDefaultShellProfileId().ToString(),
|
||||
[CurrentThemeKey] = GetCurrentThemeId().ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
public void ImportSettings(string serializedSettings)
|
||||
@@ -90,7 +103,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Themes = new List<TerminalTheme>(),
|
||||
Profiles = new List<ShellProfile>(),
|
||||
SshProfiles = new List<SshProfile>(),
|
||||
TerminalOptions = GetTerminalOptions()
|
||||
TerminalOptions = GetTerminalOptions(),
|
||||
DefaultSettings = new Dictionary<string, string>(),
|
||||
};
|
||||
|
||||
JsonConvert.PopulateObject(serializedSettings, config);
|
||||
@@ -114,22 +128,18 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
SaveTheme(theme, existingTheme != null);
|
||||
}
|
||||
|
||||
// Can't create pre-installed profiles
|
||||
foreach (var profile in config.Profiles.Where(x => !x.PreInstalled))
|
||||
foreach (var profile in config.Profiles)
|
||||
{
|
||||
var existingProfile = GetShellProfile(profile.Id);
|
||||
var isNew = existingProfile.EqualTo(default);
|
||||
var isNew = existingProfile == 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);
|
||||
// those cannot be edited
|
||||
profile.Name = existingProfile.Name;
|
||||
profile.Location = existingProfile.Location;
|
||||
|
||||
SaveShellProfile(profile, false);
|
||||
continue;
|
||||
}
|
||||
SaveShellProfile(profile, isNew);
|
||||
@@ -138,41 +148,35 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
foreach (var profile in config.SshProfiles)
|
||||
{
|
||||
var existingProfile = GetSshProfile(profile.Id);
|
||||
var isNew = existingProfile.EqualTo(default(SshProfile));
|
||||
var isNew = existingProfile == default;
|
||||
|
||||
SaveSshProfile(profile, isNew);
|
||||
}
|
||||
|
||||
SaveTerminalOptions(config.TerminalOptions);
|
||||
|
||||
if (config.DefaultSettings.ContainsKey(DefaultShellProfileKey))
|
||||
{
|
||||
SaveDefaultShellProfileId(new Guid(config.DefaultSettings[DefaultShellProfileKey]));
|
||||
}
|
||||
|
||||
if (config.DefaultSettings.ContainsKey(CurrentThemeKey))
|
||||
{
|
||||
SaveCurrentThemeId(new Guid(config.DefaultSettings[CurrentThemeKey]));
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
|
||||
|
||||
public event EventHandler<TerminalTheme> ThemeAdded;
|
||||
public event EventHandler<Guid> CurrentThemeChanged;
|
||||
public event EventHandler<Guid> ThemeDeleted;
|
||||
|
||||
public event EventHandler KeyBindingsChanged;
|
||||
|
||||
public event EventHandler<ShellProfile> ShellProfileAdded;
|
||||
public event EventHandler<Guid> ShellProfileDeleted;
|
||||
|
||||
public event EventHandler<SshProfile> SshProfileAdded;
|
||||
public event EventHandler<Guid> SshProfileDeleted;
|
||||
|
||||
public event EventHandler<TerminalOptions> TerminalOptionsChanged;
|
||||
|
||||
public void DeleteShellProfile(Guid id)
|
||||
{
|
||||
_shellProfiles.Delete(id.ToString());
|
||||
ShellProfileDeleted?.Invoke(this, id);
|
||||
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
|
||||
}
|
||||
|
||||
public void DeleteSshProfile(Guid id)
|
||||
{
|
||||
_sshProfiles.Delete(id.ToString());
|
||||
SshProfileDeleted?.Invoke(this, id);
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +193,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
}
|
||||
|
||||
ThemeDeleted?.Invoke(this, id);
|
||||
Messenger.Default.Send(new ThemeDeletedMessage(id));
|
||||
}
|
||||
|
||||
public ApplicationSettings GetApplicationSettings()
|
||||
@@ -231,13 +235,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
public SshProfile GetDefaultSshProfile()
|
||||
{
|
||||
var id = GetDefaultSshProfileId();
|
||||
var profile = _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public ShellProfile GetShellProfile(Guid id)
|
||||
{
|
||||
@@ -256,14 +253,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
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()
|
||||
{
|
||||
@@ -278,15 +267,43 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
public IEnumerable<ShellProfile> GetShellProfiles()
|
||||
{
|
||||
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string)x)).ToList();
|
||||
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string) x))
|
||||
.Select(MoshBackwardCompatibilityFixProfile);
|
||||
}
|
||||
|
||||
public IEnumerable<SshProfile> GetSshProfiles()
|
||||
{
|
||||
if (_sshProfiles == null)
|
||||
return new List<SshProfile>();
|
||||
return _sshProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x))
|
||||
.Select(MoshBackwardCompatibilityFixProfile).Cast<SshProfile>();
|
||||
}
|
||||
|
||||
public IEnumerable<ShellProfile> GetAllProfiles()
|
||||
{
|
||||
return GetShellProfiles().Union(GetSshProfiles());
|
||||
}
|
||||
|
||||
private ShellProfile MoshBackwardCompatibilityFixProfile(ShellProfile profile)
|
||||
{
|
||||
var fixedProfile = MoshBackwardCompatibility.FixProfile(profile);
|
||||
|
||||
if (ReferenceEquals(fixedProfile, profile))
|
||||
{
|
||||
// Nothing changed
|
||||
return fixedProfile;
|
||||
}
|
||||
|
||||
if (fixedProfile is SshProfile sshProfile)
|
||||
{
|
||||
DeleteSshProfile(profile.Id);
|
||||
SaveSshProfile(sshProfile);
|
||||
}
|
||||
else
|
||||
return _sshProfiles.GetAll()
|
||||
.Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x)).ToList();
|
||||
{
|
||||
DeleteShellProfile(profile.Id);
|
||||
SaveShellProfile(fixedProfile);
|
||||
}
|
||||
|
||||
return fixedProfile;
|
||||
}
|
||||
|
||||
public IEnumerable<TabTheme> GetTabThemes()
|
||||
@@ -316,29 +333,32 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
|
||||
}
|
||||
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
|
||||
{
|
||||
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
|
||||
ApplicationSettingsChanged?.Invoke(this, applicationSettings);
|
||||
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
|
||||
}
|
||||
|
||||
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
|
||||
{
|
||||
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
|
||||
}
|
||||
|
||||
public void SaveCurrentThemeId(Guid id)
|
||||
{
|
||||
_roamingSettings.SetValue(CurrentThemeKey, id);
|
||||
|
||||
CurrentThemeChanged?.Invoke(this, 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);
|
||||
|
||||
Messenger.Default.Send(new DefaultShellProfileChangedMessage(id));
|
||||
}
|
||||
|
||||
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
|
||||
@@ -349,7 +369,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
|
||||
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
|
||||
@@ -357,30 +377,39 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
|
||||
|
||||
// When saving the shell profile, we also need to update keybindings for everywhere.
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
|
||||
if (newShell)
|
||||
{
|
||||
ShellProfileAdded?.Invoke(this, shellProfile);
|
||||
Messenger.Default.Send(new ShellProfileAddedMessage(shellProfile));
|
||||
}
|
||||
else
|
||||
{
|
||||
Messenger.Default.Send(new ShellProfileChangedMessage(shellProfile));
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveSshProfile(SshProfile sshProfile, bool newShell = false)
|
||||
{
|
||||
_sshProfiles.WriteValueAsJson(sshProfile.Id.ToString(), sshProfile);
|
||||
|
||||
// When saving the shell profile, we also need to update keybindings for everywhere.
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
|
||||
if (newShell)
|
||||
{
|
||||
SshProfileAdded?.Invoke(this, sshProfile);
|
||||
Messenger.Default.Send(new ShellProfileAddedMessage(sshProfile));
|
||||
}
|
||||
else
|
||||
{
|
||||
Messenger.Default.Send(new ShellProfileChangedMessage(sshProfile));
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveTerminalOptions(TerminalOptions terminalOptions)
|
||||
{
|
||||
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
|
||||
TerminalOptionsChanged?.Invoke(this, terminalOptions);
|
||||
Messenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions));
|
||||
}
|
||||
|
||||
public void SaveTheme(TerminalTheme theme, bool newTheme = false)
|
||||
@@ -389,12 +418,12 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
if (theme.Id == GetCurrentThemeId())
|
||||
{
|
||||
CurrentThemeChanged?.Invoke(this, theme.Id);
|
||||
Messenger.Default.Send(new CurrentThemeChangedMessage(theme.Id));
|
||||
}
|
||||
|
||||
if (newTheme)
|
||||
{
|
||||
ThemeAdded?.Invoke(this, theme);
|
||||
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,4 +1,5 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.App.Services.Exceptions;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Requests;
|
||||
using FluentTerminal.Models.Responses;
|
||||
@@ -7,14 +8,35 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Collections;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public class TrayProcessCommunicationService : ITrayProcessCommunicationService
|
||||
{
|
||||
#region Static
|
||||
|
||||
private static string _userName;
|
||||
private static string _sshConfigDir;
|
||||
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
|
||||
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
|
||||
|
||||
private static ValueSet CreateMessage(IMessage content)
|
||||
{
|
||||
return new ValueSet
|
||||
{
|
||||
[MessageKeys.Type] = content.Identifier,
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(content,
|
||||
PreserveDictionaryKeyCaseContractResolver.SerializerSettings)
|
||||
};
|
||||
}
|
||||
|
||||
#endregion Static
|
||||
|
||||
private readonly ISettingsService _settingsService;
|
||||
private IAppServiceConnection _appServiceConnection;
|
||||
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers;
|
||||
// ReSharper disable once RedundantDefaultMemberInitializer
|
||||
private byte _nextTerminalId = 0;
|
||||
|
||||
public event EventHandler<TerminalExitStatus> TerminalExited;
|
||||
@@ -30,21 +52,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _nextTerminalId++;
|
||||
}
|
||||
|
||||
public async Task<GetAvailablePortResponse> GetAvailablePort()
|
||||
{
|
||||
var request = new GetAvailablePortRequest();
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>((string)responseMessage[MessageKeys.Content]);
|
||||
|
||||
Logger.Instance.Debug("Received GetAvailablePortResponse: {@response}", response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private string _userName;
|
||||
|
||||
public async Task<string> GetUserName()
|
||||
public async Task<string> GetUserNameAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_userName))
|
||||
{
|
||||
@@ -52,52 +60,56 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _userName;
|
||||
}
|
||||
|
||||
GetUserNameResponse response;
|
||||
var response = await GetResponseAsync<StringValueResponse>(new GetUserNameRequest()).ConfigureAwait(false);
|
||||
|
||||
// No need to crash for username, so try/catch
|
||||
try
|
||||
if (response.Success)
|
||||
{
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new GetUserNameRequest()));
|
||||
response = JsonConvert.DeserializeObject<GetUserNameResponse>((string)responseMessage[MessageKeys.Content]);
|
||||
_userName = response.Value;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Instance.Error(e, "Error while trying to get username.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.Instance.Debug("Received GetUserNameResponse: {@response}", response);
|
||||
|
||||
_userName = response.UserName;
|
||||
|
||||
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]);
|
||||
var response =
|
||||
await GetResponseAsync<CommonResponse>(new SaveTextFileRequest {Path = path, Content = content})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new Exception(string.IsNullOrEmpty(response.Error) ? "Failed to save the file." : response.Error);
|
||||
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error)
|
||||
? "Failed to save the file."
|
||||
: response.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string _sshConfigDir;
|
||||
public async Task<string> ReadTextFileAsync(string path)
|
||||
{
|
||||
var response = await GetResponseAsync<StringValueResponse>(new ReadTextFileRequest { Path = path }).ConfigureAwait(false);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new ReadTextFileException(string.IsNullOrWhiteSpace(response.Error) ? "Failed to read the file." : response.Error);
|
||||
}
|
||||
|
||||
return response.Value;
|
||||
}
|
||||
|
||||
public async Task<string> GetSshConfigDirAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_sshConfigDir))
|
||||
if (!string.IsNullOrEmpty(_sshConfigDir))
|
||||
{
|
||||
var response = await GetSshConfigFolderAsync(false);
|
||||
return _sshConfigDir;
|
||||
}
|
||||
|
||||
if (response?.Success ?? false)
|
||||
{
|
||||
_sshConfigDir = response.Path;
|
||||
}
|
||||
var response =
|
||||
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
|
||||
{ IncludeContent = false }).ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_sshConfigDir = response.Path;
|
||||
}
|
||||
|
||||
return _sshConfigDir;
|
||||
@@ -105,45 +117,40 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
public async Task<string[]> GetFilesFromSshConfigDirAsync()
|
||||
{
|
||||
var response = await GetSshConfigFolderAsync(false);
|
||||
var response =
|
||||
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
|
||||
{IncludeContent = true}).ConfigureAwait(false);
|
||||
|
||||
if (response == null || !response.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_sshConfigDir))
|
||||
if (response.Success)
|
||||
{
|
||||
_sshConfigDir = response.Path;
|
||||
|
||||
return response.Files;
|
||||
}
|
||||
|
||||
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]);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckFileExistsAsync(string path)
|
||||
{
|
||||
var responseMessage =
|
||||
await _appServiceConnection.SendMessageAsync(CreateMessage(new CheckFileExistsRequest {Path = path}));
|
||||
var response = await GetResponseAsync<CommonResponse>(new CheckFileExistsRequest {Path = path})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return JsonConvert.DeserializeObject<CommonResponse>((string) responseMessage[MessageKeys.Content]).Success;
|
||||
return response.Success;
|
||||
}
|
||||
|
||||
public void MuteTerminal(bool mute)
|
||||
public Task MuteTerminalAsync(bool mute)
|
||||
{
|
||||
_appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest { Mute = mute }));
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest {Mute = mute}));
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
|
||||
public void UpdateSettings(ApplicationSettings settings)
|
||||
{
|
||||
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
|
||||
SessionType sessionType)
|
||||
{
|
||||
var request = new CreateTerminalRequest
|
||||
{
|
||||
@@ -155,14 +162,19 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>((string)responseMessage[MessageKeys.Content]);
|
||||
var response = await GetResponseAsync<CreateTerminalResponse>(request).ConfigureAwait(false);
|
||||
|
||||
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause)
|
||||
{
|
||||
return GetResponseAsync<PauseTerminalOutputResponse>(
|
||||
new PauseTerminalOutputRequest {Id = id, Pause = pause});
|
||||
}
|
||||
|
||||
public void Initialize(IAppServiceConnection appServiceConnection)
|
||||
{
|
||||
_appServiceConnection = appServiceConnection;
|
||||
@@ -188,7 +200,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId);
|
||||
}
|
||||
break;
|
||||
case TerminalExitedRequest.Identifier:
|
||||
case (byte) MessageIdentifiers.TerminalExitedRequest:
|
||||
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent);
|
||||
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
|
||||
|
||||
@@ -200,15 +212,10 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
}
|
||||
|
||||
public Task ResizeTerminal(byte id, TerminalSize size)
|
||||
public Task ResizeTerminalAsync(byte id, TerminalSize size)
|
||||
{
|
||||
var request = new ResizeTerminalRequest
|
||||
{
|
||||
TerminalId = id,
|
||||
NewSize = size
|
||||
};
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(new ResizeTerminalRequest
|
||||
{TerminalId = id, NewSize = size}));
|
||||
}
|
||||
|
||||
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
|
||||
@@ -216,19 +223,23 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_terminalOutputHandlers[terminalId] = callback;
|
||||
}
|
||||
|
||||
public Task UpdateToggleWindowKeyBindings()
|
||||
public void UnsubscribeFromTerminalOutput(byte id)
|
||||
{
|
||||
if (_terminalOutputHandlers.ContainsKey(id))
|
||||
{
|
||||
_terminalOutputHandlers.Remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
public Task UpdateToggleWindowKeyBindingsAsync()
|
||||
{
|
||||
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
|
||||
|
||||
var request = new SetToggleWindowKeyBindingsRequest
|
||||
{
|
||||
KeyBindings = keyBindings
|
||||
};
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(new SetToggleWindowKeyBindingsRequest
|
||||
{KeyBindings = keyBindings}));
|
||||
}
|
||||
|
||||
public Task Write(byte id, byte[] data)
|
||||
public Task WriteAsync(byte id, byte[] data)
|
||||
{
|
||||
var message = new ValueSet
|
||||
{
|
||||
@@ -240,7 +251,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _appServiceConnection.SendMessageAsync(message);
|
||||
}
|
||||
|
||||
public Task CloseTerminal(byte terminalId)
|
||||
public Task CloseTerminalAsync(byte terminalId)
|
||||
{
|
||||
var request = new TerminalExitedRequest(terminalId, -1);
|
||||
|
||||
@@ -249,13 +260,53 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
private ValueSet CreateMessage(IMessage content)
|
||||
public async Task<string> GetCommandPathAsync(string command)
|
||||
{
|
||||
return new ValueSet
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
[MessageKeys.Type] = content.Identifier,
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
|
||||
};
|
||||
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 response = await GetResponseAsync<StringValueResponse>(new GetCommandPathRequest {Command = command})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
CommandPaths[commandLower] = response.Value;
|
||||
|
||||
return response.Value;
|
||||
}
|
||||
|
||||
CommandErrors[commandLower] = response.Error;
|
||||
|
||||
throw new Exception(response.Error);
|
||||
}
|
||||
|
||||
public Task QuitApplicationAsync()
|
||||
{
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(new QuitApplicationRequest()));
|
||||
}
|
||||
|
||||
private async Task<T> GetResponseAsync<T>(IMessage request)
|
||||
{
|
||||
var messageResponse =
|
||||
await _appServiceConnection.SendMessageAsync(CreateMessage(request)).ConfigureAwait(false);
|
||||
|
||||
return JsonConvert.DeserializeObject<T>((string)messageResponse[MessageKeys.Content]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_notificationService = notificationService;
|
||||
}
|
||||
|
||||
public async Task CheckForUpdate(bool notifyNoUpdate = false)
|
||||
public async Task CheckForUpdateAsync(bool notifyNoUpdate = false)
|
||||
{
|
||||
var latest = await GetLatestVersionAsync();
|
||||
var latest = await GetLatestVersionAsync().ConfigureAwait(false);
|
||||
if (latest > GetCurrentVersion())
|
||||
{
|
||||
_notificationService.ShowNotification("Update available",
|
||||
@@ -42,7 +42,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
var restClient = new RestClient(apiEndpoint);
|
||||
var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET);
|
||||
|
||||
var restResponse = await restClient.ExecuteTaskAsync(restRequest);
|
||||
var restResponse = await restClient.ExecuteAsync(restRequest).ConfigureAwait(false);
|
||||
if (restResponse.IsSuccessful)
|
||||
{
|
||||
dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
@@ -10,14 +11,23 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private Func<Task<string>> _selectedTextCallback;
|
||||
private bool _closingFromUI = false;
|
||||
private bool _exited = false;
|
||||
private bool _closingFromUi;
|
||||
private bool _exited;
|
||||
private readonly bool _requireShellProcessStart;
|
||||
private string _fallbackTitle;
|
||||
|
||||
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
Id = _trayProcessCommunicationService.GetNextTerminalId();
|
||||
Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId();
|
||||
_requireShellProcessStart = !terminalId.HasValue;
|
||||
}
|
||||
|
||||
public void Reconnect()
|
||||
{
|
||||
_exited = false;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
}
|
||||
|
||||
private void OnTerminalExited(object sender, TerminalExitStatus status)
|
||||
@@ -30,10 +40,12 @@ namespace FluentTerminal.App.Services
|
||||
_exited = true;
|
||||
Exited?.Invoke(this, status.ExitCode);
|
||||
|
||||
if (_closingFromUI == true || status.ExitCode <= 0)
|
||||
if (_closingFromUi || status.ExitCode <= 0)
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
|
||||
_trayProcessCommunicationService.TerminalExited -= OnTerminalExited;
|
||||
}
|
||||
|
||||
public event EventHandler<int> Exited;
|
||||
@@ -63,22 +75,23 @@ namespace FluentTerminal.App.Services
|
||||
/// </summary>
|
||||
public event EventHandler<string> TitleChanged;
|
||||
|
||||
public string FallbackTitle { get; private set; }
|
||||
|
||||
public byte Id { get; }
|
||||
|
||||
public ShellProfile Profile { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// To be called by either view or viewmodel
|
||||
/// </summary>
|
||||
public async Task Close()
|
||||
public Task CloseAsync()
|
||||
{
|
||||
if (_exited)
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
_closingFromUI = true;
|
||||
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
|
||||
_closingFromUi = true;
|
||||
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
|
||||
return _trayProcessCommunicationService.CloseTerminalAsync(Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,7 +100,7 @@ namespace FluentTerminal.App.Services
|
||||
/// <returns></returns>
|
||||
public Task<string> GetSelectedText()
|
||||
{
|
||||
return _selectedTextCallback();
|
||||
return _selectedTextCallback?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,9 +122,9 @@ namespace FluentTerminal.App.Services
|
||||
/// <summary>
|
||||
/// To be called by view
|
||||
/// </summary>
|
||||
public async Task SetSize(TerminalSize size)
|
||||
public async Task SetSizeAsync(TerminalSize size)
|
||||
{
|
||||
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
|
||||
await _trayProcessCommunicationService.ResizeTerminalAsync(Id, size).ConfigureAwait(false);
|
||||
SizeChanged?.Invoke(this, size);
|
||||
}
|
||||
|
||||
@@ -122,33 +135,46 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
title = FallbackTitle;
|
||||
title = _fallbackTitle;
|
||||
}
|
||||
TitleChanged?.Invoke(this, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view when ready
|
||||
/// To be called by view when ready
|
||||
/// </summary>
|
||||
/// <param name="shellProfile"></param>
|
||||
/// <param name="size"></param>
|
||||
/// <param name="sessionType"></param>
|
||||
public async Task<CreateTerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
|
||||
public async Task<TerminalResponse> StartShellProcessAsync(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
|
||||
{
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
|
||||
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
|
||||
Profile = shellProfile;
|
||||
|
||||
if (response.Success)
|
||||
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
|
||||
{
|
||||
FallbackTitle = response.ShellExecutableName;
|
||||
SetTitle(FallbackTitle);
|
||||
OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState));
|
||||
}
|
||||
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
|
||||
|
||||
if (_requireShellProcessStart)
|
||||
{
|
||||
var response = await _trayProcessCommunicationService
|
||||
.CreateTerminalAsync(Id, size, shellProfile, sessionType).ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_fallbackTitle = response.Name;
|
||||
SetTitle(_fallbackTitle);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
else
|
||||
{
|
||||
return await _trayProcessCommunicationService.PauseTerminalOutputAsync(Id, false);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public Task Write(byte[] data)
|
||||
{
|
||||
return _trayProcessCommunicationService.Write(Id, data);
|
||||
return _trayProcessCommunicationService.WriteAsync(Id, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,10 +8,18 @@ namespace FluentTerminal.App.Services.Utilities
|
||||
/// </summary>
|
||||
public static class I18N
|
||||
{
|
||||
private static readonly ResourceLoader ResourceLoader = ResourceLoader.GetForViewIndependentUse();
|
||||
|
||||
public static string Translate(string resource)
|
||||
{
|
||||
var resourceLoader = ResourceLoader.GetForCurrentView();
|
||||
return resourceLoader.GetString(resource.Replace('.', '/'));
|
||||
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,21 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace FluentTerminal.App.Services.Utilities
|
||||
{
|
||||
public class PreserveDictionaryKeyCaseContractResolver : DefaultContractResolver
|
||||
{
|
||||
public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
|
||||
{ ContractResolver = new PreserveDictionaryKeyCaseContractResolver() };
|
||||
|
||||
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
|
||||
{
|
||||
var contract = base.CreateDictionaryContract(objectType);
|
||||
|
||||
contract.DictionaryKeyResolver = key => key;
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using System.Reflection;
|
||||
|
||||
namespace FluentTerminal.App.Services.Utilities
|
||||
{
|
||||
public class TerminalThemeContractResolver : DefaultContractResolver
|
||||
public class TerminalThemeContractResolver : PreserveDictionaryKeyCaseContractResolver
|
||||
{
|
||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||
{
|
||||
@@ -24,4 +24,4 @@ namespace FluentTerminal.App.Services.Utilities
|
||||
return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
internal class DelayedHistorySaver : ISessionSuccessTracker
|
||||
{
|
||||
private const int DelayMilliseconds = 5000;
|
||||
|
||||
private readonly Action _saveAction;
|
||||
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
private bool _sessionStarted;
|
||||
private bool _outputReceived;
|
||||
|
||||
private bool _toSave = true;
|
||||
|
||||
internal DelayedHistorySaver(Action saveAction)
|
||||
{
|
||||
_saveAction = saveAction;
|
||||
}
|
||||
|
||||
public void SetSuccessfulSessionStart()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_toSave || _sessionStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sessionStarted = true;
|
||||
|
||||
if (_outputReceived)
|
||||
{
|
||||
InitiateSave();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOutputReceived()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_toSave || _outputReceived)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_outputReceived = true;
|
||||
|
||||
if (_sessionStarted)
|
||||
{
|
||||
InitiateSave();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetInvalid()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_toSave || !(_cts is CancellationTokenSource cts))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_toSave = false;
|
||||
|
||||
cts.Cancel();
|
||||
cts.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitiateSave()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
Task.Delay(DelayMilliseconds, _cts.Token).ContinueWith(t =>
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_toSave)
|
||||
{
|
||||
_saveAction.Invoke();
|
||||
}
|
||||
|
||||
_toSave = false;
|
||||
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.1.0" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
|
||||
<PackageReference Include="MvvmLightLibsStd10" Version="5.4.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -14,4 +13,11 @@
|
||||
<ProjectReference Include="..\FluentTerminal.Models\FluentTerminal.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Windows.Foundation.UniversalApiContract">
|
||||
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.UniversalApiContract\5.0.0.0\Windows.Foundation.UniversalApiContract.winmd</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public interface ISessionSuccessTracker
|
||||
{
|
||||
void SetSuccessfulSessionStart();
|
||||
|
||||
void SetOutputReceived();
|
||||
|
||||
void SetInvalid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public interface ITerminalView : IDisposable
|
||||
{
|
||||
Task ChangeFontSize(int fontSize);
|
||||
Task ChangeThemeAsync(TerminalTheme theme);
|
||||
Task ChangeKeyBindingsAsync();
|
||||
Task ChangeOptionsAsync(TerminalOptions options);
|
||||
Task InitializeAsync(TerminalViewModel viewModel);
|
||||
Task ReconnectAsync();
|
||||
void DisposalPrepare();
|
||||
Task FindNextAsync(SearchRequest request);
|
||||
Task FindPreviousAsync(SearchRequest request);
|
||||
Task FocusTerminalAsync();
|
||||
Task<string> SerializeXtermStateAsync();
|
||||
void Paste(string text);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace FluentTerminal.App.ViewModels.Infrastructure
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: If we're using class field _isExecuting, maybe we should introduce some locking?
|
||||
_isExecuting = true;
|
||||
await _execute();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace FluentTerminal.App.ViewModels.Infrastructure
|
||||
bool CanExecute();
|
||||
}
|
||||
|
||||
public interface IAsyncCommand<T> : ICommand
|
||||
public interface IAsyncCommand<in T> : ICommand
|
||||
{
|
||||
Task ExecuteAsync(T parameter);
|
||||
bool CanExecute(T parameter);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class ExpandableMenuItemViewModel : MenuItemViewModelBase
|
||||
{
|
||||
public ObservableCollection<MenuItemViewModelBase> SubItems { get; }
|
||||
|
||||
public ExpandableMenuItemViewModel(string text, IEnumerable<MenuItemViewModelBase> subItems = null,
|
||||
string description = null, Mdl2Icon icon = null) : base(text, description, icon)
|
||||
{
|
||||
if (subItems == null)
|
||||
{
|
||||
SubItems = new ObservableCollection<MenuItemViewModelBase>();
|
||||
}
|
||||
else if (subItems is ObservableCollection<MenuItemViewModelBase> observableSubItems)
|
||||
{
|
||||
SubItems = observableSubItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
SubItems = new ObservableCollection<MenuItemViewModelBase>(subItems);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(MenuItemViewModelBase other)
|
||||
{
|
||||
if (!base.EquivalentTo(other)) return false;
|
||||
|
||||
if (!(other is ExpandableMenuItemViewModel otherExpandable) ||
|
||||
SubItems.Count != otherExpandable.SubItems.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !SubItems.Where((t, i) => !t.EquivalentTo(otherExpandable.SubItems[i])).Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public sealed class Mdl2Icon
|
||||
{
|
||||
public string Color { get; }
|
||||
public string Glyph { get; }
|
||||
|
||||
private Mdl2Icon(string glyph, string color = null)
|
||||
{
|
||||
Glyph = glyph;
|
||||
Color = color;
|
||||
}
|
||||
|
||||
public static Mdl2Icon Link(string color = null) => new Mdl2Icon("\uE71B", color);
|
||||
|
||||
public static Mdl2Icon Copy(string color = null) => new Mdl2Icon("\uE8C8", color);
|
||||
|
||||
public static Mdl2Icon Paste(string color = null) => new Mdl2Icon("\uE77F", color);
|
||||
|
||||
public static Mdl2Icon Edit(string color = null) => new Mdl2Icon("\uE70F", color);
|
||||
|
||||
public static Mdl2Icon Search(string color = null) => new Mdl2Icon("\uE721", color);
|
||||
|
||||
public static Mdl2Icon Add(string color = null) => new Mdl2Icon("\uE710", color);
|
||||
|
||||
public static Mdl2Icon Cancel(string color = null) => new Mdl2Icon("\uE711", color);
|
||||
|
||||
public static Mdl2Icon PaginationDotOutline10(string color = null) => new Mdl2Icon("\uF126", color);
|
||||
|
||||
public static Mdl2Icon PaginationDotSolid10(string color = null) => new Mdl2Icon("\uF127", color);
|
||||
|
||||
public static Mdl2Icon NewWindow(string color = null) => new Mdl2Icon("\uE78B", color);
|
||||
|
||||
public static Mdl2Icon History(string color = null) => new Mdl2Icon("\uE81C", color);
|
||||
|
||||
public static Mdl2Icon Settings(string color = null) => new Mdl2Icon("\uE713", color);
|
||||
|
||||
public static Mdl2Icon Info(string color = null) => new Mdl2Icon("\uE946", color);
|
||||
|
||||
public static Mdl2Icon FavoriteStar(string color = null) => new Mdl2Icon("\uE734", color);
|
||||
|
||||
public static Mdl2Icon Globe(string color = null) => new Mdl2Icon("\uE774", color);
|
||||
|
||||
public static Mdl2Icon Play(string color = null) => new Mdl2Icon("\uE768", color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Windows.System;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class MenuItemKeyBindingViewModel : ViewModelBase
|
||||
{
|
||||
private int _key;
|
||||
|
||||
public int Key
|
||||
{
|
||||
get => _key;
|
||||
set => Set(ref _key, value);
|
||||
}
|
||||
|
||||
private int _keyModifiers;
|
||||
|
||||
// Corresponds to https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers
|
||||
public int KeyModifiers
|
||||
{
|
||||
get => _keyModifiers;
|
||||
private set => Set(ref _keyModifiers, value);
|
||||
}
|
||||
|
||||
private bool _ctrl;
|
||||
|
||||
public bool Ctrl
|
||||
{
|
||||
get => _ctrl;
|
||||
set
|
||||
{
|
||||
if (Set(ref _ctrl, value))
|
||||
{
|
||||
SetKeyModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _alt;
|
||||
|
||||
public bool Alt
|
||||
{
|
||||
get => _alt;
|
||||
set
|
||||
{
|
||||
if (Set(ref _alt, value))
|
||||
{
|
||||
SetKeyModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _shift;
|
||||
|
||||
public bool Shift
|
||||
{
|
||||
get => _shift;
|
||||
set
|
||||
{
|
||||
if (Set(ref _shift, value))
|
||||
{
|
||||
SetKeyModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _windows;
|
||||
|
||||
public bool Windows
|
||||
{
|
||||
get => _windows;
|
||||
set
|
||||
{
|
||||
if (Set(ref _windows, value))
|
||||
{
|
||||
SetKeyModifiers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsExtendedVirtualKey => !Enum.IsDefined(typeof(VirtualKey), Key);
|
||||
|
||||
public string GetOverrideText()
|
||||
{
|
||||
var segments = new List<string>();
|
||||
|
||||
if (Ctrl) segments.Add("Ctrl");
|
||||
if (Alt) segments.Add("Alt");
|
||||
if (Windows) segments.Add("Windows");
|
||||
if (Shift) segments.Add("Shift");
|
||||
|
||||
segments.Add(EnumHelper.GetEnumDescription((ExtendedVirtualKey)Key));
|
||||
|
||||
return string.Join("+", segments);
|
||||
}
|
||||
|
||||
public MenuItemKeyBindingViewModel(int key = 0, bool ctrl = false, bool alt = false, bool shift = false,
|
||||
bool windows = false)
|
||||
{
|
||||
_key = key;
|
||||
_ctrl = ctrl;
|
||||
_alt = alt;
|
||||
_shift = shift;
|
||||
_windows = windows;
|
||||
|
||||
SetKeyModifiers();
|
||||
}
|
||||
|
||||
public MenuItemKeyBindingViewModel(KeyBinding keyBinding) : this(keyBinding.Key, keyBinding.Ctrl,
|
||||
keyBinding.Alt, keyBinding.Shift, keyBinding.Meta)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void SetKeyModifiers()
|
||||
{
|
||||
// Based on VirtualKeyModifiers enum values (https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers)
|
||||
var result = 0;
|
||||
|
||||
if (_ctrl)
|
||||
{
|
||||
result |= 1;
|
||||
}
|
||||
|
||||
if (_alt)
|
||||
{
|
||||
result |= 2;
|
||||
}
|
||||
|
||||
if (_shift)
|
||||
{
|
||||
result |= 4;
|
||||
}
|
||||
|
||||
if (_windows)
|
||||
{
|
||||
result |= 8;
|
||||
}
|
||||
|
||||
KeyModifiers = result;
|
||||
}
|
||||
|
||||
public bool EquivalentTo(MenuItemKeyBindingViewModel other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
if (other == null) return false;
|
||||
|
||||
return _key == other._key && _keyModifiers == other._keyModifiers;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class MenuItemViewModel : MenuItemViewModelBase
|
||||
{
|
||||
#region Properties
|
||||
|
||||
private MenuItemKeyBindingViewModel _keyBinding;
|
||||
|
||||
public MenuItemKeyBindingViewModel KeyBinding
|
||||
{
|
||||
get => _keyBinding;
|
||||
set => Set(ref _keyBinding, value);
|
||||
}
|
||||
|
||||
public ICommand Command { get; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructors
|
||||
|
||||
public MenuItemViewModel(string text, ICommand command, string description = null, Mdl2Icon icon = null,
|
||||
MenuItemKeyBindingViewModel keyBinding = null) : base(text, description, icon)
|
||||
{
|
||||
_keyBinding = keyBinding;
|
||||
Command = command ?? throw new ArgumentNullException(nameof(command));
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Methods
|
||||
|
||||
public override bool EquivalentTo(MenuItemViewModelBase other)
|
||||
{
|
||||
if (!base.EquivalentTo(other)) return false;
|
||||
|
||||
if (!(other is MenuItemViewModel menuItem)) return false;
|
||||
|
||||
return _keyBinding?.EquivalentTo(menuItem._keyBinding) ?? menuItem._keyBinding == null;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class MenuItemViewModelBase : ViewModelBase
|
||||
{
|
||||
#region Properties
|
||||
|
||||
private string _text;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => _text;
|
||||
set => Set(ref _text, value);
|
||||
}
|
||||
|
||||
private string _description;
|
||||
|
||||
public string Description
|
||||
{
|
||||
get => _description;
|
||||
set => Set(ref _description, value);
|
||||
}
|
||||
|
||||
private Mdl2Icon _icon;
|
||||
|
||||
public Mdl2Icon Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => Set(ref _icon, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected MenuItemViewModelBase(string text, string description, Mdl2Icon icon)
|
||||
{
|
||||
_text = text;
|
||||
_description = description;
|
||||
_icon = icon;
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
public virtual bool EquivalentTo(MenuItemViewModelBase other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
if (other == null) return false;
|
||||
|
||||
return _text.NullableEqualTo(other._text) && _description.NullableEqualTo(other._description) &&
|
||||
(_icon?.Equals(other._icon) ?? other._icon == null);
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class MenuViewModel : ViewModelBase
|
||||
{
|
||||
public ObservableCollection<MenuItemViewModelBase> Items { get; }
|
||||
|
||||
public MenuViewModel(IEnumerable<MenuItemViewModelBase> items = null)
|
||||
{
|
||||
if (items == null)
|
||||
{
|
||||
Items = new ObservableCollection<MenuItemViewModelBase>();
|
||||
}
|
||||
else if (items is ObservableCollection<MenuItemViewModelBase> observableItems)
|
||||
{
|
||||
Items = observableItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
Items = new ObservableCollection<MenuItemViewModelBase>(items);
|
||||
}
|
||||
}
|
||||
|
||||
public bool EquivalentTo(MenuViewModel other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
if (other == null) return false;
|
||||
|
||||
if (Items.Count != other.Items.Count) return false;
|
||||
|
||||
return !Items.Where((t, i) => !t.EquivalentTo(other.Items[i])).Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class RadioMenuItemViewModel : MenuItemViewModelBase
|
||||
{
|
||||
public object BindingSource { get; }
|
||||
public string BindingPath { get; }
|
||||
public string GroupName { get; }
|
||||
|
||||
public RadioMenuItemViewModel(string text, string groupName, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null)
|
||||
: base(text, description, icon)
|
||||
{
|
||||
GroupName = groupName ?? throw new ArgumentNullException(nameof(groupName));
|
||||
BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource));
|
||||
BindingPath = bindingPath;
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(MenuItemViewModelBase other)
|
||||
{
|
||||
if (!base.EquivalentTo(other)) return false;
|
||||
|
||||
if (!(other is RadioMenuItemViewModel radioMenuItem)) return false;
|
||||
|
||||
return GroupName == radioMenuItem.GroupName && BindingSource == radioMenuItem.BindingSource && BindingPath == radioMenuItem.BindingPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class SeparatorMenuItemViewModel : MenuItemViewModelBase
|
||||
{
|
||||
public SeparatorMenuItemViewModel()
|
||||
: base(string.Empty, string.Empty, null)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(MenuItemViewModelBase other)
|
||||
{
|
||||
return other is SeparatorMenuItemViewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class ToggleMenuItemViewModel : MenuItemViewModelBase
|
||||
{
|
||||
public object BindingSource { get; }
|
||||
public string BindingPath { get; }
|
||||
|
||||
public ToggleMenuItemViewModel(string text, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null)
|
||||
: base(text, description, icon)
|
||||
{
|
||||
BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource));
|
||||
BindingPath = bindingPath;
|
||||
}
|
||||
|
||||
public override bool EquivalentTo(MenuItemViewModelBase other)
|
||||
{
|
||||
if (!base.EquivalentTo(other)) return false;
|
||||
|
||||
if (!(other is ToggleMenuItemViewModel toggleMenuItem)) return false;
|
||||
|
||||
return BindingSource == toggleMenuItem.BindingSource && BindingPath == toggleMenuItem.BindingPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
using Windows.UI.Xaml;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class OverlayViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDispatcherTimer _overlayTimer;
|
||||
private readonly DispatcherTimer _overlayTimer;
|
||||
private bool _showOverlay;
|
||||
private string _overlayContent;
|
||||
|
||||
public OverlayViewModel(IDispatcherTimer dispatcherTimer)
|
||||
// Important! The constructor has to be called from the UI thread.
|
||||
public OverlayViewModel()
|
||||
{
|
||||
_overlayTimer = dispatcherTimer;
|
||||
_overlayTimer.Interval = new TimeSpan(0, 0, 2);
|
||||
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
|
||||
_overlayTimer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 2)};
|
||||
}
|
||||
|
||||
public bool ShowOverlay
|
||||
@@ -37,13 +36,16 @@ namespace FluentTerminal.App.ViewModels
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+76
-37
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
@@ -14,10 +15,9 @@ using GalaSoft.MvvmLight.Command;
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all shell profile view models. Implements logic for saving, editing, resetting, etc.
|
||||
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class ShellProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
|
||||
public abstract class ProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
protected readonly ISettingsService SettingsService;
|
||||
|
||||
private List<KeyBinding> _bindingsBeforeEdit;
|
||||
private Dictionary<string, string> _environmentVariablesBeforeEdit;
|
||||
private string _nameBeforeEdit;
|
||||
|
||||
#endregion Fields
|
||||
@@ -53,12 +54,15 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public KeyBindingsViewModel KeyBindings { get; }
|
||||
|
||||
public ObservableCollection<EnvironmentVariableViewModel> EnvironmentVariables { get; } = new ObservableCollection<EnvironmentVariableViewModel>();
|
||||
|
||||
public T ProfileVm { get; protected set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Events
|
||||
|
||||
// Needs to be triggered from UI thread
|
||||
public event EventHandler Deleted;
|
||||
|
||||
#endregion Events
|
||||
@@ -79,7 +83,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected ShellProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
|
||||
protected ProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
DialogService = dialogService;
|
||||
@@ -87,11 +91,20 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
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);
|
||||
DeleteCommand = new AsyncCommand(DeleteAsync, CanDelete);
|
||||
EditCommand = new RelayCommand(Edit);
|
||||
CancelEditCommand = new AsyncCommand(CancelEdit);
|
||||
CancelEditCommand = new AsyncCommand(CancelEditAsync);
|
||||
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
|
||||
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
|
||||
}
|
||||
@@ -100,6 +113,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
#region Methods
|
||||
|
||||
// Requires UI thread
|
||||
// Loads view model properties from the input shellProfile
|
||||
protected void Initialize(ShellProfile shellProfile)
|
||||
{
|
||||
@@ -115,14 +129,17 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
protected virtual bool HasChanges()
|
||||
{
|
||||
return ProfileVm.HasChanges() || !_nameBeforeEdit.NullableEqualTo(_name) ||
|
||||
!(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ??
|
||||
false);
|
||||
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()
|
||||
// Requires UI thread
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
// ConfigureAwait(true) because we need to trigger Deleted event from the calling (UI) thread.
|
||||
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
@@ -141,50 +158,71 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
|
||||
_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()
|
||||
// Requires UI thread
|
||||
private async Task CancelEditAsync()
|
||||
{
|
||||
if (IsNew)
|
||||
{
|
||||
await Delete();
|
||||
await DeleteAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
if (!HasChanges())
|
||||
{
|
||||
if (HasChanges())
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true);
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
// Cancelled, so rollback
|
||||
ProfileVm.RejectChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards
|
||||
if (await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true) == DialogButton.OK)
|
||||
{
|
||||
// Cancelled, so rollback
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards
|
||||
await ProfileVm.RejectChangesAsync().ConfigureAwait(true);
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task SaveChangesAsync()
|
||||
// Requires UI thread
|
||||
private async Task SaveChangesAsync()
|
||||
{
|
||||
var error = await ProfileVm.AcceptChangesAsync();
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var error = await ProfileVm.AcceptChangesAsync().ConfigureAwait(true);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), error, DialogButton.OK);
|
||||
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"), error, DialogButton.OK)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnvironmentVariables.Select(x => x.Name).Any(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
|
||||
I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
|
||||
I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -194,6 +232,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
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)
|
||||
{
|
||||
@@ -209,9 +248,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
IsNew = false;
|
||||
}
|
||||
|
||||
public Task AddKeyboardShortcut()
|
||||
private Task AddKeyboardShortcut()
|
||||
{
|
||||
return KeyBindings.ShowAddKeyBindingDialog();
|
||||
return KeyBindings.ShowAddKeyBindingDialogAsync();
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
public class ExecutedCommand
|
||||
{
|
||||
public string Command { get; set; }
|
||||
|
||||
public string Args { get; set; }
|
||||
|
||||
public DateTime LastExecution { get; set; }
|
||||
|
||||
public int ExecutionCount { get; set; }
|
||||
}
|
||||
|
||||
public class ExecutedCommandHistory
|
||||
{
|
||||
public List<ExecutedCommand> ExecutedCommands { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
|
||||
original)
|
||||
true, original)
|
||||
{
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
@@ -88,9 +88,9 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected override void CopyToProfile(ShellProfile profile)
|
||||
protected override async Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
base.CopyToProfile(profile);
|
||||
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
|
||||
|
||||
profile.Location = _location;
|
||||
profile.Arguments = _arguments;
|
||||
@@ -99,7 +99,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
public override async Task<string> ValidateAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
var error = await base.ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
@@ -123,18 +123,22 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
!Model.Arguments.NullableEqualTo(_arguments);
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task BrowseForCustomShell()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var file = await _fileSystemService.OpenFileAsync(new[] { ".exe" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
Location = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task BrowseForWorkingDirectory()
|
||||
{
|
||||
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var directory = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
|
||||
if (directory != null)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
|
||||
@@ -4,9 +4,10 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Windows.UI.Core;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Messages;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
@@ -17,17 +18,11 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
/// </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
|
||||
|
||||
@@ -42,7 +37,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (ReferenceEquals(_model, value))
|
||||
@@ -50,7 +45,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
return;
|
||||
}
|
||||
|
||||
if (_model != null && _model.GetType() != value.GetType())
|
||||
if (_strictProfileType && _model != null && _model.GetType() != value.GetType())
|
||||
{
|
||||
throw new ArgumentException($"The value has to be of type {_model.GetType().Name}.");
|
||||
}
|
||||
@@ -60,21 +55,11 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
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
|
||||
@@ -126,13 +111,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
else if (!TerminalThemes.Contains(theme))
|
||||
{
|
||||
@@ -169,15 +148,24 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
set => Set(ref _useConPty, value);
|
||||
}
|
||||
|
||||
private bool _useBuffer;
|
||||
|
||||
public bool UseBuffer
|
||||
{
|
||||
get => _useBuffer;
|
||||
set => Set(ref _useBuffer, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected ProfileProviderViewModelBase(ISettingsService settingsService, IApplicationView applicationView,
|
||||
ShellProfile original = null)
|
||||
bool strictProfileType, ShellProfile original = null)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
ApplicationView = applicationView;
|
||||
_strictProfileType = strictProfileType;
|
||||
|
||||
_model = original ?? new ShellProfile();
|
||||
|
||||
@@ -187,12 +175,12 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
TerminalThemes = new ObservableCollection<TerminalTheme>(settingsService.GetThemes());
|
||||
|
||||
TerminalThemes.Insert(0, new TerminalTheme { Id = Guid.Empty, Name = "Default" });
|
||||
TerminalThemes.Insert(0, new TerminalTheme {Id = Guid.Empty, Name = "Default"});
|
||||
|
||||
SelectedTerminalTheme = TerminalThemes.First();
|
||||
|
||||
settingsService.ThemeAdded += OnThemeAdded;
|
||||
settingsService.ThemeDeleted += OnThemeDeleted;
|
||||
MessengerInstance.Register<ThemeAddedMessage>(this, OnThemeAdded);
|
||||
MessengerInstance.Register<ThemeDeletedMessage>(this, OnThemeDeleted);
|
||||
|
||||
Initialize(Model);
|
||||
}
|
||||
@@ -201,35 +189,35 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
#region Methods
|
||||
|
||||
private void OnThemeDeleted(object sender, Guid e)
|
||||
private void OnThemeDeleted(ThemeDeletedMessage message)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() =>
|
||||
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(message.ThemeId));
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(e));
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
TerminalThemes.Remove(theme);
|
||||
|
||||
if (_terminalThemeId.Equals(e))
|
||||
if (_terminalThemeId.Equals(message.ThemeId))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.First();
|
||||
}
|
||||
}, false);
|
||||
}, CoreDispatcherPriority.Low, true);
|
||||
}
|
||||
|
||||
private void OnThemeAdded(object sender, TerminalTheme e)
|
||||
private void OnThemeAdded(ThemeAddedMessage message)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => TerminalThemes.Add(e), false);
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => TerminalThemes.Add(message.Theme), CoreDispatcherPriority.Low);
|
||||
}
|
||||
|
||||
private void Initialize(ShellProfile profile)
|
||||
{
|
||||
LineEndingTranslation = profile.LineEndingTranslation;
|
||||
UseConPty = profile.UseConPty;
|
||||
UseBuffer = profile.UseBuffer;
|
||||
TerminalThemeId = profile.TerminalThemeId;
|
||||
TabThemeId = profile.TabThemeId;
|
||||
}
|
||||
@@ -239,12 +227,13 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected virtual void CopyToProfile(ShellProfile profile)
|
||||
protected virtual Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
profile.LineEndingTranslation = _lineEndingTranslation;
|
||||
profile.UseConPty = _useConPty;
|
||||
profile.UseBuffer = _useBuffer;
|
||||
profile.TerminalThemeId = _terminalThemeId;
|
||||
profile.TabThemeId = _tabThemeId;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task<string> ValidateAsync()
|
||||
@@ -258,8 +247,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
/// </summary>
|
||||
public virtual bool HasChanges()
|
||||
{
|
||||
return Model.LineEndingTranslation != _lineEndingTranslation ||
|
||||
Model.UseConPty != _useConPty ||
|
||||
return Model.UseConPty != _useConPty ||
|
||||
Model.UseBuffer != _useBuffer ||
|
||||
!Model.TerminalThemeId.Equals(_terminalThemeId) ||
|
||||
Model.TabThemeId != _tabThemeId;
|
||||
}
|
||||
@@ -275,19 +264,19 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
/// message returned by <see cref="ValidateAsync"/> method.</returns>
|
||||
public async Task<string> AcceptChangesAsync(bool acceptIfInvalid = false)
|
||||
{
|
||||
var error = await ValidateAsync();
|
||||
var error = await ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (acceptIfInvalid || string.IsNullOrEmpty(error))
|
||||
{
|
||||
CopyToProfile(Model);
|
||||
await CopyToProfileAsync(Model).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public void RejectChanges()
|
||||
public Task RejectChangesAsync()
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => LoadFromProfile(Model), false);
|
||||
return ApplicationView.ExecuteOnUiThreadAsync(() => LoadFromProfile(Model));
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
@@ -295,7 +284,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
#region Links/shortcuts related
|
||||
|
||||
private const string UseConPtyQueryStringName = "conpty";
|
||||
private const string LineEndingQueryStringName = "lineending";
|
||||
private const string UseBufferQueryStringName = "buffer";
|
||||
private const string TerminalThemeIdQueryStringName = "theme";
|
||||
private const string TabThemeIdQueryStringName = "tab";
|
||||
|
||||
@@ -335,8 +324,7 @@ URL={0}
|
||||
|
||||
public string GetBaseQueryString()
|
||||
{
|
||||
var queryString =
|
||||
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
|
||||
var queryString = $"{UseConPtyQueryStringName}={_useConPty}&{UseBufferQueryStringName}={_useBuffer}";
|
||||
|
||||
if (_tabThemeId != TabThemes.First().Id)
|
||||
{
|
||||
@@ -367,11 +355,11 @@ URL={0}
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
LineEndingQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
UseBufferQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && Enum.TryParse(keyValue.Item2, true, out LineEndingStyle lineEndingTranslation))
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useBuffer))
|
||||
{
|
||||
LineEndingTranslation = lineEndingTranslation;
|
||||
UseConPty = useBuffer;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -21,11 +20,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
#region Static
|
||||
|
||||
private static readonly Regex CommandValidationRx = new Regex(@"^(?<cmd>[^\s\.]+)(\.exe)?(\s+(?<args>\S.*))?$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly string[] AcceptableCommands = { Constants.SshCommandName };
|
||||
|
||||
public static string GetErrorString(SshConnectionInfoValidationResult result, string separator = "; ") =>
|
||||
string.Join(separator, GetErrors(result));
|
||||
|
||||
@@ -52,9 +46,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly IApplicationDataContainer _historyContainer;
|
||||
|
||||
private bool _commandInputOriginal;
|
||||
|
||||
// To prevent validating the existence of the same file multiple times because it's kinda expensive
|
||||
private string _validatedIdentityFile;
|
||||
@@ -63,34 +54,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
#region Properties
|
||||
|
||||
private bool _commandInput;
|
||||
|
||||
public bool CommandInput
|
||||
{
|
||||
get => _commandInput;
|
||||
set
|
||||
{
|
||||
if (Set(ref _commandInput, value) && _useMosh)
|
||||
{
|
||||
RaisePropertyChanged(nameof(MoshVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Quick SSH properties
|
||||
|
||||
private string _command;
|
||||
|
||||
public string Command
|
||||
{
|
||||
get => _command;
|
||||
set => Set(ref _command, value);
|
||||
}
|
||||
|
||||
#endregion Quick SSH properties
|
||||
|
||||
#region Full UI properties
|
||||
|
||||
private string _host;
|
||||
|
||||
public string Host
|
||||
@@ -128,13 +91,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
public bool UseMosh
|
||||
{
|
||||
get => _useMosh;
|
||||
set
|
||||
{
|
||||
if (Set(ref _useMosh, value) && !_commandInput)
|
||||
{
|
||||
RaisePropertyChanged(nameof(MoshVisible));
|
||||
}
|
||||
}
|
||||
set => Set(ref _useMosh, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortFrom;
|
||||
@@ -153,12 +110,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
set => Set(ref _moshPortTo, value);
|
||||
}
|
||||
|
||||
#endregion Full UI properties
|
||||
|
||||
public bool MoshVisible => _useMosh && !_commandInput;
|
||||
|
||||
public ObservableCollection<string> CommandHistory { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
@@ -167,66 +118,30 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructors
|
||||
#region Constructor
|
||||
|
||||
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
|
||||
IApplicationDataContainer historyContainer, SshProfile original = null) : base(settingsService, applicationView,
|
||||
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
|
||||
SshProfile original = null) : base(settingsService, applicationView, true,
|
||||
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault,
|
||||
RequestConPty = settingsService.GetApplicationSettings().UseConPty
|
||||
})
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_historyContainer = historyContainer;
|
||||
|
||||
if (string.IsNullOrEmpty(Model.Location))
|
||||
{
|
||||
_commandInput = settingsService.GetApplicationSettings().UseQuickSshConnectByDefault;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(((SshProfile)Model).Host))
|
||||
{
|
||||
_commandInput = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_commandInput = false;
|
||||
}
|
||||
|
||||
_commandInputOriginal = _commandInput;
|
||||
|
||||
FillCommandHistory();
|
||||
|
||||
Initialize((SshProfile)Model);
|
||||
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFileAsync);
|
||||
}
|
||||
|
||||
private SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
|
||||
IApplicationDataContainer historyContainer, bool useCommandInput) : base(settingsService, applicationView,
|
||||
new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_historyContainer = historyContainer;
|
||||
_commandInput = useCommandInput;
|
||||
_commandInputOriginal = useCommandInput;
|
||||
|
||||
FillCommandHistory();
|
||||
|
||||
Initialize((SshProfile)Model);
|
||||
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
// Fills the view model properties from the input sshProfile
|
||||
private void Initialize(SshProfile sshProfile)
|
||||
{
|
||||
Command = string.IsNullOrEmpty(sshProfile.Location) ? string.Empty : $"{sshProfile.Location} {sshProfile.Arguments}";
|
||||
|
||||
Host = sshProfile.Host;
|
||||
SshPort = sshProfile.SshPort;
|
||||
|
||||
@@ -234,13 +149,13 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
if (string.IsNullOrEmpty(Username))
|
||||
{
|
||||
_trayProcessCommunicationService.GetUserName().ContinueWith(t =>
|
||||
_trayProcessCommunicationService.GetUserNameAsync().ContinueWith(t =>
|
||||
{
|
||||
var username = t.Result;
|
||||
|
||||
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => Username = username, false);
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => Username = username);
|
||||
}
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
@@ -251,9 +166,11 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
MoshPortTo = sshProfile.MoshPortTo;
|
||||
}
|
||||
|
||||
private async Task BrowseForIdentityFile()
|
||||
// Requires UI thread
|
||||
private async Task BrowseForIdentityFileAsync()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { "*" });
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var file = await _fileSystemService.OpenFileAsync(new[] { "*" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
IdentityFile = file.Path;
|
||||
@@ -262,49 +179,43 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
private string GetArgumentsString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
var sshArguments = GetSshArguments();
|
||||
|
||||
if (_sshPort != SshProfile.DefaultSshPort)
|
||||
sb.Append($"-p {_sshPort:#####} ");
|
||||
if (!_useMosh)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(sshArguments) ? $"{_username}@{_host}" : $"{sshArguments} {_username}@{_host}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
sb.Append($"-i \"{_identityFile}\" ");
|
||||
return string.IsNullOrEmpty(sshArguments)
|
||||
? $"-p {_moshPortFrom}:{_moshPortTo} {_username}@{_host}"
|
||||
: $"-p {_moshPortFrom}:{_moshPortTo} --ssh=\"ssh {sshArguments}\" {_username}@{_host}";
|
||||
}
|
||||
|
||||
sb.Append($"{_username}@{_host}");
|
||||
private string GetSshArguments()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
return _sshPort == SshProfile.DefaultSshPort ? null : $"-p {_sshPort:#####}";
|
||||
}
|
||||
|
||||
if (_useMosh)
|
||||
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
|
||||
|
||||
return sb.ToString();
|
||||
return _sshPort == SshProfile.DefaultSshPort
|
||||
? $"-i \"{_identityFile}\""
|
||||
: $"-p {_sshPort:#####} -i \"{_identityFile}\"";
|
||||
}
|
||||
|
||||
protected override void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
base.LoadFromProfile(profile);
|
||||
|
||||
CommandInput = _commandInputOriginal;
|
||||
|
||||
Initialize((SshProfile)profile);
|
||||
}
|
||||
|
||||
protected override void CopyToProfile(ShellProfile profile)
|
||||
protected override async Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
base.CopyToProfile(profile);
|
||||
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
|
||||
|
||||
_commandInputOriginal = _commandInput;
|
||||
var sshProfile = (SshProfile) profile;
|
||||
|
||||
if (_commandInput)
|
||||
{
|
||||
CopyToProfileQuick((SshProfile)profile);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyToProfileFull((SshProfile)profile);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyToProfileFull(SshProfile sshProfile)
|
||||
{
|
||||
sshProfile.Location = _useMosh ? Constants.MoshCommandName : Constants.SshCommandName;
|
||||
sshProfile.Arguments = GetArgumentsString();
|
||||
|
||||
@@ -319,55 +230,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
sshProfile.MoshPortTo = _moshPortTo;
|
||||
}
|
||||
|
||||
private void CopyToProfileQuick(SshProfile sshProfile)
|
||||
public override async Task<string> ValidateAsync()
|
||||
{
|
||||
var command = _command?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
sshProfile.Location = null;
|
||||
sshProfile.Arguments = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var match = CommandValidationRx.Match(command);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
// Should not happen ever because this method gets called only if validation succeeds.
|
||||
throw new Exception("Invalid command.");
|
||||
}
|
||||
|
||||
sshProfile.Location = match.Groups["cmd"].Value;
|
||||
sshProfile.Arguments = match.Groups["args"].Success ? match.Groups["args"].Value.Trim() : null;
|
||||
|
||||
sshProfile.WorkingDirectory = null;
|
||||
|
||||
sshProfile.Host = null;
|
||||
sshProfile.SshPort = 0;
|
||||
sshProfile.Username = null;
|
||||
sshProfile.IdentityFile = null;
|
||||
sshProfile.UseMosh = false;
|
||||
sshProfile.MoshPortFrom = 0;
|
||||
sshProfile.MoshPortTo = 0;
|
||||
}
|
||||
|
||||
public override Task<string> ValidateAsync()
|
||||
{
|
||||
return _commandInput ? ValidateQuickAsync() : ValidateFullAsync();
|
||||
}
|
||||
|
||||
private async Task<string> ValidateFullAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
var error = await base.ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync();
|
||||
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
|
||||
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
@@ -384,56 +256,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
return error;
|
||||
}
|
||||
|
||||
private async Task<string> ValidateQuickAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
var match = CommandValidationRx.Match(_command?.Trim() ?? string.Empty);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
error = I18N.Translate("InvalidCommand");
|
||||
|
||||
return string.IsNullOrEmpty(error) ? "Invalid command." : error;
|
||||
}
|
||||
|
||||
if (!AcceptableCommands.Any(c => c.Equals(match.Groups["cmd"].Value, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
error = I18N.Translate("UnsupportedCommand");
|
||||
|
||||
return string.IsNullOrEmpty(error)
|
||||
? $"Unsupported command: {match.Groups["cmd"]}."
|
||||
: $"{error} {match.Groups["cmd"]}";
|
||||
}
|
||||
|
||||
if (!match.Groups["args"].Success)
|
||||
{
|
||||
error = I18N.Translate("CommandArgumentsMandatory");
|
||||
|
||||
return string.IsNullOrEmpty(error) ? "Command arguments are missing." : error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override bool HasChanges()
|
||||
{
|
||||
// ReSharper disable once ArrangeRedundantParentheses
|
||||
if (base.HasChanges() || (_commandInput != _commandInputOriginal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_commandInput)
|
||||
{
|
||||
return !_command.NullableEqualTo($"{Model.Location} {Model.Arguments}");
|
||||
}
|
||||
|
||||
var original = (SshProfile)Model;
|
||||
|
||||
return base.HasChanges() || !original.Host.NullableEqualTo(_host) || original.SshPort != _sshPort ||
|
||||
@@ -442,7 +266,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
original.MoshPortFrom != _moshPortFrom || original.MoshPortTo != _moshPortTo;
|
||||
}
|
||||
|
||||
public async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
|
||||
private async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
|
||||
{
|
||||
var result = SshConnectionInfoValidationResult.Valid;
|
||||
|
||||
@@ -461,7 +285,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (!await CheckIdentityFileExistsAsync())
|
||||
if (!await CheckIdentityFileExistsAsync().ConfigureAwait(false))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
|
||||
}
|
||||
@@ -484,6 +308,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
return result;
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task<bool> CheckIdentityFileExistsAsync()
|
||||
{
|
||||
var identityFile = _identityFile;
|
||||
@@ -503,7 +328,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
}
|
||||
else
|
||||
{
|
||||
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync().ConfigureAwait(true);
|
||||
|
||||
if (string.IsNullOrEmpty(sshConfigDir))
|
||||
{
|
||||
@@ -513,7 +339,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
fullPath = Path.Combine(sshConfigDir, identityFile);
|
||||
}
|
||||
|
||||
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath).ConfigureAwait(true))
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
|
||||
@@ -533,63 +360,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Command history
|
||||
|
||||
private const string ExecutedCommandsKey = "ExecutedCommands";
|
||||
private const int CommandHistoryLimit = 20;
|
||||
|
||||
private void FillCommandHistory()
|
||||
{
|
||||
var commands = _historyContainer.ReadValueFromJson<ExecutedCommandHistory>(ExecutedCommandsKey, null)
|
||||
?.ExecutedCommands;
|
||||
|
||||
CommandHistory = commands == null
|
||||
? new ObservableCollection<string>()
|
||||
: new ObservableCollection<string>(commands.Select(c => $"{c.Command} {c.Args}"));
|
||||
}
|
||||
|
||||
public void SaveCommand(string cmd, string args)
|
||||
{
|
||||
cmd = cmd?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(cmd))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cmd));
|
||||
}
|
||||
|
||||
var commandHistory =
|
||||
_historyContainer.ReadValueFromJson<ExecutedCommandHistory>(ExecutedCommandsKey, null) ??
|
||||
new ExecutedCommandHistory { ExecutedCommands = new List<ExecutedCommand>() };
|
||||
|
||||
var command = commandHistory.ExecutedCommands.FirstOrDefault(c =>
|
||||
string.Equals(cmd, c.Command, StringComparison.Ordinal) && args.NullableEqualTo(c.Args));
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
command = new ExecutedCommand
|
||||
{ Command = cmd, Args = args, ExecutionCount = 1, LastExecution = DateTime.UtcNow };
|
||||
}
|
||||
else
|
||||
{
|
||||
command.ExecutionCount++;
|
||||
|
||||
command.LastExecution = DateTime.UtcNow;
|
||||
|
||||
commandHistory.ExecutedCommands.Remove(command);
|
||||
}
|
||||
|
||||
commandHistory.ExecutedCommands.Insert(0, command);
|
||||
|
||||
while (commandHistory.ExecutedCommands.Count > CommandHistoryLimit)
|
||||
{
|
||||
commandHistory.ExecutedCommands.RemoveAt(CommandHistoryLimit);
|
||||
}
|
||||
|
||||
_historyContainer.WriteValueAsJson(ExecutedCommandsKey, commandHistory);
|
||||
}
|
||||
|
||||
#endregion Command history
|
||||
|
||||
#region Links/shortcuts related
|
||||
|
||||
// Resources:
|
||||
@@ -599,11 +369,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
private const string SshUriScheme = "ssh";
|
||||
private const string MoshUriScheme = "mosh";
|
||||
|
||||
private const string CustomSshUriScheme = "sshft";
|
||||
private const string CustomUriHost = "fluent.terminal";
|
||||
private const string CustomCommandQueryStringName = "cmd";
|
||||
private const string CustomArgumentsQueryStringName = "args";
|
||||
|
||||
// Constant derived from https://man.openbsd.org/ssh
|
||||
private const string IdentityFileOptionName = "IdentityFile";
|
||||
|
||||
@@ -612,41 +377,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
private static readonly Regex MoshRangeRx =
|
||||
new Regex(@"^(?<from>\d{1,5})[:-](?<to>\d{1,5})$", RegexOptions.Compiled);
|
||||
|
||||
public static bool CheckScheme(Uri uri) => CheckSchemeStandard(uri) || CheckSchemeCustom(uri);
|
||||
|
||||
private static bool CheckSchemeStandard(Uri uri) =>
|
||||
public static bool CheckScheme(Uri uri) =>
|
||||
SshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase) ||
|
||||
MoshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool CheckSchemeCustom(Uri uri) =>
|
||||
CustomSshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static SshConnectViewModel ParseUri(Uri uri, ISettingsService settingsService,
|
||||
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
|
||||
{
|
||||
if (CheckSchemeStandard(uri))
|
||||
{
|
||||
return ParseUriStandard(uri, settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer);
|
||||
}
|
||||
|
||||
if (CheckSchemeCustom(uri))
|
||||
{
|
||||
return ParseUriCustom(uri, settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer);
|
||||
}
|
||||
|
||||
// Won't happen ever
|
||||
throw new Exception($"Unsupported URI scheme: {uri.Scheme}");
|
||||
}
|
||||
|
||||
private static SshConnectViewModel ParseUriStandard(Uri uri, ISettingsService settingsService,
|
||||
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
|
||||
{
|
||||
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer, false)
|
||||
fileSystemService)
|
||||
{
|
||||
Host = uri.Host,
|
||||
UseMosh = MoshUriScheme.Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase)
|
||||
@@ -708,58 +448,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static SshConnectViewModel ParseUriCustom(Uri uri, ISettingsService settingsService,
|
||||
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
|
||||
public override async Task<Tuple<bool, string>> GetUrlAsync()
|
||||
{
|
||||
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer, true);
|
||||
|
||||
// ReSharper disable once ConstantConditionalAccessQualifier
|
||||
string queryString = uri.Query?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(queryString))
|
||||
{
|
||||
return vm;
|
||||
}
|
||||
|
||||
if (queryString.StartsWith("?", StringComparison.Ordinal))
|
||||
{
|
||||
queryString = queryString.Substring(1);
|
||||
}
|
||||
|
||||
var queryStringParams = ParseParams(queryString, '&').ToList();
|
||||
|
||||
var cmdParam = queryStringParams.FirstOrDefault(t =>
|
||||
CustomCommandQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var argsParam = queryStringParams.FirstOrDefault(t =>
|
||||
CustomArgumentsQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
vm._command =
|
||||
$"{(string.IsNullOrEmpty(cmdParam?.Item2) ? Constants.SshCommandName : cmdParam.Item2)} {argsParam?.Item2}"
|
||||
.Trim();
|
||||
|
||||
vm.LoadBaseFromQueryString(queryStringParams);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
public override Task<Tuple<bool, string>> GetUrlAsync()
|
||||
{
|
||||
return _commandInput ? GetUrlQuickAsync() : GetUrlFullAsync();
|
||||
}
|
||||
|
||||
private async Task<Tuple<bool, string>> GetUrlFullAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
var error = await base.ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync();
|
||||
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
|
||||
|
||||
if (result != SshConnectionInfoValidationResult.Valid &&
|
||||
// For links we can ignore missing username
|
||||
@@ -776,7 +474,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
|
||||
var sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
|
||||
|
||||
sb.Append("://");
|
||||
|
||||
@@ -833,28 +531,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
|
||||
return Tuple.Create(true, sb.ToString());
|
||||
}
|
||||
|
||||
private async Task<Tuple<bool, string>> GetUrlQuickAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
var match = CommandValidationRx.Match(_command);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
error = I18N.Translate("InvalidCommand");
|
||||
|
||||
return Tuple.Create(false, string.IsNullOrEmpty(error) ? "Invalid command." : error);
|
||||
}
|
||||
|
||||
return Tuple.Create(true,
|
||||
$"{CustomSshUriScheme}://{CustomUriHost}/?{CustomCommandQueryStringName}={Constants.SshCommandName}&{CustomArgumentsQueryStringName}={HttpUtility.UrlEncode(match.Groups["args"].Value.Trim())}&{GetBaseQueryString()}");
|
||||
}
|
||||
|
||||
#endregion Links/shortcuts related
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
@@ -9,18 +8,16 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public class AboutPageViewModel : ViewModelBase
|
||||
{
|
||||
private const string BaseUrl = "https://github.com/felixse/FluentTerminal/releases/tag/";
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IUpdateService _updateService;
|
||||
private string _latestVersion;
|
||||
private readonly IApplicationView _applicationView;
|
||||
|
||||
public AboutPageViewModel(ISettingsService settingsService, IUpdateService updateService, IApplicationView applicationView)
|
||||
public AboutPageViewModel(IUpdateService updateService, IApplicationView applicationView)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_updateService = updateService;
|
||||
_applicationView = applicationView;
|
||||
|
||||
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdate(true));
|
||||
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdateAsync(true));
|
||||
}
|
||||
|
||||
public IAsyncCommand CheckForUpdatesCommand { get; }
|
||||
@@ -30,7 +27,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get
|
||||
{
|
||||
var version = _updateService.GetCurrentVersion();
|
||||
return ConvertVersionToString(version);
|
||||
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,19 +58,16 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
public Task OnNavigatedTo()
|
||||
{
|
||||
return CheckForUpdate(false);
|
||||
return CheckForUpdateAsync(false);
|
||||
}
|
||||
|
||||
public async Task CheckForUpdate(bool notifyNoUpdate)
|
||||
private async Task CheckForUpdateAsync(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);
|
||||
var version = await _updateService.GetLatestVersionAsync().ConfigureAwait(false);
|
||||
await _applicationView.ExecuteOnUiThreadAsync(() =>
|
||||
LatestVersion = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}")
|
||||
.ConfigureAwait(false);
|
||||
await _updateService.CheckForUpdateAsync(notifyNoUpdate).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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;
|
||||
|
||||
@@ -23,9 +23,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
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, IApplicationLanguageService applicationLanguageService, ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
@@ -33,10 +35,12 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_startupTaskService = startupTaskService;
|
||||
_applicationLanguageService = applicationLanguageService;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
|
||||
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectoryAsync);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
|
||||
@@ -57,26 +61,14 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnNavigatedTo()
|
||||
// Requires UI thread
|
||||
public async Task OnNavigatedToAsync()
|
||||
{
|
||||
var startupTaskStatus = await _startupTaskService.GetStatus();
|
||||
// ConfigureAwait(true) because we want to execute SetStartupTaskPropertiesForStatus from the calling (UI) thread.
|
||||
var startupTaskStatus = await _startupTaskService.GetStatusAsync().ConfigureAwait(true);
|
||||
SetStartupTaskPropertiesForStatus(startupTaskStatus);
|
||||
}
|
||||
|
||||
public bool AlwaysShowTabs
|
||||
{
|
||||
get => _applicationSettings.AlwaysShowTabs;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.AlwaysShowTabs != value)
|
||||
{
|
||||
_applicationSettings.AlwaysShowTabs = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowCustomTitleInTitlebar
|
||||
{
|
||||
get => _applicationSettings.ShowCustomTitleInTitlebar;
|
||||
@@ -156,20 +148,6 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseQuickSshConnectByDefault
|
||||
{
|
||||
get => _applicationSettings.UseQuickSshConnectByDefault;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.UseQuickSshConnectByDefault != value)
|
||||
{
|
||||
_applicationSettings.UseQuickSshConnectByDefault = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RTrimCopiedLines
|
||||
{
|
||||
get => _applicationSettings.RTrimCopiedLines;
|
||||
@@ -195,7 +173,62 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
|
||||
_trayProcessCommunicationService.MuteTerminal(value);
|
||||
_trayProcessCommunicationService.MuteTerminalAsync(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseConPty
|
||||
{
|
||||
get => _applicationSettings.UseConPty;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.UseConPty != value)
|
||||
{
|
||||
_applicationSettings.UseConPty = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,22 +283,38 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_applicationSettings.NewTerminalLocation = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(TabIsSelected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
public bool TabWindowCascadingAppMenu
|
||||
{
|
||||
get => _applicationSettings.TabWindowCascadingAppMenu;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.TabWindowCascadingAppMenu != value)
|
||||
{
|
||||
_applicationSettings.TabWindowCascadingAppMenu = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IAsyncCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
public IAsyncCommand BrowseLogDirectoryCommand { get; }
|
||||
|
||||
public bool StartupTaskEnabled
|
||||
{
|
||||
get => _startupTaskEnabled;
|
||||
set
|
||||
{
|
||||
if (_startupTaskEnabled != value)
|
||||
if (Set(ref _startupTaskEnabled, value))
|
||||
{
|
||||
_startupTaskEnabled = value;
|
||||
RaisePropertyChanged(nameof(StartupTaskEnabled));
|
||||
SetStartupTaskState(value);
|
||||
// ReSharper disable once AssignmentIsFullyDiscarded
|
||||
_ = SetStartupTaskStateAsync(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,7 +328,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool TabIsSelected
|
||||
{
|
||||
get => NewTerminalLocation == NewTerminalLocation.Tab;
|
||||
set { if (value) NewTerminalLocation = NewTerminalLocation.Tab; }
|
||||
set => NewTerminalLocation = value ? NewTerminalLocation.Tab : NewTerminalLocation.Window;
|
||||
}
|
||||
|
||||
public TabsPosition TabsPosition
|
||||
@@ -342,15 +391,27 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool WindowIsSelected
|
||||
public bool ShowTextCopied
|
||||
{
|
||||
get => NewTerminalLocation == NewTerminalLocation.Window;
|
||||
set { if (value) NewTerminalLocation = NewTerminalLocation.Window; }
|
||||
get => _applicationSettings.ShowTextCopied;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.ShowTextCopied != value)
|
||||
{
|
||||
_applicationSettings.ShowTextCopied = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
@@ -360,7 +421,6 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
UnderlineSelectedTab = defaults.UnderlineSelectedTab;
|
||||
InactiveTabColorMode = defaults.InactiveTabColorMode;
|
||||
NewTerminalLocation = defaults.NewTerminalLocation;
|
||||
AlwaysShowTabs = defaults.AlwaysShowTabs;
|
||||
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
|
||||
EnableTrayIcon = defaults.EnableTrayIcon;
|
||||
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
|
||||
@@ -368,9 +428,15 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
|
||||
RTrimCopiedLines = defaults.RTrimCopiedLines;
|
||||
MuteTerminalBeeps = defaults.MuteTerminalBeeps;
|
||||
EnableLogging = defaults.EnableLogging;
|
||||
PrintableOutputOnly = defaults.PrintableOutputOnly;
|
||||
LogDirectoryPath = defaults.LogDirectoryPath;
|
||||
UseConPty = defaults.UseConPty;
|
||||
ShowTextCopied = defaults.ShowTextCopied;
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private void SetStartupTaskPropertiesForStatus(StartupTaskStatus startupTaskStatus)
|
||||
{
|
||||
switch (startupTaskStatus)
|
||||
@@ -398,22 +464,44 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy");
|
||||
CanEnableStartupTask = false;
|
||||
break;
|
||||
|
||||
case StartupTaskStatus.EnabledByPolicy:
|
||||
StartupTaskEnabled = true;
|
||||
StartupTaskErrorMessage = I18N.TranslateWithFallback("EnabledByPolicy", "Enabled by policy.");
|
||||
CanEnableStartupTask = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetStartupTaskState(bool enabled)
|
||||
// Requires UI thread
|
||||
private async Task SetStartupTaskStateAsync(bool enabled)
|
||||
{
|
||||
StartupTaskStatus status;
|
||||
if (enabled)
|
||||
{
|
||||
status = await _startupTaskService.EnableStartupTask();
|
||||
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
|
||||
status = await _startupTaskService.EnableStartupTaskAsync().ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _startupTaskService.DisableStartupTask();
|
||||
status = await _startupTaskService.GetStatus();
|
||||
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
|
||||
await _startupTaskService.DisableStartupTaskAsync().ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
|
||||
status = await _startupTaskService.GetStatusAsync().ConfigureAwait(true);
|
||||
}
|
||||
SetStartupTaskPropertiesForStatus(status);
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task BrowseLogDirectoryAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var folder = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
|
||||
|
||||
if (folder != null)
|
||||
{
|
||||
LogDirectoryPath = folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
Model = keyBinding;
|
||||
Parent = parent;
|
||||
_dialogService = dialogService;
|
||||
EditCommand = new AsyncCommand(Edit);
|
||||
DeleteCommand = new AsyncCommand(Delete);
|
||||
EditCommand = new AsyncCommand(EditAsync);
|
||||
DeleteCommand = new AsyncCommand(DeleteAsync);
|
||||
}
|
||||
|
||||
// Needs to be triggered from the UI thread
|
||||
public event EventHandler Deleted;
|
||||
|
||||
public event EventHandler Edited;
|
||||
@@ -97,8 +98,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> Edit()
|
||||
// Requires UI thread
|
||||
public async Task<bool> EditAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var keyBinding = await _dialogService.ShowCreateKeyBindingDialog().ConfigureAwait(true);
|
||||
|
||||
if (keyBinding != null)
|
||||
@@ -117,9 +120,12 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
// Requires UI thread
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
|
||||
@@ -14,20 +14,17 @@ 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<string, 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<string>(async command => await Add(command).ConfigureAwait(false));
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaultsAsync().ConfigureAwait(false));
|
||||
AddCommand = new RelayCommand<string>(async command => await AddAsync(command).ConfigureAwait(false));
|
||||
|
||||
Initialize(_settingsService.GetCommandKeyBindings());
|
||||
}
|
||||
@@ -36,12 +33,12 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public ObservableCollection<KeyBindingsViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingsViewModel>();
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
private async Task Add(string command)
|
||||
private Task AddAsync(string command)
|
||||
{
|
||||
var keyBinding = KeyBindings.FirstOrDefault(k => k.Command == command);
|
||||
await keyBinding?.ShowAddKeyBindingDialog();
|
||||
return KeyBindings.First(k => k.Command == command).ShowAddKeyBindingDialogAsync();
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private void ClearKeyBindings()
|
||||
{
|
||||
foreach (var keyBinding in KeyBindings)
|
||||
@@ -52,17 +49,16 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
KeyBindings.Clear();
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
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.ToString(), _dialogService, I18N.Translate($"{nameof(Command)}.{command}"), true);
|
||||
foreach (var keyBinding in _keyBindings[command.ToString()])
|
||||
foreach (var keyBinding in keyBindings[command.ToString()])
|
||||
{
|
||||
viewModel.Add(keyBinding);
|
||||
}
|
||||
@@ -74,12 +70,15 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private void OnEdited(string command, ICollection<KeyBinding> keyBindings)
|
||||
{
|
||||
_settingsService.SaveKeyBindings(command, keyBindings);
|
||||
_trayProcessCommunicationService.UpdateToggleWindowKeyBindings();
|
||||
_trayProcessCommunicationService.UpdateToggleWindowKeyBindingsAsync();
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we need to execute Initialize in the calling (UI) thread.
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
|
||||
@@ -42,21 +42,25 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
public ObservableCollection<KeyBindingViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingViewModel>();
|
||||
|
||||
public async Task ShowAddKeyBindingDialog()
|
||||
// Requires UI thread
|
||||
public async Task ShowAddKeyBindingDialogAsync()
|
||||
{
|
||||
var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService, this);
|
||||
|
||||
if (await newKeyBinding.Edit().ConfigureAwait(true))
|
||||
// ConfigureAwait(true) because we need to execute Add method in the calling (UI) thread.
|
||||
if (await newKeyBinding.EditAsync().ConfigureAwait(true))
|
||||
{
|
||||
Add(newKeyBinding.Model);
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
public void Clear()
|
||||
{
|
||||
KeyBindings.Clear();
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
public void Add(KeyBinding keyBinding)
|
||||
{
|
||||
var viewModel = new KeyBindingViewModel(keyBinding, _dialogService, this);
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaultsAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public bool CopyOnSelect
|
||||
@@ -71,6 +71,12 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
set { if (value) MouseRightClickAction = MouseAction.Paste; }
|
||||
}
|
||||
|
||||
public bool MouseRightClickCopySelectionOrPasteIsSelected
|
||||
{
|
||||
get => MouseRightClickAction == MouseAction.CopySelectionOrPaste;
|
||||
set { if (value) MouseRightClickAction = MouseAction.CopySelectionOrPaste; }
|
||||
}
|
||||
|
||||
public MouseAction MouseMiddleClickAction
|
||||
{
|
||||
get => _applicationSettings.MouseMiddleClickAction;
|
||||
@@ -103,11 +109,21 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.Paste; }
|
||||
}
|
||||
|
||||
public bool MouseMiddleClickCopySelectionOrPasteIsSelected
|
||||
{
|
||||
get => MouseMiddleClickAction == MouseAction.CopySelectionOrPaste;
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.CopySelectionOrPaste; }
|
||||
}
|
||||
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
|
||||
@@ -90,7 +90,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
Id = Guid.NewGuid(),
|
||||
PreInstalled = false,
|
||||
Name = "New profile",
|
||||
KeyBindings = new List<KeyBinding>()
|
||||
KeyBindings = new List<KeyBinding>(),
|
||||
UseConPty = _settingsService.GetApplicationSettings().UseConPty
|
||||
};
|
||||
|
||||
AddShellProfile(shellProfile);
|
||||
@@ -103,6 +104,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
cloned.Id = Guid.NewGuid();
|
||||
cloned.PreInstalled = false;
|
||||
cloned.Name = $"Copy of {shellProfile.Name}";
|
||||
cloned.KeyBindings = new List<KeyBinding>();
|
||||
|
||||
AddShellProfile(cloned);
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
private void OnSshProfileDeleted(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is SshProfileViewModel shellProfile)
|
||||
if (sender is SshProfileViewModel sshProfile)
|
||||
{
|
||||
if (SelectedSshProfile == shellProfile)
|
||||
if (SelectedSshProfile == sshProfile)
|
||||
{
|
||||
SelectedSshProfile = SshProfiles.First();
|
||||
}
|
||||
SshProfiles.Remove(shellProfile);
|
||||
_settingsService.DeleteSshProfile(shellProfile.Id);
|
||||
SshProfiles.Remove(sshProfile);
|
||||
_settingsService.DeleteSshProfile(sshProfile.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
PreInstalled = false,
|
||||
Name = "New SSH profile",
|
||||
KeyBindings = new List<KeyBinding>(),
|
||||
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault
|
||||
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault,
|
||||
RequestConPty = _settingsService.GetApplicationSettings().UseConPty
|
||||
};
|
||||
|
||||
AddSshProfile(shellProfile);
|
||||
@@ -88,6 +89,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
cloned.Id = Guid.NewGuid();
|
||||
cloned.PreInstalled = false;
|
||||
cloned.Name = $"Copy of {shellProfile.Name}";
|
||||
cloned.KeyBindings = new List<KeyBinding>();
|
||||
|
||||
foreach (KeyBinding keyBinding in cloned.KeyBindings)
|
||||
{
|
||||
|
||||
@@ -1,5 +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;
|
||||
@@ -67,6 +68,20 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseAcrylicBackground
|
||||
{
|
||||
get => _terminalOptions.UseAcrylicBackground;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.UseAcrylicBackground != value)
|
||||
{
|
||||
_terminalOptions.UseAcrylicBackground = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string FontFamily
|
||||
{
|
||||
get => _terminalOptions.FontFamily;
|
||||
@@ -81,6 +96,20 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public int FontWeight
|
||||
{
|
||||
get => _terminalOptions.FontWeight;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.FontWeight != value)
|
||||
{
|
||||
_terminalOptions.FontWeight = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double BackgroundOpacity
|
||||
{
|
||||
get => _terminalOptions.BackgroundOpacity;
|
||||
@@ -114,7 +143,6 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _terminalOptions.ScrollBackLimit.ToString();
|
||||
set
|
||||
{
|
||||
var oldValue = _terminalOptions.ScrollBackLimit;
|
||||
if (uint.TryParse(value, out uint intValue))
|
||||
{
|
||||
if (intValue == uint.MinValue)
|
||||
@@ -135,28 +163,15 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool BoldText
|
||||
public string WordSeparator
|
||||
{
|
||||
get => _terminalOptions.BoldText;
|
||||
get => WebViewSpecialCharEncoder.Decode(_terminalOptions.WordSeparator);
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.BoldText != value)
|
||||
value = WebViewSpecialCharEncoder.Encode(value);
|
||||
if (_terminalOptions.WordSeparator != value)
|
||||
{
|
||||
_terminalOptions.BoldText = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowTextCopied
|
||||
{
|
||||
get => _terminalOptions.ShowTextCopied;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.ShowTextCopied != value)
|
||||
{
|
||||
_terminalOptions.ShowTextCopied = value;
|
||||
_terminalOptions.WordSeparator = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
@@ -211,9 +226,13 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
@@ -223,10 +242,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
ScrollBarStyle = defaults.ScrollBarStyle;
|
||||
FontFamily = defaults.FontFamily;
|
||||
FontSize = defaults.FontSize;
|
||||
BoldText = defaults.BoldText;
|
||||
FontWeight = defaults.FontWeight;
|
||||
BackgroundOpacity = defaults.BackgroundOpacity;
|
||||
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
|
||||
ShowTextCopied = defaults.ShowTextCopied;
|
||||
WordSeparator = defaults.WordSeparator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,10 +255,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_dialogService = dialogService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaultsAsync().ConfigureAwait(false));
|
||||
|
||||
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
|
||||
Sizes = Enumerable.Range(1, 72);
|
||||
Sizes = Enumerable.Range(2, 72);
|
||||
|
||||
_terminalOptions = _settingsService.GetTerminalOptions();
|
||||
}
|
||||
|
||||
@@ -1,11 +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
|
||||
{
|
||||
@@ -18,30 +21,38 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private double _backgroundOpacity;
|
||||
private readonly IThemeParserFactory _themeParserFactory;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly IImageFileSystemService _imageFileSystemService;
|
||||
|
||||
public event EventHandler<string> SelectedThemeBackgroundColorChanged;
|
||||
public event EventHandler<ImageFile> SelectedThemeBackgroundImageChanged;
|
||||
public event EventHandler<ThemeViewModel> SelectedThemeChanged;
|
||||
|
||||
public ThemesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
|
||||
IThemeParserFactory themeParserFactory, IFileSystemService fileSystemService)
|
||||
public ThemesPageViewModel(ISettingsService settingsService,
|
||||
IDialogService dialogService,
|
||||
IDefaultValueProvider defaultValueProvider,
|
||||
IThemeParserFactory themeParserFactory,
|
||||
IFileSystemService fileSystemService,
|
||||
IImageFileSystemService imageFileSystemService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_themeParserFactory = themeParserFactory;
|
||||
_fileSystemService = fileSystemService;
|
||||
_imageFileSystemService = imageFileSystemService;
|
||||
|
||||
CreateThemeCommand = new RelayCommand(CreateTheme);
|
||||
ImportThemeCommand = new RelayCommand(ImportTheme);
|
||||
ImportThemeCommand = new AsyncCommand(ImportThemeAsync);
|
||||
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, false);
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, false);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
|
||||
@@ -56,7 +67,7 @@ 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
|
||||
@@ -73,11 +84,17 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
if (_selectedTheme != null)
|
||||
{
|
||||
_selectedTheme.BackgroundChanged -= OnSelectedThemeBackgroundChanged;
|
||||
_selectedTheme.BackgroundImageChanged -= OnSelectedThemeBackgroundImageChanged;
|
||||
}
|
||||
|
||||
Set(ref _selectedTheme, value);
|
||||
SelectedThemeChanged?.Invoke(this, _selectedTheme);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
_selectedTheme.BackgroundOpacity = BackgroundOpacity;
|
||||
value.BackgroundChanged += OnSelectedThemeBackgroundChanged;
|
||||
value.BackgroundImageChanged += OnSelectedThemeBackgroundImageChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,28 +127,49 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
AddTheme(theme);
|
||||
}
|
||||
|
||||
private async void ImportTheme()
|
||||
// Requires UI thread
|
||||
private async Task ImportThemeAsync()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(_themeParserFactory.SupportedFileTypes).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
|
||||
var file = await _fileSystemService.OpenFileAsync(_themeParserFactory.SupportedFileTypes)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
var parser = _themeParserFactory.GetParser(file.FileType);
|
||||
|
||||
if (parser == null)
|
||||
{
|
||||
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
|
||||
await _dialogService.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"),
|
||||
I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var theme = await parser.Parse(file.Name, file.Content).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
|
||||
var exportedTheme = await parser.Import(file.Name, file.Content).ConfigureAwait(true);
|
||||
|
||||
AddTheme(theme);
|
||||
if (!string.IsNullOrWhiteSpace(exportedTheme.EncodedImage))
|
||||
{
|
||||
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
|
||||
var importedImage = await _imageFileSystemService
|
||||
.ImportThemeImageAsync(exportedTheme.BackgroundImage, exportedTheme.EncodedImage)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
exportedTheme.BackgroundImage = importedImage;
|
||||
}
|
||||
|
||||
var terminalTheme = new TerminalTheme(exportedTheme);
|
||||
|
||||
AddTheme(terminalTheme);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK).ConfigureAwait(false);
|
||||
await _dialogService
|
||||
.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +178,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_settingsService.SaveTheme(theme, true);
|
||||
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, true);
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, true);
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
@@ -180,14 +218,19 @@ 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)
|
||||
{
|
||||
SelectedThemeBackgroundColorChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void OnSelectedThemeBackgroundImageChanged(object sender, ImageFile e)
|
||||
{
|
||||
SelectedThemeBackgroundImageChanged?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,22 +9,21 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
|
||||
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView applicationView,
|
||||
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers)
|
||||
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IApplicationView applicationView,
|
||||
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers,
|
||||
IImageFileSystemService imageFileSystemService)
|
||||
{
|
||||
About = new AboutPageViewModel(settingsService, updateService, applicationView);
|
||||
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, defaultValueProvider, trayProcessCommunicationService);
|
||||
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService);
|
||||
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);
|
||||
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService, imageFileSystemService);
|
||||
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
|
||||
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService,
|
||||
applicationView, trayProcessCommunicationService, containers.HistoryContainer);
|
||||
}
|
||||
|
||||
public event EventHandler Closed;
|
||||
public event EventHandler AboutPageRequested;
|
||||
|
||||
public GeneralPageViewModel General { get; }
|
||||
public KeyBindingsPageViewModel KeyBindings { get; }
|
||||
@@ -32,14 +31,8 @@ namespace FluentTerminal.App.ViewModels
|
||||
public TerminalPageViewModel Terminal { get; }
|
||||
public ThemesPageViewModel Themes { get; }
|
||||
public MousePageViewModel Mouse { get; }
|
||||
public AboutPageViewModel About { get; }
|
||||
public SshProfilesPageViewModel SshProfiles { get; }
|
||||
|
||||
public void NavigateToAboutPage()
|
||||
{
|
||||
AboutPageRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
@@ -11,9 +11,9 @@ using FluentTerminal.App.ViewModels.Profiles;
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends <see cref="ShellProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
|
||||
/// Extends <see cref="ProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
|
||||
/// </summary>
|
||||
public class ShellProfileViewModel : ShellProfileViewModelBase<CommonProfileProviderViewModel>
|
||||
public class ShellProfileViewModel : ProfileViewModelBase<CommonProfileProviderViewModel>
|
||||
{
|
||||
#region Fields
|
||||
|
||||
@@ -60,21 +60,23 @@ namespace FluentTerminal.App.ViewModels
|
||||
shellProfile);
|
||||
|
||||
SetDefaultCommand = new RelayCommand(SetDefault);
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
|
||||
}
|
||||
|
||||
#endregion Constrcutor
|
||||
|
||||
#region Methods
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
if (InEditMode || !ProfileVm.PreInstalled)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
|
||||
// ConfigureAwait(true) because we need to execute Initialize method in the calling (UI) thread.
|
||||
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmRestoreProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
|
||||
@@ -5,10 +5,10 @@ using FluentTerminal.Models;
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends <see cref="ShellProfileViewModelBase{T}"/>, and doesn't implement any additional logic because in
|
||||
/// 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 : ShellProfileViewModelBase<SshConnectViewModel>
|
||||
public class SshProfileViewModel : ProfileViewModelBase<SshConnectViewModel>
|
||||
{
|
||||
#region Constructor
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
isNew)
|
||||
{
|
||||
ProfileVm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, historyContainer, sshProfile);
|
||||
fileSystemService, sshProfile);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class TabThemeViewModel : ViewModelBase
|
||||
{
|
||||
private bool _isSelected;
|
||||
|
||||
public TabThemeViewModel(TabTheme theme, TerminalViewModel terminal)
|
||||
{
|
||||
Theme = theme;
|
||||
Terminal = terminal;
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set
|
||||
{
|
||||
if (Set(ref _isSelected, value) && value)
|
||||
{
|
||||
Terminal.TabTheme = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TabTheme Theme { get; }
|
||||
public TerminalViewModel Terminal { get; }
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,19 @@ using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Core;
|
||||
using FluentTerminal.Models.Messages;
|
||||
using System.Windows.Input;
|
||||
using FluentTerminal.App.ViewModels.Menu;
|
||||
using Windows.UI.Xaml.Data;
|
||||
using Windows.UI.Xaml;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
@@ -22,28 +29,108 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
#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 int TabThemeId { get; set; }
|
||||
public bool ShowSearchPanel { get; set; }
|
||||
public string SearchText { get; set; }
|
||||
public bool SearchMatchCase { get; set; }
|
||||
public bool SearchWholeWord { get; set; }
|
||||
public bool SearchWithRegex { get; set; }
|
||||
public string XtermBufferState { get; set; }
|
||||
public byte TerminalId { get; set; }
|
||||
public ShellProfile ShellProfile { get; set; }
|
||||
public int FontSize { get; set; }
|
||||
}
|
||||
|
||||
public async Task<string> SerializeAsync()
|
||||
{
|
||||
TerminalState state = new TerminalState
|
||||
{
|
||||
HasCustomTitle = _hasCustomTitle,
|
||||
ShellTitle = ShellTitle,
|
||||
TabTitle = TabTitle,
|
||||
TerminalTheme = TerminalTheme,
|
||||
TabThemeId = TabTheme.Theme.Id,
|
||||
ShowSearchPanel = ShowSearchPanel,
|
||||
SearchText = SearchText,
|
||||
SearchMatchCase = SearchMatchCase,
|
||||
SearchWholeWord = SearchWholeWord,
|
||||
SearchWithRegex = SearchWithRegex,
|
||||
XtermBufferState = await SerializeXtermStateAsync().ConfigureAwait(false),
|
||||
TerminalId = Terminal.Id,
|
||||
ShellProfile = ShellProfile,
|
||||
FontSize = FontSize
|
||||
};
|
||||
|
||||
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 = TabThemes.FirstOrDefault(t => t.Theme.Id == state.TabThemeId);
|
||||
ShowSearchPanel = state.ShowSearchPanel;
|
||||
SearchText = state.SearchText;
|
||||
SearchMatchCase = state.SearchMatchCase;
|
||||
SearchWholeWord = state.SearchWholeWord;
|
||||
SearchWithRegex = state.SearchWithRegex;
|
||||
XtermBufferState = state.XtermBufferState;
|
||||
_terminalId = state.TerminalId;
|
||||
ShellProfile = state.ShellProfile;
|
||||
FontSize = state.FontSize;
|
||||
TabTheme.IsSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Serialize terminal state
|
||||
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private bool _isSelected;
|
||||
private bool _isHovered;
|
||||
private bool _hasNewOutput;
|
||||
private bool _hasExitedWithError;
|
||||
private string _searchText;
|
||||
private bool _showSearchPanel;
|
||||
private TabTheme _tabTheme;
|
||||
private bool _searchMatchCase;
|
||||
private bool _searchWholeWord;
|
||||
private bool _searchWithRegex;
|
||||
private TabThemeViewModel _tabTheme;
|
||||
private TerminalTheme _terminalTheme;
|
||||
private TerminalOptions _terminalOptions;
|
||||
private string _tabTitle;
|
||||
private string _shellTitle;
|
||||
private bool _hasCustomTitle;
|
||||
private byte? _terminalId;
|
||||
private bool _hasSelection;
|
||||
private string _hoveredUri;
|
||||
private MenuViewModel _contextMenu;
|
||||
private MenuViewModel _tabContextMenu;
|
||||
private readonly TabThemeViewModel _transparentTabThemeViewModel;
|
||||
private int _fontSize;
|
||||
|
||||
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
|
||||
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
IApplicationView applicationView, IClipboardService clipboardService, string terminalState = null)
|
||||
{
|
||||
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
|
||||
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
|
||||
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
|
||||
MessengerInstance.Register<KeyBindingsChangedMessage>(this, OnKeyBindingsChanged);
|
||||
|
||||
SettingsService = settingsService;
|
||||
SettingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
SettingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
|
||||
SettingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
SettingsService.KeyBindingsChanged += OnKeyBindingsChanged;
|
||||
|
||||
_terminalOptions = SettingsService.GetTerminalOptions();
|
||||
|
||||
@@ -58,17 +145,37 @@ namespace FluentTerminal.App.ViewModels
|
||||
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);
|
||||
TabThemes = new ObservableCollection<TabThemeViewModel>(SettingsService.GetTabThemes().Select(theme => new TabThemeViewModel(theme, this)));
|
||||
CloseCommand = new AsyncCommand(TryCloseAsync, 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);
|
||||
EditTitleCommand = new AsyncCommand(EditTitleAsync, CanExecuteCommand);
|
||||
DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand);
|
||||
ReconnectTabCommand = new AsyncCommand(ReconnectTabAsync, () => CanExecuteCommand() && HasExitedWithError);
|
||||
CopyCommand = new AsyncCommand(Copy, () => HasSelection);
|
||||
PasteCommand = new AsyncCommand(Paste);
|
||||
CopyLinkCommand = new AsyncCommand(() => CopyTextAsync(HoveredUri), () => !string.IsNullOrWhiteSpace(HoveredUri));
|
||||
ShowSearchPanelCommand = new RelayCommand(() => ShowSearchPanel = true, () => !ShowSearchPanel);
|
||||
|
||||
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
|
||||
FindNextCommand = new RelayCommand(FindNext);
|
||||
FindPreviousCommand = new RelayCommand(FindPrevious);
|
||||
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
|
||||
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
|
||||
EditTitleCommand = new AsyncCommand(EditTitle);
|
||||
FontSize = _terminalOptions.FontSize;
|
||||
|
||||
Terminal = new Terminal(TrayProcessCommunicationService);
|
||||
if (!string.IsNullOrEmpty(terminalState))
|
||||
{
|
||||
Restore(terminalState);
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultTabTheme = TabThemes.FirstOrDefault(t => t.Theme.Id == ShellProfile.TabThemeId);
|
||||
defaultTabTheme.IsSelected = true;
|
||||
}
|
||||
|
||||
_transparentTabThemeViewModel = TabThemes.FirstOrDefault(t => t.Theme.Id == 0);
|
||||
|
||||
Terminal = new Terminal(TrayProcessCommunicationService, _terminalId);
|
||||
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
|
||||
Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
Terminal.SizeChanged += Terminal_SizeChanged;
|
||||
@@ -76,37 +183,70 @@ namespace FluentTerminal.App.ViewModels
|
||||
Terminal.Exited += Terminal_Exited;
|
||||
Terminal.Closed += Terminal_Closed;
|
||||
|
||||
Overlay = new OverlayViewModel(dispatcherTimer);
|
||||
ContextMenu = BuidContextMenu();
|
||||
TabContextMenu = BuildTabContextMenu();
|
||||
}
|
||||
|
||||
public MenuViewModel ContextMenu
|
||||
{
|
||||
get => _contextMenu;
|
||||
set => Set(ref _contextMenu, value);
|
||||
}
|
||||
|
||||
public MenuViewModel TabContextMenu
|
||||
{
|
||||
get => _tabContextMenu;
|
||||
set => Set(ref _tabContextMenu, value);
|
||||
}
|
||||
|
||||
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 KeyBindingsChanged;
|
||||
public event EventHandler<TerminalOptions> OptionsChanged;
|
||||
public event EventHandler<SearchRequest> FindNextRequested;
|
||||
public event EventHandler<SearchRequest> 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 event EventHandler<int> FontSizeChanged;
|
||||
|
||||
public ApplicationSettings ApplicationSettings { get; private set; }
|
||||
|
||||
public IApplicationView ApplicationView { get; }
|
||||
|
||||
public TabTheme BackgroundTabTheme
|
||||
public TabThemeViewModel 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);
|
||||
TabTheme :
|
||||
_transparentTabThemeViewModel;
|
||||
}
|
||||
|
||||
public IClipboardService ClipboardService { get; }
|
||||
|
||||
public RelayCommand CloseCommand { 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; }
|
||||
|
||||
@@ -118,6 +258,20 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public RelayCommand FindPreviousCommand { get; }
|
||||
|
||||
public RelayCommand DuplicateTabCommand { get; }
|
||||
|
||||
public AsyncCommand ReconnectTabCommand { get; }
|
||||
|
||||
public ICommand CopyLinkCommand { get; private set; }
|
||||
|
||||
public ICommand CopyCommand { get; private set; }
|
||||
|
||||
public ICommand PasteCommand { get; private set; }
|
||||
|
||||
public ICommand ShowSearchPanelCommand { get; private set; }
|
||||
|
||||
public double BackgroundOpacity => _terminalOptions?.BackgroundOpacity ?? 1.0;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
@@ -127,16 +281,57 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.Search), () => ShowSearchPanel = !ShowSearchPanel);
|
||||
HasNewOutput = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_keyboardCommandService.DeregisterCommandHandler(nameof(Command.Search));
|
||||
}
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
RaisePropertyChanged(nameof(ShowCloseButton));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasSelection
|
||||
{
|
||||
get => _hasSelection;
|
||||
set => Set(ref _hasSelection, value);
|
||||
}
|
||||
|
||||
public string HoveredUri
|
||||
{
|
||||
get => _hoveredUri;
|
||||
set
|
||||
{
|
||||
if (Set(ref _hoveredUri, value))
|
||||
{
|
||||
ContextMenu = BuidContextMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsHovered
|
||||
{
|
||||
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);
|
||||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Theme.Color != null);
|
||||
|
||||
public bool HasNewOutput
|
||||
{
|
||||
@@ -153,6 +348,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
HasNewOutput = false;
|
||||
}
|
||||
ReconnectTabCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,11 +358,27 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _searchText, value);
|
||||
}
|
||||
|
||||
public RelayCommand<string> SelectTabThemeCommand { get; }
|
||||
public bool SearchMatchCase
|
||||
{
|
||||
get => _searchMatchCase;
|
||||
set => Set(ref _searchMatchCase, value);
|
||||
}
|
||||
|
||||
public bool SearchWholeWord
|
||||
{
|
||||
get => _searchWholeWord;
|
||||
set => Set(ref _searchWholeWord, value);
|
||||
}
|
||||
|
||||
public bool SearchWithRegex
|
||||
{
|
||||
get => _searchWithRegex;
|
||||
set => Set(ref _searchWithRegex, value);
|
||||
}
|
||||
|
||||
public ISettingsService SettingsService { get; }
|
||||
|
||||
public ShellProfile ShellProfile { get; }
|
||||
public ShellProfile ShellProfile { get; private set; }
|
||||
|
||||
public bool ShowSearchPanel
|
||||
{
|
||||
@@ -174,7 +386,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _showSearchPanel, value);
|
||||
}
|
||||
|
||||
public TabTheme TabTheme
|
||||
public bool SearchHasFocus { get; set; }
|
||||
|
||||
public TabThemeViewModel TabTheme
|
||||
{
|
||||
get => _tabTheme;
|
||||
set
|
||||
@@ -182,14 +396,17 @@ namespace FluentTerminal.App.ViewModels
|
||||
Set(ref _tabTheme, value);
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
|
||||
// necessary to update this constantly so that duplicated tabs will also carry over the color
|
||||
ShellProfile.TabThemeId = value.Theme.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
public ObservableCollection<TabThemeViewModel> TabThemes { get; }
|
||||
|
||||
public Terminal Terminal { get; private set; }
|
||||
|
||||
public OverlayViewModel Overlay { get; private set; }
|
||||
public OverlayViewModel Overlay { get; set; }
|
||||
|
||||
public TerminalTheme TerminalTheme
|
||||
{
|
||||
@@ -197,6 +414,8 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _terminalTheme, value);
|
||||
}
|
||||
|
||||
public bool Initialized { get; set; }
|
||||
|
||||
public string TabTitle
|
||||
{
|
||||
get => _tabTitle;
|
||||
@@ -223,10 +442,8 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
if (Set(ref _tabTitle, title))
|
||||
{
|
||||
CustomTitleChanged?.Invoke(this, title);
|
||||
}
|
||||
Set(ref _tabTitle, title);
|
||||
ShellProfile.Name = title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,25 +459,43 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public int FontSize
|
||||
{
|
||||
get => _fontSize;
|
||||
set
|
||||
{
|
||||
_fontSize = value > 0 ? value : 1;
|
||||
}
|
||||
}
|
||||
|
||||
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
|
||||
|
||||
public Task Close()
|
||||
public Task CloseAsync()
|
||||
{
|
||||
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
|
||||
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
|
||||
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
|
||||
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
|
||||
return Terminal.Close();
|
||||
MessengerInstance.Unregister(this);
|
||||
|
||||
return Terminal.CloseAsync();
|
||||
}
|
||||
|
||||
public void CopyText(string text)
|
||||
public Task CopyTextAsync(string text)
|
||||
{
|
||||
ClipboardService.SetText(text);
|
||||
return ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
ClipboardService.SetText(text);
|
||||
|
||||
if (ApplicationSettings.ShowTextCopied)
|
||||
{
|
||||
Overlay.Show(I18N.Translate("TextCopied"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task EditTitle()
|
||||
// Requires UI thread
|
||||
public async Task EditTitleAsync()
|
||||
{
|
||||
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"));
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"))
|
||||
.ConfigureAwait(true);
|
||||
if (result != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
@@ -288,64 +523,129 @@ namespace FluentTerminal.App.ViewModels
|
||||
FocusTerminal();
|
||||
}
|
||||
|
||||
private bool CanExecuteCommand<T>(T a)
|
||||
{
|
||||
return CanExecuteCommand();
|
||||
}
|
||||
|
||||
private bool CanExecuteCommand()
|
||||
{
|
||||
return Initialized && !_disposalRequested;
|
||||
}
|
||||
|
||||
private void CloseLeftTabs()
|
||||
{
|
||||
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);
|
||||
FindNextRequested?.Invoke(this, new SearchRequest { MatchCase = SearchMatchCase, Regex = SearchWithRegex, Term = SearchText, WholeWord = SearchWholeWord });
|
||||
}
|
||||
|
||||
public ITerminalView TerminalView { get; set; }
|
||||
|
||||
private Task<string> SerializeXtermStateAsync()
|
||||
{
|
||||
return TerminalView?.SerializeXtermStateAsync() ?? Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
private void FindPrevious()
|
||||
{
|
||||
FindPreviousRequested?.Invoke(this, SearchText);
|
||||
FindPreviousRequested?.Invoke(this, new SearchRequest { MatchCase = SearchMatchCase, Regex = SearchWithRegex, Term = SearchText, WholeWord = SearchWholeWord });
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
private void DuplicateTab()
|
||||
{
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public async Task ReconnectTabAsync()
|
||||
{
|
||||
HasExitedWithError = false;
|
||||
await TerminalView?.ReconnectAsync();
|
||||
}
|
||||
|
||||
private void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
|
||||
{
|
||||
ApplicationSettings = message.ApplicationSettings;
|
||||
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
ApplicationSettings = e;
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnCurrentThemeChanged(object sender, Guid e)
|
||||
private void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
|
||||
{
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
// only change theme if not overwritten by profile
|
||||
if (ShellProfile.TerminalThemeId == Guid.Empty)
|
||||
{
|
||||
// only change theme if not overwritten by profile
|
||||
if (ShellProfile.TerminalThemeId == Guid.Empty)
|
||||
var currentTheme = SettingsService.GetTheme(message.ThemeId);
|
||||
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
var currentTheme = SettingsService.GetTheme(e);
|
||||
TerminalTheme = currentTheme;
|
||||
ThemeChanged?.Invoke(this, currentTheme);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
|
||||
{
|
||||
_terminalOptions = message.TerminalOptions;
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => RaisePropertyChanged(nameof(BackgroundOpacity)), CoreDispatcherPriority.Low);
|
||||
}
|
||||
|
||||
private void OnKeyBindingsChanged(KeyBindingsChangedMessage message)
|
||||
{
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
var contextMenu = BuidContextMenu();
|
||||
if (!contextMenu.EquivalentTo(ContextMenu))
|
||||
{
|
||||
ContextMenu = contextMenu;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnKeyBindingsChanged(object sender, EventArgs e)
|
||||
{
|
||||
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
var tabContextMenu = BuildTabContextMenu();
|
||||
if (!tabContextMenu.EquivalentTo(TabContextMenu))
|
||||
{
|
||||
TabContextMenu = tabContextMenu;
|
||||
}
|
||||
|
||||
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
{
|
||||
_terminalOptions = e;
|
||||
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
|
||||
}
|
||||
|
||||
private void SelectTabTheme(string id)
|
||||
{
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
|
||||
}, CoreDispatcherPriority.Low, true);
|
||||
}
|
||||
|
||||
private void Terminal_Exited(object sender, int exitCode)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker && exitCode != 0)
|
||||
{
|
||||
tracker.SetInvalid();
|
||||
}
|
||||
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => HasExitedWithError = exitCode > 0);
|
||||
}
|
||||
|
||||
private void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
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)
|
||||
@@ -354,83 +654,228 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
case nameof(Command.Copy):
|
||||
{
|
||||
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
|
||||
ClipboardService.SetText(selection);
|
||||
if(_terminalOptions.ShowTextCopied)
|
||||
{
|
||||
Overlay.Show(I18N.Translate("TextCopied"));
|
||||
}
|
||||
break;
|
||||
await Copy().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
await Paste().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
case nameof(Command.PasteWithoutNewlines):
|
||||
{
|
||||
string content = await ClipboardService.GetText().ConfigureAwait(true);
|
||||
string content = await ClipboardService.GetTextAsync().ConfigureAwait(false);
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
|
||||
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
|
||||
TerminalView.Paste(content);
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
case nameof(Command.Search):
|
||||
{
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
await ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
SearchStarted?.Invoke(this, EventArgs.Empty);
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
{
|
||||
SearchStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
case nameof(Command.IncreaseFontSize):
|
||||
{
|
||||
FontSize++;
|
||||
FontSizeChanged?.Invoke(this, FontSize);
|
||||
return;
|
||||
}
|
||||
case nameof(Command.DecreaseFontSize):
|
||||
{
|
||||
if (FontSize > 2)
|
||||
{
|
||||
FontSize--;
|
||||
FontSizeChanged?.Invoke(this, FontSize);
|
||||
}
|
||||
break;
|
||||
|
||||
return;
|
||||
}
|
||||
case nameof(Command.ResetFontSize):
|
||||
{
|
||||
FontSize = _terminalOptions.FontSize;
|
||||
FontSizeChanged?.Invoke(this, FontSize);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{
|
||||
_keyboardCommandService.SendCommand(e);
|
||||
break;
|
||||
await ApplicationView.ExecuteOnUiThreadAsync(() => _keyboardCommandService.SendCommand(e))
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
|
||||
{
|
||||
tracker.SetOutputReceived();
|
||||
}
|
||||
|
||||
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => HasNewOutput = true);
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_SizeChanged(object sender, TerminalSize e)
|
||||
{
|
||||
Overlay.Show($"{e.Columns} x {e.Rows}");
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => Overlay.Show($"{e.Columns} x {e.Rows}"));
|
||||
}
|
||||
|
||||
private void Terminal_TitleChanged(object sender, string e)
|
||||
{
|
||||
ShellTitle = e;
|
||||
ShellTitleChanged?.Invoke(this, e);
|
||||
if (e == Terminal.Profile.Location) return;
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => ShellTitle = e);
|
||||
}
|
||||
|
||||
private async Task TryClose()
|
||||
private async Task TryCloseAsync()
|
||||
{
|
||||
if (ApplicationSettings.ConfirmClosingTabs)
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
|
||||
{
|
||||
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmCloseTab"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
tracker.SetInvalid();
|
||||
}
|
||||
|
||||
await Close().ConfigureAwait(true);
|
||||
if (!ApplicationSettings.ConfirmClosingTabs || await DialogService
|
||||
.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
string.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK,
|
||||
DialogButton.Cancel).ConfigureAwait(false) == DialogButton.OK)
|
||||
{
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Copy()
|
||||
{
|
||||
var selection = await Terminal.GetSelectedText().ConfigureAwait(false);
|
||||
await CopyTextAsync(selection).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task Paste()
|
||||
{
|
||||
// prevent from pasting something into the terminal window when the actual command is executed
|
||||
// while being in the search box
|
||||
if (!SearchHasFocus)
|
||||
{
|
||||
var content = await ClipboardService.GetTextAsync().ConfigureAwait(false);
|
||||
if (content != null)
|
||||
{
|
||||
TerminalView.Paste(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MenuViewModel BuidContextMenu()
|
||||
{
|
||||
var commandKeyBindings = SettingsService.GetCommandKeyBindings();
|
||||
var contextMenu = new MenuViewModel();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(HoveredUri))
|
||||
{
|
||||
var copyLink = new MenuItemViewModel(I18N.Translate("CopyLink"), CopyLinkCommand, icon: Mdl2Icon.Link());
|
||||
contextMenu.Items.Add(copyLink);
|
||||
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
|
||||
}
|
||||
|
||||
var copy = new MenuItemViewModel(I18N.Translate("Command.Copy"), CopyCommand, icon: Mdl2Icon.Copy());
|
||||
AddKeyBindings(copy, Command.Copy, commandKeyBindings);
|
||||
contextMenu.Items.Add(copy);
|
||||
|
||||
var paste = new MenuItemViewModel(I18N.Translate("Command.Paste"), PasteCommand, icon: Mdl2Icon.Paste());
|
||||
AddKeyBindings(paste, Command.Paste, commandKeyBindings);
|
||||
contextMenu.Items.Add(paste);
|
||||
|
||||
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
|
||||
|
||||
var editTitle = new MenuItemViewModel(I18N.Translate("EditTitle.Text"), EditTitleCommand, icon: Mdl2Icon.Edit());
|
||||
AddKeyBindings(editTitle, Command.ChangeTabTitle, commandKeyBindings);
|
||||
contextMenu.Items.Add(editTitle);
|
||||
|
||||
var search = new MenuItemViewModel(I18N.Translate("Search.Text"), ShowSearchPanelCommand, icon: Mdl2Icon.Search());
|
||||
AddKeyBindings(search, Command.Search, commandKeyBindings);
|
||||
contextMenu.Items.Add(search);
|
||||
|
||||
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
|
||||
|
||||
var duplicate = new MenuItemViewModel(I18N.Translate("Command.DuplicateTab"), DuplicateTabCommand);
|
||||
AddKeyBindings(duplicate, Command.DuplicateTab, commandKeyBindings);
|
||||
contextMenu.Items.Add(duplicate);
|
||||
|
||||
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
|
||||
|
||||
var close = new MenuItemViewModel(I18N.Translate("Close"), CloseCommand, icon: Mdl2Icon.Cancel());
|
||||
AddKeyBindings(close, Command.CloseTab, commandKeyBindings);
|
||||
contextMenu.Items.Add(close);
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private MenuViewModel BuildTabContextMenu()
|
||||
{
|
||||
var commandKeyBindings = SettingsService.GetCommandKeyBindings();
|
||||
var contextMenu = new MenuViewModel();
|
||||
|
||||
RadioMenuItemViewModel createTabColorItem(TabThemeViewModel tabTheme)
|
||||
{
|
||||
//todo localise name
|
||||
var icon = tabTheme.Theme.Id == 0 ? Mdl2Icon.PaginationDotOutline10() : Mdl2Icon.PaginationDotSolid10(tabTheme.Theme.Color);
|
||||
return new RadioMenuItemViewModel(I18N.Translate($"{tabTheme.Theme.Name}"), $"{Terminal.Id}_tabTheme", tabTheme, bindingPath: nameof(TabThemeViewModel.IsSelected) , icon: icon);
|
||||
}
|
||||
|
||||
var tabColor = new ExpandableMenuItemViewModel(I18N.Translate("Color.Text"));
|
||||
|
||||
foreach (var tabTheme in TabThemes)
|
||||
{
|
||||
tabColor.SubItems.Add(createTabColorItem(tabTheme));
|
||||
}
|
||||
|
||||
contextMenu.Items.Add(tabColor);
|
||||
|
||||
var editTitle = new MenuItemViewModel(I18N.Translate("EditTitle.Text"), EditTitleCommand, icon: Mdl2Icon.Edit());
|
||||
AddKeyBindings(editTitle, Command.ChangeTabTitle, commandKeyBindings);
|
||||
contextMenu.Items.Add(editTitle);
|
||||
|
||||
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
|
||||
|
||||
var duplicate = new MenuItemViewModel(I18N.Translate("Command.DuplicateTab"), DuplicateTabCommand);
|
||||
AddKeyBindings(duplicate, Command.DuplicateTab, commandKeyBindings);
|
||||
contextMenu.Items.Add(duplicate);
|
||||
|
||||
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
|
||||
|
||||
var closeLeft = new MenuItemViewModel(I18N.Translate("CloseLeft.Text"), CloseLeftTabsCommand);
|
||||
contextMenu.Items.Add(closeLeft);
|
||||
|
||||
var closeRight = new MenuItemViewModel(I18N.Translate("CloseRight.Text"), CloseRightTabsCommand);
|
||||
contextMenu.Items.Add(closeRight);
|
||||
|
||||
var closeOther = new MenuItemViewModel(I18N.Translate("CloseOther.Text"), CloseOtherTabsCommand);
|
||||
contextMenu.Items.Add(closeOther);
|
||||
|
||||
var close = new MenuItemViewModel(I18N.Translate("Close"), CloseCommand, icon: Mdl2Icon.Cancel());
|
||||
AddKeyBindings(close, Command.CloseTab, commandKeyBindings);
|
||||
contextMenu.Items.Add(close);
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private void AddKeyBindings(MenuItemViewModel menuItem, Command command, IDictionary<string, ICollection<KeyBinding>> commandKeyBindings)
|
||||
{
|
||||
var keyBindings = commandKeyBindings[command.ToString()];
|
||||
if (keyBindings?.Any() == true)
|
||||
{
|
||||
menuItem.KeyBinding = new MenuItemKeyBindingViewModel(keyBindings.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
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 Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
@@ -41,16 +43,25 @@ namespace FluentTerminal.App.ViewModels
|
||||
private string _selection;
|
||||
private string _white;
|
||||
private string _yellow;
|
||||
private ImageFile _backgroundThemeFile;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly IImageFileSystemService _imageFileSystemService;
|
||||
|
||||
public event EventHandler<string> BackgroundChanged;
|
||||
public event EventHandler<ImageFile> BackgroundImageChanged;
|
||||
|
||||
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, bool isNew)
|
||||
public ThemeViewModel(TerminalTheme theme,
|
||||
ISettingsService settingsService,
|
||||
IDialogService dialogService,
|
||||
IFileSystemService fileSystemService,
|
||||
IImageFileSystemService imageFileSystemService,
|
||||
bool isNew)
|
||||
{
|
||||
Model = theme;
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_imageFileSystemService = imageFileSystemService;
|
||||
_isNew = isNew;
|
||||
|
||||
Name = Model.Name;
|
||||
@@ -81,12 +92,16 @@ namespace FluentTerminal.App.ViewModels
|
||||
CursorAccent = Model.Colors.CursorAccent;
|
||||
Selection = Model.Colors.Selection;
|
||||
|
||||
BackgroundThemeFile = Model.BackgroundImage;
|
||||
|
||||
SetActiveCommand = new RelayCommand(SetActive);
|
||||
DeleteCommand = new RelayCommand(async () => await Delete().ConfigureAwait(false), NotPreInstalled);
|
||||
DeleteCommand = new AsyncCommand(DeleteAsync, NotPreInstalled);
|
||||
EditCommand = new RelayCommand(Edit, NotPreInstalled);
|
||||
CancelEditCommand = new RelayCommand(async () => await CancelEdit().ConfigureAwait(false));
|
||||
SaveChangesCommand = new RelayCommand(SaveChanges);
|
||||
ExportCommand = new RelayCommand(async () => await Export().ConfigureAwait(false), NotPreInstalled);
|
||||
CancelEditCommand = new AsyncCommand(CancelEditAsync);
|
||||
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
|
||||
ExportCommand = new AsyncCommand(Export, NotPreInstalled);
|
||||
ChooseBackgroundImageCommand = new AsyncCommand(ChooseBackgroundImageAsync, NotPreInstalled);
|
||||
DeleteBackgroundImageCommand = new AsyncCommand(DeleteBackgroundImageAsync, NotPreInstalled);
|
||||
}
|
||||
|
||||
public event EventHandler Activated;
|
||||
@@ -177,7 +192,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _brightYellow, value);
|
||||
}
|
||||
|
||||
public RelayCommand CancelEditCommand { get; }
|
||||
public ICommand CancelEditCommand { get; }
|
||||
|
||||
public string Cursor
|
||||
{
|
||||
@@ -197,9 +212,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _cyan, value);
|
||||
}
|
||||
|
||||
public RelayCommand DeleteCommand { get; }
|
||||
public RelayCommand EditCommand { get; }
|
||||
public RelayCommand ExportCommand { get; }
|
||||
public ICommand DeleteCommand { get; }
|
||||
public ICommand EditCommand { get; }
|
||||
public ICommand ExportCommand { get; }
|
||||
|
||||
public string Foreground
|
||||
{
|
||||
@@ -245,7 +260,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _red, value);
|
||||
}
|
||||
|
||||
public RelayCommand SaveChangesCommand { get; }
|
||||
public ICommand SaveChangesCommand { get; }
|
||||
|
||||
public string Selection
|
||||
{
|
||||
@@ -253,7 +268,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _selection, value);
|
||||
}
|
||||
|
||||
public RelayCommand SetActiveCommand { get; }
|
||||
public ICommand SetActiveCommand { get; }
|
||||
|
||||
public string White
|
||||
{
|
||||
@@ -267,7 +282,22 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _yellow, value);
|
||||
}
|
||||
|
||||
public void SaveChanges()
|
||||
public ImageFile BackgroundThemeFile
|
||||
{
|
||||
get => _backgroundThemeFile;
|
||||
set
|
||||
{
|
||||
Set(ref _backgroundThemeFile, value);
|
||||
BackgroundImageChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ChooseBackgroundImageCommand { get; }
|
||||
|
||||
public ICommand DeleteBackgroundImageCommand { get; }
|
||||
|
||||
// Requires UI thread
|
||||
private async Task SaveChangesAsync()
|
||||
{
|
||||
Model.Name = Name;
|
||||
Model.Author = Author;
|
||||
@@ -296,92 +326,114 @@ namespace FluentTerminal.App.ViewModels
|
||||
Model.Colors.CursorAccent = CursorAccent;
|
||||
Model.Colors.Selection = Selection;
|
||||
|
||||
if (Model.BackgroundImage != null &&
|
||||
BackgroundThemeFile != Model?.BackgroundImage)
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
await _imageFileSystemService
|
||||
.RemoveImportedImageAsync($"{Model.BackgroundImage?.Name}{Model.BackgroundImage?.FileType}")
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
BackgroundThemeFile = await SaveBackgroundImageAsync().ConfigureAwait(true);
|
||||
|
||||
Model.BackgroundImage = BackgroundThemeFile;
|
||||
|
||||
_settingsService.SaveTheme(Model);
|
||||
|
||||
InEditMode = false;
|
||||
_isNew = false;
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
// Requires UI thread
|
||||
private async Task CancelEditAsync()
|
||||
{
|
||||
if (_isNew)
|
||||
{
|
||||
await Delete();
|
||||
await DeleteAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
TerminalTheme changedTheme = new TerminalTheme()
|
||||
{
|
||||
Name = Name,
|
||||
Author = Author,
|
||||
BackgroundImage = BackgroundThemeFile,
|
||||
Colors = new TerminalColors()
|
||||
{
|
||||
Black = Black,
|
||||
Red = Red,
|
||||
Green = Green,
|
||||
Yellow = Yellow,
|
||||
Blue = Blue,
|
||||
Magenta = Magenta,
|
||||
Cyan = Cyan,
|
||||
White = White,
|
||||
|
||||
BrightBlack = BrightBlack,
|
||||
BrightRed = BrightRed,
|
||||
BrightGreen = BrightGreen,
|
||||
BrightYellow = BrightYellow,
|
||||
BrightBlue = BrightBlue,
|
||||
BrightMagenta = BrightMagenta,
|
||||
BrightCyan = BrightCyan,
|
||||
BrightWhite = BrightWhite,
|
||||
|
||||
Background = Background,
|
||||
Foreground = Foreground,
|
||||
Cursor = Cursor,
|
||||
CursorAccent = CursorAccent,
|
||||
Selection = Selection
|
||||
}
|
||||
};
|
||||
|
||||
if (!_fallbackTheme.Equals(changedTheme))
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
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;
|
||||
|
||||
BackgroundThemeFile = _fallbackTheme.BackgroundImage;
|
||||
|
||||
InEditMode = false;
|
||||
|
||||
await _imageFileSystemService.RemoveTemporaryBackgroundThemeImageAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
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 = BrightBlack,
|
||||
BrightRed = BrightRed,
|
||||
BrightGreen = BrightGreen,
|
||||
BrightYellow = BrightYellow,
|
||||
BrightBlue = BrightBlue,
|
||||
BrightMagenta = BrightMagenta,
|
||||
BrightCyan = BrightCyan,
|
||||
BrightWhite = BrightWhite,
|
||||
|
||||
Background = Background,
|
||||
Foreground = Foreground,
|
||||
Cursor = Cursor,
|
||||
CursorAccent = CursorAccent,
|
||||
Selection = Selection
|
||||
}
|
||||
};
|
||||
|
||||
if (!_fallbackTheme.Equals(changedTheme))
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
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;
|
||||
}
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,16 +442,38 @@ namespace FluentTerminal.App.ViewModels
|
||||
return !Model.PreInstalled;
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
// Requires UI thread
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteTheme"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDeleteTheme"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
|
||||
await DeleteBackgroundImageIfExistsAsync().ConfigureAwait(true);
|
||||
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
|
||||
await _imageFileSystemService.RemoveTemporaryBackgroundThemeImageAsync().ConfigureAwait(true);
|
||||
|
||||
Deleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task DeleteBackgroundImageAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDeleteBackgroundImage"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
BackgroundThemeFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
_fallbackTheme = new TerminalTheme(Model);
|
||||
@@ -413,8 +487,60 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private Task Export()
|
||||
{
|
||||
var content = JsonConvert.SerializeObject(Model, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
|
||||
return _fileSystemService.SaveTextFile(Name, "Fluent Terminal Theme", ".flutecolors", content);
|
||||
var encodedImage = _imageFileSystemService.EncodeImage(BackgroundThemeFile);
|
||||
var exportedTheme = new ExportedTerminalTheme(Model, encodedImage);
|
||||
var content = JsonConvert.SerializeObject(exportedTheme, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
|
||||
return _fileSystemService.SaveTextFileAsync(Name, "Fluent Terminal Theme", ".flutecolors", content);
|
||||
}
|
||||
|
||||
private async Task<ImageFile> SaveBackgroundImageAsync()
|
||||
{
|
||||
if (BackgroundThemeFile == null)
|
||||
{
|
||||
return BackgroundThemeFile;
|
||||
}
|
||||
|
||||
if (BackgroundThemeFile == Model.BackgroundImage)
|
||||
{
|
||||
return BackgroundThemeFile;
|
||||
}
|
||||
|
||||
var importedBackgroundThemeFile =
|
||||
await _fileSystemService.SaveImageInRoamingAsync(BackgroundThemeFile).ConfigureAwait(false);
|
||||
|
||||
await _imageFileSystemService.RemoveTemporaryBackgroundThemeImageAsync().ConfigureAwait(false);
|
||||
|
||||
return importedBackgroundThemeFile;
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task ChooseBackgroundImageAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var chosenImage = await _imageFileSystemService
|
||||
.ImportTemporaryImageFileAsync(new[] {".jpeg", ".png", ".jpg"}).ConfigureAwait(true);
|
||||
|
||||
if(chosenImage != null)
|
||||
{
|
||||
BackgroundThemeFile = chosenImage;
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task DeleteBackgroundImageIfExistsAsync()
|
||||
{
|
||||
if (BackgroundThemeFile != null)
|
||||
{
|
||||
var imageFile = Model.BackgroundImage;
|
||||
|
||||
BackgroundThemeFile = null;
|
||||
|
||||
if (imageFile != null)
|
||||
{
|
||||
await _imageFileSystemService.RemoveImportedImageAsync($"{imageFile.Name}{imageFile.FileType}")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("\", "\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.Xaml.Interactivity;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
|
||||
namespace FluentTerminal.App.Actions
|
||||
{
|
||||
public class FocusAction : DependencyObject, IAction
|
||||
{
|
||||
public object Execute(object sender, object parameter)
|
||||
{
|
||||
var control = TargetObject ?? sender as Control;
|
||||
if (control != null)
|
||||
{
|
||||
if (!control.IsLoaded)
|
||||
control.Loaded += Control_Loaded;
|
||||
else
|
||||
control.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Control_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Control control = sender as Control;
|
||||
control.Focus(FocusState.Programmatic);
|
||||
control.Loaded -= Control_Loaded; // won't be needed anymore. Remove reference just in case
|
||||
}
|
||||
|
||||
public Control TargetObject
|
||||
{
|
||||
get { return (Control)GetValue(TargetObjectProperty); }
|
||||
set { SetValue(TargetObjectProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TargetObjectProperty =
|
||||
DependencyProperty.Register(nameof(TargetObject), typeof(Control), typeof(FocusAction), new PropertyMetadata(null));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.AppService;
|
||||
using Windows.Foundation.Collections;
|
||||
@@ -27,7 +25,7 @@ namespace FluentTerminal.App.Adapters
|
||||
|
||||
public async Task<ValueSet> SendMessageAsync(ValueSet message)
|
||||
{
|
||||
var response = await _appServiceConnection.SendMessageAsync(message).AsTask();
|
||||
var response = await _appServiceConnection.SendMessageAsync(message);
|
||||
|
||||
if (response.Status == AppServiceResponseStatus.Success)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using Windows.Foundation.Metadata;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Core.Preview;
|
||||
using Windows.UI.ViewManagement;
|
||||
using FluentTerminal.App.Utilities;
|
||||
|
||||
namespace FluentTerminal.App.Adapters
|
||||
{
|
||||
@@ -24,13 +25,14 @@ namespace FluentTerminal.App.Adapters
|
||||
_applicationView = ApplicationView.GetForCurrentView();
|
||||
_applicationView.Consolidated += _applicationView_Consolidated;
|
||||
_dispatcher = CoreApplication.GetCurrentView().Dispatcher;
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequest;
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequested;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -47,27 +49,21 @@ namespace FluentTerminal.App.Adapters
|
||||
return ApiInformation.IsApiContractPresent(api, version);
|
||||
}
|
||||
|
||||
public Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true)
|
||||
{
|
||||
if (!enforceNewSchedule && _dispatcher.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
public Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
|
||||
bool enforceNewSchedule = false) => _dispatcher.ExecuteAsync(action, priority, enforceNewSchedule);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
|
||||
bool enforceNewSchedule = false) => _dispatcher.ExecuteAsync(func, priority, enforceNewSchedule);
|
||||
|
||||
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
|
||||
}
|
||||
|
||||
public async Task<bool> TryClose()
|
||||
public async Task<bool> TryCloseAsync()
|
||||
{
|
||||
if (_closed)
|
||||
{
|
||||
Logger.Instance.Debug("ApplicationViewAdapter.TryClose was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
|
||||
Logger.Instance.Debug("ApplicationViewAdapter.TryCloseAsync 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();
|
||||
Logger.Instance.Debug("TryCloseAsync ApplicationView with Id: {Id}", _applicationView.Id);
|
||||
_closed = await _applicationView.TryConsolidateAsync();
|
||||
return _closed;
|
||||
}
|
||||
|
||||
@@ -78,28 +74,27 @@ namespace FluentTerminal.App.Adapters
|
||||
_applicationView.ExitFullScreenMode();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _applicationView.TryEnterFullScreenMode();
|
||||
}
|
||||
|
||||
return _applicationView.TryEnterFullScreenMode();
|
||||
}
|
||||
|
||||
private async void OnCloseRequest(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
|
||||
private async void OnCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
|
||||
{
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
var args = new CancelableEventArgs();
|
||||
|
||||
if (CloseRequested != null)
|
||||
if (CloseRequested is { } closeRequestedHandler)
|
||||
{
|
||||
await CloseRequested.Invoke(this, args);
|
||||
// ConfigureAwait(true) because SystemNavigationManagerPreview.GetForCurrentView().CloseRequested requires the calling (UI) thread
|
||||
await closeRequestedHandler(this, args).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
e.Handled = args.Cancelled;
|
||||
|
||||
if (!e.Handled)
|
||||
{
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequested;
|
||||
}
|
||||
|
||||
deferral.Complete();
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using System;
|
||||
using Windows.UI.Xaml;
|
||||
|
||||
namespace FluentTerminal.App.Adapters
|
||||
{
|
||||
public class DispatcherTimerAdapter : IDispatcherTimer
|
||||
{
|
||||
private readonly DispatcherTimer _dispatcherTimer;
|
||||
|
||||
public DispatcherTimerAdapter()
|
||||
{
|
||||
_dispatcherTimer = new DispatcherTimer();
|
||||
_dispatcherTimer.Tick += OnDispatcherTimerTick;
|
||||
}
|
||||
|
||||
private void OnDispatcherTimerTick(object sender, object e)
|
||||
{
|
||||
Tick?.Invoke(this, null);
|
||||
}
|
||||
|
||||
public TimeSpan Interval
|
||||
{
|
||||
get => _dispatcherTimer.Interval;
|
||||
set => _dispatcherTimer.Interval = value;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _dispatcherTimer.IsEnabled;
|
||||
|
||||
public event EventHandler<object> Tick;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_dispatcherTimer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_dispatcherTimer.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 515 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user