Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||
@@ -92,5 +92,18 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
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);
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -43,10 +43,11 @@ namespace FluentTerminal.App.Services.Test
|
||||
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, sessionType);
|
||||
var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
|
||||
|
||||
result.Should().BeEquivalentTo(response);
|
||||
}
|
||||
@@ -72,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>>
|
||||
@@ -86,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]
|
||||
@@ -150,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,
|
||||
@@ -178,7 +179,7 @@ namespace FluentTerminal.App.Services.Test
|
||||
|
||||
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
|
||||
|
||||
receivedOutput.Should().Be(output);
|
||||
receivedOutput.Should().BeEquivalentTo(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentTerminal.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ISystemFontService
|
||||
{
|
||||
IEnumerable<string> GetSystemFontFamilies();
|
||||
IEnumerable<FontInfo> GetSystemFontFamilies();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services
|
||||
{
|
||||
public interface ITerminalView
|
||||
{
|
||||
event EventHandler<TerminalSize> TerminalSizeChanged;
|
||||
|
||||
event EventHandler<string> TerminalTitleChanged;
|
||||
|
||||
event EventHandler<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, SessionType sessionType);
|
||||
|
||||
Task ConnectToSocket(string url);
|
||||
|
||||
void Close();
|
||||
|
||||
Task FocusTerminal();
|
||||
|
||||
Task<string> GetSelection();
|
||||
|
||||
Task FindNext(string searchText);
|
||||
|
||||
Task FindPrevious(string searchText);
|
||||
|
||||
void FocusSearchTextBox();
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,18 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
void Initialize(IAppServiceConnection appServiceConnection);
|
||||
|
||||
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
AlwaysShowTabs = false,
|
||||
AlwaysUseWinPty = true,
|
||||
ShowNewOutputIndicator = false,
|
||||
EnableTrayIcon = true
|
||||
EnableTrayIcon = true,
|
||||
ShowCustomTitleInTitlebar = true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@ using FluentTerminal.Models.Responses;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation.Metadata;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
@@ -15,30 +13,52 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private IAppServiceConnection _appServiceConnection;
|
||||
private readonly 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, SessionType sessionType)
|
||||
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,
|
||||
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)
|
||||
@@ -62,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);
|
||||
}
|
||||
}
|
||||
@@ -84,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;
|
||||
}
|
||||
@@ -101,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));
|
||||
@@ -119,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>
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IDispatcherTimer _dispatcherTimer;
|
||||
@@ -26,6 +25,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
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)
|
||||
@@ -39,12 +39,12 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
_trayProcessCommunicationService = trayProcessCommunicationService;
|
||||
_dialogService = dialogService;
|
||||
_applicationView = applicationView;
|
||||
ApplicationView = applicationView;
|
||||
_dispatcherTimer = dispatcherTimer;
|
||||
_clipboardService = clipboardService;
|
||||
_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.ConfigurableNewTab), () => AddConfigurableTerminal());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), () => SelectedTerminal.EditTitle());
|
||||
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
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();
|
||||
@@ -78,14 +78,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());
|
||||
@@ -93,7 +99,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)
|
||||
@@ -112,6 +118,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);
|
||||
@@ -149,6 +167,14 @@ namespace FluentTerminal.App.ViewModels
|
||||
if (SelectedTerminal != null)
|
||||
{
|
||||
SelectedTerminal.IsSelected = true;
|
||||
if (_applicationSettings.ShowCustomTitleInTitlebar)
|
||||
{
|
||||
WindowTitle = SelectedTerminal.TabTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowTitle = SelectedTerminal.ShellTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,54 +192,89 @@ 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)
|
||||
{
|
||||
if (Terminals.Count == 0)
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
_applicationView.TryClose();
|
||||
}
|
||||
|
||||
return;
|
||||
await ApplicationView.TryClose();
|
||||
}
|
||||
}
|
||||
else if (profileId == Guid.Empty)
|
||||
{
|
||||
profile = _settingsService.GetDefaultShellProfile();
|
||||
}
|
||||
else
|
||||
{
|
||||
profile = _settingsService.GetShellProfile(profileId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,12 +295,21 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,8 +321,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
if (result == DialogButton.OK)
|
||||
{
|
||||
CloseAllTerminals();
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
await CloseAllTerminals();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -261,42 +330,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;
|
||||
});
|
||||
@@ -336,7 +403,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private void ToggleFullScreen()
|
||||
{
|
||||
_applicationView.ToggleFullScreen();
|
||||
ApplicationView.ToggleFullScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,20 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowCustomTitleInTitlebar
|
||||
{
|
||||
get => _applicationSettings.ShowCustomTitleInTitlebar;
|
||||
set
|
||||
{
|
||||
if (_applicationSettings.ShowCustomTitleInTitlebar != value)
|
||||
{
|
||||
_applicationSettings.ShowCustomTitleInTitlebar = value;
|
||||
_settingsService.SaveApplicationSettings(_applicationSettings);
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableTrayIcon
|
||||
{
|
||||
get => _applicationSettings.EnableTrayIcon;
|
||||
@@ -281,6 +295,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
|
||||
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
|
||||
EnableTrayIcon = defaults.EnableTrayIcon;
|
||||
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,12 +126,12 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.ScrollBackLimit = intValue;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,12 +145,12 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.BoldText = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> Fonts { get; }
|
||||
public IEnumerable<FontInfo> Fonts { get; }
|
||||
|
||||
public int FontSize
|
||||
{
|
||||
@@ -161,7 +161,7 @@ namespace FluentTerminal.App.ViewModels.Settings
|
||||
{
|
||||
_terminalOptions.FontSize = value;
|
||||
_settingsService.SaveTerminalOptions(_terminalOptions);
|
||||
this.RaisePropertyChanged();
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,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;
|
||||
}
|
||||
}
|
||||
@@ -199,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;
|
||||
}
|
||||
}
|
||||
@@ -234,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
private string _workingDirectory;
|
||||
private readonly IApplicationView _applicationView;
|
||||
private readonly IDefaultValueProvider _defaultValueProvider;
|
||||
private bool _editingLineEndingStyle;
|
||||
|
||||
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider, Boolean isNew)
|
||||
{
|
||||
@@ -200,37 +199,31 @@ namespace FluentTerminal.App.ViewModels
|
||||
public LineEndingStyle LineEndingStyle
|
||||
{
|
||||
get => _lineEndingStyle;
|
||||
set
|
||||
{
|
||||
if (_lineEndingStyle != value && !_editingLineEndingStyle)
|
||||
{
|
||||
_editingLineEndingStyle = true;
|
||||
_lineEndingStyle = value;
|
||||
RaisePropertyChanged();
|
||||
RaisePropertyChanged(nameof(DoNotModifyIsSelected));
|
||||
RaisePropertyChanged(nameof(ToCRLFIsSelected));
|
||||
RaisePropertyChanged(nameof(ToLFIsSelected));
|
||||
_editingLineEndingStyle = false;
|
||||
}
|
||||
}
|
||||
set => Set(ref _lineEndingStyle, value);
|
||||
}
|
||||
|
||||
public bool DoNotModifyIsSelected
|
||||
{
|
||||
get => LineEndingStyle == LineEndingStyle.DoNotModify;
|
||||
set => LineEndingStyle = LineEndingStyle.DoNotModify;
|
||||
set { if (value) LineEndingStyle = LineEndingStyle.DoNotModify; }
|
||||
}
|
||||
|
||||
public bool ToCRLFIsSelected
|
||||
{
|
||||
get => LineEndingStyle == LineEndingStyle.ToCRLF;
|
||||
set => LineEndingStyle = LineEndingStyle.ToCRLF;
|
||||
set { if (value) LineEndingStyle = LineEndingStyle.ToCRLF; }
|
||||
}
|
||||
|
||||
public bool ToLFIsSelected
|
||||
{
|
||||
get => LineEndingStyle == LineEndingStyle.ToLF;
|
||||
set => 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()
|
||||
@@ -363,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,
|
||||
|
||||
@@ -1,106 +1,107 @@
|
||||
using Fleck;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.ViewModels.Infrastructure;
|
||||
using FluentTerminal.App.ViewModels.Utilities;
|
||||
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 InputBuffer _buffer;
|
||||
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 int _terminalId;
|
||||
private TerminalTheme _terminalTheme;
|
||||
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 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; }
|
||||
|
||||
@@ -108,8 +109,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public RelayCommand FindPreviousCommand { get; }
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
@@ -119,8 +118,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
_applicationView.Title = Title;
|
||||
_applicationView.RunOnDispatcherThread(() => NewOutput = false);
|
||||
HasNewOutput = false;
|
||||
}
|
||||
RaisePropertyChanged(nameof(IsUnderlined));
|
||||
RaisePropertyChanged(nameof(BackgroundTabTheme));
|
||||
@@ -131,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
|
||||
@@ -151,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;
|
||||
@@ -187,145 +189,97 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public ObservableCollection<TabTheme> TabThemes { get; }
|
||||
|
||||
public Terminal Terminal { get; private set; }
|
||||
|
||||
public TerminalTheme TerminalTheme
|
||||
{
|
||||
get => _terminalTheme;
|
||||
set => Set(ref _terminalTheme, value);
|
||||
}
|
||||
|
||||
public string Title
|
||||
public string TabTitle
|
||||
{
|
||||
get => _title;
|
||||
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);
|
||||
}
|
||||
|
||||
public Task FocusTerminal()
|
||||
{
|
||||
return _terminalView?.FocusTerminal();
|
||||
ClipboardService.SetText(text);
|
||||
}
|
||||
|
||||
public async Task EditTitle()
|
||||
{
|
||||
var result = await _dialogService.ShowInputDialogAsync("Edit Title");
|
||||
var result = await DialogService.ShowInputDialogAsync("Edit Title");
|
||||
if (result != null)
|
||||
{
|
||||
Title = result;
|
||||
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 settings = _settingsService.GetApplicationSettings();
|
||||
var sessionType = SessionType.Unknown;
|
||||
if (!_applicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7) || settings.AlwaysUseWinPty)
|
||||
{
|
||||
sessionType = SessionType.WinPty;
|
||||
}
|
||||
else
|
||||
{
|
||||
sessionType = SessionType.ConPty;
|
||||
}
|
||||
|
||||
var size = await _terminalView.CreateTerminal(options, TerminalTheme.Colors, FlattenKeyBindings(keyBindings, profiles), sessionType).ConfigureAwait(true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_startupDirectory))
|
||||
{
|
||||
_shellProfile.WorkingDirectory = _startupDirectory;
|
||||
}
|
||||
|
||||
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile, sessionType).ConfigureAwait(true);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_terminalId = response.Id;
|
||||
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
|
||||
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
|
||||
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
|
||||
|
||||
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
|
||||
{
|
||||
_connectedEvent.Wait();
|
||||
_buffer.Write(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;
|
||||
_buffer = new InputBuffer(_webSocket.Send);
|
||||
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));
|
||||
@@ -335,73 +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)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case nameof(Command.Copy):
|
||||
{
|
||||
var selection = await _terminalView.GetSelection().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 _trayProcessCommunicationService.WriteText(_terminalId, 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 _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case nameof(Command.Search):
|
||||
{
|
||||
ShowSearchPanel = !ShowSearchPanel;
|
||||
if (ShowSearchPanel)
|
||||
{
|
||||
_terminalView.FocusSearchTextBox();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
_keyboardCommandService.SendCommand(command);
|
||||
break;
|
||||
}
|
||||
}
|
||||
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
private void OnResizeOverlayTimerFinished(object sender, object e)
|
||||
@@ -410,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)
|
||||
@@ -447,11 +322,83 @@ namespace FluentTerminal.App.ViewModels
|
||||
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
|
||||
}
|
||||
|
||||
private void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
private async void Terminal_KeyboardCommandReceived(object sender, string e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -459,8 +406,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
await Close().ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels.Utilities
|
||||
{
|
||||
public class InputBuffer
|
||||
{
|
||||
private readonly Timer _timer = new Timer();
|
||||
private readonly Func<string, Task> _writeCallback;
|
||||
private readonly List<string> _buffer = new List<string>();
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public InputBuffer(Func<string, Task> writeCallback)
|
||||
{
|
||||
_timer.AutoReset = true;
|
||||
_timer.Elapsed += _timer_Elapsed;
|
||||
_writeCallback = writeCallback;
|
||||
_timer.Interval = 5;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var data = string.Concat(_buffer);
|
||||
_buffer.Clear();
|
||||
_writeCallback.Invoke(data);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string data)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_buffer.Add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,20 +14,32 @@ 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)
|
||||
@@ -40,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()
|
||||
@@ -63,6 +82,7 @@ namespace FluentTerminal.App.Adapters
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
var args = new CancelableEventArgs();
|
||||
|
||||
if (CloseRequested != null)
|
||||
{
|
||||
await CloseRequested.Invoke(this, args);
|
||||
@@ -70,6 +90,11 @@ namespace FluentTerminal.App.Adapters
|
||||
|
||||
e.Handled = args.Cancelled;
|
||||
|
||||
if (!e.Handled)
|
||||
{
|
||||
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
|
||||
}
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
|
||||
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
|
||||
<converters:EnumToDescriptionConverter x:Key="EnumToDescriptionConverter" />
|
||||
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
+180
-48
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
@@ -10,9 +12,13 @@ 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;
|
||||
@@ -42,6 +48,7 @@ namespace FluentTerminal.App
|
||||
private int? _settingsWindowId;
|
||||
private IAppServiceConnection _appServiceConnection;
|
||||
private BackgroundTaskDeferral _appServiceDeferral;
|
||||
private Parser _commandLineParser;
|
||||
|
||||
public App()
|
||||
{
|
||||
@@ -49,6 +56,8 @@ namespace FluentTerminal.App
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
UnhandledException += OnUnhandledException;
|
||||
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
|
||||
@@ -91,9 +100,55 @@ namespace FluentTerminal.App
|
||||
_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;
|
||||
});
|
||||
}
|
||||
|
||||
protected override async void OnActivated(IActivatedEventArgs args)
|
||||
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)
|
||||
{
|
||||
@@ -102,42 +157,67 @@ namespace FluentTerminal.App
|
||||
return;
|
||||
}
|
||||
|
||||
var arguments = commandLineActivated.Operation.Arguments.Split(' ', 2);
|
||||
var command = arguments[0];
|
||||
var parameter = arguments.Length > 1 ? arguments[1] : string.Empty;
|
||||
|
||||
if (_alreadyLaunched)
|
||||
_commandLineParser.ParseArguments(SplitArguments(commandLineActivated.Operation.Arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
|
||||
{
|
||||
if (command == "settings")
|
||||
if (verb is SettingsVerb)
|
||||
{
|
||||
await ShowSettings().ConfigureAwait(true);
|
||||
}
|
||||
else if (command == "new")
|
||||
else if (verb is NewVerb newVerb)
|
||||
{
|
||||
if (_applicationSettings.NewTerminalLocation == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
|
||||
var profile = default(ShellProfile);
|
||||
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
|
||||
{
|
||||
await _mainViewModels.Last().AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
|
||||
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
|
||||
}
|
||||
else
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
await CreateNewTerminalWindow(parameter, false).ConfigureAwait(true);
|
||||
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 (command == "settings")
|
||||
else if (verb is RunVerb runVerb)
|
||||
{
|
||||
var viewModel = _container.Resolve<SettingsViewModel>();
|
||||
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,14 +225,29 @@ namespace FluentTerminal.App
|
||||
{
|
||||
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>();
|
||||
await viewModel.AddTerminal(null, false, Guid.Empty).ConfigureAwait(true);
|
||||
viewModel.AddTerminal();
|
||||
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
|
||||
Window.Current.Activate();
|
||||
}
|
||||
else if (_mainViewModels.Count == 0)
|
||||
{
|
||||
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, false, string.Empty).ConfigureAwait(true);
|
||||
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,13 +308,19 @@ namespace FluentTerminal.App
|
||||
Window.Current.Activate();
|
||||
}
|
||||
|
||||
private async Task CreateNewTerminalWindow(string startupDirectory, bool showProfileSelection)
|
||||
private async Task<MainViewModel> CreateNewTerminalWindow()
|
||||
{
|
||||
var id = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, showProfileSelection, startupDirectory).ConfigureAwait(true);
|
||||
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(id);
|
||||
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<int> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar, bool showProfileSelection, object parameter)
|
||||
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar)
|
||||
{
|
||||
int windowId = 0;
|
||||
TViewModel viewModel = default;
|
||||
@@ -236,23 +337,16 @@ namespace FluentTerminal.App
|
||||
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, showProfileSelection, Guid.Empty).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
if (viewModel is SettingsViewModel settingsViewModel)
|
||||
{
|
||||
_settingsViewModel = settingsViewModel;
|
||||
_settingsViewModel.Closed += OnSettingsClosed;
|
||||
_settingsWindowId = windowId;
|
||||
}
|
||||
|
||||
return windowId;
|
||||
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(windowId);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
|
||||
@@ -264,9 +358,12 @@ namespace FluentTerminal.App
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -274,7 +371,16 @@ namespace FluentTerminal.App
|
||||
|
||||
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
|
||||
{
|
||||
await CreateNewTerminalWindow(string.Empty, e.ShowProfileSelection).ConfigureAwait(true);
|
||||
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
|
||||
|
||||
if (e.ShowProfileSelection)
|
||||
{
|
||||
await viewModel.AddConfigurableTerminal().ConfigureAwait(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
viewModel.AddTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSettingsClosed(object sender, EventArgs e)
|
||||
@@ -300,20 +406,46 @@ namespace FluentTerminal.App
|
||||
_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 (_settingsViewModel == null)
|
||||
if (!_alreadyLaunched)
|
||||
{
|
||||
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, false, null).ConfigureAwait(true);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
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,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,10 @@
|
||||
<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" />
|
||||
@@ -124,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">
|
||||
@@ -138,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>
|
||||
@@ -183,9 +189,13 @@
|
||||
</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\XtermTerminalView.xaml.cs">
|
||||
<DependentUpon>XtermTerminalView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AppxManifest Include="Package.appxmanifest">
|
||||
@@ -202,7 +212,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" />
|
||||
@@ -350,25 +364,32 @@
|
||||
<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="Win2D.uwp">
|
||||
<Version>1.21.0</Version>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,14 +76,108 @@
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
<TextBlock
|
||||
<RelativePanel
|
||||
Grid.Column="1"
|
||||
IsHitTestVisible="False"
|
||||
Style="{StaticResource WindowTitleTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedTerminal.Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,11 @@
|
||||
Header="Always show tabs"
|
||||
IsOn="{x:Bind ViewModel.AlwaysShowTabs, Mode=TwoWay}" />
|
||||
|
||||
<ToggleSwitch
|
||||
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"
|
||||
|
||||
@@ -292,6 +292,10 @@
|
||||
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"
|
||||
|
||||
@@ -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" />
|
||||
@@ -45,7 +46,14 @@
|
||||
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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,67 @@
|
||||
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 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, 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);
|
||||
}
|
||||
|
||||
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 +73,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,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.11.0"
|
||||
"xterm": "^3.12.0"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -126,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) {
|
||||
|
||||
@@ -17,5 +17,6 @@ namespace FluentTerminal.Models
|
||||
public bool ShowNewOutputIndicator { get; set; }
|
||||
public bool EnableTrayIcon { get; set; }
|
||||
public bool AlwaysUseWinPty { get; set; }
|
||||
public bool ShowCustomTitleInTitlebar { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ namespace FluentTerminal.Models.Enums
|
||||
[Description("Do not modify")]
|
||||
DoNotModify,
|
||||
|
||||
[Description("Convert to CR")]
|
||||
ToCR,
|
||||
|
||||
[Description("Convert to CRLF")]
|
||||
ToCRLF,
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace FluentTerminal.Models
|
||||
{
|
||||
public class FontInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool IsMonospaced { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace FluentTerminal.Models.Requests
|
||||
{
|
||||
public class CreateTerminalRequest
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public TerminalSize Size { get; set; }
|
||||
public ShellProfile Profile { get; set; }
|
||||
public SessionType SessionType { get; set; }
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
public class DisplayTerminalOutputRequest
|
||||
{
|
||||
public int TerminalId { get; set; }
|
||||
public string Output { get; set; }
|
||||
public byte[] Output { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace FluentTerminal.Models.Requests
|
||||
{
|
||||
public class GetAvailablePortRequest
|
||||
{
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
namespace FluentTerminal.Models.Requests
|
||||
{
|
||||
public class WriteTextRequest
|
||||
public class WriteDataRequest
|
||||
{
|
||||
public int TerminalId { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
{
|
||||
public class CreateTerminalResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string Error { get; set; }
|
||||
public string ShellExecutableName { get; set; }
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FluentTerminal.Models.Responses
|
||||
{
|
||||
public class GetAvailablePortResponse
|
||||
{
|
||||
public int Port { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ namespace FluentTerminal.Models
|
||||
{
|
||||
switch (LineEndingTranslation)
|
||||
{
|
||||
case LineEndingStyle.ToCR:
|
||||
return NewlinePattern.Replace(content, "\r");
|
||||
case LineEndingStyle.ToCRLF:
|
||||
return NewlinePattern.Replace(content, "\r\n");
|
||||
case LineEndingStyle.ToLF:
|
||||
|
||||
@@ -27,9 +27,9 @@ namespace FluentTerminal.Models
|
||||
{
|
||||
if (obj is TerminalTheme other)
|
||||
{
|
||||
return other.Name.Equals(Name)
|
||||
&& other.Author == null ? Author == null : other.Author.Equals(Author)
|
||||
&& other.Colors.Equals(Colors);
|
||||
return Equals(other.Name, Name)
|
||||
&& Equals(other.Author, Author)
|
||||
&& Equals(other.Colors, Colors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -108,13 +108,13 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Enums\MouseButton.cs" />
|
||||
<Compile Include="Interfaces\ITerminalEventListener.cs" />
|
||||
<Compile Include="Interfaces\IxtermEventListener.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="WebAllowedObjects\TerminalBridge.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
|
||||
<Version>6.1.5</Version>
|
||||
<Version>6.2.8</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||
|
||||
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace FluentTerminal.RuntimeComponent.Interfaces
|
||||
{
|
||||
public interface ITerminalEventListener
|
||||
public interface IxtermEventListener
|
||||
{
|
||||
void OnTerminalResized(int columns, int rows);
|
||||
|
||||
@@ -13,5 +13,7 @@ namespace FluentTerminal.RuntimeComponent.Interfaces
|
||||
void OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection);
|
||||
|
||||
void OnSelectionChanged(string selection);
|
||||
|
||||
void OnError(string error);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ namespace FluentTerminal.RuntimeComponent.WebAllowedObjects
|
||||
[AllowForWeb]
|
||||
public sealed class TerminalBridge
|
||||
{
|
||||
private readonly ITerminalEventListener _terminalEventListener;
|
||||
private readonly IxtermEventListener _terminalEventListener;
|
||||
|
||||
public TerminalBridge(ITerminalEventListener terminalEventListener)
|
||||
public TerminalBridge(IxtermEventListener terminalEventListener)
|
||||
{
|
||||
_terminalEventListener = terminalEventListener;
|
||||
}
|
||||
@@ -43,5 +43,10 @@ namespace FluentTerminal.RuntimeComponent.WebAllowedObjects
|
||||
{
|
||||
_terminalEventListener.OnSelectionChanged(selection);
|
||||
}
|
||||
|
||||
public void ReportError(string error)
|
||||
{
|
||||
_terminalEventListener.OnError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<TargetFrameworkProfile />
|
||||
<LangVersion>7.1</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -112,6 +113,7 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Native\ProcessApi.cs" />
|
||||
<Compile Include="Native\WindowApi.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
@@ -160,7 +162,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac">
|
||||
<Version>4.8.1</Version>
|
||||
<Version>4.9.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="GlobalHotKey">
|
||||
<Version>1.1.0</Version>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FluentTerminal.SystemTray.Native
|
||||
{
|
||||
public static class ProcessApi
|
||||
{
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern int GetProcessId(IntPtr handle);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,11 @@ using FluentTerminal.App.Services.Adapters;
|
||||
using FluentTerminal.App.Services.Implementation;
|
||||
using FluentTerminal.SystemTray.Services;
|
||||
using GlobalHotKey;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
@@ -27,6 +31,17 @@ namespace FluentTerminal.SystemTray
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
JsonConvert.DefaultSettings = () =>
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
};
|
||||
settings.Converters.Add(new StringEnumConverter(typeof(CamelCaseNamingStrategy)));
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
var applicationDataContainers = new ApplicationDataContainers
|
||||
{
|
||||
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
|
||||
@@ -42,7 +57,7 @@ namespace FluentTerminal.SystemTray
|
||||
containerBuilder.RegisterType<NotificationService>().As<INotificationService>().InstancePerDependency();
|
||||
containerBuilder.RegisterType<TerminalsManager>().SingleInstance();
|
||||
containerBuilder.RegisterType<ToggleWindowService>().SingleInstance();
|
||||
containerBuilder.RegisterType<HotKeyManager>().SingleInstance();
|
||||
containerBuilder.RegisterInstance(new HotKeyManager()).SingleInstance();
|
||||
containerBuilder.RegisterType<SystemTrayApplicationContext>().SingleInstance();
|
||||
containerBuilder.RegisterType<AppCommunicationService>().SingleInstance();
|
||||
containerBuilder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>();
|
||||
@@ -52,6 +67,19 @@ namespace FluentTerminal.SystemTray
|
||||
|
||||
var container = containerBuilder.Build();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
|
||||
var logDirectory = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("Logs", CreationCollisionOption.OpenIfExists).AsTask().ConfigureAwait(true);
|
||||
var logFile = Path.Combine(logDirectory.Path, "fluentterminal.systemtray.log");
|
||||
var configFile = await logDirectory.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists).AsTask().ConfigureAwait(true);
|
||||
var configContent = await FileIO.ReadTextAsync(configFile).AsTask().ConfigureAwait(true);
|
||||
var config = JsonConvert.DeserializeObject<Logger.Configuration>(configContent) ?? new Logger.Configuration();
|
||||
Logger.Instance.Initialize(logFile, config);
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
});
|
||||
|
||||
var appCommunicationService = container.Resolve<AppCommunicationService>();
|
||||
|
||||
if (args.Length > 0 && args[2] == "appLaunched")
|
||||
@@ -79,5 +107,10 @@ namespace FluentTerminal.SystemTray
|
||||
eventWaitHandle.Set();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.Instance.Error((Exception)e.ExceptionObject, "Unhandled Exception");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ using Windows.ApplicationModel;
|
||||
using Windows.ApplicationModel.AppService;
|
||||
using Windows.Foundation.Collections;
|
||||
using System;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using FluentTerminal.App.Services;
|
||||
|
||||
namespace FluentTerminal.SystemTray.Services
|
||||
{
|
||||
@@ -44,7 +46,7 @@ namespace FluentTerminal.SystemTray.Services
|
||||
TerminalId = e
|
||||
};
|
||||
|
||||
_appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
_appServiceConnection?.SendMessageAsync(CreateMessage(request));
|
||||
}
|
||||
|
||||
private void _terminalsManager_DisplayOutputRequested(object sender, DisplayTerminalOutputRequest e)
|
||||
@@ -83,8 +85,13 @@ namespace FluentTerminal.SystemTray.Services
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
var request = JsonConvert.DeserializeObject<CreateTerminalRequest>(messageContent);
|
||||
|
||||
Logger.Instance.Debug("Received CreateTerminalRequest: {@request}", request);
|
||||
|
||||
var response = _terminalsManager.CreateTerminal(request);
|
||||
|
||||
Logger.Instance.Debug("Sending CreateTerminalResponse: {@response}", response);
|
||||
|
||||
await args.Request.SendResponseAsync(CreateMessage(response));
|
||||
|
||||
deferral.Complete();
|
||||
@@ -101,16 +108,26 @@ namespace FluentTerminal.SystemTray.Services
|
||||
|
||||
_toggleWindowService.SetHotKeys(request.KeyBindings);
|
||||
}
|
||||
else if (messageType == nameof(WriteTextRequest))
|
||||
else if (messageType == nameof(WriteDataRequest))
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<WriteTextRequest>(messageContent);
|
||||
_terminalsManager.WriteText(request.TerminalId, request.Text);
|
||||
var request = JsonConvert.DeserializeObject<WriteDataRequest>(messageContent);
|
||||
_terminalsManager.Write(request.TerminalId, request.Data);
|
||||
}
|
||||
else if (messageType == nameof(TerminalExitedRequest))
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
|
||||
_terminalsManager.CloseTerminal(request.TerminalId);
|
||||
}
|
||||
}
|
||||
else if (messageType == nameof(GetAvailablePortRequest))
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
var response = new GetAvailablePortResponse { Port = Utilities.GetAvailablePort().Value };
|
||||
|
||||
await args.Request.SendResponseAsync(CreateMessage(response));
|
||||
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private ValueSet CreateMessage(object content)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Requests;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Requests;
|
||||
|
||||
namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
{
|
||||
@@ -10,8 +10,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
{
|
||||
private TerminalsManager _terminalsManager;
|
||||
private Terminal _terminal;
|
||||
private bool _exited;
|
||||
|
||||
public int Id { get; private set; }
|
||||
|
||||
public string ShellExecutableName { get; private set; }
|
||||
|
||||
public event EventHandler ConnectionClosed;
|
||||
@@ -28,28 +30,31 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
|
||||
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
|
||||
{
|
||||
Id = request.Id;
|
||||
_terminalsManager = terminalsManager;
|
||||
|
||||
var port = Utilities.GetAvailablePort();
|
||||
|
||||
if (port == null)
|
||||
{
|
||||
throw new Exception("no port available");
|
||||
}
|
||||
|
||||
Id = port.Value;
|
||||
ShellExecutableName = Path.GetFileNameWithoutExtension(request.Profile.Location);
|
||||
var cwd = GetWorkingDirectory(request.Profile);
|
||||
string args = $"\"{request.Profile.Location}\" {request.Profile.Arguments}";
|
||||
|
||||
var args = string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(request.Profile.Location))
|
||||
{
|
||||
args = $"\"{request.Profile.Location}\" {request.Profile.Arguments}";
|
||||
}
|
||||
else
|
||||
{
|
||||
args = request.Profile.Arguments;
|
||||
}
|
||||
|
||||
_terminal = new Terminal();
|
||||
_terminal.OutputReady += _terminal_OutputReady;
|
||||
_terminal.Exited += _terminal_Exited;
|
||||
Task.Run(() => _terminal.Start(args, cwd, request.Size.Columns, request.Size.Rows));
|
||||
Task.Run(() => _terminal.Start(args, cwd, terminalsManager.GetDefaultEnvironmentVariableString(), request.Size.Columns, request.Size.Rows));
|
||||
}
|
||||
|
||||
private void _terminal_Exited(object sender, EventArgs e)
|
||||
{
|
||||
_exited = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -69,34 +74,34 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
|
||||
private void ListenToStdOut()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
var reader = new StreamReader(_terminal.ConsoleOutStream);
|
||||
|
||||
do
|
||||
using (var reader = new StreamReader(_terminal.ConsoleOutStream))
|
||||
{
|
||||
var offset = 0;
|
||||
var buffer = new char[1024];
|
||||
var readChars = await reader.ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
|
||||
|
||||
if (readChars > 0)
|
||||
do
|
||||
{
|
||||
var output = new String(buffer, 0, readChars);
|
||||
_terminalsManager.DisplayTerminalOutput(Id, output);
|
||||
var buffer = new byte[1024];
|
||||
var readBytes = await _terminal.ConsoleOutStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
var read = new byte[readBytes];
|
||||
Buffer.BlockCopy(buffer, 0, read, 0, readBytes);
|
||||
|
||||
if (readBytes > 0)
|
||||
{
|
||||
_terminalsManager.DisplayTerminalOutput(Id, read);
|
||||
}
|
||||
}
|
||||
while (!_exited);
|
||||
}
|
||||
while (!reader.EndOfStream);
|
||||
});
|
||||
|
||||
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
public void WriteText(string text)
|
||||
public void Write(byte[] data)
|
||||
{
|
||||
_terminal.WriteToPseudoConsole(text);
|
||||
_terminal.WriteToPseudoConsole(data);
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
|
||||
private bool disposedValue = false;
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
@@ -114,8 +119,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_terminal.Exited -= _terminal_Exited;
|
||||
Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endregion IDisposable Support
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using FluentTerminal.App.Services;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using static FluentTerminal.SystemTray.Services.ConPty.Native.ProcessApi;
|
||||
@@ -16,10 +17,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty.Processes
|
||||
/// <summary>
|
||||
/// Start and configure a process. The return value represents the process and should be disposed.
|
||||
/// </summary>
|
||||
internal static Process Start(string command, string directory, IntPtr attributes, IntPtr hPC)
|
||||
internal static Process Start(string command, string directory, string environment, IntPtr attributes, IntPtr hPC)
|
||||
{
|
||||
var startupInfo = ConfigureProcessThread(hPC, attributes);
|
||||
var processInfo = RunProcess(ref startupInfo, command, directory);
|
||||
var processInfo = RunProcess(ref startupInfo, command, directory, environment);
|
||||
return new Process(startupInfo, processInfo);
|
||||
}
|
||||
|
||||
@@ -71,8 +72,9 @@ namespace FluentTerminal.SystemTray.Services.ConPty.Processes
|
||||
return startupInfo;
|
||||
}
|
||||
|
||||
private static PROCESS_INFORMATION RunProcess(ref STARTUPINFOEX sInfoEx, string commandLine, string directory)
|
||||
private static PROCESS_INFORMATION RunProcess(ref STARTUPINFOEX sInfoEx, string commandLine, string directory, string environment)
|
||||
{
|
||||
var lpEnvironment = Marshal.StringToHGlobalAnsi(environment);
|
||||
int securityAttributeSize = Marshal.SizeOf<SECURITY_ATTRIBUTES>();
|
||||
var pSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
|
||||
var tSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
|
||||
@@ -83,16 +85,20 @@ namespace FluentTerminal.SystemTray.Services.ConPty.Processes
|
||||
lpThreadAttributes: ref tSec,
|
||||
bInheritHandles: false,
|
||||
dwCreationFlags: EXTENDED_STARTUPINFO_PRESENT,
|
||||
lpEnvironment: IntPtr.Zero,
|
||||
lpEnvironment: lpEnvironment,
|
||||
lpCurrentDirectory: directory,
|
||||
lpStartupInfo: ref sInfoEx,
|
||||
lpProcessInformation: out PROCESS_INFORMATION pInfo
|
||||
);
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not create process.");
|
||||
var errorCode = Marshal.GetLastWin32Error();
|
||||
Logger.Instance.Error("RunProcess: Could not create Process. Win32 Error: {@errorCode}", errorCode);
|
||||
Marshal.FreeHGlobal(lpEnvironment);
|
||||
throw new Win32Exception(errorCode, "Could not create process.");
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(lpEnvironment);
|
||||
return pInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
public sealed class Terminal : IDisposable
|
||||
{
|
||||
private SafeFileHandle _consoleInputPipeWriteHandle;
|
||||
private StreamWriter _consoleInputWriter;
|
||||
private FileStream _consoleInputWriter;
|
||||
private PseudoConsolePipe _inputPipe;
|
||||
private PseudoConsolePipe _outputPipe;
|
||||
private PseudoConsole _pseudoConsole;
|
||||
@@ -75,13 +75,13 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
/// <param name="command">the command to run, e.g. cmd.exe</param>
|
||||
/// <param name="consoleHeight">The height (in characters) to start the pseudoconsole with. Defaults to 80.</param>
|
||||
/// <param name="consoleWidth">The width (in characters) to start the pseudoconsole with. Defaults to 30.</param>
|
||||
public void Start(string command, string directory, int consoleWidth = 80, int consoleHeight = 30)
|
||||
public void Start(string command, string directory, string environment, int consoleWidth = 80, int consoleHeight = 30)
|
||||
{
|
||||
_inputPipe = new PseudoConsolePipe();
|
||||
_outputPipe = new PseudoConsolePipe();
|
||||
_pseudoConsole = PseudoConsole.Create(_inputPipe.ReadSide, _outputPipe.WriteSide, consoleWidth, consoleHeight);
|
||||
|
||||
using (var process = ProcessFactory.Start(command, directory, PseudoConsole.PseudoConsoleThreadAttribute, _pseudoConsole.Handle))
|
||||
using (var process = ProcessFactory.Start(command, directory, environment, PseudoConsole.PseudoConsoleThreadAttribute, _pseudoConsole.Handle))
|
||||
{
|
||||
// copy all pseudoconsole output to a FileStream and expose it to the rest of the app
|
||||
ConsoleOutStream = new FileStream(_outputPipe.ReadSide, FileAccess.Read);
|
||||
@@ -89,10 +89,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
|
||||
// Store input pipe handle, and a writer for later reuse
|
||||
_consoleInputPipeWriteHandle = _inputPipe.WriteSide;
|
||||
_consoleInputWriter = new StreamWriter(new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write))
|
||||
{
|
||||
AutoFlush = true
|
||||
};
|
||||
_consoleInputWriter = new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write);
|
||||
|
||||
// free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar)
|
||||
OnClose(() => DisposeResources(process, _pseudoConsole, _outputPipe, _inputPipe, _consoleInputWriter));
|
||||
@@ -110,14 +107,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
/// <summary>
|
||||
/// Sends the given string to the anonymous pipe that writes to the active pseudoconsole.
|
||||
/// </summary>
|
||||
/// <param name="input">A string of characters to write to the console. Supports VT-100 codes.</param>
|
||||
public void WriteToPseudoConsole(string input)
|
||||
public void WriteToPseudoConsole(byte[] data)
|
||||
{
|
||||
if (_consoleInputWriter == null)
|
||||
{
|
||||
throw new InvalidOperationException("There is no writer attached to a pseudoconsole. Have you called Start on this instance yet?");
|
||||
}
|
||||
_consoleInputWriter.Write(input);
|
||||
_consoleInputWriter.Write(data, 0, data.Length);
|
||||
_consoleInputWriter.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace FluentTerminal.SystemTray.Services
|
||||
|
||||
void Close();
|
||||
void Resize(TerminalSize size);
|
||||
void WriteText(string text);
|
||||
void Write(byte[] data);
|
||||
void Start(CreateTerminalRequest request, TerminalsManager terminalsManager);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,11 @@ using FluentTerminal.Models.Responses;
|
||||
using FluentTerminal.SystemTray.Services.ConPty;
|
||||
using FluentTerminal.SystemTray.Services.WinPty;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace FluentTerminal.SystemTray.Services
|
||||
{
|
||||
@@ -25,7 +28,7 @@ namespace FluentTerminal.SystemTray.Services
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public void DisplayTerminalOutput(int terminalId, string output)
|
||||
public void DisplayTerminalOutput(int terminalId, byte[] output)
|
||||
{
|
||||
DisplayOutputRequested?.Invoke(this, new DisplayTerminalOutputRequest
|
||||
{
|
||||
@@ -36,6 +39,16 @@ namespace FluentTerminal.SystemTray.Services
|
||||
|
||||
public CreateTerminalResponse CreateTerminal(CreateTerminalRequest request)
|
||||
{
|
||||
if (_terminals.ContainsKey(request.Id))
|
||||
{
|
||||
// App terminated without cleaning up, removing orphaned sessions
|
||||
foreach (var item in _terminals.Values)
|
||||
{
|
||||
item.Dispose();
|
||||
}
|
||||
_terminals.Clear();
|
||||
}
|
||||
|
||||
ITerminalSession terminal = null;
|
||||
try
|
||||
{
|
||||
@@ -59,16 +72,15 @@ namespace FluentTerminal.SystemTray.Services
|
||||
return new CreateTerminalResponse
|
||||
{
|
||||
Success = true,
|
||||
Id = terminal.Id,
|
||||
ShellExecutableName = terminal.ShellExecutableName
|
||||
};
|
||||
}
|
||||
|
||||
public void WriteText(int id, string text)
|
||||
public void Write(int id, byte[] data)
|
||||
{
|
||||
if (_terminals.TryGetValue(id, out ITerminalSession terminal))
|
||||
{
|
||||
terminal.WriteText(text);
|
||||
terminal.Write(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +101,28 @@ namespace FluentTerminal.SystemTray.Services
|
||||
if (_terminals.TryGetValue(id, out ITerminalSession terminal))
|
||||
{
|
||||
_terminals.Remove(terminal.Id);
|
||||
terminal.Dispose();
|
||||
terminal.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDefaultEnvironmentVariableString()
|
||||
{
|
||||
var environmentVariables = Environment.GetEnvironmentVariables();
|
||||
environmentVariables["TERM"] = "xterm-256color";
|
||||
environmentVariables["TERM_PROGRAM"] = "FluentTerminal";
|
||||
environmentVariables["TERM_PROGRAM_VERSION"] = $"{Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}.{Package.Current.Id.Version.Revision}";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
foreach (DictionaryEntry item in environmentVariables)
|
||||
{
|
||||
builder.Append(item.Key).Append("=").Append(item.Value).Append("\0");
|
||||
}
|
||||
builder.Append('\0');
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void OnTerminalConnectionClosed(object sender, System.EventArgs e)
|
||||
{
|
||||
if (sender is ITerminalSession terminal)
|
||||
@@ -103,4 +133,4 @@ namespace FluentTerminal.SystemTray.Services
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Requests;
|
||||
using FluentTerminal.SystemTray.Native;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel;
|
||||
using static winpty.WinPty;
|
||||
|
||||
namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
@@ -16,9 +21,12 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
private Stream _stdin;
|
||||
private Stream _stdout;
|
||||
private TerminalsManager _terminalsManager;
|
||||
private Process _shellProcess;
|
||||
private bool _exited;
|
||||
|
||||
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
|
||||
{
|
||||
Id = request.Id;
|
||||
_terminalsManager = terminalsManager;
|
||||
|
||||
var configHandle = IntPtr.Zero;
|
||||
@@ -36,13 +44,18 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
throw new Exception(winpty_error_msg(errorHandle));
|
||||
}
|
||||
|
||||
string exe = request.Profile.Location;
|
||||
string args = $"\"{exe}\" {request.Profile.Arguments}";
|
||||
string cwd = GetWorkingDirectory(request.Profile);
|
||||
spawnConfigHandle = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, exe, args, cwd, null, out errorHandle);
|
||||
var cwd = GetWorkingDirectory(request.Profile);
|
||||
var args = request.Profile.Arguments;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Profile.Location))
|
||||
{
|
||||
args = $"\"{request.Profile.Location}\" {args}";
|
||||
}
|
||||
|
||||
spawnConfigHandle = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, request.Profile.Location, args, cwd, terminalsManager.GetDefaultEnvironmentVariableString(), out errorHandle);
|
||||
if (errorHandle != IntPtr.Zero)
|
||||
{
|
||||
throw new Exception(winpty_error_msg(errorHandle).ToString());
|
||||
throw new Exception(winpty_error_msg(errorHandle));
|
||||
}
|
||||
|
||||
_stdin = CreatePipe(winpty_conin_name(_handle), PipeDirection.Out);
|
||||
@@ -50,10 +63,22 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
|
||||
if (!winpty_spawn(_handle, spawnConfigHandle, out IntPtr process, out IntPtr thread, out int procError, out errorHandle))
|
||||
{
|
||||
throw new Exception($"Failed to start the shell process. Please check your shell settings.\nTried to start: {args}");
|
||||
throw new Exception($@"Failed to start the shell process. Please check your shell settings.\nTried to start: {request.Profile.Location} ""{request.Profile.Arguments}""");
|
||||
}
|
||||
|
||||
ShellExecutableName = Path.GetFileNameWithoutExtension(exe);
|
||||
var shellProcessId = ProcessApi.GetProcessId(process);
|
||||
_shellProcess = Process.GetProcessById(shellProcessId);
|
||||
_shellProcess.EnableRaisingEvents = true;
|
||||
_shellProcess.Exited += _shellProcess_Exited;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Profile.Location))
|
||||
{
|
||||
ShellExecutableName = Path.GetFileNameWithoutExtension(request.Profile.Location);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShellExecutableName = request.Profile.Arguments.Split(' ')[0];
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -62,23 +87,22 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
winpty_error_free(errorHandle);
|
||||
}
|
||||
|
||||
var port = Utilities.GetAvailablePort();
|
||||
|
||||
if (port == null)
|
||||
{
|
||||
throw new Exception("no port available");
|
||||
}
|
||||
|
||||
Id = port.Value;
|
||||
|
||||
ListenToStdOut();
|
||||
}
|
||||
|
||||
private void _shellProcess_Exited(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
_exited = true;
|
||||
}
|
||||
|
||||
~WinPtySession()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private string GetWorkingDirectory(ShellProfile configuration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuration.WorkingDirectory) || !Directory.Exists(configuration.WorkingDirectory))
|
||||
@@ -96,6 +120,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_shellProcess.Exited -= _shellProcess_Exited;
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
@@ -105,10 +130,9 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
ConnectionClosed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void WriteText(string text)
|
||||
public void Write(byte[] data)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
_stdin.Write(bytes, 0, bytes.Length);
|
||||
_stdin.Write(data, 0, data.Length);
|
||||
}
|
||||
|
||||
public void Resize(TerminalSize size)
|
||||
@@ -119,7 +143,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
winpty_set_size(_handle, size.Columns, size.Rows, out errorHandle);
|
||||
if (errorHandle != IntPtr.Zero)
|
||||
{
|
||||
throw new Exception(winpty_error_msg(errorHandle).ToString());
|
||||
throw new Exception(winpty_error_msg(errorHandle));
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -168,25 +192,25 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
|
||||
private void ListenToStdOut()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
var reader = new StreamReader(_stdout);
|
||||
|
||||
do
|
||||
using (var reader = new StreamReader(_stdout))
|
||||
{
|
||||
var offset = 0;
|
||||
var buffer = new char[1024];
|
||||
var readChars = await reader.ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
|
||||
|
||||
if (readChars > 0)
|
||||
do
|
||||
{
|
||||
var output = new System.String(buffer, 0, readChars);
|
||||
_terminalsManager.DisplayTerminalOutput(Id, output);
|
||||
var buffer = new byte[1024];
|
||||
var readBytes = await _stdout.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
var read = new byte[readBytes];
|
||||
Buffer.BlockCopy(buffer, 0, read, 0, readBytes);
|
||||
|
||||
if (readBytes > 0)
|
||||
{
|
||||
_terminalsManager.DisplayTerminalOutput(Id, read);
|
||||
}
|
||||
}
|
||||
while (!_exited);
|
||||
}
|
||||
while (!reader.EndOfStream);
|
||||
Close();
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace FluentTerminal.SystemTray
|
||||
public static class Utilities
|
||||
{
|
||||
private const int FirstDynamicPort = 49151;
|
||||
private static readonly List<int> _sentOutPorts = new List<int>();
|
||||
|
||||
public static int? GetAvailablePort()
|
||||
{
|
||||
@@ -26,12 +27,15 @@ namespace FluentTerminal.SystemTray
|
||||
endPoints = properties.GetActiveUdpListeners();
|
||||
usedPorts.AddRange(endPoints.Where(e => e.Port >= FirstDynamicPort).Select(e => e.Port));
|
||||
|
||||
usedPorts.AddRange(_sentOutPorts);
|
||||
|
||||
usedPorts.Sort();
|
||||
|
||||
for (var i = FirstDynamicPort; i < UInt16.MaxValue; i++)
|
||||
{
|
||||
if (!usedPorts.Contains(i))
|
||||
{
|
||||
_sentOutPorts.Add(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -497,19 +497,19 @@ function InstallContextMenuIntegration
|
||||
-RegistryPath "hkcu:\Software\Classes\Directory\shell\Open Fluent Terminal here\command"`
|
||||
-Name "(Default)"`
|
||||
-Type "ExpandString"`
|
||||
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new %1"
|
||||
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new `"%1`""
|
||||
|
||||
ForceAddRegKeyValue `
|
||||
-RegistryPath "hkcu:\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command"`
|
||||
-Name "(Default)"`
|
||||
-Type "ExpandString"`
|
||||
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new %V"
|
||||
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new `"%V`""
|
||||
|
||||
ForceAddRegKeyValue `
|
||||
-RegistryPath "hkcu:\Software\Classes\Directory\LibraryFolder\shell\Open Fluent Terminal here\command"`
|
||||
-Name "(Default)"`
|
||||
-Type "ExpandString"`
|
||||
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new %V"
|
||||
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new `"%V`""
|
||||
}
|
||||
|
||||
#
|
||||
|
||||
@@ -26,7 +26,7 @@ A Terminal Emulator based on UWP and web technologies.
|
||||
|
||||
## Up Next
|
||||
|
||||
- Launch shell profile with a custom defined keybinding
|
||||
- ~~Launch shell profile with a custom defined keybinding~~
|
||||
- Copy&Paste options
|
||||
- Improved tabs
|
||||
- Split screen support
|
||||
@@ -38,6 +38,7 @@ A Terminal Emulator based on UWP and web technologies.
|
||||
|
||||
### Chocolatey package manager installation
|
||||
|
||||
- In **Settings**, click **For Developers**, and select **Developer mode**.
|
||||
- Install [Chocolatey](https://chocolatey.org/)
|
||||
- From an elevated/admin shell, execute `choco install fluent-terminal`
|
||||
|
||||
|
||||
Reference in new issue
Block a user