Compare commits
68
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bbed15874 | ||
|
|
39960dd755 | ||
|
|
8e5d3dea0a | ||
|
|
973b1a4963 | ||
|
|
997cb9814e | ||
|
|
dbd8632076 | ||
|
|
fffa7ebcb9 | ||
|
|
6524d14687 | ||
|
|
7876984327 | ||
|
|
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
@@ -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>);
|
||||
|
||||
dialogService.ShowCreateKeyBindingDialog();
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var title = string.Empty;
|
||||
var content = _fixture.Create<string>();
|
||||
var buttons = _fixture.CreateMany<DialogButton>(2);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var title = _fixture.Create<string>();
|
||||
var content = string.Empty;
|
||||
var buttons = _fixture.CreateMany<DialogButton>(2);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
{
|
||||
var title = _fixture.Create<string>();
|
||||
var content = _fixture.Create<string>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
|
||||
|
||||
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
var content = _fixture.Create<string>();
|
||||
var buttons = _fixture.CreateMany<DialogButton>(2);
|
||||
var messageDialog = new Mock<IMessageDialog>();
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
|
||||
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
|
||||
|
||||
dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
|
||||
|
||||
@@ -86,11 +86,24 @@ namespace FluentTerminal.App.Services.Test
|
||||
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
|
||||
{
|
||||
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
|
||||
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
|
||||
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
|
||||
|
||||
dialogService.ShowProfileSelectionDialogAsync();
|
||||
|
||||
shellProfileSelectionDialog.Verify(x => x.SelectProfile(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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>);
|
||||
|
||||
dialogService.ShowInputDialogAsync(title);
|
||||
|
||||
inputDialog.Verify(x => x.SetTitle(title), Times.Once);
|
||||
inputDialog.Verify(x => x.GetInput(), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.8.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||
<PackageReference Include="Moq" Version="4.10.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
|
||||
<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>
|
||||
|
||||
|
||||
@@ -42,10 +42,12 @@ namespace FluentTerminal.App.Services.Test
|
||||
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
|
||||
var terminalSize = _fixture.Create<TerminalSize>();
|
||||
var shellProfile = _fixture.Create<ShellProfile>();
|
||||
var sessionType = _fixture.Create<SessionType>();
|
||||
var id = _fixture.Create<int>();
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile);
|
||||
var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
|
||||
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
@@ -71,10 +73,10 @@ namespace FluentTerminal.App.Services.Test
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteText_Default_SendsWriteTextRequest()
|
||||
public async Task Write_Default_SendsWriteDataRequest()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var text = _fixture.Create<string>();
|
||||
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,9 +87,9 @@ namespace FluentTerminal.App.Services.Test
|
||||
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
|
||||
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
|
||||
|
||||
await trayProcessCommunicationService.WriteText(terminalId, text);
|
||||
await trayProcessCommunicationService.Write(terminalId, data);
|
||||
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteTextRequest))), Times.Once);
|
||||
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteDataRequest))), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -149,8 +151,8 @@ namespace FluentTerminal.App.Services.Test
|
||||
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
|
||||
{
|
||||
var terminalId = _fixture.Create<int>();
|
||||
var output = _fixture.Create<string>();
|
||||
var receivedOutput = string.Empty;
|
||||
var output = _fixture.Create<byte[]>();
|
||||
var receivedOutput = default(byte[]);
|
||||
var request = new DisplayTerminalOutputRequest
|
||||
{
|
||||
TerminalId = terminalId,
|
||||
@@ -177,7 +179,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
|
||||
|
||||
receivedOutput.Should().Be(output);
|
||||
receivedOutput.Should().BeEquivalentTo(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ISshConnectionInfo
|
||||
{
|
||||
string Host { get; set; }
|
||||
|
||||
ushort Port { get; set; }
|
||||
|
||||
string Username { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Dialogs
|
||||
{
|
||||
public interface ISshConnectionInfoDialog
|
||||
{
|
||||
Task<ISshConnectionInfo> GetSshConnectionInfoAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FluentTerminal.App.Services.EventArgs
|
||||
{
|
||||
public class NewWindowRequestedEventArgs : System.EventArgs
|
||||
{
|
||||
public bool ShowProfileSelection { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
|
||||
<PackageReference Include="PListNet" Version="2.0.3" />
|
||||
<PackageReference Include="RestSharp" Version="106.5.4" />
|
||||
<PackageReference Include="RestSharp" Version="106.6.9" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.3.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ namespace FluentTerminal.App.Services
|
||||
public interface IApplicationView
|
||||
{
|
||||
event CloseRequestedHandler CloseRequested;
|
||||
event EventHandler Closed;
|
||||
|
||||
int Id { get; }
|
||||
string Title { get; set; }
|
||||
|
||||
Task RunOnDispatcherThread(Action action);
|
||||
@@ -17,5 +19,6 @@ namespace FluentTerminal.App.Services
|
||||
Task<bool> TryClose();
|
||||
|
||||
bool ToggleFullScreen();
|
||||
bool IsApiContractPresent(string api, ushort version);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services.Dialogs;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
@@ -17,5 +18,7 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
Task<KeyBinding> ShowCreateKeyBindingDialog();
|
||||
Task<string> ShowInputDialogAsync(string title);
|
||||
|
||||
Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@@ -11,16 +12,18 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
void Initialize(IAppServiceConnection appServiceConnection);
|
||||
|
||||
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile);
|
||||
Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
|
||||
|
||||
Task ResizeTerminal(int id, TerminalSize size);
|
||||
|
||||
Task UpdateToggleWindowKeyBindings();
|
||||
|
||||
Task WriteText(int terminalId, string text);
|
||||
Task Write(int terminalId, byte[] data);
|
||||
|
||||
void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
|
||||
void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback);
|
||||
|
||||
Task CloseTerminal(int terminalId);
|
||||
Task<GetAvailablePortResponse> GetAvailablePort();
|
||||
int GetNextTerminalId();
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,10 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
MouseMiddleClickAction = MouseAction.None,
|
||||
MouseRightClickAction = MouseAction.ContextMenu,
|
||||
AlwaysShowTabs = false,
|
||||
AlwaysUseWinPty = true,
|
||||
ShowNewOutputIndicator = false,
|
||||
EnableTrayIcon = true
|
||||
EnableTrayIcon = true,
|
||||
ShowCustomTitleInTitlebar = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,6 +118,32 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
};
|
||||
|
||||
case Command.NewRemoteTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.NewRemoteTab),
|
||||
Ctrl = false,
|
||||
Alt = true,
|
||||
Shift = false,
|
||||
Key = (int)ExtendedVirtualKey.T
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ChangeTabTitle:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ChangeTabTitle),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.R
|
||||
}
|
||||
};
|
||||
|
||||
case Command.CloseTab:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -142,6 +170,19 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ConfigurableNewWindow:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.ConfigurableNewWindow),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.N
|
||||
}
|
||||
};
|
||||
|
||||
case Command.ShowSettings:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -181,6 +222,20 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
case Command.PasteWithoutNewlines:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
new KeyBinding
|
||||
{
|
||||
Command = nameof(Command.PasteWithoutNewlines),
|
||||
Ctrl = true,
|
||||
Alt = false,
|
||||
Shift = true,
|
||||
Key = (int)ExtendedVirtualKey.V
|
||||
}
|
||||
};
|
||||
|
||||
case Command.Search:
|
||||
return new List<KeyBinding>
|
||||
{
|
||||
@@ -305,6 +360,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
ScrollBarStyle = ScrollBarStyle.Hidden,
|
||||
FontFamily = "Consolas",
|
||||
FontSize = 13,
|
||||
BoldText = false,
|
||||
BackgroundOpacity = 0.8,
|
||||
Padding = 12,
|
||||
ScrollBackLimit = 1000
|
||||
@@ -328,6 +384,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
|
||||
PreInstalled = true,
|
||||
WorkingDirectory = string.Empty,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -349,6 +406,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Location = @"C:\Windows\System32\cmd.exe",
|
||||
PreInstalled = true,
|
||||
WorkingDirectory = string.Empty,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -370,6 +428,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
Location = @"C:\windows\system32\wsl.exe",
|
||||
PreInstalled = true,
|
||||
WorkingDirectory = string.Empty,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
KeyBindings = new []
|
||||
{
|
||||
new KeyBinding
|
||||
@@ -532,18 +591,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,17 @@ 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;
|
||||
|
||||
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)
|
||||
{
|
||||
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
|
||||
_messageDialogFactory = messageDialogFactory;
|
||||
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
|
||||
_inputDialogFactory = inputDialogFactory;
|
||||
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
|
||||
}
|
||||
|
||||
public Task<KeyBinding> ShowCreateKeyBindingDialog()
|
||||
@@ -70,5 +74,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
|
||||
return dialog.SelectProfile();
|
||||
}
|
||||
|
||||
public Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync() =>
|
||||
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using FluentTerminal.Models.Responses;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
@@ -14,27 +13,52 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private IAppServiceConnection _appServiceConnection;
|
||||
private Dictionary<int, Action<string>> _terminalOutputHandlers;
|
||||
private readonly Dictionary<int, Action<byte[]>> _terminalOutputHandlers;
|
||||
private int _nextTerminalId = 0;
|
||||
|
||||
public event EventHandler<int> TerminalExited;
|
||||
|
||||
public TrayProcessCommunicationService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
|
||||
_terminalOutputHandlers = new Dictionary<int, Action<byte[]>>();
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile)
|
||||
public int GetNextTerminalId()
|
||||
{
|
||||
return _nextTerminalId++;
|
||||
}
|
||||
|
||||
public async Task<GetAvailablePortResponse> GetAvailablePort()
|
||||
{
|
||||
var request = new GetAvailablePortRequest();
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
|
||||
|
||||
Logger.Instance.Debug("Received GetAvailablePortResponse: {@response}", response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<CreateTerminalResponse> CreateTerminal(int 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 responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>(responseMessage[MessageKeys.Content]);
|
||||
|
||||
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void Initialize(IAppServiceConnection appServiceConnection)
|
||||
@@ -58,13 +82,15 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("output was not handled: " + request.Output);
|
||||
Logger.Instance.Error("Received output for unknown terminal Id {id}", request.TerminalId);
|
||||
}
|
||||
}
|
||||
else if (messageType == nameof(TerminalExitedRequest))
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
|
||||
|
||||
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
|
||||
|
||||
TerminalExited?.Invoke(this, request.TerminalId);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +106,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
public void SubscribeForTerminalOutput(int terminalId, Action<string> callback)
|
||||
public void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback)
|
||||
{
|
||||
_terminalOutputHandlers[terminalId] = callback;
|
||||
}
|
||||
@@ -97,12 +123,12 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
public Task WriteText(int id, string text)
|
||||
public Task Write(int id, byte[] data)
|
||||
{
|
||||
var request = new WriteTextRequest
|
||||
var request = new WriteDataRequest
|
||||
{
|
||||
TerminalId = id,
|
||||
Text = text
|
||||
Data = data
|
||||
};
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
@@ -115,6 +141,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
TerminalId = terminalId
|
||||
};
|
||||
|
||||
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
|
||||
|
||||
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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.Information("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,144 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public class Terminal
|
||||
{
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private Func<Task<string>> _selectedTextCallback;
|
||||
|
||||
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
|
||||
{
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
Id = _trayProcessCommunicationService.GetNextTerminalId();
|
||||
}
|
||||
|
||||
private void OnTerminalExited(object sender, int e)
|
||||
{
|
||||
if (e == Id)
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be observed by both view and viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler Closed;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<string> KeyboardCommandReceived;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by view
|
||||
/// </summary>
|
||||
public event EventHandler<byte[]> OutputReceived;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<TerminalSize> SizeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
/// </summary>
|
||||
public event EventHandler<string> TitleChanged;
|
||||
|
||||
public string FallbackTitle { get; private set; }
|
||||
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// To be called by either view or viewmodel
|
||||
/// </summary>
|
||||
public async Task Close()
|
||||
{
|
||||
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by viewmodel
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<string> GetSelectedText()
|
||||
{
|
||||
return _selectedTextCallback();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view
|
||||
/// </summary>
|
||||
public void ProcessKeyboardCommand(string command)
|
||||
{
|
||||
KeyboardCommandReceived?.Invoke(this, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be set by view
|
||||
/// </summary>
|
||||
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
|
||||
{
|
||||
_selectedTextCallback = selectedTextCallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be called by view
|
||||
/// </summary>
|
||||
public async Task SetSize(TerminalSize size)
|
||||
{
|
||||
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
|
||||
SizeChanged?.Invoke(this, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view
|
||||
/// </summary>
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
title = FallbackTitle;
|
||||
}
|
||||
TitleChanged?.Invoke(this, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// to be called by view when ready
|
||||
/// </summary>
|
||||
/// <param name="shellProfile"></param>
|
||||
/// <param name="size"></param>
|
||||
/// <param name="sessionType"></param>
|
||||
public async Task<CreateTerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
|
||||
{
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
|
||||
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
FallbackTitle = response.ShellExecutableName;
|
||||
SetTitle(FallbackTitle);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public Task Write(byte[] data)
|
||||
{
|
||||
return _trayProcessCommunicationService.Write(Id, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells the ViewModel that this view failed to launch and can be closed.
|
||||
/// </summary>
|
||||
public void ReportLauchFailed()
|
||||
{
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -9,26 +9,28 @@ using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services.Implementation;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDispatcherTimer _dispatcherTimer;
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private ApplicationSettings _applicationSettings;
|
||||
private string _background;
|
||||
private double _backgroundOpacity;
|
||||
private TerminalViewModel _selectedTerminal;
|
||||
private TabsPosition _tabsPosition;
|
||||
private string _windowTitle;
|
||||
|
||||
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, IDefaultValueProvider defaultValueProvider)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
@@ -39,12 +41,15 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_dialogService = dialogService;
|
||||
_applicationView = applicationView;
|
||||
ApplicationView = applicationView;
|
||||
_dispatcherTimer = dispatcherTimer;
|
||||
_clipboardService = clipboardService;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_keyboardCommandService = keyboardCommandService;
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), () => AddTerminal(null, false, Guid.Empty));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewTab), () => AddTerminal(null, true, Guid.Empty));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), () => AddTerminal());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewRemoteTab), () => AddRemoteTerminal());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewTab), () => AddConfigurableTerminal());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), () => SelectedTerminal.EditTitle());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
|
||||
|
||||
// Add all of the commands for switching to a tab of a given ID, if there's one open there
|
||||
@@ -59,13 +64,15 @@ namespace FluentTerminal.App.ViewModels
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NextTab), SelectNextTab);
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.PreviousTab), SelectPreviousTab);
|
||||
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), NewWindow);
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), () => NewWindow(false));
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewWindow), () => NewWindow(true));
|
||||
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ShowSettings), ShowSettings);
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ToggleFullScreen), ToggleFullScreen);
|
||||
|
||||
foreach (ShellProfile profile in _settingsService.GetShellProfiles())
|
||||
{
|
||||
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), () => AddTerminal(profile.WorkingDirectory, false, profile.Id));
|
||||
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), () => AddTerminal(profile.Id));
|
||||
}
|
||||
|
||||
var currentTheme = _settingsService.GetCurrentTheme();
|
||||
@@ -75,14 +82,20 @@ namespace FluentTerminal.App.ViewModels
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
TabsPosition = _applicationSettings.TabsPosition;
|
||||
|
||||
AddTerminalCommand = new RelayCommand(() => AddTerminal(null, false, Guid.Empty));
|
||||
AddTerminalCommand = new RelayCommand(() => AddTerminal());
|
||||
ShowAboutCommand = new RelayCommand(ShowAbout);
|
||||
ShowSettingsCommand = new RelayCommand(ShowSettings);
|
||||
|
||||
_applicationView.CloseRequested += OnCloseRequest;
|
||||
ApplicationView.CloseRequested += OnCloseRequest;
|
||||
ApplicationView.Closed += OnClosed;
|
||||
Terminals.CollectionChanged += OnTerminalsCollectionChanged;
|
||||
}
|
||||
|
||||
private void OnClosed(object sender, EventArgs e)
|
||||
{
|
||||
Closed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void OnShellProfileDeleted(object sender, Guid e)
|
||||
{
|
||||
_keyboardCommandService.DeregisterCommandHandler(e.ToString());
|
||||
@@ -90,7 +103,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private void OnShellProfileAdded(object sender, ShellProfile e)
|
||||
{
|
||||
_keyboardCommandService.RegisterCommandHandler(e.Id.ToString(), () => AddTerminal(e.WorkingDirectory, false, e.Id));
|
||||
_keyboardCommandService.RegisterCommandHandler(e.Id.ToString(), () => AddTerminal(e.Id));
|
||||
}
|
||||
|
||||
private void OnTerminalsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
@@ -101,7 +114,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public event EventHandler Closed;
|
||||
|
||||
public event EventHandler NewWindowRequested;
|
||||
public event EventHandler<NewWindowRequestedEventArgs> NewWindowRequested;
|
||||
|
||||
public event EventHandler ShowSettingsRequested;
|
||||
|
||||
@@ -109,6 +122,18 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public RelayCommand AddTerminalCommand { get; }
|
||||
|
||||
public string WindowTitle
|
||||
{
|
||||
get => _windowTitle;
|
||||
set
|
||||
{
|
||||
if (Set(ref _windowTitle, value))
|
||||
{
|
||||
ApplicationView.Title = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowTabsOnTop
|
||||
{
|
||||
get => TabsPosition == TabsPosition.Top && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
|
||||
@@ -146,6 +171,14 @@ namespace FluentTerminal.App.ViewModels
|
||||
if (SelectedTerminal != null)
|
||||
{
|
||||
SelectedTerminal.IsSelected = true;
|
||||
if (_applicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
WindowTitle = SelectedTerminal.TabTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowTitle = SelectedTerminal.ShellTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,47 +196,118 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Add a new terminal window, either with a specified terminal profile, with the default, or by showing the profile selection dialog.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task AddTerminal(string startupDirectory, bool showProfileSelection, Guid profileId)
|
||||
public IApplicationView ApplicationView { get; }
|
||||
|
||||
public Task AddConfigurableTerminal()
|
||||
{
|
||||
return _applicationView.RunOnDispatcherThread(async () =>
|
||||
return ApplicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
ShellProfile profile = null;
|
||||
var profile = await _dialogService.ShowProfileSelectionDialogAsync();
|
||||
|
||||
if (showProfileSelection)
|
||||
if (profile == null)
|
||||
{
|
||||
profile = await _dialogService.ShowProfileSelectionDialogAsync();
|
||||
|
||||
if (profile == null)
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
return;
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
}
|
||||
else if (profileId == Guid.Empty)
|
||||
{
|
||||
profile = _settingsService.GetDefaultShellProfile();
|
||||
} else
|
||||
{
|
||||
profile = _settingsService.GetShellProfile(profileId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
AddTerminal(profile);
|
||||
});
|
||||
}
|
||||
|
||||
public Task AddRemoteTerminal()
|
||||
{
|
||||
return ApplicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
var connectionInfo = await _dialogService.ShowSshConnectionInfoDialogAsync();
|
||||
|
||||
if (connectionInfo == null)
|
||||
{
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var profile = new ShellProfile
|
||||
{
|
||||
Arguments = $"-p {connectionInfo.Port:#####} {connectionInfo.Username}@{connectionInfo.Host}",
|
||||
Location = @"C:\Windows\System32\OpenSSH\ssh.exe",
|
||||
WorkingDirectory = string.Empty,
|
||||
LineEndingTranslation = LineEndingStyle.DoNotModify,
|
||||
};
|
||||
|
||||
AddTerminal(profile);
|
||||
});
|
||||
}
|
||||
|
||||
public void AddTerminal()
|
||||
{
|
||||
var profile = _settingsService.GetDefaultShellProfile();
|
||||
AddTerminal(profile);
|
||||
}
|
||||
|
||||
public void AddTerminal(Guid shellProfileId)
|
||||
{
|
||||
var profile = _settingsService.GetShellProfile(shellProfileId);
|
||||
AddTerminal(profile);
|
||||
}
|
||||
|
||||
public void AddTerminal(ShellProfile profile)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService,
|
||||
_applicationSettings, startupDirectory, profile, _applicationView, _dispatcherTimer, _clipboardService);
|
||||
terminal.Closed += OnTerminalCloseRequested;
|
||||
_applicationSettings, profile, ApplicationView, _dispatcherTimer, _clipboardService);
|
||||
|
||||
terminal.Closed += OnTerminalClosed;
|
||||
terminal.ShellTitleChanged += Terminal_ShellTitleChanged;
|
||||
terminal.CustomTitleChanged += Terminal_CustomTitleChanged;
|
||||
Terminals.Add(terminal);
|
||||
|
||||
SelectedTerminal = terminal;
|
||||
});
|
||||
}
|
||||
|
||||
public void CloseAllTerminals()
|
||||
private void Terminal_CustomTitleChanged(object sender, string e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
if (terminal.IsSelected && _applicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e))
|
||||
{
|
||||
WindowTitle = terminal.ShellTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowTitle = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_ShellTitleChanged(object sender, string e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal && terminal.IsSelected && !_applicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
WindowTitle = e;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CloseAllTerminals()
|
||||
{
|
||||
foreach (var terminal in Terminals)
|
||||
{
|
||||
terminal.CloseView();
|
||||
terminal.Closed -= OnTerminalClosed;
|
||||
terminal.ShellTitleChanged -= Terminal_ShellTitleChanged;
|
||||
terminal.CustomTitleChanged -= Terminal_CustomTitleChanged;
|
||||
await terminal.Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,19 +316,33 @@ namespace FluentTerminal.App.ViewModels
|
||||
SelectedTerminal?.CloseCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void NewWindow()
|
||||
private void NewWindow(bool showProfileSelection)
|
||||
{
|
||||
NewWindowRequested?.Invoke(this, EventArgs.Empty);
|
||||
var args = new NewWindowRequestedEventArgs
|
||||
{
|
||||
ShowProfileSelection = showProfileSelection
|
||||
};
|
||||
|
||||
NewWindowRequested?.Invoke(this, args);
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
_applicationSettings = e;
|
||||
TabsPosition = e.TabsPosition;
|
||||
RaisePropertyChanged(nameof(ShowTabsOnTop));
|
||||
RaisePropertyChanged(nameof(ShowTabsOnBottom));
|
||||
|
||||
if (e.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
WindowTitle = SelectedTerminal?.TabTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowTitle = SelectedTerminal?.ShellTitle;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,8 +354,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
CloseAllTerminals();
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
await CloseAllTerminals();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -246,42 +363,40 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
else
|
||||
{
|
||||
CloseAllTerminals();
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
await CloseAllTerminals();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCurrentThemeChanged(object sender, Guid e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
var currentTheme = _settingsService.GetTheme(e);
|
||||
Background = currentTheme.Colors.Background;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnTerminalCloseRequested(object sender, EventArgs e)
|
||||
private async void OnTerminalClosed(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is TerminalViewModel terminal)
|
||||
{
|
||||
terminal.CloseView();
|
||||
if (SelectedTerminal == terminal)
|
||||
{
|
||||
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
|
||||
}
|
||||
Logger.Instance.Debug("Terminal with Id: {@id} closed.", terminal.Terminal.Id);
|
||||
Terminals.Remove(terminal);
|
||||
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
_applicationView.TryClose();
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
BackgroundOpacity = e.BackgroundOpacity;
|
||||
});
|
||||
@@ -321,7 +436,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private void ToggleFullScreen()
|
||||
{
|
||||
_applicationView.ToggleFullScreen();
|
||||
ApplicationView.ToggleFullScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,34 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool AlwaysUseWinPty
|
||||
{
|
||||
get => _applicationSettings.AlwaysUseWinPty;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.AlwaysUseWinPty != value)
|
||||
{
|
||||
_applicationSettings.AlwaysUseWinPty = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowCustomTitleInTitlebar
|
||||
{
|
||||
get => _applicationSettings.ShowCustomTitleInTitlebar;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.ShowCustomTitleInTitlebar != value)
|
||||
{
|
||||
_applicationSettings.ShowCustomTitleInTitlebar = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableTrayIcon
|
||||
{
|
||||
get => _applicationSettings.EnableTrayIcon;
|
||||
@@ -264,8 +292,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
InactiveTabColorMode = defaults.InactiveTabColorMode;
|
||||
NewTerminalLocation = defaults.NewTerminalLocation;
|
||||
AlwaysShowTabs = defaults.AlwaysShowTabs;
|
||||
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
|
||||
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
|
||||
EnableTrayIcon = defaults.EnableTrayIcon;
|
||||
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_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;
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
using FluentTerminal.App.Services;
|
||||
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;
|
||||
@@ -65,7 +63,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.CursorBlink = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +77,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.FontFamily = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +90,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
if (_terminalOptions.BackgroundOpacity != value)
|
||||
{
|
||||
_terminalOptions.BackgroundOpacity = value;
|
||||
this.RaisePropertyChanged();
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +104,8 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
if (_terminalOptions.Padding != value)
|
||||
{
|
||||
_terminalOptions.Padding = value;
|
||||
this.RaisePropertyChanged();
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,17 +126,31 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.ScrollBackLimit = intValue;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> Fonts { get; }
|
||||
public bool BoldText
|
||||
{
|
||||
get => _terminalOptions.BoldText;
|
||||
set
|
||||
{
|
||||
if (_terminalOptions.BoldText != value)
|
||||
{
|
||||
_terminalOptions.BoldText = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<FontInfo> Fonts { get; }
|
||||
|
||||
public int FontSize
|
||||
{
|
||||
@@ -147,7 +161,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.FontSize = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,10 +180,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_isEditingCursorStyle = true;
|
||||
_terminalOptions.CursorStyle = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
this.RaisePropertyChanged(nameof(BlockIsSelected));
|
||||
this.RaisePropertyChanged(nameof(BarIsSelected));
|
||||
this.RaisePropertyChanged(nameof(UnderlineIsSelected));
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(BlockIsSelected));
|
||||
RaisePropertyChanged(nameof(BarIsSelected));
|
||||
RaisePropertyChanged(nameof(UnderlineIsSelected));
|
||||
_isEditingCursorStyle = false;
|
||||
}
|
||||
}
|
||||
@@ -185,10 +199,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
_isEditingScrollBarStyle = true;
|
||||
_terminalOptions.ScrollBarStyle = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
this.RaisePropertyChanged(nameof(HiddenIsSelected));
|
||||
this.RaisePropertyChanged(nameof(AutoHidingIsSelected));
|
||||
this.RaisePropertyChanged(nameof(VisibleIsSelected));
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(HiddenIsSelected));
|
||||
RaisePropertyChanged(nameof(AutoHidingIsSelected));
|
||||
RaisePropertyChanged(nameof(VisibleIsSelected));
|
||||
_isEditingScrollBarStyle = false;
|
||||
}
|
||||
}
|
||||
@@ -206,6 +220,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
ScrollBarStyle = defaults.ScrollBarStyle;
|
||||
FontFamily = defaults.FontFamily;
|
||||
FontSize = defaults.FontSize;
|
||||
BoldText = defaults.BoldText;
|
||||
BackgroundOpacity = defaults.BackgroundOpacity;
|
||||
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
|
||||
}
|
||||
@@ -219,19 +234,10 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
|
||||
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
|
||||
|
||||
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s);
|
||||
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
|
||||
Sizes = Enumerable.Range(1, 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
var activeThemeId = _settingsService.GetCurrentThemeId();
|
||||
foreach (var theme in _settingsService.GetThemes())
|
||||
{
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService);
|
||||
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService, false);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
|
||||
@@ -139,7 +139,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, true);
|
||||
viewModel.EditCommand.Execute(null);
|
||||
viewModel.Activated += OnThemeActivated;
|
||||
viewModel.Deleted += OnThemeDeleted;
|
||||
Themes.Add(viewModel);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
@@ -21,14 +22,16 @@ namespace FluentTerminal.App.ViewModels
|
||||
private bool _inEditMode;
|
||||
private bool _isDefault;
|
||||
private string _location;
|
||||
private LineEndingStyle _lineEndingStyle;
|
||||
private string _name;
|
||||
private bool _isNew;
|
||||
private TabTheme _selectedTabTheme;
|
||||
private TerminalTheme _selectedTerminalTheme;
|
||||
private string _workingDirectory;
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
|
||||
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider)
|
||||
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider, Boolean isNew)
|
||||
{
|
||||
Model = shellProfile;
|
||||
_settingsService = settingsService;
|
||||
@@ -36,6 +39,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
_fileSystemService = fileSystemService;
|
||||
_applicationView = applicationView;
|
||||
_defaultValueProvider = defaultValueProvider;
|
||||
_isNew = isNew;
|
||||
|
||||
_settingsService.ThemeAdded += OnThemeAdded;
|
||||
_settingsService.ThemeDeleted += OnThemeDeleted;
|
||||
@@ -78,6 +82,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
WorkingDirectory = shellProfile.WorkingDirectory;
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == shellProfile.TabThemeId);
|
||||
PreInstalled = shellProfile.PreInstalled;
|
||||
LineEndingStyle = shellProfile.LineEndingTranslation;
|
||||
|
||||
KeyBindings.Clear();
|
||||
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
@@ -191,6 +196,36 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _workingDirectory, value);
|
||||
}
|
||||
|
||||
public LineEndingStyle LineEndingStyle
|
||||
{
|
||||
get => _lineEndingStyle;
|
||||
set => Set(ref _lineEndingStyle, value);
|
||||
}
|
||||
|
||||
public bool DoNotModifyIsSelected
|
||||
{
|
||||
get => LineEndingStyle == LineEndingStyle.DoNotModify;
|
||||
set { if (value) LineEndingStyle = LineEndingStyle.DoNotModify; }
|
||||
}
|
||||
|
||||
public bool ToCRLFIsSelected
|
||||
{
|
||||
get => LineEndingStyle == LineEndingStyle.ToCRLF;
|
||||
set { if (value) LineEndingStyle = LineEndingStyle.ToCRLF; }
|
||||
}
|
||||
|
||||
public bool ToLFIsSelected
|
||||
{
|
||||
get => LineEndingStyle == LineEndingStyle.ToLF;
|
||||
set { if (value) LineEndingStyle = LineEndingStyle.ToLF; }
|
||||
}
|
||||
|
||||
public bool ToCRIsSelected
|
||||
{
|
||||
get => LineEndingStyle == LineEndingStyle.ToCR;
|
||||
set { if (value) LineEndingStyle = LineEndingStyle.ToCR; }
|
||||
}
|
||||
|
||||
public void SaveChanges()
|
||||
{
|
||||
Model.Arguments = Arguments;
|
||||
@@ -199,11 +234,13 @@ namespace FluentTerminal.App.ViewModels
|
||||
Model.WorkingDirectory = WorkingDirectory;
|
||||
Model.TabThemeId = SelectedTabTheme.Id;
|
||||
Model.TerminalThemeId = SelectedTerminalTheme.Id;
|
||||
Model.LineEndingTranslation = _lineEndingStyle;
|
||||
Model.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
|
||||
_settingsService.SaveShellProfile(Model);
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
_isNew = false;
|
||||
}
|
||||
|
||||
private async Task RestoreDefaults()
|
||||
@@ -248,25 +285,53 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
if (_isNew)
|
||||
{
|
||||
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())
|
||||
await Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShellProfile changedProfile = new ShellProfile
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
}
|
||||
Arguments = Arguments,
|
||||
Location = Location,
|
||||
Name = Name,
|
||||
WorkingDirectory = WorkingDirectory,
|
||||
TabThemeId = SelectedTabTheme.Id,
|
||||
TerminalThemeId = SelectedTerminalTheme.Id,
|
||||
LineEndingTranslation = _lineEndingStyle,
|
||||
KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList()
|
||||
};
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
if (!_fallbackProfile.Equals(changedProfile))
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Arguments = _fallbackProfile.Arguments;
|
||||
Location = _fallbackProfile.Location;
|
||||
Name = _fallbackProfile.Name;
|
||||
WorkingDirectory = _fallbackProfile.WorkingDirectory;
|
||||
LineEndingStyle = _fallbackProfile.LineEndingTranslation;
|
||||
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TerminalThemeId);
|
||||
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TabThemeId);
|
||||
|
||||
KeyBindings.KeyBindings.Clear();
|
||||
foreach (var keyBinding in Model.KeyBindings.Select(x => new KeyBinding(x)).ToList())
|
||||
{
|
||||
KeyBindings.Add(keyBinding);
|
||||
}
|
||||
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyBindings.Editable = false;
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +356,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
_fallbackProfile = new ShellProfile
|
||||
{
|
||||
Arguments = Model.Arguments,
|
||||
LineEndingTranslation = Model.LineEndingTranslation,
|
||||
Location = Model.Location,
|
||||
Name = Model.Name,
|
||||
WorkingDirectory = Model.WorkingDirectory,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using FluentTerminal.App.Services.Dialogs;
|
||||
using GalaSoft.MvvmLight;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class SshConnectionInfoViewModel : ViewModelBase, ISshConnectionInfo
|
||||
{
|
||||
private string _host = string.Empty;
|
||||
|
||||
public string Host
|
||||
{
|
||||
get => _host;
|
||||
set => Set(ref _host, value);
|
||||
}
|
||||
|
||||
private ushort _port = 22;
|
||||
|
||||
public ushort Port
|
||||
{
|
||||
get => _port;
|
||||
set => Set(ref _port, value);
|
||||
}
|
||||
|
||||
private string _username = string.Empty;
|
||||
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => Set(ref _username, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,114 @@
|
||||
using Fleck;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class TerminalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ManualResetEventSlim _connectedEvent;
|
||||
private readonly IDialogService _dialogService;
|
||||
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 bool _hasNewOutput;
|
||||
private string _resizeOverlayContent;
|
||||
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 string _tabTitle;
|
||||
private string _shellTitle;
|
||||
private bool _hasCustomTitle;
|
||||
|
||||
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
|
||||
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
|
||||
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
|
||||
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
|
||||
{
|
||||
Title = DefaultTitle;
|
||||
SettingsService = settingsService;
|
||||
SettingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
SettingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
|
||||
SettingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
SettingsService.KeyBindingsChanged += OnKeyBindingsChanged;
|
||||
|
||||
_connectedEvent = new ManualResetEventSlim(false);
|
||||
TrayProcessCommunicationService = trayProcessCommunicationService;
|
||||
|
||||
_settingsService = settingsService;
|
||||
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
|
||||
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
|
||||
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
_settingsService.KeyBindingsChanged += OnKeyBindingsChanged;
|
||||
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
|
||||
|
||||
_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 RelayCommand(async () => await TryClose().ConfigureAwait(false));
|
||||
FindNextCommand = new RelayCommand(async () => await FindNext().ConfigureAwait(false));
|
||||
FindPreviousCommand = new RelayCommand(async () => await FindPrevious().ConfigureAwait(false));
|
||||
FindNextCommand = new RelayCommand(FindNext);
|
||||
FindPreviousCommand = new RelayCommand(FindPrevious);
|
||||
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
|
||||
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
|
||||
EditTitleCommand = new AsyncCommand(EditTitle);
|
||||
|
||||
Terminal = new Terminal(TrayProcessCommunicationService);
|
||||
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
|
||||
Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
Terminal.SizeChanged += Terminal_SizeChanged;
|
||||
Terminal.TitleChanged += Terminal_TitleChanged;
|
||||
Terminal.Closed += Terminal_Closed;
|
||||
}
|
||||
|
||||
public event EventHandler Activated;
|
||||
public event EventHandler Closed;
|
||||
public event EventHandler<string> FindNextRequested;
|
||||
public event EventHandler<string> FindPreviousRequested;
|
||||
public event EventHandler KeyBindingsChanged;
|
||||
public event EventHandler<TerminalOptions> OptionsChanged;
|
||||
public event EventHandler SearchStarted;
|
||||
public event EventHandler<TerminalTheme> ThemeChanged;
|
||||
public event EventHandler<string> ShellTitleChanged;
|
||||
public event EventHandler<string> CustomTitleChanged;
|
||||
|
||||
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 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 bool Initialized { get; private set; }
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
@@ -123,8 +118,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
_applicationView.Title = Title;
|
||||
_applicationView.RunOnDispatcherThread(() => NewOutput = false);
|
||||
HasNewOutput = false;
|
||||
}
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
@@ -135,10 +129,10 @@ namespace FluentTerminal.App.ViewModels
|
||||
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
|
||||
@@ -155,6 +149,10 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public RelayCommand<string> SelectTabThemeCommand { get; }
|
||||
|
||||
public ISettingsService SettingsService { get; }
|
||||
|
||||
public ShellProfile ShellProfile { get; }
|
||||
|
||||
public bool ShowResizeOverlay
|
||||
{
|
||||
get => _showResizeOverlay;
|
||||
@@ -191,118 +189,97 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public string Title
|
||||
public Terminal Terminal { get; private set; }
|
||||
|
||||
public TerminalTheme TerminalTheme
|
||||
{
|
||||
get => _title;
|
||||
get => _terminalTheme;
|
||||
set => Set(ref _terminalTheme, value);
|
||||
}
|
||||
|
||||
public string TabTitle
|
||||
{
|
||||
get => _tabTitle;
|
||||
set
|
||||
{
|
||||
value = string.IsNullOrWhiteSpace(value) ? DefaultTitle : value;
|
||||
|
||||
if (Set(ref _title, value) && IsSelected)
|
||||
if (Set(ref _tabTitle, value))
|
||||
{
|
||||
_applicationView.Title = Title;
|
||||
CustomTitleChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CloseView()
|
||||
public string ShellTitle
|
||||
{
|
||||
_terminalView.Close();
|
||||
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
|
||||
get => _shellTitle;
|
||||
set
|
||||
{
|
||||
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
|
||||
{
|
||||
TabTitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
|
||||
|
||||
public Task Close()
|
||||
{
|
||||
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
|
||||
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
|
||||
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
|
||||
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
|
||||
return Terminal.Close();
|
||||
}
|
||||
|
||||
public void CopyText(string text)
|
||||
{
|
||||
_clipboardService.SetText(text);
|
||||
ClipboardService.SetText(text);
|
||||
}
|
||||
|
||||
public Task FocusTerminal()
|
||||
public async Task EditTitle()
|
||||
{
|
||||
return _terminalView?.FocusTerminal();
|
||||
var result = await DialogService.ShowInputDialogAsync("Edit Title");
|
||||
if (result != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
{
|
||||
_hasCustomTitle = false;
|
||||
TabTitle = ShellTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
_hasCustomTitle = true;
|
||||
TabTitle = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnViewIsReady(ITerminalView terminalView)
|
||||
public void FocusTerminal()
|
||||
{
|
||||
_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))
|
||||
{
|
||||
_shellProfile.WorkingDirectory = _startupDirectory;
|
||||
}
|
||||
|
||||
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile).ConfigureAwait(true);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_terminalId = response.Id;
|
||||
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
|
||||
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
|
||||
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
|
||||
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
|
||||
{
|
||||
_connectedEvent.Wait();
|
||||
_webSocket.Send(t);
|
||||
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
|
||||
{
|
||||
_applicationView.RunOnDispatcherThread(() => NewOutput = true);
|
||||
}
|
||||
});
|
||||
|
||||
DefaultTitle = response.ShellExecutableName;
|
||||
Title = DefaultTitle;
|
||||
|
||||
var webSocketUrl = "ws://127.0.0.1:" + response.Id;
|
||||
var webSocketServer = new WebSocketServer(webSocketUrl);
|
||||
webSocketServer.Start(socket =>
|
||||
{
|
||||
_webSocket = socket;
|
||||
socket.OnOpen = () => _connectedEvent.Set();
|
||||
socket.OnMessage = message => _trayProcessCommunicationService.WriteText(_terminalId, message);
|
||||
});
|
||||
|
||||
await _terminalView.ConnectToSocket(webSocketUrl).ConfigureAwait(true);
|
||||
Initialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _dialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
await FocusTerminal().ConfigureAwait(true);
|
||||
Activated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void CloseSearchPanel()
|
||||
{
|
||||
SearchText = string.Empty;
|
||||
ShowSearchPanel = false;
|
||||
_terminalView.FocusTerminal();
|
||||
FocusTerminal();
|
||||
}
|
||||
|
||||
private Task FindNext()
|
||||
private void FindNext()
|
||||
{
|
||||
return _terminalView.FindNext(SearchText);
|
||||
FindNextRequested?.Invoke(this, SearchText);
|
||||
}
|
||||
|
||||
private Task FindPrevious()
|
||||
private void FindPrevious()
|
||||
{
|
||||
return _terminalView.FindPrevious(SearchText);
|
||||
}
|
||||
|
||||
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<string, ICollection<KeyBinding>> commandKeyBindings, IEnumerable<ShellProfile> profiles)
|
||||
{
|
||||
return commandKeyBindings.Values.SelectMany(k => k).Concat(profiles.SelectMany(x => x.KeyBindings));
|
||||
FindPreviousRequested?.Invoke(this, SearchText);
|
||||
}
|
||||
|
||||
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(() =>
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
ApplicationSettings = e;
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
@@ -312,55 +289,21 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private async void OnCurrentThemeChanged(object sender, Guid e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(async () =>
|
||||
await ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
// only change theme if not overwritten by profile
|
||||
if (_shellProfile.TerminalThemeId == Guid.Empty)
|
||||
if (ShellProfile.TerminalThemeId == Guid.Empty)
|
||||
{
|
||||
var currentTheme = _settingsService.GetTheme(e);
|
||||
var currentTheme = SettingsService.GetTheme(e);
|
||||
TerminalTheme = currentTheme;
|
||||
await _terminalView.ChangeTheme(currentTheme.Colors).ConfigureAwait(true);
|
||||
ThemeChanged?.Invoke(this, currentTheme);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
private void OnResizeOverlayTimerFinished(object sender, object e)
|
||||
@@ -369,36 +312,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
ShowResizeOverlay = false;
|
||||
}
|
||||
|
||||
private void OnTerminalExited(object sender, int e)
|
||||
{
|
||||
if (e == _terminalId)
|
||||
{
|
||||
_applicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
{
|
||||
await _applicationView.RunOnDispatcherThread(async () =>
|
||||
{
|
||||
await _terminalView.ChangeOptions(e).ConfigureAwait(true);
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnTerminalSizeChanged(object sender, TerminalSize e)
|
||||
{
|
||||
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;
|
||||
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
|
||||
}
|
||||
|
||||
private void SelectTabTheme(string name)
|
||||
@@ -406,20 +322,83 @@ namespace FluentTerminal.App.ViewModels
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
|
||||
}
|
||||
|
||||
private async Task EditTitle()
|
||||
private void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
var result = await _dialogService.ShowInputDialogAsync("Edit Title");
|
||||
if (result != null)
|
||||
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
private async void Terminal_KeyboardCommandReceived(object sender, string e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
Title = result;
|
||||
case nameof(Command.Copy):
|
||||
{
|
||||
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
|
||||
ClipboardService.SetText(selection);
|
||||
break;
|
||||
}
|
||||
case nameof(Command.Paste):
|
||||
{
|
||||
string content = await ClipboardService.GetText().ConfigureAwait(true);
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.TranslateLineEndings(content);
|
||||
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case nameof(Command.PasteWithoutNewlines):
|
||||
{
|
||||
string content = await ClipboardService.GetText().ConfigureAwait(true);
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
|
||||
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case nameof(Command.Search):
|
||||
{
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
{
|
||||
SearchStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
_keyboardCommandService.SendCommand(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_SizeChanged(object sender, TerminalSize e)
|
||||
{
|
||||
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
|
||||
ShowResizeOverlay = true;
|
||||
}
|
||||
|
||||
private void Terminal_TitleChanged(object sender, string e)
|
||||
{
|
||||
ShellTitle = e;
|
||||
ShellTitleChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
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);
|
||||
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)
|
||||
{
|
||||
@@ -427,8 +406,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
await Close().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,15 +29,14 @@ namespace FluentTerminal.App.ViewModels
|
||||
private string _cursor;
|
||||
private string _cursorAccent;
|
||||
private string _cyan;
|
||||
private string _fallBackAuthor;
|
||||
private TerminalColors _fallBackColors;
|
||||
private string _fallBackName;
|
||||
private TerminalTheme _fallbackTheme;
|
||||
private string _foreground;
|
||||
private string _green;
|
||||
private bool _inEditMode;
|
||||
private bool _isActive;
|
||||
private string _magenta;
|
||||
private string _name;
|
||||
private bool _isNew;
|
||||
private string _red;
|
||||
private string _selection;
|
||||
private string _white;
|
||||
@@ -46,12 +45,13 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public event EventHandler<string> BackgroundChanged;
|
||||
|
||||
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService)
|
||||
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, bool isNew)
|
||||
{
|
||||
Model = theme;
|
||||
_settingsService = settingsService;
|
||||
_dialogService = dialogService;
|
||||
_fileSystemService = fileSystemService;
|
||||
_isNew = isNew;
|
||||
|
||||
Name = Model.Name;
|
||||
Author = Model.Author;
|
||||
@@ -299,42 +299,89 @@ namespace FluentTerminal.App.ViewModels
|
||||
_settingsService.SaveTheme(Model);
|
||||
|
||||
InEditMode = false;
|
||||
_isNew = false;
|
||||
}
|
||||
|
||||
private async Task CancelEdit()
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
if (_isNew)
|
||||
{
|
||||
Black = _fallBackColors.Black;
|
||||
Red = _fallBackColors.Red;
|
||||
Green = _fallBackColors.Green;
|
||||
Yellow = _fallBackColors.Yellow;
|
||||
Blue = _fallBackColors.Blue;
|
||||
Magenta = _fallBackColors.Magenta;
|
||||
Cyan = _fallBackColors.Cyan;
|
||||
White = _fallBackColors.White;
|
||||
await Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
TerminalTheme changedTheme = new TerminalTheme()
|
||||
{
|
||||
Name = Name,
|
||||
Author = Author,
|
||||
Colors = new TerminalColors()
|
||||
{
|
||||
Black = Black,
|
||||
Red = Red,
|
||||
Green = Green,
|
||||
Yellow = Yellow,
|
||||
Blue = Blue,
|
||||
Magenta = Magenta,
|
||||
Cyan = Cyan,
|
||||
White = White,
|
||||
|
||||
BrightBlack = _fallBackColors.BrightBlack;
|
||||
BrightRed = _fallBackColors.BrightRed;
|
||||
BrightGreen = _fallBackColors.BrightGreen;
|
||||
BrightYellow = _fallBackColors.BrightYellow;
|
||||
BrightBlue = _fallBackColors.BrightBlue;
|
||||
BrightMagenta = _fallBackColors.BrightMagenta;
|
||||
BrightCyan = _fallBackColors.BrightCyan;
|
||||
BrightWhite = _fallBackColors.BrightWhite;
|
||||
BrightBlack = BrightBlack,
|
||||
BrightRed = BrightRed,
|
||||
BrightGreen = BrightGreen,
|
||||
BrightYellow = BrightYellow,
|
||||
BrightBlue = BrightBlue,
|
||||
BrightMagenta = BrightMagenta,
|
||||
BrightCyan = BrightCyan,
|
||||
BrightWhite = BrightWhite,
|
||||
|
||||
Background = _fallBackColors.Background;
|
||||
Foreground = _fallBackColors.Foreground;
|
||||
Cursor = _fallBackColors.Cursor;
|
||||
CursorAccent = _fallBackColors.CursorAccent;
|
||||
Selection = _fallBackColors.Selection;
|
||||
Background = Background,
|
||||
Foreground = Foreground,
|
||||
Cursor = Cursor,
|
||||
CursorAccent = CursorAccent,
|
||||
Selection = Selection
|
||||
}
|
||||
};
|
||||
|
||||
Name = _fallBackName;
|
||||
Author = _fallBackAuthor;
|
||||
if (!_fallbackTheme.Equals(changedTheme))
|
||||
{
|
||||
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
|
||||
|
||||
InEditMode = false;
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
Black = _fallbackTheme.Colors.Black;
|
||||
Red = _fallbackTheme.Colors.Red;
|
||||
Green = _fallbackTheme.Colors.Green;
|
||||
Yellow = _fallbackTheme.Colors.Yellow;
|
||||
Blue = _fallbackTheme.Colors.Blue;
|
||||
Magenta = _fallbackTheme.Colors.Magenta;
|
||||
Cyan = _fallbackTheme.Colors.Cyan;
|
||||
White = _fallbackTheme.Colors.White;
|
||||
|
||||
BrightBlack = _fallbackTheme.Colors.BrightBlack;
|
||||
BrightRed = _fallbackTheme.Colors.BrightRed;
|
||||
BrightGreen = _fallbackTheme.Colors.BrightGreen;
|
||||
BrightYellow = _fallbackTheme.Colors.BrightYellow;
|
||||
BrightBlue = _fallbackTheme.Colors.BrightBlue;
|
||||
BrightMagenta = _fallbackTheme.Colors.BrightMagenta;
|
||||
BrightCyan = _fallbackTheme.Colors.BrightCyan;
|
||||
BrightWhite = _fallbackTheme.Colors.BrightWhite;
|
||||
|
||||
Background = _fallbackTheme.Colors.Background;
|
||||
Foreground = _fallbackTheme.Colors.Foreground;
|
||||
Cursor = _fallbackTheme.Colors.Cursor;
|
||||
CursorAccent = _fallbackTheme.Colors.CursorAccent;
|
||||
Selection = _fallbackTheme.Colors.Selection;
|
||||
|
||||
Name = _fallbackTheme.Name;
|
||||
Author = _fallbackTheme.Author;
|
||||
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
InEditMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,9 +402,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
_fallBackColors = new TerminalColors(Model.Colors);
|
||||
_fallBackName = Name;
|
||||
_fallBackAuthor = Author;
|
||||
_fallbackTheme = new TerminalTheme(Model);
|
||||
InEditMode = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using FluentTerminal.App.Services.EventArgs;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.Core;
|
||||
using Windows.Foundation.Metadata;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Core.Preview;
|
||||
using Windows.UI.ViewManagement;
|
||||
@@ -13,20 +14,37 @@ namespace FluentTerminal.App.Adapters
|
||||
{
|
||||
private readonly ApplicationView _applicationView;
|
||||
private readonly CoreDispatcher _dispatcher;
|
||||
private bool _closed;
|
||||
|
||||
public event CloseRequestedHandler CloseRequested;
|
||||
public event EventHandler Closed;
|
||||
|
||||
public ApplicationViewAdapter()
|
||||
{
|
||||
_applicationView = ApplicationView.GetForCurrentView();
|
||||
_applicationView.Consolidated += _applicationView_Consolidated;
|
||||
_dispatcher = CoreApplication.GetCurrentView().Dispatcher;
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequest;
|
||||
|
||||
Logger.Instance.Debug("Created ApplicationViewAdapter for ApplicationView with Id: {Id}", _applicationView.Id);
|
||||
}
|
||||
|
||||
private void _applicationView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public int Id => _applicationView.Id;
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => _applicationView.Title;
|
||||
set => _applicationView.Title = value;
|
||||
set => _applicationView.Title = value ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool IsApiContractPresent(string api, ushort version)
|
||||
{
|
||||
return ApiInformation.IsApiContractPresent(api, version);
|
||||
}
|
||||
|
||||
public Task RunOnDispatcherThread(Action action)
|
||||
@@ -34,9 +52,16 @@ namespace FluentTerminal.App.Adapters
|
||||
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
|
||||
}
|
||||
|
||||
public Task<bool> TryClose()
|
||||
public async Task<bool> TryClose()
|
||||
{
|
||||
return _applicationView.TryConsolidateAsync().AsTask();
|
||||
if (_closed)
|
||||
{
|
||||
Logger.Instance.Debug("ApplicationViewAdapter.TryClose was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
|
||||
return true;
|
||||
}
|
||||
Logger.Instance.Debug("TryClose ApplicationView with Id: {Id}", _applicationView.Id);
|
||||
_closed = await _applicationView.TryConsolidateAsync().AsTask();
|
||||
return _closed;
|
||||
}
|
||||
|
||||
public bool ToggleFullScreen()
|
||||
@@ -57,6 +82,7 @@ namespace FluentTerminal.App.Adapters
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
var args = new CancelableEventArgs();
|
||||
|
||||
if (CloseRequested != null)
|
||||
{
|
||||
await CloseRequested.Invoke(this, args);
|
||||
@@ -64,6 +90,11 @@ namespace FluentTerminal.App.Adapters
|
||||
|
||||
e.Handled = args.Cancelled;
|
||||
|
||||
if (!e.Handled)
|
||||
{
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
|
||||
}
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
<converters:EnumValueToVisibiltyConverter x:Key="EnumValueToVisibiltyConverter" />
|
||||
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
|
||||
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
|
||||
<converters:EnumToDescriptionConverter x:Key="EnumToDescriptionConverter" />
|
||||
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
+452
-318
@@ -1,319 +1,453 @@
|
||||
using Autofac;
|
||||
using FluentTerminal.App.Adapters;
|
||||
using FluentTerminal.App.Dialogs;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Adapters;
|
||||
using FluentTerminal.App.Services.Dialogs;
|
||||
using FluentTerminal.App.Services.Implementation;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.App.Views;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.Activation;
|
||||
using Windows.ApplicationModel.AppService;
|
||||
using Windows.ApplicationModel.Background;
|
||||
using Windows.ApplicationModel.Core;
|
||||
using Windows.Storage;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.ViewManagement;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using IContainer = Autofac.IContainer;
|
||||
|
||||
namespace FluentTerminal.App
|
||||
{
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
public TaskCompletionSource<int> _trayReady = new TaskCompletionSource<int>();
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private bool _alreadyLaunched;
|
||||
private ApplicationSettings _applicationSettings;
|
||||
private readonly IContainer _container;
|
||||
private readonly List<MainViewModel> _mainViewModels;
|
||||
private SettingsViewModel _settingsViewModel;
|
||||
private int? _settingsWindowId;
|
||||
private IAppServiceConnection _appServiceConnection;
|
||||
private BackgroundTaskDeferral _appServiceDeferral;
|
||||
|
||||
public App()
|
||||
{
|
||||
_mainViewModels = new List<MainViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
|
||||
RoamingSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings),
|
||||
KeyBindings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.KeyBindingsContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
ShellProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings.CreateContainer(Constants.ShellProfilesContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
Themes = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ThemesContainerName, ApplicationDataCreateDisposition.Always))
|
||||
};
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
|
||||
builder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>().SingleInstance();
|
||||
builder.RegisterType<TrayProcessCommunicationService>().As<ITrayProcessCommunicationService>().SingleInstance();
|
||||
builder.RegisterType<DialogService>().As<IDialogService>().SingleInstance();
|
||||
builder.RegisterType<KeyboardCommandService>().As<IKeyboardCommandService>().InstancePerDependency();
|
||||
builder.RegisterType<NotificationService>().As<INotificationService>().InstancePerDependency();
|
||||
builder.RegisterType<UpdateService>().As<IUpdateService>().InstancePerDependency();
|
||||
builder.RegisterType<MainViewModel>().InstancePerDependency();
|
||||
builder.RegisterType<SettingsViewModel>().InstancePerDependency();
|
||||
builder.RegisterType<ThemeParserFactory>().As<IThemeParserFactory>().SingleInstance();
|
||||
builder.RegisterType<ITermThemeParser>().As<IThemeParser>().SingleInstance();
|
||||
builder.RegisterType<FluentTerminalThemeParser>().As<IThemeParser>().SingleInstance();
|
||||
builder.RegisterType<ClipboardService>().As<IClipboardService>().SingleInstance();
|
||||
builder.RegisterType<FileSystemService>().As<IFileSystemService>().SingleInstance();
|
||||
builder.RegisterType<SystemFontService>().As<ISystemFontService>().SingleInstance();
|
||||
builder.RegisterType<ShellProfileSelectionDialog>().As<IShellProfileSelectionDialog>().InstancePerDependency();
|
||||
builder.RegisterType<CreateKeyBindingDialog>().As<ICreateKeyBindingDialog>().InstancePerDependency();
|
||||
builder.RegisterType<InputDialog>().As<IInputDialog>().InstancePerDependency();
|
||||
builder.RegisterType<MessageDialogAdapter>().As<IMessageDialog>().InstancePerDependency();
|
||||
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
|
||||
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
|
||||
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
|
||||
builder.RegisterInstance(applicationDataContainers);
|
||||
|
||||
_container = builder.Build();
|
||||
|
||||
_settingsService = _container.Resolve<ISettingsService>();
|
||||
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
|
||||
_trayProcessCommunicationService = _container.Resolve<ITrayProcessCommunicationService>();
|
||||
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
}
|
||||
|
||||
protected override async void OnActivated(IActivatedEventArgs args)
|
||||
{
|
||||
if (args is CommandLineActivatedEventArgs commandLineActivated)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandLineActivated.Operation.Arguments))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var arguments = commandLineActivated.Operation.Arguments.Split(' ', 2);
|
||||
var command = arguments[0];
|
||||
var parameter = arguments.Length > 1 ? arguments[1] : string.Empty;
|
||||
|
||||
if (_alreadyLaunched)
|
||||
{
|
||||
if (command == "settings")
|
||||
{
|
||||
await ShowSettings().ConfigureAwait(true);
|
||||
}
|
||||
else if (command == "new")
|
||||
{
|
||||
if (_applicationSettings.NewTerminalLocation == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
|
||||
{
|
||||
await _mainViewModels.Last().AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CreateNewTerminalWindow(parameter).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (command == "settings")
|
||||
{
|
||||
var viewModel = _container.Resolve<SettingsViewModel>();
|
||||
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
|
||||
}
|
||||
else if (command == "new")
|
||||
{
|
||||
var viewModel = _container.Resolve<MainViewModel>();
|
||||
await viewModel.AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
|
||||
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
if (!_alreadyLaunched)
|
||||
{
|
||||
var viewModel = _container.Resolve<MainViewModel>();
|
||||
await viewModel.AddTerminal(null, false, Guid.Empty).ConfigureAwait(true);
|
||||
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
|
||||
Window.Current.Activate();
|
||||
}
|
||||
else if (_mainViewModels.Count == 0)
|
||||
{
|
||||
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, string.Empty).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
|
||||
{
|
||||
base.OnBackgroundActivated(args);
|
||||
|
||||
if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails details)
|
||||
{
|
||||
if (details.CallerPackageFamilyName == Package.Current.Id.FamilyName)
|
||||
{
|
||||
_appServiceDeferral = args.TaskInstance.GetDeferral();
|
||||
args.TaskInstance.Canceled += OnTaskCanceled;
|
||||
|
||||
_appServiceConnection = new AppServiceConnectionAdapter(details.AppServiceConnection);
|
||||
|
||||
_trayReady.SetResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
|
||||
{
|
||||
_appServiceDeferral?.Complete();
|
||||
_appServiceDeferral = null;
|
||||
_appServiceConnection = null;
|
||||
|
||||
Application.Current.Exit();
|
||||
}
|
||||
|
||||
private async Task CreateMainView(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
|
||||
{
|
||||
await StartSystemTray().ConfigureAwait(true);
|
||||
|
||||
Frame rootFrame = Window.Current.Content as Frame;
|
||||
if (rootFrame == null)
|
||||
{
|
||||
rootFrame = new Frame();
|
||||
Window.Current.Content = rootFrame;
|
||||
}
|
||||
|
||||
if (rootFrame.Content == null)
|
||||
{
|
||||
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
|
||||
|
||||
if (viewModel is MainViewModel mainViewModel)
|
||||
{
|
||||
mainViewModel.Closed += OnMainViewModelClosed;
|
||||
mainViewModel.NewWindowRequested += OnNewWindowRequested;
|
||||
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
|
||||
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
|
||||
_mainViewModels.Add(mainViewModel);
|
||||
}
|
||||
|
||||
rootFrame.Navigate(pageType, viewModel);
|
||||
}
|
||||
_alreadyLaunched = true;
|
||||
Window.Current.Activate();
|
||||
}
|
||||
|
||||
private async Task CreateNewTerminalWindow(string startupDirectory)
|
||||
{
|
||||
var id = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, startupDirectory).ConfigureAwait(true);
|
||||
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(id);
|
||||
}
|
||||
|
||||
private async Task<int> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar, object parameter)
|
||||
{
|
||||
int windowId = 0;
|
||||
TViewModel viewModel = default;
|
||||
await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
|
||||
{
|
||||
viewModel = _container.Resolve<TViewModel>();
|
||||
|
||||
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = ExtendViewIntoTitleBar;
|
||||
var frame = new Frame();
|
||||
frame.Navigate(pageType, viewModel);
|
||||
Window.Current.Content = frame;
|
||||
Window.Current.Activate();
|
||||
|
||||
windowId = ApplicationView.GetForCurrentView().Id;
|
||||
});
|
||||
|
||||
if (viewModel is MainViewModel mainViewModel && parameter is string directory)
|
||||
{
|
||||
mainViewModel.Closed += OnMainViewModelClosed;
|
||||
mainViewModel.NewWindowRequested += OnNewWindowRequested;
|
||||
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
|
||||
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
|
||||
_mainViewModels.Add(mainViewModel);
|
||||
await mainViewModel.AddTerminal(directory, false, Guid.Empty).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
if (viewModel is SettingsViewModel settingsViewModel)
|
||||
{
|
||||
_settingsViewModel = settingsViewModel;
|
||||
_settingsViewModel.Closed += OnSettingsClosed;
|
||||
}
|
||||
|
||||
return windowId;
|
||||
}
|
||||
|
||||
private void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
{
|
||||
_applicationSettings = e;
|
||||
}
|
||||
|
||||
private void OnMainViewModelClosed(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is MainViewModel viewModel)
|
||||
{
|
||||
viewModel.Closed -= OnMainViewModelClosed;
|
||||
viewModel.NewWindowRequested -= OnNewWindowRequested;
|
||||
viewModel.ShowSettingsRequested -= OnShowSettingsRequested;
|
||||
|
||||
_mainViewModels.Remove(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnNewWindowRequested(object sender, EventArgs e)
|
||||
{
|
||||
await CreateNewTerminalWindow(string.Empty).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void OnSettingsClosed(object sender, EventArgs e)
|
||||
{
|
||||
_settingsViewModel.Closed -= OnSettingsClosed;
|
||||
_settingsViewModel = null;
|
||||
_settingsWindowId = null;
|
||||
}
|
||||
|
||||
private void OnShowAboutRequested(object sender, EventArgs e)
|
||||
{
|
||||
ShowAbout().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async void OnShowSettingsRequested(object sender, EventArgs e)
|
||||
{
|
||||
await ShowSettings().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async Task ShowAbout()
|
||||
{
|
||||
await ShowSettings().ConfigureAwait(true);
|
||||
_settingsViewModel.NavigateToAboutPage();
|
||||
}
|
||||
|
||||
private async Task ShowSettings()
|
||||
{
|
||||
if (_settingsViewModel == null)
|
||||
{
|
||||
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, null).ConfigureAwait(true);
|
||||
}
|
||||
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
|
||||
}
|
||||
|
||||
private async Task StartSystemTray()
|
||||
{
|
||||
var launch = FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("AppLaunchedParameterGroup").AsTask();
|
||||
var clearCache = WebView.ClearTemporaryWebDataAsync().AsTask();
|
||||
await Task.WhenAll(launch, clearCache, _trayReady.Task).ConfigureAwait(true);
|
||||
_trayProcessCommunicationService.Initialize(_appServiceConnection);
|
||||
}
|
||||
}
|
||||
using Autofac;
|
||||
using CommandLine;
|
||||
using FluentTerminal.App.Adapters;
|
||||
using FluentTerminal.App.CommandLineArguments;
|
||||
using FluentTerminal.App.Dialogs;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services.Adapters;
|
||||
using FluentTerminal.App.Services.Dialogs;
|
||||
using FluentTerminal.App.Services.EventArgs;
|
||||
using FluentTerminal.App.Services.Implementation;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.App.Views;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.Activation;
|
||||
using Windows.ApplicationModel.AppService;
|
||||
using Windows.ApplicationModel.Background;
|
||||
using Windows.ApplicationModel.Core;
|
||||
using Windows.Storage;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.ViewManagement;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using IContainer = Autofac.IContainer;
|
||||
|
||||
namespace FluentTerminal.App
|
||||
{
|
||||
public sealed partial class App : Application
|
||||
{
|
||||
public TaskCompletionSource<int> _trayReady = new TaskCompletionSource<int>();
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
|
||||
private bool _alreadyLaunched;
|
||||
private ApplicationSettings _applicationSettings;
|
||||
private readonly IContainer _container;
|
||||
private readonly List<MainViewModel> _mainViewModels;
|
||||
private SettingsViewModel _settingsViewModel;
|
||||
private int? _settingsWindowId;
|
||||
private IAppServiceConnection _appServiceConnection;
|
||||
private BackgroundTaskDeferral _appServiceDeferral;
|
||||
private Parser _commandLineParser;
|
||||
|
||||
public App()
|
||||
{
|
||||
_mainViewModels = new List<MainViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
UnhandledException += OnUnhandledException;
|
||||
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
|
||||
RoamingSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings),
|
||||
KeyBindings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.KeyBindingsContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
ShellProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings.CreateContainer(Constants.ShellProfilesContainerName, ApplicationDataCreateDisposition.Always)),
|
||||
Themes = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ThemesContainerName, ApplicationDataCreateDisposition.Always))
|
||||
};
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
|
||||
builder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>().SingleInstance();
|
||||
builder.RegisterType<TrayProcessCommunicationService>().As<ITrayProcessCommunicationService>().SingleInstance();
|
||||
builder.RegisterType<DialogService>().As<IDialogService>().SingleInstance();
|
||||
builder.RegisterType<KeyboardCommandService>().As<IKeyboardCommandService>().InstancePerDependency();
|
||||
builder.RegisterType<NotificationService>().As<INotificationService>().InstancePerDependency();
|
||||
builder.RegisterType<UpdateService>().As<IUpdateService>().InstancePerDependency();
|
||||
builder.RegisterType<MainViewModel>().InstancePerDependency();
|
||||
builder.RegisterType<SettingsViewModel>().InstancePerDependency();
|
||||
builder.RegisterType<ThemeParserFactory>().As<IThemeParserFactory>().SingleInstance();
|
||||
builder.RegisterType<ITermThemeParser>().As<IThemeParser>().SingleInstance();
|
||||
builder.RegisterType<FluentTerminalThemeParser>().As<IThemeParser>().SingleInstance();
|
||||
builder.RegisterType<ClipboardService>().As<IClipboardService>().SingleInstance();
|
||||
builder.RegisterType<FileSystemService>().As<IFileSystemService>().SingleInstance();
|
||||
builder.RegisterType<SystemFontService>().As<ISystemFontService>().SingleInstance();
|
||||
builder.RegisterType<ShellProfileSelectionDialog>().As<IShellProfileSelectionDialog>().InstancePerDependency();
|
||||
builder.RegisterType<CreateKeyBindingDialog>().As<ICreateKeyBindingDialog>().InstancePerDependency();
|
||||
builder.RegisterType<InputDialog>().As<IInputDialog>().InstancePerDependency();
|
||||
builder.RegisterType<MessageDialogAdapter>().As<IMessageDialog>().InstancePerDependency();
|
||||
builder.RegisterType<SshInfoDialog>().As<ISshConnectionInfoDialog>().InstancePerDependency();
|
||||
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
|
||||
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
|
||||
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
|
||||
builder.RegisterInstance(applicationDataContainers);
|
||||
|
||||
_container = builder.Build();
|
||||
|
||||
_settingsService = _container.Resolve<ISettingsService>();
|
||||
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
|
||||
|
||||
_trayProcessCommunicationService = _container.Resolve<ITrayProcessCommunicationService>();
|
||||
|
||||
_applicationSettings = _settingsService.GetApplicationSettings();
|
||||
|
||||
JsonConvert.DefaultSettings = () =>
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
};
|
||||
settings.Converters.Add(new StringEnumConverter(typeof(CamelCaseNamingStrategy)));
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
_commandLineParser = new Parser(settings =>
|
||||
{
|
||||
settings.CaseSensitive = false;
|
||||
settings.CaseInsensitiveEnumValues = true;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.Instance.Error(e.Exception, "Unhandled Exception");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitArguments(string arguments)
|
||||
{
|
||||
var chars = arguments.ToCharArray();
|
||||
var inQuote = false;
|
||||
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
{
|
||||
if (chars[i] == '"')
|
||||
{
|
||||
inQuote = !inQuote;
|
||||
}
|
||||
|
||||
if (!inQuote && chars[i] == ' ')
|
||||
{
|
||||
chars[i] = '\n';
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var value in new string(chars).Split('\n'))
|
||||
{
|
||||
yield return value.Trim('"');
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnActivated(IActivatedEventArgs args)
|
||||
{
|
||||
if (args is CommandLineActivatedEventArgs commandLineActivated)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandLineActivated.Operation.Arguments))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_commandLineParser.ParseArguments(SplitArguments(commandLineActivated.Operation.Arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
|
||||
{
|
||||
if (verb is SettingsVerb)
|
||||
{
|
||||
await ShowSettings().ConfigureAwait(true);
|
||||
}
|
||||
else if (verb is NewVerb newVerb)
|
||||
{
|
||||
var profile = default(ShellProfile);
|
||||
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
|
||||
{
|
||||
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
|
||||
}
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
profile = _settingsService.GetDefaultShellProfile();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newVerb.Directory))
|
||||
{
|
||||
profile.WorkingDirectory = newVerb.Directory;
|
||||
}
|
||||
|
||||
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
|
||||
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
|
||||
: NewTerminalLocation.Window;
|
||||
|
||||
await CreateTerminal(profile, location).ConfigureAwait(true);
|
||||
}
|
||||
else if (verb is RunVerb runVerb)
|
||||
{
|
||||
var profile = new ShellProfile
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Location = null,
|
||||
Arguments = runVerb.Command,
|
||||
WorkingDirectory = runVerb.Directory
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(runVerb.Theme))
|
||||
{
|
||||
var theme = _settingsService.GetThemes().FirstOrDefault(x => x.Name.Equals(runVerb.Theme, StringComparison.CurrentCultureIgnoreCase));
|
||||
if (theme != null)
|
||||
{
|
||||
profile.TerminalThemeId = theme.Id;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(profile.WorkingDirectory))
|
||||
{
|
||||
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
|
||||
}
|
||||
|
||||
var location = runVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
|
||||
: runVerb.Target == Target.Tab ? NewTerminalLocation.Tab
|
||||
: NewTerminalLocation.Window;
|
||||
|
||||
await CreateTerminal(profile, location).ConfigureAwait(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
if (!_alreadyLaunched)
|
||||
{
|
||||
var logDirectory = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("Logs", CreationCollisionOption.OpenIfExists);
|
||||
var logFile = Path.Combine(logDirectory.Path, "fluentterminal.app.log");
|
||||
var configFile = await logDirectory.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
|
||||
var configContent = await FileIO.ReadTextAsync(configFile);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configContent))
|
||||
{
|
||||
configContent = JsonConvert.SerializeObject(new Logger.Configuration());
|
||||
await FileIO.WriteTextAsync(configFile, configContent);
|
||||
}
|
||||
|
||||
var config = JsonConvert.DeserializeObject<Logger.Configuration>(configContent) ?? new Logger.Configuration();
|
||||
|
||||
Logger.Instance.Initialize(logFile, config);
|
||||
|
||||
var viewModel = _container.Resolve<MainViewModel>();
|
||||
viewModel.AddTerminal();
|
||||
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
|
||||
Window.Current.Activate();
|
||||
}
|
||||
else if (_mainViewModels.Count == 0)
|
||||
{
|
||||
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
|
||||
{
|
||||
base.OnBackgroundActivated(args);
|
||||
|
||||
if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails details)
|
||||
{
|
||||
if (details.CallerPackageFamilyName == Package.Current.Id.FamilyName)
|
||||
{
|
||||
_appServiceDeferral = args.TaskInstance.GetDeferral();
|
||||
args.TaskInstance.Canceled += OnTaskCanceled;
|
||||
|
||||
_appServiceConnection = new AppServiceConnectionAdapter(details.AppServiceConnection);
|
||||
|
||||
_trayReady.SetResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
|
||||
{
|
||||
_appServiceDeferral?.Complete();
|
||||
_appServiceDeferral = null;
|
||||
_appServiceConnection = null;
|
||||
|
||||
Application.Current.Exit();
|
||||
}
|
||||
|
||||
private async Task CreateMainView(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
|
||||
{
|
||||
await StartSystemTray().ConfigureAwait(true);
|
||||
|
||||
Frame rootFrame = Window.Current.Content as Frame;
|
||||
if (rootFrame == null)
|
||||
{
|
||||
rootFrame = new Frame();
|
||||
Window.Current.Content = rootFrame;
|
||||
}
|
||||
|
||||
if (rootFrame.Content == null)
|
||||
{
|
||||
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
|
||||
|
||||
if (viewModel is MainViewModel mainViewModel)
|
||||
{
|
||||
mainViewModel.Closed += OnMainViewModelClosed;
|
||||
mainViewModel.NewWindowRequested += OnNewWindowRequested;
|
||||
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
|
||||
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
|
||||
_mainViewModels.Add(mainViewModel);
|
||||
}
|
||||
|
||||
rootFrame.Navigate(pageType, viewModel);
|
||||
}
|
||||
_alreadyLaunched = true;
|
||||
Window.Current.Activate();
|
||||
}
|
||||
|
||||
private async Task<MainViewModel> CreateNewTerminalWindow()
|
||||
{
|
||||
var viewModel = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
|
||||
viewModel.Closed += OnMainViewModelClosed;
|
||||
viewModel.NewWindowRequested += OnNewWindowRequested;
|
||||
viewModel.ShowSettingsRequested += OnShowSettingsRequested;
|
||||
viewModel.ShowAboutRequested += OnShowAboutRequested;
|
||||
_mainViewModels.Add(viewModel);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar)
|
||||
{
|
||||
int windowId = 0;
|
||||
TViewModel viewModel = default;
|
||||
await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
|
||||
{
|
||||
viewModel = _container.Resolve<TViewModel>();
|
||||
|
||||
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = ExtendViewIntoTitleBar;
|
||||
var frame = new Frame();
|
||||
frame.Navigate(pageType, viewModel);
|
||||
Window.Current.Content = frame;
|
||||
Window.Current.Activate();
|
||||
|
||||
windowId = ApplicationView.GetForCurrentView().Id;
|
||||
});
|
||||
|
||||
if (viewModel is SettingsViewModel settingsViewModel)
|
||||
{
|
||||
_settingsViewModel = settingsViewModel;
|
||||
_settingsViewModel.Closed += OnSettingsClosed;
|
||||
_settingsWindowId = windowId;
|
||||
}
|
||||
|
||||
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(windowId);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
{
|
||||
_applicationSettings = e;
|
||||
}
|
||||
|
||||
private void OnMainViewModelClosed(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is MainViewModel viewModel)
|
||||
{
|
||||
Logger.Instance.Debug("MainViewModel with ApplicationView Id: {@id} closed.", viewModel.ApplicationView.Id);
|
||||
|
||||
viewModel.Closed -= OnMainViewModelClosed;
|
||||
viewModel.NewWindowRequested -= OnNewWindowRequested;
|
||||
viewModel.ShowSettingsRequested -= OnShowSettingsRequested;
|
||||
viewModel.ShowAboutRequested -= OnShowAboutRequested;
|
||||
|
||||
_mainViewModels.Remove(viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
|
||||
{
|
||||
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
|
||||
|
||||
if (e.ShowProfileSelection)
|
||||
{
|
||||
await viewModel.AddConfigurableTerminal().ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
viewModel.AddTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSettingsClosed(object sender, EventArgs e)
|
||||
{
|
||||
_settingsViewModel.Closed -= OnSettingsClosed;
|
||||
_settingsViewModel = null;
|
||||
_settingsWindowId = null;
|
||||
}
|
||||
|
||||
private void OnShowAboutRequested(object sender, EventArgs e)
|
||||
{
|
||||
ShowAbout().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async void OnShowSettingsRequested(object sender, EventArgs e)
|
||||
{
|
||||
await ShowSettings().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async Task ShowAbout()
|
||||
{
|
||||
await ShowSettings().ConfigureAwait(true);
|
||||
_settingsViewModel.NavigateToAboutPage();
|
||||
}
|
||||
|
||||
private async Task CreateTerminal(ShellProfile profile, NewTerminalLocation location)
|
||||
{
|
||||
if (!_alreadyLaunched)
|
||||
{
|
||||
var viewModel = _container.Resolve<MainViewModel>();
|
||||
viewModel.AddTerminal(profile);
|
||||
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
|
||||
}
|
||||
else if (location == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
|
||||
{
|
||||
_mainViewModels.Last().AddTerminal(profile);
|
||||
}
|
||||
else
|
||||
{
|
||||
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
|
||||
viewModel.AddTerminal(profile);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowSettings()
|
||||
{
|
||||
if (!_alreadyLaunched)
|
||||
{
|
||||
var viewModel = _container.Resolve<SettingsViewModel>();
|
||||
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
|
||||
}
|
||||
else if (_settingsViewModel == null)
|
||||
{
|
||||
await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true).ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartSystemTray()
|
||||
{
|
||||
var launch = FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("AppLaunchedParameterGroup").AsTask();
|
||||
await Task.WhenAll(launch, _trayReady.Task).ConfigureAwait(true);
|
||||
_trayProcessCommunicationService.Initialize(_appServiceConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace FluentTerminal.App.CommandLineArguments
|
||||
{
|
||||
[Verb("new")]
|
||||
public class NewVerb
|
||||
{
|
||||
[Value(0)]
|
||||
public string Directory { get; set; }
|
||||
|
||||
[Option("profile")]
|
||||
public string Profile { get; set; }
|
||||
|
||||
[Option("target")]
|
||||
public Target Target { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace FluentTerminal.App.CommandLineArguments
|
||||
{
|
||||
[Verb("run")]
|
||||
public class RunVerb
|
||||
{
|
||||
[Value(0)]
|
||||
public string Command { get; set; }
|
||||
|
||||
[Option("directory")]
|
||||
public string Directory { get; set; }
|
||||
|
||||
[Option("theme")]
|
||||
public string Theme { get; set; }
|
||||
|
||||
[Option("target")]
|
||||
public Target Target { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace FluentTerminal.App.CommandLineArguments
|
||||
{
|
||||
[Verb("settings")]
|
||||
public class SettingsVerb
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace FluentTerminal.App.CommandLineArguments
|
||||
{
|
||||
public enum Target
|
||||
{
|
||||
Default,
|
||||
Tab,
|
||||
Window
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Toolkit.Uwp.Helpers;
|
||||
using System;
|
||||
using Windows.UI;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Data;
|
||||
using Windows.UI.Xaml.Media;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using FluentTerminal.App.Services.Utilities;
|
||||
using Windows.UI.Xaml.Data;
|
||||
|
||||
namespace FluentTerminal.App.Converters
|
||||
{
|
||||
public class EnumToDescriptionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
// value is the Enum, parameter is the string.
|
||||
if (value is Enum enumValue)
|
||||
{
|
||||
return EnumHelper.GetEnumDescription(enumValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Windows.UI.Text;
|
||||
using Windows.UI.Xaml.Data;
|
||||
|
||||
namespace FluentTerminal.App.Converters
|
||||
{
|
||||
public class TrueToBoldConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool bold)
|
||||
{
|
||||
return bold ? FontWeights.Bold : FontWeights.Normal;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
CloseButtonText="Cancel"
|
||||
PrimaryButtonText="OK"
|
||||
KeyDown="Dialog_KeyUp"
|
||||
mc:Ignorable="d">
|
||||
<TextBox VerticalAlignment="Center" Text="{x:Bind Input, Mode=TwoWay}" />
|
||||
</ContentDialog>
|
||||
@@ -2,11 +2,15 @@
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using System;
|
||||
using Windows.UI.Xaml.Input;
|
||||
using Windows.System;
|
||||
|
||||
namespace FluentTerminal.App.Dialogs
|
||||
{
|
||||
public sealed partial class InputDialog : ContentDialog, IInputDialog
|
||||
{
|
||||
|
||||
private bool EnterPressed = false;
|
||||
public string Input { get; private set; }
|
||||
|
||||
public InputDialog()
|
||||
@@ -17,7 +21,7 @@ namespace FluentTerminal.App.Dialogs
|
||||
public async Task<string> GetInput()
|
||||
{
|
||||
var result = await ShowAsync();
|
||||
if (result == ContentDialogResult.Primary)
|
||||
if (result == ContentDialogResult.Primary || EnterPressed)
|
||||
{
|
||||
return Input;
|
||||
}
|
||||
@@ -28,5 +32,14 @@ namespace FluentTerminal.App.Dialogs
|
||||
{
|
||||
Title = title;
|
||||
}
|
||||
|
||||
void Dialog_KeyUp(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Enter)
|
||||
{
|
||||
EnterPressed = true;
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<ContentDialog
|
||||
x:Class="FluentTerminal.App.Dialogs.SshInfoDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:FluentTerminal.App.ViewModels"
|
||||
Title="SSH Info"
|
||||
PrimaryButtonText="OK"
|
||||
SecondaryButtonText="Cancel"
|
||||
mc:Ignorable="d">
|
||||
<ContentDialog.DataContext>
|
||||
<vm:SshConnectionInfoViewModel />
|
||||
</ContentDialog.DataContext>
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBox
|
||||
Width="50"
|
||||
PlaceholderText="user"
|
||||
Text="{Binding Username, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Width="20"
|
||||
FontSize="22"
|
||||
Text="@" />
|
||||
<TextBox
|
||||
Width="120"
|
||||
PlaceholderText="host"
|
||||
Text="{Binding Host, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Width="20"
|
||||
FontSize="22"
|
||||
Text=":" />
|
||||
<TextBox
|
||||
Width="50"
|
||||
PlaceholderText="port"
|
||||
Text="{Binding Port, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using FluentTerminal.App.Services.Dialogs;
|
||||
using FluentTerminal.App.Utilities;
|
||||
using FluentTerminal.App.Services;
|
||||
|
||||
namespace FluentTerminal.App.Dialogs
|
||||
{
|
||||
public sealed partial class SshInfoDialog : ContentDialog, ISshConnectionInfoDialog
|
||||
{
|
||||
public SshInfoDialog(ISettingsService settingsService)
|
||||
{
|
||||
InitializeComponent();
|
||||
var currentTheme = settingsService.GetCurrentTheme();
|
||||
RequestedTheme = ContrastHelper.GetIdealThemeForBackgroundColor(currentTheme.Colors.Background);
|
||||
}
|
||||
|
||||
public async Task<ISshConnectionInfo> GetSshConnectionInfoAsync()
|
||||
{
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
|
||||
return result == ContentDialogResult.Primary ? (ISshConnectionInfo)DataContext : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<WindowsXamlEnableOverview>true</WindowsXamlEnableOverview>
|
||||
<PackageCertificateKeyFile>FluentTerminal.App_StoreKey.pfx</PackageCertificateKeyFile>
|
||||
<PackageCertificateThumbprint>8FA8EF727E767FAE5409DDD1694C1CEE054A2962</PackageCertificateThumbprint>
|
||||
<PackageCertificateKeyFile>FluentTerminal.App_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||
<PackageCertificateThumbprint>F96F3E0E485F07E8E98DA6E5C06AA4B6F112B498</PackageCertificateThumbprint>
|
||||
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
|
||||
@@ -111,8 +111,13 @@
|
||||
<DependentUpon>App.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Behaviors\MiddleClickBehavior.cs" />
|
||||
<Compile Include="CommandLineArguments\NewVerb.cs" />
|
||||
<Compile Include="CommandLineArguments\RunVerb.cs" />
|
||||
<Compile Include="CommandLineArguments\SettingsVerb.cs" />
|
||||
<Compile Include="CommandLineArguments\Target.cs" />
|
||||
<Compile Include="Converters\BackgroundToApplicationThemeConverter.cs" />
|
||||
<Compile Include="Converters\ColorResourceKeyFallbackConverter.cs" />
|
||||
<Compile Include="Converters\EnumToDescriptionConverter.cs" />
|
||||
<Compile Include="Converters\TabColorFallbackConverter.cs" />
|
||||
<Compile Include="Converters\EnumValueToVisibiltyConverter.cs" />
|
||||
<Compile Include="Converters\FalseToVisibleConverter.cs" />
|
||||
@@ -123,6 +128,7 @@
|
||||
<Compile Include="Converters\TabThemeSelectedConverter.cs" />
|
||||
<Compile Include="Converters\TerminalViewModelToViewConverter.cs" />
|
||||
<Compile Include="Converters\ToolTipValueToPixelConverter.cs" />
|
||||
<Compile Include="Converters\TrueToBoldConverter.cs" />
|
||||
<Compile Include="Converters\TrueToVisibleConverter.cs" />
|
||||
<Compile Include="Adapters\MessageDialogAdapter.cs" />
|
||||
<Compile Include="Dialogs\InputDialog.xaml.cs">
|
||||
@@ -137,6 +143,7 @@
|
||||
<Compile Include="Services\SystemFontService.cs" />
|
||||
<Compile Include="Utilities\ColorExtensions.cs" />
|
||||
<Compile Include="Utilities\ContrastHelper.cs" />
|
||||
<Compile Include="Utilities\DebouncedAction.cs" />
|
||||
<Compile Include="Views\KeyBindingsView.xaml.cs">
|
||||
<DependentUpon>KeyBindingsView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -147,6 +154,26 @@
|
||||
<Compile Include="Dialogs\CreateKeyBindingDialog.xaml.cs">
|
||||
<DependentUpon>CreateKeyBindingDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\NativeFrontend\Ansi\AnsiParser.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Ansi\C0.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Ansi\CharAttributes.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Ansi\TerminalCode.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Ansi\TerminalCodeType.cs" />
|
||||
<Compile Include="Views\NativeFrontend\TerminalColorHelper.cs" />
|
||||
<Compile Include="Views\NativeFrontend\TerminalControl.xaml.cs">
|
||||
<DependentUpon>TerminalControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\NativeFrontend\Terminal\SelectionMode.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\SpecialColorIds.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalBuffer.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalBufferChar.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalBufferLine.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalPoint.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalSelection.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalSize.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalTag.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Terminal\TerminalTagArray.cs" />
|
||||
<Compile Include="Views\NativeFrontend\Utility\ArrayReader.cs" />
|
||||
<Compile Include="Views\SettingsPage.xaml.cs">
|
||||
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -171,18 +198,30 @@
|
||||
<Compile Include="Views\SettingsPages\ShellProfileSettings.xaml.cs">
|
||||
<DependentUpon>ShellProfileSettings.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Dialogs\SshInfoDialog.xaml.cs">
|
||||
<DependentUpon>SshInfoDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\TabBar.xaml.cs">
|
||||
<DependentUpon>TabBar.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\TabBarBackgroundBindingHelper.cs" />
|
||||
<Compile Include="Views\TerminalKeybindTemplateSelector.cs" />
|
||||
<Compile Include="Views\TabThemeTemplateSelector.cs" />
|
||||
<Compile Include="Views\TerminalColorPicker.xaml.cs">
|
||||
<DependentUpon>TerminalColorPicker.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\NoValueTemplateSelector.cs" />
|
||||
<Compile Include="Views\TerminalThemeTemplateSelector.cs" />
|
||||
<Compile Include="Views\ITerminalView.cs" />
|
||||
<Compile Include="Views\TerminalView.xaml.cs">
|
||||
<DependentUpon>TerminalView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\VtNetCoreFrontend\VtNetCoreTerminal.xaml.cs">
|
||||
<DependentUpon>VtNetCoreTerminal.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\XtermTerminalView.xaml.cs">
|
||||
<DependentUpon>XtermTerminalView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AppxManifest Include="Package.appxmanifest">
|
||||
@@ -199,7 +238,11 @@
|
||||
<Content Include="Win32\FluentTerminal.SystemTray.pdb" />
|
||||
<Content Include="Win32\GlobalHotKey.dll" />
|
||||
<Content Include="Win32\Newtonsoft.Json.dll" />
|
||||
<Content Include="Win32\PListNet.dll" />
|
||||
<Content Include="Win32\RestSharp.dll" />
|
||||
<Content Include="Win32\Serilog.dll" />
|
||||
<Content Include="Win32\Serilog.Sinks.Async.dll" />
|
||||
<Content Include="Win32\Serilog.Sinks.File.dll" />
|
||||
<Content Include="Win32\System.Data.Common.dll" />
|
||||
<Content Include="Win32\System.Diagnostics.StackTrace.dll" />
|
||||
<Content Include="Win32\System.Diagnostics.Tracing.dll" />
|
||||
@@ -217,7 +260,6 @@
|
||||
<Content Include="Win32\winpty.dll" />
|
||||
<Content Include="Win32\winpty.NET.dll" />
|
||||
<None Include="FluentTerminal.App_StoreKey.pfx" />
|
||||
<None Include="FluentTerminal.App_TemporaryKey.pfx" />
|
||||
<Content Include="Client\bundle.js" />
|
||||
<Content Include="Client\index.html" />
|
||||
<Content Include="Client\style.css" />
|
||||
@@ -270,7 +312,6 @@
|
||||
<Content Include="Assets\AppIcons\Wide310x150Logo.scale-150.png" />
|
||||
<Content Include="Assets\AppIcons\Wide310x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\AppIcons\Wide310x150Logo.scale-400.png" />
|
||||
<None Include="Package.StoreAssociation.xml" />
|
||||
<Content Include="Properties\Default.rd.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -303,6 +344,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\NativeFrontend\TerminalControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\SettingsPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -335,6 +380,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Dialogs\SshInfoDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Views\TabBar.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -347,25 +396,39 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\VtNetCoreFrontend\VtNetCoreTerminal.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\XtermTerminalView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac">
|
||||
<Version>4.8.1</Version>
|
||||
<Version>4.9.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommandLineParser">
|
||||
<Version>2.4.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
|
||||
<Version>6.1.5</Version>
|
||||
<Version>6.2.8</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.UI">
|
||||
<Version>3.0.0</Version>
|
||||
<Version>5.1.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.UI.Controls">
|
||||
<Version>3.0.0</Version>
|
||||
<Version>5.1.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Uwp.Managed">
|
||||
<Version>2.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json">
|
||||
<Version>11.0.2</Version>
|
||||
<Version>12.0.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="VtNetCore">
|
||||
<Version>1.0.15</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Win2D.uwp">
|
||||
<Version>1.21.0</Version>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" IgnorableNamespaces="uap mp desktop rescap">
|
||||
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.3.1.0" />
|
||||
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.4.1.0" />
|
||||
<mp:PhoneIdentity PhoneProductId="f7225014-f405-4342-ad2e-ebf6ac60ea2c" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
|
||||
<Properties>
|
||||
<DisplayName>Fluent Terminal</DisplayName>
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using FluentTerminal.Models;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public class SystemFontService : ISystemFontService
|
||||
{
|
||||
public IEnumerable<string> GetSystemFontFamilies()
|
||||
public IEnumerable<FontInfo> GetSystemFontFamilies()
|
||||
{
|
||||
return CanvasTextFormat.GetSystemFontFamilies();
|
||||
return CanvasFontSet.GetSystemFontSet().Fonts.Select(x => new FontInfo { Name = x.FamilyNames.Values.FirstOrDefault(), IsMonospaced = x.IsMonospaced }).Distinct(new FontInfoComparer());
|
||||
}
|
||||
|
||||
private class FontInfoComparer : IEqualityComparer<FontInfo>
|
||||
{
|
||||
public bool Equals(FontInfo x, FontInfo y)
|
||||
{
|
||||
return x.Name == y.Name;
|
||||
}
|
||||
|
||||
public int GetHashCode(FontInfo obj)
|
||||
{
|
||||
return obj.Name.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Thickness x:Key="HeaderMargin">24,0,0,0</Thickness>
|
||||
<Thickness x:Key="ContainerMargin">24,0,24,24</Thickness>
|
||||
<Thickness x:Key="ItemMargin">0,24,0,0</Thickness>
|
||||
|
||||
<Style
|
||||
x:Key="WindowTitleTextBlockStyle"
|
||||
BasedOn="{StaticResource CaptionTextBlockStyle}"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Xaml;
|
||||
|
||||
namespace FluentTerminal.App.Utilities
|
||||
{
|
||||
public class DebouncedAction<T>
|
||||
{
|
||||
private DispatcherTimer timer;
|
||||
private readonly CoreDispatcher _dispatcher;
|
||||
private readonly TimeSpan _interval;
|
||||
private readonly Action<T> _action;
|
||||
private T _parameter;
|
||||
|
||||
public DebouncedAction(CoreDispatcher dispatcher, TimeSpan interval, Action<T> action)
|
||||
{
|
||||
_dispatcher = dispatcher;
|
||||
_interval = interval;
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public void Invoke(T parameter)
|
||||
{
|
||||
_parameter = parameter;
|
||||
|
||||
timer?.Stop();
|
||||
timer = null;
|
||||
|
||||
timer = new DispatcherTimer
|
||||
{
|
||||
Interval = _interval
|
||||
};
|
||||
timer.Tick += (s, e) =>
|
||||
{
|
||||
if (timer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer?.Stop();
|
||||
timer = null;
|
||||
|
||||
_dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => _action.Invoke(_parameter));
|
||||
};
|
||||
|
||||
timer.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views
|
||||
{
|
||||
public interface ITerminalView
|
||||
{
|
||||
Task ChangeTheme(TerminalTheme theme);
|
||||
Task ChangeKeyBindings();
|
||||
Task ChangeOptions(TerminalOptions options);
|
||||
Task Initialize(TerminalViewModel viewModel);
|
||||
Task FindNext(string searchText);
|
||||
Task FindPrevious(string searchText);
|
||||
Task FocusTerminal();
|
||||
}
|
||||
}
|
||||
@@ -76,12 +76,108 @@
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
<TextBlock
|
||||
<RelativePanel
|
||||
Grid.Column="1"
|
||||
IsHitTestVisible="False"
|
||||
Style="{StaticResource WindowTitleTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedTerminal.Title, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ShowTabsOnTop, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
|
||||
Height="32"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabColorFallbackConverter}, ConverterParameter=Background}">
|
||||
<RelativePanel.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutSubItem Text="Color">
|
||||
<ToggleMenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
|
||||
CommandParameter="None"
|
||||
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=None}"
|
||||
Text="None">
|
||||
<ToggleMenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="Segoe MDL2 Assets" Glyph="" />
|
||||
</ToggleMenuFlyoutItem.Icon>
|
||||
</ToggleMenuFlyoutItem>
|
||||
<ToggleMenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
|
||||
CommandParameter="Red"
|
||||
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Red}"
|
||||
Text="Red">
|
||||
<ToggleMenuFlyoutItem.Icon>
|
||||
<FontIcon
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="#E81123"
|
||||
Glyph="" />
|
||||
</ToggleMenuFlyoutItem.Icon>
|
||||
</ToggleMenuFlyoutItem>
|
||||
<ToggleMenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
|
||||
CommandParameter="Green"
|
||||
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Green}"
|
||||
Text="Green">
|
||||
<ToggleMenuFlyoutItem.Icon>
|
||||
<FontIcon
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="#10893E"
|
||||
Glyph="" />
|
||||
</ToggleMenuFlyoutItem.Icon>
|
||||
</ToggleMenuFlyoutItem>
|
||||
<ToggleMenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
|
||||
CommandParameter="Blue"
|
||||
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Blue}"
|
||||
Text="Blue">
|
||||
<ToggleMenuFlyoutItem.Icon>
|
||||
<FontIcon
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="#0078D7"
|
||||
Glyph="" />
|
||||
</ToggleMenuFlyoutItem.Icon>
|
||||
</ToggleMenuFlyoutItem>
|
||||
<ToggleMenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
|
||||
CommandParameter="Purple"
|
||||
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Purple}"
|
||||
Text="Purple">
|
||||
<ToggleMenuFlyoutItem.Icon>
|
||||
<FontIcon
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="#881798"
|
||||
Glyph="" />
|
||||
</ToggleMenuFlyoutItem.Icon>
|
||||
</ToggleMenuFlyoutItem>
|
||||
<ToggleMenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
|
||||
CommandParameter="Orange"
|
||||
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Orange}"
|
||||
Text="Orange">
|
||||
<ToggleMenuFlyoutItem.Icon>
|
||||
<FontIcon
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="#FF8C00"
|
||||
Glyph="" />
|
||||
</ToggleMenuFlyoutItem.Icon>
|
||||
</ToggleMenuFlyoutItem>
|
||||
<ToggleMenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
|
||||
CommandParameter="Teal"
|
||||
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Teal}"
|
||||
Text="Teal">
|
||||
<ToggleMenuFlyoutItem.Icon>
|
||||
<FontIcon
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
Foreground="#00B7C3"
|
||||
Glyph="" />
|
||||
</ToggleMenuFlyoutItem.Icon>
|
||||
</ToggleMenuFlyoutItem>
|
||||
</MenuFlyoutSubItem>
|
||||
<MenuFlyoutItem Command="{x:Bind ViewModel.SelectedTerminal.EditTitleCommand}" Text="Edit Title" />
|
||||
</MenuFlyout>
|
||||
</RelativePanel.ContextFlyout>
|
||||
<TextBlock
|
||||
Margin="12,8,12,0"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.WindowTitle, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind ViewModel.ShowTabsOnTop, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
|
||||
</RelativePanel>
|
||||
<views:TabBar
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Left"
|
||||
|
||||
@@ -62,11 +62,11 @@ namespace FluentTerminal.App.Views
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnWindowActivated(object sender, WindowActivatedEventArgs e)
|
||||
private void OnWindowActivated(object sender, WindowActivatedEventArgs e)
|
||||
{
|
||||
if (e.WindowActivationState != CoreWindowActivationState.Deactivated && TerminalContainer.Content is TerminalView terminal)
|
||||
{
|
||||
await terminal.FocusWebView().ConfigureAwait(true);
|
||||
terminal.ViewModel.FocusTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
using FluentTerminal.App.Views.NativeFrontend.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Ansi
|
||||
{
|
||||
internal class AnsiParser
|
||||
{
|
||||
private ParserState _state;
|
||||
private int _position;
|
||||
private object _currentParam;
|
||||
private List<object> _params = new List<object>();
|
||||
private List<TerminalCode> _terminalCodes = new List<TerminalCode>();
|
||||
|
||||
/// <summary>
|
||||
/// CSI codes can have prefixes of '?', '>'
|
||||
/// </summary>
|
||||
private string _prefix;
|
||||
|
||||
/// <summary>
|
||||
/// CSI codes can have postfixes of '$',
|
||||
/// </summary>
|
||||
private string _postfix;
|
||||
|
||||
private readonly StringBuilder _currentTextEmit = new StringBuilder(capacity: 128);
|
||||
|
||||
public TerminalCode[] Parse(ArrayReader<char> reader)
|
||||
{
|
||||
_terminalCodes.Clear();
|
||||
while (reader.TryRead(out char ch))
|
||||
{
|
||||
int code = ch;
|
||||
// if (0xD800 <= code && code <= 0xDBFF)
|
||||
// {
|
||||
// // we got a surrogate high
|
||||
// // get surrogate low (next 2 bytes)
|
||||
// if (!reader.TryPeek(out char low))
|
||||
// {
|
||||
// // end of data stream, save surrogate high
|
||||
// // this._terminal.surrogate_high = ch;
|
||||
// continue;
|
||||
// }
|
||||
// code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
|
||||
// // ch += low;
|
||||
// }
|
||||
// // surrogate low - already handled above
|
||||
// if (0xDC00 <= code && code <= 0xDFFF)
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case ParserState.Normal:
|
||||
if (!NormalStateHandler(ch))
|
||||
{
|
||||
Emit(ch);
|
||||
}
|
||||
break;
|
||||
case ParserState.Escaped:
|
||||
if (!EscapedStateHandler(ch))
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
// ESC (,),*,+,-,. Designate G0-G2 Character Set.
|
||||
case '(': // <-- this seems to get all the attention
|
||||
// this._terminal.gcharset = 0;
|
||||
_state = ParserState.Charset;
|
||||
break;
|
||||
case ')':
|
||||
case '-':
|
||||
// this._terminal.gcharset = 1;
|
||||
_state = ParserState.Charset;
|
||||
break;
|
||||
case '*':
|
||||
case '.':
|
||||
// this._terminal.gcharset = 2;
|
||||
_state = ParserState.Charset;
|
||||
break;
|
||||
case '+':
|
||||
// this._terminal.gcharset = 3;
|
||||
_state = ParserState.Charset;
|
||||
break;
|
||||
|
||||
// Designate G3 Character Set (VT300).
|
||||
// A = ISO Latin-1 Supplemental.
|
||||
// Not implemented.
|
||||
case '/':
|
||||
// _terminal.gcharset = 3;
|
||||
_state = ParserState.Charset;
|
||||
// _position--
|
||||
break;
|
||||
|
||||
// ESC N
|
||||
// Single Shift Select of G2 Character Set
|
||||
// ( SS2 is 0x8e). This affects next character only.
|
||||
case 'N':
|
||||
break;
|
||||
// ESC O
|
||||
// Single Shift Select of G3 Character Set
|
||||
// ( SS3 is 0x8f). This affects next character only.
|
||||
case 'O':
|
||||
break;
|
||||
// ESC n
|
||||
// Invoke the G2 Character Set as GL (LS2).
|
||||
case 'n':
|
||||
// this._terminal.setgLevel(2);
|
||||
break;
|
||||
// ESC o
|
||||
// Invoke the G3 Character Set as GL (LS3).
|
||||
case 'o':
|
||||
// this._terminal.setgLevel(3);
|
||||
break;
|
||||
// ESC |
|
||||
// Invoke the G3 Character Set as GR (LS3R).
|
||||
case '|':
|
||||
// this._terminal.setgLevel(3);
|
||||
break;
|
||||
// ESC }
|
||||
// Invoke the G2 Character Set as GR (LS2R).
|
||||
case '}':
|
||||
// this._terminal.setgLevel(2);
|
||||
break;
|
||||
// ESC ~
|
||||
// Invoke the G1 Character Set as GR (LS1R).
|
||||
case '~':
|
||||
// this._terminal.setgLevel(1);
|
||||
break;
|
||||
|
||||
// ESC 7 Save Cursor (DECSC).
|
||||
case '7':
|
||||
Emit(TerminalCodeType.SaveCursor);
|
||||
_state = ParserState.Normal;
|
||||
break;
|
||||
|
||||
// ESC 8 Restore Cursor (DECRC).
|
||||
case '8':
|
||||
Emit(TerminalCodeType.RestoreCursor);
|
||||
_state = ParserState.Normal;
|
||||
break;
|
||||
|
||||
// ESC # 3 DEC line height/width
|
||||
case '#':
|
||||
_state = ParserState.Normal;
|
||||
// _position++;
|
||||
break;
|
||||
|
||||
// ESC H Tab Set (HTS is 0x88).
|
||||
case 'H':
|
||||
Emit(TerminalCodeType.TabSet);
|
||||
_state = ParserState.Normal;
|
||||
break;
|
||||
|
||||
// ESC = Application Keypad (DECKPAM).
|
||||
case '=':
|
||||
// this._terminal.log('Serial port requested application keypad.');
|
||||
// this._terminal.applicationKeypad = true;
|
||||
// this._terminal.viewport.syncScrollArea();
|
||||
_state = ParserState.Normal;
|
||||
break;
|
||||
|
||||
// ESC > Normal Keypad (DECKPNM).
|
||||
case '>':
|
||||
// this._terminal.log('Switching back to normal keypad.');
|
||||
// this._terminal.applicationKeypad = false;
|
||||
// this._terminal.viewport.syncScrollArea();
|
||||
_state = ParserState.Normal;
|
||||
break;
|
||||
|
||||
default:
|
||||
_state = ParserState.Normal;
|
||||
// _terminal.error('Unknown ESC control: %s.', ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserState.Charset:
|
||||
// if (ch in CHARSETS)
|
||||
// {
|
||||
// cs = CHARSETS[ch];
|
||||
// if (ch === '/')
|
||||
// { // ISOLatin is actually /A
|
||||
// this.skipNextChar();
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// cs = DEFAULT_CHARSET;
|
||||
// }
|
||||
// this._terminal.setgCharset(this._terminal.gcharset, cs);
|
||||
// this._terminal.gcharset = null;
|
||||
_state = ParserState.Normal;
|
||||
break;
|
||||
case ParserState.Osc:
|
||||
// OSC Ps ; Pt ST
|
||||
// OSC Ps ; Pt BEL
|
||||
// Set Text Parameters.
|
||||
if (ch == C0.ESC || ch == C0.BEL)
|
||||
{
|
||||
if (ch == C0.ESC)
|
||||
{
|
||||
_position++;
|
||||
}
|
||||
_params.Add(_currentParam);
|
||||
switch ((int)_params[0])
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
if (_params.Count >= 1 && _params[1] != null)
|
||||
{
|
||||
Emit(new TerminalCode(TerminalCodeType.SetTitle, (string)_params[1]));
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// set X property
|
||||
break;
|
||||
case 4:
|
||||
case 5:
|
||||
// change dynamic colors
|
||||
break;
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
case 13:
|
||||
case 14:
|
||||
case 15:
|
||||
case 16:
|
||||
case 17:
|
||||
case 18:
|
||||
case 19:
|
||||
// change dynamic ui colors
|
||||
break;
|
||||
case 46:
|
||||
// change log file
|
||||
break;
|
||||
case 50:
|
||||
// dynamic font
|
||||
break;
|
||||
case 51:
|
||||
// emacs shell
|
||||
break;
|
||||
case 52:
|
||||
// manipulate selection data
|
||||
break;
|
||||
case 104:
|
||||
case 105:
|
||||
case 110:
|
||||
case 111:
|
||||
case 112:
|
||||
case 113:
|
||||
case 114:
|
||||
case 115:
|
||||
case 116:
|
||||
case 117:
|
||||
case 118:
|
||||
// reset colors
|
||||
break;
|
||||
}
|
||||
|
||||
_params.Clear();
|
||||
_currentParam = 0;
|
||||
_state = ParserState.Normal;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_params.Count == 0)
|
||||
{
|
||||
if (ch >= '0' && ch <= '9')
|
||||
{
|
||||
_currentParam = ((int)_currentParam * 10) + ch - 48;
|
||||
}
|
||||
else if (ch == ';')
|
||||
{
|
||||
_params.Add(_currentParam);
|
||||
_currentParam = "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentParam = (string)_currentParam + ch;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserState.CsiParam:
|
||||
if (!CsiParamStateHandler(ch))
|
||||
{
|
||||
FinaliseParam();
|
||||
_state = ParserState.Csi;
|
||||
goto case ParserState.Csi;
|
||||
}
|
||||
break;
|
||||
case ParserState.Csi:
|
||||
if (!CsiStateHandler(ch))
|
||||
{
|
||||
//throw new Exception($"Unknown CSI code: {ch}"); // conpty is sending 'X' ?!
|
||||
}
|
||||
_state = ParserState.Normal;
|
||||
_prefix = "";
|
||||
_postfix = "";
|
||||
break;
|
||||
|
||||
case ParserState.Dcs:
|
||||
if (ch == C0.ESC || ch == C0.BEL)
|
||||
{
|
||||
if (ch == C0.ESC)
|
||||
{
|
||||
// _position++;
|
||||
}
|
||||
switch (_prefix)
|
||||
{
|
||||
// User-Defined Keys (DECUDK)
|
||||
case "":
|
||||
break;
|
||||
|
||||
// Request Status String (DECRQSS).
|
||||
// test: echo -e '\eP$q"p\e\\'
|
||||
case "$q":
|
||||
{
|
||||
string pt = (string)_currentParam;
|
||||
bool valid = false;
|
||||
switch (pt)
|
||||
{
|
||||
// DECSCA
|
||||
case "\"q":
|
||||
pt = "0\"q";
|
||||
break;
|
||||
|
||||
// DECSCL
|
||||
case "\"p":
|
||||
pt = "61\"p";
|
||||
break;
|
||||
|
||||
// DECSTBM
|
||||
case "r":
|
||||
// pt = ""
|
||||
// + (this._terminal.scrollTop + 1)
|
||||
// + ";"
|
||||
// + (this._terminal.scrollBottom + 1)
|
||||
// + "r";
|
||||
break;
|
||||
|
||||
// SGR
|
||||
case "m":
|
||||
pt = "0m";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception($"Unknown DCS Pt: {pt}.");
|
||||
pt = "";
|
||||
break;
|
||||
}
|
||||
|
||||
// this._terminal.send(C0.ESC + 'P' + +valid + '$r' + pt + C0.ESC + '\\');
|
||||
break;
|
||||
}
|
||||
|
||||
// Set Termcap/Terminfo Data (xterm, experimental).
|
||||
case "+p":
|
||||
break;
|
||||
|
||||
// Request Termcap/Terminfo String (xterm, experimental)
|
||||
// Regular xterm does not even respond to this sequence.
|
||||
// This can cause a small glitch in vim.
|
||||
// test: echo -ne '\eP+q6b64\e\\'
|
||||
case "+q":
|
||||
{
|
||||
string pt = (string)_currentParam;
|
||||
bool valid = false;
|
||||
// this._terminal.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\');
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Exception($"Unknown DCS prefix: {_prefix}");
|
||||
break;
|
||||
}
|
||||
|
||||
_currentParam = 0;
|
||||
_prefix = "";
|
||||
_state = ParserState.Normal;
|
||||
}
|
||||
else if (_currentParam == null)
|
||||
{
|
||||
if (_prefix.Length == 0 && ch != '$' && ch != '+')
|
||||
{
|
||||
_currentParam = ch;
|
||||
}
|
||||
else if (_prefix.Length == 2)
|
||||
{
|
||||
_currentParam = ch;
|
||||
}
|
||||
else
|
||||
{
|
||||
_prefix += ch;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentParam = (string)_currentParam + ch;
|
||||
}
|
||||
break;
|
||||
case ParserState.Ignore:
|
||||
// For PM and APC.
|
||||
if (ch == C0.ESC || ch == C0.BEL)
|
||||
{
|
||||
if (ch == C0.ESC)
|
||||
{
|
||||
_position++;
|
||||
}
|
||||
_state = ParserState.Normal;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
FinialiseEmit();
|
||||
return _terminalCodes.ToArray();
|
||||
}
|
||||
|
||||
private bool NormalStateHandler(char c)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case C0.BEL:
|
||||
Emit(TerminalCodeType.Bell);
|
||||
return true;
|
||||
case C0.LF:
|
||||
case C0.VT:
|
||||
case C0.FF:
|
||||
Emit(TerminalCodeType.LineFeed);
|
||||
return true;
|
||||
case C0.CR:
|
||||
Emit(TerminalCodeType.CarriageReturn);
|
||||
return true;
|
||||
case C0.BS:
|
||||
Emit(TerminalCodeType.Backspace);
|
||||
return true;
|
||||
case C0.HT:
|
||||
Emit(TerminalCodeType.Tab);
|
||||
return true;
|
||||
case C0.SO:
|
||||
Emit(TerminalCodeType.ShiftOut);
|
||||
return true;
|
||||
case C0.SI:
|
||||
Emit(TerminalCodeType.ShiftIn);
|
||||
return true;
|
||||
case C0.ESC:
|
||||
_state = ParserState.Escaped;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool EscapedStateHandler(char ch)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case '[':
|
||||
// ESC [ Control Sequence Introducer (CSI is 0x9b)
|
||||
_params.Clear();
|
||||
_currentParam = 0;
|
||||
_state = ParserState.CsiParam;
|
||||
return true;
|
||||
case ']':
|
||||
// ESC ] Operating System Command (OSC is 0x9d)
|
||||
_params.Clear();
|
||||
_currentParam = 0;
|
||||
_state = ParserState.Osc;
|
||||
return true;
|
||||
case 'P':
|
||||
// ESC P Device Control String (DCS is 0x90)
|
||||
_params.Clear();
|
||||
_currentParam = 0;
|
||||
_state = ParserState.Dcs;
|
||||
return true;
|
||||
case '_':
|
||||
// ESC _ Application Program Command ( APC is 0x9f).
|
||||
_state = ParserState.Ignore;
|
||||
return true;
|
||||
case '^':
|
||||
// ESC ^ Privacy Message ( PM is 0x9e).
|
||||
_state = ParserState.Ignore;
|
||||
return true;
|
||||
case 'c':
|
||||
// ESC c Full Reset (RIS).
|
||||
Emit(TerminalCodeType.Reset);
|
||||
return true;
|
||||
case 'E':
|
||||
// ESC E Next Line ( NEL is 0x85).
|
||||
// terminal.x = 0;
|
||||
// termina.index();
|
||||
_state = ParserState.Normal;
|
||||
return true;
|
||||
case 'D':
|
||||
// ESC D Index ( IND is 0x84).
|
||||
// terminal.index();
|
||||
_state = ParserState.Normal;
|
||||
return true;
|
||||
case 'M':
|
||||
// ESC M Reverse Index ( RI is 0x8d).
|
||||
// terminal.reverseIndex();
|
||||
_state = ParserState.Normal;
|
||||
return true;
|
||||
case '%':
|
||||
// ESC % Select default/utf-8 character set.
|
||||
// @ = default, G = utf-8
|
||||
// terminal.setgLevel(0);
|
||||
// terminal.setgCharset(0, DEFAULT_CHARSET); // US (default)
|
||||
_state = ParserState.Normal;
|
||||
// parser.skipNextChar();
|
||||
return true;
|
||||
case C0.CAN:
|
||||
_state = ParserState.Normal;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CsiStateHandler(char ch)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case 'A':
|
||||
Emit(new TerminalCode(TerminalCodeType.CursorUp, (int)_params[0], 0));
|
||||
return true;
|
||||
case 'G':
|
||||
Emit(new TerminalCode(TerminalCodeType.CursorCharAbsolute, 0, (int)_params[0] - 1));
|
||||
return true;
|
||||
case 'H':
|
||||
var line = _params.Count > 1 ? (int)_params[1] - 1 : 0; // make conpty happy
|
||||
Emit(new TerminalCode(TerminalCodeType.CursorPosition, line, (int)_params[0] - 1));
|
||||
return true;
|
||||
case 'K':
|
||||
Emit(TerminalCodeType.EraseInLine);
|
||||
return true;
|
||||
case 'J':
|
||||
Emit(TerminalCodeType.EraseInDisplay);
|
||||
return true;
|
||||
case 'h':
|
||||
Emit(TerminalCodeType.SetMode);
|
||||
return true;
|
||||
case 'l':
|
||||
Emit(TerminalCodeType.ResetMode);
|
||||
return true;
|
||||
case 'n':
|
||||
return true;
|
||||
case 'm':
|
||||
Emit(new TerminalCode(TerminalCodeType.CharAttributes, GetCharAttributes()));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CsiParamStateHandler(char ch)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case '?':
|
||||
case '>':
|
||||
case '!':
|
||||
case '$':
|
||||
case '"':
|
||||
case ' ':
|
||||
case '\'':
|
||||
_prefix = ch.ToString();
|
||||
return true;
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
_currentParam = (int)_currentParam * 10 + (ch - '0');
|
||||
return true;
|
||||
case ';':
|
||||
FinaliseParam();
|
||||
return true;
|
||||
case C0.CAN:
|
||||
_state = ParserState.Normal;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void FinaliseParam()
|
||||
{
|
||||
_params.Add(_currentParam);
|
||||
_currentParam = 0;
|
||||
}
|
||||
|
||||
private CharAttributes GetCharAttributes()
|
||||
{
|
||||
var attributes = new CharAttributes();
|
||||
for (int i = 0; i < _params.Count; i++)
|
||||
{
|
||||
int p = (int)_params[i];
|
||||
if (p >= 30 && p <= 37)
|
||||
{
|
||||
// fg color 8
|
||||
attributes.ForegroundColor = p - 30;
|
||||
}
|
||||
else if (p >= 40 && p <= 47)
|
||||
{
|
||||
// bg color 8
|
||||
attributes.BackgroundColor = p - 40;
|
||||
}
|
||||
else if (p >= 90 && p <= 97)
|
||||
{
|
||||
// fg color 16
|
||||
p += 8;
|
||||
attributes.ForegroundColor = p - 90;
|
||||
}
|
||||
else if (p == 0)
|
||||
{
|
||||
attributes.Flags = 0;
|
||||
attributes.ForegroundColor = 0;
|
||||
attributes.BackgroundColor = 0;
|
||||
}
|
||||
else if (p == 1)
|
||||
{
|
||||
attributes.Flags |= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private void Emit(TerminalCodeType type)
|
||||
{
|
||||
Emit(new TerminalCode(type));
|
||||
}
|
||||
|
||||
private void Emit(char c)
|
||||
{
|
||||
_currentTextEmit.Append(c);
|
||||
}
|
||||
|
||||
private void Emit(TerminalCode code)
|
||||
{
|
||||
FinialiseEmit();
|
||||
_terminalCodes.Add(code);
|
||||
}
|
||||
|
||||
private void FinialiseEmit()
|
||||
{
|
||||
if (_currentTextEmit.Length != 0)
|
||||
{
|
||||
string text = _currentTextEmit.ToString();
|
||||
_currentTextEmit.Clear();
|
||||
_terminalCodes.Add(new TerminalCode(TerminalCodeType.Text, text));
|
||||
}
|
||||
}
|
||||
|
||||
private enum ParserState
|
||||
{
|
||||
Normal,
|
||||
Escaped,
|
||||
CsiParam,
|
||||
Csi,
|
||||
Osc,
|
||||
Charset,
|
||||
Dcs,
|
||||
Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Ansi
|
||||
{
|
||||
public class C0
|
||||
{
|
||||
/// <summary>Null (Caret = ^@, C = \0)</summary>
|
||||
public const char NUL = (char)0x00;
|
||||
/// <summary>Start of Heading (Caret = ^A)</summary>
|
||||
public const char SOH = (char)0x01;
|
||||
/// <summary>Start of Text (Caret = ^B)</summary>
|
||||
public const char STX = (char)0x02;
|
||||
/// <summary>End of Text (Caret = ^C)</summary>
|
||||
public const char ETX = (char)0x03;
|
||||
/// <summary>End of Transmission (Caret = ^D)</summary>
|
||||
public const char EOT = (char)0x04;
|
||||
/// <summary>Enquiry (Caret = ^E)</summary>
|
||||
public const char ENQ = (char)0x05;
|
||||
/// <summary>Acknowledge (Caret = ^F)</summary>
|
||||
public const char ACK = (char)0x06;
|
||||
/// <summary>Bell (Caret = ^G, C = \a)</summary>
|
||||
public const char BEL = (char)0x07;
|
||||
/// <summary>Backspace (Caret = ^H, C = \b)</summary>
|
||||
public const char BS = (char)0x08;
|
||||
/// <summary>Character Tabulation, Horizontal Tabulation (Caret = ^I, C = \t)</summary>
|
||||
public const char HT = (char)0x09;
|
||||
/// <summary>Line Feed (Caret = ^J, C = \n)</summary>
|
||||
public const char LF = (char)0x0a;
|
||||
/// <summary>Line Tabulation, Vertical Tabulation (Caret = ^K, C = \v)</summary>
|
||||
public const char VT = (char)0x0b;
|
||||
/// <summary>Form Feed (Caret = ^L, C = \f)</summary>
|
||||
public const char FF = (char)0x0c;
|
||||
/// <summary>Carriage Return (Caret = ^M, C = \r)</summary>
|
||||
public const char CR = (char)0x0d;
|
||||
/// <summary>Shift Out (Caret = ^N)</summary>
|
||||
public const char SO = (char)0x0e;
|
||||
/// <summary>Shift In (Caret = ^O)</summary>
|
||||
public const char SI = (char)0x0f;
|
||||
/// <summary>Data Link Escape (Caret = ^P)</summary>
|
||||
public const char DLE = (char)0x10;
|
||||
/// <summary>Device Control One (XON) (Caret = ^Q)</summary>
|
||||
public const char DC1 = (char)0x11;
|
||||
/// <summary>Device Control Two (Caret = ^R)</summary>
|
||||
public const char DC2 = (char)0x12;
|
||||
/// <summary>Device Control Three (XOFF) (Caret = ^S)</summary>
|
||||
public const char DC3 = (char)0x13;
|
||||
/// <summary>Device Control Four (Caret = ^T)</summary>
|
||||
public const char DC4 = (char)0x14;
|
||||
/// <summary>Negative Acknowledge (Caret = ^U)</summary>
|
||||
public const char NAK = (char)0x15;
|
||||
/// <summary>Synchronous Idle (Caret = ^V)</summary>
|
||||
public const char SYN = (char)0x16;
|
||||
/// <summary>End of Transmission Block (Caret = ^W)</summary>
|
||||
public const char ETB = (char)0x17;
|
||||
/// <summary>Cancel (Caret = ^X)</summary>
|
||||
public const char CAN = (char)0x18;
|
||||
/// <summary>End of Medium (Caret = ^Y)</summary>
|
||||
public const char EM = (char)0x19;
|
||||
/// <summary>Substitute (Caret = ^Z)</summary>
|
||||
public const char SUB = (char)0x1a;
|
||||
/// <summary>Escape (Caret = ^[, C = \e)</summary>
|
||||
public const char ESC = (char)0x1b;
|
||||
/// <summary>File Separator (Caret = ^\)</summary>
|
||||
public const char FS = (char)0x1c;
|
||||
/// <summary>Group Separator (Caret = ^])</summary>
|
||||
public const char GS = (char)0x1d;
|
||||
/// <summary>Record Separator (Caret = ^^)</summary>
|
||||
public const char RS = (char)0x1e;
|
||||
/// <summary>Unit Separator (Caret = ^_)</summary>
|
||||
public const char US = (char)0x1f;
|
||||
/// <summary>Space</summary>
|
||||
public const char SP = (char)0x20;
|
||||
/// <summary>Delete (Caret = ^?)</summary>
|
||||
public const char DEL = (char)0x7f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Ansi
|
||||
{
|
||||
public struct CharAttributes : IEquatable<CharAttributes>
|
||||
{
|
||||
public int Flags { get; set; }
|
||||
public int BackgroundColor { get; set; }
|
||||
public int ForegroundColor { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public bool Equals(CharAttributes other)
|
||||
{
|
||||
return base.Equals(other);
|
||||
}
|
||||
|
||||
public static bool operator ==(CharAttributes a, CharAttributes b)
|
||||
{
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(CharAttributes a, CharAttributes b)
|
||||
{
|
||||
return !a.Equals(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Ansi
|
||||
{
|
||||
internal struct TerminalCode
|
||||
{
|
||||
public TerminalCodeType Type { get; }
|
||||
public int Line { get; }
|
||||
public int Column { get; }
|
||||
public string Text { get; }
|
||||
public CharAttributes CharAttributes { get; }
|
||||
|
||||
public TerminalCode(TerminalCodeType type) : this()
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public TerminalCode(TerminalCodeType type, string text) : this(type)
|
||||
{
|
||||
Text = text;
|
||||
}
|
||||
|
||||
public TerminalCode(TerminalCodeType type, int line, int column) : this(type)
|
||||
{
|
||||
Line = line;
|
||||
Column = column;
|
||||
}
|
||||
|
||||
public TerminalCode(TerminalCodeType type, CharAttributes charAttributes) : this(type)
|
||||
{
|
||||
CharAttributes = charAttributes;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Type == TerminalCodeType.Text)
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Type.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Ansi
|
||||
{
|
||||
public enum TerminalCodeType
|
||||
{
|
||||
Text,
|
||||
Bell,
|
||||
LineFeed,
|
||||
CarriageReturn,
|
||||
Backspace,
|
||||
Tab,
|
||||
ShiftOut,
|
||||
ShiftIn,
|
||||
SaveCursor,
|
||||
Reset,
|
||||
SetTitle,
|
||||
RestoreCursor,
|
||||
TabSet,
|
||||
|
||||
CursorPosition,
|
||||
CursorCharAbsolute,
|
||||
CursorUp,
|
||||
CursorDown,
|
||||
CursorForward,
|
||||
CursorBackward,
|
||||
RestoreCursorPosition,
|
||||
EraseInDisplay,
|
||||
EraseInLine,
|
||||
SetGraphicsMode,
|
||||
SetMode,
|
||||
CharAttributes,
|
||||
ResetMode,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
public enum SelectionMode
|
||||
{
|
||||
Stream,
|
||||
Block
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
internal static class SpecialColorIds
|
||||
{
|
||||
public const int Cursor = -1;
|
||||
public const int Selection = -2;
|
||||
public const int Historic = -3;
|
||||
public const int Futuristic = -4;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
using FluentTerminal.App.Views.NativeFrontend.Ansi;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
internal class TerminalBuffer
|
||||
{
|
||||
private const int MaxHistorySize = 1024;
|
||||
|
||||
private TerminalBufferChar[] _buffer;
|
||||
private TerminalSize _size;
|
||||
private readonly List<TerminalBufferLine> _history = new List<TerminalBufferLine>();
|
||||
|
||||
public bool ShowCursor { get; set; }
|
||||
public int CursorX { get; set; }
|
||||
public int CursorY { get; set; }
|
||||
public CharAttributes CurrentCharAttributes { get; set; }
|
||||
public TerminalSelection Selection { get; set; }
|
||||
|
||||
public int WindowTop { get; set; }
|
||||
public int WindowBottom
|
||||
{
|
||||
get => WindowTop + _size.Rows - 1;
|
||||
set
|
||||
{
|
||||
WindowTop = value - _size.Rows + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalBuffer(TerminalSize size)
|
||||
{
|
||||
Initialise(size);
|
||||
}
|
||||
|
||||
private void Initialise(TerminalSize size)
|
||||
{
|
||||
_buffer = new TerminalBufferChar[size.Rows * size.Columns];
|
||||
_size = size;
|
||||
CursorX = 0;
|
||||
CursorY = 0;
|
||||
Clear();
|
||||
}
|
||||
|
||||
public TerminalSize Size
|
||||
{
|
||||
get => _size;
|
||||
set
|
||||
{
|
||||
if (value != _size)
|
||||
{
|
||||
var srcSize = _size;
|
||||
var dstSize = value;
|
||||
|
||||
var srcBuffer = _buffer;
|
||||
var dstBuffer = new TerminalBufferChar[dstSize.Rows * dstSize.Columns];
|
||||
|
||||
int srcLeft = 0;
|
||||
int srcRight = Math.Min(srcSize.Columns, dstSize.Columns) - 1;
|
||||
int srcTop = Math.Max(0, CursorY - dstSize.Rows + 1);
|
||||
int srcBottom = srcTop + Math.Min(srcSize.Rows, dstSize.Rows) - 1;
|
||||
int dstLeft = 0;
|
||||
int dstTop = 0;
|
||||
|
||||
CopyBufferToBuffer(srcBuffer, srcSize, srcLeft, srcTop, srcRight, srcBottom,
|
||||
dstBuffer, dstSize, dstLeft, dstTop);
|
||||
|
||||
_buffer = dstBuffer;
|
||||
_size = dstSize;
|
||||
|
||||
CursorY = Math.Min(CursorY, _size.Rows - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearBlock(0, 0, _size.Columns - 1, _size.Rows - 1);
|
||||
}
|
||||
|
||||
public void ClearBlock(int left, int top, int right, int bottom)
|
||||
{
|
||||
left = Math.Max(0, left);
|
||||
top = Math.Max(0, top);
|
||||
right = Math.Min(right, _size.Columns - 1);
|
||||
bottom = Math.Min(bottom, _size.Rows - 1);
|
||||
|
||||
for (int y = top; y <= bottom; y++)
|
||||
{
|
||||
for (int x = left; x <= right; x++)
|
||||
{
|
||||
int index = GetBufferIndex(x, y);
|
||||
_buffer[index] = new TerminalBufferChar(' ', default(CharAttributes));
|
||||
}
|
||||
}
|
||||
ScrollToCursor();
|
||||
}
|
||||
|
||||
public void Type(char c)
|
||||
{
|
||||
if (IsInBuffer(CursorX, CursorY))
|
||||
{
|
||||
int index = GetBufferIndex(CursorX, CursorY);
|
||||
_buffer[index] = new TerminalBufferChar(c, CurrentCharAttributes);
|
||||
CursorX++;
|
||||
}
|
||||
ScrollToCursor();
|
||||
}
|
||||
|
||||
internal void Type(string text)
|
||||
{
|
||||
foreach (char c in text)
|
||||
{
|
||||
if (IsInBuffer(CursorX, CursorY))
|
||||
{
|
||||
int index = GetBufferIndex(CursorX, CursorY);
|
||||
_buffer[index] = new TerminalBufferChar(c, CurrentCharAttributes);
|
||||
CursorX++;
|
||||
}
|
||||
}
|
||||
ScrollToCursor();
|
||||
}
|
||||
|
||||
public void ShiftUp()
|
||||
{
|
||||
var topLine = GetBufferLine(0);
|
||||
AddHistory(topLine);
|
||||
|
||||
for (int y = 0; y < _size.Rows - 1; y++)
|
||||
{
|
||||
Array.Copy(_buffer, (y + 1) * _size.Columns, _buffer, y * _size.Columns, _size.Columns);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetText(int y)
|
||||
{
|
||||
return GetText(0, y, _size.Columns);
|
||||
}
|
||||
|
||||
public string[] GetText(int left, int top, int right, int bottom)
|
||||
{
|
||||
int width = right - left + 1;
|
||||
int height = bottom - top + 1;
|
||||
string[] result = new string[height];
|
||||
for (int i = 0; i < height; i++)
|
||||
{
|
||||
result[i] = GetText(left, top + i, width);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetText(int x, int y, int length)
|
||||
{
|
||||
if (x + length > _size.Columns)
|
||||
{
|
||||
throw new ArgumentException("Range overflows line length.", nameof(length));
|
||||
}
|
||||
|
||||
(var buffer, int startIndex, int endIndex) = GetLineBufferRange(y);
|
||||
if (buffer == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
int maxLength = endIndex - startIndex;
|
||||
string line = GetTextAtIndex(buffer, startIndex + x, Math.Min(length, maxLength));
|
||||
return line;
|
||||
}
|
||||
|
||||
private static string GetTextAtIndex(IList<TerminalBufferChar> buffer, int index, int length)
|
||||
{
|
||||
var chars = new char[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
chars[i] = buffer[index + i].Char;
|
||||
}
|
||||
string line = new string(chars);
|
||||
return line;
|
||||
}
|
||||
|
||||
public string[] GetSelectionText()
|
||||
{
|
||||
var selection = Selection;
|
||||
if (selection.Mode == SelectionMode.Stream)
|
||||
{
|
||||
(var start, var end) = selection.GetMinMax();
|
||||
var result = new List<string>();
|
||||
for (int y = start.Row; y <= end.Row; y++)
|
||||
{
|
||||
int x0 = 0;
|
||||
int x1 = _size.Columns - 1;
|
||||
if (y == start.Row)
|
||||
{
|
||||
x0 = start.Column;
|
||||
}
|
||||
if (y == end.Row)
|
||||
{
|
||||
x1 = end.Column;
|
||||
}
|
||||
result.Add(GetText(x0, y, x1 - x0 + 1));
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
else if (selection.Mode == SelectionMode.Block)
|
||||
{
|
||||
int left = Math.Min(selection.Start.Column, selection.End.Column);
|
||||
int right = Math.Max(selection.Start.Column, selection.End.Column);
|
||||
int top = Math.Min(selection.Start.Row, selection.End.Row);
|
||||
int bottom = Math.Max(selection.Start.Row, selection.End.Row);
|
||||
return GetText(left, top, right, bottom);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unknown selection mode.");
|
||||
}
|
||||
}
|
||||
|
||||
public void CopySelection()
|
||||
{
|
||||
//string[] selectionText = GetSelectionText()
|
||||
// .Select(x => x.TrimEnd())
|
||||
// .ToArray();
|
||||
//string text = String.Join(Environment.NewLine, selectionText);
|
||||
//var dataObject = new DataObject();
|
||||
//dataObject.SetText(text);
|
||||
//dataObject.SetData("MSDEVColumnSelect", true);
|
||||
//Clipboard.SetDataObject(dataObject, copy: true);
|
||||
}
|
||||
|
||||
private TerminalBufferLine GetBufferLine(int y)
|
||||
{
|
||||
int startIndex = GetBufferIndex(0, y);
|
||||
var bufferLine = new TerminalBufferLine(_buffer, startIndex, _size.Columns);
|
||||
return bufferLine;
|
||||
}
|
||||
|
||||
public TerminalTagArray GetFormattedLine(int y)
|
||||
{
|
||||
var tags = ImmutableArray.CreateBuilder<TerminalTag>(initialCapacity: _size.Columns);
|
||||
|
||||
(var buffer, int startIndex, int endIndex) = GetLineBufferRange(y);
|
||||
if (buffer == null)
|
||||
{
|
||||
AddFillerTags(tags, 0, y, _size.Columns);
|
||||
return new TerminalTagArray(tags.ToImmutable());
|
||||
}
|
||||
|
||||
// Group sequentially by attribute
|
||||
int lineLength = endIndex - startIndex;
|
||||
var currentTagStartIndex = startIndex;
|
||||
var currentTagAttribute = GetAttributesAt(buffer[startIndex], 0, y);
|
||||
for (int i = startIndex + 1; i < endIndex; i++)
|
||||
{
|
||||
int x = i - startIndex;
|
||||
var c = buffer[i];
|
||||
var attr = GetAttributesAt(c, x, y);
|
||||
if (!CanContinueTag(currentTagAttribute, attr, c.Char))
|
||||
{
|
||||
string tagText = GetTextAtIndex(buffer, currentTagStartIndex, i - currentTagStartIndex);
|
||||
tags.Add(new TerminalTag(tagText, currentTagAttribute));
|
||||
|
||||
currentTagStartIndex = i;
|
||||
currentTagAttribute = attr;
|
||||
}
|
||||
}
|
||||
|
||||
// Last tag
|
||||
{
|
||||
string tagText = GetTextAtIndex(buffer, currentTagStartIndex, endIndex - currentTagStartIndex);
|
||||
tags.Add(new TerminalTag(tagText, currentTagAttribute));
|
||||
}
|
||||
|
||||
// Filler tags
|
||||
if (lineLength < _size.Columns)
|
||||
{
|
||||
AddFillerTags(tags, lineLength, y, _size.Columns - lineLength);
|
||||
}
|
||||
|
||||
return new TerminalTagArray(tags.ToImmutable());
|
||||
}
|
||||
|
||||
private void AddFillerTags(ImmutableArray<TerminalTag>.Builder builder, int x, int y, int length)
|
||||
{
|
||||
int tagLength = 0;
|
||||
var lastAttr = GetAttributesAt(default(TerminalBufferChar), x, y);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
tagLength++;
|
||||
var attr = GetAttributesAt(default(TerminalBufferChar), x + i, y);
|
||||
if (attr != lastAttr)
|
||||
{
|
||||
string text = new string(' ', tagLength);
|
||||
builder.Add(new TerminalTag(text, lastAttr));
|
||||
lastAttr = attr;
|
||||
tagLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Last tag
|
||||
{
|
||||
string text = new string(' ', tagLength);
|
||||
builder.Add(new TerminalTag(text, lastAttr));
|
||||
}
|
||||
}
|
||||
|
||||
private CharAttributes GetAttributesAt(TerminalBufferChar bufferChar, int x, int y)
|
||||
{
|
||||
CharAttributes attr = bufferChar.Attributes;
|
||||
if (ShowCursor && x == CursorX && y == CursorY)
|
||||
{
|
||||
attr.BackgroundColor = SpecialColorIds.Cursor;
|
||||
}
|
||||
else if (IsPointInSelection(x, y))
|
||||
{
|
||||
attr.BackgroundColor = SpecialColorIds.Selection;
|
||||
}
|
||||
else if (y < 0)
|
||||
{
|
||||
attr.BackgroundColor = SpecialColorIds.Historic;
|
||||
}
|
||||
else if (y >= _size.Rows)
|
||||
{
|
||||
attr.BackgroundColor = SpecialColorIds.Futuristic;
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
|
||||
private (IList<TerminalBufferChar> buffer, int startIndex, int endIndex) GetLineBufferRange(int y)
|
||||
{
|
||||
if (y < 0)
|
||||
{
|
||||
int historyIndex = _history.Count + y;
|
||||
if (historyIndex < 0)
|
||||
{
|
||||
return (null, 0, 0);
|
||||
}
|
||||
var historyLine = _history[historyIndex];
|
||||
var buffer = historyLine.Buffer;
|
||||
return (buffer, 0, Math.Min(buffer.Length, _size.Columns));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (y >= _size.Rows)
|
||||
{
|
||||
return (null, 0, 0);
|
||||
}
|
||||
int startIndex = GetBufferIndex(0, y);
|
||||
return (_buffer, startIndex, startIndex + _size.Columns);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointInSelection(int x, int y)
|
||||
{
|
||||
var selection = Selection;
|
||||
if (selection == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (selection.Mode == SelectionMode.Stream)
|
||||
{
|
||||
(var start, var end) = selection.GetMinMax();
|
||||
if (y == start.Row)
|
||||
{
|
||||
if (x < start.Column)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (y == end.Row)
|
||||
{
|
||||
if (x > end.Column)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (y < start.Row || y > end.Row)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (selection.Mode == SelectionMode.Block)
|
||||
{
|
||||
int left = Math.Min(selection.Start.Column, selection.End.Column);
|
||||
int right = Math.Max(selection.Start.Column, selection.End.Column);
|
||||
int top = Math.Min(selection.Start.Row, selection.End.Row);
|
||||
int bottom = Math.Max(selection.Start.Row, selection.End.Row);
|
||||
return (x >= left && x <= right &&
|
||||
y >= top && y <= bottom);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Unknown selection mode.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanContinueTag(CharAttributes previous, CharAttributes next, char nextC)
|
||||
{
|
||||
if (nextC == ' ')
|
||||
{
|
||||
return previous.BackgroundColor == next.BackgroundColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
return previous == next;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInBuffer(int x, int y)
|
||||
{
|
||||
return (x >= 0 && x < _size.Columns && y >= 0 && y < _size.Rows);
|
||||
}
|
||||
|
||||
private int GetBufferIndex(int x, int y)
|
||||
{
|
||||
return x + (y * _size.Columns);
|
||||
}
|
||||
|
||||
private TerminalPoint GetBufferPoint(int index)
|
||||
{
|
||||
return new TerminalPoint(index % _size.Columns, index % _size.Columns);
|
||||
}
|
||||
|
||||
private void AddHistory(TerminalBufferLine line)
|
||||
{
|
||||
if (_history.Count >= MaxHistorySize)
|
||||
{
|
||||
_history.RemoveAt(0);
|
||||
}
|
||||
_history.Add(line);
|
||||
}
|
||||
|
||||
public int CurrentHistoryLength => _history.Count;
|
||||
|
||||
public void Scroll(int scroll)
|
||||
{
|
||||
int top = WindowTop + scroll;
|
||||
top = Math.Max(top, -_history.Count);
|
||||
// top = Math.Min(top, _size.Rows - 1);
|
||||
top = Math.Min(top, CursorY);
|
||||
WindowTop = top;
|
||||
}
|
||||
|
||||
public void ScrollToCursor()
|
||||
{
|
||||
int windowTop = WindowTop;
|
||||
int windowBottom = WindowBottom;
|
||||
if (CursorY < windowTop)
|
||||
{
|
||||
WindowTop = CursorY;
|
||||
}
|
||||
else if (CursorY > windowBottom)
|
||||
{
|
||||
WindowBottom = CursorY;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyBufferToBuffer(TerminalBufferChar[] srcBuffer, TerminalSize srcSize, int srcLeft, int srcTop, int srcRight, int srcBottom,
|
||||
TerminalBufferChar[] dstBuffer, TerminalSize dstSize, int dstLeft, int dstTop)
|
||||
{
|
||||
int cols = srcRight - srcLeft + 1;
|
||||
int rows = srcBottom - srcTop + 1;
|
||||
for (int y = 0; y < rows; y++)
|
||||
{
|
||||
int srcY = srcTop + y;
|
||||
int dstY = dstTop + y;
|
||||
int srcIndex = srcLeft + (srcY * srcSize.Columns);
|
||||
int dstIndex = dstLeft + (dstY * dstSize.Columns);
|
||||
Array.Copy(srcBuffer, srcIndex, dstBuffer, dstIndex, cols);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FluentTerminal.App.Views.NativeFrontend.Ansi;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
public struct TerminalBufferChar
|
||||
{
|
||||
public char Char { get; }
|
||||
public CharAttributes Attributes { get; }
|
||||
|
||||
public TerminalBufferChar(char c, CharAttributes attr)
|
||||
{
|
||||
Char = c;
|
||||
Attributes = attr;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Char.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single immutable line from a terminal buffer.
|
||||
/// </summary>
|
||||
internal class TerminalBufferLine
|
||||
{
|
||||
public int Columns { get; }
|
||||
public ImmutableArray<TerminalBufferChar> Buffer { get; }
|
||||
|
||||
public TerminalBufferLine(TerminalBufferChar[] buffer, int index, int length)
|
||||
{
|
||||
Buffer = buffer.Skip(index).Take(length).ToImmutableArray();
|
||||
Columns = length;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return new string(Buffer.Select(x => x.Char).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
[DebuggerDisplay("{Column}, {Row}")]
|
||||
public struct TerminalPoint : IEquatable<TerminalPoint>, IComparable<TerminalPoint>
|
||||
{
|
||||
public int Column { get; set; }
|
||||
public int Row { get; set; }
|
||||
|
||||
public TerminalPoint(int col, int row)
|
||||
{
|
||||
Column = col;
|
||||
Row = row;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public bool Equals(TerminalPoint other)
|
||||
{
|
||||
return Column == other.Column &&
|
||||
Row == other.Row;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public int CompareTo(TerminalPoint other)
|
||||
{
|
||||
if (Row < other.Row) return -1;
|
||||
if (Row > other.Row) return 1;
|
||||
if (Column < other.Column) return -1;
|
||||
if (Column > other.Column) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static bool operator ==(TerminalPoint a, TerminalPoint b)
|
||||
{
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(TerminalPoint a, TerminalPoint b)
|
||||
{
|
||||
return !a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator >(TerminalPoint a, TerminalPoint b)
|
||||
{
|
||||
return a.CompareTo(b) == 1;
|
||||
}
|
||||
|
||||
public static bool operator <(TerminalPoint a, TerminalPoint b)
|
||||
{
|
||||
return a.CompareTo(b) == -1;
|
||||
}
|
||||
|
||||
public static bool operator >=(TerminalPoint a, TerminalPoint b)
|
||||
{
|
||||
return a.CompareTo(b) >= 0;
|
||||
}
|
||||
|
||||
public static bool operator <=(TerminalPoint a, TerminalPoint b)
|
||||
{
|
||||
return a.CompareTo(b) <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
internal class TerminalSelection
|
||||
{
|
||||
public SelectionMode Mode { get; }
|
||||
public TerminalPoint Start { get; }
|
||||
public TerminalPoint End { get; }
|
||||
public bool IsReversed { get; }
|
||||
|
||||
public TerminalSelection(SelectionMode mode, TerminalPoint start, TerminalPoint end)
|
||||
{
|
||||
Mode = mode;
|
||||
Start = start;
|
||||
End = end;
|
||||
IsReversed = start > end;
|
||||
}
|
||||
|
||||
public (TerminalPoint, TerminalPoint) GetMinMax()
|
||||
{
|
||||
return IsReversed ? (End, Start) : (Start, End);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
public struct TerminalSize : IEquatable<TerminalSize>
|
||||
{
|
||||
public int Columns { get; }
|
||||
public int Rows { get; }
|
||||
|
||||
public TerminalSize(int columns, int rows)
|
||||
{
|
||||
Columns = columns;
|
||||
Rows = rows;
|
||||
}
|
||||
|
||||
public bool Equals(TerminalSize other)
|
||||
{
|
||||
return base.Equals(other);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Columns} x {Rows}";
|
||||
}
|
||||
|
||||
public static bool operator ==(TerminalSize a, TerminalSize b)
|
||||
{
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(TerminalSize a, TerminalSize b)
|
||||
{
|
||||
return !a.Equals(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using FluentTerminal.App.Views.NativeFrontend.Ansi;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
public struct TerminalTag : IEquatable<TerminalTag>
|
||||
{
|
||||
public string Text { get; }
|
||||
public CharAttributes Attributes { get; }
|
||||
|
||||
public TerminalTag(string text, CharAttributes attributes)
|
||||
{
|
||||
Text = text;
|
||||
Attributes = attributes;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return Equals((TerminalTag)obj);
|
||||
}
|
||||
|
||||
public bool Equals(TerminalTag other)
|
||||
{
|
||||
return Text == other.Text &&
|
||||
Attributes == other.Attributes;
|
||||
}
|
||||
|
||||
public TerminalTag Substring(int index)
|
||||
{
|
||||
return new TerminalTag(Text.Substring(index), Attributes);
|
||||
}
|
||||
|
||||
public TerminalTag Substring(int index, int length)
|
||||
{
|
||||
return new TerminalTag(Text.Substring(index, length), Attributes);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Text;
|
||||
}
|
||||
|
||||
public static bool operator ==(TerminalTag a, TerminalTag b)
|
||||
{
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(TerminalTag a, TerminalTag b)
|
||||
{
|
||||
return !a.Equals(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Terminal
|
||||
{
|
||||
public struct TerminalTagArray : IEquatable<TerminalTagArray>, IEnumerable<TerminalTag>
|
||||
{
|
||||
private readonly ImmutableArray<TerminalTag> _tags;
|
||||
private readonly int _hash;
|
||||
|
||||
public int Length => _tags.IsDefaultOrEmpty ? 0 : _tags.Length;
|
||||
|
||||
public TerminalTagArray(ImmutableArray<TerminalTag> tags)
|
||||
{
|
||||
_tags = tags;
|
||||
_hash = GetHashCode(tags);
|
||||
}
|
||||
|
||||
private static int GetHashCode(ImmutableArray<TerminalTag> tags)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
for (int i = 0; i < tags.Length; i++)
|
||||
{
|
||||
hash = hash * 23 + tags[i].GetHashCode();
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _hash;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return Equals((ImmutableArray<TerminalTag>)obj);
|
||||
}
|
||||
|
||||
public bool Equals(TerminalTagArray other)
|
||||
{
|
||||
if (_hash != other._hash)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var tagsA = _tags;
|
||||
var tagsB = other._tags;
|
||||
if (tagsA != tagsB)
|
||||
{
|
||||
if (tagsA.IsDefaultOrEmpty || tagsB.IsDefaultOrEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (tagsA.Length != tagsB.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < tagsA.Length; i++)
|
||||
{
|
||||
if (tagsA[i] != tagsB[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString();
|
||||
}
|
||||
|
||||
public IEnumerator<TerminalTag> GetEnumerator()
|
||||
{
|
||||
if (_tags.IsDefaultOrEmpty)
|
||||
{
|
||||
return Enumerable.Empty<TerminalTag>().GetEnumerator();
|
||||
}
|
||||
return ((IEnumerable<TerminalTag>)_tags).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public static bool operator ==(TerminalTagArray a, TerminalTagArray b)
|
||||
{
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(TerminalTagArray a, TerminalTagArray b)
|
||||
{
|
||||
return !a.Equals(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using FluentTerminal.App.Utilities;
|
||||
using FluentTerminal.App.Views.NativeFrontend.Terminal;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI;
|
||||
using Windows.UI.Xaml.Media;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend
|
||||
{
|
||||
internal class TerminalColorHelper
|
||||
{
|
||||
private readonly Dictionary<int, Brush> _brushDictionary = new Dictionary<int, Brush>();
|
||||
private readonly Brush _defaultForegroundBrush = new SolidColorBrush(Color.FromArgb(255, 204, 204, 204));
|
||||
|
||||
public Brush GetForegroundBrush(int id)
|
||||
{
|
||||
Brush brush = GetBrush(id);
|
||||
if (brush == null)
|
||||
{
|
||||
brush = _defaultForegroundBrush;
|
||||
}
|
||||
return brush;
|
||||
}
|
||||
|
||||
public Brush GetBackgroundBrush(int id)
|
||||
{
|
||||
return GetBrush(id);
|
||||
}
|
||||
|
||||
public Brush GetBrush(int id)
|
||||
{
|
||||
Brush result = null;
|
||||
if (id != 0)
|
||||
{
|
||||
if (!_brushDictionary.TryGetValue(id, out result))
|
||||
{
|
||||
result = new SolidColorBrush(GetColour(id));
|
||||
_brushDictionary.Add(id, result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Color GetColour(int id)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case SpecialColorIds.Cursor:
|
||||
return Color.FromArgb(255, 204, 204, 204);
|
||||
case SpecialColorIds.Selection:
|
||||
return Color.FromArgb(255, 203, 203, 203);
|
||||
case SpecialColorIds.Historic:
|
||||
case SpecialColorIds.Futuristic:
|
||||
return Color.FromArgb(255, 24, 24, 24);
|
||||
default:
|
||||
return TangoColours[id % 16].FromString();
|
||||
}
|
||||
}
|
||||
|
||||
// Colors 0-15
|
||||
private readonly static string[] TangoColours =
|
||||
{
|
||||
// dark:
|
||||
"#2e3436",
|
||||
"#cc0000",
|
||||
"#0dbc79",
|
||||
"#c4a000",
|
||||
"#3465a4",
|
||||
"#75507b",
|
||||
"#06989a",
|
||||
"#d3d7cf",
|
||||
|
||||
// bright:
|
||||
"#555753",
|
||||
"#ef2929",
|
||||
"#23D18B",
|
||||
"#fce94f",
|
||||
"#729fcf",
|
||||
"#ad7fa8",
|
||||
"#34e2e2",
|
||||
"#eeeeec"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<UserControl
|
||||
x:Class="FluentTerminal.App.Views.NativeFrontend.TerminalControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:FluentTerminal.App.Views.NativeFrontend"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:xaml="using:Microsoft.Graphics.Canvas.UI.Xaml"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="400"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<ScrollViewer x:Name="scroll" VerticalScrollBarVisibility="Auto">
|
||||
<xaml:CanvasVirtualControl x:Name="canvas" IsTabStop="True" RegionsInvalidated="OnRegionsInvalidated"/>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,476 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.App.Views.NativeFrontend.Ansi;
|
||||
using FluentTerminal.App.Views.NativeFrontend.Terminal;
|
||||
using FluentTerminal.App.Views.NativeFrontend.Utility;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Input;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend
|
||||
{
|
||||
public sealed partial class TerminalControl : UserControl, ITerminalView
|
||||
{
|
||||
private TerminalBuffer _buffer;
|
||||
private double _characterHeight;
|
||||
private double _characterWidth;
|
||||
private TerminalColorHelper _colorHelper = new TerminalColorHelper();
|
||||
private readonly TerminalOptions _terminalOptions;
|
||||
private int _cols;
|
||||
private CanvasTextFormat _format;
|
||||
private Ansi.AnsiParser _parser;
|
||||
private int _rows;
|
||||
private int _gridSize = 150;
|
||||
private CoreDispatcher _dispatcher;
|
||||
|
||||
public TerminalControl(ISettingsService settingsService)
|
||||
{
|
||||
InitializeComponent();
|
||||
canvas.KeyUp += _canvas_KeyUp;
|
||||
canvas.GotFocus += _canvas_GotFocus;
|
||||
canvas.CharacterReceived += Canvas_CharacterReceived;
|
||||
|
||||
_terminalOptions = settingsService.GetTerminalOptions();
|
||||
_dispatcher = Window.Current.CoreWindow.Dispatcher;
|
||||
}
|
||||
|
||||
int regionsInvalidatedEventCount = 0;
|
||||
int regionsInvalidatedCount = 0;
|
||||
|
||||
private void OnRegionsInvalidated(CanvasVirtualControl sender, CanvasRegionsInvalidatedEventArgs args)
|
||||
{
|
||||
++regionsInvalidatedEventCount;
|
||||
|
||||
var invalidatedRegions = args.InvalidatedRegions;
|
||||
regionsInvalidatedCount += invalidatedRegions.Count();
|
||||
|
||||
foreach (var region in invalidatedRegions)
|
||||
{
|
||||
DrawRegion(sender, region);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRegion(CanvasVirtualControl sender, Rect region)
|
||||
{
|
||||
var top = (int)(region.Top / _characterHeight);
|
||||
var bottom = top + (int)(region.Height / _characterHeight);
|
||||
|
||||
using (var ds = sender.CreateDrawingSession(region))
|
||||
{
|
||||
var y = 0.0;
|
||||
for (int i = top; i < bottom; i++)
|
||||
{
|
||||
var x = 0.0;
|
||||
var line = _buffer.GetFormattedLine(i);
|
||||
foreach (var item in line)
|
||||
{
|
||||
if (item.Attributes.BackgroundColor != 0)
|
||||
{
|
||||
ds.FillRectangle(new Windows.Foundation.Rect { Height = _characterHeight, Width = _characterWidth * item.Text.Length, X = x, Y = y }, _colorHelper.GetColour(item.Attributes.BackgroundColor));
|
||||
}
|
||||
|
||||
var foreground = Colors.White;
|
||||
if (item.Attributes.ForegroundColor != 0)
|
||||
{
|
||||
foreground = _colorHelper.GetColour(item.Attributes.ForegroundColor);
|
||||
}
|
||||
var pos = new System.Numerics.Vector2 { X = (float)x, Y = (float)y };
|
||||
|
||||
ds.DrawText(item.Text, pos, foreground, _format);
|
||||
x += item.Text.Length * _characterWidth;
|
||||
}
|
||||
y += _characterHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Measure()
|
||||
{
|
||||
var device = CanvasDevice.GetSharedDevice();
|
||||
using (CanvasRenderTarget offscreen = new CanvasRenderTarget(device, (float)256, (float)256, canvas.Dpi))
|
||||
{
|
||||
using (CanvasDrawingSession ds = offscreen.CreateDrawingSession())
|
||||
{
|
||||
_format = new CanvasTextFormat
|
||||
{
|
||||
FontSize = _terminalOptions.FontSize,
|
||||
FontFamily = _terminalOptions.FontFamily,
|
||||
FontWeight = _terminalOptions.FontWeight
|
||||
};
|
||||
|
||||
var textLayout = new CanvasTextLayout(ds, "\u2560", _format, 0.0f, 0.0f);
|
||||
_characterWidth = textLayout.DrawBounds.Right;
|
||||
_characterHeight = textLayout.DrawBounds.Bottom;
|
||||
|
||||
_cols = (int)Math.Floor(Window.Current.CoreWindow.Bounds.Width / _characterWidth);
|
||||
_rows = (int)Math.Floor((Window.Current.CoreWindow.Bounds.Height - 32) / _characterHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void UserControl_Unloaded(object sender, RoutedEventArgs args)
|
||||
{
|
||||
// Explicitly remove references to allow the Win2D controls to get garbage collected
|
||||
canvas.RemoveFromVisualTree();
|
||||
canvas = null;
|
||||
}
|
||||
|
||||
public TerminalViewModel ViewModel { get; private set; }
|
||||
|
||||
public Task ChangeKeyBindings()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeOptions(TerminalOptions options)
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeTheme(TerminalTheme theme)
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FindNext(string searchText)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task FindPrevious(string searchText)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task FocusTerminal()
|
||||
{
|
||||
canvas.Focus(FocusState.Programmatic);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Initialize(TerminalViewModel viewModel)
|
||||
{
|
||||
Measure();
|
||||
ViewModel = viewModel;
|
||||
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
//ViewModel.Terminal.RegisterSelectedTextCallback(() => ExecuteScriptAsync("term.getSelection()"));
|
||||
//ViewModel.Terminal.Closed += Terminal_Closed;
|
||||
|
||||
var options = ViewModel.SettingsService.GetTerminalOptions();
|
||||
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
|
||||
var profiles = ViewModel.SettingsService.GetShellProfiles();
|
||||
var settings = ViewModel.SettingsService.GetApplicationSettings();
|
||||
var theme = ViewModel.TerminalTheme;
|
||||
var sessionType = SessionType.Unknown;
|
||||
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
|
||||
{
|
||||
sessionType = SessionType.WinPty;
|
||||
}
|
||||
else
|
||||
{
|
||||
sessionType = SessionType.ConPty;
|
||||
}
|
||||
var size = new Models.TerminalSize { Rows = _rows, Columns = _cols };
|
||||
_buffer = new TerminalBuffer(new Terminal.TerminalSize(size.Columns, size.Rows));
|
||||
_parser = new Ansi.AnsiParser();
|
||||
var response = await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType).ConfigureAwait(true);
|
||||
if (!response.Success)
|
||||
{
|
||||
await ViewModel.DialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
|
||||
ViewModel.Terminal.ReportLauchFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.Focus(FocusState.Keyboard);
|
||||
}
|
||||
|
||||
private void _canvas_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Debug.WriteLine("focus");
|
||||
}
|
||||
|
||||
private void _canvas_KeyUp(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var controlPressed = (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down));
|
||||
var shiftPressed = (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down));
|
||||
var windowsPressed = (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down));
|
||||
var altPressed = (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down));
|
||||
|
||||
int modCode = 0;
|
||||
if (shiftPressed) modCode |= 1;
|
||||
if (altPressed) modCode |= 2;
|
||||
if (controlPressed) modCode |= 4;
|
||||
if (windowsPressed) modCode |= 8;
|
||||
|
||||
//if (IsSelecting && !modifiers.HasFlag(ModifierKeys.Alt))
|
||||
//{
|
||||
// ClearSelection();
|
||||
// if (e.Key == Key.Escape)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
//}
|
||||
|
||||
string text = string.Empty;
|
||||
switch (e.Key)
|
||||
{
|
||||
case VirtualKey.Escape:
|
||||
text = $"{C0.ESC}{C0.ESC}{C0.ESC}";
|
||||
break;
|
||||
|
||||
case VirtualKey.Back:
|
||||
text = shiftPressed ?
|
||||
C0.BS.ToString() :
|
||||
C0.DEL.ToString();
|
||||
break;
|
||||
|
||||
case VirtualKey.Delete:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[3~" :
|
||||
$"{C0.ESC}[3;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.Tab:
|
||||
text = shiftPressed ?
|
||||
$"{C0.ESC}[Z" :
|
||||
C0.HT.ToString();
|
||||
break;
|
||||
|
||||
case VirtualKey.Up:
|
||||
text = Construct(1, 'A');
|
||||
break;
|
||||
|
||||
case VirtualKey.Down:
|
||||
text = Construct(1, 'B');
|
||||
break;
|
||||
|
||||
case VirtualKey.Right:
|
||||
text = Construct(1, 'C');
|
||||
break;
|
||||
|
||||
case VirtualKey.Left:
|
||||
text = Construct(1, 'D');
|
||||
break;
|
||||
|
||||
case VirtualKey.Home:
|
||||
text = Construct(1, 'H');
|
||||
break;
|
||||
|
||||
case VirtualKey.End:
|
||||
text = Construct(1, 'F');
|
||||
break;
|
||||
|
||||
case VirtualKey.PageUp:
|
||||
text = $"{C0.ESC}[5~";
|
||||
break;
|
||||
|
||||
case VirtualKey.PageDown:
|
||||
text = $"{C0.ESC}[6~";
|
||||
break;
|
||||
|
||||
case VirtualKey.Enter:
|
||||
text = C0.CR.ToString();
|
||||
break;
|
||||
|
||||
case VirtualKey.Space:
|
||||
text = " ";
|
||||
break;
|
||||
|
||||
case VirtualKey.F1:
|
||||
text = Construct(1, 'P');
|
||||
break;
|
||||
|
||||
case VirtualKey.F2:
|
||||
text = Construct(1, 'Q');
|
||||
break;
|
||||
|
||||
case VirtualKey.F3:
|
||||
text = Construct(1, 'R');
|
||||
break;
|
||||
|
||||
case VirtualKey.F4:
|
||||
text = Construct(1, 'S');
|
||||
break;
|
||||
|
||||
case VirtualKey.F5:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[15~" :
|
||||
$"{C0.ESC}[15;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.F6:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[17~" :
|
||||
$"{C0.ESC}[17;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.F7:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[18~" :
|
||||
$"{C0.ESC}[18;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.F8:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[19~" :
|
||||
$"{C0.ESC}[19;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.F9:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[20~" :
|
||||
$"{C0.ESC}[20;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.F10:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[21~" :
|
||||
$"{C0.ESC}[21;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.F11:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[23~" :
|
||||
$"{C0.ESC}[23;{modCode + 1}~";
|
||||
break;
|
||||
|
||||
case VirtualKey.F12:
|
||||
text = (modCode == 0) ?
|
||||
$"{C0.ESC}[24~" :
|
||||
$"{C0.ESC}[24;{modCode + 1}~";
|
||||
break;
|
||||
}
|
||||
if (text != string.Empty)
|
||||
{
|
||||
ViewModel.Terminal.Write(Encoding.UTF8.GetBytes(text));
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
string Construct(int a, char c)
|
||||
{
|
||||
return (modCode == 0) ?
|
||||
$"{C0.ESC}O{c}" :
|
||||
$"{C0.ESC}[{a};{modCode + 1}{c}";
|
||||
}
|
||||
}
|
||||
|
||||
private void Canvas_CharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs args)
|
||||
{
|
||||
var text = Encoding.UTF8.GetBytes(new[] { args.Character });
|
||||
ViewModel.Terminal.Write(text);
|
||||
}
|
||||
|
||||
private void ProcessTerminalCode(TerminalCode code)
|
||||
{
|
||||
switch (code.Type)
|
||||
{
|
||||
case TerminalCodeType.ResetMode:
|
||||
_buffer.ShowCursor = false;
|
||||
break;
|
||||
|
||||
case TerminalCodeType.SetMode:
|
||||
_buffer.ShowCursor = true;
|
||||
|
||||
// HACK We want clear to reset the window position but not general typing.
|
||||
// We therefore reset the window only if the cursor is moved to the top.
|
||||
if (_buffer.CursorY == 0)
|
||||
{
|
||||
_buffer.WindowTop = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case TerminalCodeType.Text:
|
||||
_buffer.Type(code.Text);
|
||||
break;
|
||||
|
||||
case TerminalCodeType.LineFeed:
|
||||
if (_buffer.CursorY == _buffer.Size.Rows - 1)
|
||||
{
|
||||
_buffer.ShiftUp();
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer.CursorY++;
|
||||
}
|
||||
break;
|
||||
|
||||
case TerminalCodeType.CarriageReturn:
|
||||
_buffer.CursorX = 0;
|
||||
break;
|
||||
|
||||
case TerminalCodeType.CharAttributes:
|
||||
_buffer.CurrentCharAttributes = code.CharAttributes;
|
||||
break;
|
||||
|
||||
case TerminalCodeType.CursorPosition:
|
||||
_buffer.CursorX = code.Column;
|
||||
_buffer.CursorY = code.Line;
|
||||
break;
|
||||
|
||||
case TerminalCodeType.CursorUp:
|
||||
_buffer.CursorY -= code.Line;
|
||||
break;
|
||||
|
||||
case TerminalCodeType.CursorCharAbsolute:
|
||||
_buffer.CursorX = code.Column;
|
||||
break;
|
||||
|
||||
case TerminalCodeType.EraseInLine:
|
||||
if (code.Line == 0)
|
||||
{
|
||||
_buffer.ClearBlock(_buffer.CursorX, _buffer.CursorY, _buffer.Size.Columns - 1, _buffer.CursorY);
|
||||
}
|
||||
break;
|
||||
|
||||
case TerminalCodeType.EraseInDisplay:
|
||||
_buffer.Clear();
|
||||
_buffer.CursorX = 0;
|
||||
_buffer.CursorY = 0;
|
||||
break;
|
||||
|
||||
case TerminalCodeType.SetTitle:
|
||||
//ViewModel.Terminal.SetTitle(code.Text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
|
||||
var text = Encoding.UTF8.GetString(e, 0, e.Length);
|
||||
var reader = new ArrayReader<char>(text.ToCharArray());
|
||||
var codes = _parser.Parse(reader);
|
||||
foreach (var code in codes)
|
||||
{
|
||||
ProcessTerminalCode(code);
|
||||
}
|
||||
|
||||
_dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
|
||||
{
|
||||
canvas.Height = Math.Max(_buffer.CurrentHistoryLength * _characterHeight, _buffer.Size.Rows * _characterHeight);
|
||||
canvas.Invalidate();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Views.NativeFrontend.Utility
|
||||
{
|
||||
internal class ArrayReader<T>
|
||||
{
|
||||
private readonly T[] _array;
|
||||
|
||||
public int Offset { get; private set; }
|
||||
public int RemainingLength { get; private set; }
|
||||
|
||||
public ArrayReader(T[] array)
|
||||
{
|
||||
_array = array;
|
||||
Offset = 0;
|
||||
RemainingLength = array.Length;
|
||||
}
|
||||
|
||||
public ArrayReader(T[] array, int offset, int count)
|
||||
{
|
||||
_array = array;
|
||||
Offset = offset;
|
||||
RemainingLength = count;
|
||||
}
|
||||
|
||||
public ArrayReader(ArraySegment<T> segment)
|
||||
{
|
||||
_array = segment.Array;
|
||||
Offset = segment.Offset;
|
||||
RemainingLength = segment.Count;
|
||||
}
|
||||
|
||||
public bool TryRead(out T element)
|
||||
{
|
||||
if (RemainingLength > 0)
|
||||
{
|
||||
element = _array[Offset];
|
||||
Offset++;
|
||||
RemainingLength--;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
element = default(T);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryPeek(out T element)
|
||||
{
|
||||
if (RemainingLength > 0)
|
||||
{
|
||||
element = _array[Offset];
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
element = default(T);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public T Read()
|
||||
{
|
||||
T element;
|
||||
if (!TryRead(out element))
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
public T Peek()
|
||||
{
|
||||
T element;
|
||||
if (!TryPeek(out element))
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
|
||||
namespace FluentTerminal.App.Views
|
||||
{
|
||||
public class NoValueTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate DefaultTemplate { get; set; }
|
||||
public DataTemplate CustomTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
return SelectTemplateCore(item);
|
||||
}
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
if (item is String arguments)
|
||||
{
|
||||
return String.Empty.Equals(arguments) ? DefaultTemplate : CustomTemplate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,13 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1" Margin="24,0,0,24">
|
||||
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
@@ -36,52 +35,56 @@
|
||||
Label="Check for updates" />
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
|
||||
<Grid>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<Grid Margin="{StaticResource ContainerMargin}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0">
|
||||
<!-- App Logo -->
|
||||
<Image
|
||||
MaxWidth="106"
|
||||
MaxHeight="106"
|
||||
Margin="24,0,24,0"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Source="/Assets/AppIcons/Square106x106Logo.black.png"
|
||||
Stretch="None" />
|
||||
|
||||
<!-- Current version -->
|
||||
<TextBlock Margin="0,24,0,8" Text="Current version" />
|
||||
<TextBlock><Run Text="{x:Bind ViewModel.CurrentVersion}" />
|
||||
- <Hyperlink NavigateUri="{x:Bind ViewModel.CurrentVersionReleaseNotesURL}">Release notes</Hyperlink></TextBlock>
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Current version" />
|
||||
<TextBlock>
|
||||
<Run Text="{x:Bind ViewModel.CurrentVersion}" />
|
||||
-<Hyperlink NavigateUri="{x:Bind ViewModel.CurrentVersionReleaseNotesURL}">Release notes</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Latest version -->
|
||||
<TextBlock Margin="0,24,0,8" Text="Latest version" />
|
||||
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionLoading, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}"><Italic>loading...</Italic></TextBlock>
|
||||
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionNotFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}"><Italic>Not found</Italic></TextBlock>
|
||||
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<Run Text="{x:Bind ViewModel.LatestVersion, Mode=OneWay}" />
|
||||
- <Hyperlink NavigateUri="{x:Bind ViewModel.LatestVersionReleaseNotesURL, Mode=OneWay}">
|
||||
Release notes
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Latest version" />
|
||||
<StackPanel Orientation="Horizontal" Visibility="{x:Bind ViewModel.LatestVersionLoading, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<ProgressRing Margin="0,0,8,0" IsActive="True" />
|
||||
<TextBlock><Italic>Checking for updates...</Italic></TextBlock>
|
||||
</StackPanel>
|
||||
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionNotFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<Italic>Not found</Italic>
|
||||
</TextBlock>
|
||||
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<Run Text="{x:Bind ViewModel.LatestVersion, Mode=OneWay}" />
|
||||
-<Hyperlink NavigateUri="{x:Bind ViewModel.LatestVersionReleaseNotesURL, Mode=OneWay}">Release notes</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- License -->
|
||||
<TextBlock Margin="0,24,0,8" Text="License" />
|
||||
<TextBlock><Hyperlink NavigateUri="https://github.com/felixse/FluentTerminal/blob/master/LICENSE">GNU General Public License v3.0</Hyperlink></TextBlock>
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="License" />
|
||||
<TextBlock><Hyperlink NavigateUri="https://github.com/felixse/FluentTerminal/blob/master/LICENSE">GNU General Public License v3.0</Hyperlink></TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,72,0,24"
|
||||
Margin="0,72,0,0"
|
||||
VerticalAlignment="Bottom">
|
||||
<!-- Created by... -->
|
||||
<TextBlock>
|
||||
Created by <Italic>felixse</Italic>
|
||||
and <Hyperlink NavigateUri="https://github.com/felixse/FluentTerminal/graphs/contributors">contributors</Hyperlink></TextBlock>
|
||||
<TextBlock x:Name="CreatedBy" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
</Page>
|
||||
@@ -1,5 +1,7 @@
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
using System;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Documents;
|
||||
using Windows.UI.Xaml.Navigation;
|
||||
|
||||
namespace FluentTerminal.App.Views.SettingsPages
|
||||
@@ -11,6 +13,10 @@ namespace FluentTerminal.App.Views.SettingsPages
|
||||
public About()
|
||||
{
|
||||
InitializeComponent();
|
||||
CreatedBy.Inlines.Add(new Run { Text = "Created by " });
|
||||
CreatedBy.Inlines.Add(new Italic { Inlines = { new Run { Text = "felixse " } } });
|
||||
CreatedBy.Inlines.Add(new Run { Text = "and " });
|
||||
CreatedBy.Inlines.Add(new Hyperlink { Inlines = { new Run { Text = "contributors" } }, NavigateUri = new Uri("https://github.com/felixse/FluentTerminal/graphs/contributors") });
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1" Margin="24,00,0,24">
|
||||
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
@@ -34,75 +34,98 @@
|
||||
Label="Restore Defaults" />
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
|
||||
<StackPanel>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<StackPanel Margin="{StaticResource ContainerMargin}">
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<ToggleSwitch
|
||||
Header="Run on startup"
|
||||
IsEnabled="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay}"
|
||||
IsOn="{x:Bind ViewModel.StartupTaskEnabled, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Foreground="Red"
|
||||
Text="{x:Bind ViewModel.StartupTaskErrorMessage, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<ToggleSwitch Header="Show in system tray" IsOn="{x:Bind ViewModel.EnableTrayIcon, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Foreground="Red"
|
||||
Text="Relaunch required to apply setting"
|
||||
Visibility="{x:Bind ViewModel.ShouldRestartForTrayMessage, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Open new terminals as" />
|
||||
<RadioButton
|
||||
Content="Tabs"
|
||||
GroupName="NewTerminalLocation"
|
||||
IsChecked="{x:Bind ViewModel.TabIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Windows"
|
||||
GroupName="NewTerminalLocation"
|
||||
IsChecked="{x:Bind ViewModel.WindowIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Show tabs on" />
|
||||
<RadioButton
|
||||
Content="Top"
|
||||
GroupName="TabsPosition"
|
||||
IsChecked="{x:Bind ViewModel.TopIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Bottom"
|
||||
GroupName="TabsPosition"
|
||||
IsChecked="{x:Bind ViewModel.BottomIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<ToggleSwitch
|
||||
Header="Run on startup"
|
||||
IsEnabled="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay}"
|
||||
IsOn="{x:Bind ViewModel.StartupTaskEnabled, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Foreground="Red"
|
||||
Text="{x:Bind ViewModel.StartupTaskErrorMessage, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
|
||||
<ToggleSwitch
|
||||
Margin="0,24,0,0"
|
||||
Header="Show in system tray"
|
||||
IsOn="{x:Bind ViewModel.EnableTrayIcon, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Foreground="Red"
|
||||
Text="Relaunch required to apply setting"
|
||||
Visibility="{x:Bind ViewModel.ShouldRestartForTrayMessage, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
<TextBlock Margin="0,24,0,8" Text="Open new terminals as" />
|
||||
<RadioButton
|
||||
Content="Tabs"
|
||||
GroupName="NewTerminalLocation"
|
||||
IsChecked="{x:Bind ViewModel.TabIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Margin="0,0,0,24"
|
||||
Content="Windows"
|
||||
GroupName="NewTerminalLocation"
|
||||
IsChecked="{x:Bind ViewModel.WindowIsSelected, Mode=TwoWay}" />
|
||||
<TextBlock Margin="0,0,0,8" Text="Show tabs on" />
|
||||
<RadioButton
|
||||
Content="Top"
|
||||
GroupName="TabsPosition"
|
||||
IsChecked="{x:Bind ViewModel.TopIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Margin="0,0,0,24"
|
||||
Content="Bottom"
|
||||
GroupName="TabsPosition"
|
||||
IsChecked="{x:Bind ViewModel.BottomIsSelected, Mode=TwoWay}" />
|
||||
<ToggleSwitch
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Always show tabs"
|
||||
IsOn="{x:Bind ViewModel.AlwaysShowTabs, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Show custom tab title also in titlebar"
|
||||
IsOn="{x:Bind ViewModel.ShowCustomTitleInTitlebar, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Underline selected tab"
|
||||
IsOn="{x:Bind ViewModel.UnderlineSelectedTab, Mode=TwoWay}" />
|
||||
<TextBlock Margin="0,0,0,8" Text="Color on inactive tabs" />
|
||||
<RadioButton
|
||||
Content="Background"
|
||||
GroupName="InactiveTabColorMode"
|
||||
IsChecked="{x:Bind ViewModel.BackgroundIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Margin="0,0,0,24"
|
||||
Content="Underline"
|
||||
GroupName="InactiveTabColorMode"
|
||||
IsChecked="{x:Bind ViewModel.UnderlinedIsSelected, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Color on inactive tabs" />
|
||||
<RadioButton
|
||||
Content="Background"
|
||||
GroupName="InactiveTabColorMode"
|
||||
IsChecked="{x:Bind ViewModel.BackgroundIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Underline"
|
||||
GroupName="InactiveTabColorMode"
|
||||
IsChecked="{x:Bind ViewModel.UnderlinedIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Ask before closing windows"
|
||||
IsOn="{x:Bind ViewModel.ConfirmClosingWindows, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Ask before closing tabs"
|
||||
IsOn="{x:Bind ViewModel.ConfirmClosingTabs, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Show new output indicator on background tabs"
|
||||
IsOn="{x:Bind ViewModel.ShowNewOutputIndicator, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Windows 10 1809+: Always use WinPty"
|
||||
IsOn="{x:Bind ViewModel.AlwaysUseWinPty, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
</Page>
|
||||
@@ -13,7 +13,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1" Margin="24,0,0,0">
|
||||
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
@@ -42,11 +42,11 @@
|
||||
Label="Restore Defaults" />
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="2" Margin="24,24,0,0">
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.KeyBindings, Mode=OneWay}">
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<ItemsControl Margin="{StaticResource ContainerMargin}" ItemsSource="{x:Bind ViewModel.KeyBindings, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<views:KeyBindingsView Margin="0,0,0,24" ViewModel="{x:Bind}" />
|
||||
<views:KeyBindingsView Margin="{StaticResource ItemMargin}" ViewModel="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1" Margin="24,00,0,24">
|
||||
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
@@ -36,40 +35,44 @@
|
||||
Label="Restore Defaults" />
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
|
||||
<StackPanel>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<StackPanel Margin="{StaticResource ContainerMargin}">
|
||||
<ToggleSwitch
|
||||
Margin="0,0,0,24"
|
||||
Header="Copy on select"
|
||||
IsOn="{x:Bind ViewModel.CopyOnSelect, Mode=TwoWay}" />
|
||||
<TextBlock Margin="0,0,0,8" Text="Middle button action" />
|
||||
<RadioButton
|
||||
Content="Nothing"
|
||||
GroupName="MiddleButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseMiddleClickNoneIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Context menu"
|
||||
GroupName="MiddleButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseMiddleClickContextMenuIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Margin="0,0,0,24"
|
||||
Content="Paste"
|
||||
GroupName="MiddleButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseMiddleClickPasteIsSelected, Mode=TwoWay}" />
|
||||
<TextBlock Margin="0,0,0,8" Text="Right button action" />
|
||||
<RadioButton
|
||||
Content="Nothing"
|
||||
GroupName="RightButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseRightClickNoneIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Context menu"
|
||||
GroupName="RightButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseRightClickContextMenuIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Margin="0,0,0,24"
|
||||
Content="Paste"
|
||||
GroupName="RightButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseRightClickPasteIsSelected, Mode=TwoWay}" />
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Copy on select"
|
||||
IsOn="{x:Bind ViewModel.CopyOnSelect, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Middle button action" />
|
||||
<RadioButton
|
||||
Content="Nothing"
|
||||
GroupName="MiddleButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseMiddleClickNoneIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Context menu"
|
||||
GroupName="MiddleButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseMiddleClickContextMenuIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Paste"
|
||||
GroupName="MiddleButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseMiddleClickPasteIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Right button action" />
|
||||
<RadioButton
|
||||
Content="Nothing"
|
||||
GroupName="RightButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseRightClickNoneIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Context menu"
|
||||
GroupName="RightButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseRightClickContextMenuIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Paste"
|
||||
GroupName="RightButton"
|
||||
IsChecked="{x:Bind ViewModel.MouseRightClickPasteIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
@@ -48,6 +48,33 @@
|
||||
</DataTemplate>
|
||||
</views:TerminalThemeTemplateSelector.ThemeTemplate>
|
||||
</views:TerminalThemeTemplateSelector>
|
||||
<views:TerminalKeybindTemplateSelector x:Key="TerminalKeybindTemplateSelector">
|
||||
<views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<TextBlock FontStyle="Italic" Text="None" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.DefaultTemplate>
|
||||
<views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
|
||||
<views:KeyBindingsView
|
||||
Margin="0,6,0,0"
|
||||
ShowCommandName="False"
|
||||
ViewModel="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
|
||||
</views:TerminalKeybindTemplateSelector>
|
||||
<views:NoValueTemplateSelector x:Key="NoValueTemplateSelector">
|
||||
<views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock FontStyle="Italic" Text="None" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.DefaultTemplate>
|
||||
<views:NoValueTemplateSelector.CustomTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{x:Bind}" />
|
||||
</DataTemplate>
|
||||
</views:NoValueTemplateSelector.CustomTemplate>
|
||||
</views:NoValueTemplateSelector>
|
||||
</Page.Resources>
|
||||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -63,16 +90,13 @@
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="0"
|
||||
Background="{ThemeResource SystemControlAcrylicWindowBrush}" />
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,0,0,24">
|
||||
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="{StaticResource HeaderMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Margin="24,10,0,0"
|
||||
Margin="0,10,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="28"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
@@ -130,7 +154,7 @@
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<ListView ItemsSource="{x:Bind ViewModel.ShellProfiles}" SelectedItem="{x:Bind ViewModel.SelectedShellProfile, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:ShellProfileViewModel">
|
||||
@@ -181,23 +205,26 @@
|
||||
<ScrollViewer
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="24,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ContentControl HorizontalContentAlignment="Stretch" Content="{x:Bind ViewModel.SelectedShellProfile, Mode=OneWay}">
|
||||
<ContentControl
|
||||
Margin="{StaticResource ContainerMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Content="{x:Bind ViewModel.SelectedShellProfile, Mode=OneWay}">
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:ShellProfileViewModel">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<StackPanel Margin="0,0,24,0" Visibility="{x:Bind InEditMode, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
|
||||
<StackPanel Visibility="{x:Bind InEditMode, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
|
||||
<TextBox
|
||||
Width="300"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Name"
|
||||
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<StackPanel Margin="0,24,0,0" Orientation="Horizontal">
|
||||
<StackPanel Margin="{StaticResource ItemMargin}" Orientation="Horizontal">
|
||||
<TextBox
|
||||
Width="300"
|
||||
Header="Shell executable location"
|
||||
@@ -213,7 +240,7 @@
|
||||
Style="{StaticResource ButtonRevealStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,24,0,0" Orientation="Horizontal">
|
||||
<StackPanel Margin="{StaticResource ItemMargin}" Orientation="Horizontal">
|
||||
<TextBox
|
||||
Width="300"
|
||||
Header="Working directory"
|
||||
@@ -230,83 +257,126 @@
|
||||
|
||||
<TextBox
|
||||
Width="300"
|
||||
Margin="0,24,0,0"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Arguments"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
IsSpellCheckEnabled="False"
|
||||
Text="{x:Bind Arguments, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<ComboBox
|
||||
Width="150"
|
||||
Margin="0,24,0,0"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Tab color"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
ItemTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TabThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTabTheme, Mode=TwoWay}" />
|
||||
|
||||
<ComboBox
|
||||
Width="150"
|
||||
Margin="0,24,0,0"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Theme"
|
||||
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
|
||||
ItemTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
ItemsSource="{x:Bind TerminalThemes, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedTerminalTheme, Mode=TwoWay}" />
|
||||
<StackPanel
|
||||
Width="300"
|
||||
Margin="0,24,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Line ending translation mode on paste" />
|
||||
<RadioButton
|
||||
Content="Do not modify line endings"
|
||||
GroupName="LineEndingStyle"
|
||||
IsChecked="{x:Bind DoNotModifyIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Convert line endings to CR+LF"
|
||||
GroupName="LineEndingStyle"
|
||||
IsChecked="{x:Bind ToCRLFIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Convert line endings to CR"
|
||||
GroupName="LineEndingStyle"
|
||||
IsChecked="{x:Bind ToCRIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Convert line endings to LF"
|
||||
GroupName="LineEndingStyle"
|
||||
IsChecked="{x:Bind ToLFIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock
|
||||
Margin="0,0,6,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Keyboard shortcut" />
|
||||
<HyperlinkButton Command="{x:Bind AddKeyboardShortcutCommand}" Content="Add" />
|
||||
<views:KeyBindingsView ShowCommandName="False" ViewModel="{x:Bind KeyBindings}" />
|
||||
</StackPanel>
|
||||
<views:KeyBindingsView ShowCommandName="False" ViewModel="{x:Bind KeyBindings}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0" Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind Name, Mode=OneWay}" />
|
||||
<StackPanel Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
|
||||
<controls:HeaderedTextBlock
|
||||
Margin="0,58,0,0"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Shell executable location"
|
||||
Text="{x:Bind Location, Mode=OneWay}" />
|
||||
<controls:HeaderedTextBlock
|
||||
Margin="0,36,0,0"
|
||||
Header="Working directory"
|
||||
Text="{x:Bind WorkingDirectory, Mode=OneWay}" />
|
||||
<controls:HeaderedTextBlock
|
||||
Margin="0,36,0,0"
|
||||
Header="Arguments"
|
||||
Text="{x:Bind Arguments, Mode=OneWay}" />
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
Margin="0,36,0,0"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Theme">
|
||||
Header="Working directory">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind SelectedTerminalTheme, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}" />
|
||||
Content="{x:Bind WorkingDirectory, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
Margin="0,36,0,0"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Arguments">
|
||||
<ContentControl
|
||||
Content="{x:Bind Arguments, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Tab color">
|
||||
<ContentControl
|
||||
Margin="0,6,0,0"
|
||||
Content="{x:Bind SelectedTabTheme, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}" />
|
||||
</controls:HeaderedContentControl>
|
||||
<controls:HeaderedContentControl
|
||||
Margin="0,36,0,0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Keyboard shortcut">
|
||||
<views:KeyBindingsView
|
||||
Margin="0,6,0,0"
|
||||
ShowCommandName="False"
|
||||
ViewModel="{x:Bind KeyBindings}" />
|
||||
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Line ending translation mode">
|
||||
<ContentControl
|
||||
Content="{x:Bind LineEndingStyle, Mode=OneWay, Converter={StaticResource EnumToDescriptionConverter}}"
|
||||
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
|
||||
<controls:HeaderedContentControl
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Header="Keyboard shortcut">
|
||||
<ContentControl
|
||||
Content="{x:Bind KeyBindings, Mode=OneWay}"
|
||||
ContentTemplateSelector="{StaticResource TerminalKeybindTemplateSelector}"
|
||||
FontSize="18"
|
||||
FontWeight="Light" />
|
||||
</controls:HeaderedContentControl>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
@@ -314,4 +384,4 @@
|
||||
</ContentControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
</Page>
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:converters="using:FluentTerminal.App.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:FluentTerminal.Models"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<converters:ToolTipValueToPixelConverter x:Key="ToolTipValueToPixelConverter" />
|
||||
@@ -15,7 +16,7 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1" Margin="24,00,0,24">
|
||||
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
@@ -38,32 +39,47 @@
|
||||
Label="Restore Defaults" />
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
|
||||
<StackPanel>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<StackPanel Margin="{StaticResource ContainerMargin}">
|
||||
<ComboBox
|
||||
Width="200"
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Font family"
|
||||
ItemsSource="{x:Bind ViewModel.Fonts}"
|
||||
SelectedValue="{x:Bind ViewModel.FontFamily, Mode=TwoWay}" />
|
||||
SelectedValue="{x:Bind ViewModel.FontFamily, Mode=TwoWay}"
|
||||
SelectedValuePath="Name">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:FontInfo">
|
||||
<TextBlock FontWeight="{x:Bind IsMonospaced, Converter={StaticResource TrueToBoldConverter}}" Text="{x:Bind Name}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<ComboBox
|
||||
Width="200"
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Font size"
|
||||
ItemsSource="{x:Bind ViewModel.Sizes}"
|
||||
SelectedValue="{x:Bind ViewModel.FontSize, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Bold text"
|
||||
IsOn="{x:Bind ViewModel.BoldText, Mode=TwoWay}" />
|
||||
|
||||
<Slider
|
||||
Width="200"
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Background opacity"
|
||||
Maximum="1"
|
||||
Minimum="0"
|
||||
StepFrequency="0.01"
|
||||
Value="{x:Bind ViewModel.BackgroundOpacity, Mode=TwoWay}" />
|
||||
|
||||
<Slider
|
||||
Width="200"
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Padding"
|
||||
Maximum="64"
|
||||
@@ -71,45 +87,52 @@
|
||||
StepFrequency="1"
|
||||
ThumbToolTipValueConverter="{StaticResource ToolTipValueToPixelConverter}"
|
||||
Value="{x:Bind ViewModel.Padding, Mode=TwoWay}" />
|
||||
|
||||
<TextBox
|
||||
Width="200"
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Scrollback limit"
|
||||
KeyDown="ScrollBackTextBoxOnKeyDown"
|
||||
Text="{x:Bind ViewModel.ScrollBackLimit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<ToggleSwitch
|
||||
Margin="0,0,0,24"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Header="Blinking cursor"
|
||||
IsOn="{x:Bind ViewModel.CursorBlink, Mode=TwoWay}" />
|
||||
<TextBlock Margin="0,0,0,8" Text="Cursor style" />
|
||||
<RadioButton
|
||||
Content="Block"
|
||||
GroupName="Cursor"
|
||||
IsChecked="{x:Bind ViewModel.BlockIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Underline"
|
||||
GroupName="Cursor"
|
||||
IsChecked="{x:Bind ViewModel.UnderlineIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Bar"
|
||||
GroupName="Cursor"
|
||||
IsChecked="{x:Bind ViewModel.BarIsSelected, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Margin="0,24,0,8" Text="Scrollbar style" />
|
||||
<RadioButton
|
||||
Content="Hidden"
|
||||
GroupName="ScrollBar"
|
||||
IsChecked="{x:Bind ViewModel.HiddenIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Auto hiding"
|
||||
GroupName="ScrollBar"
|
||||
IsChecked="{x:Bind ViewModel.AutoHidingIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Visible"
|
||||
GroupName="ScrollBar"
|
||||
IsChecked="{x:Bind ViewModel.VisibleIsSelected, Mode=TwoWay}" />
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Cursor style" />
|
||||
<RadioButton
|
||||
Content="Block"
|
||||
GroupName="Cursor"
|
||||
IsChecked="{x:Bind ViewModel.BlockIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Underline"
|
||||
GroupName="Cursor"
|
||||
IsChecked="{x:Bind ViewModel.UnderlineIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Bar"
|
||||
GroupName="Cursor"
|
||||
IsChecked="{x:Bind ViewModel.BarIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<TextBlock Margin="0,0,0,8" Text="Scrollbar style" />
|
||||
<RadioButton
|
||||
Content="Hidden"
|
||||
GroupName="ScrollBar"
|
||||
IsChecked="{x:Bind ViewModel.HiddenIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Auto hiding"
|
||||
GroupName="ScrollBar"
|
||||
IsChecked="{x:Bind ViewModel.AutoHidingIsSelected, Mode=TwoWay}" />
|
||||
<RadioButton
|
||||
Content="Visible"
|
||||
GroupName="ScrollBar"
|
||||
IsChecked="{x:Bind ViewModel.VisibleIsSelected, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
</Page>
|
||||
@@ -28,16 +28,13 @@
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="0"
|
||||
Background="{ThemeResource SystemControlAcrylicWindowBrush}" />
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,0,0,24">
|
||||
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="{StaticResource HeaderMargin}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Margin="24,10,0,0"
|
||||
Margin="0,10,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="28"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
@@ -103,7 +100,7 @@
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<StackPanel Margin="{StaticResource ItemMargin}">
|
||||
<ListView ItemsSource="{x:Bind ViewModel.Themes}" SelectedItem="{x:Bind ViewModel.SelectedTheme, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:ThemeViewModel">
|
||||
@@ -175,15 +172,16 @@
|
||||
<ContentControl
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="24,0,0,0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Content="{x:Bind ViewModel.SelectedTheme, Mode=TwoWay}"
|
||||
RequestedTheme="{x:Bind ViewModel.SelectedTheme.Background, Mode=OneWay, Converter={StaticResource BackgroundToApplicationThemeConverter}}">
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate x:DataType="viewmodels:ThemeViewModel">
|
||||
<ScrollViewer Padding="0,0,24,0" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="{StaticResource ContainerMargin}">
|
||||
<!-- VIEW THEME -->
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}"
|
||||
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
@@ -191,22 +189,42 @@
|
||||
<SolidColorBrush Color="{x:Bind Foreground, Mode=OneWay}" />
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind Author, Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
FontSize="14"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Author, Mode=OneWay}"
|
||||
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush Color="{x:Bind Foreground, Mode=OneWay}" />
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
|
||||
<!-- EDIT THEME -->
|
||||
<TextBox
|
||||
Width="200"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Name"
|
||||
Text="{x:Bind Name, Mode=TwoWay}"
|
||||
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
|
||||
<TextBox
|
||||
Margin="0,24,0,0"
|
||||
Width="200"
|
||||
Margin="{StaticResource ItemMargin}"
|
||||
HorizontalAlignment="Left"
|
||||
Header="Author"
|
||||
Text="{x:Bind Author, Mode=TwoWay}"
|
||||
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
<Grid Margin="0,48,0,0">
|
||||
|
||||
<Grid Margin="{StaticResource ItemMargin}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.Resources>
|
||||
<Style TargetType="Border">
|
||||
@@ -215,7 +233,16 @@
|
||||
<Setter Property="Height" Value="24" />
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal">
|
||||
<TextBlock Grid.Row="0" Text="Color palette" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,0,0,8"
|
||||
FontSize="14"
|
||||
FontStyle="Italic"
|
||||
Foreground="LightGray"
|
||||
Text="Click on a color to edit"
|
||||
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal">
|
||||
<views:TerminalColorPicker
|
||||
ColorName="Black"
|
||||
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
|
||||
@@ -249,7 +276,7 @@
|
||||
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
|
||||
SelectedColor="{x:Bind White, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=false}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal">
|
||||
<views:TerminalColorPicker
|
||||
ColorName="Bright Black"
|
||||
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
|
||||
@@ -283,7 +310,7 @@
|
||||
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
|
||||
SelectedColor="{x:Bind BrightWhite, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=false}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal">
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal">
|
||||
<views:TerminalColorPicker
|
||||
ColorName="Foreground"
|
||||
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
|
||||
@@ -306,11 +333,6 @@
|
||||
IsAlphaEnabled="True"
|
||||
SelectedColor="{x:Bind Selection, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=true}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Margin="0,6,0,0"
|
||||
Text="Click on a color to edit"
|
||||
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
@@ -318,4 +340,4 @@
|
||||
</ContentControl.ContentTemplate>
|
||||
</ContentControl>
|
||||
</Grid>
|
||||
</Page>
|
||||
</Page>
|
||||
@@ -58,7 +58,7 @@
|
||||
Margin="0"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
|
||||
ToolTipService.ToolTip="{x:Bind TabTitle, Mode=OneWay}">
|
||||
<Interactivity:Interaction.Behaviors>
|
||||
<behaviors:MiddleClickBehavior>
|
||||
<behaviors:MiddleClickBehavior.Actions>
|
||||
@@ -171,7 +171,7 @@
|
||||
RelativePanel.AlignLeftWithPanel="True"
|
||||
RelativePanel.LeftOf="CloseButton"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
Text="{x:Bind TabTitle, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<Grid
|
||||
@@ -189,7 +189,7 @@
|
||||
<SymbolIcon Symbol="Cancel" />
|
||||
</Viewbox>
|
||||
</Button>
|
||||
<Grid Height="32" Visibility="{x:Bind NewOutput, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<Grid Height="32" Visibility="{x:Bind HasNewOutput, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
|
||||
<Viewbox
|
||||
Width="12"
|
||||
Height="12"
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
mc:Ignorable="d">
|
||||
<Button
|
||||
x:Name="Root"
|
||||
Background="{x:Bind ButtonBackground, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind EnableEditing, Mode=OneWay}"
|
||||
Style="{StaticResource ColorPreviewButtonStyle}"
|
||||
ToolTipService.ToolTip="{x:Bind ColorName}"
|
||||
Background="{x:Bind ButtonBackground, Mode=OneWay}">
|
||||
ToolTipService.ToolTip="{x:Bind ColorName}">
|
||||
<!--<Button.Background>
|
||||
<SolidColorBrush Color="{x:Bind VisibleColor, Mode=OneWay}" />
|
||||
</Button.Background>-->
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace FluentTerminal.App.Views
|
||||
{
|
||||
public sealed partial class TerminalColorPicker : UserControl
|
||||
{
|
||||
bool colorIsNull = false;
|
||||
private bool _colorIsNull;
|
||||
|
||||
public static readonly DependencyProperty ColorNameProperty =
|
||||
DependencyProperty.Register(nameof(ColorName), typeof(string), typeof(TerminalColorPicker), new PropertyMetadata(null));
|
||||
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Views
|
||||
{
|
||||
if (s is TerminalColorPicker terminalColorPicker)
|
||||
{
|
||||
terminalColorPicker.colorIsNull = ((Color?)e.NewValue) == null;
|
||||
terminalColorPicker._colorIsNull = ((Color?)e.NewValue) == null;
|
||||
|
||||
// When leading the color, or changing the selected color (happens on page load)
|
||||
// only set the picker (triggering the event action later) if the color is non-null
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using FluentTerminal.App.ViewModels.Settings;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
|
||||
namespace FluentTerminal.App.Views
|
||||
{
|
||||
public class TerminalKeybindTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate DefaultTemplate { get; set; }
|
||||
public DataTemplate KeyBindingsTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
return SelectTemplateCore(item);
|
||||
}
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
if (item is KeyBindingsViewModel keyBindings)
|
||||
{
|
||||
return keyBindings.KeyBindings.Count == 0 ? DefaultTemplate : KeyBindingsTemplate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid x:Name="WebViewContainer" Grid.Row="0" />
|
||||
<Grid x:Name="TerminalContainer" Grid.Row="0" />
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Margin="12"
|
||||
@@ -38,7 +38,7 @@
|
||||
Name="SearchTextBox"
|
||||
Width="300"
|
||||
Margin="6,0,6,0"
|
||||
KeyUp="SearchTextBox_KeyUp"
|
||||
KeyUp="OnSearchTextBoxKeyUp"
|
||||
PlaceholderText="Enter search term"
|
||||
RelativePanel.RightOf="SearchLabel"
|
||||
Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
@@ -1,248 +1,69 @@
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.RuntimeComponent.Enums;
|
||||
using FluentTerminal.RuntimeComponent.Interfaces;
|
||||
using FluentTerminal.RuntimeComponent.WebAllowedObjects;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.Core;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
|
||||
namespace FluentTerminal.App.Views
|
||||
{
|
||||
public sealed partial class TerminalView : UserControl, ITerminalView, ITerminalEventListener
|
||||
public sealed partial class TerminalView : UserControl
|
||||
{
|
||||
private MenuFlyoutItem _copyMenuItem;
|
||||
private BlockingCollection<Action> _dispatcherJobs;
|
||||
private readonly SemaphoreSlim _loaded;
|
||||
private MenuFlyoutItem _pasteMenuItem;
|
||||
private WebView _webView;
|
||||
private readonly ITerminalView _terminalView;
|
||||
|
||||
public TerminalView(TerminalViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
ViewModel.SearchStarted += OnSearchStarted;
|
||||
ViewModel.Activated += OnActivated;
|
||||
ViewModel.ThemeChanged += OnThemeChanged;
|
||||
ViewModel.OptionsChanged += OnOptionsChanged;
|
||||
ViewModel.KeyBindingsChanged += OnKeyBindingsChanged;
|
||||
ViewModel.FindNextRequested += OnFindNextRequested;
|
||||
ViewModel.FindPreviousRequested += OnFindPreviousRequested;
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
_loaded = new SemaphoreSlim(0, 1);
|
||||
|
||||
JsonConvert.DefaultSettings = () =>
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
};
|
||||
settings.Converters.Add(new StringEnumConverter(true));
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
StartMediatorTask();
|
||||
_terminalView = new NativeFrontend.TerminalControl(ViewModel.SettingsService);
|
||||
//_terminalView = new VtNetCoreFrontend.VtNetCoreTerminal();
|
||||
//_terminalView = new XtermTerminalView();
|
||||
TerminalContainer.Children.Add((UIElement)_terminalView);
|
||||
_terminalView.Initialize(ViewModel);
|
||||
}
|
||||
|
||||
public event EventHandler<string> KeyboardCommandReceived;
|
||||
|
||||
public event EventHandler<TerminalSize> TerminalSizeChanged;
|
||||
|
||||
public event EventHandler<string> TerminalTitleChanged;
|
||||
|
||||
public TerminalViewModel ViewModel { get; }
|
||||
|
||||
public Task ChangeOptions(TerminalOptions options)
|
||||
private async void OnActivated(object sender, EventArgs e)
|
||||
{
|
||||
var serialized = JsonConvert.SerializeObject(options);
|
||||
return ExecuteScriptAsync($"changeOptions('{serialized}')");
|
||||
}
|
||||
|
||||
public Task ChangeTheme(TerminalColors theme)
|
||||
{
|
||||
var serialized = JsonConvert.SerializeObject(theme);
|
||||
return ExecuteScriptAsync($"changeTheme('{serialized}')");
|
||||
}
|
||||
|
||||
public Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings)
|
||||
{
|
||||
var serialized = JsonConvert.SerializeObject(keyBindings);
|
||||
return ExecuteScriptAsync($"changeKeyBindings('{serialized}')");
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_webView?.Navigate(new Uri("about:blank"));
|
||||
_webView = null;
|
||||
}
|
||||
|
||||
public Task ConnectToSocket(string url)
|
||||
{
|
||||
return ExecuteScriptAsync($"connectToWebSocket('{url}');");
|
||||
}
|
||||
|
||||
public async Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings)
|
||||
{
|
||||
var serializedOptions = JsonConvert.SerializeObject(options);
|
||||
var serializedTheme = JsonConvert.SerializeObject(theme);
|
||||
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
|
||||
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}')").ConfigureAwait(true);
|
||||
return JsonConvert.DeserializeObject<TerminalSize>(size);
|
||||
}
|
||||
|
||||
public Task<string> ExecuteScriptAsync(string script)
|
||||
{
|
||||
try
|
||||
if (_terminalView != null)
|
||||
{
|
||||
return _webView.InvokeScriptAsync("eval", new[] { script }).AsTask();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine($"Exception while running:\n \"{script}\"\n\n {e}");
|
||||
}
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public async Task FocusTerminal()
|
||||
{
|
||||
if (_webView != null)
|
||||
{
|
||||
_webView.Focus(FocusState.Programmatic);
|
||||
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
|
||||
await _terminalView.FocusTerminal().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task FocusWebView()
|
||||
private async void OnFindNextRequested(object sender, string e)
|
||||
{
|
||||
if (_webView != null)
|
||||
{
|
||||
_webView.Focus(FocusState.Programmatic);
|
||||
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
|
||||
}
|
||||
await _terminalView.FindNext(e).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public Task<string> GetSelection() => ExecuteScriptAsync("term.getSelection()");
|
||||
|
||||
public void OnKeyboardCommand(string command)
|
||||
private async void OnFindPreviousRequested(object sender, string e)
|
||||
{
|
||||
if (Enum.TryParse(command, true, out Command commandValue))
|
||||
{
|
||||
_dispatcherJobs.Add(() => KeyboardCommandReceived?.Invoke(this, commandValue.ToString()));
|
||||
}
|
||||
else if (Guid.TryParse(command, out Guid shellProfileId))
|
||||
{
|
||||
_dispatcherJobs.Add(() => KeyboardCommandReceived?.Invoke(this, shellProfileId.ToString()));
|
||||
}
|
||||
await _terminalView.FindPrevious(e).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public void OnTerminalResized(int columns, int rows)
|
||||
private async void OnKeyBindingsChanged(object sender, EventArgs e)
|
||||
{
|
||||
_dispatcherJobs.Add(() => TerminalSizeChanged?.Invoke(this, new TerminalSize { Columns = columns, Rows = rows }));
|
||||
await _terminalView.ChangeKeyBindings().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public void OnTitleChanged(string title)
|
||||
private async void OnOptionsChanged(object sender, Models.TerminalOptions e)
|
||||
{
|
||||
_dispatcherJobs.Add(() => TerminalTitleChanged?.Invoke(this, title));
|
||||
await _terminalView.ChangeOptions(e).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
public Task FindNext(string searchText)
|
||||
private void OnSearchStarted(object sender, EventArgs e)
|
||||
{
|
||||
return ExecuteScriptAsync($"findNext('{searchText}')");
|
||||
SearchTextBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
public Task FindPrevious(string searchText)
|
||||
{
|
||||
return ExecuteScriptAsync($"findPrevious('{searchText}')");
|
||||
}
|
||||
|
||||
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
|
||||
{
|
||||
_loaded.Release();
|
||||
}
|
||||
|
||||
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
|
||||
{
|
||||
var bridge = new TerminalBridge(this);
|
||||
_webView.AddWebAllowedObject("terminalBridge", bridge);
|
||||
}
|
||||
|
||||
private void Copy_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OnKeyboardCommand(nameof(Command.Copy));
|
||||
}
|
||||
|
||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (WebViewContainer.Children.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_webView = new WebView(WebViewExecutionMode.SeparateThread)
|
||||
{
|
||||
DefaultBackgroundColor = Colors.Transparent
|
||||
};
|
||||
WebViewContainer.Children.Add(_webView);
|
||||
|
||||
_webView.NavigationCompleted += _webView_NavigationCompleted;
|
||||
_webView.NavigationStarting += _webView_NavigationStarting;
|
||||
|
||||
_copyMenuItem = new MenuFlyoutItem { Text = "Copy" };
|
||||
_copyMenuItem.Click += Copy_Click;
|
||||
|
||||
_pasteMenuItem = new MenuFlyoutItem { Text = "Paste" };
|
||||
_pasteMenuItem.Click += Paste_Click;
|
||||
|
||||
_webView.ContextFlyout = new MenuFlyout
|
||||
{
|
||||
Items = { _copyMenuItem, _pasteMenuItem }
|
||||
};
|
||||
|
||||
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
|
||||
|
||||
await _loaded.WaitAsync().ConfigureAwait(true);
|
||||
|
||||
_webView.Focus(FocusState.Programmatic);
|
||||
|
||||
await ViewModel.OnViewIsReady(this).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void Paste_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OnKeyboardCommand(nameof(Command.Paste));
|
||||
}
|
||||
|
||||
private void StartMediatorTask()
|
||||
{
|
||||
_dispatcherJobs = new BlockingCollection<Action>();
|
||||
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
|
||||
{
|
||||
try
|
||||
{
|
||||
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => job.Invoke());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void SearchTextBox_KeyUp(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||
private void OnSearchTextBoxKeyUp(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Escape)
|
||||
{
|
||||
@@ -254,57 +75,9 @@ namespace FluentTerminal.App.Views
|
||||
}
|
||||
}
|
||||
|
||||
public void FocusSearchTextBox()
|
||||
private async void OnThemeChanged(object sender, Models.TerminalTheme e)
|
||||
{
|
||||
SearchTextBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
public void OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection)
|
||||
{
|
||||
_dispatcherJobs.Add(() =>
|
||||
{
|
||||
if (mouseButton == MouseButton.Middle)
|
||||
{
|
||||
if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.ContextMenu)
|
||||
{
|
||||
ShowContextMenu(x, y, hasSelection);
|
||||
}
|
||||
else if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.Paste)
|
||||
{
|
||||
OnKeyboardCommand(nameof(Command.Paste));
|
||||
}
|
||||
}
|
||||
else if (mouseButton == MouseButton.Right)
|
||||
{
|
||||
if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.ContextMenu)
|
||||
{
|
||||
ShowContextMenu(x, y, hasSelection);
|
||||
}
|
||||
else if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.Paste)
|
||||
{
|
||||
OnKeyboardCommand(nameof(Command.Paste));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async void OnSelectionChanged(string selection)
|
||||
{
|
||||
if (ViewModel.ApplicationSettings.CopyOnSelect && !ViewModel.ShowSearchPanel)
|
||||
{
|
||||
_dispatcherJobs.Add(async () =>
|
||||
{
|
||||
ViewModel.CopyText(selection);
|
||||
await ExecuteScriptAsync("term.clearSelection()");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowContextMenu(int x, int y, bool terminalHasSelection)
|
||||
{
|
||||
var flyout = (MenuFlyout)_webView.ContextFlyout;
|
||||
_copyMenuItem.IsEnabled = terminalHasSelection;
|
||||
flyout.ShowAt(_webView, new Point(x, y));
|
||||
await _terminalView.ChangeTheme(e).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<UserControl
|
||||
x:Class="FluentTerminal.App.Views.VtNetCoreFrontend.VtNetCoreTerminal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:FluentTerminal.App.Views.VtNetCoreFrontend"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:xaml="using:Microsoft.Graphics.Canvas.UI.Xaml"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="400"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<xaml:CanvasControl x:Name="canvas" IsTabStop="true" />
|
||||
</UserControl>
|
||||
@@ -0,0 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Utilities;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using VtNetCore.VirtualTerminal;
|
||||
using VtNetCore.XTermParser;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Controls.Primitives;
|
||||
using Windows.UI.Xaml.Data;
|
||||
using Windows.UI.Xaml.Input;
|
||||
using Windows.UI.Xaml.Media;
|
||||
using Windows.UI.Xaml.Navigation;
|
||||
|
||||
// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
|
||||
|
||||
namespace FluentTerminal.App.Views.VtNetCoreFrontend
|
||||
{
|
||||
public sealed partial class VtNetCoreTerminal : UserControl, ITerminalView
|
||||
{
|
||||
private double _characterHeight;
|
||||
private double _characterWidth;
|
||||
private int _cols;
|
||||
private CanvasTextFormat _format;
|
||||
private int _rows;
|
||||
|
||||
public VtNetCoreTerminal()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
canvas.Draw += _canvas_Draw;
|
||||
canvas.KeyDown += Canvas_KeyDown;
|
||||
canvas.GotFocus += Canvas_GotFocus;
|
||||
canvas.LostFocus += Canvas_LostFocus;
|
||||
canvas.Tapped += Canvas_Tapped;
|
||||
}
|
||||
|
||||
private void Canvas_LostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Window.Current.CoreWindow.CharacterReceived -= CoreWindow_CharacterReceived;
|
||||
}
|
||||
|
||||
private void Canvas_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
canvas.Focus(FocusState.Pointer);
|
||||
}
|
||||
|
||||
private void Canvas_GotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Window.Current.CoreWindow.CharacterReceived += CoreWindow_CharacterReceived;
|
||||
Debug.WriteLine("focus");
|
||||
}
|
||||
|
||||
private void Canvas_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var controlPressed = (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down));
|
||||
var shiftPressed = (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down));
|
||||
|
||||
//Scrolling = false;
|
||||
|
||||
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
|
||||
// I lookup whether KeyPressed should handle the key here or there.
|
||||
var code = _terminal.GetKeySequence(e.Key.ToString(), controlPressed, shiftPressed);
|
||||
if (code != null)
|
||||
e.Handled = _terminal.KeyPressed(e.Key.ToString(), controlPressed, shiftPressed);
|
||||
|
||||
canvas.Invalidate();
|
||||
|
||||
//if (ViewTop != _terminal.ViewPort.TopRow)
|
||||
//{
|
||||
// ViewTop = _terminal.ViewPort.TopRow;
|
||||
// canvas.Invalidate();
|
||||
//}
|
||||
}
|
||||
|
||||
private void CoreWindow_CharacterReceived(CoreWindow sender, CharacterReceivedEventArgs args)
|
||||
{
|
||||
//if (!Connected)
|
||||
// return;
|
||||
|
||||
var ch = char.ConvertFromUtf32((int)args.KeyCode);
|
||||
switch (ch)
|
||||
{
|
||||
case "\b":
|
||||
case "\t":
|
||||
case "\n":
|
||||
return;
|
||||
|
||||
case "\r":
|
||||
////LogText = "";
|
||||
//lock(_rawText)
|
||||
//{
|
||||
// _rawTextLength = 0;
|
||||
// _rawTextChanged = true;
|
||||
//}
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
|
||||
// I lookup whether KeyPressed should handle the key here or there.
|
||||
var code = _terminal.GetKeySequence(ch, false, false);
|
||||
if (code == null)
|
||||
args.Handled = _terminal.KeyPressed(ch, false, false);
|
||||
}
|
||||
|
||||
private void _canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
|
||||
{
|
||||
var spans = _terminal.ViewPort.GetPageSpans(0, _rows, _cols);
|
||||
|
||||
for (var i = 0; i < spans.Count; i++)
|
||||
{
|
||||
var span = spans[i];
|
||||
var x = 0.0;
|
||||
foreach (var item in span.Spans)
|
||||
{
|
||||
|
||||
|
||||
|
||||
if (item.BackgroundColor != "#000000")
|
||||
{
|
||||
var background = item.BackgroundColor.FromString();
|
||||
args.DrawingSession.FillRectangle(new Rect { Height = _characterHeight, Width = _characterWidth * item.Text.Length, X = x, Y = i * 16 }, background);
|
||||
}
|
||||
|
||||
var foreground = item.ForgroundColor.FromString();
|
||||
args.DrawingSession.DrawText(item.Text, new System.Numerics.Vector2 { X = (float)x, Y = i * 16 }, foreground, _format);
|
||||
|
||||
|
||||
x += item.Text.Length * _characterWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task ChangeKeyBindings()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeOptions(TerminalOptions options)
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeTheme(TerminalTheme theme)
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FindNext(string searchText)
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FindPrevious(string searchText)
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FocusTerminal()
|
||||
{
|
||||
canvas.Focus(FocusState.Programmatic);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public TerminalViewModel ViewModel { get; private set; }
|
||||
|
||||
public async Task Initialize(TerminalViewModel viewModel)
|
||||
{
|
||||
Measure();
|
||||
|
||||
ViewModel = viewModel;
|
||||
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
//ViewModel.Terminal.RegisterSelectedTextCallback(() => ExecuteScriptAsync("term.getSelection()"));
|
||||
//ViewModel.Terminal.Closed += Terminal_Closed;
|
||||
|
||||
var options = ViewModel.SettingsService.GetTerminalOptions();
|
||||
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
|
||||
var profiles = ViewModel.SettingsService.GetShellProfiles();
|
||||
var settings = ViewModel.SettingsService.GetApplicationSettings();
|
||||
var theme = ViewModel.TerminalTheme;
|
||||
var sessionType = SessionType.Unknown;
|
||||
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
|
||||
{
|
||||
sessionType = SessionType.WinPty;
|
||||
}
|
||||
else
|
||||
{
|
||||
sessionType = SessionType.ConPty;
|
||||
}
|
||||
var size = new Models.TerminalSize { Rows = _rows, Columns = _cols };
|
||||
|
||||
_terminal = new VirtualTerminalController();
|
||||
_terminal.SendData = new EventHandler<SendDataEventArgs>((s, e) => { });
|
||||
_dataConsumer = new DataConsumer(_terminal);
|
||||
|
||||
var response = await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType).ConfigureAwait(true);
|
||||
if (!response.Success)
|
||||
{
|
||||
await ViewModel.DialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
|
||||
ViewModel.Terminal.ReportLauchFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.Focus(FocusState.Keyboard);
|
||||
}
|
||||
|
||||
private DataConsumer _dataConsumer;
|
||||
private VirtualTerminalController _terminal;
|
||||
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
_dataConsumer.Push(e);
|
||||
|
||||
if (_terminal.Changed)
|
||||
{
|
||||
_terminal.ClearChanges();
|
||||
canvas.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void Measure()
|
||||
{
|
||||
var device = CanvasDevice.GetSharedDevice();
|
||||
using (CanvasRenderTarget offscreen = new CanvasRenderTarget(device, (float)256, (float)256, canvas.Dpi))
|
||||
{
|
||||
using (CanvasDrawingSession ds = offscreen.CreateDrawingSession())
|
||||
{
|
||||
_format = new CanvasTextFormat
|
||||
{
|
||||
FontSize = 13,
|
||||
FontFamily = "Consolas NF"
|
||||
};
|
||||
|
||||
var textLayout = new CanvasTextLayout(ds, "\u2560", _format, 0.0f, 0.0f);
|
||||
_characterWidth = textLayout.DrawBounds.Right;
|
||||
_characterHeight = textLayout.DrawBounds.Bottom;
|
||||
|
||||
_cols = (int)Math.Floor(Window.Current.CoreWindow.Bounds.Width / _characterWidth);
|
||||
_rows = (int)Math.Floor((Window.Current.CoreWindow.Bounds.Height - 32) / _characterHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<UserControl
|
||||
x:Class="FluentTerminal.App.Views.XtermTerminalView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:FluentTerminal.App.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="400"
|
||||
mc:Ignorable="d">
|
||||
<Grid x:Name="Root" />
|
||||
</UserControl>
|
||||
@@ -0,0 +1,361 @@
|
||||
using Fleck;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Utilities;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.RuntimeComponent.Enums;
|
||||
using FluentTerminal.RuntimeComponent.Interfaces;
|
||||
using FluentTerminal.RuntimeComponent.WebAllowedObjects;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.Core;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
|
||||
namespace FluentTerminal.App.Views
|
||||
{
|
||||
public sealed partial class XtermTerminalView : UserControl, IxtermEventListener, ITerminalView
|
||||
{
|
||||
private readonly ManualResetEventSlim _connectedEvent;
|
||||
private readonly SemaphoreSlim _navigationCompleted;
|
||||
private readonly MenuFlyoutItem _copyMenuItem;
|
||||
private BlockingCollection<Action> _dispatcherJobs;
|
||||
private readonly MenuFlyoutItem _pasteMenuItem;
|
||||
private WebView _webView;
|
||||
private readonly DebouncedAction<TerminalOptions> _optionsChanged;
|
||||
private IWebSocketConnection _socket;
|
||||
|
||||
public XtermTerminalView()
|
||||
{
|
||||
InitializeComponent();
|
||||
StartMediatorTask();
|
||||
|
||||
_webView = new WebView(WebViewExecutionMode.SeparateThread)
|
||||
{
|
||||
DefaultBackgroundColor = Colors.Transparent
|
||||
};
|
||||
Root.Children.Add(_webView);
|
||||
|
||||
_webView.NavigationCompleted += _webView_NavigationCompleted;
|
||||
_webView.NavigationStarting += _webView_NavigationStarting;
|
||||
|
||||
_copyMenuItem = new MenuFlyoutItem { Text = "Copy" };
|
||||
_copyMenuItem.Click += Copy_Click;
|
||||
|
||||
_pasteMenuItem = new MenuFlyoutItem { Text = "Paste" };
|
||||
_pasteMenuItem.Click += Paste_Click;
|
||||
|
||||
_webView.ContextFlyout = new MenuFlyout
|
||||
{
|
||||
Items = { _copyMenuItem, _pasteMenuItem }
|
||||
};
|
||||
|
||||
_optionsChanged = new DebouncedAction<TerminalOptions>(Dispatcher, TimeSpan.FromMilliseconds(800), async options =>
|
||||
{
|
||||
var serialized = JsonConvert.SerializeObject(options);
|
||||
await ExecuteScriptAsync($"changeOptions('{serialized}')");
|
||||
});
|
||||
|
||||
_navigationCompleted = new SemaphoreSlim(0, 1);
|
||||
_connectedEvent = new ManualResetEventSlim(false);
|
||||
|
||||
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
|
||||
}
|
||||
|
||||
public TerminalViewModel ViewModel { get; private set; }
|
||||
|
||||
public Task ChangeKeyBindings()
|
||||
{
|
||||
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
|
||||
var profiles = ViewModel.SettingsService.GetShellProfiles();
|
||||
var serialized = JsonConvert.SerializeObject(FlattenKeyBindings(keyBindings, profiles));
|
||||
return ExecuteScriptAsync($"changeKeyBindings('{serialized}')");
|
||||
}
|
||||
|
||||
public Task ChangeOptions(TerminalOptions options)
|
||||
{
|
||||
_optionsChanged.Invoke(options);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeTheme(TerminalTheme theme)
|
||||
{
|
||||
var serialized = JsonConvert.SerializeObject(theme.Colors);
|
||||
return ExecuteScriptAsync($"changeTheme('{serialized}')");
|
||||
}
|
||||
|
||||
public Task FindNext(string searchText)
|
||||
{
|
||||
return ExecuteScriptAsync($"findNext('{searchText}')");
|
||||
}
|
||||
|
||||
public Task FindPrevious(string searchText)
|
||||
{
|
||||
return ExecuteScriptAsync($"findPrevious('{searchText}')");
|
||||
}
|
||||
|
||||
public async Task FocusTerminal()
|
||||
{
|
||||
if (_webView != null)
|
||||
{
|
||||
_webView.Focus(FocusState.Programmatic);
|
||||
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Initialize(TerminalViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
ViewModel.Terminal.RegisterSelectedTextCallback(() => ExecuteScriptAsync("term.getSelection()"));
|
||||
ViewModel.Terminal.Closed += Terminal_Closed;
|
||||
|
||||
var options = ViewModel.SettingsService.GetTerminalOptions();
|
||||
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
|
||||
var profiles = ViewModel.SettingsService.GetShellProfiles();
|
||||
var settings = ViewModel.SettingsService.GetApplicationSettings();
|
||||
var theme = ViewModel.TerminalTheme;
|
||||
var sessionType = SessionType.Unknown;
|
||||
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
|
||||
{
|
||||
sessionType = SessionType.WinPty;
|
||||
}
|
||||
else
|
||||
{
|
||||
sessionType = SessionType.ConPty;
|
||||
}
|
||||
await _navigationCompleted.WaitAsync().ConfigureAwait(true);
|
||||
var size = await CreateXtermView(options, theme.Colors, FlattenKeyBindings(keyBindings, profiles), sessionType).ConfigureAwait(true);
|
||||
var port = await ViewModel.TrayProcessCommunicationService.GetAvailablePort().ConfigureAwait(true);
|
||||
await CreateWebSocketServer(port.Port).ConfigureAwait(true);
|
||||
_connectedEvent.Wait();
|
||||
var response = await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType).ConfigureAwait(true);
|
||||
if (!response.Success)
|
||||
{
|
||||
await ViewModel.DialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
|
||||
ViewModel.Terminal.ReportLauchFailed();
|
||||
return;
|
||||
}
|
||||
_webView.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
void IxtermEventListener.OnKeyboardCommand(string command)
|
||||
{
|
||||
Logger.Instance.Debug("Received keyboard command: '{command}'", command);
|
||||
|
||||
if (Enum.TryParse(command, true, out Command commandValue))
|
||||
{
|
||||
_dispatcherJobs.Add(() => ViewModel.Terminal.ProcessKeyboardCommand(commandValue.ToString()));
|
||||
}
|
||||
else if (Guid.TryParse(command, out Guid shellProfileId))
|
||||
{
|
||||
_dispatcherJobs.Add(() => ViewModel.Terminal.ProcessKeyboardCommand(shellProfileId.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
void IxtermEventListener.OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection)
|
||||
{
|
||||
_dispatcherJobs.Add(() =>
|
||||
{
|
||||
if (mouseButton == MouseButton.Middle)
|
||||
{
|
||||
if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.ContextMenu)
|
||||
{
|
||||
ShowContextMenu(x, y, hasSelection);
|
||||
}
|
||||
else if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.Paste)
|
||||
{
|
||||
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
|
||||
}
|
||||
}
|
||||
else if (mouseButton == MouseButton.Right)
|
||||
{
|
||||
if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.ContextMenu)
|
||||
{
|
||||
ShowContextMenu(x, y, hasSelection);
|
||||
}
|
||||
else if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.Paste)
|
||||
{
|
||||
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IxtermEventListener.OnSelectionChanged(string selection)
|
||||
{
|
||||
if (ViewModel.ApplicationSettings.CopyOnSelect && !ViewModel.ShowSearchPanel)
|
||||
{
|
||||
_dispatcherJobs.Add(async () =>
|
||||
{
|
||||
ViewModel.CopyText(selection);
|
||||
await ExecuteScriptAsync("term.clearSelection()");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void IxtermEventListener.OnTerminalResized(int columns, int rows)
|
||||
{
|
||||
if (_connectedEvent.IsSet) // only propagate after xterm.js is finished with fitting
|
||||
{
|
||||
_dispatcherJobs.Add(async () => await ViewModel.Terminal.SetSize(new TerminalSize { Columns = columns, Rows = rows }).ConfigureAwait(true));
|
||||
}
|
||||
}
|
||||
|
||||
void IxtermEventListener.OnTitleChanged(string title)
|
||||
{
|
||||
_dispatcherJobs.Add(() => ViewModel.Terminal.SetTitle(title));
|
||||
}
|
||||
|
||||
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
|
||||
{
|
||||
Logger.Instance.Debug("WebView navigation completed. Target: {uri}", args.Uri);
|
||||
_navigationCompleted.Release();
|
||||
}
|
||||
|
||||
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
|
||||
{
|
||||
var bridge = new TerminalBridge(this);
|
||||
_webView.AddWebAllowedObject("terminalBridge", bridge);
|
||||
}
|
||||
|
||||
private void Copy_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Copy));
|
||||
}
|
||||
|
||||
private async Task CreateWebSocketServer(int port)
|
||||
{
|
||||
FleckLog.LogAction = (level, message, exception) =>
|
||||
{
|
||||
// todo: send debug to verbose
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel.Info:
|
||||
Logger.Instance.Information(message);
|
||||
break;
|
||||
case LogLevel.Warn:
|
||||
Logger.Instance.Warning(message);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
Logger.Instance.Error(message);
|
||||
break;
|
||||
}
|
||||
if (exception != null)
|
||||
{
|
||||
Logger.Instance.Error(exception, "Fleck Exception");
|
||||
}
|
||||
};
|
||||
|
||||
var webSocketUrl = "ws://127.0.0.1:" + port;
|
||||
var webSocketServer = new WebSocketServer(webSocketUrl);
|
||||
webSocketServer.Start(socket =>
|
||||
{
|
||||
_socket = socket;
|
||||
socket.OnOpen = () =>
|
||||
{
|
||||
Logger.Instance.Debug("WebSocket open");
|
||||
_connectedEvent.Set();
|
||||
};
|
||||
socket.OnMessage = message => ViewModel.Terminal.Write(Encoding.UTF8.GetBytes(message));
|
||||
});
|
||||
|
||||
Logger.Instance.Debug("WebSocketServer started. Calling connectToWebSocket() now.");
|
||||
|
||||
await ExecuteScriptAsync($"connectToWebSocket('{webSocketUrl}');").ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async Task<TerminalSize> CreateXtermView(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType)
|
||||
{
|
||||
var serializedOptions = JsonConvert.SerializeObject(options);
|
||||
var serializedTheme = JsonConvert.SerializeObject(theme);
|
||||
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
|
||||
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}', '{sessionType}')").ConfigureAwait(true);
|
||||
return JsonConvert.DeserializeObject<TerminalSize>(size);
|
||||
}
|
||||
|
||||
private Task<string> ExecuteScriptAsync(string script)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _webView.InvokeScriptAsync("eval", new[] { script }).AsTask();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine($"Exception while running:\n \"{script}\"\n\n {e}");
|
||||
}
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<string, ICollection<KeyBinding>> commandKeyBindings, IEnumerable<ShellProfile> profiles)
|
||||
{
|
||||
return commandKeyBindings.Values.SelectMany(k => k).Concat(profiles.SelectMany(x => x.KeyBindings));
|
||||
}
|
||||
|
||||
private void Paste_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
|
||||
}
|
||||
|
||||
private void ShowContextMenu(int x, int y, bool terminalHasSelection)
|
||||
{
|
||||
var flyout = (MenuFlyout)_webView.ContextFlyout;
|
||||
_copyMenuItem.IsEnabled = terminalHasSelection;
|
||||
flyout.ShowAt(_webView, new Point(x, y));
|
||||
}
|
||||
|
||||
private void StartMediatorTask()
|
||||
{
|
||||
_dispatcherJobs = new BlockingCollection<Action>();
|
||||
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
|
||||
{
|
||||
try
|
||||
{
|
||||
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => job.Invoke());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
private async void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
await ViewModel.ApplicationView.RunOnDispatcherThread(() =>
|
||||
{
|
||||
ViewModel.Terminal.OutputReceived -= Terminal_OutputReceived;
|
||||
ViewModel.Terminal.Closed -= Terminal_Closed;
|
||||
_webView?.Navigate(new Uri("about:blank"));
|
||||
_webView = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
_socket.Send(Encoding.UTF8.GetString(e));
|
||||
}
|
||||
|
||||
void IxtermEventListener.OnError(string error)
|
||||
{
|
||||
Logger.Instance.Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1759
-2095
File diff suppressed because it is too large.
Load diff
@@ -10,10 +10,10 @@
|
||||
"author": "FS Apps",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-cli": "^3.1.0"
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-cli": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"xterm": "^3.10.1"
|
||||
"xterm": "^3.12.0"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="xterm.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as Terminal from '../node_modules/xterm/dist/xterm';
|
||||
import * as attach from '../node_modules/xterm/dist/addons/attach/attach';
|
||||
import * as fit from '../node_modules/xterm/dist/addons/fit/fit';
|
||||
import * as winptyCompat from '../node_modules/xterm/dist/addons/winptyCompat/winptyCompat';
|
||||
import * as search from '../node_modules/xterm/dist/addons/search/search';
|
||||
|
||||
import { Terminal } from "xterm";
|
||||
import * as attach from "xterm/lib/addons/attach/attach";
|
||||
import * as fit from "xterm/lib/addons/fit/fit";
|
||||
import * as winptyCompat from "xterm/lib/addons/winptyCompat/winptyCompat";
|
||||
import * as search from "xterm/lib/addons/search/search";
|
||||
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
@@ -13,7 +12,7 @@ Terminal.applyAddon(search);
|
||||
var term, socket;
|
||||
var terminalContainer = document.getElementById('terminal-container');
|
||||
|
||||
function createTerminal(options, theme, keyBindings) {
|
||||
function createTerminal(options, theme, keyBindings, sessionType) {
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
@@ -29,6 +28,8 @@ function createTerminal(options, theme, keyBindings) {
|
||||
var terminalOptions = {
|
||||
fontFamily: options.fontFamily,
|
||||
fontSize: options.fontSize,
|
||||
fontWeight: options.boldText ? 'bold' : 'normal',
|
||||
fontWeightBold: options.boldText ? 'bolder' : 'bold',
|
||||
cursorStyle: options.cursorStyle,
|
||||
cursorBlink: options.cursorBlink,
|
||||
bellStyle: options.bellStyle,
|
||||
@@ -56,7 +57,11 @@ function createTerminal(options, theme, keyBindings) {
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.fit();
|
||||
term.winptyCompatInit();
|
||||
|
||||
if (sessionType === 'WinPty') {
|
||||
term.winptyCompatInit();
|
||||
}
|
||||
|
||||
term.focus();
|
||||
|
||||
setPadding(options.padding);
|
||||
@@ -120,10 +125,13 @@ function attachTerminal() {
|
||||
|
||||
function connectToWebSocket(url) {
|
||||
socket = new WebSocket(url);
|
||||
socket.onopen = attachTerminal;
|
||||
socket.onclose = function() {
|
||||
terminalBridge.invokeCommand('CloseTab');
|
||||
socket.onerror = function(event) {
|
||||
terminalBridge.reportError(`Socket error: ${JSON.stringify(event)}`);
|
||||
}
|
||||
socket.onclose = function(event) {
|
||||
terminalBridge.reportError(`Socket closed: ${JSON.stringify(event)}`);
|
||||
};
|
||||
socket.onopen = attachTerminal;
|
||||
}
|
||||
|
||||
function changeTheme(theme) {
|
||||
@@ -139,20 +147,19 @@ function changeOptions(options) {
|
||||
term.setOption('cursorStyle', options.cursorStyle);
|
||||
term.setOption('fontFamily', options.fontFamily);
|
||||
term.setOption('fontSize', options.fontSize);
|
||||
term.setOption('fontWeight', options.boldText ? 'bold' : 'normal');
|
||||
term.setOption('fontWeightBold', options.boldText ? 'bolder' : 'bold');
|
||||
term.setOption('scrollback', options.scrollBackLimit);
|
||||
setScrollBarStyle(options.scrollBarStyle);
|
||||
setPadding(options.padding);
|
||||
}
|
||||
|
||||
function setScrollBarStyle(scrollBarStyle) {
|
||||
if (scrollBarStyle == 'hidden') {
|
||||
document.getElementById('terminal-container').style['-ms-overflow-style'] = 'none';
|
||||
} else if (scrollBarStyle == 'autoHiding') {
|
||||
document.getElementById('terminal-container').style['-ms-overflow-style'] = '-ms-autohiding-scrollbar';
|
||||
} else if (scrollBarStyle == 'visible') {
|
||||
document.getElementById('terminal-container').style['-ms-overflow-style'] = 'scrollbar';
|
||||
switch (scrollBarStyle) {
|
||||
case 'hidden': return terminalContainer.style['-ms-overflow-style'] = 'none';
|
||||
case 'autoHiding': return terminalContainer.style['-ms-overflow-style'] = '-ms-autohiding-scrollbar';
|
||||
case 'visible': return terminalContainer.style['-ms-overflow-style'] = 'scrollbar';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function setPadding(padding) {
|
||||
|
||||
@@ -5,11 +5,19 @@
|
||||
html, body {
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
}
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
-ms-content-zooming: none;
|
||||
}
|
||||
|
||||
#terminal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* re-enable scrolling only in the xterm container */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
touch-action: pan-y;
|
||||
-ms-touch-action: pan-y;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
|
||||
@@ -16,5 +16,7 @@ namespace FluentTerminal.Models
|
||||
public bool AlwaysShowTabs { get; set; }
|
||||
public bool ShowNewOutputIndicator { get; set; }
|
||||
public bool EnableTrayIcon { get; set; }
|
||||
public bool AlwaysUseWinPty { get; set; }
|
||||
public bool ShowCustomTitleInTitlebar { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,21 @@ namespace FluentTerminal.Models.Enums
|
||||
[Description("Configurable new tab")]
|
||||
ConfigurableNewTab,
|
||||
|
||||
[Description("New remote tab")]
|
||||
NewRemoteTab,
|
||||
|
||||
[Description("Change tab title")]
|
||||
ChangeTabTitle,
|
||||
|
||||
[Description("Close tab")]
|
||||
CloseTab,
|
||||
|
||||
[Description("New window")]
|
||||
NewWindow,
|
||||
|
||||
[Description("Configurable new window")]
|
||||
ConfigurableNewWindow,
|
||||
|
||||
[Description("Show settings")]
|
||||
ShowSettings,
|
||||
|
||||
@@ -34,6 +43,9 @@ namespace FluentTerminal.Models.Enums
|
||||
[Description("Paste")]
|
||||
Paste,
|
||||
|
||||
[Description("Paste without newlines")]
|
||||
PasteWithoutNewlines,
|
||||
|
||||
[Description("Search")]
|
||||
Search,
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace FluentTerminal.Models.Enums
|
||||
{
|
||||
public enum LineEndingStyle
|
||||
{
|
||||
[Description("Do not modify")]
|
||||
DoNotModify,
|
||||
|
||||
[Description("Convert to CR")]
|
||||
ToCR,
|
||||
|
||||
[Description("Convert to CRLF")]
|
||||
ToCRLF,
|
||||
|
||||
[Description("Convert to LF")]
|
||||
ToLF
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace FluentTerminal.Models.Enums
|
||||
{
|
||||
public enum SessionType
|
||||
{
|
||||
Unknown,
|
||||
WinPty,
|
||||
ConPty
|
||||
}
|
||||
}
|
||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user