Files
FluentTerminal/FluentTerminal.App.ViewModels/TerminalViewModel.cs
T

603 lines
21 KiB
C#

using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
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;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FluentTerminal.Models.Messages;
namespace FluentTerminal.App.ViewModels
{
public class TerminalViewModel : ViewModelBase
{
#region Static
private static readonly Regex SshTitleRx = new Regex(@"^\w+@\S+:", RegexOptions.Compiled);
#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<string> 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<TerminalState>(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 _isHovered;
private bool _hasNewOutput;
private bool _hasExitedWithError;
private string _searchText;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private TerminalTheme _terminalTheme;
private TerminalOptions _terminalOptions;
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, string terminalState = null)
{
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
SettingsService = settingsService;
_terminalOptions = SettingsService.GetTerminalOptions();
TrayProcessCommunicationService = trayProcessCommunicationService;
DialogService = dialogService;
_keyboardCommandService = keyboardCommandService;
ApplicationSettings = applicationSettings;
ApplicationView = applicationView;
ClipboardService = clipboardService;
ShellProfile = shellProfile;
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? SettingsService.GetCurrentTheme() : SettingsService.GetTheme(shellProfile.TerminalThemeId);
TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
CloseCommand = new AsyncCommand(CloseTab, CanExecuteCommand);
CloseLeftTabsCommand = new RelayCommand(CloseLeftTabs, CanExecuteCommand);
CloseRightTabsCommand = new RelayCommand(CloseRightTabs, CanExecuteCommand);
CloseOtherTabsCommand = new RelayCommand(CloseOtherTabs, CanExecuteCommand);
FindNextCommand = new RelayCommand(FindNext, CanExecuteCommand);
FindPreviousCommand = new RelayCommand(FindPrevious, CanExecuteCommand);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel, CanExecuteCommand);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme, CanExecuteCommand);
EditTitleCommand = new AsyncCommand(EditTitle, CanExecuteCommand);
DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand);
if (!String.IsNullOrEmpty(terminalState))
{
Restore(terminalState);
}
Terminal = new Terminal(TrayProcessCommunicationService, _terminalId);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
Terminal.OutputReceived += Terminal_OutputReceived;
Terminal.SizeChanged += Terminal_SizeChanged;
Terminal.TitleChanged += Terminal_TitleChanged;
Terminal.Exited += Terminal_Exited;
Terminal.Closed += Terminal_Closed;
Overlay = new OverlayViewModel(dispatcherTimer);
}
private bool _disposalRequested;
public void DisposalPrepare()
{
_disposalRequested = true;
CloseCommand = null;
TerminalView.DisposalPrepare();
TerminalView = null;
Terminal = null;
Overlay = null;
}
public event EventHandler Activated;
public event EventHandler Closed;
public event EventHandler<string> FindNextRequested;
public event EventHandler<string> FindPreviousRequested;
public event EventHandler SearchStarted;
public event EventHandler<TerminalTheme> ThemeChanged;
public event EventHandler<string> ShellTitleChanged;
public event EventHandler<string> CustomTitleChanged;
public event EventHandler CloseLeftTabsRequested;
public event EventHandler CloseRightTabsRequested;
public event EventHandler CloseOtherTabsRequested;
public event EventHandler DuplicateTabRequested;
public ApplicationSettings ApplicationSettings { get; private set; }
public IApplicationView ApplicationView { get; }
public TabTheme BackgroundTabTheme
{
// 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).
get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ?
_tabTheme :
SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
}
public IClipboardService ClipboardService { get; }
public string XtermBufferState { get; private set; }
public AsyncCommand CloseCommand { get; private set; }
public RelayCommand CloseRightTabsCommand { get; }
public RelayCommand CloseLeftTabsCommand { get; }
public RelayCommand CloseOtherTabsCommand { get; }
public RelayCommand CloseSearchPanelCommand { get; }
public IDialogService DialogService { get; }
public IAsyncCommand EditTitleCommand { get; }
public RelayCommand FindNextCommand { get; }
public RelayCommand FindPreviousCommand { get; }
public RelayCommand DuplicateTabCommand { get; }
public bool IsSelected
{
get => _isSelected;
set
{
if (Set(ref _isSelected, value))
{
if (IsSelected)
{
HasNewOutput = false;
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
RaisePropertyChanged(nameof(ShowCloseButton));
}
}
}
public bool IsHovered
{
get => _isHovered;
set
{
if (Set(ref _isHovered, value))
{
RaisePropertyChanged(nameof(ShowCloseButton));
}
}
}
public bool ShowCloseButton
{
get => IsHovered || IsSelected;
}
public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) ||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null);
public bool HasNewOutput
{
get => _hasNewOutput;
set => Set(ref _hasNewOutput, value);
}
public bool HasExitedWithError
{
get => _hasExitedWithError;
set
{
if (Set(ref _hasExitedWithError, value) && value)
{
HasNewOutput = false;
}
}
}
public string SearchText
{
get => _searchText;
set => Set(ref _searchText, value);
}
public RelayCommand<string> SelectTabThemeCommand { get; }
public ISettingsService SettingsService { get; }
public ShellProfile ShellProfile { get; private set; }
public bool ShowSearchPanel
{
get => _showSearchPanel;
set => Set(ref _showSearchPanel, value);
}
public TabTheme TabTheme
{
get => _tabTheme;
set
{
Set(ref _tabTheme, value);
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
}
}
public ObservableCollection<TabTheme> TabThemes { get; }
public Terminal Terminal { get; private set; }
public OverlayViewModel Overlay { get; private set; }
public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}
public bool Initialized { get; set; }
public string TabTitle
{
get => _tabTitle;
set
{
var title = value?.Trim() ?? string.Empty;
if (ShellProfile is SshProfile)
{
// For SshProfile we are adjusting title.
if (title.Equals("ssh", StringComparison.OrdinalIgnoreCase) ||
title.EndsWith("\\ssh.exe", StringComparison.OrdinalIgnoreCase))
{
title = $"[ssh] {I18N.Translate("Authenticate")}";
}
else if (title.Equals("mosh", StringComparison.OrdinalIgnoreCase) ||
title.EndsWith("\\mosh.exe", StringComparison.OrdinalIgnoreCase))
{
title = $"[mosh] {I18N.Translate("Authenticate")}";
}
else if (SshTitleRx.IsMatch(title))
{
title = $"[ssh] {title}";
}
}
if (Set(ref _tabTitle, title))
{
CustomTitleChanged?.Invoke(this, title);
}
}
}
public string ShellTitle
{
get => _shellTitle;
set
{
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
{
TabTitle = value;
}
}
}
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task Close()
{
MessengerInstance.Unregister(this);
return Terminal.Close();
}
public void CopyText(string text)
{
ClipboardService.SetText(text);
if (_terminalOptions.ShowTextCopied)
{
Overlay.Show(I18N.Translate("TextCopied"));
}
}
public async Task EditTitle()
{
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"));
if (result != null)
{
if (string.IsNullOrWhiteSpace(result))
{
_hasCustomTitle = false;
TabTitle = ShellTitle;
}
else
{
_hasCustomTitle = true;
TabTitle = result;
}
}
}
public void FocusTerminal()
{
Activated?.Invoke(this, EventArgs.Empty);
}
private void CloseSearchPanel()
{
SearchText = string.Empty;
ShowSearchPanel = false;
FocusTerminal();
}
private bool CanExecuteCommand<T>(T a)
{
return CanExecuteCommand();
}
private bool CanExecuteCommand()
{
return Initialized && !_disposalRequested;
}
private async Task CloseTab()
{
await TryClose().ConfigureAwait(false);
}
private void CloseLeftTabs()
{
CloseLeftTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void CloseRightTabs()
{
CloseRightTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void CloseOtherTabs()
{
CloseOtherTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void FindNext()
{
FindNextRequested?.Invoke(this, SearchText);
}
public ITerminalView TerminalView { get; set; }
public async Task<string> SerializeXtermState()
{
return await TerminalView?.SerializeXtermState();
}
private void FindPrevious()
{
FindPreviousRequested?.Invoke(this, SearchText);
}
private void DuplicateTab()
{
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
}
private async void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
{
await ApplicationView.RunOnDispatcherThread(() =>
{
ApplicationSettings = message.ApplicationSettings;
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
});
}
private async void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
{
await ApplicationView.RunOnDispatcherThread(() =>
{
// only change theme if not overwritten by profile
if (ShellProfile.TerminalThemeId == Guid.Empty)
{
var currentTheme = SettingsService.GetTheme(message.ThemeId);
TerminalTheme = currentTheme;
ThemeChanged?.Invoke(this, currentTheme);
}
});
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
{
_terminalOptions = message.TerminalOptions;
}
private void SelectTabTheme(string id)
{
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
}
private void Terminal_Exited(object sender, int exitCode)
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker && exitCode != 0)
{
tracker.SetInvalid();
}
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
}
private void Terminal_Closed(object sender, EventArgs e)
{
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
Terminal.KeyboardCommandReceived -= Terminal_KeyboardCommandReceived;
Terminal.OutputReceived -= Terminal_OutputReceived;
Terminal.SizeChanged -= Terminal_SizeChanged;
Terminal.TitleChanged -= Terminal_TitleChanged;
Terminal.Exited -= Terminal_Exited;
Terminal.Closed -= Terminal_Closed;
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
{
switch (e)
{
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
CopyText(selection);
break;
}
case nameof(Command.Paste):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(Encoding.UTF8.GetBytes(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(Encoding.UTF8.GetBytes(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, byte[] e)
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
tracker.SetOutputReceived();
}
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
}
}
private void Terminal_SizeChanged(object sender, TerminalSize e)
{
Overlay.Show($"{e.Columns} x {e.Rows}");
}
private void Terminal_TitleChanged(object sender, string e)
{
ShellTitle = e;
ShellTitleChanged?.Invoke(this, e);
}
private async Task TryClose()
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
tracker.SetInvalid();
}
if (ApplicationSettings.ConfirmClosingTabs)
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), String.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.Cancel)
{
return;
}
}
await Close().ConfigureAwait(true);
}
}
}