Skip to content
Permalink
Browse files

Experimental ConPTY support (#199)

* First steps of ConPty support

* Added resize functionality

* fix cwd, args, hide console window

* Handle shell process exited

* Make ConPTY optional
  • Loading branch information...
felixse committed Feb 8, 2019
1 parent f235ddf commit 0d85f933c7b67f0d19571f24155fcd0b679db95a
Showing with 992 additions and 56 deletions.
  1. +2 −1 FluentTerminal.App.Services.Test/TrayProcessCommunicationServiceTests.cs
  2. +1 −0 FluentTerminal.App.Services/IApplicationView.cs
  3. +2 −1 FluentTerminal.App.Services/ITerminalView.cs
  4. +2 −1 FluentTerminal.App.Services/ITrayProcessCommunicationService.cs
  5. +1 −0 FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs
  6. +7 −3 FluentTerminal.App.Services/Implementation/TrayProcessCommunicationService.cs
  7. +15 −0 FluentTerminal.App.ViewModels/Settings/GeneralPageViewModel.cs
  8. +35 −12 FluentTerminal.App.ViewModels/TerminalViewModel.cs
  9. +35 −0 FluentTerminal.App.ViewModels/Utilities/InputBuffer.cs
  10. +6 −0 FluentTerminal.App/Adapters/ApplicationViewAdapter.cs
  11. +4 −0 FluentTerminal.App/Views/SettingsPages/GeneralSettings.xaml
  12. +2 −2 FluentTerminal.App/Views/TerminalColorPicker.xaml.cs
  13. +2 −2 FluentTerminal.App/Views/TerminalView.xaml.cs
  14. +3 −3 FluentTerminal.Client/package-lock.json
  15. +1 −1 FluentTerminal.Client/package.json
  16. +6 −2 FluentTerminal.Client/src/index.js
  17. +1 −0 FluentTerminal.Client/webpack.config.js
  18. +1 −0 FluentTerminal.Models/ApplicationSettings.cs
  19. +9 −0 FluentTerminal.Models/Enums/SessionType.cs
  20. +4 −1 FluentTerminal.Models/Requests/CreateTerminalRequest.cs
  21. +13 −1 FluentTerminal.SystemTray/FluentTerminal.SystemTray.csproj
  22. +23 −0 FluentTerminal.SystemTray/Native/WindowApi.cs
  23. +121 −0 FluentTerminal.SystemTray/Services/ConPty/ConPtySession.cs
  24. +56 −0 FluentTerminal.SystemTray/Services/ConPty/Native/ConsoleApi.cs
  25. +86 −0 FluentTerminal.SystemTray/Services/ConPty/Native/ProcessApi.cs
  26. +33 −0 FluentTerminal.SystemTray/Services/ConPty/Native/PseudoConsoleApi.cs
  27. +70 −0 FluentTerminal.SystemTray/Services/ConPty/Processes/Process.cs
  28. +99 −0 FluentTerminal.SystemTray/Services/ConPty/Processes/ProcessFactory.cs
  29. +45 −0 FluentTerminal.SystemTray/Services/ConPty/PseudoConsole.cs
  30. +48 −0 FluentTerminal.SystemTray/Services/ConPty/PseudoConsolePipe.cs
  31. +205 −0 FluentTerminal.SystemTray/Services/ConPty/Terminal.cs
  32. +19 −0 FluentTerminal.SystemTray/Services/ITerminalSession.cs
  33. +23 −10 FluentTerminal.SystemTray/Services/TerminalsManager.cs
  34. +2 −6 FluentTerminal.SystemTray/Services/ToggleWindowService.cs
  35. +10 −10 FluentTerminal.SystemTray/Services/{TerminalSession.cs → WinPty/WinPtySession.cs}
@@ -42,10 +42,11 @@ public async Task CreateTerminal_Default_ReturnsResponseFromAppServiceConnection
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);

var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile);
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile, sessionType);

result.Should().BeEquivalentTo(response);
}
@@ -17,5 +17,6 @@ public interface IApplicationView
Task<bool> TryClose();

bool ToggleFullScreen();
bool IsApiContractPresent(string api, ushort version);
}
}
@@ -1,4 +1,5 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -19,7 +20,7 @@ public interface ITerminalView

Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings);

Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings);
Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType);

Task ConnectToSocket(string url);

