Compare commits
3
Commits
722
...
Dev/UWPRenderer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0733b1ab02 | ||
|
|
4e54668092 | ||
|
|
74abd4f448 |
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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}" />
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user