Compare commits
326
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4153035037 | ||
|
|
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 | ||
|
|
90c299da83 | ||
|
|
02a695056a | ||
|
|
50a3a7c422 | ||
|
|
3f699f40e3 | ||
|
|
b87ec9f348 | ||
|
|
79dda1d4fd | ||
|
|
5467bafaed | ||
|
|
ac3190111d | ||
|
|
298b402d5d | ||
|
|
a7b734b3bf | ||
|
|
3dd95e994f | ||
|
|
96e32fcc96 | ||
|
|
04a50e8152 | ||
|
|
e89853b6b5 | ||
|
|
c6bb9c9e28 | ||
|
|
d027ab9a02 | ||
|
|
13321cbc1a | ||
|
|
9a5af6437f | ||
|
|
a1967edb1d | ||
|
|
5900c51e70 | ||
|
|
6209727037 | ||
|
|
df15cb2231 | ||
|
|
eb2a44d2de | ||
|
|
e4fff8f638 | ||
|
|
80ec667f8b | ||
|
|
112c8c7c6c | ||
|
|
45338d00b8 | ||
|
|
04ed5d85e5 | ||
|
|
96ac5cd49f | ||
|
|
c70061995f | ||
|
|
501c5e20d8 | ||
|
|
5cc7d813a8 | ||
|
|
a0462ef502 | ||
|
|
1b1e29cbfe | ||
|
|
94855b7487 | ||
|
|
8ec0d58065 | ||
|
|
4476fe3061 | ||
|
|
4dbc75f540 | ||
|
|
a82c2a59f5 | ||
|
|
00b250c1b2 | ||
|
|
4ec9f0a5a6 | ||
|
|
5f82724039 | ||
|
|
6c65258439 | ||
|
|
f23493e3c8 | ||
|
|
de07e277f1 | ||
|
|
5c52810149 | ||
|
|
05a5ad56ea | ||
|
|
c51ffaa6e9 | ||
|
|
288b661611 | ||
|
|
32ab18f5f9 | ||
|
|
fddbd12820 | ||
|
|
563a7071ea | ||
|
|
d2c89ccb08 | ||
|
|
b5e8ce7639 | ||
|
|
6829c4c7f3 | ||
|
|
09bd970673 | ||
|
|
af61c83949 | ||
|
|
5ed24f85cf | ||
|
|
763a071488 | ||
|
|
bdb4d57d62 | ||
|
|
1be5fecf41 | ||
|
|
14dfb8b26a | ||
|
|
7284ce7f52 | ||
|
|
ac9f3c2a0d | ||
|
|
9dfab2223f | ||
|
|
c15da646fc | ||
|
|
2a47555987 | ||
|
|
ccf75bd45f | ||
|
|
d9b0b86cd5 | ||
|
|
d157b68384 | ||
|
|
0733381cd0 | ||
|
|
1870209878 | ||
|
|
565b3c3f69 | ||
|
|
7cdda79d67 | ||
|
|
a0f57ab4a3 | ||
|
|
edee7628d0 | ||
|
|
d06ad45423 | ||
|
|
9cf6a61e0f | ||
|
|
0ea4722dc8 | ||
|
|
39960dd755 | ||
|
|
8e5d3dea0a | ||
|
|
973b1a4963 | ||
|
|
dbd8632076 | ||
|
|
fffa7ebcb9 | ||
|
|
ded043db1d | ||
|
|
61ad64519b | ||
|
|
ef7f909d26 | ||
|
|
f2618fa940 | ||
|
|
363f885aea | ||
|
|
ed4a5adb16 | ||
|
|
82a3532e90 | ||
|
|
15d713191c | ||
|
|
575022139a | ||
|
|
b9686ac4ec | ||
|
|
43e4f8290e | ||
|
|
93387ebc57 | ||
|
|
54a9d7ce0d | ||
|
|
44b306ac20 | ||
|
|
14e1e0e3d3 | ||
|
|
136ba3c54f | ||
|
|
1a0c51c6d4 | ||
|
|
363018a43f | ||
|
|
3dcfaa94e9 | ||
|
|
05104e450e | ||
|
|
37f20e70bb | ||
|
|
e5a39fbe0d | ||
|
|
5c2f3e6f95 | ||
|
|
b3e831e3e6 | ||
|
|
31ff544379 | ||
|
|
7c4f62d16b | ||
|
|
f4b4623dab | ||
|
|
a32a86e15f | ||
|
|
6b64d3985d | ||
|
|
ed9c9ad6a6 | ||
|
|
bece957ea9 | ||
|
|
4e83e66a85 | ||
|
|
57d86a9723 | ||
|
|
8b6aae7bda | ||
|
|
2d204e48ab | ||
|
|
38a7b71fa8 | ||
|
|
95c94e3820 | ||
|
|
1920fdf35c | ||
|
|
9f3c46c366 | ||
|
|
221a2c2c6f | ||
|
|
bde65fa865 | ||
|
|
d52860e90a | ||
|
|
b85be454ce | ||
|
|
1f3dd72214 | ||
|
|
c4fcd986c0 | ||
|
|
ea21d37089 | ||
|
|
2f133720cc | ||
|
|
250c90fa02 | ||
|
|
7e2c5d2f45 | ||
|
|
9fed3fb192 | ||
|
|
0d85f933c7 | ||
|
|
f235ddf0f1 | ||
|
|
887cd45b4c | ||
|
|
aac2f220d6 | ||
|
|
90f15cc1e3 | ||
|
|
292b66af4d | ||
|
|
b6d835984c | ||
|
|
369b5ddacf | ||
|
|
6be29f49d7 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# Git will handle the files in whatever way it thinks is best.
|
||||
# Overwrites global core.autocrlf git setting
|
||||
* text=auto
|
||||
@@ -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: |
|
||||
npm install
|
||||
npm run 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
|
||||
@@ -292,3 +292,5 @@ __pycache__/
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
!FluentTerminal.App/MoshExecutables/*
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
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.
@@ -1,3 +1,3 @@
|
||||
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%1" /f
|
||||
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%V" /f
|
||||
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%V" /f
|
||||
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%1\"" /f
|
||||
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
|
||||
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
|
||||
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
|
||||
{
|
||||
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, 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>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, 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>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, 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>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, 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,9 +75,9 @@ 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>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => 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);
|
||||
}
|
||||
@@ -86,11 +86,24 @@ namespace FluentTerminal.App.Services.Test
|
||||
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
|
||||
{
|
||||
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
|
||||
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
|
||||
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowProfileSelectionDialogAsync();
|
||||
|
||||
shellProfileSelectionDialog.Verify(x => x.SelectProfile(), Times.Once);
|
||||
shellProfileSelectionDialog.Verify(x => x.SelectProfileAsync(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowInputDialogAsync_UsesIInputDialogSetTitle()
|
||||
{
|
||||
var title = "title";
|
||||
var inputDialog = new Mock<IInputDialog>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
|
||||
|
||||
dialogService.ShowInputDialogAsync(title);
|
||||
|
||||
inputDialog.Verify(x => x.SetTitle(title), Times.Once);
|
||||
inputDialog.Verify(x => x.GetInput(), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
@@ -11,13 +11,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.4.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="Moq" Version="4.8.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
<PackageReference Include="AutoFixture" Version="4.11.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.10.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.13.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -31,4 +34,11 @@
|
||||
<ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Windows.Foundation.FoundationContract">
|
||||
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -41,6 +41,41 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
invoke.Should().Throw<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeregisterCommandHandler_CommandIsNull_ThrowsArgumentNullException()
|
||||
{
|
||||
var keyboardCommandService = new KeyboardCommandService();
|
||||
|
||||
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(null);
|
||||
|
||||
invoke.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeregisterCommandHandler_CommandRegistered_HandlerGetsRemoved()
|
||||
{
|
||||
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
|
||||
var handler = _fixture.Create<Action>();
|
||||
var keyboardCommandService = new KeyboardCommandService();
|
||||
|
||||
keyboardCommandService.RegisterCommandHandler(command, handler);
|
||||
|
||||
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
|
||||
|
||||
invoke.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeregisterCommandHandler_CommandNotRegistered_ShouldNotThrow()
|
||||
{
|
||||
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
|
||||
var keyboardCommandService = new KeyboardCommandService();
|
||||
|
||||
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
|
||||
|
||||
invoke.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendCommand_CommandIsRegistered_HandlerGetsInvoked()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -471,17 +496,14 @@ namespace FluentTerminal.App.Services.Test
|
||||
var commands = _fixture.CreateMany<Command>(5);
|
||||
var defaultValueProvider = new Mock<IDefaultValueProvider>();
|
||||
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
|
||||
foreach (var command in commands)
|
||||
{
|
||||
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(EnumHelper.GetEnumDescription(command), null)).Returns(new Collection<KeyBinding> { });
|
||||
}
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
KeyBindings = keyBindingsContainer.Object,
|
||||
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);
|
||||
|
||||
@@ -506,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);
|
||||
|
||||
@@ -529,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);
|
||||
|
||||
@@ -550,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);
|
||||
|
||||
@@ -575,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();
|
||||
|
||||
@@ -598,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);
|
||||
@@ -623,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);
|
||||
@@ -644,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>(),
|
||||
@@ -653,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);
|
||||
}
|
||||
|
||||
@@ -666,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>(),
|
||||
@@ -687,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>(),
|
||||
@@ -711,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,
|
||||
@@ -739,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,
|
||||
|
||||
@@ -10,6 +10,7 @@ using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Collections;
|
||||
using Xunit;
|
||||
|
||||
namespace FluentTerminal.App.Services.Test
|
||||
@@ -34,18 +35,20 @@ namespace FluentTerminal.App.Services.Test
|
||||
});
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
var response = _fixture.Create<CreateTerminalResponse>();
|
||||
var responseMessage = new Dictionary<string, string>
|
||||
var responseMessage = new ValueSet
|
||||
{
|
||||
[MessageKeys.Type] = _fixture.Create<string>(),
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(response)
|
||||
};
|
||||
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
|
||||
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<ValueSet>())).Returns(Task.FromResult(responseMessage));
|
||||
var terminalSize = _fixture.Create<TerminalSize>();
|
||||
var shellProfile = _fixture.Create<ShellProfile>();
|
||||
var sessionType = _fixture.Create<SessionType>();
|
||||
var id = _fixture.Create<byte>();
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile);
|
||||
var result = await trayProcessCommunicationService.CreateTerminalAsync(id, terminalSize, shellProfile, sessionType);
|
||||
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
@@ -53,7 +56,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
[Fact]
|
||||
public async Task ResizeTerminal_Default_SendsResizeTerminalRequest()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var terminalSize = _fixture.Create<TerminalSize>();
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
@@ -65,16 +68,16 @@ 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<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(ResizeTerminalRequest))), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteText_Default_SendsWriteTextRequest()
|
||||
public async Task Write_Default_SendsWriteDataRequest()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var text = _fixture.Create<string>();
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var data = _fixture.Create<byte[]>();
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
@@ -85,15 +88,15 @@ namespace FluentTerminal.App.Services.Test
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
await trayProcessCommunicationService.WriteText(terminalId, text);
|
||||
await trayProcessCommunicationService.WriteAsync(terminalId, data);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteTextRequest))), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseTerminal_Default_SendsTerminalExitedRequest()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
|
||||
@@ -104,24 +107,21 @@ 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<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(TerminalExitedRequest))), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnMessageReceived_TerminalExitedRequest_InvokesTerminalExitedEvent()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var receivedTerminalId = 0;
|
||||
var terminalExitedEventCalled = false;
|
||||
var request = new TerminalExitedRequest
|
||||
var request = new TerminalExitedRequest(_fixture.Create<byte>(), _fixture.Create<int>());
|
||||
|
||||
var receivedExitStatus = (TerminalExitStatus) null;
|
||||
|
||||
var message = new ValueSet
|
||||
{
|
||||
TerminalId = terminalId
|
||||
};
|
||||
var message = new Dictionary<string, string>
|
||||
{
|
||||
[MessageKeys.Type] = nameof(TerminalExitedRequest),
|
||||
[MessageKeys.Type] = (byte) MessageIdentifiers.TerminalExitedRequest,
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
|
||||
};
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
@@ -133,33 +133,29 @@ namespace FluentTerminal.App.Services.Test
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
var appServiceConnection = new Mock<IAppServiceConnection>();
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
trayProcessCommunicationService.TerminalExited += (s, e) =>
|
||||
trayProcessCommunicationService.TerminalExited += (s, status) =>
|
||||
{
|
||||
terminalExitedEventCalled = true;
|
||||
receivedTerminalId = e;
|
||||
receivedExitStatus = status;
|
||||
};
|
||||
|
||||
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
|
||||
|
||||
terminalExitedEventCalled.Should().BeTrue();
|
||||
receivedTerminalId.Should().Be(terminalId);
|
||||
receivedExitStatus.Should().NotBeNull();
|
||||
receivedExitStatus.TerminalId.Should().Be(request.TerminalId);
|
||||
receivedExitStatus.ExitCode.Should().Be(request.ExitCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var output = _fixture.Create<string>();
|
||||
var receivedOutput = string.Empty;
|
||||
var request = new DisplayTerminalOutputRequest
|
||||
var terminalId = _fixture.Create<byte>();
|
||||
var output = _fixture.Create<byte[]>();
|
||||
var receivedOutput = default(byte[]);
|
||||
var message = new ValueSet
|
||||
{
|
||||
TerminalId = terminalId,
|
||||
Output = output
|
||||
};
|
||||
var message = new Dictionary<string, string>
|
||||
{
|
||||
[MessageKeys.Type] = nameof(DisplayTerminalOutputRequest),
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
|
||||
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
|
||||
[MessageKeys.TerminalId] = terminalId,
|
||||
[MessageKeys.Content] = output
|
||||
};
|
||||
var settingsService = new Mock<ISettingsService>();
|
||||
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
|
||||
@@ -177,7 +173,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
|
||||
|
||||
receivedOutput.Should().Be(output);
|
||||
receivedOutput.Should().BeEquivalentTo(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,7 @@
|
||||
public IApplicationDataContainer Themes { get; set; }
|
||||
public IApplicationDataContainer KeyBindings { get; set; }
|
||||
public IApplicationDataContainer ShellProfiles { get; set; }
|
||||
public IApplicationDataContainer SshProfiles { get; set; }
|
||||
public IApplicationDataContainer HistoryContainer { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,11 @@
|
||||
public const string ThemesContainerName = "Themes";
|
||||
public const string KeyBindingsContainerName = "KeyBindings";
|
||||
public const string ShellProfilesContainerName = "ShellProfiles";
|
||||
public const string SshProfilesContainerName = "SshProfiles";
|
||||
public const byte TerminalBufferRequestIdentifier = 0;
|
||||
public const string SshCommandName = "ssh";
|
||||
public const string MoshCommandName = "mosh";
|
||||
public const string ExecutedCommandsContainerName = "HistoryContainer";
|
||||
public const string TerminalViewModelStateId = "TerminalViewModelState";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface IAboutDialog
|
||||
{
|
||||
Task ShowAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ICustomCommandDialog
|
||||
{
|
||||
Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,6 @@ namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
ShellProfile SelectedProfile { get; }
|
||||
|
||||
Task<ShellProfile> SelectProfile();
|
||||
Task<ShellProfile> SelectProfileAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ISshConnectionInfoDialog
|
||||
{
|
||||
Task<SshProfile> GetSshConnectionInfoAsync(SshProfile input = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using Windows.UI.Xaml;
|
||||
|
||||
namespace FluentTerminal.App.Services.EventArgs
|
||||
{
|
||||
public class NewTabRequestedEventArgs : System.EventArgs
|
||||
{
|
||||
public DragEventArgs DragEventArgs { get; set; }
|
||||
public int Position { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services.EventArgs
|
||||
{
|
||||
public class NewWindowRequestedEventArgs : System.EventArgs
|
||||
{
|
||||
public ShellProfile Profile { get; }
|
||||
|
||||
public NewWindowRequestedEventArgs(ShellProfile profile) =>
|
||||
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||||
}
|
||||
}
|
||||
@@ -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,10 +6,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="PListNet" Version="2.0.3" />
|
||||
<PackageReference Include="RestSharp" Version="106.5.4" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="PListNet" Version="3.0.0" />
|
||||
<PackageReference Include="RestSharp" Version="106.10.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
||||
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using FluentTerminal.Models.Enums;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
// This validator is needed because accelerator key has to be member of VirtualKey enum.
|
||||
public interface IAcceleratorKeyValidator
|
||||
{
|
||||
bool Valid(int key);
|
||||
|
||||
bool Valid(ExtendedVirtualKey key);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IAppServiceConnection
|
||||
{
|
||||
event EventHandler<IDictionary<string, string>> MessageReceived;
|
||||
event EventHandler<ValueSet> MessageReceived;
|
||||
|
||||
Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message);
|
||||
Task<ValueSet> SendMessageAsync(ValueSet message);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
void Delete(string key);
|
||||
|
||||
void Clear();
|
||||
|
||||
IEnumerable<object> GetAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IApplicationLanguageService
|
||||
{
|
||||
IEnumerable<string> Languages { get; }
|
||||
|
||||
void SetLanguage(string language);
|
||||
|
||||
string GetCurrentLanguage();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using FluentTerminal.App.Services.EventArgs;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Core;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
@@ -9,13 +10,20 @@ namespace FluentTerminal.App.Services
|
||||
public interface IApplicationView
|
||||
{
|
||||
event CloseRequestedHandler CloseRequested;
|
||||
event EventHandler Closed;
|
||||
|
||||
int Id { get; }
|
||||
string Title { get; set; }
|
||||
|
||||
Task RunOnDispatcherThread(Action action);
|
||||
Task 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,17 +5,24 @@ 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<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons);
|
||||
|
||||
Task<ShellProfile> ShowProfileSelectionDialogAsync();
|
||||
|
||||
Task<KeyBinding> ShowCreateKeyBindingDialog();
|
||||
Task<string> ShowInputDialogAsync(string title);
|
||||
|
||||
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null);
|
||||
|
||||
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,16 +6,8 @@ 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<TerminalOptions> TerminalOptionsChanged;
|
||||
event EventHandler<Guid> ThemeDeleted;
|
||||
event EventHandler<TerminalTheme> ThemeAdded;
|
||||
|
||||
void DeleteShellProfile(Guid id);
|
||||
void DeleteSshProfile(Guid id);
|
||||
void DeleteTheme(Guid id);
|
||||
ApplicationSettings GetApplicationSettings();
|
||||
TerminalTheme GetCurrentTheme();
|
||||
@@ -24,18 +16,28 @@ namespace FluentTerminal.App.Services
|
||||
Guid GetDefaultShellProfileId();
|
||||
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 SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
|
||||
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
|
||||
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
|
||||
void SaveTerminalOptions(TerminalOptions terminalOptions);
|
||||
void SaveTheme(TerminalTheme theme, bool newTheme = false);
|
||||
ShellProfile GetShellProfile(Guid id);
|
||||
SshProfile GetSshProfile(Guid id);
|
||||
string ExportSettings();
|
||||
void ImportSettings(string serializedSettings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IShellProfileMigrationService
|
||||
{
|
||||
void Migrate(ShellProfile profile);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentTerminal.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ISystemFontService
|
||||
{
|
||||
IEnumerable<string> GetSystemFontFamilies();
|
||||
IEnumerable<FontInfo> GetSystemFontFamilies();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using FluentTerminal.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ITerminalView
|
||||
{
|
||||
event EventHandler<TerminalSize> TerminalSizeChanged;
|
||||
|
||||
event EventHandler<string> TerminalTitleChanged;
|
||||
|
||||
event EventHandler<string> KeyboardCommandReceived;
|
||||
|
||||
Task ChangeTheme(TerminalColors theme);
|
||||
|
||||
Task ChangeOptions(TerminalOptions options);
|
||||
|
||||
Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings);
|
||||
|
||||
Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings);
|
||||
|
||||
Task ConnectToSocket(string url);
|
||||
|
||||
void Close();
|
||||
|
||||
Task FocusTerminal();
|
||||
|
||||
Task<string> GetSelection();
|
||||
|
||||
Task FindNext(string searchText);
|
||||
|
||||
Task FindPrevious(string searchText);
|
||||
|
||||
void FocusSearchTextBox();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,20 +8,45 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ITrayProcessCommunicationService
|
||||
{
|
||||
event EventHandler<int> TerminalExited;
|
||||
event EventHandler<TerminalExitStatus> TerminalExited;
|
||||
|
||||
void Initialize(IAppServiceConnection appServiceConnection);
|
||||
|
||||
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile);
|
||||
Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
|
||||
SessionType sessionType);
|
||||
|
||||
Task ResizeTerminal(int id, TerminalSize size);
|
||||
Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause);
|
||||
|
||||
Task UpdateToggleWindowKeyBindings();
|
||||
Task ResizeTerminalAsync(byte id, TerminalSize size);
|
||||
|
||||
Task WriteText(int terminalId, string text);
|
||||
Task UpdateToggleWindowKeyBindingsAsync();
|
||||
|
||||
void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
|
||||
Task WriteAsync(byte terminalId, byte[] data);
|
||||
|
||||
Task CloseTerminal(int terminalId);
|
||||
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
|
||||
|
||||
void UnsubscribeFromTerminalOutput(byte terminalId);
|
||||
|
||||
Task CloseTerminalAsync(byte terminalId);
|
||||
|
||||
byte GetNextTerminalId();
|
||||
|
||||
Task<string> GetUserNameAsync();
|
||||
|
||||
Task SaveTextFileAsync(string path, string content);
|
||||
|
||||
Task<string> GetSshConfigDirAsync();
|
||||
|
||||
Task<string[]> GetFilesFromSshConfigDirAsync();
|
||||
|
||||
Task<bool> CheckFileExistsAsync(string path);
|
||||
|
||||
Task MuteTerminalAsync(bool mute);
|
||||
|
||||
void UpdateSettings(ApplicationSettings settings);
|
||||
|
||||
Task<string> GetCommandPathAsync(string command);
|
||||
|
||||
Task QuitApplicationAsync();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface IUpdateService
|
||||
{
|
||||
Task CheckForUpdate(bool notifyNoUpdate = false);
|
||||
Task CheckForUpdateAsync(bool notifyNoUpdate = false);
|
||||
Version GetCurrentVersion();
|
||||
Task<Version> GetLatestVersionAsync();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Windows.Foundation.Metadata;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
@@ -9,6 +12,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,
|
||||
@@ -16,13 +27,22 @@ 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
|
||||
EnableTrayIcon = true,
|
||||
ShowCustomTitleInTitlebar = true,
|
||||
UseMoshByDefault = true,
|
||||
AutoFallbackToWindowsUsernameInLinks = true,
|
||||
RTrimCopiedLines = true,
|
||||
MuteTerminalBeeps = true,
|
||||
EnableLogging = false,
|
||||
PrintableOutputOnly = true,
|
||||
LogDirectoryPath = logDirectoryPath,
|
||||
UseConPty = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,9 +56,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ToggleWindow),
|
||||
Ctrl = false,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Key = (int)ExtendedVirtualKey.Scroll
|
||||
}
|
||||
};
|
||||
@@ -50,8 +67,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.NextTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Key = (int)ExtendedVirtualKey.Tab
|
||||
}
|
||||
};
|
||||
@@ -63,7 +78,6 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.PreviousTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.Tab
|
||||
}
|
||||
@@ -83,13 +97,23 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
new KeyBinding
|
||||
{
|
||||
Command = command.ToString(),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.Number1 + (command - Command.SwitchToTerm1)
|
||||
}
|
||||
};
|
||||
|
||||
case Command.DuplicateTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.DuplicateTab),
|
||||
Ctrl = true,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.D
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -97,8 +121,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.NewTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.T
|
||||
}
|
||||
};
|
||||
@@ -110,12 +133,71 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.ConfigurableNewTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.T
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewSshTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewSshTab),
|
||||
Ctrl = true,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.Y
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewSshWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewSshWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.Y
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewCustomCommandTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewCustomCommandTab),
|
||||
Ctrl = true,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.Q
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewCustomCommandWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewCustomCommandWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.Q
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ChangeTabTitle:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ChangeTabTitle),
|
||||
Ctrl = true,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.R
|
||||
}
|
||||
};
|
||||
|
||||
case Command.CloseTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -123,8 +205,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.CloseTab),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.W
|
||||
}
|
||||
};
|
||||
@@ -136,8 +217,19 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.NewWindow),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.N
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ConfigurableNewWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ConfigurableNewWindow),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.N
|
||||
}
|
||||
};
|
||||
@@ -149,9 +241,9 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.ShowSettings),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Key = (int)ExtendedVirtualKey.Comma
|
||||
Shift = true,
|
||||
// I've had to replace ExtendedVirtualKey.Comma because it isn't a member of VirtualKey enum, thus cannot be shown in menu.
|
||||
Key = (int)ExtendedVirtualKey.S
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,8 +254,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.Copy),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.C
|
||||
}
|
||||
};
|
||||
@@ -175,8 +266,20 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.Paste),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.V
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
case Command.PasteWithoutNewlines:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.PasteWithoutNewlines),
|
||||
Ctrl = true,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.V
|
||||
}
|
||||
};
|
||||
@@ -188,8 +291,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.Search),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.F
|
||||
}
|
||||
};
|
||||
@@ -200,10 +302,13 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ToggleFullScreen),
|
||||
Ctrl = false,
|
||||
Alt = true,
|
||||
Shift = false,
|
||||
Key = (int)ExtendedVirtualKey.Enter
|
||||
},
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ToggleFullScreen),
|
||||
Key = (int)ExtendedVirtualKey.F11
|
||||
}
|
||||
};
|
||||
|
||||
@@ -214,8 +319,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.SelectAll),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.A
|
||||
}
|
||||
};
|
||||
@@ -227,14 +331,14 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
Command = nameof(Command.Clear),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = false,
|
||||
Alt = true,
|
||||
Key = (int)ExtendedVirtualKey.L
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw new InvalidOperationException($"Default keybindings for Command '{command}' are missing");
|
||||
}
|
||||
}
|
||||
|
||||
public Guid GetDefaultShellProfileId()
|
||||
@@ -249,7 +353,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
new TabTheme
|
||||
{
|
||||
Id = 0,
|
||||
Name = "None",
|
||||
Name = I18N.Translate("TabTheme.None"),
|
||||
BackgroundOpacity = double.NaN,
|
||||
BackgroundPointerOverOpacity = double.NaN,
|
||||
BackgroundPressedOpacity = double.NaN,
|
||||
@@ -260,41 +364,42 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
new TabTheme
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Red",
|
||||
Name = I18N.Translate("TabTheme.Red"),
|
||||
Color = "#E81123"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Green",
|
||||
Name = I18N.Translate("TabTheme.Green"),
|
||||
Color = "#10893E"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 3,
|
||||
Name = "Blue",
|
||||
Name = I18N.Translate("TabTheme.Blue"),
|
||||
Color = "#0078D7"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 4,
|
||||
Name = "Purple",
|
||||
Name = I18N.Translate("TabTheme.Purple"),
|
||||
Color = "#881798"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 5,
|
||||
Name = "Orange",
|
||||
Name = I18N.Translate("TabTheme.Orange"),
|
||||
Color = "#FF8C00"
|
||||
},
|
||||
new TabTheme
|
||||
{
|
||||
Id = 6,
|
||||
Name = "Teal",
|
||||
Name = I18N.Translate("TabTheme.Teal"),
|
||||
Color = "#00B7C3"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public TerminalOptions GetDefaultTerminalOptions()
|
||||
{
|
||||
return new TerminalOptions
|
||||
@@ -305,9 +410,12 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
ScrollBarStyle = ScrollBarStyle.Hidden,
|
||||
FontFamily = "Consolas",
|
||||
FontSize = 13,
|
||||
FontWeight = 400,
|
||||
BackgroundOpacity = 0.8,
|
||||
Padding = 12,
|
||||
ScrollBackLimit = 1000
|
||||
ScrollBackLimit = 1000,
|
||||
WordSeparator = " ()[]{}:;|│!&*<>@"&squo;",
|
||||
UseAcrylicBackground = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -324,10 +432,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+
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -345,10 +460,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+
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -366,10 +488,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+
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["TERM"] = "xterm-256color"
|
||||
},
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -532,18 +661,18 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Black = "#300A24",
|
||||
Red = "#CC0000",
|
||||
Green = "#4E9A06",
|
||||
Yellow = "#06989A",
|
||||
Yellow = "#C4A000",
|
||||
Blue = "#3465A4",
|
||||
Magenta = "#75507B",
|
||||
Cyan = "#C4A000",
|
||||
Cyan = "#06989A",
|
||||
White = "#D3D7CF",
|
||||
BrightBlack = "#554E53",
|
||||
BrightRed = "#EF2929",
|
||||
BrightGreen = "#8AE234",
|
||||
BrightYellow = "#34E2E2",
|
||||
BrightYellow = "#FCE94F",
|
||||
BrightBlue = "#729FCF",
|
||||
BrightMagenta = "#AD7FA8",
|
||||
BrightCyan = "#FCE94F",
|
||||
BrightCyan = "#34E2E2",
|
||||
BrightWhite = "#EEEEEE"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,13 +11,23 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
private readonly Func<IMessageDialog> _messageDialogFactory;
|
||||
private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory;
|
||||
private readonly Func<IInputDialog> _inputDialogFactory;
|
||||
private readonly Func<ISshConnectionInfoDialog> _sshConnectionInfoDialogFactory;
|
||||
private readonly Func<ICustomCommandDialog> _customCommandDialogFactory;
|
||||
private readonly Func<IAboutDialog> _aboutDialogFactory;
|
||||
|
||||
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory, Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory, Func<IInputDialog> inputDialogFactory)
|
||||
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
|
||||
Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
|
||||
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
|
||||
Func<ICustomCommandDialog> customCommandDialogFactory,
|
||||
Func<IAboutDialog> aboutDialogFactory)
|
||||
{
|
||||
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
|
||||
_messageDialogFactory = messageDialogFactory;
|
||||
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
|
||||
_inputDialogFactory = inputDialogFactory;
|
||||
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
|
||||
_customCommandDialogFactory = customCommandDialogFactory;
|
||||
_aboutDialogFactory = aboutDialogFactory;
|
||||
}
|
||||
|
||||
public Task<KeyBinding> ShowCreateKeyBindingDialog()
|
||||
@@ -27,7 +37,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))
|
||||
{
|
||||
@@ -68,7 +78,20 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
var dialog = _shellProfileSelectionDialogFactory();
|
||||
|
||||
return dialog.SelectProfile();
|
||||
return dialog.SelectProfileAsync();
|
||||
}
|
||||
|
||||
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) =>
|
||||
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
|
||||
|
||||
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
|
||||
_customCommandDialogFactory().GetCustomCommandAsync(input);
|
||||
|
||||
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
|
||||
{
|
||||
@@ -12,12 +15,14 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
public const string CurrentThemeKey = "CurrentTheme";
|
||||
public const string DefaultShellProfileKey = "DefaultShellProfile";
|
||||
public const string DefaultSshProfileKey = "DefaultSshProfile";
|
||||
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private readonly IApplicationDataContainer _keyBindings;
|
||||
private readonly IApplicationDataContainer _localSettings;
|
||||
private readonly IApplicationDataContainer _roamingSettings;
|
||||
private readonly IApplicationDataContainer _shellProfiles;
|
||||
private readonly IApplicationDataContainer _sshProfiles;
|
||||
private readonly IApplicationDataContainer _themes;
|
||||
|
||||
public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers)
|
||||
@@ -29,6 +34,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_themes = containers.Themes;
|
||||
_keyBindings = containers.KeyBindings;
|
||||
_shellProfiles = containers.ShellProfiles;
|
||||
_sshProfiles = containers.SshProfiles;
|
||||
|
||||
|
||||
foreach (var theme in _defaultValueProvider.GetPreInstalledThemes())
|
||||
{
|
||||
@@ -47,25 +54,115 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
|
||||
public string ExportSettings()
|
||||
{
|
||||
var config = new
|
||||
{
|
||||
App = GetApplicationSettings(),
|
||||
KeyBindings = GetCommandKeyBindings(),
|
||||
TerminalOptions = GetTerminalOptions(),
|
||||
Themes = new List<TerminalTheme>(),
|
||||
Profiles = new List<ShellProfile>(),
|
||||
SshProfiles = new List<SshProfile>()
|
||||
};
|
||||
|
||||
public event EventHandler<TerminalTheme> ThemeAdded;
|
||||
public event EventHandler<Guid> CurrentThemeChanged;
|
||||
public event EventHandler<Guid> ThemeDeleted;
|
||||
foreach (var theme in GetThemes().Where(x => !x.PreInstalled))
|
||||
{
|
||||
config.Themes.Add(theme);
|
||||
}
|
||||
|
||||
public event EventHandler KeyBindingsChanged;
|
||||
foreach (var profile in GetShellProfiles().Where(x => !x.PreInstalled))
|
||||
{
|
||||
config.Profiles.Add(profile);
|
||||
}
|
||||
|
||||
public event EventHandler<ShellProfile> ShellProfileAdded;
|
||||
public event EventHandler<Guid> ShellProfileDeleted;
|
||||
foreach (var profile in GetSshProfiles())
|
||||
{
|
||||
config.SshProfiles.Add(profile);
|
||||
}
|
||||
|
||||
public event EventHandler<TerminalOptions> TerminalOptionsChanged;
|
||||
return JsonConvert.SerializeObject(config, PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
|
||||
}
|
||||
|
||||
public void ImportSettings(string serializedSettings)
|
||||
{
|
||||
var config = new
|
||||
{
|
||||
App = GetApplicationSettings(),
|
||||
KeyBindings = new Dictionary<string, ICollection<KeyBinding>>(),
|
||||
Themes = new List<TerminalTheme>(),
|
||||
Profiles = new List<ShellProfile>(),
|
||||
SshProfiles = new List<SshProfile>(),
|
||||
TerminalOptions = GetTerminalOptions()
|
||||
};
|
||||
|
||||
JsonConvert.PopulateObject(serializedSettings, config);
|
||||
|
||||
SaveApplicationSettings(config.App);
|
||||
|
||||
// Since we set each command sepaartely, we don't need all existing settings
|
||||
foreach (var pair in config.KeyBindings)
|
||||
{
|
||||
SaveKeyBindings(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
// Can't create/modify pre-installed themes
|
||||
foreach (var theme in config.Themes.Where(x => !x.PreInstalled))
|
||||
{
|
||||
var existingTheme = GetTheme(theme.Id);
|
||||
if (existingTheme?.PreInstalled == true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
SaveTheme(theme, existingTheme != null);
|
||||
}
|
||||
|
||||
// Can't create pre-installed profiles
|
||||
foreach (var profile in config.Profiles.Where(x => !x.PreInstalled))
|
||||
{
|
||||
var existingProfile = GetShellProfile(profile.Id);
|
||||
var isNew = existingProfile.EqualTo(default);
|
||||
|
||||
// You can only edit certain parts of preinstalled profiles
|
||||
if (!isNew && existingProfile.PreInstalled)
|
||||
{
|
||||
existingProfile.WorkingDirectory = profile.WorkingDirectory;
|
||||
existingProfile.Arguments = profile.Arguments;
|
||||
existingProfile.TabThemeId = profile.TabThemeId;
|
||||
existingProfile.TerminalThemeId = profile.TerminalThemeId;
|
||||
existingProfile.LineEndingTranslation = profile.LineEndingTranslation;
|
||||
existingProfile.KeyBindings = profile.KeyBindings;
|
||||
SaveShellProfile(profile, isNew);
|
||||
continue;
|
||||
}
|
||||
SaveShellProfile(profile, isNew);
|
||||
}
|
||||
|
||||
foreach (var profile in config.SshProfiles)
|
||||
{
|
||||
var existingProfile = GetSshProfile(profile.Id);
|
||||
var isNew = existingProfile.EqualTo(default(SshProfile));
|
||||
|
||||
SaveSshProfile(profile, isNew);
|
||||
}
|
||||
|
||||
SaveTerminalOptions(config.TerminalOptions);
|
||||
}
|
||||
|
||||
public void DeleteShellProfile(Guid id)
|
||||
{
|
||||
_shellProfiles.Delete(id.ToString());
|
||||
ShellProfileDeleted?.Invoke(this, id);
|
||||
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
|
||||
}
|
||||
|
||||
public void DeleteSshProfile(Guid id)
|
||||
{
|
||||
_sshProfiles.Delete(id.ToString());
|
||||
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
|
||||
public void DeleteTheme(Guid id)
|
||||
{
|
||||
_themes.Delete(id.ToString());
|
||||
@@ -79,7 +176,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
}
|
||||
|
||||
ThemeDeleted?.Invoke(this, id);
|
||||
Messenger.Default.Send(new ThemeDeletedMessage(id));
|
||||
}
|
||||
|
||||
public ApplicationSettings GetApplicationSettings()
|
||||
@@ -126,6 +223,10 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
return _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
|
||||
}
|
||||
public SshProfile GetSshProfile(Guid id)
|
||||
{
|
||||
return _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
|
||||
}
|
||||
|
||||
public Guid GetDefaultShellProfileId()
|
||||
{
|
||||
@@ -149,7 +250,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()
|
||||
{
|
||||
return _sshProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x))
|
||||
.Select(MoshBackwardCompatibilityFixProfile).Cast<SshProfile>();
|
||||
}
|
||||
|
||||
public IEnumerable<ShellProfile> GetAllProfiles()
|
||||
{
|
||||
return GetShellProfiles().Union(GetSshProfiles());
|
||||
}
|
||||
|
||||
private ShellProfile MoshBackwardCompatibilityFixProfile(ShellProfile profile)
|
||||
{
|
||||
var fixedProfile = MoshBackwardCompatibility.FixProfile(profile);
|
||||
|
||||
if (ReferenceEquals(fixedProfile, profile))
|
||||
{
|
||||
// Nothing changed
|
||||
return fixedProfile;
|
||||
}
|
||||
|
||||
if (fixedProfile is SshProfile sshProfile)
|
||||
{
|
||||
DeleteSshProfile(profile.Id);
|
||||
SaveSshProfile(sshProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
DeleteShellProfile(profile.Id);
|
||||
SaveShellProfile(fixedProfile);
|
||||
}
|
||||
|
||||
return fixedProfile;
|
||||
}
|
||||
|
||||
public IEnumerable<TabTheme> GetTabThemes()
|
||||
@@ -179,36 +316,43 @@ 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);
|
||||
|
||||
Messenger.Default.Send(new DefaultShellProfileChangedMessage(id));
|
||||
}
|
||||
|
||||
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(Command), command))
|
||||
if (!Enum.TryParse<Command>(command, true, out var enumValue))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_keyBindings.WriteValueAsJson(command.ToString(), keyBindings);
|
||||
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
|
||||
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
}
|
||||
|
||||
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
|
||||
@@ -216,18 +360,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.
|
||||
Messenger.Default.Send(new KeyBindingsChangedMessage());
|
||||
|
||||
if (newShell)
|
||||
{
|
||||
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)
|
||||
@@ -236,12 +401,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,40 +1,166 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.App.Services.Exceptions;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Requests;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Collections;
|
||||
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 Dictionary<int, Action<string>> _terminalOutputHandlers;
|
||||
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers;
|
||||
// ReSharper disable once RedundantDefaultMemberInitializer
|
||||
private byte _nextTerminalId = 0;
|
||||
|
||||
public event EventHandler<int> TerminalExited;
|
||||
public event EventHandler<TerminalExitStatus> TerminalExited;
|
||||
|
||||
public TrayProcessCommunicationService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
|
||||
_terminalOutputHandlers = new Dictionary<byte, Action<byte[]>>();
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile)
|
||||
public byte GetNextTerminalId()
|
||||
{
|
||||
return _nextTerminalId++;
|
||||
}
|
||||
|
||||
public async Task<string> GetUserNameAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_userName))
|
||||
{
|
||||
// Returning the username from cache
|
||||
return _userName;
|
||||
}
|
||||
|
||||
var response = await GetResponseAsync<StringValueResponse>(new GetUserNameRequest()).ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_userName = response.Value;
|
||||
}
|
||||
|
||||
return _userName;
|
||||
}
|
||||
|
||||
public async Task SaveTextFileAsync(string path, string content)
|
||||
{
|
||||
var response =
|
||||
await GetResponseAsync<CommonResponse>(new SaveTextFileRequest {Path = path, Content = content})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error)
|
||||
? "Failed to save the file."
|
||||
: response.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetSshConfigDirAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_sshConfigDir))
|
||||
{
|
||||
return _sshConfigDir;
|
||||
}
|
||||
|
||||
var response =
|
||||
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
|
||||
{ IncludeContent = false }).ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_sshConfigDir = response.Path;
|
||||
}
|
||||
|
||||
return _sshConfigDir;
|
||||
}
|
||||
|
||||
public async Task<string[]> GetFilesFromSshConfigDirAsync()
|
||||
{
|
||||
var response =
|
||||
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
|
||||
{IncludeContent = true}).ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_sshConfigDir = response.Path;
|
||||
|
||||
return response.Files;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckFileExistsAsync(string path)
|
||||
{
|
||||
var response = await GetResponseAsync<CommonResponse>(new CheckFileExistsRequest {Path = path})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return response.Success;
|
||||
}
|
||||
|
||||
public Task MuteTerminalAsync(bool mute)
|
||||
{
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest {Mute = mute}));
|
||||
}
|
||||
|
||||
public void UpdateSettings(ApplicationSettings settings)
|
||||
{
|
||||
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
|
||||
SessionType sessionType)
|
||||
{
|
||||
var request = new CreateTerminalRequest
|
||||
{
|
||||
Id = id,
|
||||
Size = size,
|
||||
Profile = shellProfile
|
||||
Profile = shellProfile,
|
||||
SessionType = sessionType
|
||||
};
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
|
||||
|
||||
return JsonConvert.DeserializeObject<CreateTerminalResponse>(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)
|
||||
@@ -43,88 +169,132 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
_appServiceConnection.MessageReceived += OnMessageReceived;
|
||||
}
|
||||
|
||||
private void OnMessageReceived(object sender, IDictionary<string, string> e)
|
||||
private void OnMessageReceived(object sender, IDictionary<string, object> e)
|
||||
{
|
||||
var messageType = e[MessageKeys.Type];
|
||||
var messageType = (byte)e[MessageKeys.Type];
|
||||
var messageContent = e[MessageKeys.Content];
|
||||
|
||||
if (messageType == nameof(DisplayTerminalOutputRequest))
|
||||
switch (messageType)
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<DisplayTerminalOutputRequest>(messageContent);
|
||||
case Constants.TerminalBufferRequestIdentifier:
|
||||
var terminalId = (byte)e[MessageKeys.TerminalId];
|
||||
|
||||
if (_terminalOutputHandlers.ContainsKey(request.TerminalId))
|
||||
{
|
||||
_terminalOutputHandlers[request.TerminalId].Invoke(request.Output);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("output was not handled: " + request.Output);
|
||||
}
|
||||
}
|
||||
else if (messageType == nameof(TerminalExitedRequest))
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
|
||||
if (_terminalOutputHandlers.ContainsKey(terminalId))
|
||||
{
|
||||
_terminalOutputHandlers[terminalId].Invoke((byte[])messageContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId);
|
||||
}
|
||||
break;
|
||||
case (byte) MessageIdentifiers.TerminalExitedRequest:
|
||||
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent);
|
||||
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
|
||||
|
||||
TerminalExited?.Invoke(this, request.TerminalId);
|
||||
TerminalExited?.Invoke(this, request.ToStatus());
|
||||
break;
|
||||
default:
|
||||
Logger.Instance.Error("Received unknown message type: {messageType}", messageType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Task ResizeTerminal(int id, TerminalSize size)
|
||||
public Task 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(int terminalId, Action<string> callback)
|
||||
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
|
||||
{
|
||||
_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
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(new SetToggleWindowKeyBindingsRequest
|
||||
{KeyBindings = keyBindings}));
|
||||
}
|
||||
|
||||
public Task WriteAsync(byte id, byte[] data)
|
||||
{
|
||||
var message = new ValueSet
|
||||
{
|
||||
KeyBindings = keyBindings
|
||||
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
|
||||
[MessageKeys.TerminalId] = id,
|
||||
[MessageKeys.Content] = data
|
||||
};
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(message);
|
||||
}
|
||||
|
||||
public Task CloseTerminalAsync(byte terminalId)
|
||||
{
|
||||
var request = new TerminalExitedRequest(terminalId, -1);
|
||||
|
||||
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
public Task WriteText(int id, string text)
|
||||
public async Task<string> GetCommandPathAsync(string command)
|
||||
{
|
||||
var request = new WriteTextRequest
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
TerminalId = id,
|
||||
Text = text
|
||||
};
|
||||
throw new ArgumentException("Input value is null or empty.", nameof(command));
|
||||
}
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
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 CloseTerminal(int terminalId)
|
||||
public Task QuitApplicationAsync()
|
||||
{
|
||||
var request = new TerminalExitedRequest
|
||||
{
|
||||
TerminalId = terminalId
|
||||
};
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(new QuitApplicationRequest()));
|
||||
}
|
||||
|
||||
private IDictionary<string, string> CreateMessage(object content)
|
||||
private async Task<T> GetResponseAsync<T>(IMessage request)
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
[MessageKeys.Type] = content.GetType().Name,
|
||||
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
|
||||
};
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public sealed class Logger
|
||||
{
|
||||
public enum LogLevel
|
||||
{
|
||||
Verbose = 0,
|
||||
Debug = 1,
|
||||
Information = 2,
|
||||
Warning = 3,
|
||||
Error = 4,
|
||||
Fatal = 5
|
||||
}
|
||||
|
||||
public class Configuration
|
||||
{
|
||||
public LogLevel LogLevel { get; set; } = LogLevel.Error;
|
||||
}
|
||||
|
||||
public static Logger Instance { get; } = new Logger();
|
||||
|
||||
private Logger()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Initialize(string filePath, Configuration configuration)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Async(a => a.File(filePath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7))
|
||||
.MinimumLevel.Is((LogEventLevel)configuration.LogLevel)
|
||||
.CreateLogger();
|
||||
|
||||
// Log file is not created during unhandled exception processing.
|
||||
// Force log file creation with the highest severity initialization record.
|
||||
Log.Fatal("Initialized");
|
||||
}
|
||||
|
||||
public void Debug(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Debug(text, propertyValues, propertyValues);
|
||||
}
|
||||
|
||||
public void Information(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Information(text, propertyValues);
|
||||
}
|
||||
|
||||
public void Warning(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Warning(text, propertyValues);
|
||||
}
|
||||
|
||||
public void Error(string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Error(text, propertyValues);
|
||||
}
|
||||
|
||||
public void Error(Exception exception, string text, params object[] propertyValues)
|
||||
{
|
||||
Log.Error(exception, text, propertyValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public class Terminal
|
||||
{
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private Func<Task<string>> _selectedTextCallback;
|
||||
private bool _closingFromUi;
|
||||
private bool _exited;
|
||||
private readonly bool _requireShellProcessStart;
|
||||
private string _fallbackTitle;
|
||||
|
||||
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId();
|
||||
_requireShellProcessStart = !terminalId.HasValue;
|
||||
}
|
||||
|
||||
private void OnTerminalExited(object sender, TerminalExitStatus status)
|
||||
{
|
||||
if (status.TerminalId != Id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_exited = true;
|
||||
Exited?.Invoke(this, status.ExitCode);
|
||||
|
||||
if (_closingFromUi || status.ExitCode <= 0)
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
|
||||
_trayProcessCommunicationService.TerminalExited -= OnTerminalExited;
|
||||
}
|
||||
|
||||
public event EventHandler<int> Exited;
|
||||
|
||||
/// <summary>
|
||||
/// To be observed by both view and viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler Closed;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<string> KeyboardCommandReceived;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by view
|
||||
/// </summary>
|
||||
public event EventHandler<byte[]> OutputReceived;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<TerminalSize> SizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<string> TitleChanged;
|
||||
|
||||
public byte Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// To be called by either view or viewmodel
|
||||
/// </summary>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
if (_exited)
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
_closingFromUi = true;
|
||||
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
|
||||
return _trayProcessCommunicationService.CloseTerminalAsync(Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by viewmodel
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<string> GetSelectedText()
|
||||
{
|
||||
return _selectedTextCallback?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view
|
||||
/// </summary>
|
||||
public void ProcessKeyboardCommand(string command)
|
||||
{
|
||||
KeyboardCommandReceived?.Invoke(this, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be set by view
|
||||
/// </summary>
|
||||
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
|
||||
{
|
||||
_selectedTextCallback = selectedTextCallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be called by view
|
||||
/// </summary>
|
||||
public async Task SetSizeAsync(TerminalSize size)
|
||||
{
|
||||
await _trayProcessCommunicationService.ResizeTerminalAsync(Id, size).ConfigureAwait(false);
|
||||
SizeChanged?.Invoke(this, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view
|
||||
/// </summary>
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
title = _fallbackTitle;
|
||||
}
|
||||
TitleChanged?.Invoke(this, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be called by view when ready
|
||||
/// </summary>
|
||||
public async Task<TerminalResponse> StartShellProcessAsync(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
|
||||
{
|
||||
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
|
||||
{
|
||||
OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState));
|
||||
}
|
||||
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
|
||||
|
||||
if (_requireShellProcessStart)
|
||||
{
|
||||
var response = await _trayProcessCommunicationService
|
||||
.CreateTerminalAsync(Id, size, shellProfile, sessionType).ConfigureAwait(false);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_fallbackTitle = response.ShellExecutableName;
|
||||
SetTitle(_fallbackTitle);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
else
|
||||
{
|
||||
return await _trayProcessCommunicationService.PauseTerminalOutputAsync(Id, false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task Write(byte[] data)
|
||||
{
|
||||
return _trayProcessCommunicationService.WriteAsync(Id, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells the ViewModel that this view failed to launch and can be closed.
|
||||
/// </summary>
|
||||
public void ReportLauchFailed()
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace FluentTerminal.App.Services.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Get strings in different languages.
|
||||
/// Author: Zuozishi
|
||||
/// </summary>
|
||||
public static class I18N
|
||||
{
|
||||
private static readonly ResourceLoader ResourceLoader = ResourceLoader.GetForViewIndependentUse();
|
||||
|
||||
public static string Translate(string resource)
|
||||
{
|
||||
return ResourceLoader.GetString(resource.Replace('.', '/'));
|
||||
}
|
||||
|
||||
public static string TranslateWithFallback(string resource, string fallback)
|
||||
{
|
||||
var result = Translate(resource);
|
||||
|
||||
return string.IsNullOrEmpty(result) ? fallback : result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,13 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fleck" Version="1.0.3" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
|
||||
<PackageReference Include="ReactiveUI" Version="9.4.1" />
|
||||
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -15,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,20 @@
|
||||
using System;
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public interface ITerminalView : IDisposable
|
||||
{
|
||||
Task ChangeThemeAsync(TerminalTheme theme);
|
||||
Task ChangeKeyBindingsAsync();
|
||||
Task ChangeOptionsAsync(TerminalOptions options);
|
||||
Task InitializeAsync(TerminalViewModel viewModel);
|
||||
void DisposalPrepare();
|
||||
Task FindNextAsync(string searchText);
|
||||
Task FindPreviousAsync(string searchText);
|
||||
Task FocusTerminalAsync();
|
||||
Task<string> SerializeXtermStateAsync();
|
||||
Task PasteAsync(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,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class AppMenuViewModel : ViewModelBase
|
||||
{
|
||||
public ObservableCollection<MenuItemViewModelBase> Items { get; }
|
||||
|
||||
public AppMenuViewModel(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(AppMenuViewModel 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,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, object 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,147 @@
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Menu
|
||||
{
|
||||
public class MenuItemKeyBindingViewModel : ViewModelBase
|
||||
{
|
||||
#region Properties
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructors
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Methods
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -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, object 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,84 @@
|
||||
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 object _icon;
|
||||
|
||||
/// <summary>
|
||||
/// Defines menu item icon.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Acceptable values of this property are:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.iconelement">IconElement</see>
|
||||
/// (usually <see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.symbolicon">SymbolIcon</see>
|
||||
/// or <see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.fonticon">FontIcon</see>);</item>
|
||||
/// <item><see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.symbol">Symbol</see> enum value,
|
||||
/// which will be used for creating
|
||||
/// <see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.symbolicon">SymbolIcon</see>;</item>
|
||||
/// <item><see cref="int"/>, in which case it'll be converted to
|
||||
/// <see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.symbol">Symbol</see> enum value;</item>
|
||||
/// <item><see cref="string"/>, in which case a new
|
||||
/// <see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.fonticon">FontIcon</see> instance will
|
||||
/// be created, whose
|
||||
/// <see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.fonticon.glyph">Glyph</see> property
|
||||
/// will be set to the value of this property.
|
||||
/// <see cref="!:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.fonticon.fontfamily">FontFamily</see>
|
||||
/// property of the newly created instance will be set to <c>"Segoe MDL2 Assets"</c>.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public object Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => Set(ref _icon, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected MenuItemViewModelBase(string text, string description, object 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,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,53 @@
|
||||
using GalaSoft.MvvmLight;
|
||||
using System;
|
||||
using Windows.UI.Xaml;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class OverlayViewModel : ViewModelBase
|
||||
{
|
||||
private readonly DispatcherTimer _overlayTimer;
|
||||
private bool _showOverlay;
|
||||
private string _overlayContent;
|
||||
|
||||
// Important! The constructor has to be called from the UI thread.
|
||||
public OverlayViewModel()
|
||||
{
|
||||
_overlayTimer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 2)};
|
||||
}
|
||||
|
||||
public bool ShowOverlay
|
||||
{
|
||||
get => _showOverlay;
|
||||
set => Set(ref _showOverlay, value);
|
||||
}
|
||||
|
||||
public string OverlayContent
|
||||
{
|
||||
get => _overlayContent;
|
||||
set => Set(ref _overlayContent, value);
|
||||
}
|
||||
|
||||
public void Show(string message)
|
||||
{
|
||||
OverlayContent = message;
|
||||
ShowOverlay = true;
|
||||
|
||||
if (_overlayTimer.IsEnabled)
|
||||
{
|
||||
_overlayTimer.Stop();
|
||||
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
|
||||
}
|
||||
_overlayTimer.Start();
|
||||
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
|
||||
}
|
||||
|
||||
private void OnResizeOverlayTimerFinished(object sender, object e)
|
||||
{
|
||||
_overlayTimer.Stop();
|
||||
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
|
||||
ShowOverlay = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
|
||||
/// </summary>
|
||||
public abstract class ProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
protected readonly IDialogService DialogService;
|
||||
protected readonly ISettingsService SettingsService;
|
||||
|
||||
private List<KeyBinding> _bindingsBeforeEdit;
|
||||
private Dictionary<string, string> _environmentVariablesBeforeEdit;
|
||||
private string _nameBeforeEdit;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
protected bool IsNew { get; private set; }
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
private bool _inEditMode;
|
||||
|
||||
public bool InEditMode
|
||||
{
|
||||
get => _inEditMode;
|
||||
set => Set(ref _inEditMode, value);
|
||||
}
|
||||
|
||||
private string _name;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(ref _name, value);
|
||||
}
|
||||
|
||||
public KeyBindingsViewModel KeyBindings { get; }
|
||||
|
||||
public ObservableCollection<EnvironmentVariableViewModel> EnvironmentVariables { get; } = new ObservableCollection<EnvironmentVariableViewModel>();
|
||||
|
||||
public T ProfileVm { get; protected set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Events
|
||||
|
||||
// Needs to be triggered from UI thread
|
||||
public event EventHandler Deleted;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand CancelEditCommand { get; }
|
||||
|
||||
public IAsyncCommand DeleteCommand { get; }
|
||||
|
||||
public RelayCommand EditCommand { get; }
|
||||
|
||||
public IAsyncCommand SaveChangesCommand { get; }
|
||||
|
||||
public IAsyncCommand AddKeyboardShortcutCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected ProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
DialogService = dialogService;
|
||||
IsNew = isNew;
|
||||
|
||||
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), dialogService, string.Empty, false);
|
||||
|
||||
foreach (var environmentVariable in shellProfile.EnvironmentVariables)
|
||||
{
|
||||
EnvironmentVariables.Add(new EnvironmentVariableViewModel
|
||||
{
|
||||
Name = environmentVariable.Key,
|
||||
Value = environmentVariable.Value
|
||||
});
|
||||
}
|
||||
|
||||
Initialize(shellProfile);
|
||||
|
||||
DeleteCommand = new AsyncCommand(DeleteAsync, CanDelete);
|
||||
EditCommand = new RelayCommand(Edit);
|
||||
CancelEditCommand = new AsyncCommand(CancelEditAsync);
|
||||
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
|
||||
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
// Requires UI thread
|
||||
// Loads view model properties from the input shellProfile
|
||||
protected void Initialize(ShellProfile shellProfile)
|
||||
{
|
||||
Id = shellProfile.Id;
|
||||
Name = shellProfile.Name;
|
||||
|
||||
KeyBindings.Clear();
|
||||
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool HasChanges()
|
||||
{
|
||||
return ProfileVm.HasChanges()
|
||||
|| !_nameBeforeEdit.NullableEqualTo(_name)
|
||||
|| !(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ?? false)
|
||||
|| !(_environmentVariablesBeforeEdit?.SequenceEqual(EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value)) ?? false);
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we need to trigger Deleted event from the calling (UI) thread.
|
||||
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Deleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool CanDelete();
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
if (InEditMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bindingsBeforeEdit = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
_environmentVariablesBeforeEdit = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
|
||||
_nameBeforeEdit = _name;
|
||||
|
||||
KeyBindings.Editable = true;
|
||||
InEditMode = true;
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task CancelEditAsync()
|
||||
{
|
||||
if (IsNew)
|
||||
{
|
||||
await DeleteAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasChanges())
|
||||
{
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards
|
||||
if (await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
|
||||
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
|
||||
.ConfigureAwait(true) == DialogButton.OK)
|
||||
{
|
||||
// Cancelled, so rollback
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards
|
||||
await ProfileVm.RejectChangesAsync().ConfigureAwait(true);
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task SaveChangesAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var error = await ProfileVm.AcceptChangesAsync().ConfigureAwait(true);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"), error, DialogButton.OK)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnvironmentVariables.Select(x => x.Name).Any(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
|
||||
I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
|
||||
{
|
||||
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
|
||||
I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var profile = ProfileVm.Model;
|
||||
|
||||
profile.Id = Id;
|
||||
profile.Name = _name;
|
||||
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
profile.EnvironmentVariables = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
|
||||
|
||||
if (profile is SshProfile sshProfile)
|
||||
{
|
||||
SettingsService.SaveSshProfile(sshProfile, IsNew);
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingsService.SaveShellProfile(profile, IsNew);
|
||||
}
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
IsNew = false;
|
||||
}
|
||||
|
||||
private Task AddKeyboardShortcut()
|
||||
{
|
||||
return KeyBindings.ShowAddKeyBindingDialogAsync();
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for common profiles (PowerShell, CMD, WSL).
|
||||
/// </summary>
|
||||
public class CommonProfileProviderViewModel : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
public bool PreInstalled { get; private set; }
|
||||
|
||||
private string _location;
|
||||
|
||||
public string Location
|
||||
{
|
||||
get => _location;
|
||||
set => Set(ref _location, value);
|
||||
}
|
||||
|
||||
private string _workingDirectory;
|
||||
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get => _workingDirectory;
|
||||
set => Set(ref _workingDirectory, value);
|
||||
}
|
||||
|
||||
private string _arguments;
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => _arguments;
|
||||
set => Set(ref _arguments, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForCustomShellCommand { get; }
|
||||
|
||||
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructor
|
||||
|
||||
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
|
||||
true, original)
|
||||
{
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
|
||||
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
|
||||
|
||||
Initialize(Model);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
private void Initialize(ShellProfile profile)
|
||||
{
|
||||
PreInstalled = profile.PreInstalled;
|
||||
Location = profile.Location;
|
||||
Arguments = profile.Arguments;
|
||||
WorkingDirectory = profile.WorkingDirectory;
|
||||
}
|
||||
|
||||
protected override void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
base.LoadFromProfile(profile);
|
||||
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected override async Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
|
||||
|
||||
profile.Location = _location;
|
||||
profile.Arguments = _arguments;
|
||||
profile.WorkingDirectory = _workingDirectory;
|
||||
}
|
||||
|
||||
public override async Task<string> ValidateAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_location))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
error = I18N.Translate("LocationCantBeEmpty");
|
||||
|
||||
return string.IsNullOrEmpty(error) ? "Location cannot be empty." : error;
|
||||
}
|
||||
|
||||
public override bool HasChanges()
|
||||
{
|
||||
return base.HasChanges() || !Model.Location.NullableEqualTo(_location) ||
|
||||
!Model.WorkingDirectory.NullableEqualTo(_workingDirectory) ||
|
||||
!Model.Arguments.NullableEqualTo(_arguments);
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task BrowseForCustomShell()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var file = await _fileSystemService.OpenFileAsync(new[] { ".exe" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
Location = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task BrowseForWorkingDirectory()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var directory = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
|
||||
if (directory != null)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Windows.UI.Core;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Messages;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all profile view model classes. It contains properties shared by all other view models
|
||||
/// (theme-related properties, line-ending translation, and WinPTY/ConPTY selection).
|
||||
/// </summary>
|
||||
public abstract class ProfileProviderViewModelBase : ViewModelBase
|
||||
{
|
||||
#region Static
|
||||
|
||||
private static readonly LineEndingStyle[] LineEndingStylesArray =
|
||||
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Fields
|
||||
|
||||
protected readonly ISettingsService SettingsService;
|
||||
protected readonly IApplicationView ApplicationView;
|
||||
private readonly bool _strictProfileType;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
private ShellProfile _model;
|
||||
|
||||
public ShellProfile Model
|
||||
{
|
||||
get => _model;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (ReferenceEquals(_model, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_strictProfileType && _model != null && _model.GetType() != value.GetType())
|
||||
{
|
||||
throw new ArgumentException($"The value has to be of type {_model.GetType().Name}.");
|
||||
}
|
||||
|
||||
_model = value;
|
||||
|
||||
LoadFromProfile(value);
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
|
||||
|
||||
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
|
||||
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
|
||||
|
||||
private LineEndingStyle _lineEndingTranslation;
|
||||
|
||||
public LineEndingStyle LineEndingTranslation
|
||||
{
|
||||
get => _lineEndingTranslation;
|
||||
set => Set(ref _lineEndingTranslation, value);
|
||||
}
|
||||
|
||||
private TabTheme _selectedTabTheme;
|
||||
|
||||
public TabTheme SelectedTabTheme
|
||||
{
|
||||
get => _selectedTabTheme ?? (_selectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(_tabThemeId)));
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// Ignore attempt setting null.
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that it's from the list
|
||||
var theme = TabThemes.Contains(value)
|
||||
? value
|
||||
: TabThemes.FirstOrDefault(t => t.Id == value.Id) ?? TabThemes.First();
|
||||
|
||||
if (Set(ref _selectedTabTheme, theme))
|
||||
{
|
||||
_tabThemeId = theme.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _tabThemeId;
|
||||
|
||||
public int TabThemeId
|
||||
{
|
||||
get => _tabThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _tabThemeId, value))
|
||||
{
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalTheme _selectedTerminalTheme;
|
||||
|
||||
public TerminalTheme SelectedTerminalTheme
|
||||
{
|
||||
get => _selectedTerminalTheme ??
|
||||
(_selectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(_terminalThemeId)));
|
||||
set
|
||||
{
|
||||
TerminalTheme theme = value;
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
// How to handle attempt to setting null theme?
|
||||
// 1) Throw an exception:
|
||||
//throw new ArgumentNullException();
|
||||
// 2) Ignore the attempt:
|
||||
//return;
|
||||
// 3) Use default theme (probably the best):
|
||||
theme = TerminalThemes.First();
|
||||
}
|
||||
else if (!TerminalThemes.Contains(theme))
|
||||
{
|
||||
// Ensure that it's from the list
|
||||
theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(theme.Id)) ?? TerminalThemes.First();
|
||||
}
|
||||
|
||||
if (Set(ref _selectedTerminalTheme, theme))
|
||||
{
|
||||
_terminalThemeId = theme.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Guid _terminalThemeId;
|
||||
|
||||
public Guid TerminalThemeId
|
||||
{
|
||||
get => _terminalThemeId;
|
||||
set
|
||||
{
|
||||
if (Set(ref _terminalThemeId, value))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _useConPty;
|
||||
|
||||
public bool UseConPty
|
||||
{
|
||||
get => _useConPty;
|
||||
set => Set(ref _useConPty, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Constructor
|
||||
|
||||
protected ProfileProviderViewModelBase(ISettingsService settingsService, IApplicationView applicationView,
|
||||
bool strictProfileType, ShellProfile original = null)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
ApplicationView = applicationView;
|
||||
_strictProfileType = strictProfileType;
|
||||
|
||||
_model = original ?? new ShellProfile();
|
||||
|
||||
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
|
||||
|
||||
SelectedTabTheme = TabThemes.First();
|
||||
|
||||
TerminalThemes = new ObservableCollection<TerminalTheme>(settingsService.GetThemes());
|
||||
|
||||
TerminalThemes.Insert(0, new TerminalTheme {Id = Guid.Empty, Name = "Default"});
|
||||
|
||||
SelectedTerminalTheme = TerminalThemes.First();
|
||||
|
||||
MessengerInstance.Register<ThemeAddedMessage>(this, OnThemeAdded);
|
||||
MessengerInstance.Register<ThemeDeletedMessage>(this, OnThemeDeleted);
|
||||
|
||||
Initialize(Model);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
private void OnThemeDeleted(ThemeDeletedMessage message)
|
||||
{
|
||||
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(message.ThemeId));
|
||||
|
||||
if (theme == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
TerminalThemes.Remove(theme);
|
||||
|
||||
if (_terminalThemeId.Equals(message.ThemeId))
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.First();
|
||||
}
|
||||
}, CoreDispatcherPriority.Low, true);
|
||||
}
|
||||
|
||||
private void OnThemeAdded(ThemeAddedMessage message)
|
||||
{
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => TerminalThemes.Add(message.Theme), CoreDispatcherPriority.Low);
|
||||
}
|
||||
|
||||
private void Initialize(ShellProfile profile)
|
||||
{
|
||||
LineEndingTranslation = profile.LineEndingTranslation;
|
||||
UseConPty = profile.UseConPty;
|
||||
TerminalThemeId = profile.TerminalThemeId;
|
||||
TabThemeId = profile.TabThemeId;
|
||||
}
|
||||
|
||||
protected virtual void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
Initialize(profile);
|
||||
}
|
||||
|
||||
protected virtual Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
profile.LineEndingTranslation = _lineEndingTranslation;
|
||||
profile.UseConPty = _useConPty;
|
||||
profile.TerminalThemeId = _terminalThemeId;
|
||||
profile.TabThemeId = _tabThemeId;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task<string> ValidateAsync()
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if view model values aren't equal to the corresponding values of the
|
||||
/// underlying <see cref="ShellProfile"/>.
|
||||
/// </summary>
|
||||
public virtual bool HasChanges()
|
||||
{
|
||||
return Model.LineEndingTranslation != _lineEndingTranslation ||
|
||||
Model.UseConPty != _useConPty ||
|
||||
!Model.TerminalThemeId.Equals(_terminalThemeId) ||
|
||||
Model.TabThemeId != _tabThemeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies values from the view model to the underlying <see cref="ShellProfile"/>.
|
||||
/// </summary>
|
||||
/// <param name="acceptIfInvalid">Defines what will happen if the view model data is invalid (if
|
||||
/// <see cref="ValidateAsync"/> returns non-empty string). If this parameter is set to <c>true</c>, the
|
||||
/// data will be copied to the underlying <see cref="ShellProfile"/> anyway. If it's set to <c>false</c>,
|
||||
/// (default) the underlying <see cref="ShellProfile"/> won't be changed at all.</param>
|
||||
/// <returns><c>null</c> or empty string if the operation was successful (the data is valid), or an error
|
||||
/// message returned by <see cref="ValidateAsync"/> method.</returns>
|
||||
public async Task<string> AcceptChangesAsync(bool acceptIfInvalid = false)
|
||||
{
|
||||
var error = await ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (acceptIfInvalid || string.IsNullOrEmpty(error))
|
||||
{
|
||||
await CopyToProfileAsync(Model).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public Task RejectChangesAsync()
|
||||
{
|
||||
return ApplicationView.ExecuteOnUiThreadAsync(() => LoadFromProfile(Model));
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Links/shortcuts related
|
||||
|
||||
private const string UseConPtyQueryStringName = "conpty";
|
||||
private const string LineEndingQueryStringName = "lineending";
|
||||
private const string TerminalThemeIdQueryStringName = "theme";
|
||||
private const string TabThemeIdQueryStringName = "tab";
|
||||
|
||||
private const string ShortcutFileFormat = @"[{{000214A0-0000-0000-C000-000000000046}}]
|
||||
Prop3=19,0
|
||||
[InternetShortcut]
|
||||
IDList=
|
||||
URL={0}
|
||||
";
|
||||
|
||||
public static string GetShortcutFileContent(string url) => string.Format(ShortcutFileFormat, url);
|
||||
|
||||
public static IEnumerable<Tuple<string, string>> ParseParams(string uriOpts, char separator) =>
|
||||
uriOpts.Split(separator).Select(ParseSshOptionFromUri).Where(p => p != null);
|
||||
|
||||
private static Tuple<string, string> ParseSshOptionFromUri(string option)
|
||||
{
|
||||
string[] nv = option.Split('=');
|
||||
|
||||
if (nv.Length != 2 || string.IsNullOrEmpty(nv[0]))
|
||||
{
|
||||
// For now simply ignore invalid options
|
||||
return null;
|
||||
//throw new FormatException($"Invalid SSH option '{option}'.");
|
||||
}
|
||||
|
||||
return Tuple.Create(HttpUtility.UrlDecode(nv[0]), HttpUtility.UrlDecode(nv[1]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates URL.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Tuple"/> whose first item represents success. If the first item is <c>true</c>,
|
||||
/// the second contains the URL. If the first item is <c>false</c>, the second contains error message.</returns>
|
||||
public virtual Task<Tuple<bool, string>> GetUrlAsync() =>
|
||||
Task.FromResult(Tuple.Create(false, "Not supported."));
|
||||
|
||||
public string GetBaseQueryString()
|
||||
{
|
||||
var queryString =
|
||||
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
|
||||
|
||||
if (_tabThemeId != TabThemes.First().Id)
|
||||
{
|
||||
queryString += $"&{TabThemeIdQueryStringName}={_tabThemeId:##########}";
|
||||
}
|
||||
|
||||
if (!_terminalThemeId.Equals(TerminalThemes.First().Id))
|
||||
{
|
||||
queryString += $"&{TerminalThemeIdQueryStringName}={_terminalThemeId}";
|
||||
}
|
||||
|
||||
return queryString;
|
||||
}
|
||||
|
||||
public void LoadBaseFromQueryString(IList<Tuple<string, string>> queryStringParams)
|
||||
{
|
||||
if (queryStringParams == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
UseConPtyQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useConPty))
|
||||
{
|
||||
UseConPty = useConPty;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
LineEndingQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && Enum.TryParse(keyValue.Item2, true, out LineEndingStyle lineEndingTranslation))
|
||||
{
|
||||
LineEndingTranslation = lineEndingTranslation;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
TabThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && int.TryParse(keyValue.Item2, out int tabThemeId))
|
||||
{
|
||||
TabThemeId = tabThemeId;
|
||||
}
|
||||
|
||||
keyValue = queryStringParams.FirstOrDefault(t =>
|
||||
TerminalThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(keyValue?.Item2) && Guid.TryParse(keyValue.Item2, out Guid terminalThemeId))
|
||||
{
|
||||
TerminalThemeId = terminalThemeId;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Links/shortcuts related
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
/// <summary>
|
||||
/// View-model for rich-UI based SSH profiles.
|
||||
/// </summary>
|
||||
public class SshConnectViewModel : ProfileProviderViewModelBase
|
||||
{
|
||||
#region Static
|
||||
|
||||
public static string GetErrorString(SshConnectionInfoValidationResult result, string separator = "; ") =>
|
||||
string.Join(separator, GetErrors(result));
|
||||
|
||||
public static IEnumerable<string> GetErrors(SshConnectionInfoValidationResult result)
|
||||
{
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var value in Enum.GetValues(typeof(SshConnectionInfoValidationResult))
|
||||
.Cast<SshConnectionInfoValidationResult>().Where(r => r != SshConnectionInfoValidationResult.Valid))
|
||||
{
|
||||
if ((value & result) == value)
|
||||
{
|
||||
yield return I18N.Translate($"{nameof(SshConnectionInfoValidationResult)}.{value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
// To prevent validating the existence of the same file multiple times because it's kinda expensive
|
||||
private string _validatedIdentityFile;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
private string _host;
|
||||
|
||||
public string Host
|
||||
{
|
||||
get => _host;
|
||||
set => Set(ref _host, value);
|
||||
}
|
||||
|
||||
private ushort _sshPort;
|
||||
|
||||
public ushort SshPort
|
||||
{
|
||||
get => _sshPort;
|
||||
set => Set(ref _sshPort, value);
|
||||
}
|
||||
|
||||
private string _username;
|
||||
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => Set(ref _username, value);
|
||||
}
|
||||
|
||||
private string _identityFile;
|
||||
|
||||
public string IdentityFile
|
||||
{
|
||||
get => _identityFile;
|
||||
set => Set(ref _identityFile, value);
|
||||
}
|
||||
|
||||
private bool _useMosh;
|
||||
|
||||
public bool UseMosh
|
||||
{
|
||||
get => _useMosh;
|
||||
set => Set(ref _useMosh, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortFrom;
|
||||
|
||||
public ushort MoshPortFrom
|
||||
{
|
||||
get => _moshPortFrom;
|
||||
set => Set(ref _moshPortFrom, value);
|
||||
}
|
||||
|
||||
private ushort _moshPortTo;
|
||||
|
||||
public ushort MoshPortTo
|
||||
{
|
||||
get => _moshPortTo;
|
||||
set => Set(ref _moshPortTo, value);
|
||||
}
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand BrowseForIdentityFileCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constructor
|
||||
|
||||
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
|
||||
SshProfile original = null) : base(settingsService, applicationView, true,
|
||||
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault,
|
||||
RequestConPty = settingsService.GetApplicationSettings().UseConPty
|
||||
})
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
Initialize((SshProfile)Model);
|
||||
|
||||
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFileAsync);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
// Fills the view model properties from the input sshProfile
|
||||
private void Initialize(SshProfile sshProfile)
|
||||
{
|
||||
Host = sshProfile.Host;
|
||||
SshPort = sshProfile.SshPort;
|
||||
|
||||
Username = sshProfile.Username;
|
||||
|
||||
if (string.IsNullOrEmpty(Username))
|
||||
{
|
||||
_trayProcessCommunicationService.GetUserNameAsync().ContinueWith(t =>
|
||||
{
|
||||
var username = t.Result;
|
||||
|
||||
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
|
||||
{
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => Username = username);
|
||||
}
|
||||
}, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
|
||||
IdentityFile = sshProfile.IdentityFile;
|
||||
UseMosh = sshProfile.UseMosh;
|
||||
MoshPortFrom = sshProfile.MoshPortFrom;
|
||||
MoshPortTo = sshProfile.MoshPortTo;
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task BrowseForIdentityFileAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var file = await _fileSystemService.OpenFileAsync(new[] { "*" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
IdentityFile = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetArgumentsString()
|
||||
{
|
||||
var sshArguments = GetSshArguments();
|
||||
|
||||
if (!_useMosh)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(sshArguments) ? $"{_username}@{_host}" : $"{sshArguments} {_username}@{_host}";
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(sshArguments)
|
||||
? $"-p {_moshPortFrom}:{_moshPortTo} {_username}@{_host}"
|
||||
: $"-p {_moshPortFrom}:{_moshPortTo} --ssh=\"ssh {sshArguments}\" {_username}@{_host}";
|
||||
}
|
||||
|
||||
private string GetSshArguments()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
return _sshPort == SshProfile.DefaultSshPort ? null : $"-p {_sshPort:#####}";
|
||||
}
|
||||
|
||||
return _sshPort == SshProfile.DefaultSshPort
|
||||
? $"-i '{_identityFile}'"
|
||||
: $"-p {_sshPort:#####} -i '{_identityFile}'";
|
||||
}
|
||||
|
||||
protected override void LoadFromProfile(ShellProfile profile)
|
||||
{
|
||||
base.LoadFromProfile(profile);
|
||||
|
||||
Initialize((SshProfile)profile);
|
||||
}
|
||||
|
||||
protected override async Task CopyToProfileAsync(ShellProfile profile)
|
||||
{
|
||||
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
|
||||
|
||||
var sshProfile = (SshProfile) profile;
|
||||
|
||||
sshProfile.Location = _useMosh ? Constants.MoshCommandName : Constants.SshCommandName;
|
||||
sshProfile.Arguments = GetArgumentsString();
|
||||
|
||||
sshProfile.WorkingDirectory = null;
|
||||
|
||||
sshProfile.Host = _host;
|
||||
sshProfile.SshPort = _sshPort;
|
||||
sshProfile.Username = _username;
|
||||
sshProfile.IdentityFile = _identityFile;
|
||||
sshProfile.UseMosh = _useMosh;
|
||||
sshProfile.MoshPortFrom = _moshPortFrom;
|
||||
sshProfile.MoshPortTo = _moshPortTo;
|
||||
}
|
||||
|
||||
public override async Task<string> ValidateAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
|
||||
|
||||
if (result == SshConnectionInfoValidationResult.Valid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
error = GetErrorString(result, Environment.NewLine);
|
||||
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
error = "Invalid input.";
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public override bool HasChanges()
|
||||
{
|
||||
var original = (SshProfile)Model;
|
||||
|
||||
return base.HasChanges() || !original.Host.NullableEqualTo(_host) || original.SshPort != _sshPort ||
|
||||
!original.Username.NullableEqualTo(_username) ||
|
||||
!original.IdentityFile.NullableEqualTo(_identityFile) || original.UseMosh != _useMosh ||
|
||||
original.MoshPortFrom != _moshPortFrom || original.MoshPortTo != _moshPortTo;
|
||||
}
|
||||
|
||||
private async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
|
||||
{
|
||||
var result = SshConnectionInfoValidationResult.Valid;
|
||||
|
||||
if (string.IsNullOrEmpty(_username))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.UsernameEmpty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_host))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.HostEmpty;
|
||||
}
|
||||
|
||||
if (_sshPort < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (!await CheckIdentityFileExistsAsync().ConfigureAwait(false))
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
|
||||
}
|
||||
|
||||
if (!_useMosh)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (_moshPortFrom < 1)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortZeroOrNegative;
|
||||
}
|
||||
|
||||
if (_moshPortFrom > _moshPortTo)
|
||||
{
|
||||
result |= SshConnectionInfoValidationResult.MoshPortRangeInvalid;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private async Task<bool> CheckIdentityFileExistsAsync()
|
||||
{
|
||||
var identityFile = _identityFile;
|
||||
|
||||
if (string.IsNullOrEmpty(identityFile) ||
|
||||
identityFile.Equals(_validatedIdentityFile, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Here we need to take into account that files from ssh config dir can be provided by name, without full path.
|
||||
string fullPath;
|
||||
|
||||
if (Path.IsPathRooted(identityFile))
|
||||
{
|
||||
fullPath = identityFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync().ConfigureAwait(true);
|
||||
|
||||
if (string.IsNullOrEmpty(sshConfigDir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
fullPath = Path.Combine(sshConfigDir, identityFile);
|
||||
}
|
||||
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath).ConfigureAwait(true))
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
|
||||
IdentityFile = fullPath;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SetValidatedIdentityFile(string identityFile)
|
||||
{
|
||||
_validatedIdentityFile = identityFile;
|
||||
IdentityFile = identityFile;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
|
||||
#region Links/shortcuts related
|
||||
|
||||
// Resources:
|
||||
// https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04#section-3
|
||||
// https://man.openbsd.org/ssh
|
||||
|
||||
private const string SshUriScheme = "ssh";
|
||||
private const string MoshUriScheme = "mosh";
|
||||
|
||||
// Constant derived from https://man.openbsd.org/ssh
|
||||
private const string IdentityFileOptionName = "IdentityFile";
|
||||
|
||||
private static readonly string[] ValidMoshPortsNames = { "mosh_ports", "mosh-ports" };
|
||||
|
||||
private static readonly Regex MoshRangeRx =
|
||||
new Regex(@"^(?<from>\d{1,5})[:-](?<to>\d{1,5})$", RegexOptions.Compiled);
|
||||
|
||||
public static bool CheckScheme(Uri uri) =>
|
||||
SshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase) ||
|
||||
MoshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static SshConnectViewModel ParseUri(Uri uri, ISettingsService settingsService,
|
||||
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
|
||||
{
|
||||
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService)
|
||||
{
|
||||
Host = uri.Host,
|
||||
UseMosh = MoshUriScheme.Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
|
||||
if (uri.Port >= 0)
|
||||
vm.SshPort = (ushort)uri.Port;
|
||||
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
string[] parts = uri.UserInfo.Split(';');
|
||||
|
||||
if (parts.Length > 2)
|
||||
throw new FormatException($"UserInfo part contains {parts.Length} elements.");
|
||||
|
||||
vm.Username = HttpUtility.UrlDecode(parts[0]);
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
// For now we are only interested in IdentityFile option
|
||||
Tuple<string, string> identityFileOption = ParseParams(parts[1], ',').FirstOrDefault(p =>
|
||||
string.Equals(p.Item1, IdentityFileOptionName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
vm.IdentityFile = identityFileOption?.Item2;
|
||||
}
|
||||
}
|
||||
|
||||
// ReSharper disable once ConstantConditionalAccessQualifier
|
||||
string queryString = uri.Query?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(queryString))
|
||||
{
|
||||
return vm;
|
||||
}
|
||||
|
||||
if (queryString.StartsWith("?", StringComparison.Ordinal))
|
||||
{
|
||||
queryString = queryString.Substring(1);
|
||||
}
|
||||
|
||||
var queryStringParams = ParseParams(queryString, '&').ToList();
|
||||
|
||||
var moshPorts = queryStringParams.FirstOrDefault(p =>
|
||||
ValidMoshPortsNames.Any(n => string.Equals(p.Item1, n, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (moshPorts != null)
|
||||
{
|
||||
Match match = MoshRangeRx.Match(moshPorts.Item2);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
vm.MoshPortFrom = ushort.Parse(match.Groups["from"].Value);
|
||||
vm.MoshPortTo = ushort.Parse(match.Groups["to"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
vm.LoadBaseFromQueryString(queryStringParams);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
public override async Task<Tuple<bool, string>> GetUrlAsync()
|
||||
{
|
||||
var error = await base.ValidateAsync().ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
|
||||
|
||||
if (result != SshConnectionInfoValidationResult.Valid &&
|
||||
// For links we can ignore missing username
|
||||
result != SshConnectionInfoValidationResult.UsernameEmpty)
|
||||
{
|
||||
|
||||
error = GetErrorString(result, Environment.NewLine);
|
||||
|
||||
if (string.IsNullOrEmpty(error))
|
||||
{
|
||||
error = "Invalid input.";
|
||||
}
|
||||
|
||||
return Tuple.Create(false, error);
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
|
||||
|
||||
sb.Append("://");
|
||||
|
||||
bool containsUserInfo = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
sb.Append(HttpUtility.UrlEncode(_username));
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
sb.Append(";");
|
||||
|
||||
if (!string.IsNullOrEmpty(_identityFile))
|
||||
{
|
||||
sb.Append($"{IdentityFileOptionName}={HttpUtility.UrlEncode(_identityFile)}");
|
||||
}
|
||||
|
||||
containsUserInfo = true;
|
||||
}
|
||||
|
||||
if (containsUserInfo)
|
||||
sb.Append("@");
|
||||
|
||||
sb.Append(_host);
|
||||
|
||||
if (_sshPort != SshProfile.DefaultSshPort)
|
||||
{
|
||||
sb.Append($":{_sshPort:#####}");
|
||||
}
|
||||
|
||||
sb.Append("/");
|
||||
|
||||
bool queryStringAdded = false;
|
||||
|
||||
if (_useMosh)
|
||||
{
|
||||
sb.Append(
|
||||
$"?{ValidMoshPortsNames[0]}={_moshPortFrom:#####}-{_moshPortTo:#####}");
|
||||
|
||||
queryStringAdded = true;
|
||||
}
|
||||
|
||||
var terminalInfoQueryString = GetBaseQueryString();
|
||||
|
||||
if (!string.IsNullOrEmpty(terminalInfoQueryString))
|
||||
{
|
||||
sb.Append($"{(queryStringAdded ? "&" : "?")}{terminalInfoQueryString}");
|
||||
}
|
||||
|
||||
return Tuple.Create(true, sb.ToString());
|
||||
}
|
||||
|
||||
#endregion Links/shortcuts related
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Profiles
|
||||
{
|
||||
[Flags]
|
||||
public enum SshConnectionInfoValidationResult
|
||||
{
|
||||
Valid = 0,
|
||||
UsernameEmpty = 1,
|
||||
HostEmpty = 2,
|
||||
SshPortZeroOrNegative = 4,
|
||||
MoshPortZeroOrNegative = 8,
|
||||
MoshPortRangeInvalid = 16,
|
||||
IdentityFileDoesNotExist = 32
|
||||
}
|
||||
}
|
||||
@@ -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,8 +1,10 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
@@ -15,39 +17,66 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IStartupTaskService _startupTaskService;
|
||||
private bool _canEnableStartupTask;
|
||||
private bool _editingNewTerminalLocation;
|
||||
private bool _editingTabsPosition;
|
||||
private bool _editingInactiveTabColorMode;
|
||||
private bool _startupTaskEnabled;
|
||||
private bool _shouldRestartForTrayMessage;
|
||||
private string _startupTaskErrorMessage;
|
||||
private bool _needsToRestart;
|
||||
private readonly IApplicationLanguageService _applicationLanguageService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
|
||||
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IStartupTaskService startupTaskService)
|
||||
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
|
||||
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_startupTaskService = startupTaskService;
|
||||
_applicationLanguageService = applicationLanguageService;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_fileSystemService = fileSystemService;
|
||||
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
|
||||
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectoryAsync);
|
||||
}
|
||||
|
||||
public async Task OnNavigatedTo()
|
||||
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
|
||||
|
||||
public bool NeedsToRestart
|
||||
{
|
||||
var startupTaskStatus = await _startupTaskService.GetStatus();
|
||||
get => _needsToRestart;
|
||||
set => Set(ref _needsToRestart, value);
|
||||
}
|
||||
|
||||
public string SelectedLanguage
|
||||
{
|
||||
get => _applicationLanguageService.GetCurrentLanguage();
|
||||
set
|
||||
{
|
||||
_applicationLanguageService.SetLanguage(value);
|
||||
NeedsToRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
public async Task OnNavigatedToAsync()
|
||||
{
|
||||
// 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
|
||||
public bool ShowCustomTitleInTitlebar
|
||||
{
|
||||
get => _applicationSettings.AlwaysShowTabs;
|
||||
get => _applicationSettings.ShowCustomTitleInTitlebar;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.AlwaysShowTabs != value)
|
||||
if (_applicationSettings.ShowCustomTitleInTitlebar != value)
|
||||
{
|
||||
_applicationSettings.AlwaysShowTabs = value;
|
||||
_applicationSettings.ShowCustomTitleInTitlebar = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
@@ -91,10 +120,123 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseMoshByDefault
|
||||
{
|
||||
get => _applicationSettings.UseMoshByDefault;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.UseMoshByDefault != value)
|
||||
{
|
||||
_applicationSettings.UseMoshByDefault = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoFallbackToWindowsUsernameInLinks
|
||||
{
|
||||
get => _applicationSettings.AutoFallbackToWindowsUsernameInLinks;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks != value)
|
||||
{
|
||||
_applicationSettings.AutoFallbackToWindowsUsernameInLinks = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RTrimCopiedLines
|
||||
{
|
||||
get => _applicationSettings.RTrimCopiedLines;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.RTrimCopiedLines != value)
|
||||
{
|
||||
_applicationSettings.RTrimCopiedLines = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool MuteTerminalBeeps
|
||||
{
|
||||
get => _applicationSettings.MuteTerminalBeeps;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.MuteTerminalBeeps != value)
|
||||
{
|
||||
_applicationSettings.MuteTerminalBeeps = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
|
||||
_trayProcessCommunicationService.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool BottomIsSelected
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Bottom;
|
||||
set => TabsPosition = TabsPosition.Bottom;
|
||||
set { if (value) TabsPosition = TabsPosition.Bottom; }
|
||||
}
|
||||
|
||||
public bool CanEnableStartupTask
|
||||
@@ -136,31 +278,43 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.NewTerminalLocation;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.NewTerminalLocation != value && !_editingNewTerminalLocation)
|
||||
if (_applicationSettings.NewTerminalLocation != value)
|
||||
{
|
||||
_editingNewTerminalLocation = true;
|
||||
_applicationSettings.NewTerminalLocation = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(WindowIsSelected));
|
||||
RaisePropertyChanged(nameof(TabIsSelected));
|
||||
_editingNewTerminalLocation = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,7 +328,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool TabIsSelected
|
||||
{
|
||||
get => NewTerminalLocation == NewTerminalLocation.Tab;
|
||||
set => NewTerminalLocation = NewTerminalLocation.Tab;
|
||||
set => NewTerminalLocation = value ? NewTerminalLocation.Tab : NewTerminalLocation.Window;
|
||||
}
|
||||
|
||||
public TabsPosition TabsPosition
|
||||
@@ -182,15 +336,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.TabsPosition;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.TabsPosition != value && !_editingTabsPosition)
|
||||
if (_applicationSettings.TabsPosition != value)
|
||||
{
|
||||
_editingTabsPosition = true;
|
||||
_applicationSettings.TabsPosition = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(TopIsSelected));
|
||||
RaisePropertyChanged(nameof(BottomIsSelected));
|
||||
_editingTabsPosition = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +348,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool TopIsSelected
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Top;
|
||||
set => TabsPosition = TabsPosition.Top;
|
||||
set { if (value) TabsPosition = TabsPosition.Top; }
|
||||
}
|
||||
|
||||
public bool UnderlineSelectedTab
|
||||
@@ -218,13 +368,13 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool BackgroundIsSelected
|
||||
{
|
||||
get => InactiveTabColorMode == InactiveTabColorMode.Background;
|
||||
set => InactiveTabColorMode = InactiveTabColorMode.Background;
|
||||
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Background; }
|
||||
}
|
||||
|
||||
public bool UnderlinedIsSelected
|
||||
{
|
||||
get => InactiveTabColorMode == InactiveTabColorMode.Underlined;
|
||||
set => InactiveTabColorMode = InactiveTabColorMode.Underlined;
|
||||
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Underlined; }
|
||||
}
|
||||
|
||||
public InactiveTabColorMode InactiveTabColorMode
|
||||
@@ -232,28 +382,22 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.InactiveTabColorMode;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.InactiveTabColorMode != value && !_editingInactiveTabColorMode)
|
||||
if (_applicationSettings.InactiveTabColorMode != value)
|
||||
{
|
||||
_editingInactiveTabColorMode = true;
|
||||
_applicationSettings.InactiveTabColorMode = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(BackgroundIsSelected));
|
||||
RaisePropertyChanged(nameof(UnderlinedIsSelected));
|
||||
_editingInactiveTabColorMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool WindowIsSelected
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
get => NewTerminalLocation == NewTerminalLocation.Window;
|
||||
set => NewTerminalLocation = NewTerminalLocation.Window;
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the general settings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// 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)
|
||||
{
|
||||
@@ -263,12 +407,21 @@ 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;
|
||||
UseMoshByDefault = defaults.UseMoshByDefault;
|
||||
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
|
||||
RTrimCopiedLines = defaults.RTrimCopiedLines;
|
||||
MuteTerminalBeeps = defaults.MuteTerminalBeeps;
|
||||
EnableLogging = defaults.EnableLogging;
|
||||
PrintableOutputOnly = defaults.PrintableOutputOnly;
|
||||
LogDirectoryPath = defaults.LogDirectoryPath;
|
||||
UseConPty = defaults.UseConPty;
|
||||
}
|
||||
}
|
||||
|
||||
// Requires UI thread
|
||||
private void SetStartupTaskPropertiesForStatus(StartupTaskStatus startupTaskStatus)
|
||||
{
|
||||
switch (startupTaskStatus)
|
||||
@@ -287,31 +440,53 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
case StartupTaskStatus.DisabledByUser:
|
||||
StartupTaskEnabled = false;
|
||||
StartupTaskErrorMessage = "Disabled by user. Please reactivate it in the Startup tab of the Task Manager.";
|
||||
StartupTaskErrorMessage = I18N.Translate("DisabledByUser");
|
||||
CanEnableStartupTask = false;
|
||||
break;
|
||||
|
||||
case StartupTaskStatus.DisabledByPolicy:
|
||||
StartupTaskEnabled = false;
|
||||
StartupTaskErrorMessage = "Disabled by policy.";
|
||||
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy");
|
||||
CanEnableStartupTask = false;
|
||||
break;
|
||||
|
||||
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
|
||||
{
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
@@ -16,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;
|
||||
@@ -96,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)
|
||||
@@ -116,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("Please confirm", "Are you sure you want to delete this keybinding?", 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, EnumHelper.GetEnumDescription(command), true);
|
||||
foreach (var keyBinding in _keyBindings[command.ToString()])
|
||||
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, I18N.Translate($"{nameof(Command)}.{command}"), true);
|
||||
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("Please confirm", "Are you sure you want to restore the default keybindings?", 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);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
@@ -16,8 +14,6 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private readonly ApplicationSettings _applicationSettings;
|
||||
private bool _editingMouseRightClickAction;
|
||||
private bool _editingMouseMiddleClickAction;
|
||||
|
||||
public MousePageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider)
|
||||
{
|
||||
@@ -26,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
|
||||
@@ -48,16 +44,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.MouseRightClickAction;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.MouseRightClickAction != value && !_editingMouseRightClickAction)
|
||||
if (_applicationSettings.MouseRightClickAction != value)
|
||||
{
|
||||
_editingMouseRightClickAction = true;
|
||||
_applicationSettings.MouseRightClickAction = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(MouseRightClickNoneIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseRightClickContextMenuIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseRightClickPasteIsSelected));
|
||||
_editingMouseRightClickAction = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,19 +56,19 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool MouseRightClickNoneIsSelected
|
||||
{
|
||||
get => MouseRightClickAction == MouseAction.None;
|
||||
set => MouseRightClickAction = MouseAction.None;
|
||||
set { if (value) MouseRightClickAction = MouseAction.None; }
|
||||
}
|
||||
|
||||
public bool MouseRightClickContextMenuIsSelected
|
||||
{
|
||||
get => MouseRightClickAction == MouseAction.ContextMenu;
|
||||
set => MouseRightClickAction = MouseAction.ContextMenu;
|
||||
set { if (value) MouseRightClickAction = MouseAction.ContextMenu; }
|
||||
}
|
||||
|
||||
public bool MouseRightClickPasteIsSelected
|
||||
{
|
||||
get => MouseRightClickAction == MouseAction.Paste;
|
||||
set => MouseRightClickAction = MouseAction.Paste;
|
||||
set { if (value) MouseRightClickAction = MouseAction.Paste; }
|
||||
}
|
||||
|
||||
public MouseAction MouseMiddleClickAction
|
||||
@@ -85,16 +76,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _applicationSettings.MouseMiddleClickAction;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.MouseMiddleClickAction != value && !_editingMouseMiddleClickAction)
|
||||
if (_applicationSettings.MouseMiddleClickAction != value)
|
||||
{
|
||||
_editingMouseMiddleClickAction = true;
|
||||
_applicationSettings.MouseMiddleClickAction = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(MouseMiddleClickNoneIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseMiddleClickContextMenuIsSelected));
|
||||
RaisePropertyChanged(nameof(MouseMiddleClickPasteIsSelected));
|
||||
_editingMouseMiddleClickAction = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,26 +88,30 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
public bool MouseMiddleClickNoneIsSelected
|
||||
{
|
||||
get => MouseMiddleClickAction == MouseAction.None;
|
||||
set => MouseMiddleClickAction = MouseAction.None;
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.None; }
|
||||
}
|
||||
|
||||
public bool MouseMiddleClickContextMenuIsSelected
|
||||
{
|
||||
get => MouseMiddleClickAction == MouseAction.ContextMenu;
|
||||
set => MouseMiddleClickAction = MouseAction.ContextMenu;
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.ContextMenu; }
|
||||
}
|
||||
|
||||
public bool MouseMiddleClickPasteIsSelected
|
||||
{
|
||||
get => MouseMiddleClickAction == MouseAction.Paste;
|
||||
set => MouseMiddleClickAction = MouseAction.Paste;
|
||||
set { if (value) MouseMiddleClickAction = MouseAction.Paste; }
|
||||
}
|
||||
|
||||
public RelayCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the mouse settings?", 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)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
var defaultShellProfileId = _settingsService.GetDefaultShellProfileId();
|
||||
foreach (var shellProfile in _settingsService.GetShellProfiles())
|
||||
{
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider);
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider, false);
|
||||
viewModel.Deleted += OnShellProfileDeleted;
|
||||
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
|
||||
|
||||
@@ -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);
|
||||
@@ -98,21 +99,19 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
private void Clone(ShellProfileViewModel shellProfile)
|
||||
{
|
||||
var cloned = new ShellProfile(shellProfile.Model)
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PreInstalled = false,
|
||||
Name = $"Copy of {shellProfile.Name}"
|
||||
};
|
||||
var cloned = shellProfile.ProfileVm.Model.Clone();
|
||||
|
||||
cloned.Id = Guid.NewGuid();
|
||||
cloned.PreInstalled = false;
|
||||
cloned.Name = $"Copy of {shellProfile.Name}";
|
||||
cloned.KeyBindings = new List<KeyBinding>();
|
||||
|
||||
AddShellProfile(cloned);
|
||||
}
|
||||
|
||||
private void AddShellProfile(ShellProfile shellProfile)
|
||||
{
|
||||
_settingsService.SaveShellProfile(shellProfile, true);
|
||||
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider);
|
||||
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider, true);
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
|
||||
viewModel.Deleted += OnShellProfileDeleted;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
public class SshProfilesPageViewModel: ViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private SshProfileViewModel _selectedShellProfile;
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IApplicationDataContainer _historyContainer;
|
||||
|
||||
public SshProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService,
|
||||
IFileSystemService fileSystemService, IApplicationView applicationView,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IApplicationDataContainer historyContainer)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_applicationView = applicationView;
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_historyContainer = historyContainer;
|
||||
|
||||
CreateSshProfileCommand = new RelayCommand(CreateSshProfile);
|
||||
CloneCommand = new RelayCommand<SshProfileViewModel>(Clone);
|
||||
|
||||
foreach (var sshProfile in _settingsService.GetSshProfiles())
|
||||
{
|
||||
var viewModel = new SshProfileViewModel(sshProfile, settingsService, dialogService, fileSystemService,
|
||||
applicationView, _trayProcessCommunicationService, historyContainer, false);
|
||||
viewModel.Deleted += OnSshProfileDeleted;
|
||||
SshProfiles.Add(viewModel);
|
||||
}
|
||||
|
||||
if (SshProfiles.Count == 0)
|
||||
{
|
||||
CreateSshProfile();
|
||||
}
|
||||
|
||||
SelectedSshProfile = SshProfiles.First();
|
||||
}
|
||||
|
||||
private void OnSshProfileDeleted(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is SshProfileViewModel sshProfile)
|
||||
{
|
||||
if (SelectedSshProfile == sshProfile)
|
||||
{
|
||||
SelectedSshProfile = SshProfiles.First();
|
||||
}
|
||||
SshProfiles.Remove(sshProfile);
|
||||
_settingsService.DeleteSshProfile(sshProfile.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand CreateSshProfileCommand { get; }
|
||||
|
||||
public RelayCommand<SshProfileViewModel> CloneCommand { get; }
|
||||
|
||||
public void CreateSshProfile()
|
||||
{
|
||||
var shellProfile = new SshProfile
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PreInstalled = false,
|
||||
Name = "New SSH profile",
|
||||
KeyBindings = new List<KeyBinding>(),
|
||||
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault,
|
||||
RequestConPty = _settingsService.GetApplicationSettings().UseConPty
|
||||
};
|
||||
|
||||
AddSshProfile(shellProfile);
|
||||
}
|
||||
|
||||
private void Clone(SshProfileViewModel shellProfile)
|
||||
{
|
||||
var cloned = (SshProfile) shellProfile.ProfileVm.Model.Clone();
|
||||
|
||||
cloned.Id = Guid.NewGuid();
|
||||
cloned.PreInstalled = false;
|
||||
cloned.Name = $"Copy of {shellProfile.Name}";
|
||||
cloned.KeyBindings = new List<KeyBinding>();
|
||||
|
||||
foreach (KeyBinding keyBinding in cloned.KeyBindings)
|
||||
{
|
||||
keyBinding.Command = cloned.Id.ToString();
|
||||
}
|
||||
AddSshProfile(cloned);
|
||||
}
|
||||
|
||||
private void AddSshProfile(SshProfile sshProfile)
|
||||
{
|
||||
var viewModel = new SshProfileViewModel(sshProfile, _settingsService, _dialogService, _fileSystemService,
|
||||
_applicationView, _trayProcessCommunicationService, _historyContainer, true);
|
||||
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.Deleted += OnSshProfileDeleted;
|
||||
SshProfiles.Add(viewModel);
|
||||
SelectedSshProfile = viewModel;
|
||||
}
|
||||
|
||||
public SshProfileViewModel SelectedSshProfile
|
||||
{
|
||||
get => _selectedShellProfile;
|
||||
set => Set(ref _selectedShellProfile, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<SshProfileViewModel> SshProfiles { get; } = new ObservableCollection<SshProfileViewModel>();
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,57 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
public class TerminalPageViewModel : ReactiveObject
|
||||
public class TerminalPageViewModel : ViewModelBase
|
||||
{
|
||||
private readonly TerminalOptions _terminalOptions;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private bool _isEditingCursorStyle;
|
||||
private bool _isEditingScrollBarStyle;
|
||||
|
||||
public bool BarIsSelected
|
||||
{
|
||||
get => CursorStyle == CursorStyle.Bar;
|
||||
set => CursorStyle = CursorStyle.Bar;
|
||||
set { if (value) CursorStyle = CursorStyle.Bar; }
|
||||
}
|
||||
|
||||
public bool BlockIsSelected
|
||||
{
|
||||
get => CursorStyle == CursorStyle.Block;
|
||||
set => CursorStyle = CursorStyle.Block;
|
||||
set { if (value) CursorStyle = CursorStyle.Block; }
|
||||
}
|
||||
|
||||
public bool UnderlineIsSelected
|
||||
{
|
||||
get => CursorStyle == CursorStyle.Underline;
|
||||
set => CursorStyle = CursorStyle.Underline;
|
||||
set { if (value) CursorStyle = CursorStyle.Underline; }
|
||||
}
|
||||
|
||||
public bool HiddenIsSelected
|
||||
{
|
||||
get => ScrollBarStyle == ScrollBarStyle.Hidden;
|
||||
set => ScrollBarStyle = ScrollBarStyle.Hidden;
|
||||
set { if (value) ScrollBarStyle = ScrollBarStyle.Hidden; }
|
||||
}
|
||||
|
||||
public bool AutoHidingIsSelected
|
||||
{
|
||||
get => ScrollBarStyle == ScrollBarStyle.AutoHiding;
|
||||
set => ScrollBarStyle = ScrollBarStyle.AutoHiding;
|
||||
set { if (value) ScrollBarStyle = ScrollBarStyle.AutoHiding; }
|
||||
}
|
||||
|
||||
public bool VisibleIsSelected
|
||||
{
|
||||
get => ScrollBarStyle == ScrollBarStyle.Visible;
|
||||
set => ScrollBarStyle = ScrollBarStyle.Visible;
|
||||
set { if (value) ScrollBarStyle = ScrollBarStyle.Visible; }
|
||||
}
|
||||
|
||||
public bool CursorBlink
|
||||
@@ -65,7 +63,21 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.CursorBlink = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseAcrylicBackground
|
||||
{
|
||||
get => _terminalOptions.UseAcrylicBackground;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.UseAcrylicBackground != value)
|
||||
{
|
||||
_terminalOptions.UseAcrylicBackground = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +91,21 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.FontFamily = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int FontWeight
|
||||
{
|
||||
get => _terminalOptions.FontWeight;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.FontWeight != value)
|
||||
{
|
||||
_terminalOptions.FontWeight = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +118,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
if (_terminalOptions.BackgroundOpacity != value)
|
||||
{
|
||||
_terminalOptions.BackgroundOpacity = value;
|
||||
this.RaisePropertyChanged();
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +132,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
if (_terminalOptions.Padding != value)
|
||||
{
|
||||
_terminalOptions.Padding = value;
|
||||
this.RaisePropertyChanged();
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,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)
|
||||
@@ -126,17 +153,46 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.ScrollBackLimit = intValue;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> Fonts { get; }
|
||||
public string WordSeparator
|
||||
{
|
||||
get => WebViewSpecialCharEncoder.Decode(_terminalOptions.WordSeparator);
|
||||
set
|
||||
{
|
||||
value = WebViewSpecialCharEncoder.Encode(value);
|
||||
if (_terminalOptions.WordSeparator != value)
|
||||
{
|
||||
_terminalOptions.WordSeparator = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowTextCopied
|
||||
{
|
||||
get => _terminalOptions.ShowTextCopied;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.ShowTextCopied != value)
|
||||
{
|
||||
_terminalOptions.ShowTextCopied = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<FontInfo> Fonts { get; }
|
||||
|
||||
public int FontSize
|
||||
{
|
||||
@@ -147,7 +203,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.FontSize = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,16 +217,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _terminalOptions.CursorStyle;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.CursorStyle != value && !_isEditingCursorStyle)
|
||||
if (_terminalOptions.CursorStyle != value)
|
||||
{
|
||||
_isEditingCursorStyle = true;
|
||||
_terminalOptions.CursorStyle = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
this.RaisePropertyChanged(nameof(BlockIsSelected));
|
||||
this.RaisePropertyChanged(nameof(BarIsSelected));
|
||||
this.RaisePropertyChanged(nameof(UnderlineIsSelected));
|
||||
_isEditingCursorStyle = false;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,23 +231,22 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
get => _terminalOptions.ScrollBarStyle;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.ScrollBarStyle != value && !_isEditingScrollBarStyle)
|
||||
if (_terminalOptions.ScrollBarStyle != value)
|
||||
{
|
||||
_isEditingScrollBarStyle = true;
|
||||
_terminalOptions.ScrollBarStyle = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
this.RaisePropertyChanged(nameof(HiddenIsSelected));
|
||||
this.RaisePropertyChanged(nameof(AutoHidingIsSelected));
|
||||
this.RaisePropertyChanged(nameof(VisibleIsSelected));
|
||||
_isEditingScrollBarStyle = false;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the default terminal options?", 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)
|
||||
{
|
||||
@@ -206,8 +256,11 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
ScrollBarStyle = defaults.ScrollBarStyle;
|
||||
FontFamily = defaults.FontFamily;
|
||||
FontSize = defaults.FontSize;
|
||||
FontWeight = defaults.FontWeight;
|
||||
BackgroundOpacity = defaults.BackgroundOpacity;
|
||||
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
|
||||
ShowTextCopied = defaults.ShowTextCopied;
|
||||
WordSeparator = defaults.WordSeparator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,21 +270,12 @@ 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);
|
||||
Sizes = Enumerable.Range(1, 72);
|
||||
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
|
||||
Sizes = Enumerable.Range(2, 72);
|
||||
|
||||
_terminalOptions = _settingsService.GetTerminalOptions();
|
||||
|
||||
this.ObservableForProperty(x => x.BackgroundOpacity).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
|
||||
{
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
});
|
||||
this.ObservableForProperty(x => x.Padding).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
|
||||
{
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models.Messages;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
@@ -17,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);
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, false);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
|
||||
@@ -55,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
|
||||
@@ -72,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,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("Import theme failed", "No suitable parser found", 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("Import theme failed", exception.Message, DialogButton.OK).ConfigureAwait(false);
|
||||
await _dialogService
|
||||
.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,7 +178,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_settingsService.SaveTheme(theme, true);
|
||||
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService);
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, true);
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
Themes.Add(viewModel);
|
||||
@@ -178,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,32 +9,29 @@ 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)
|
||||
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);
|
||||
Shell = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
|
||||
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; }
|
||||
public ProfilesPageViewModel Shell { get; }
|
||||
public ProfilesPageViewModel Profiles { get; }
|
||||
public TerminalPageViewModel Terminal { get; }
|
||||
public ThemesPageViewModel Themes { get; }
|
||||
public MousePageViewModel Mouse { get; }
|
||||
public AboutPageViewModel About { get; }
|
||||
|
||||
public void NavigateToAboutPage()
|
||||
{
|
||||
AboutPageRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
public SshProfilesPageViewModel SshProfiles { get; }
|
||||
|
||||
public void Close()
|
||||
{
|
||||
|
||||
@@ -1,147 +1,29 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
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.App.ViewModels.Profiles;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class ShellProfileViewModel : ViewModelBase
|
||||
/// <summary>
|
||||
/// Extends <see cref="ProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
|
||||
/// </summary>
|
||||
public class ShellProfileViewModel : ProfileViewModelBase<CommonProfileProviderViewModel>
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IFileSystemService _fileSystemService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private string _arguments;
|
||||
private ShellProfile _fallbackProfile;
|
||||
private bool _inEditMode;
|
||||
private bool _isDefault;
|
||||
private string _location;
|
||||
private string _name;
|
||||
private TabTheme _selectedTabTheme;
|
||||
private TerminalTheme _selectedTerminalTheme;
|
||||
private string _workingDirectory;
|
||||
private readonly IApplicationView _applicationView;
|
||||
#region Fields
|
||||
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
|
||||
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider)
|
||||
{
|
||||
Model = shellProfile;
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_applicationView = applicationView;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
#endregion Fields
|
||||
|
||||
_settingsService.ThemeAdded += OnThemeAdded;
|
||||
_settingsService.ThemeDeleted += OnThemeDeleted;
|
||||
#region Properties
|
||||
|
||||
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
|
||||
|
||||
TerminalThemes = new ObservableCollection<TerminalTheme>();
|
||||
TerminalThemes.Add(new TerminalTheme
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Name = "Default"
|
||||
});
|
||||
foreach (var theme in _settingsService.GetThemes())
|
||||
{
|
||||
TerminalThemes.Add(theme);
|
||||
}
|
||||
|
||||
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), _dialogService, string.Empty, false);
|
||||
|
||||
InitializeViewModelProperties(shellProfile);
|
||||
|
||||
SetDefaultCommand = new RelayCommand(SetDefault);
|
||||
DeleteCommand = new AsyncCommand(Delete, CanDelete);
|
||||
EditCommand = new RelayCommand(Edit);
|
||||
CancelEditCommand = new AsyncCommand(CancelEdit);
|
||||
SaveChangesCommand = new RelayCommand(SaveChanges);
|
||||
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
|
||||
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
|
||||
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
|
||||
}
|
||||
|
||||
private void InitializeViewModelProperties(ShellProfile shellProfile)
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == shellProfile.TerminalThemeId);
|
||||
Id = shellProfile.Id;
|
||||
Name = shellProfile.Name;
|
||||
Arguments = shellProfile.Arguments;
|
||||
Location = shellProfile.Location;
|
||||
WorkingDirectory = shellProfile.WorkingDirectory;
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == shellProfile.TabThemeId);
|
||||
PreInstalled = shellProfile.PreInstalled;
|
||||
|
||||
KeyBindings.Clear();
|
||||
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThemeAdded(object sender, TerminalTheme e)
|
||||
{
|
||||
_applicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
TerminalThemes.Add(e);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeDeleted(object sender, Guid e)
|
||||
{
|
||||
if (SelectedTerminalTheme.Id == e)
|
||||
{
|
||||
_applicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(x => x.Id == Guid.Empty);
|
||||
Model.TerminalThemeId = Guid.Empty;
|
||||
if (_fallbackProfile != null)
|
||||
{
|
||||
_fallbackProfile.TerminalThemeId = Guid.Empty;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler Deleted;
|
||||
public event EventHandler SetAsDefault;
|
||||
|
||||
public IAsyncCommand BrowseForCustomShellCommand { get; }
|
||||
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
|
||||
public IAsyncCommand CancelEditCommand { get; }
|
||||
public IAsyncCommand DeleteCommand { get; }
|
||||
public RelayCommand EditCommand { get; }
|
||||
public RelayCommand SaveChangesCommand { get; }
|
||||
public RelayCommand SetDefaultCommand { get; }
|
||||
public IAsyncCommand RestoreDefaultsCommand { get; }
|
||||
public IAsyncCommand AddKeyboardShortcutCommand { get; }
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
public KeyBindingsViewModel KeyBindings { get; }
|
||||
|
||||
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
|
||||
public ShellProfile Model { get; private set; }
|
||||
public bool PreInstalled { get; private set; }
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => _arguments;
|
||||
set => Set(ref _arguments, value);
|
||||
}
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
public bool InEditMode
|
||||
{
|
||||
get => _inEditMode;
|
||||
set => Set(ref _inEditMode, value);
|
||||
}
|
||||
private bool _isDefault;
|
||||
|
||||
public bool IsDefault
|
||||
{
|
||||
@@ -149,162 +31,81 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _isDefault, value);
|
||||
}
|
||||
|
||||
public string Location
|
||||
#endregion Properties
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler SetAsDefault;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Commands
|
||||
|
||||
public RelayCommand SetDefaultCommand { get; }
|
||||
|
||||
public IAsyncCommand RestoreDefaultsCommand { get; }
|
||||
|
||||
#endregion Commands
|
||||
|
||||
#region Constrcutor
|
||||
|
||||
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService,
|
||||
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
|
||||
IDefaultValueProvider defaultValueProvider, bool isNew) : base(shellProfile, settingsService, dialogService,
|
||||
isNew)
|
||||
{
|
||||
get => _location;
|
||||
set => Set(ref _location, value);
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
|
||||
ProfileVm = new CommonProfileProviderViewModel(settingsService, applicationView, fileSystemService,
|
||||
shellProfile);
|
||||
|
||||
SetDefaultCommand = new RelayCommand(SetDefault);
|
||||
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(ref _name, value);
|
||||
}
|
||||
#endregion Constrcutor
|
||||
|
||||
public TabTheme SelectedTabTheme
|
||||
{
|
||||
get => _selectedTabTheme;
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
Set(ref _selectedTabTheme, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
#region Methods
|
||||
|
||||
public TerminalTheme SelectedTerminalTheme
|
||||
// Requires UI thread
|
||||
private async Task RestoreDefaultsAsync()
|
||||
{
|
||||
get => _selectedTerminalTheme;
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
Set(ref _selectedTerminalTheme, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get => _workingDirectory;
|
||||
set => Set(ref _workingDirectory, value);
|
||||
}
|
||||
|
||||
public void SaveChanges()
|
||||
{
|
||||
Model.Arguments = Arguments;
|
||||
Model.Location = Location;
|
||||
Model.Name = Name;
|
||||
Model.WorkingDirectory = WorkingDirectory;
|
||||
Model.TabThemeId = SelectedTabTheme.Id;
|
||||
Model.TerminalThemeId = SelectedTerminalTheme.Id;
|
||||
Model.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
_settingsService.SaveShellProfile(Model);
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
{
|
||||
if (InEditMode || !PreInstalled)
|
||||
if (InEditMode || !ProfileVm.PreInstalled)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore this profile?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
// 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)
|
||||
{
|
||||
Model = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id == Model.Id);
|
||||
InitializeViewModelProperties(Model);
|
||||
_settingsService.SaveShellProfile(Model);
|
||||
}
|
||||
}
|
||||
var profile = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id.Equals(Id));
|
||||
|
||||
public Task AddKeyboardShortcut()
|
||||
{
|
||||
return KeyBindings.ShowAddKeyBindingDialog();
|
||||
}
|
||||
|
||||
private async Task BrowseForCustomShell()
|
||||
{
|
||||
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
|
||||
if (file != null)
|
||||
{
|
||||
Location = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BrowseForWorkingDirectory()
|
||||
{
|
||||
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
|
||||
if (directory != null)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Arguments = _fallbackProfile.Arguments;
|
||||
Location = _fallbackProfile.Location;
|
||||
Name = _fallbackProfile.Name;
|
||||
WorkingDirectory = _fallbackProfile.WorkingDirectory;
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TerminalThemeId);
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TabThemeId);
|
||||
|
||||
KeyBindings.KeyBindings.Clear();
|
||||
foreach (var keyBinding in Model.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
if (profile == null)
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
// Should not happen ever, but...
|
||||
return;
|
||||
}
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
ProfileVm.Model = profile;
|
||||
Initialize(profile);
|
||||
|
||||
SettingsService.SaveShellProfile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanDelete()
|
||||
protected override bool CanDelete()
|
||||
{
|
||||
return !Model.PreInstalled;
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this theme?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Deleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
// todo write copy ctor
|
||||
_fallbackProfile = new ShellProfile
|
||||
{
|
||||
Arguments = Model.Arguments,
|
||||
Location = Model.Location,
|
||||
Name = Model.Name,
|
||||
WorkingDirectory = Model.WorkingDirectory,
|
||||
TabThemeId = Model.TabThemeId,
|
||||
TerminalThemeId = Model.TerminalThemeId
|
||||
};
|
||||
|
||||
KeyBindings.Editable = true;
|
||||
InEditMode = true;
|
||||
return !ProfileVm.PreInstalled;
|
||||
}
|
||||
|
||||
private void SetDefault()
|
||||
{
|
||||
SetAsDefault?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels.Profiles;
|
||||
using FluentTerminal.Models;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends <see cref="ProfileViewModelBase{T}"/>, and doesn't implement any additional logic because in
|
||||
/// case of SSH profiles no additional logic is needed.
|
||||
/// </summary>
|
||||
public class SshProfileViewModel : ProfileViewModelBase<SshConnectViewModel>
|
||||
{
|
||||
#region Constructor
|
||||
|
||||
public SshProfileViewModel(SshProfile sshProfile, ISettingsService settingsService,
|
||||
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
|
||||
ITrayProcessCommunicationService trayProcessCommunicationService,
|
||||
IApplicationDataContainer historyContainer, bool isNew) : base(sshProfile, settingsService, dialogService,
|
||||
isNew)
|
||||
{
|
||||
ProfileVm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
|
||||
fileSystemService, sshProfile);
|
||||
}
|
||||
|
||||
#endregion Constructor
|
||||
|
||||
#region Methods
|
||||
|
||||
protected override bool CanDelete()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
@@ -1,118 +1,211 @@
|
||||
using Fleck;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Core;
|
||||
using FluentTerminal.Models.Messages;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class TerminalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ManualResetEventSlim _connectedEvent;
|
||||
private readonly IDialogService _dialogService;
|
||||
#region Static
|
||||
|
||||
private static readonly Regex SshTitleRx = new Regex(@"^\w+@\S+:", RegexOptions.Compiled);
|
||||
|
||||
#endregion Static
|
||||
|
||||
#region Serialize terminal state
|
||||
|
||||
private class TerminalState
|
||||
{
|
||||
public bool HasCustomTitle { get; set; }
|
||||
public string ShellTitle { get; set; }
|
||||
public string TabTitle { get; set; }
|
||||
public TerminalTheme TerminalTheme { get; set; }
|
||||
public TabTheme TabTheme { get; set; }
|
||||
public bool ShowSearchPanel { get; set; }
|
||||
public string SearchText { get; set; }
|
||||
public string XtermBufferState { get; set; }
|
||||
public byte TerminalId { get; set; }
|
||||
public ShellProfile ShellProfile { get; set; }
|
||||
}
|
||||
|
||||
public async Task<string> SerializeAsync()
|
||||
{
|
||||
TerminalState state = new TerminalState
|
||||
{
|
||||
HasCustomTitle = _hasCustomTitle,
|
||||
ShellTitle = ShellTitle,
|
||||
TabTitle = TabTitle,
|
||||
TerminalTheme = TerminalTheme,
|
||||
TabTheme = TabTheme,
|
||||
ShowSearchPanel = ShowSearchPanel,
|
||||
SearchText = SearchText,
|
||||
XtermBufferState = await SerializeXtermStateAsync().ConfigureAwait(false),
|
||||
TerminalId = Terminal.Id,
|
||||
ShellProfile = ShellProfile
|
||||
};
|
||||
|
||||
return JsonConvert.SerializeObject(state);
|
||||
}
|
||||
|
||||
public void Restore(string data)
|
||||
{
|
||||
TerminalState state = JsonConvert.DeserializeObject<TerminalState>(data);
|
||||
if (state != null)
|
||||
{
|
||||
_hasCustomTitle = state.HasCustomTitle;
|
||||
ShellTitle = state.ShellTitle;
|
||||
TabTitle = state.TabTitle;
|
||||
TerminalTheme = state.TerminalTheme;
|
||||
TabTheme = state.TabTheme;
|
||||
ShowSearchPanel = state.ShowSearchPanel;
|
||||
SearchText = state.SearchText;
|
||||
XtermBufferState = state.XtermBufferState;
|
||||
_terminalId = state.TerminalId;
|
||||
ShellProfile = state.ShellProfile;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Serialize terminal state
|
||||
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private readonly IDispatcherTimer _resizeOverlayTimer;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ShellProfile _shellProfile;
|
||||
private readonly string _startupDirectory;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private bool _isSelected;
|
||||
private bool _newOutput;
|
||||
private string _resizeOverlayContent;
|
||||
private bool _isHovered;
|
||||
private bool _hasNewOutput;
|
||||
private bool _hasExitedWithError;
|
||||
private string _searchText;
|
||||
private bool _showResizeOverlay;
|
||||
private bool _showSearchPanel;
|
||||
private TabTheme _tabTheme;
|
||||
private TerminalTheme _terminalTheme;
|
||||
private int _terminalId;
|
||||
private ITerminalView _terminalView;
|
||||
private string _title;
|
||||
private IWebSocketConnection _webSocket;
|
||||
private TerminalOptions _terminalOptions;
|
||||
private string _tabTitle;
|
||||
private string _shellTitle;
|
||||
private bool _hasCustomTitle;
|
||||
private byte? _terminalId;
|
||||
|
||||
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
|
||||
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
|
||||
IApplicationView applicationView, IClipboardService clipboardService, string terminalState = null)
|
||||
{
|
||||
Title = DefaultTitle;
|
||||
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
|
||||
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
|
||||
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
|
||||
|
||||
_connectedEvent = new ManualResetEventSlim(false);
|
||||
SettingsService = settingsService;
|
||||
|
||||
_settingsService = settingsService;
|
||||
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
|
||||
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
_settingsService.KeyBindingsChanged += OnKeyBindingsChanged;
|
||||
_terminalOptions = SettingsService.GetTerminalOptions();
|
||||
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
TrayProcessCommunicationService = trayProcessCommunicationService;
|
||||
|
||||
_dialogService = dialogService;
|
||||
DialogService = dialogService;
|
||||
_keyboardCommandService = keyboardCommandService;
|
||||
ApplicationSettings = applicationSettings;
|
||||
_startupDirectory = startupDirectory;
|
||||
_shellProfile = shellProfile;
|
||||
_applicationView = applicationView;
|
||||
_clipboardService = clipboardService;
|
||||
ApplicationView = applicationView;
|
||||
ClipboardService = clipboardService;
|
||||
|
||||
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? _settingsService.GetCurrentTheme() : _settingsService.GetTheme(shellProfile.TerminalThemeId);
|
||||
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<TabTheme>(SettingsService.GetTabThemes());
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
|
||||
|
||||
_resizeOverlayTimer = dispatcherTimer;
|
||||
_resizeOverlayTimer.Interval = new TimeSpan(0, 0, 2);
|
||||
_resizeOverlayTimer.Tick += OnResizeOverlayTimerFinished;
|
||||
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);
|
||||
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme, CanExecuteCommand);
|
||||
EditTitleCommand = new AsyncCommand(EditTitleAsync, CanExecuteCommand);
|
||||
DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand);
|
||||
|
||||
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
|
||||
FindNextCommand = new RelayCommand(async () => await FindNext().ConfigureAwait(false));
|
||||
FindPreviousCommand = new RelayCommand(async () => await FindPrevious().ConfigureAwait(false));
|
||||
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
|
||||
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
|
||||
EditTitleCommand = new AsyncCommand(EditTitle);
|
||||
if (!String.IsNullOrEmpty(terminalState))
|
||||
{
|
||||
Restore(terminalState);
|
||||
}
|
||||
|
||||
Terminal = new Terminal(TrayProcessCommunicationService, _terminalId);
|
||||
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
|
||||
Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
Terminal.SizeChanged += Terminal_SizeChanged;
|
||||
Terminal.TitleChanged += Terminal_TitleChanged;
|
||||
Terminal.Exited += Terminal_Exited;
|
||||
Terminal.Closed += Terminal_Closed;
|
||||
}
|
||||
|
||||
private bool _disposalRequested;
|
||||
|
||||
public void DisposalPrepare()
|
||||
{
|
||||
_disposalRequested = true;
|
||||
CloseCommand = null;
|
||||
TerminalView.DisposalPrepare();
|
||||
TerminalView = null;
|
||||
Terminal = null;
|
||||
Overlay = null;
|
||||
}
|
||||
|
||||
public event EventHandler Activated;
|
||||
public event EventHandler Closed;
|
||||
public event EventHandler<string> FindNextRequested;
|
||||
public event EventHandler<string> FindPreviousRequested;
|
||||
public event EventHandler SearchStarted;
|
||||
public event EventHandler<TerminalTheme> ThemeChanged;
|
||||
public event EventHandler CloseLeftTabsRequested;
|
||||
public event EventHandler CloseRightTabsRequested;
|
||||
public event EventHandler CloseOtherTabsRequested;
|
||||
public event EventHandler DuplicateTabRequested;
|
||||
|
||||
public ApplicationSettings ApplicationSettings { get; private set; }
|
||||
|
||||
public IApplicationView ApplicationView { get; }
|
||||
|
||||
public TabTheme BackgroundTabTheme
|
||||
{
|
||||
// The effective background theme depends on whether it is selected (use the theme), or if it is inactive
|
||||
// (if we're set to underline inactive tabs, use the null theme).
|
||||
get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ?
|
||||
_tabTheme :
|
||||
_settingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
|
||||
SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
|
||||
}
|
||||
|
||||
public TerminalTheme TerminalTheme
|
||||
{
|
||||
get => _terminalTheme;
|
||||
set => Set(ref _terminalTheme, value);
|
||||
}
|
||||
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; }
|
||||
|
||||
public string DefaultTitle { get; private set; } = string.Empty;
|
||||
public IDialogService DialogService { get; }
|
||||
|
||||
public IAsyncCommand EditTitleCommand { get; }
|
||||
|
||||
public RelayCommand FindNextCommand { get; }
|
||||
|
||||
public RelayCommand FindPreviousCommand { get; }
|
||||
|
||||
public IAsyncCommand EditTitleCommand { get; }
|
||||
public RelayCommand DuplicateTabCommand { get; }
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
public double BackgroundOpacity => _terminalOptions?.BackgroundOpacity ?? 1.0;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
@@ -123,28 +216,51 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
_applicationView.Title = Title;
|
||||
_applicationView.RunOnDispatcherThread(() => NewOutput = false);
|
||||
HasNewOutput = false;
|
||||
}
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
RaisePropertyChanged(nameof(ShowCloseButton));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
public bool NewOutput
|
||||
public bool HasNewOutput
|
||||
{
|
||||
get => _newOutput;
|
||||
set => Set(ref _newOutput, value);
|
||||
get => _hasNewOutput;
|
||||
set => Set(ref _hasNewOutput, value);
|
||||
}
|
||||
|
||||
public string ResizeOverlayContent
|
||||
public bool HasExitedWithError
|
||||
{
|
||||
get => _resizeOverlayContent;
|
||||
set => Set(ref _resizeOverlayContent, value);
|
||||
get => _hasExitedWithError;
|
||||
set
|
||||
{
|
||||
if (Set(ref _hasExitedWithError, value) && value)
|
||||
{
|
||||
HasNewOutput = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SearchText
|
||||
@@ -155,22 +271,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public RelayCommand<string> SelectTabThemeCommand { get; }
|
||||
|
||||
public bool ShowResizeOverlay
|
||||
{
|
||||
get => _showResizeOverlay;
|
||||
set
|
||||
{
|
||||
Set(ref _showResizeOverlay, value);
|
||||
if (value)
|
||||
{
|
||||
if (_resizeOverlayTimer.IsEnabled)
|
||||
{
|
||||
_resizeOverlayTimer.Stop();
|
||||
}
|
||||
_resizeOverlayTimer.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
public ISettingsService SettingsService { get; }
|
||||
|
||||
public ShellProfile ShellProfile { get; private set; }
|
||||
|
||||
public bool ShowSearchPanel
|
||||
{
|
||||
@@ -191,244 +294,308 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public string Title
|
||||
public Terminal Terminal { get; private set; }
|
||||
|
||||
public OverlayViewModel Overlay { get; set; }
|
||||
|
||||
public TerminalTheme TerminalTheme
|
||||
{
|
||||
get => _title;
|
||||
get => _terminalTheme;
|
||||
set => Set(ref _terminalTheme, value);
|
||||
}
|
||||
|
||||
public bool Initialized { get; set; }
|
||||
|
||||
public string TabTitle
|
||||
{
|
||||
get => _tabTitle;
|
||||
set
|
||||
{
|
||||
value = string.IsNullOrWhiteSpace(value) ? DefaultTitle : value;
|
||||
var title = value?.Trim() ?? string.Empty;
|
||||
|
||||
if (Set(ref _title, value) && IsSelected)
|
||||
if (ShellProfile is SshProfile)
|
||||
{
|
||||
_applicationView.Title = Title;
|
||||
// For SshProfile we are adjusting title.
|
||||
if (title.Equals("ssh", StringComparison.OrdinalIgnoreCase) ||
|
||||
title.EndsWith("\\ssh.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
title = $"[ssh] {I18N.Translate("Authenticate")}";
|
||||
}
|
||||
else if (title.Equals("mosh", StringComparison.OrdinalIgnoreCase) ||
|
||||
title.EndsWith("\\mosh.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
title = $"[mosh] {I18N.Translate("Authenticate")}";
|
||||
}
|
||||
else if (SshTitleRx.IsMatch(title))
|
||||
{
|
||||
title = $"[ssh] {title}";
|
||||
}
|
||||
}
|
||||
|
||||
Set(ref _tabTitle, title);
|
||||
}
|
||||
}
|
||||
|
||||
public string ShellTitle
|
||||
{
|
||||
get => _shellTitle;
|
||||
set
|
||||
{
|
||||
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
|
||||
{
|
||||
TabTitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CloseView()
|
||||
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
_terminalView.Close();
|
||||
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
|
||||
MessengerInstance.Unregister(this);
|
||||
|
||||
return Terminal.CloseAsync();
|
||||
}
|
||||
|
||||
public void CopyText(string text)
|
||||
public Task CopyTextAsync(string text)
|
||||
{
|
||||
_clipboardService.SetText(text);
|
||||
}
|
||||
|
||||
public Task FocusTerminal()
|
||||
{
|
||||
return _terminalView?.FocusTerminal();
|
||||
}
|
||||
|
||||
public async Task OnViewIsReady(ITerminalView terminalView)
|
||||
{
|
||||
_terminalView = terminalView;
|
||||
|
||||
var options = _settingsService.GetTerminalOptions();
|
||||
var keyBindings = _settingsService.GetCommandKeyBindings();
|
||||
var profiles = _settingsService.GetShellProfiles();
|
||||
|
||||
var size = await _terminalView.CreateTerminal(options, TerminalTheme.Colors, FlattenKeyBindings(keyBindings, profiles)).ConfigureAwait(true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_startupDirectory))
|
||||
return ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
_shellProfile.WorkingDirectory = _startupDirectory;
|
||||
}
|
||||
ClipboardService.SetText(text);
|
||||
|
||||
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile).ConfigureAwait(true);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_terminalId = response.Id;
|
||||
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
|
||||
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
|
||||
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
|
||||
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
|
||||
if (_terminalOptions.ShowTextCopied)
|
||||
{
|
||||
_connectedEvent.Wait();
|
||||
_webSocket.Send(t);
|
||||
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
|
||||
{
|
||||
_applicationView.RunOnDispatcherThread(() => NewOutput = true);
|
||||
}
|
||||
});
|
||||
Overlay.Show(I18N.Translate("TextCopied"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
DefaultTitle = response.ShellExecutableName;
|
||||
Title = DefaultTitle;
|
||||
|
||||
var webSocketUrl = "ws://127.0.0.1:" + response.Id;
|
||||
var webSocketServer = new WebSocketServer(webSocketUrl);
|
||||
webSocketServer.Start(socket =>
|
||||
{
|
||||
_webSocket = socket;
|
||||
socket.OnOpen = () => _connectedEvent.Set();
|
||||
socket.OnMessage = message => _trayProcessCommunicationService.WriteText(_terminalId, message);
|
||||
});
|
||||
|
||||
await _terminalView.ConnectToSocket(webSocketUrl).ConfigureAwait(true);
|
||||
Initialized = true;
|
||||
}
|
||||
else
|
||||
// Requires UI thread
|
||||
public async Task EditTitleAsync()
|
||||
{
|
||||
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
|
||||
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"))
|
||||
.ConfigureAwait(true);
|
||||
if (result != null)
|
||||
{
|
||||
await _dialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
{
|
||||
_hasCustomTitle = false;
|
||||
TabTitle = ShellTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
_hasCustomTitle = true;
|
||||
TabTitle = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await FocusTerminal().ConfigureAwait(true);
|
||||
public void FocusTerminal()
|
||||
{
|
||||
Activated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void CloseSearchPanel()
|
||||
{
|
||||
SearchText = string.Empty;
|
||||
ShowSearchPanel = false;
|
||||
_terminalView.FocusTerminal();
|
||||
FocusTerminal();
|
||||
}
|
||||
|
||||
private Task FindNext()
|
||||
private bool CanExecuteCommand<T>(T a)
|
||||
{
|
||||
return _terminalView.FindNext(SearchText);
|
||||
return CanExecuteCommand();
|
||||
}
|
||||
|
||||
private Task FindPrevious()
|
||||
private bool CanExecuteCommand()
|
||||
{
|
||||
return _terminalView.FindPrevious(SearchText);
|
||||
return Initialized && !_disposalRequested;
|
||||
}
|
||||
|
||||
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<string, ICollection<KeyBinding>> commandKeyBindings, IEnumerable<ShellProfile> profiles)
|
||||
private void CloseLeftTabs()
|
||||
{
|
||||
return commandKeyBindings.Values.SelectMany(k => k).Concat(profiles.SelectMany(x => x.KeyBindings));
|
||||
CloseLeftTabsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
private void CloseRightTabs()
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
CloseRightTabsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void CloseOtherTabs()
|
||||
{
|
||||
CloseOtherTabsRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void FindNext()
|
||||
{
|
||||
FindNextRequested?.Invoke(this, SearchText);
|
||||
}
|
||||
|
||||
public ITerminalView TerminalView { get; set; }
|
||||
|
||||
private Task<string> SerializeXtermStateAsync()
|
||||
{
|
||||
return TerminalView?.SerializeXtermStateAsync() ?? Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
private void FindPrevious()
|
||||
{
|
||||
FindPreviousRequested?.Invoke(this, SearchText);
|
||||
}
|
||||
|
||||
private void DuplicateTab()
|
||||
{
|
||||
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
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(async () =>
|
||||
// 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;
|
||||
await _terminalView.ChangeTheme(currentTheme.Colors).ConfigureAwait(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnKeyBindingsChanged(object sender, EventArgs e)
|
||||
{
|
||||
var keyBindings = _settingsService.GetCommandKeyBindings();
|
||||
var profiles = _settingsService.GetShellProfiles();
|
||||
await _applicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
await _terminalView.ChangeKeyBindings(FlattenKeyBindings(keyBindings, profiles)).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnKeyboardCommandReceived(object sender, string command)
|
||||
{
|
||||
if (command == nameof(Command.Copy))
|
||||
{
|
||||
var selection = await _terminalView.GetSelection().ConfigureAwait(true);
|
||||
_clipboardService.SetText(selection);
|
||||
}
|
||||
else if (command == nameof(Command.Paste))
|
||||
{
|
||||
var content = await _clipboardService.GetText().ConfigureAwait(true);
|
||||
if (content != null)
|
||||
{
|
||||
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
else if (command == nameof(Command.Search))
|
||||
{
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
{
|
||||
_terminalView.FocusSearchTextBox();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_keyboardCommandService.SendCommand(command);
|
||||
ThemeChanged?.Invoke(this, currentTheme);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResizeOverlayTimerFinished(object sender, object e)
|
||||
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
|
||||
{
|
||||
_resizeOverlayTimer.Stop();
|
||||
ShowResizeOverlay = false;
|
||||
_terminalOptions = message.TerminalOptions;
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => RaisePropertyChanged(nameof(BackgroundOpacity)), CoreDispatcherPriority.Low);
|
||||
}
|
||||
|
||||
private void OnTerminalExited(object sender, int e)
|
||||
private void SelectTabTheme(string id)
|
||||
{
|
||||
if (e == _terminalId)
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
|
||||
}
|
||||
|
||||
private void Terminal_Exited(object sender, int exitCode)
|
||||
{
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker && exitCode != 0)
|
||||
{
|
||||
_applicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
await _terminalView.ChangeOptions(e).ConfigureAwait(true);
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnTerminalSizeChanged(object sender, TerminalSize e)
|
||||
{
|
||||
if (!Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
|
||||
ShowResizeOverlay = true;
|
||||
await _trayProcessCommunicationService.ResizeTerminal(_terminalId, e).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void OnTerminalTitleChanged(object sender, string e)
|
||||
{
|
||||
Title = e;
|
||||
}
|
||||
|
||||
private void SelectTabTheme(string name)
|
||||
{
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
|
||||
}
|
||||
|
||||
private async Task EditTitle()
|
||||
{
|
||||
var result = await _dialogService.ShowInputDialogAsync("Edit Title");
|
||||
if (result != null)
|
||||
{
|
||||
Title = result;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryClose()
|
||||
{
|
||||
if (ApplicationSettings.ConfirmClosingTabs)
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
tracker.SetInvalid();
|
||||
}
|
||||
|
||||
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => HasExitedWithError = exitCode > 0);
|
||||
}
|
||||
|
||||
private void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
Terminal.KeyboardCommandReceived -= Terminal_KeyboardCommandReceived;
|
||||
Terminal.OutputReceived -= Terminal_OutputReceived;
|
||||
Terminal.SizeChanged -= Terminal_SizeChanged;
|
||||
Terminal.TitleChanged -= Terminal_TitleChanged;
|
||||
Terminal.Exited -= Terminal_Exited;
|
||||
Terminal.Closed -= Terminal_Closed;
|
||||
}
|
||||
|
||||
private async void Terminal_KeyboardCommandReceived(object sender, string e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case nameof(Command.Copy):
|
||||
{
|
||||
var selection = await Terminal.GetSelectedText().ConfigureAwait(false);
|
||||
await CopyTextAsync(selection).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
case nameof(Command.Paste):
|
||||
{
|
||||
string content = await ClipboardService.GetTextAsync().ConfigureAwait(false);
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.TranslateLineEndings(content);
|
||||
await TerminalView.PasteAsync(content).ConfigureAwait(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case nameof(Command.PasteWithoutNewlines):
|
||||
{
|
||||
string content = await ClipboardService.GetTextAsync().ConfigureAwait(false);
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
|
||||
await TerminalView.PasteAsync(content).ConfigureAwait(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case nameof(Command.Search):
|
||||
{
|
||||
await ApplicationView.ExecuteOnUiThreadAsync(() =>
|
||||
{
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
{
|
||||
SearchStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{
|
||||
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.ExecuteOnUiThreadAsync(() => HasNewOutput = true);
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_SizeChanged(object sender, TerminalSize e)
|
||||
{
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => Overlay.Show($"{e.Columns} x {e.Rows}"));
|
||||
}
|
||||
|
||||
private void Terminal_TitleChanged(object sender, string e)
|
||||
{
|
||||
ApplicationView.ExecuteOnUiThreadAsync(() => ShellTitle = e);
|
||||
}
|
||||
|
||||
private async Task TryCloseAsync()
|
||||
{
|
||||
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
|
||||
{
|
||||
tracker.SetInvalid();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 465 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user