@@ -1,4 +1,5 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Threading.Tasks;
@@ -11,7 +12,7 @@ public interface ITrayProcessCommunicationService

void Initialize(IAppServiceConnection appServiceConnection);

Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile);
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile, SessionType sessionType);

Task ResizeTerminal(int id, TerminalSize size);

@@ -21,6 +21,7 @@ public ApplicationSettings GetDefaultApplicationSettings()
MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu,
AlwaysShowTabs = false,
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false,
EnableTrayIcon = true
};
@@ -7,14 +7,15 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Foundation.Metadata;

namespace FluentTerminal.App.Services.Implementation
{
public class TrayProcessCommunicationService : ITrayProcessCommunicationService
{
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private Dictionary<int, Action<string>> _terminalOutputHandlers;
private readonly Dictionary<int, Action<string>> _terminalOutputHandlers;

public event EventHandler<int> TerminalExited;

@@ -24,12 +25,15 @@ public TrayProcessCommunicationService(ISettingsService settingsService)
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
}

public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile)
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
{


var request = new CreateTerminalRequest
{
Size = size,
Profile = shellProfile
Profile = shellProfile,
SessionType = sessionType
};

var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
@@ -54,6 +54,20 @@ public bool AlwaysShowTabs
}
}

public bool AlwaysUseWinPty
{
get => _applicationSettings.AlwaysUseWinPty;
set
{
if (_applicationSettings.AlwaysUseWinPty != value)
{
_applicationSettings.AlwaysUseWinPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}

public bool EnableTrayIcon
{
get => _applicationSettings.EnableTrayIcon;
@@ -264,6 +278,7 @@ private async Task RestoreDefaults()
InactiveTabColorMode = defaults.InactiveTabColorMode;
NewTerminalLocation = defaults.NewTerminalLocation;
AlwaysShowTabs = defaults.AlwaysShowTabs;
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon;
}
@@ -1,6 +1,7 @@
using Fleck;
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
@@ -26,15 +27,16 @@ public class TerminalViewModel : ViewModelBase
private readonly ShellProfile _shellProfile;
private readonly string _startupDirectory;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private InputBuffer _buffer;
private bool _isSelected;
private bool _newOutput;
private string _resizeOverlayContent;
private string _searchText;
private bool _showResizeOverlay;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private TerminalTheme _terminalTheme;
private int _terminalId;
private TerminalTheme _terminalTheme;
private ITerminalView _terminalView;
private string _title;
private IWebSocketConnection _webSocket;
@@ -94,24 +96,18 @@ public TabTheme BackgroundTabTheme
_settingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
}

public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}

public RelayCommand CloseCommand { get; }

public RelayCommand CloseSearchPanelCommand { get; }

public string DefaultTitle { get; private set; } = string.Empty;

public IAsyncCommand EditTitleCommand { get; }

public RelayCommand FindNextCommand { get; }

public RelayCommand FindPreviousCommand { get; }

public IAsyncCommand EditTitleCommand { get; }

public bool Initialized { get; private set; }

public bool IsSelected
@@ -191,6 +187,12 @@ public TabTheme TabTheme

public ObservableCollection<TabTheme> TabThemes { get; }

public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}

public string Title
{
get => _title;
@@ -238,14 +240,25 @@ public async Task OnViewIsReady(ITerminalView terminalView)
var keyBindings = _settingsService.GetCommandKeyBindings();
var profiles = _settingsService.GetShellProfiles();

var size = await _terminalView.CreateTerminal(options, TerminalTheme.Colors, FlattenKeyBindings(keyBindings, profiles)).ConfigureAwait(true);
var settings = _settingsService.GetApplicationSettings();
var sessionType = SessionType.Unknown;
if (!_applicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7) || settings.AlwaysUseWinPty)
{
sessionType = SessionType.WinPty;
}
else
{
sessionType = SessionType.ConPty;
}

var size = await _terminalView.CreateTerminal(options, TerminalTheme.Colors, FlattenKeyBindings(keyBindings, profiles), sessionType).ConfigureAwait(true);

if (!string.IsNullOrWhiteSpace(_startupDirectory))
{
_shellProfile.WorkingDirectory = _startupDirectory;
}

var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile).ConfigureAwait(true);
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile, sessionType).ConfigureAwait(true);

