Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ namespace FluentTerminal.App.Services
|
||||
|
||||
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();
|
||||
|
||||
@@ -23,7 +23,8 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
AlwaysShowTabs = false,
|
||||
AlwaysUseWinPty = true,
|
||||
ShowNewOutputIndicator = false,
|
||||
EnableTrayIcon = true
|
||||
EnableTrayIcon = true,
|
||||
ShowCustomTitleInTitlebar = true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using FluentTerminal.Models.Responses;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.Services.Implementation
|
||||
@@ -14,7 +13,7 @@ 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;
|
||||
@@ -22,7 +21,7 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
public TrayProcessCommunicationService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
|
||||
_terminalOutputHandlers = new Dictionary<int, Action<byte[]>>();
|
||||
}
|
||||
|
||||
public int GetNextTerminalId()
|
||||
@@ -35,8 +34,11 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
var request = new GetAvailablePortRequest();
|
||||
|
||||
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
|
||||
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
|
||||
|
||||
return 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)
|
||||
@@ -49,9 +51,14 @@ namespace FluentTerminal.App.Services.Implementation
|
||||
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)
|
||||
@@ -75,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);
|
||||
}
|
||||
}
|
||||
@@ -97,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;
|
||||
}
|
||||
@@ -114,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));
|
||||
@@ -132,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,18 @@ namespace FluentTerminal.App.Services
|
||||
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>
|
||||
@@ -29,7 +38,7 @@ namespace FluentTerminal.App.Services
|
||||
/// <summary>
|
||||
/// to be observed by view
|
||||
/// </summary>
|
||||
public event EventHandler<string> OutputReceived;
|
||||
public event EventHandler<byte[]> OutputReceived;
|
||||
|
||||
/// <summary>
|
||||
/// to be observed by viewmodel
|
||||
@@ -51,7 +60,6 @@ namespace FluentTerminal.App.Services
|
||||
public async Task Close()
|
||||
{
|
||||
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
|
||||
Closed?.Invoke(this, System.EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,9 +126,9 @@ namespace FluentTerminal.App.Services
|
||||
}
|
||||
}
|
||||
|
||||
public Task Write(string text)
|
||||
public Task Write(byte[] data)
|
||||
{
|
||||
return _trayProcessCommunicationService.WriteText(Id, text);
|
||||
return _trayProcessCommunicationService.Write(Id, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Close();
|
||||
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.Close();
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using GalaSoft.MvvmLight.Command;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FluentTerminal.App.ViewModels
|
||||
@@ -16,17 +17,19 @@ namespace FluentTerminal.App.ViewModels
|
||||
private readonly IKeyboardCommandService _keyboardCommandService;
|
||||
private readonly IDispatcherTimer _resizeOverlayTimer;
|
||||
private bool _isSelected;
|
||||
private bool _newOutput;
|
||||
private bool _hasNewOutput;
|
||||
private string _resizeOverlayContent;
|
||||
private string _searchText;
|
||||
private bool _showResizeOverlay;
|
||||
private bool _showSearchPanel;
|
||||
private TabTheme _tabTheme;
|
||||
private TerminalTheme _terminalTheme;
|
||||
private string _title;
|
||||
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)
|
||||
{
|
||||
SettingsService = settingsService;
|
||||
@@ -40,7 +43,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
DialogService = dialogService;
|
||||
_keyboardCommandService = keyboardCommandService;
|
||||
ApplicationSettings = applicationSettings;
|
||||
StartupDirectory = startupDirectory;
|
||||
ApplicationView = applicationView;
|
||||
ClipboardService = clipboardService;
|
||||
|
||||
@@ -61,11 +63,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
|
||||
EditTitleCommand = new AsyncCommand(EditTitle);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(StartupDirectory))
|
||||
{
|
||||
ShellProfile.WorkingDirectory = StartupDirectory;
|
||||
}
|
||||
|
||||
Terminal = new Terminal(TrayProcessCommunicationService);
|
||||
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
|
||||
Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
@@ -82,6 +79,8 @@ namespace FluentTerminal.App.ViewModels
|
||||
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; }
|
||||
|
||||
@@ -119,8 +118,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
{
|
||||
if (IsSelected)
|
||||
{
|
||||
ApplicationView.Title = Title ?? string.Empty;
|
||||
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
|
||||
@@ -178,8 +176,6 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _showSearchPanel, value);
|
||||
}
|
||||
|
||||
public string StartupDirectory { get; }
|
||||
|
||||
public TabTheme TabTheme
|
||||
{
|
||||
get => _tabTheme;
|
||||
@@ -201,14 +197,26 @@ namespace FluentTerminal.App.ViewModels
|
||||
set => Set(ref _terminalTheme, value);
|
||||
}
|
||||
|
||||
public string Title
|
||||
public string TabTitle
|
||||
{
|
||||
get => _title;
|
||||
get => _tabTitle;
|
||||
set
|
||||
{
|
||||
if (Set(ref _title, value) && IsSelected)
|
||||
if (Set(ref _tabTitle, value))
|
||||
{
|
||||
ApplicationView.Title = Title;
|
||||
CustomTitleChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ShellTitle
|
||||
{
|
||||
get => _shellTitle;
|
||||
set
|
||||
{
|
||||
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
|
||||
{
|
||||
TabTitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,6 +225,10 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
public Task Close()
|
||||
{
|
||||
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
|
||||
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
|
||||
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
|
||||
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
|
||||
return Terminal.Close();
|
||||
}
|
||||
|
||||
@@ -230,7 +242,16 @@ namespace FluentTerminal.App.ViewModels
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,9 +301,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
});
|
||||
}
|
||||
|
||||
private void OnKeyBindingsChanged(object sender, EventArgs e)
|
||||
private async void OnKeyBindingsChanged(object sender, EventArgs e)
|
||||
{
|
||||
KeyBindingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
private void OnResizeOverlayTimerFinished(object sender, object e)
|
||||
@@ -291,9 +312,9 @@ namespace FluentTerminal.App.ViewModels
|
||||
ShowResizeOverlay = false;
|
||||
}
|
||||
|
||||
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
|
||||
{
|
||||
OptionsChanged?.Invoke(this, e);
|
||||
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
|
||||
}
|
||||
|
||||
private void SelectTabTheme(string name)
|
||||
@@ -303,7 +324,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
private async void Terminal_KeyboardCommandReceived(object sender, string e)
|
||||
@@ -322,7 +343,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.TranslateLineEndings(content);
|
||||
await Terminal.Write(content).ConfigureAwait(true);
|
||||
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -332,7 +353,7 @@ namespace FluentTerminal.App.ViewModels
|
||||
if (content != null)
|
||||
{
|
||||
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
|
||||
await Terminal.Write(content).ConfigureAwait(true);
|
||||
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -353,11 +374,11 @@ namespace FluentTerminal.App.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void Terminal_OutputReceived(object sender, string e)
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
|
||||
{
|
||||
NewOutput = true;
|
||||
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +390,8 @@ namespace FluentTerminal.App.ViewModels
|
||||
|
||||
private void Terminal_TitleChanged(object sender, string e)
|
||||
{
|
||||
Title = e;
|
||||
ShellTitle = e;
|
||||
ShellTitleChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private async Task TryClose()
|
||||
|
||||
@@ -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>
|
||||
+174
-49
@@ -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()
|
||||
{
|
||||
@@ -93,14 +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;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(e.Exception.ToString());
|
||||
Logger.Instance.Error(e.Exception, "Unhandled Exception");
|
||||
}
|
||||
|
||||
protected override async void OnActivated(IActivatedEventArgs args)
|
||||
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)
|
||||
{
|
||||
@@ -109,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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,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;
|
||||
@@ -243,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)
|
||||
@@ -271,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);
|
||||
}
|
||||
@@ -281,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)
|
||||
@@ -307,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>
|
||||
@@ -187,9 +193,6 @@
|
||||
<Compile Include="Views\TerminalView.xaml.cs">
|
||||
<DependentUpon>TerminalView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\VtNetCoreTerminalView.xaml.cs">
|
||||
<DependentUpon>VtNetCoreTerminalView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\XtermTerminalView.xaml.cs">
|
||||
<DependentUpon>XtermTerminalView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -209,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" />
|
||||
@@ -357,10 +364,6 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\VtNetCoreTerminalView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\XtermTerminalView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -368,25 +371,25 @@
|
||||
</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>
|
||||
</PackageReference>
|
||||
<PackageReference Include="VtNetCore">
|
||||
<Version>1.0.15</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.0.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -21,8 +21,7 @@ namespace FluentTerminal.App.Views
|
||||
ViewModel.FindNextRequested += OnFindNextRequested;
|
||||
ViewModel.FindPreviousRequested += OnFindPreviousRequested;
|
||||
InitializeComponent();
|
||||
//_terminalView = new XtermTerminalView();
|
||||
_terminalView = new VtNetCoreTerminalView();
|
||||
_terminalView = new XtermTerminalView();
|
||||
TerminalContainer.Children.Add((UIElement)_terminalView);
|
||||
_terminalView.Initialize(ViewModel);
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="FluentTerminal.App.Views.VtNetCoreTerminalView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="400"
|
||||
IsTabStop="True"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<canvas:CanvasControl x:Name="Canvas" ClearColor="Transparent" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,660 +0,0 @@
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VtNetCore.VirtualTerminal;
|
||||
using VtNetCore.VirtualTerminal.Layout;
|
||||
using VtNetCore.XTermParser;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
using Windows.UI.Core;
|
||||
using Windows.UI.Text;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Input;
|
||||
using Windows.UI.Xaml.Media;
|
||||
|
||||
namespace FluentTerminal.App.Views
|
||||
{
|
||||
public sealed partial class VtNetCoreTerminalView : UserControl, ITerminalView
|
||||
{
|
||||
private readonly int _blinkHideMs = 300;
|
||||
private readonly int _blinkShowMs = 600;
|
||||
private readonly DataConsumer _consumer;
|
||||
private readonly VirtualTerminalController _terminal;
|
||||
private readonly DispatcherTimer blinkDispatcher;
|
||||
private double _characterHeight = -1;
|
||||
private double _characterWidth = -1;
|
||||
private int _columns = -1;
|
||||
private TextPosition _mouseOver = new TextPosition();
|
||||
private int _rows = -1;
|
||||
private bool _scrolling;
|
||||
private bool _selecting;
|
||||
|
||||
private TextRange _textSelection;
|
||||
|
||||
public VtNetCoreTerminalView()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
_terminal = new VirtualTerminalController();
|
||||
_consumer = new DataConsumer(_terminal);
|
||||
ViewTop = _terminal.ViewPort.TopRow;
|
||||
_terminal.SendData += OnSendData;
|
||||
|
||||
blinkDispatcher = new DispatcherTimer();
|
||||
blinkDispatcher.Tick += BlinkTimerHandler;
|
||||
blinkDispatcher.Interval = TimeSpan.FromMilliseconds(Math.Min(150, GCD(_blinkShowMs, _blinkHideMs)));
|
||||
blinkDispatcher.Start();
|
||||
|
||||
Canvas.Draw += OnDraw;
|
||||
|
||||
KeyDown += TerminalKeyDown;
|
||||
Tapped += OnTapped;
|
||||
GotFocus += TerminalGotFocus;
|
||||
LostFocus += TerminalLostFocus;
|
||||
PointerWheelChanged += OnWheelChanged;
|
||||
PointerMoved += TerminalPointerMoved;
|
||||
PointerExited += TerminalPointerExited;
|
||||
PointerPressed += TerminalPointerPressed;
|
||||
PointerReleased += TerminalPointerReleased;
|
||||
}
|
||||
|
||||
public int MaxScrollValue => _terminal.ViewPort.TopRow;
|
||||
|
||||
public TextPosition MousePressedAt { get; set; }
|
||||
|
||||
public int ScrollValue
|
||||
{
|
||||
get { return ViewTop; }
|
||||
set
|
||||
{
|
||||
_scrolling = true;
|
||||
ViewTop = value;
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalViewModel ViewModel { get; private set; }
|
||||
public int ViewTop { get; set; }
|
||||
|
||||
public Task ChangeKeyBindings()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeOptions(TerminalOptions options)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeTheme(TerminalTheme theme)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FindNext(string searchText)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FindPrevious(string searchText)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FocusTerminal()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Color GetSolidColorBrush(string hex)
|
||||
{
|
||||
byte a = 255; // (byte)(Convert.ToUInt32(hex.Substring(0, 2), 16));
|
||||
byte r = (byte)(Convert.ToUInt32(hex.Substring(1, 2), 16));
|
||||
byte g = (byte)(Convert.ToUInt32(hex.Substring(3, 2), 16));
|
||||
byte b = (byte)(Convert.ToUInt32(hex.Substring(5, 2), 16));
|
||||
return Color.FromArgb(a, r, g, b);
|
||||
}
|
||||
|
||||
public async Task Initialize(TerminalViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
|
||||
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
|
||||
ViewModel.Terminal.Closed += Terminal_Closed;
|
||||
ViewModel.Terminal.RegisterSelectedTextCallback(() => Task.FromResult(_terminal.GetText(_textSelection.Start.Column, _textSelection.Start.Row, _textSelection.End.Column, _textSelection.End.Row)));
|
||||
|
||||
_terminal.WindowTitleChanged += OnWindowTitleChanged;
|
||||
_terminal.SizeChanged += OnSizeChanged;
|
||||
|
||||
var settings = ViewModel.SettingsService.GetApplicationSettings();
|
||||
var options = ViewModel.SettingsService.GetTerminalOptions();
|
||||
|
||||
Canvas.FontFamily = new FontFamily(options.FontFamily);
|
||||
Canvas.FontSize = options.FontSize;
|
||||
Canvas.Margin = new Thickness(options.Padding, options.Padding, options.Padding, options.Padding);
|
||||
|
||||
var sessionType = SessionType.Unknown;
|
||||
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
|
||||
{
|
||||
sessionType = SessionType.WinPty;
|
||||
}
|
||||
else
|
||||
{
|
||||
sessionType = SessionType.ConPty;
|
||||
}
|
||||
|
||||
await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, new TerminalSize { Columns = 80, Rows = 30 }, sessionType).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void BlinkTimerHandler(object sender, object e)
|
||||
{
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
|
||||
private bool BlinkVisible()
|
||||
{
|
||||
var blinkCycle = _blinkShowMs + _blinkHideMs;
|
||||
|
||||
return (DateTime.Now.Subtract(DateTime.MinValue).Milliseconds % blinkCycle) < _blinkHideMs;
|
||||
}
|
||||
|
||||
private bool ControlPressed()
|
||||
{
|
||||
return (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Control) & CoreVirtualKeyStates.Down) != 0;
|
||||
}
|
||||
|
||||
private void CoreWindow_CharacterReceived(CoreWindow sender, CharacterReceivedEventArgs args)
|
||||
{
|
||||
var ch = char.ConvertFromUtf32((int)args.KeyCode);
|
||||
|
||||
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
|
||||
// I lookup whether KeyPressed should handle the key here or there.
|
||||
var code = _terminal.GetKeySequence(ch, false, false);
|
||||
if (code == null)
|
||||
{
|
||||
args.Handled = _terminal.KeyPressed(ch, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private long GCD(long a, long b)
|
||||
{
|
||||
a = Math.Abs(a);
|
||||
b = Math.Abs(b);
|
||||
|
||||
// Pull out remainders.
|
||||
for (; ; )
|
||||
{
|
||||
long remainder = a % b;
|
||||
if (remainder == 0) return b;
|
||||
a = b;
|
||||
b = remainder;
|
||||
};
|
||||
}
|
||||
|
||||
private void OnDraw(CanvasControl sender, CanvasDrawEventArgs args)
|
||||
{
|
||||
var drawingSession = args.DrawingSession;
|
||||
|
||||
var textFormat =
|
||||
new CanvasTextFormat
|
||||
{
|
||||
FontSize = Convert.ToSingle(Canvas.FontSize),
|
||||
FontFamily = Canvas.FontFamily.Source,
|
||||
FontWeight = Canvas.FontWeight,
|
||||
WordWrapping = CanvasWordWrapping.NoWrap
|
||||
};
|
||||
|
||||
ProcessTextFormat(drawingSession, textFormat);
|
||||
|
||||
var showBlink = BlinkVisible();
|
||||
var spans = default(List<LayoutRow>);
|
||||
var cursorPosition = default(TextPosition);
|
||||
var showCursor = false;
|
||||
var cursorColor = Colors.Green;
|
||||
var TerminalTop = -1;
|
||||
|
||||
lock (_terminal)
|
||||
{
|
||||
TerminalTop = _terminal.ViewPort.TopRow;
|
||||
|
||||
if (_terminal.ViewPort.CursorPosition.Row > _rows)
|
||||
{
|
||||
throw new Exception("We should never be here");
|
||||
}
|
||||
|
||||
spans = _terminal.ViewPort.GetPageSpans(ViewTop, _rows, _columns, _textSelection);
|
||||
showCursor = _terminal.CursorState.ShowCursor;
|
||||
cursorPosition = _terminal.ViewPort.CursorPosition.Clone();
|
||||
cursorColor = GetSolidColorBrush(_terminal.CursorState.Attributes.WebColor);
|
||||
}
|
||||
|
||||
if (!_scrolling && ViewTop != TerminalTop)
|
||||
{
|
||||
ViewTop = TerminalTop;
|
||||
}
|
||||
|
||||
var defaultTransform = drawingSession.Transform;
|
||||
|
||||
PaintBackgroundLayer(drawingSession, spans);
|
||||
|
||||
PaintTextLayer(drawingSession, spans, textFormat, showBlink);
|
||||
|
||||
if (showCursor)
|
||||
{
|
||||
PaintCursor(drawingSession, spans, textFormat, cursorPosition.OffsetBy(0, TerminalTop - ViewTop), cursorColor);
|
||||
}
|
||||
|
||||
drawingSession.Transform = defaultTransform;
|
||||
}
|
||||
|
||||
private void OnSendData(object sender, SendDataEventArgs e)
|
||||
{
|
||||
ViewModel.Terminal.Write(Encoding.UTF8.GetString(e.Data));
|
||||
}
|
||||
|
||||
private async void OnSizeChanged(object sender, SizeEventArgs args)
|
||||
{
|
||||
await ViewModel.Terminal.SetSize(new TerminalSize { Rows = args.Height, Columns = args.Width }).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private void OnWindowTitleChanged(object sender, TextEventArgs args)
|
||||
{
|
||||
ViewModel.Terminal.SetTitle(args.Text);
|
||||
}
|
||||
|
||||
private void PaintBackgroundLayer(CanvasDrawingSession drawingSession, List<LayoutRow> spans)
|
||||
{
|
||||
double lineY = 0;
|
||||
foreach (var textRow in spans)
|
||||
{
|
||||
drawingSession.Transform =
|
||||
Matrix3x2.CreateScale(
|
||||
(float)(textRow.DoubleWidth ? 2.0 : 1.0), // Scale double width
|
||||
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0) // Scale double high
|
||||
);
|
||||
|
||||
var drawY =
|
||||
(lineY - (textRow.DoubleHeightBottom ? _characterHeight : 0)) * // Offset position upwards for bottom of double high char
|
||||
((textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0); // Scale position for double height
|
||||
|
||||
double drawX = 0;
|
||||
foreach (var textSpan in textRow.Spans)
|
||||
{
|
||||
var bounds =
|
||||
new Rect(
|
||||
drawX,
|
||||
drawY,
|
||||
_characterWidth * (textSpan.Text.Length) + 0.9,
|
||||
_characterHeight + 0.9
|
||||
);
|
||||
|
||||
if (textSpan.BackgroundColor != "#000000") // todo set theme
|
||||
{
|
||||
drawingSession.FillRectangle(bounds, GetSolidColorBrush(textSpan.BackgroundColor));
|
||||
}
|
||||
|
||||
|
||||
drawX += _characterWidth * (textSpan.Text.Length);
|
||||
}
|
||||
|
||||
lineY += _characterHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private void PaintCursor(CanvasDrawingSession drawingSession, List<LayoutRow> spans, CanvasTextFormat textFormat, TextPosition cursorPosition, Color cursorColor)
|
||||
{
|
||||
var cursorY = cursorPosition.Row;
|
||||
if (cursorY < 0 || cursorY >= _rows)
|
||||
return;
|
||||
|
||||
var drawX = cursorPosition.Column * _characterWidth;
|
||||
var drawY = (cursorY * _characterHeight);
|
||||
|
||||
if (cursorY < spans.Count)
|
||||
{
|
||||
var textRow = spans[cursorY];
|
||||
|
||||
drawingSession.Transform =
|
||||
Matrix3x2.CreateTranslation(
|
||||
1.0f,
|
||||
(float)(textRow.DoubleHeightBottom ? -_characterHeight : 0)
|
||||
) *
|
||||
Matrix3x2.CreateScale(
|
||||
(float)(textRow.DoubleWidth ? 2.0 : 1.0),
|
||||
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0)
|
||||
);
|
||||
|
||||
drawY *= (textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0;
|
||||
}
|
||||
|
||||
var cursorRect = new Rect(
|
||||
drawX,
|
||||
drawY,
|
||||
_characterWidth,
|
||||
_characterHeight + 0.9
|
||||
);
|
||||
|
||||
drawingSession.DrawRectangle(cursorRect, cursorColor);
|
||||
}
|
||||
|
||||
private void PaintTextLayer(CanvasDrawingSession drawingSession, List<LayoutRow> spans, CanvasTextFormat textFormat, bool showBlink)
|
||||
{
|
||||
var dipToDpiRatio = Windows.Graphics.Display.DisplayInformation.GetForCurrentView().LogicalDpi / 96;
|
||||
|
||||
double lineY = 0;
|
||||
foreach (var textRow in spans)
|
||||
{
|
||||
drawingSession.Transform =
|
||||
Matrix3x2.CreateScale(
|
||||
(float)(textRow.DoubleWidth ? 2.0 : 1.0), // Scale double width
|
||||
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0) // Scale double high
|
||||
);
|
||||
|
||||
var drawY =
|
||||
(lineY - (textRow.DoubleHeightBottom ? _characterHeight : 0)) * // Offset position upwards for bottom of double high char
|
||||
((textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0); // Scale position for double height
|
||||
|
||||
double drawX = 0;
|
||||
foreach (var textSpan in textRow.Spans)
|
||||
{
|
||||
var runWidth = _characterWidth * (textSpan.Text.Length);
|
||||
|
||||
if (textSpan.Hidden || (textSpan.Blink && !showBlink))
|
||||
{
|
||||
drawX += runWidth;
|
||||
continue;
|
||||
}
|
||||
|
||||
var color = GetSolidColorBrush(textSpan.ForgroundColor);
|
||||
textFormat.FontWeight = textSpan.Bold ? FontWeights.Bold : FontWeights.Light;
|
||||
|
||||
var textLayout = new CanvasTextLayout(drawingSession, textSpan.Text, textFormat, 0.0f, 0.0f);
|
||||
drawingSession.DrawTextLayout(textLayout, (float)drawX, (float)drawY, color);
|
||||
|
||||
if (textSpan.Underline)
|
||||
{
|
||||
// TODO : Come up with a better means of identifying line weight and offset
|
||||
var underlineOffset = textLayout.LineMetrics[0].Baseline * dipToDpiRatio * 1.07;
|
||||
|
||||
drawingSession.DrawLine(new Vector2((float)drawX, (float)(drawY + underlineOffset)), new Vector2((float)(drawX + runWidth), (float)(drawY + underlineOffset)), color);
|
||||
}
|
||||
|
||||
drawX += _characterWidth * (textSpan.Text.Length);
|
||||
}
|
||||
|
||||
lineY += _characterHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessTextFormat(CanvasDrawingSession drawingSession, CanvasTextFormat format)
|
||||
{
|
||||
CanvasTextLayout textLayout = new CanvasTextLayout(drawingSession, "\u2560", format, 0.0f, 0.0f);
|
||||
if (_characterWidth != textLayout.DrawBounds.Width || _characterHeight != textLayout.DrawBounds.Height)
|
||||
{
|
||||
_characterWidth = textLayout.DrawBounds.Width;
|
||||
_characterHeight = textLayout.DrawBounds.Height;
|
||||
}
|
||||
|
||||
int columns = Convert.ToInt32(Math.Floor(Canvas.RenderSize.Width / _characterWidth));
|
||||
int rows = Convert.ToInt32(Math.Floor(Canvas.RenderSize.Height / _characterHeight));
|
||||
if (_columns != columns || _rows != rows)
|
||||
{
|
||||
_columns = columns;
|
||||
_rows = rows;
|
||||
ViewModel.Terminal.SetSize(new TerminalSize { Columns = columns, Rows = rows });
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShiftPressed()
|
||||
{
|
||||
return (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Shift) & CoreVirtualKeyStates.Down) != 0;
|
||||
}
|
||||
|
||||
private void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
private void Terminal_OutputReceived(object sender, string e)
|
||||
{
|
||||
var oldTopRow = _terminal.ViewPort.TopRow;
|
||||
|
||||
_consumer.Push(Encoding.UTF8.GetBytes(e));
|
||||
|
||||
if (_terminal.Changed)
|
||||
{
|
||||
_terminal.ClearChanges();
|
||||
|
||||
if (oldTopRow != _terminal.ViewPort.TopRow && oldTopRow >= ViewTop)
|
||||
{
|
||||
ViewTop = _terminal.ViewPort.TopRow;
|
||||
}
|
||||
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void TerminalGotFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Window.Current.CoreWindow.CharacterReceived += CoreWindow_CharacterReceived;
|
||||
|
||||
_terminal?.FocusIn();
|
||||
}
|
||||
|
||||
private void TerminalKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
_scrolling = false;
|
||||
|
||||
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
|
||||
// I lookup whether KeyPressed should handle the key here or there.
|
||||
var code = _terminal.GetKeySequence(e.Key.ToString(), ControlPressed(), ShiftPressed());
|
||||
if (code != null)
|
||||
e.Handled = _terminal.KeyPressed(e.Key.ToString(), ControlPressed(), ShiftPressed());
|
||||
|
||||
if (ViewTop != _terminal.ViewPort.TopRow)
|
||||
{
|
||||
ViewTop = _terminal.ViewPort.TopRow;
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void TerminalLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Window.Current.CoreWindow.CharacterReceived -= CoreWindow_CharacterReceived;
|
||||
_terminal?.FocusOut();
|
||||
}
|
||||
|
||||
private void TerminalPointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
_mouseOver = null;
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
|
||||
private void TerminalPointerMoved(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
var pointer = e.GetCurrentPoint(Canvas);
|
||||
var position = PointToTextPosition(pointer.Position);
|
||||
|
||||
var textPosition = position.OffsetBy(0, ViewTop);
|
||||
|
||||
if ((_terminal.UseAllMouseTracking || _terminal.CellMotionMouseTracking) && position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
|
||||
{
|
||||
var button =
|
||||
pointer.Properties.IsLeftButtonPressed ? 0 :
|
||||
pointer.Properties.IsRightButtonPressed ? 1 :
|
||||
pointer.Properties.IsMiddleButtonPressed ? 2 :
|
||||
3; // No button
|
||||
|
||||
_terminal.MouseMove(position.Column, position.Row, button, ControlPressed(), ShiftPressed());
|
||||
|
||||
if (button == 3 && !_terminal.UseAllMouseTracking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_mouseOver != null && _mouseOver == position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_mouseOver = position;
|
||||
|
||||
if (pointer.Properties.IsLeftButtonPressed && MousePressedAt != null)
|
||||
{
|
||||
TextRange newSelection;
|
||||
|
||||
if (MousePressedAt != textPosition)
|
||||
{
|
||||
if (MousePressedAt <= textPosition)
|
||||
{
|
||||
newSelection = new TextRange
|
||||
{
|
||||
Start = MousePressedAt,
|
||||
End = textPosition.OffsetBy(-1, 0)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
newSelection = new TextRange
|
||||
{
|
||||
Start = textPosition,
|
||||
End = MousePressedAt
|
||||
};
|
||||
}
|
||||
|
||||
_selecting = true;
|
||||
|
||||
if (_textSelection != newSelection)
|
||||
{
|
||||
_textSelection = newSelection;
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TerminalPointerPressed(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
var pointer = e.GetCurrentPoint(Canvas);
|
||||
var position = PointToTextPosition(pointer.Position);
|
||||
|
||||
var textPosition = position.OffsetBy(0, ViewTop);
|
||||
|
||||
if (!_terminal.MouseTrackingEnabled)
|
||||
{
|
||||
if (pointer.Properties.IsLeftButtonPressed)
|
||||
MousePressedAt = textPosition;
|
||||
//else if (pointer.Properties.IsRightButtonPressed)
|
||||
//PasteClipboard();
|
||||
}
|
||||
|
||||
if (position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
|
||||
{
|
||||
var button =
|
||||
pointer.Properties.IsLeftButtonPressed ? 0 :
|
||||
pointer.Properties.IsRightButtonPressed ? 1 :
|
||||
pointer.Properties.IsMiddleButtonPressed ? 2 :
|
||||
3; // No button
|
||||
|
||||
_terminal.MousePress(position.Column, position.Row, button, ControlPressed(), ShiftPressed());
|
||||
}
|
||||
}
|
||||
|
||||
private void TerminalPointerReleased(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
var pointer = e.GetCurrentPoint(Canvas);
|
||||
var position = PointToTextPosition(pointer.Position);
|
||||
var textPosition = position.OffsetBy(0, ViewTop);
|
||||
|
||||
if (!pointer.Properties.IsLeftButtonPressed)
|
||||
{
|
||||
if (_selecting)
|
||||
{
|
||||
MousePressedAt = null;
|
||||
_selecting = false;
|
||||
|
||||
var captured = _terminal.GetText(_textSelection.Start.Column, _textSelection.Start.Row, _textSelection.End.Column, _textSelection.End.Row);
|
||||
|
||||
var dataPackage = new DataPackage();
|
||||
dataPackage.SetText(captured);
|
||||
dataPackage.Properties.EnterpriseId = "FluentTerminal";
|
||||
try
|
||||
{
|
||||
Clipboard.SetContent(dataPackage);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.WriteLine("Failed to post to clipboard: " + exception.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_textSelection = null;
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
if (position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
|
||||
{
|
||||
_terminal.MouseRelease(position.Column, position.Row, ControlPressed(), ShiftPressed());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
Focus(FocusState.Pointer);
|
||||
}
|
||||
|
||||
private void OnWheelChanged(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
var pointer = e.GetCurrentPoint(Canvas);
|
||||
|
||||
int oldViewTop = ViewTop;
|
||||
|
||||
ViewTop -= pointer.Properties.MouseWheelDelta / 40;
|
||||
_scrolling = true;
|
||||
|
||||
if (ViewTop < 0)
|
||||
{
|
||||
ViewTop = 0;
|
||||
}
|
||||
else if (ViewTop > _terminal.ViewPort.TopRow)
|
||||
{
|
||||
ViewTop = _terminal.ViewPort.TopRow;
|
||||
}
|
||||
|
||||
if (oldViewTop != ViewTop)
|
||||
{
|
||||
Canvas.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private TextPosition PointToTextPosition(Point point)
|
||||
{
|
||||
var overColumn = (int)Math.Floor(point.X / _characterWidth);
|
||||
if (overColumn >= _columns)
|
||||
{
|
||||
overColumn = _columns - 1;
|
||||
}
|
||||
|
||||
var overRow = (int)Math.Floor(point.Y / _characterHeight);
|
||||
if (overRow >= _rows)
|
||||
{
|
||||
overRow = _rows - 1;
|
||||
}
|
||||
|
||||
return new TextPosition { Column = overColumn, Row = overRow };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Fleck;
|
||||
using FluentTerminal.App.Services;
|
||||
using FluentTerminal.App.Utilities;
|
||||
using FluentTerminal.App.ViewModels;
|
||||
using FluentTerminal.App.ViewModels.Utilities;
|
||||
using FluentTerminal.Models;
|
||||
using FluentTerminal.Models.Enums;
|
||||
using FluentTerminal.RuntimeComponent.Enums;
|
||||
@@ -14,6 +15,7 @@ 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;
|
||||
@@ -28,37 +30,49 @@ namespace FluentTerminal.App.Views
|
||||
public sealed partial class XtermTerminalView : UserControl, IxtermEventListener, ITerminalView
|
||||
{
|
||||
private readonly ManualResetEventSlim _connectedEvent;
|
||||
private readonly SemaphoreSlim _loaded;
|
||||
private bool _alreadyLoaded;
|
||||
private InputBuffer _buffer;
|
||||
private MenuFlyoutItem _copyMenuItem;
|
||||
private readonly SemaphoreSlim _navigationCompleted;
|
||||
private readonly MenuFlyoutItem _copyMenuItem;
|
||||
private BlockingCollection<Action> _dispatcherJobs;
|
||||
private MenuFlyoutItem _pasteMenuItem;
|
||||
private IWebSocketConnection _webSocket;
|
||||
private readonly MenuFlyoutItem _pasteMenuItem;
|
||||
private WebView _webView;
|
||||
private readonly DebouncedAction<TerminalOptions> _optionsChanged;
|
||||
private IWebSocketConnection _socket;
|
||||
|
||||
public XtermTerminalView()
|
||||
{
|
||||
_loaded = new SemaphoreSlim(0, 1);
|
||||
_connectedEvent = new ManualResetEventSlim(false);
|
||||
|
||||
JsonConvert.DefaultSettings = () =>
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
};
|
||||
settings.Converters.Add(new StringEnumConverter(true));
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
StartMediatorTask();
|
||||
|
||||
Loaded += XtermTerminalView_Loaded;
|
||||
GotFocus += XtermTerminalView_GotFocus;
|
||||
_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; }
|
||||
@@ -73,8 +87,8 @@ namespace FluentTerminal.App.Views
|
||||
|
||||
public Task ChangeOptions(TerminalOptions options)
|
||||
{
|
||||
var serialized = JsonConvert.SerializeObject(options);
|
||||
return ExecuteScriptAsync($"changeOptions('{serialized}')");
|
||||
_optionsChanged.Invoke(options);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChangeTheme(TerminalTheme theme)
|
||||
@@ -123,7 +137,7 @@ namespace FluentTerminal.App.Views
|
||||
{
|
||||
sessionType = SessionType.ConPty;
|
||||
}
|
||||
await _loaded.WaitAsync().ConfigureAwait(true);
|
||||
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);
|
||||
@@ -134,6 +148,8 @@ namespace FluentTerminal.App.Views
|
||||
|
||||
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()));
|
||||
@@ -200,7 +216,8 @@ namespace FluentTerminal.App.Views
|
||||
|
||||
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
|
||||
{
|
||||
_loaded.Release();
|
||||
Logger.Instance.Debug("WebView navigation completed. Target: {uri}", args.Uri);
|
||||
_navigationCompleted.Release();
|
||||
}
|
||||
|
||||
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
|
||||
@@ -216,16 +233,42 @@ namespace FluentTerminal.App.Views
|
||||
|
||||
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 =>
|
||||
{
|
||||
_webSocket = socket;
|
||||
_buffer = new InputBuffer(_webSocket.Send);
|
||||
socket.OnOpen = () => _connectedEvent.Set();
|
||||
socket.OnMessage = message => ViewModel.Terminal.Write(message);
|
||||
_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);
|
||||
}
|
||||
|
||||
@@ -272,7 +315,7 @@ namespace FluentTerminal.App.Views
|
||||
{
|
||||
_dispatcherJobs = new BlockingCollection<Action>();
|
||||
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
|
||||
Task.Run(async () =>
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
|
||||
{
|
||||
@@ -285,66 +328,28 @@ namespace FluentTerminal.App.Views
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
private async void Terminal_Closed(object sender, EventArgs e)
|
||||
{
|
||||
ViewModel.Terminal.OutputReceived -= Terminal_OutputReceived;
|
||||
ViewModel.Terminal.Closed -= Terminal_Closed;
|
||||
_webView?.Navigate(new Uri("about:blank"));
|
||||
_webView = null;
|
||||
await ViewModel.Terminal.Close().ConfigureAwait(true);
|
||||
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, string e)
|
||||
private void Terminal_OutputReceived(object sender, byte[] e)
|
||||
{
|
||||
_webSocket.Send(e);
|
||||
_socket.Send(Encoding.UTF8.GetString(e));
|
||||
}
|
||||
|
||||
private async void XtermTerminalView_GotFocus(object sender, RoutedEventArgs e)
|
||||
void IxtermEventListener.OnError(string error)
|
||||
{
|
||||
if (_webView != null)
|
||||
{
|
||||
_webView.Focus(FocusState.Programmatic);
|
||||
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void XtermTerminalView_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_alreadyLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Root.Children.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_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 }
|
||||
};
|
||||
|
||||
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
|
||||
_alreadyLoaded = true;
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -126,10 +126,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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,6 @@
|
||||
public class DisplayTerminalOutputRequest
|
||||
{
|
||||
public int TerminalId { get; set; }
|
||||
public string Output { get; set; }
|
||||
public byte[] Output { get; set; }
|
||||
}
|
||||
}
|
||||
+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; }
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@
|
||||
</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' ">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Windows.ApplicationModel.AppService;
|
||||
using Windows.Foundation.Collections;
|
||||
using System;
|
||||
using FluentTerminal.Models.Responses;
|
||||
using FluentTerminal.App.Services;
|
||||
|
||||
namespace FluentTerminal.SystemTray.Services
|
||||
{
|
||||
@@ -45,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)
|
||||
@@ -84,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();
|
||||
@@ -102,10 +108,10 @@ 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))
|
||||
{
|
||||
|
||||
@@ -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,6 +10,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
{
|
||||
private TerminalsManager _terminalsManager;
|
||||
private Terminal _terminal;
|
||||
private bool _exited;
|
||||
|
||||
public int Id { get; private set; }
|
||||
|
||||
@@ -34,16 +35,26 @@ namespace FluentTerminal.SystemTray.Services.ConPty
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -63,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)
|
||||
@@ -108,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
|
||||
{
|
||||
@@ -63,11 +76,11 @@ namespace FluentTerminal.SystemTray.Services
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,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.Add("TERM", "xterm-256color");
|
||||
environmentVariables.Add("TERM_PROGRAM", "FluentTerminal");
|
||||
environmentVariables.Add("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)
|
||||
@@ -102,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,6 +21,8 @@ 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)
|
||||
{
|
||||
@@ -37,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);
|
||||
@@ -51,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
|
||||
{
|
||||
@@ -66,11 +90,19 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
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))
|
||||
@@ -88,6 +120,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_shellProcess.Exited -= _shellProcess_Exited;
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
@@ -97,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)
|
||||
@@ -111,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
|
||||
@@ -160,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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