From 8b18773da931ea630a25e21bb3dab818e4d79fef Mon Sep 17 00:00:00 2001 From: Max Risuhin Date: Wed, 31 Jul 2019 12:16:22 -0700 Subject: [PATCH] Web browser like tabs (#467) * Web browser like tabs. * Turn on AlwaysShowTabs by default. * removed unused event handler --- FluentTerminal.App.Services/Constants.cs | 1 + .../ITrayProcessCommunicationService.cs | 2 + .../Implementation/DefaultValueProvider.cs | 2 +- .../TrayProcessCommunicationService.cs | 12 +++ FluentTerminal.App.Services/Terminal.cs | 36 ++++++--- .../ITerminalView.cs | 6 +- .../MainViewModel.cs | 21 ++++- .../TerminalViewModel.cs | 76 ++++++++++++++++++- FluentTerminal.App/App.xaml.cs | 11 +++ FluentTerminal.App/FluentTerminal.App.csproj | 1 - FluentTerminal.App/Views/MainPage.xaml | 10 ++- FluentTerminal.App/Views/MainPage.xaml.cs | 33 ++++++++ FluentTerminal.App/Views/TabBar.xaml | 10 ++- FluentTerminal.App/Views/TabBar.xaml.cs | 67 +++++++++++++++- FluentTerminal.App/Views/TerminalView.xaml.cs | 1 + .../Views/XtermTerminalView.xaml.cs | 7 +- FluentTerminal.Client/src/index.ts | 12 ++- .../src/SerializeAddon.ts | 44 +++++++++++ .../Requests/PauseTerminalOutputRequest.cs | 13 ++++ .../Responses/CreateTerminalResponse.cs | 4 +- .../Responses/PauseTerminalOutputResponse.cs | 9 +++ .../Responses/TerminalResponse.cs | 12 +++ .../Services/AppCommunicationService.cs | 15 ++++ .../Services/ConPty/ConPtySession.cs | 11 +++ .../Services/ITerminalSession.cs | 1 + .../Services/TerminalsManager.cs | 13 ++++ .../Services/WinPty/WinPtySession.cs | 11 +++ 27 files changed, 410 insertions(+), 31 deletions(-) rename {FluentTerminal.App/Views => FluentTerminal.App.ViewModels}/ITerminalView.cs (76%) create mode 100644 FluentTerminal.Client/src/xterm-addon-serialize/src/SerializeAddon.ts create mode 100644 FluentTerminal.Models/Requests/PauseTerminalOutputRequest.cs create mode 100644 FluentTerminal.Models/Responses/PauseTerminalOutputResponse.cs create mode 100644 FluentTerminal.Models/Responses/TerminalResponse.cs diff --git a/FluentTerminal.App.Services/Constants.cs b/FluentTerminal.App.Services/Constants.cs index 83ceaa9..2b408dc 100644 --- a/FluentTerminal.App.Services/Constants.cs +++ b/FluentTerminal.App.Services/Constants.cs @@ -10,5 +10,6 @@ public const string SshCommandName = "ssh"; public const string MoshCommandName = "mosh"; public const string ExecutedCommandsContainerName = "HistoryContainer"; + public const string TerminalViewModelStateId = "TerminalViewModelState"; } } diff --git a/FluentTerminal.App.Services/ITrayProcessCommunicationService.cs b/FluentTerminal.App.Services/ITrayProcessCommunicationService.cs index f92c47c..48e2fec 100644 --- a/FluentTerminal.App.Services/ITrayProcessCommunicationService.cs +++ b/FluentTerminal.App.Services/ITrayProcessCommunicationService.cs @@ -14,6 +14,8 @@ namespace FluentTerminal.App.Services Task CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType); + Task PauseTerminalOutput(byte id, bool pause); + Task ResizeTerminal(byte id, TerminalSize size); Task UpdateToggleWindowKeyBindings(); diff --git a/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs b/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs index f651926..324d877 100644 --- a/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs +++ b/FluentTerminal.App.Services/Implementation/DefaultValueProvider.cs @@ -33,7 +33,7 @@ namespace FluentTerminal.App.Services.Implementation CopyOnSelect = false, MouseMiddleClickAction = MouseAction.None, MouseRightClickAction = MouseAction.ContextMenu, - AlwaysShowTabs = false, + AlwaysShowTabs = true, ShowNewOutputIndicator = false, EnableTrayIcon = true, ShowCustomTitleInTitlebar = true, diff --git a/FluentTerminal.App.Services/Implementation/TrayProcessCommunicationService.cs b/FluentTerminal.App.Services/Implementation/TrayProcessCommunicationService.cs index d003258..edf72fe 100644 --- a/FluentTerminal.App.Services/Implementation/TrayProcessCommunicationService.cs +++ b/FluentTerminal.App.Services/Implementation/TrayProcessCommunicationService.cs @@ -168,6 +168,18 @@ namespace FluentTerminal.App.Services.Implementation return response; } + public async Task PauseTerminalOutput(byte id, bool pause) + { + var request = new PauseTerminalOutputRequest + { + Id = id, + Pause = pause + }; + + var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request)); + return JsonConvert.DeserializeObject((string)responseMessage[MessageKeys.Content]); + } + public void Initialize(IAppServiceConnection appServiceConnection) { _appServiceConnection = appServiceConnection; diff --git a/FluentTerminal.App.Services/Terminal.cs b/FluentTerminal.App.Services/Terminal.cs index 4e081d6..4fc4f33 100644 --- a/FluentTerminal.App.Services/Terminal.cs +++ b/FluentTerminal.App.Services/Terminal.cs @@ -2,6 +2,7 @@ using FluentTerminal.Models.Enums; using FluentTerminal.Models.Responses; using System; +using System.Text; using System.Threading.Tasks; namespace FluentTerminal.App.Services @@ -12,12 +13,14 @@ namespace FluentTerminal.App.Services private Func> _selectedTextCallback; private bool _closingFromUI = false; private bool _exited = false; + private bool _requireShellProcessStart = true; - public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService) + public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null) { _trayProcessCommunicationService = trayProcessCommunicationService; _trayProcessCommunicationService.TerminalExited += OnTerminalExited; - Id = _trayProcessCommunicationService.GetNextTerminalId(); + Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId(); + _requireShellProcessStart = !terminalId.HasValue; } private void OnTerminalExited(object sender, TerminalExitStatus status) @@ -133,17 +136,30 @@ namespace FluentTerminal.App.Services /// /// /// - public async Task StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType) + public async Task StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState) { - _trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t)); - var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true); - - if (response.Success) + if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState)) { - FallbackTitle = response.ShellExecutableName; - SetTitle(FallbackTitle); + OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState)); + } + + _trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t)); + + if (_requireShellProcessStart) + { + var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true); + + if (response.Success) + { + FallbackTitle = response.ShellExecutableName; + SetTitle(FallbackTitle); + } + return response; + } + else + { + return await _trayProcessCommunicationService.PauseTerminalOutput(Id, false); } - return response; } public Task Write(byte[] data) diff --git a/FluentTerminal.App/Views/ITerminalView.cs b/FluentTerminal.App.ViewModels/ITerminalView.cs similarity index 76% rename from FluentTerminal.App/Views/ITerminalView.cs rename to FluentTerminal.App.ViewModels/ITerminalView.cs index e98394a..f50f7a5 100644 --- a/FluentTerminal.App/Views/ITerminalView.cs +++ b/FluentTerminal.App.ViewModels/ITerminalView.cs @@ -1,8 +1,7 @@ -using FluentTerminal.App.ViewModels; -using FluentTerminal.Models; +using FluentTerminal.Models; using System.Threading.Tasks; -namespace FluentTerminal.App.Views +namespace FluentTerminal.App.ViewModels { public interface ITerminalView { @@ -13,5 +12,6 @@ namespace FluentTerminal.App.Views Task FindNext(string searchText); Task FindPrevious(string searchText); Task FocusTerminal(); + Task SerializeXtermState(); } } diff --git a/FluentTerminal.App.ViewModels/MainViewModel.cs b/FluentTerminal.App.ViewModels/MainViewModel.cs index d381d0a..805f210 100644 --- a/FluentTerminal.App.ViewModels/MainViewModel.cs +++ b/FluentTerminal.App.ViewModels/MainViewModel.cs @@ -141,6 +141,13 @@ namespace FluentTerminal.App.ViewModels public event EventHandler ActivatedMv; + public event EventHandler TabTearedOff; + + public void TearOffTab(TerminalViewModel model) + { + TabTearedOff?.Invoke(this, model); + } + public void FocusWindow() { ActivatedMv?.Invoke(this, EventArgs.Empty); @@ -313,12 +320,12 @@ namespace FluentTerminal.App.ViewModels } } - public Task AddTerminalAsync(ShellProfile profile) + public Task AddTerminalAsync(ShellProfile profile, string terminalState) { return ApplicationView.RunOnDispatcherThread(() => { var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService, - _applicationSettings, profile, ApplicationView, _dispatcherTimer, _clipboardService); + _applicationSettings, profile, ApplicationView, _dispatcherTimer, _clipboardService, terminalState); terminal.Closed += OnTerminalClosed; terminal.ShellTitleChanged += Terminal_ShellTitleChanged; @@ -329,6 +336,16 @@ namespace FluentTerminal.App.ViewModels }); } + public Task AddTerminalAsync(ShellProfile profile) + { + return AddTerminalAsync(profile, ""); + } + + public Task AddTerminalAsync(string terminalState) + { + return AddTerminalAsync(new ShellProfile(), terminalState); + } + private void Terminal_CustomTitleChanged(object sender, string e) { if (sender is TerminalViewModel terminal) diff --git a/FluentTerminal.App.ViewModels/TerminalViewModel.cs b/FluentTerminal.App.ViewModels/TerminalViewModel.cs index fa83f1d..c54a92a 100644 --- a/FluentTerminal.App.ViewModels/TerminalViewModel.cs +++ b/FluentTerminal.App.ViewModels/TerminalViewModel.cs @@ -5,6 +5,7 @@ using FluentTerminal.Models; using FluentTerminal.Models.Enums; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; +using Newtonsoft.Json; using System; using System.Collections.ObjectModel; using System.Linq; @@ -22,6 +23,60 @@ namespace FluentTerminal.App.ViewModels #endregion Static + #region Serialize terminal state + private class TerminalState + { + public bool HasCustomTitle { get; set; } + public string ShellTitle { get; set; } + public string TabTitle { get; set; } + public TerminalTheme TerminalTheme { get; set; } + public TabTheme TabTheme { get; set; } + public bool ShowSearchPanel { get; set; } + public string SearchText { get; set; } + public string XtermBufferState { get; set; } + public byte TerminalId { get; set; } + public ShellProfile ShellProfile { get; set; } + } + + public async Task Serialize() + { + TerminalState state = new TerminalState + { + HasCustomTitle = _hasCustomTitle, + ShellTitle = ShellTitle, + TabTitle = TabTitle, + TerminalTheme = TerminalTheme, + TabTheme = TabTheme, + ShowSearchPanel = ShowSearchPanel, + SearchText = SearchText, + XtermBufferState = await SerializeXtermState(), + TerminalId = Terminal.Id, + ShellProfile = ShellProfile + }; + + return JsonConvert.SerializeObject(state); + } + + public void Restore(string data) + { + TerminalState state = JsonConvert.DeserializeObject(data); + if (state != null) + { + _hasCustomTitle = state.HasCustomTitle; + ShellTitle = state.ShellTitle; + TabTitle = state.TabTitle; + TerminalTheme = state.TerminalTheme; + TabTheme = state.TabTheme; + ShowSearchPanel = state.ShowSearchPanel; + SearchText = state.SearchText; + XtermBufferState = state.XtermBufferState; + _terminalId = state.TerminalId; + ShellProfile = state.ShellProfile; + } + } + + #endregion Serialize terminal state + private readonly IKeyboardCommandService _keyboardCommandService; private bool _isSelected; private bool _hasNewOutput; @@ -34,10 +89,11 @@ namespace FluentTerminal.App.ViewModels private string _tabTitle; private string _shellTitle; private bool _hasCustomTitle; + private byte? _terminalId; public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile, - IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService) + IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, string terminalState = null) { SettingsService = settingsService; SettingsService.CurrentThemeChanged += OnCurrentThemeChanged; @@ -68,7 +124,12 @@ namespace FluentTerminal.App.ViewModels SelectTabThemeCommand = new RelayCommand(SelectTabTheme); EditTitleCommand = new AsyncCommand(EditTitle); - Terminal = new Terminal(TrayProcessCommunicationService); + if (!String.IsNullOrEmpty(terminalState)) + { + Restore(terminalState); + } + + Terminal = new Terminal(TrayProcessCommunicationService, _terminalId); Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived; Terminal.OutputReceived += Terminal_OutputReceived; Terminal.SizeChanged += Terminal_SizeChanged; @@ -106,6 +167,8 @@ namespace FluentTerminal.App.ViewModels public IClipboardService ClipboardService { get; } + public string XtermBufferState { get; private set; } + public RelayCommand CloseCommand { get; } public RelayCommand CloseSearchPanelCommand { get; } @@ -166,7 +229,7 @@ namespace FluentTerminal.App.ViewModels public ISettingsService SettingsService { get; } - public ShellProfile ShellProfile { get; } + public ShellProfile ShellProfile { get; private set; } public bool ShowSearchPanel { @@ -293,6 +356,13 @@ namespace FluentTerminal.App.ViewModels FindNextRequested?.Invoke(this, SearchText); } + public ITerminalView TerminalView { get; set; } + + public async Task SerializeXtermState() + { + return await TerminalView?.SerializeXtermState(); + } + private void FindPrevious() { FindPreviousRequested?.Invoke(this, SearchText); diff --git a/FluentTerminal.App/App.xaml.cs b/FluentTerminal.App/App.xaml.cs index 7e41e2b..fc5dab2 100755 --- a/FluentTerminal.App/App.xaml.cs +++ b/FluentTerminal.App/App.xaml.cs @@ -457,6 +457,7 @@ namespace FluentTerminal.App mainViewModel.ShowSettingsRequested += OnShowSettingsRequested; mainViewModel.ShowAboutRequested += OnShowAboutRequested; mainViewModel.ActivatedMv += OnMainViewActivated; + mainViewModel.TabTearedOff += OnTabTearOff; _mainViewModels.Add(mainViewModel); } @@ -474,6 +475,7 @@ namespace FluentTerminal.App viewModel.ShowSettingsRequested += OnShowSettingsRequested; viewModel.ShowAboutRequested += OnShowAboutRequested; viewModel.ActivatedMv += OnMainViewActivated; + viewModel.TabTearedOff += OnTabTearOff; _mainViewModels.Add(viewModel); return viewModel; @@ -525,6 +527,7 @@ namespace FluentTerminal.App viewModel.ShowSettingsRequested -= OnShowSettingsRequested; viewModel.ShowAboutRequested -= OnShowAboutRequested; viewModel.ActivatedMv -= OnMainViewActivated; + viewModel.TabTearedOff -= OnTabTearOff; if (_activeWindowId == viewModel.ApplicationView.Id) { _activeWindowId = 0; @@ -543,6 +546,14 @@ namespace FluentTerminal.App } } + private async void OnTabTearOff(object sender, TerminalViewModel model) + { + Logger.Instance.Debug("App.xaml.cs on tab tear off"); + + var newViewModel = await CreateNewTerminalWindow().ConfigureAwait(true); + await newViewModel.AddTerminalAsync(await model.Serialize()); + } + private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e) { ShellProfile profile = null; diff --git a/FluentTerminal.App/FluentTerminal.App.csproj b/FluentTerminal.App/FluentTerminal.App.csproj index 6cdde3c..98a1fb5 100644 --- a/FluentTerminal.App/FluentTerminal.App.csproj +++ b/FluentTerminal.App/FluentTerminal.App.csproj @@ -207,7 +207,6 @@ - TerminalView.xaml diff --git a/FluentTerminal.App/Views/MainPage.xaml b/FluentTerminal.App/Views/MainPage.xaml index 82d749c..95b64f0 100644 --- a/FluentTerminal.App/Views/MainPage.xaml +++ b/FluentTerminal.App/Views/MainPage.xaml @@ -187,7 +187,10 @@ AddCommand="{x:Bind ViewModel.AddLocalShellCommand}" ItemsSource="{x:Bind ViewModel.Terminals, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedTerminal, Mode=TwoWay}" - Visibility="{x:Bind ViewModel.ShowTabsOnTop, Mode=OneWay}" /> + Visibility="{x:Bind ViewModel.ShowTabsOnTop, Mode=OneWay}" + TabDraggedOutside="TabBar_TabDraggedOutside" + TabWindowChanged="TabView_Drop" + TabDraggingCompleted="TabBar_TabDraggingCompleted" /> + Visibility="{x:Bind ViewModel.ShowTabsOnBottom, Mode=OneWay}" + TabDraggedOutside="TabBar_TabDraggedOutside" + TabWindowChanged="TabView_Drop" + TabDraggingCompleted="TabBar_TabDraggingCompleted" /> \ No newline at end of file diff --git a/FluentTerminal.App/Views/MainPage.xaml.cs b/FluentTerminal.App/Views/MainPage.xaml.cs index fae27e0..d8e6cba 100644 --- a/FluentTerminal.App/Views/MainPage.xaml.cs +++ b/FluentTerminal.App/Views/MainPage.xaml.cs @@ -2,12 +2,14 @@ using FluentTerminal.App.ViewModels; using System; using System.ComponentModel; +using System.Linq; using Windows.ApplicationModel.Core; using Windows.UI.Core; using Windows.UI.ViewManagement; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; +using FluentTerminal.App.Services; namespace FluentTerminal.App.Views { @@ -95,5 +97,36 @@ namespace FluentTerminal.App.Views PropertyChanged(this, new PropertyChangedEventArgs(nameof(CoreTitleBarPadding))); } } + + private async void TabView_Drop(object sender, DragEventArgs e) + { + if (e.DataView.Properties.TryGetValue(Constants.TerminalViewModelStateId, out object stateObj) && stateObj is string terminalViewModelState) + { + await ViewModel.AddTerminalAsync(terminalViewModelState); + } + } + + private void TabBar_TabDraggedOutside(ListViewBase sender, DragItemsCompletedEventArgs args) + { + var item = args.Items.FirstOrDefault(); + if (item is TerminalViewModel model) + { + ViewModel.TearOffTab(model); + } + } + + private void TabBar_TabDraggingCompleted(object sender, TerminalViewModel model) + { + int position = ViewModel.Terminals.IndexOf(model); + ViewModel.Terminals.Remove(model); + if (ViewModel.Terminals.Count > 0) + { + ViewModel.SelectedTerminal = ViewModel.Terminals[Math.Max(0, position - 1)]; + } + else + { + ViewModel.ApplicationView.TryClose(); + } + } } } \ No newline at end of file diff --git a/FluentTerminal.App/Views/TabBar.xaml b/FluentTerminal.App/Views/TabBar.xaml index 01d48cb..56f924c 100644 --- a/FluentTerminal.App/Views/TabBar.xaml +++ b/FluentTerminal.App/Views/TabBar.xaml @@ -47,10 +47,14 @@ x:Name="ListView" AllowDrop="True" Background="Transparent" - CanDragItems="True" + CanDragItems="true" CanReorderItems="True" ItemsSource="{x:Bind ItemsSource}" - SelectedItem="{x:Bind SelectedItem, Mode=TwoWay}"> + SelectedItem="{x:Bind SelectedItem, Mode=TwoWay}" + DragEnter="ListView_DragEnter" + DragItemsStarting="ListView_DragItemsStarting" + DragItemsCompleted="ListView_DragItemsCompleted" + Drop="ListView_Drop"> - + diff --git a/FluentTerminal.App/Views/TabBar.xaml.cs b/FluentTerminal.App/Views/TabBar.xaml.cs index b603153..77464eb 100644 --- a/FluentTerminal.App/Views/TabBar.xaml.cs +++ b/FluentTerminal.App/Views/TabBar.xaml.cs @@ -1,8 +1,12 @@ -using FluentTerminal.App.ViewModels; +using FluentTerminal.App.Services; +using FluentTerminal.App.ViewModels; using GalaSoft.MvvmLight.Command; using System; using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -112,5 +116,66 @@ namespace FluentTerminal.App.Views ScrollLeftButton.IsEnabled = ScrollViewer.HorizontalOffset > 0; ScrollRightButton.IsEnabled = ScrollViewer.HorizontalOffset < ScrollViewer.ScrollableWidth; } + + #region Drag and drop support + + private static bool _itemWasDropped; + + public event DragEventHandler TabWindowChanged; + public event EventHandler TabDraggingCompleted; + public event TypedEventHandler TabDraggedOutside; + + private void ListView_DragEnter(object sender, DragEventArgs e) + { + Logger.Instance.Debug($"ListView_DragEnter."); + e.AcceptedOperation = DataPackageOperation.Move; + e.DragUIOverride.IsGlyphVisible = false; + e.DragUIOverride.Caption = "Drop tab here"; + } + + private async void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) + { + _itemWasDropped = false; + + Logger.Instance.Debug($"ListView_DragItemsStarting. e.Data.RequestedOperation: {e.Data.RequestedOperation}. Items count: {e.Items.Count}."); + + var item = e.Items.FirstOrDefault(); + if (item is TerminalViewModel model) + { + await model.TrayProcessCommunicationService.PauseTerminalOutput(model.Terminal.Id, true); + e.Data.Properties.Add(Constants.TerminalViewModelStateId, await model.Serialize()); + } + } + + private void ListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) + { + Logger.Instance.Debug($"ListView_DragItemsCompleted. Drop result: {args.DropResult}. Items count: {args.Items.Count}"); + + var item = args.Items.FirstOrDefault(); + if (item is TerminalViewModel model) + { + if (ItemsSource.Count > 1 && !_itemWasDropped && args.DropResult == DataPackageOperation.None) + { + TabDraggedOutside?.Invoke(sender, args); + _itemWasDropped = true; + } + + if (_itemWasDropped) + { + TabDraggingCompleted?.Invoke(sender, model); + } + + model.TrayProcessCommunicationService.PauseTerminalOutput(model.Terminal.Id, false); + } + } + + private void ListView_Drop(object sender, DragEventArgs e) + { + Logger.Instance.Debug($"ListView_Drop. e.AcceptedOperation: {e.AcceptedOperation}."); + TabWindowChanged?.Invoke(sender, e); + _itemWasDropped = true; + } + + #endregion Drag and drop support } } \ No newline at end of file diff --git a/FluentTerminal.App/Views/TerminalView.xaml.cs b/FluentTerminal.App/Views/TerminalView.xaml.cs index e905337..8977653 100644 --- a/FluentTerminal.App/Views/TerminalView.xaml.cs +++ b/FluentTerminal.App/Views/TerminalView.xaml.cs @@ -24,6 +24,7 @@ namespace FluentTerminal.App.Views _terminalView = new XtermTerminalView(); TerminalContainer.Children.Add((UIElement)_terminalView); _terminalView.Initialize(ViewModel); + ViewModel.TerminalView = _terminalView; } public TerminalViewModel ViewModel { get; } diff --git a/FluentTerminal.App/Views/XtermTerminalView.xaml.cs b/FluentTerminal.App/Views/XtermTerminalView.xaml.cs index 444c453..f0db5a5 100644 --- a/FluentTerminal.App/Views/XtermTerminalView.xaml.cs +++ b/FluentTerminal.App/Views/XtermTerminalView.xaml.cs @@ -125,6 +125,11 @@ namespace FluentTerminal.App.Views return ExecuteScriptAsync($"changeTheme('{serialized}')"); } + public async Task SerializeXtermState() + { + return await ExecuteScriptAsync(@"serializeTerminal()"); + } + public Task FindNext(string searchText) { return ExecuteScriptAsync($"findNext('{searchText}')"); @@ -171,7 +176,7 @@ namespace FluentTerminal.App.Views var port = await ViewModel.TrayProcessCommunicationService.GetAvailablePort().ConfigureAwait(true); await CreateWebSocketServer(port.Port).ConfigureAwait(true); _connectedEvent.Wait(); - var response = await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType).ConfigureAwait(true); + var response = await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType, ViewModel.XtermBufferState).ConfigureAwait(true); if (!response.Success) { await ViewModel.DialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true); diff --git a/FluentTerminal.Client/src/index.ts b/FluentTerminal.Client/src/index.ts index 13c0c4c..e597c99 100644 --- a/FluentTerminal.Client/src/index.ts +++ b/FluentTerminal.Client/src/index.ts @@ -2,6 +2,7 @@ import { Terminal, ITerminalOptions } from 'xterm'; import { AttachAddon } from 'xterm-addon-attach'; import { FitAddon } from 'xterm-addon-fit'; import { SearchAddon } from 'xterm-addon-search'; +import * as SerializeAddon from "./xterm-addon-serialize/src/SerializeAddon"; interface ExtendedWindow extends Window { keyBindings: any[]; @@ -15,6 +16,7 @@ interface ExtendedWindow extends Window { changeKeyBindings(keyBindings: any): void; findNext(content: string): void; findPrevious(content: string): void; + serializeTerminal() : void; } declare var window: ExtendedWindow; @@ -22,9 +24,15 @@ declare var window: ExtendedWindow; let term: any; let fitAddon: any; let searchAddon: any; +let serializeAddon: any; let socket: WebSocket; const terminalContainer = document.getElementById('terminal-container'); +window.serializeTerminal = () => { + let serialized = serializeAddon.serialize(); + return serialized; +} + window.createTerminal = (options, theme, keyBindings) => { while (terminalContainer.children.length) { terminalContainer.removeChild(terminalContainer.children[0]); @@ -59,6 +67,8 @@ window.createTerminal = (options, theme, keyBindings) => { term.loadAddon(searchAddon); fitAddon = new FitAddon(); term.loadAddon(fitAddon); + serializeAddon = new SerializeAddon.SerializeAddon(); + term.loadAddon(serializeAddon); window.term = term; @@ -95,7 +105,7 @@ window.createTerminal = (options, theme, keyBindings) => { } term.attachCustomKeyEventHandler(function (e) { - if (e.type != "keydown"){ + if (e.type != "keydown") { return true; } for (var i = 0; i < window.keyBindings.length; i++) { diff --git a/FluentTerminal.Client/src/xterm-addon-serialize/src/SerializeAddon.ts b/FluentTerminal.Client/src/xterm-addon-serialize/src/SerializeAddon.ts new file mode 100644 index 0000000..fa41535 --- /dev/null +++ b/FluentTerminal.Client/src/xterm-addon-serialize/src/SerializeAddon.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +function crop(value: number, from: number, to: number): number { + return Math.max(from, Math.min(value, to)); +} + +export class SerializeAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() { } + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public serialize(rows?: number): string { + // TODO: Add frontground/background color support later + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + const terminalRows = this._terminal.buffer.baseY + this._terminal.buffer.cursorY + 1; + if (rows === undefined) { + rows = terminalRows; + } + rows = crop(rows, 0, terminalRows); + + const buffer = this._terminal.buffer; + const lines: string[] = new Array(rows); + + for (let i = terminalRows - rows; i < terminalRows; i++) { + const line = buffer.getLine(i); + lines[i - terminalRows + rows] = line ? line.translateToString(true) : ''; + } + + return lines.join('\r\n'); + } + + public dispose(): void { } +} diff --git a/FluentTerminal.Models/Requests/PauseTerminalOutputRequest.cs b/FluentTerminal.Models/Requests/PauseTerminalOutputRequest.cs new file mode 100644 index 0000000..8fa4ec4 --- /dev/null +++ b/FluentTerminal.Models/Requests/PauseTerminalOutputRequest.cs @@ -0,0 +1,13 @@ +namespace FluentTerminal.Models.Requests +{ + public class PauseTerminalOutputRequest : IMessage + { + public const byte Identifier = 16; + + byte IMessage.Identifier => Identifier; + + public bool Pause { get; set; } + + public byte Id { get; set; } + } +} \ No newline at end of file diff --git a/FluentTerminal.Models/Responses/CreateTerminalResponse.cs b/FluentTerminal.Models/Responses/CreateTerminalResponse.cs index 171edfc..287803d 100644 --- a/FluentTerminal.Models/Responses/CreateTerminalResponse.cs +++ b/FluentTerminal.Models/Responses/CreateTerminalResponse.cs @@ -1,13 +1,11 @@ namespace FluentTerminal.Models.Responses { - public class CreateTerminalResponse :IMessage + public class CreateTerminalResponse : TerminalResponse, IMessage { public const byte Identifier = 9; byte IMessage.Identifier => Identifier; - public bool Success { get; set; } - public string Error { get; set; } public string ShellExecutableName { get; set; } } } \ No newline at end of file diff --git a/FluentTerminal.Models/Responses/PauseTerminalOutputResponse.cs b/FluentTerminal.Models/Responses/PauseTerminalOutputResponse.cs new file mode 100644 index 0000000..3f5f50b --- /dev/null +++ b/FluentTerminal.Models/Responses/PauseTerminalOutputResponse.cs @@ -0,0 +1,9 @@ +namespace FluentTerminal.Models.Responses +{ + public class PauseTerminalOutputResponse : TerminalResponse, IMessage + { + public const byte Identifier = 16; + + byte IMessage.Identifier => Identifier; + } +} \ No newline at end of file diff --git a/FluentTerminal.Models/Responses/TerminalResponse.cs b/FluentTerminal.Models/Responses/TerminalResponse.cs new file mode 100644 index 0000000..df3bb5e --- /dev/null +++ b/FluentTerminal.Models/Responses/TerminalResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace FluentTerminal.Models.Responses +{ + public class TerminalResponse + { + public bool Success { get; set; } + public string Error { get; set; } + } +} diff --git a/FluentTerminal.SystemTray/Services/AppCommunicationService.cs b/FluentTerminal.SystemTray/Services/AppCommunicationService.cs index 246aaf5..55b636e 100644 --- a/FluentTerminal.SystemTray/Services/AppCommunicationService.cs +++ b/FluentTerminal.SystemTray/Services/AppCommunicationService.cs @@ -125,6 +125,9 @@ namespace FluentTerminal.SystemTray.Services case UpdateSettingsRequest.Identifier: await HandleUpdateSettingsRequest(args); break; + case PauseTerminalOutputRequest.Identifier: + await HandlePauseTerminalOutputRequest(args); + break; default: Logger.Instance.Error("Received unknown message type: {messageType}", messageType); break; @@ -256,6 +259,18 @@ namespace FluentTerminal.SystemTray.Services var request = JsonConvert.DeserializeObject(messageContent); _settingsService.NotifyApplicationSettingsChanged(request.Settings); } + + private async Task HandlePauseTerminalOutputRequest(AppServiceRequestReceivedEventArgs args) + { + var deferral = args.GetDeferral(); + var messageContent = (string)args.Request.Message[MessageKeys.Content]; + var request = JsonConvert.DeserializeObject(messageContent); + var response = _terminalsManager.PauseTermimal(request.Id, request.Pause); + + await args.Request.SendResponseAsync(CreateMessage(response)); + + deferral.Complete(); + } private async Task HandleCheckFileExistsRequest(AppServiceRequestReceivedEventArgs args) { diff --git a/FluentTerminal.SystemTray/Services/ConPty/ConPtySession.cs b/FluentTerminal.SystemTray/Services/ConPty/ConPtySession.cs index ab4d99d..d5472de 100644 --- a/FluentTerminal.SystemTray/Services/ConPty/ConPtySession.cs +++ b/FluentTerminal.SystemTray/Services/ConPty/ConPtySession.cs @@ -11,6 +11,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty private TerminalsManager _terminalsManager; private Terminal _terminal; private bool _exited; + private bool _paused; private TerminalSize _terminalSize; public byte Id { get; private set; } @@ -92,6 +93,11 @@ namespace FluentTerminal.SystemTray.Services.ConPty { _terminalsManager.DisplayTerminalOutput(Id, read); } + + while(_paused && !_exited) + { + await Task.Delay(50); + } } while (!_exited); } @@ -103,6 +109,11 @@ namespace FluentTerminal.SystemTray.Services.ConPty _terminal.WriteToPseudoConsole(data); } + public void Pause(bool value) + { + _paused = value; + } + #region IDisposable Support private bool disposedValue = false; diff --git a/FluentTerminal.SystemTray/Services/ITerminalSession.cs b/FluentTerminal.SystemTray/Services/ITerminalSession.cs index 074d142..b9b08c9 100644 --- a/FluentTerminal.SystemTray/Services/ITerminalSession.cs +++ b/FluentTerminal.SystemTray/Services/ITerminalSession.cs @@ -15,5 +15,6 @@ namespace FluentTerminal.SystemTray.Services void Resize(TerminalSize size); void Write(byte[] data); void Start(CreateTerminalRequest request, TerminalsManager terminalsManager); + void Pause(bool value); } } \ No newline at end of file diff --git a/FluentTerminal.SystemTray/Services/TerminalsManager.cs b/FluentTerminal.SystemTray/Services/TerminalsManager.cs index 309075d..a616d2d 100644 --- a/FluentTerminal.SystemTray/Services/TerminalsManager.cs +++ b/FluentTerminal.SystemTray/Services/TerminalsManager.cs @@ -173,6 +173,19 @@ namespace FluentTerminal.SystemTray.Services } } + public PauseTerminalOutputResponse PauseTermimal(byte id, bool pause) + { + var response = new PauseTerminalOutputResponse() + { + Success = true + }; + if (_terminals.TryGetValue(id, out TerminalSessionInfo sessionInfo)) + { + sessionInfo.Session.Pause(pause); + } + return response; + } + public string GetDefaultEnvironmentVariableString(Dictionary additionalVariables) { var environmentVariables = Environment.GetEnvironmentVariables(); diff --git a/FluentTerminal.SystemTray/Services/WinPty/WinPtySession.cs b/FluentTerminal.SystemTray/Services/WinPty/WinPtySession.cs index 134f906..1f7ca4c 100644 --- a/FluentTerminal.SystemTray/Services/WinPty/WinPtySession.cs +++ b/FluentTerminal.SystemTray/Services/WinPty/WinPtySession.cs @@ -19,6 +19,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty private TerminalsManager _terminalsManager; private Process _shellProcess; private bool _exited; + private bool _paused; private TerminalSize _terminalSize; public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager) @@ -211,10 +212,20 @@ namespace FluentTerminal.SystemTray.Services.WinPty { _terminalsManager.DisplayTerminalOutput(Id, read); } + + while (_paused && !_exited) + { + await Task.Delay(50); + } } while (!_exited); } }, TaskCreationOptions.LongRunning); } + + public void Pause(bool value) + { + _paused = value; + } } } \ No newline at end of file