if (response.Success)
{
@@ -257,7 +270,7 @@ public async Task OnViewIsReady(ITerminalView terminalView)
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
{
_connectedEvent.Wait();
_webSocket.Send(t);
_buffer.Write(t);
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
_applicationView.RunOnDispatcherThread(() => NewOutput = true);
@@ -272,6 +285,7 @@ public async Task OnViewIsReady(ITerminalView terminalView)
webSocketServer.Start(socket =>
{
_webSocket = socket;
_buffer = new InputBuffer(_webSocket.Send);
socket.OnOpen = () => _connectedEvent.Set();
socket.OnMessage = message => _trayProcessCommunicationService.WriteText(_terminalId, message);
});
@@ -294,6 +308,15 @@ private void CloseSearchPanel()
_terminalView.FocusTerminal();
}

private async Task EditTitle()

This comment has been minimized.

Copy link
@ericcornelissen

ericcornelissen Feb 9, 2019

Contributor

This EditTitle clashes with the one on line 226 which was added in #193

{
var result = await _dialogService.ShowInputDialogAsync("Edit Title");
if (result != null)
{
Title = result;
}
}

private Task FindNext()
{
return _terminalView.FindNext(SearchText);
@@ -0,0 +1,35 @@
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>();

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)
{
var data = string.Concat(_buffer);
_buffer.Clear();
_writeCallback.Invoke(data);
}

public void Write(string data)
{
_buffer.Add(data);
}
}
}
@@ -3,6 +3,7 @@
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation.Metadata;
using Windows.UI.Core;
using Windows.UI.Core.Preview;
using Windows.UI.ViewManagement;
@@ -29,6 +30,11 @@ public string Title
set => _applicationView.Title = value;
}

public bool IsApiContractPresent(string api, ushort version)
{
return ApiInformation.IsApiContractPresent(api, version);
}

public Task RunOnDispatcherThread(Action action)
{
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
@@ -102,6 +102,10 @@
Margin="0,0,0,24"
Header="Show new output indicator on background tabs"
IsOn="{x:Bind ViewModel.ShowNewOutputIndicator, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Header="Windows 10 1809+: Always use WinPty"
IsOn="{x:Bind ViewModel.AlwaysUseWinPty, Mode=TwoWay}" />
</StackPanel>
</ScrollViewer>
</Grid>
@@ -7,7 +7,7 @@ namespace FluentTerminal.App.Views
{
public sealed partial class TerminalColorPicker : UserControl
{
bool colorIsNull = false;
private bool _colorIsNull;

public static readonly DependencyProperty ColorNameProperty =
DependencyProperty.Register(nameof(ColorName), typeof(string), typeof(TerminalColorPicker), new PropertyMetadata(null));
@@ -23,7 +23,7 @@ public sealed partial class TerminalColorPicker : UserControl
{
if (s is TerminalColorPicker terminalColorPicker)
{
terminalColorPicker.colorIsNull = ((Color?)e.NewValue) == null;
terminalColorPicker._colorIsNull = ((Color?)e.NewValue) == null;

// When leading the color, or changing the selected color (happens on page load)
// only set the picker (triggering the event action later) if the color is non-null
@@ -91,12 +91,12 @@ public Task ConnectToSocket(string url)
return ExecuteScriptAsync($"connectToWebSocket('{url}');");
}

public async Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings)
public async Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType)
{
var serializedOptions = JsonConvert.SerializeObject(options);
var serializedTheme = JsonConvert.SerializeObject(theme);
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}')").ConfigureAwait(true);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}', '{sessionType}')").ConfigureAwait(true);
return JsonConvert.DeserializeObject<TerminalSize>(size);
}

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -14,6 +14,6 @@
"webpack-cli": "^3.1.0"
},
"dependencies": {
"xterm": "^3.10.1"
"xterm": "^3.11.0"
}
}
@@ -13,7 +13,7 @@ Terminal.applyAddon(search);
var term, socket;
var terminalContainer = document.getElementById('terminal-container');

function createTerminal(options, theme, keyBindings) {
function createTerminal(options, theme, keyBindings, sessionType) {
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
@@ -58,7 +58,11 @@ function createTerminal(options, theme, keyBindings) {

term.open(terminalContainer);
term.fit();
term.winptyCompatInit();

if (sessionType === 'WinPty') {
term.winptyCompatInit();
}

term.focus();

setPadding(options.padding);
Oops, something went wrong.

0 comments on commit 0d85f93

Please sign in to comment.
You can’t perform that action at this time.