Compare commits

...
3 Commits
Author SHA1 Message Date
FelixSe 0733b1ab02 Initial adaption of VtNetCore.UWP 2019-03-03 00:29:00 +01:00
FelixSe 4e54668092 fix TerminalTheme Equals() 2019-03-02 19:23:18 +01:00
FelixSe 74abd4f448 Encapsulate xterm.js implementation in XtermTerminalView 2019-03-02 19:23:05 +01:00
29 changed files with 1446 additions and 556 deletions

No files matched your search

@@ -1,39 +0,0 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface ITerminalView
{
event EventHandler<TerminalSize> TerminalSizeChanged;
event EventHandler<string> TerminalTitleChanged;
event EventHandler<string> KeyboardCommandReceived;
Task ChangeTheme(TerminalColors theme);
Task ChangeOptions(TerminalOptions options);
Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings);
Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType);
Task ConnectToSocket(string url);
void Close();
Task FocusTerminal();
Task<string> GetSelection();
Task FindNext(string searchText);
Task FindPrevious(string searchText);
void FocusSearchTextBox();
}
}
@@ -12,7 +12,7 @@ namespace FluentTerminal.App.Services
void Initialize(IAppServiceConnection appServiceConnection); void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile, SessionType sessionType); Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task ResizeTerminal(int id, TerminalSize size); Task ResizeTerminal(int id, TerminalSize size);
@@ -23,5 +23,7 @@ namespace FluentTerminal.App.Services
void SubscribeForTerminalOutput(int terminalId, Action<string> callback); void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
Task CloseTerminal(int terminalId); Task CloseTerminal(int terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
int GetNextTerminalId();
} }
} }
@@ -7,7 +7,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Foundation.Metadata;
namespace FluentTerminal.App.Services.Implementation namespace FluentTerminal.App.Services.Implementation
{ {
@@ -16,6 +15,7 @@ namespace FluentTerminal.App.Services.Implementation
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection; private IAppServiceConnection _appServiceConnection;
private readonly Dictionary<int, Action<string>> _terminalOutputHandlers; private readonly Dictionary<int, Action<string>> _terminalOutputHandlers;
private int _nextTerminalId = 0;
public event EventHandler<int> TerminalExited; public event EventHandler<int> TerminalExited;
@@ -25,12 +25,25 @@ namespace FluentTerminal.App.Services.Implementation
_terminalOutputHandlers = new Dictionary<int, Action<string>>(); _terminalOutputHandlers = new Dictionary<int, Action<string>>();
} }
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile, SessionType sessionType) public int GetNextTerminalId()
{ {
return _nextTerminalId++;
}
public async Task<GetAvailablePortResponse> GetAvailablePort()
{
var request = new GetAvailablePortRequest();
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
return JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
}
public async Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
{
var request = new CreateTerminalRequest var request = new CreateTerminalRequest
{ {
Id = id,
Size = size, Size = size,
Profile = shellProfile, Profile = shellProfile,
SessionType = sessionType SessionType = sessionType
+126
View File
@@ -0,0 +1,126 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public class Terminal
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
Id = _trayProcessCommunicationService.GetNextTerminalId();
}
/// <summary>
/// To be observed by both view and viewmodel
/// </summary>
public event EventHandler Closed;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> KeyboardCommandReceived;
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<string> OutputReceived;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<TerminalSize> SizeChanged;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public int Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
{
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
Closed?.Invoke(this, System.EventArgs.Empty);
}
/// <summary>
/// to be called by viewmodel
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback();
}
/// <summary>
/// to be called by view
/// </summary>
public void ProcessKeyboardCommand(string command)
{
KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be set by view
/// </summary>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
{
_selectedTextCallback = selectedTextCallback;
}
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
SizeChanged?.Invoke(this, size);
}
/// <summary>
/// to be called by view
/// </summary>
public void SetTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// </summary>
/// <param name="shellProfile"></param>
/// <param name="size"></param>
/// <param name="sessionType"></param>
public async Task StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
{
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
}
}
public Task Write(string text)
{
return _trayProcessCommunicationService.WriteText(Id, text);
}
}
}
@@ -213,7 +213,7 @@ namespace FluentTerminal.App.ViewModels
{ {
foreach (var terminal in Terminals) foreach (var terminal in Terminals)
{ {
terminal.CloseView(); terminal.Close();
} }
} }
@@ -279,7 +279,7 @@ namespace FluentTerminal.App.ViewModels
{ {
if (sender is TerminalViewModel terminal) if (sender is TerminalViewModel terminal)
{ {
terminal.CloseView(); terminal.Close();
if (SelectedTerminal == terminal) if (SelectedTerminal == terminal)
{ {
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal); SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
+148 -224
View File
@@ -1,33 +1,20 @@
using Fleck; using FluentTerminal.App.Services;
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure; using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models; using FluentTerminal.Models;
using FluentTerminal.Models.Enums; using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command; using GalaSoft.MvvmLight.Command;
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels namespace FluentTerminal.App.ViewModels
{ {
public class TerminalViewModel : ViewModelBase public class TerminalViewModel : ViewModelBase
{ {
private readonly IApplicationView _applicationView;
private readonly IClipboardService _clipboardService;
private readonly ManualResetEventSlim _connectedEvent;
private readonly IDialogService _dialogService;
private readonly IKeyboardCommandService _keyboardCommandService; private readonly IKeyboardCommandService _keyboardCommandService;
private readonly IDispatcherTimer _resizeOverlayTimer; private readonly IDispatcherTimer _resizeOverlayTimer;
private readonly ISettingsService _settingsService;
private readonly ShellProfile _shellProfile;
private readonly string _startupDirectory;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private InputBuffer _buffer;
private bool _isSelected; private bool _isSelected;
private bool _newOutput; private bool _newOutput;
private string _resizeOverlayContent; private string _resizeOverlayContent;
@@ -35,72 +22,87 @@ namespace FluentTerminal.App.ViewModels
private bool _showResizeOverlay; private bool _showResizeOverlay;
private bool _showSearchPanel; private bool _showSearchPanel;
private TabTheme _tabTheme; private TabTheme _tabTheme;
private int _terminalId;
private TerminalTheme _terminalTheme; private TerminalTheme _terminalTheme;
private ITerminalView _terminalView;
private string _title; private string _title;
private IWebSocketConnection _webSocket;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile, IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService) IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
{ {
Title = DefaultTitle; SettingsService = settingsService;
SettingsService.CurrentThemeChanged += OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged += OnKeyBindingsChanged;
_connectedEvent = new ManualResetEventSlim(false); TrayProcessCommunicationService = trayProcessCommunicationService;
_settingsService = settingsService; DialogService = dialogService;
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_settingsService.KeyBindingsChanged += OnKeyBindingsChanged;
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
_dialogService = dialogService;
_keyboardCommandService = keyboardCommandService; _keyboardCommandService = keyboardCommandService;
ApplicationSettings = applicationSettings; ApplicationSettings = applicationSettings;
_startupDirectory = startupDirectory; StartupDirectory = startupDirectory;
_shellProfile = shellProfile; ApplicationView = applicationView;
_applicationView = applicationView; ClipboardService = clipboardService;
_clipboardService = clipboardService;
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? _settingsService.GetCurrentTheme() : _settingsService.GetTheme(shellProfile.TerminalThemeId); ShellProfile = shellProfile;
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? SettingsService.GetCurrentTheme() : SettingsService.GetTheme(shellProfile.TerminalThemeId);
TabThemes = new ObservableCollection<TabTheme>(_settingsService.GetTabThemes()); TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == _shellProfile.TabThemeId); TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
_resizeOverlayTimer = dispatcherTimer; _resizeOverlayTimer = dispatcherTimer;
_resizeOverlayTimer.Interval = new TimeSpan(0, 0, 2); _resizeOverlayTimer.Interval = new TimeSpan(0, 0, 2);
_resizeOverlayTimer.Tick += OnResizeOverlayTimerFinished; _resizeOverlayTimer.Tick += OnResizeOverlayTimerFinished;
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false)); CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
FindNextCommand = new RelayCommand(async () => await FindNext().ConfigureAwait(false)); FindNextCommand = new RelayCommand(FindNext);
FindPreviousCommand = new RelayCommand(async () => await FindPrevious().ConfigureAwait(false)); FindPreviousCommand = new RelayCommand(FindPrevious);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel); CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme); SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
EditTitleCommand = new AsyncCommand(EditTitle); EditTitleCommand = new AsyncCommand(EditTitle);
if (!string.IsNullOrWhiteSpace(StartupDirectory))
{
ShellProfile.WorkingDirectory = StartupDirectory;
}
Terminal = new Terminal(TrayProcessCommunicationService);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
Terminal.OutputReceived += Terminal_OutputReceived;
Terminal.SizeChanged += Terminal_SizeChanged;
Terminal.TitleChanged += Terminal_TitleChanged;
Terminal.Closed += Terminal_Closed;
} }
public event EventHandler Activated;
public event EventHandler Closed; public event EventHandler Closed;
public event EventHandler<string> FindNextRequested;
public event EventHandler<string> FindPreviousRequested;
public event EventHandler KeyBindingsChanged;
public event EventHandler<TerminalOptions> OptionsChanged;
public event EventHandler SearchStarted;
public event EventHandler<TerminalTheme> ThemeChanged;
public ApplicationSettings ApplicationSettings { get; private set; } public ApplicationSettings ApplicationSettings { get; private set; }
public IApplicationView ApplicationView { get; }
public TabTheme BackgroundTabTheme public TabTheme BackgroundTabTheme
{ {
// The effective background theme depends on whether it is selected (use the theme), or if it is inactive // The effective background theme depends on whether it is selected (use the theme), or if it is inactive
// (if we're set to underline inactive tabs, use the null theme). // (if we're set to underline inactive tabs, use the null theme).
get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ? get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ?
_tabTheme : _tabTheme :
_settingsService.GetTabThemes().FirstOrDefault(t => t.Color == null); SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
} }
public IClipboardService ClipboardService { get; }
public RelayCommand CloseCommand { get; } public RelayCommand CloseCommand { get; }
public RelayCommand CloseSearchPanelCommand { get; } public RelayCommand CloseSearchPanelCommand { get; }
public string DefaultTitle { get; private set; } = string.Empty; public IDialogService DialogService { get; }
public IAsyncCommand EditTitleCommand { get; } public IAsyncCommand EditTitleCommand { get; }
@@ -108,8 +110,6 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand FindPreviousCommand { get; } public RelayCommand FindPreviousCommand { get; }
public bool Initialized { get; private set; }
public bool IsSelected public bool IsSelected
{ {
get => _isSelected; get => _isSelected;
@@ -119,8 +119,8 @@ namespace FluentTerminal.App.ViewModels
{ {
if (IsSelected) if (IsSelected)
{ {
_applicationView.Title = Title; ApplicationView.Title = Title ?? string.Empty;
_applicationView.RunOnDispatcherThread(() => NewOutput = false); NewOutput = false;
} }
RaisePropertyChanged(nameof(IsUnderlined)); RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme)); RaisePropertyChanged(nameof(BackgroundTabTheme));
@@ -151,6 +151,10 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand<string> SelectTabThemeCommand { get; } public RelayCommand<string> SelectTabThemeCommand { get; }
public ISettingsService SettingsService { get; }
public ShellProfile ShellProfile { get; }
public bool ShowResizeOverlay public bool ShowResizeOverlay
{ {
get => _showResizeOverlay; get => _showResizeOverlay;
@@ -174,6 +178,8 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _showSearchPanel, value); set => Set(ref _showSearchPanel, value);
} }
public string StartupDirectory { get; }
public TabTheme TabTheme public TabTheme TabTheme
{ {
get => _tabTheme; get => _tabTheme;
@@ -187,6 +193,8 @@ namespace FluentTerminal.App.ViewModels
public ObservableCollection<TabTheme> TabThemes { get; } public ObservableCollection<TabTheme> TabThemes { get; }
public Terminal Terminal { get; private set; }
public TerminalTheme TerminalTheme public TerminalTheme TerminalTheme
{ {
get => _terminalTheme; get => _terminalTheme;
@@ -198,134 +206,59 @@ namespace FluentTerminal.App.ViewModels
get => _title; get => _title;
set set
{ {
value = string.IsNullOrWhiteSpace(value) ? DefaultTitle : value;
if (Set(ref _title, value) && IsSelected) if (Set(ref _title, value) && IsSelected)
{ {
_applicationView.Title = Title; ApplicationView.Title = Title;
} }
} }
} }
public async Task CloseView() public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task Close()
{ {
_terminalView.Close(); return Terminal.Close();
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
} }
public void CopyText(string text) public void CopyText(string text)
{ {
_clipboardService.SetText(text); ClipboardService.SetText(text);
}
public Task FocusTerminal()
{
return _terminalView?.FocusTerminal();
} }
public async Task EditTitle() public async Task EditTitle()
{ {
var result = await _dialogService.ShowInputDialogAsync("Edit Title"); var result = await DialogService.ShowInputDialogAsync("Edit Title");
if (result != null) if (result != null)
{ {
Title = result; Title = result;
} }
} }
public async Task OnViewIsReady(ITerminalView terminalView) public void FocusTerminal()
{ {
_terminalView = terminalView; Activated?.Invoke(this, EventArgs.Empty);
var options = _settingsService.GetTerminalOptions();
var keyBindings = _settingsService.GetCommandKeyBindings();
var profiles = _settingsService.GetShellProfiles();
var settings = _settingsService.GetApplicationSettings();
var sessionType = SessionType.Unknown;
if (!_applicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7) || settings.AlwaysUseWinPty)
{
sessionType = SessionType.WinPty;
}
else
{
sessionType = SessionType.ConPty;
}
var size = await _terminalView.CreateTerminal(options, TerminalTheme.Colors, FlattenKeyBindings(keyBindings, profiles), sessionType).ConfigureAwait(true);
if (!string.IsNullOrWhiteSpace(_startupDirectory))
{
_shellProfile.WorkingDirectory = _startupDirectory;
}
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
_terminalId = response.Id;
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
{
_connectedEvent.Wait();
_buffer.Write(t);
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
_applicationView.RunOnDispatcherThread(() => NewOutput = true);
}
});
DefaultTitle = response.ShellExecutableName;
Title = DefaultTitle;
var webSocketUrl = "ws://127.0.0.1:" + response.Id;
var webSocketServer = new WebSocketServer(webSocketUrl);
webSocketServer.Start(socket =>
{
_webSocket = socket;
_buffer = new InputBuffer(_webSocket.Send);
socket.OnOpen = () => _connectedEvent.Set();
socket.OnMessage = message => _trayProcessCommunicationService.WriteText(_terminalId, message);
});
await _terminalView.ConnectToSocket(webSocketUrl).ConfigureAwait(true);
Initialized = true;
}
else
{
await _dialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
}
await FocusTerminal().ConfigureAwait(true);
} }
private void CloseSearchPanel() private void CloseSearchPanel()
{ {
SearchText = string.Empty; SearchText = string.Empty;
ShowSearchPanel = false; ShowSearchPanel = false;
_terminalView.FocusTerminal(); FocusTerminal();
} }
private Task FindNext() private void FindNext()
{ {
return _terminalView.FindNext(SearchText); FindNextRequested?.Invoke(this, SearchText);
} }
private Task FindPrevious() private void FindPrevious()
{ {
return _terminalView.FindPrevious(SearchText); FindPreviousRequested?.Invoke(this, SearchText);
}
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<string, ICollection<KeyBinding>> commandKeyBindings, IEnumerable<ShellProfile> profiles)
{
return commandKeyBindings.Values.SelectMany(k => k).Concat(profiles.SelectMany(x => x.KeyBindings));
} }
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e) private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{ {
await _applicationView.RunOnDispatcherThread(() => await ApplicationView.RunOnDispatcherThread(() =>
{ {
ApplicationSettings = e; ApplicationSettings = e;
RaisePropertyChanged(nameof(IsUnderlined)); RaisePropertyChanged(nameof(IsUnderlined));
@@ -335,73 +268,21 @@ namespace FluentTerminal.App.ViewModels
private async void OnCurrentThemeChanged(object sender, Guid e) private async void OnCurrentThemeChanged(object sender, Guid e)
{ {
await _applicationView.RunOnDispatcherThread(async () => await ApplicationView.RunOnDispatcherThread(() =>
{ {
// only change theme if not overwritten by profile // only change theme if not overwritten by profile
if (_shellProfile.TerminalThemeId == Guid.Empty) if (ShellProfile.TerminalThemeId == Guid.Empty)
{ {
var currentTheme = _settingsService.GetTheme(e); var currentTheme = SettingsService.GetTheme(e);
TerminalTheme = currentTheme; TerminalTheme = currentTheme;
await _terminalView.ChangeTheme(currentTheme.Colors).ConfigureAwait(true); ThemeChanged?.Invoke(this, currentTheme);
} }
}); });
} }
private async void OnKeyBindingsChanged(object sender, EventArgs e) private void OnKeyBindingsChanged(object sender, EventArgs e)
{ {
var keyBindings = _settingsService.GetCommandKeyBindings(); KeyBindingsChanged?.Invoke(this, EventArgs.Empty);
var profiles = _settingsService.GetShellProfiles();
await _applicationView.RunOnDispatcherThread(async () =>
{
await _terminalView.ChangeKeyBindings(FlattenKeyBindings(keyBindings, profiles)).ConfigureAwait(false);
});
}
private async void OnKeyboardCommandReceived(object sender, string command)
{
switch (command)
{
case nameof(Command.Copy):
{
var selection = await _terminalView.GetSelection().ConfigureAwait(true);
_clipboardService.SetText(selection);
break;
}
case nameof(Command.Paste):
{
string content = await _clipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = _shellProfile.TranslateLineEndings(content);
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
}
break;
}
case nameof(Command.PasteWithoutNewlines):
{
string content = await _clipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
}
break;
}
case nameof(Command.Search):
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
_terminalView.FocusSearchTextBox();
}
break;
}
default:
{
_keyboardCommandService.SendCommand(command);
break;
}
}
} }
private void OnResizeOverlayTimerFinished(object sender, object e) private void OnResizeOverlayTimerFinished(object sender, object e)
@@ -410,36 +291,9 @@ namespace FluentTerminal.App.ViewModels
ShowResizeOverlay = false; ShowResizeOverlay = false;
} }
private void OnTerminalExited(object sender, int e) private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{ {
if (e == _terminalId) OptionsChanged?.Invoke(this, e);
{
_applicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
}
}
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
await _applicationView.RunOnDispatcherThread(async () =>
{
await _terminalView.ChangeOptions(e).ConfigureAwait(true);
});
}
private async void OnTerminalSizeChanged(object sender, TerminalSize e)
{
if (!Initialized)
{
return;
}
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
ShowResizeOverlay = true;
await _trayProcessCommunicationService.ResizeTerminal(_terminalId, e).ConfigureAwait(true);
}
private void OnTerminalTitleChanged(object sender, string e)
{
Title = e;
} }
private void SelectTabTheme(string name) private void SelectTabTheme(string name)
@@ -447,11 +301,82 @@ namespace FluentTerminal.App.ViewModels
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name); TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
} }
private void Terminal_Closed(object sender, EventArgs e)
{
Closed?.Invoke(this, EventArgs.Empty);
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
{
switch (e)
{
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
ClipboardService.SetText(selection);
break;
}
case nameof(Command.Paste):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(content).ConfigureAwait(true);
}
break;
}
case nameof(Command.PasteWithoutNewlines):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await Terminal.Write(content).ConfigureAwait(true);
}
break;
}
case nameof(Command.Search):
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
SearchStarted?.Invoke(this, EventArgs.Empty);
}
break;
}
default:
{
_keyboardCommandService.SendCommand(e);
break;
}
}
}
private void Terminal_OutputReceived(object sender, string e)
{
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
NewOutput = true;
}
}
private void Terminal_SizeChanged(object sender, TerminalSize e)
{
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
ShowResizeOverlay = true;
}
private void Terminal_TitleChanged(object sender, string e)
{
Title = e;
}
private async Task TryClose() private async Task TryClose()
{ {
if (ApplicationSettings.ConfirmClosingTabs) if (ApplicationSettings.ConfirmClosingTabs)
{ {
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true); var result = await DialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.Cancel) if (result == DialogButton.Cancel)
{ {
@@ -459,8 +384,7 @@ namespace FluentTerminal.App.ViewModels
} }
} }
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true); await Close().ConfigureAwait(true);
Closed?.Invoke(this, EventArgs.Empty);
} }
} }
} }
+7
View File
@@ -49,6 +49,8 @@ namespace FluentTerminal.App
InitializeComponent(); InitializeComponent();
UnhandledException += OnUnhandledException;
var applicationDataContainers = new ApplicationDataContainers var applicationDataContainers = new ApplicationDataContainers
{ {
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings), LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
@@ -93,6 +95,11 @@ namespace FluentTerminal.App
_applicationSettings = _settingsService.GetApplicationSettings(); _applicationSettings = _settingsService.GetApplicationSettings();
} }
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
{
System.Diagnostics.Debug.WriteLine(e.Exception.ToString());
}
protected override async void OnActivated(IActivatedEventArgs args) protected override async void OnActivated(IActivatedEventArgs args)
{ {
if (args is CommandLineActivatedEventArgs commandLineActivated) if (args is CommandLineActivatedEventArgs commandLineActivated)
@@ -183,9 +183,16 @@
</Compile> </Compile>
<Compile Include="Views\NoValueTemplateSelector.cs" /> <Compile Include="Views\NoValueTemplateSelector.cs" />
<Compile Include="Views\TerminalThemeTemplateSelector.cs" /> <Compile Include="Views\TerminalThemeTemplateSelector.cs" />
<Compile Include="Views\ITerminalView.cs" />
<Compile Include="Views\TerminalView.xaml.cs"> <Compile Include="Views\TerminalView.xaml.cs">
<DependentUpon>TerminalView.xaml</DependentUpon> <DependentUpon>TerminalView.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Views\VtNetCoreTerminalView.xaml.cs">
<DependentUpon>VtNetCoreTerminalView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\XtermTerminalView.xaml.cs">
<DependentUpon>XtermTerminalView.xaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AppxManifest Include="Package.appxmanifest"> <AppxManifest Include="Package.appxmanifest">
@@ -350,6 +357,14 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </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>
</Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac"> <PackageReference Include="Autofac">
@@ -370,6 +385,9 @@
<PackageReference Include="Newtonsoft.Json"> <PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version> <Version>11.0.2</Version>
</PackageReference> </PackageReference>
<PackageReference Include="VtNetCore">
<Version>1.0.15</Version>
</PackageReference>
<PackageReference Include="Win2D.uwp"> <PackageReference Include="Win2D.uwp">
<Version>1.21.0</Version> <Version>1.21.0</Version>
</PackageReference> </PackageReference>
+17
View File
@@ -0,0 +1,17 @@
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Views
{
public interface ITerminalView
{
Task ChangeTheme(TerminalTheme theme);
Task ChangeKeyBindings();
Task ChangeOptions(TerminalOptions options);
Task Initialize(TerminalViewModel viewModel);
Task FindNext(string searchText);
Task FindPrevious(string searchText);
Task FocusTerminal();
}
}
+2 -2
View File
@@ -62,11 +62,11 @@ namespace FluentTerminal.App.Views
} }
} }
private async void OnWindowActivated(object sender, WindowActivatedEventArgs e) private void OnWindowActivated(object sender, WindowActivatedEventArgs e)
{ {
if (e.WindowActivationState != CoreWindowActivationState.Deactivated && TerminalContainer.Content is TerminalView terminal) if (e.WindowActivationState != CoreWindowActivationState.Deactivated && TerminalContainer.Content is TerminalView terminal)
{ {
await terminal.FocusWebView().ConfigureAwait(true); terminal.ViewModel.FocusTerminal();
} }
} }
+2 -2
View File
@@ -12,7 +12,7 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid x:Name="WebViewContainer" Grid.Row="0" /> <Grid x:Name="TerminalContainer" Grid.Row="0" />
<Grid <Grid
Grid.Row="0" Grid.Row="0"
Margin="12" Margin="12"
@@ -38,7 +38,7 @@
Name="SearchTextBox" Name="SearchTextBox"
Width="300" Width="300"
Margin="6,0,6,0" Margin="6,0,6,0"
KeyUp="SearchTextBox_KeyUp" KeyUp="OnSearchTextBoxKeyUp"
PlaceholderText="Enter search term" PlaceholderText="Enter search term"
RelativePanel.RightOf="SearchLabel" RelativePanel.RightOf="SearchLabel"
Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
+30 -258
View File
@@ -1,248 +1,68 @@
using FluentTerminal.App.Services; using FluentTerminal.App.ViewModels;
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.RuntimeComponent.Enums;
using FluentTerminal.RuntimeComponent.Interfaces;
using FluentTerminal.RuntimeComponent.WebAllowedObjects;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
using Windows.System; using Windows.System;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views namespace FluentTerminal.App.Views
{ {
public sealed partial class TerminalView : UserControl, ITerminalView, ITerminalEventListener public sealed partial class TerminalView : UserControl
{ {
private MenuFlyoutItem _copyMenuItem; private readonly ITerminalView _terminalView;
private BlockingCollection<Action> _dispatcherJobs;
private readonly SemaphoreSlim _loaded;
private MenuFlyoutItem _pasteMenuItem;
private WebView _webView;
public TerminalView(TerminalViewModel viewModel) public TerminalView(TerminalViewModel viewModel)
{ {
ViewModel = viewModel; ViewModel = viewModel;
ViewModel.SearchStarted += OnSearchStarted;
ViewModel.Activated += OnActivated;
ViewModel.ThemeChanged += OnThemeChanged;
ViewModel.OptionsChanged += OnOptionsChanged;
ViewModel.KeyBindingsChanged += OnKeyBindingsChanged;
ViewModel.FindNextRequested += OnFindNextRequested;
ViewModel.FindPreviousRequested += OnFindPreviousRequested;
InitializeComponent(); InitializeComponent();
Loaded += OnLoaded; //_terminalView = new XtermTerminalView();
_loaded = new SemaphoreSlim(0, 1); _terminalView = new VtNetCoreTerminalView();
TerminalContainer.Children.Add((UIElement)_terminalView);
JsonConvert.DefaultSettings = () => _terminalView.Initialize(ViewModel);
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(true));
return settings;
};
StartMediatorTask();
} }
public event EventHandler<string> KeyboardCommandReceived;
public event EventHandler<TerminalSize> TerminalSizeChanged;
public event EventHandler<string> TerminalTitleChanged;
public TerminalViewModel ViewModel { get; } public TerminalViewModel ViewModel { get; }
public Task ChangeOptions(TerminalOptions options) private async void OnActivated(object sender, EventArgs e)
{ {
var serialized = JsonConvert.SerializeObject(options); if (_terminalView != null)
return ExecuteScriptAsync($"changeOptions('{serialized}')");
}
public Task ChangeTheme(TerminalColors theme)
{
var serialized = JsonConvert.SerializeObject(theme);
return ExecuteScriptAsync($"changeTheme('{serialized}')");
}
public Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings)
{
var serialized = JsonConvert.SerializeObject(keyBindings);
return ExecuteScriptAsync($"changeKeyBindings('{serialized}')");
}
public void Close()
{
_webView?.Navigate(new Uri("about:blank"));
_webView = null;
}
public Task ConnectToSocket(string url)
{
return ExecuteScriptAsync($"connectToWebSocket('{url}');");
}
public async Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType)
{
var serializedOptions = JsonConvert.SerializeObject(options);
var serializedTheme = JsonConvert.SerializeObject(theme);
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}', '{sessionType}')").ConfigureAwait(true);
return JsonConvert.DeserializeObject<TerminalSize>(size);
}
public Task<string> ExecuteScriptAsync(string script)
{
try
{ {
return _webView.InvokeScriptAsync("eval", new[] { script }).AsTask(); await _terminalView.FocusTerminal().ConfigureAwait(true);
}
catch (Exception e)
{
Debug.WriteLine($"Exception while running:\n \"{script}\"\n\n {e}");
}
return Task.FromResult(string.Empty);
}
public async Task FocusTerminal()
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
} }
} }
public async Task FocusWebView() private async void OnFindNextRequested(object sender, string e)
{ {
if (_webView != null) await _terminalView.FindNext(e).ConfigureAwait(true);
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
}
} }
public Task<string> GetSelection() => ExecuteScriptAsync("term.getSelection()"); private async void OnFindPreviousRequested(object sender, string e)
public void OnKeyboardCommand(string command)
{ {
if (Enum.TryParse(command, true, out Command commandValue)) await _terminalView.FindPrevious(e).ConfigureAwait(true);
{
_dispatcherJobs.Add(() => KeyboardCommandReceived?.Invoke(this, commandValue.ToString()));
}
else if (Guid.TryParse(command, out Guid shellProfileId))
{
_dispatcherJobs.Add(() => KeyboardCommandReceived?.Invoke(this, shellProfileId.ToString()));
}
} }
public void OnTerminalResized(int columns, int rows) private async void OnKeyBindingsChanged(object sender, EventArgs e)
{ {
_dispatcherJobs.Add(() => TerminalSizeChanged?.Invoke(this, new TerminalSize { Columns = columns, Rows = rows })); await _terminalView.ChangeKeyBindings().ConfigureAwait(true);
} }
public void OnTitleChanged(string title) private async void OnOptionsChanged(object sender, Models.TerminalOptions e)
{ {
_dispatcherJobs.Add(() => TerminalTitleChanged?.Invoke(this, title)); await _terminalView.ChangeOptions(e).ConfigureAwait(true);
} }
public Task FindNext(string searchText) private void OnSearchStarted(object sender, EventArgs e)
{ {
return ExecuteScriptAsync($"findNext('{searchText}')"); SearchTextBox.Focus(FocusState.Programmatic);
} }
public Task FindPrevious(string searchText) private void OnSearchTextBoxKeyUp(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
{
return ExecuteScriptAsync($"findPrevious('{searchText}')");
}
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
{
_loaded.Release();
}
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
var bridge = new TerminalBridge(this);
_webView.AddWebAllowedObject("terminalBridge", bridge);
}
private void Copy_Click(object sender, RoutedEventArgs e)
{
OnKeyboardCommand(nameof(Command.Copy));
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
if (WebViewContainer.Children.Any())
{
return;
}
_webView = new WebView(WebViewExecutionMode.SeparateThread)
{
DefaultBackgroundColor = Colors.Transparent
};
WebViewContainer.Children.Add(_webView);
_webView.NavigationCompleted += _webView_NavigationCompleted;
_webView.NavigationStarting += _webView_NavigationStarting;
_copyMenuItem = new MenuFlyoutItem { Text = "Copy" };
_copyMenuItem.Click += Copy_Click;
_pasteMenuItem = new MenuFlyoutItem { Text = "Paste" };
_pasteMenuItem.Click += Paste_Click;
_webView.ContextFlyout = new MenuFlyout
{
Items = { _copyMenuItem, _pasteMenuItem }
};
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
await _loaded.WaitAsync().ConfigureAwait(true);
_webView.Focus(FocusState.Programmatic);
await ViewModel.OnViewIsReady(this).ConfigureAwait(true);
}
private void Paste_Click(object sender, RoutedEventArgs e)
{
OnKeyboardCommand(nameof(Command.Paste));
}
private void StartMediatorTask()
{
_dispatcherJobs = new BlockingCollection<Action>();
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
Task.Run(async () =>
{
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
{
try
{
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => job.Invoke());
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
});
}
private void SearchTextBox_KeyUp(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
{ {
if (e.Key == VirtualKey.Escape) if (e.Key == VirtualKey.Escape)
{ {
@@ -254,57 +74,9 @@ namespace FluentTerminal.App.Views
} }
} }
public void FocusSearchTextBox() private async void OnThemeChanged(object sender, Models.TerminalTheme e)
{ {
SearchTextBox.Focus(FocusState.Programmatic); await _terminalView.ChangeTheme(e).ConfigureAwait(true);
}
public void OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection)
{
_dispatcherJobs.Add(() =>
{
if (mouseButton == MouseButton.Middle)
{
if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.Paste)
{
OnKeyboardCommand(nameof(Command.Paste));
}
}
else if (mouseButton == MouseButton.Right)
{
if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.Paste)
{
OnKeyboardCommand(nameof(Command.Paste));
}
}
});
}
public async void OnSelectionChanged(string selection)
{
if (ViewModel.ApplicationSettings.CopyOnSelect && !ViewModel.ShowSearchPanel)
{
_dispatcherJobs.Add(async () =>
{
ViewModel.CopyText(selection);
await ExecuteScriptAsync("term.clearSelection()");
});
}
}
private void ShowContextMenu(int x, int y, bool terminalHasSelection)
{
var flyout = (MenuFlyout)_webView.ContextFlyout;
_copyMenuItem.IsEnabled = terminalHasSelection;
flyout.ShowAt(_webView, new Point(x, y));
} }
} }
} }
@@ -0,0 +1,15 @@
<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>
@@ -0,0 +1,660 @@
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 };
}
}
}
@@ -0,0 +1,12 @@
<UserControl
x:Class="FluentTerminal.App.Views.XtermTerminalView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:FluentTerminal.App.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
<Grid x:Name="Root" />
</UserControl>
@@ -0,0 +1,350 @@
using Fleck;
using FluentTerminal.App.ViewModels;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.RuntimeComponent.Enums;
using FluentTerminal.RuntimeComponent.Interfaces;
using FluentTerminal.RuntimeComponent.WebAllowedObjects;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public sealed partial class XtermTerminalView : UserControl, IxtermEventListener, ITerminalView
{
private readonly ManualResetEventSlim _connectedEvent;
private readonly SemaphoreSlim _loaded;
private bool _alreadyLoaded;
private InputBuffer _buffer;
private MenuFlyoutItem _copyMenuItem;
private BlockingCollection<Action> _dispatcherJobs;
private MenuFlyoutItem _pasteMenuItem;
private IWebSocketConnection _webSocket;
private WebView _webView;
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;
}
public TerminalViewModel ViewModel { get; private set; }
public Task ChangeKeyBindings()
{
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
var profiles = ViewModel.SettingsService.GetShellProfiles();
var serialized = JsonConvert.SerializeObject(FlattenKeyBindings(keyBindings, profiles));
return ExecuteScriptAsync($"changeKeyBindings('{serialized}')");
}
public Task ChangeOptions(TerminalOptions options)
{
var serialized = JsonConvert.SerializeObject(options);
return ExecuteScriptAsync($"changeOptions('{serialized}')");
}
public Task ChangeTheme(TerminalTheme theme)
{
var serialized = JsonConvert.SerializeObject(theme.Colors);
return ExecuteScriptAsync($"changeTheme('{serialized}')");
}
public Task FindNext(string searchText)
{
return ExecuteScriptAsync($"findNext('{searchText}')");
}
public Task FindPrevious(string searchText)
{
return ExecuteScriptAsync($"findPrevious('{searchText}')");
}
public async Task FocusTerminal()
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
}
}
public async Task Initialize(TerminalViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
ViewModel.Terminal.RegisterSelectedTextCallback(() => ExecuteScriptAsync("term.getSelection()"));
ViewModel.Terminal.Closed += Terminal_Closed;
var options = ViewModel.SettingsService.GetTerminalOptions();
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
var profiles = ViewModel.SettingsService.GetShellProfiles();
var settings = ViewModel.SettingsService.GetApplicationSettings();
var theme = ViewModel.TerminalTheme;
var sessionType = SessionType.Unknown;
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
{
sessionType = SessionType.WinPty;
}
else
{
sessionType = SessionType.ConPty;
}
await _loaded.WaitAsync().ConfigureAwait(true);
var size = await CreateXtermView(options, theme.Colors, FlattenKeyBindings(keyBindings, profiles), sessionType).ConfigureAwait(true);
var port = await ViewModel.TrayProcessCommunicationService.GetAvailablePort().ConfigureAwait(true);
await CreateWebSocketServer(port.Port).ConfigureAwait(true);
_connectedEvent.Wait();
await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType).ConfigureAwait(true);
_webView.Focus(FocusState.Programmatic);
}
void IxtermEventListener.OnKeyboardCommand(string command)
{
if (Enum.TryParse(command, true, out Command commandValue))
{
_dispatcherJobs.Add(() => ViewModel.Terminal.ProcessKeyboardCommand(commandValue.ToString()));
}
else if (Guid.TryParse(command, out Guid shellProfileId))
{
_dispatcherJobs.Add(() => ViewModel.Terminal.ProcessKeyboardCommand(shellProfileId.ToString()));
}
}
void IxtermEventListener.OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection)
{
_dispatcherJobs.Add(() =>
{
if (mouseButton == MouseButton.Middle)
{
if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.Paste)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
}
}
else if (mouseButton == MouseButton.Right)
{
if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.Paste)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
}
}
});
}
void IxtermEventListener.OnSelectionChanged(string selection)
{
if (ViewModel.ApplicationSettings.CopyOnSelect && !ViewModel.ShowSearchPanel)
{
_dispatcherJobs.Add(async () =>
{
ViewModel.CopyText(selection);
await ExecuteScriptAsync("term.clearSelection()");
});
}
}
void IxtermEventListener.OnTerminalResized(int columns, int rows)
{
if (_connectedEvent.IsSet) // only propagate after xterm.js is finished with fitting
{
_dispatcherJobs.Add(async () => await ViewModel.Terminal.SetSize(new TerminalSize { Columns = columns, Rows = rows }).ConfigureAwait(true));
}
}
void IxtermEventListener.OnTitleChanged(string title)
{
_dispatcherJobs.Add(() => ViewModel.Terminal.SetTitle(title));
}
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
{
_loaded.Release();
}
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
var bridge = new TerminalBridge(this);
_webView.AddWebAllowedObject("terminalBridge", bridge);
}
private void Copy_Click(object sender, RoutedEventArgs e)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Copy));
}
private async Task CreateWebSocketServer(int port)
{
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);
});
await ExecuteScriptAsync($"connectToWebSocket('{webSocketUrl}');").ConfigureAwait(true);
}
private async Task<TerminalSize> CreateXtermView(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType)
{
var serializedOptions = JsonConvert.SerializeObject(options);
var serializedTheme = JsonConvert.SerializeObject(theme);
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}', '{sessionType}')").ConfigureAwait(true);
return JsonConvert.DeserializeObject<TerminalSize>(size);
}
private Task<string> ExecuteScriptAsync(string script)
{
try
{
return _webView.InvokeScriptAsync("eval", new[] { script }).AsTask();
}
catch (Exception e)
{
Debug.WriteLine($"Exception while running:\n \"{script}\"\n\n {e}");
}
return Task.FromResult(string.Empty);
}
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<string, ICollection<KeyBinding>> commandKeyBindings, IEnumerable<ShellProfile> profiles)
{
return commandKeyBindings.Values.SelectMany(k => k).Concat(profiles.SelectMany(x => x.KeyBindings));
}
private void Paste_Click(object sender, RoutedEventArgs e)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
}
private void ShowContextMenu(int x, int y, bool terminalHasSelection)
{
var flyout = (MenuFlyout)_webView.ContextFlyout;
_copyMenuItem.IsEnabled = terminalHasSelection;
flyout.ShowAt(_webView, new Point(x, y));
}
private void StartMediatorTask()
{
_dispatcherJobs = new BlockingCollection<Action>();
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
Task.Run(async () =>
{
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
{
try
{
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => job.Invoke());
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
});
}
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);
}
private void Terminal_OutputReceived(object sender, string e)
{
_webSocket.Send(e);
}
private async void XtermTerminalView_GotFocus(object sender, RoutedEventArgs e)
{
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;
}
}
}
@@ -4,6 +4,7 @@ namespace FluentTerminal.Models.Requests
{ {
public class CreateTerminalRequest public class CreateTerminalRequest
{ {
public int Id { get; set; }
public TerminalSize Size { get; set; } public TerminalSize Size { get; set; }
public ShellProfile Profile { get; set; } public ShellProfile Profile { get; set; }
public SessionType SessionType { get; set; } public SessionType SessionType { get; set; }
@@ -0,0 +1,6 @@
namespace FluentTerminal.Models.Requests
{
public class GetAvailablePortRequest
{
}
}
@@ -2,7 +2,6 @@
{ {
public class CreateTerminalResponse public class CreateTerminalResponse
{ {
public int Id { get; set; }
public bool Success { get; set; } public bool Success { get; set; }
public string Error { get; set; } public string Error { get; set; }
public string ShellExecutableName { get; set; } public string ShellExecutableName { get; set; }
@@ -0,0 +1,7 @@
namespace FluentTerminal.Models.Responses
{
public class GetAvailablePortResponse
{
public int Port { get; set; }
}
}
+3 -3
View File
@@ -27,9 +27,9 @@ namespace FluentTerminal.Models
{ {
if (obj is TerminalTheme other) if (obj is TerminalTheme other)
{ {
return other.Name.Equals(Name) return Equals(other.Name, Name)
&& other.Author == null ? Author == null : other.Author.Equals(Author) && Equals(other.Author, Author)
&& other.Colors.Equals(Colors); && Equals(other.Colors, Colors);
} }
return false; return false;
} }
@@ -108,7 +108,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Enums\MouseButton.cs" /> <Compile Include="Enums\MouseButton.cs" />
<Compile Include="Interfaces\ITerminalEventListener.cs" /> <Compile Include="Interfaces\IxtermEventListener.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="WebAllowedObjects\TerminalBridge.cs" /> <Compile Include="WebAllowedObjects\TerminalBridge.cs" />
</ItemGroup> </ItemGroup>
@@ -2,7 +2,7 @@
namespace FluentTerminal.RuntimeComponent.Interfaces namespace FluentTerminal.RuntimeComponent.Interfaces
{ {
public interface ITerminalEventListener public interface IxtermEventListener
{ {
void OnTerminalResized(int columns, int rows); void OnTerminalResized(int columns, int rows);
@@ -7,9 +7,9 @@ namespace FluentTerminal.RuntimeComponent.WebAllowedObjects
[AllowForWeb] [AllowForWeb]
public sealed class TerminalBridge public sealed class TerminalBridge
{ {
private readonly ITerminalEventListener _terminalEventListener; private readonly IxtermEventListener _terminalEventListener;
public TerminalBridge(ITerminalEventListener terminalEventListener) public TerminalBridge(IxtermEventListener terminalEventListener)
{ {
_terminalEventListener = terminalEventListener; _terminalEventListener = terminalEventListener;
} }
@@ -7,6 +7,7 @@ using Windows.ApplicationModel;
using Windows.ApplicationModel.AppService; using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections; using Windows.Foundation.Collections;
using System; using System;
using FluentTerminal.Models.Responses;
namespace FluentTerminal.SystemTray.Services namespace FluentTerminal.SystemTray.Services
{ {
@@ -111,6 +112,16 @@ namespace FluentTerminal.SystemTray.Services
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent); var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
_terminalsManager.CloseTerminal(request.TerminalId); _terminalsManager.CloseTerminal(request.TerminalId);
} }
else if (messageType == nameof(GetAvailablePortRequest))
{
var deferral = args.GetDeferral();
var response = new GetAvailablePortResponse { Port = Utilities.GetAvailablePort().Value };
await args.Request.SendResponseAsync(CreateMessage(response));
deferral.Complete();
}
} }
private ValueSet CreateMessage(object content) private ValueSet CreateMessage(object content)
@@ -12,6 +12,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty
private Terminal _terminal; private Terminal _terminal;
public int Id { get; private set; } public int Id { get; private set; }
public string ShellExecutableName { get; private set; } public string ShellExecutableName { get; private set; }
public event EventHandler ConnectionClosed; public event EventHandler ConnectionClosed;
@@ -28,16 +29,9 @@ namespace FluentTerminal.SystemTray.Services.ConPty
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager) public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
{ {
Id = request.Id;
_terminalsManager = terminalsManager; _terminalsManager = terminalsManager;
var port = Utilities.GetAvailablePort();
if (port == null)
{
throw new Exception("no port available");
}
Id = port.Value;
ShellExecutableName = Path.GetFileNameWithoutExtension(request.Profile.Location); ShellExecutableName = Path.GetFileNameWithoutExtension(request.Profile.Location);
var cwd = GetWorkingDirectory(request.Profile); var cwd = GetWorkingDirectory(request.Profile);
string args = $"\"{request.Profile.Location}\" {request.Profile.Arguments}"; string args = $"\"{request.Profile.Location}\" {request.Profile.Arguments}";
@@ -59,7 +59,6 @@ namespace FluentTerminal.SystemTray.Services
return new CreateTerminalResponse return new CreateTerminalResponse
{ {
Success = true, Success = true,
Id = terminal.Id,
ShellExecutableName = terminal.ShellExecutableName ShellExecutableName = terminal.ShellExecutableName
}; };
} }
@@ -19,6 +19,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager) public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
{ {
Id = request.Id;
_terminalsManager = terminalsManager; _terminalsManager = terminalsManager;
var configHandle = IntPtr.Zero; var configHandle = IntPtr.Zero;
@@ -62,15 +63,6 @@ namespace FluentTerminal.SystemTray.Services.WinPty
winpty_error_free(errorHandle); winpty_error_free(errorHandle);
} }
var port = Utilities.GetAvailablePort();
if (port == null)
{
throw new Exception("no port available");
}
Id = port.Value;
ListenToStdOut(); ListenToStdOut();
} }
+4
View File
@@ -10,6 +10,7 @@ namespace FluentTerminal.SystemTray
public static class Utilities public static class Utilities
{ {
private const int FirstDynamicPort = 49151; private const int FirstDynamicPort = 49151;
private static readonly List<int> _sentOutPorts = new List<int>();
public static int? GetAvailablePort() public static int? GetAvailablePort()
{ {
@@ -26,12 +27,15 @@ namespace FluentTerminal.SystemTray
endPoints = properties.GetActiveUdpListeners(); endPoints = properties.GetActiveUdpListeners();
usedPorts.AddRange(endPoints.Where(e => e.Port >= FirstDynamicPort).Select(e => e.Port)); usedPorts.AddRange(endPoints.Where(e => e.Port >= FirstDynamicPort).Select(e => e.Port));
usedPorts.AddRange(_sentOutPorts);
usedPorts.Sort(); usedPorts.Sort();
for (var i = FirstDynamicPort; i < UInt16.MaxValue; i++) for (var i = FirstDynamicPort; i < UInt16.MaxValue; i++)
{ {
if (!usedPorts.Contains(i)) if (!usedPorts.Contains(i))
{ {
_sentOutPorts.Add(i);
return i; return i;
} }
} }