Compare commits

..
Author SHA1 Message Date
FelixSe 0733b1ab02 Initial adaption of VtNetCore.UWP 2019-03-03 00:29:00 +01:00
FelixSe 4e54668092 fix TerminalTheme Equals() 2019-03-02 19:23:18 +01:00
FelixSe 74abd4f448 Encapsulate xterm.js implementation in XtermTerminalView 2019-03-02 19:23:05 +01:00
Eric Cornelissen b85be454ce Improve settings window (#213)
* Update settings window layout with consistent margins

Updated the layout of the different views of the settings window so that
there is always margin at the bottom. In some cases this was achieved
using <StackPanels> for clarity and extensibility, grouping e.g. a title
and some radio options together with one unified margin.

Also made some small consistency changes to the .xaml files.

* Add width to Themes text input

* Normalize margins in settings

* Add default display values in Terminal settings

... for Arguments and Working Directory

* Generalize TemplateSelector for empty strings

* Add default display value for profile keybindings

* Update margins and fontsize in theme preview

* Update 'loading...' message in About

... to "Checking for updates..." and a spinner as suggested by
@skipmeister123

* No popup when cancelling profile edit no changes

* Delete profile when new and cancelled

* Open new and cloned themes in edit mode

... and make it so canceling deletes the theme.

* No popup when cancelling theme edit no changes

* Add resources for margin definitions

* Check for null Author in TerminalTheme

* Add resource for header margin definition

* Moved createdBy text to code behind
2019-02-28 21:09:52 +01:00
FelixSe 1f3dd72214 Improved text wrapping on window title 2019-02-28 20:44:00 +01:00
Eric Cornelissen c4fcd986c0 Add keybinding for configurable new window (#216)
* Add keybind to open new configurable window

* Close new configurable window on cancel

* renamed event args
2019-02-28 19:24:18 +01:00
FelixSe ea21d37089 Make InputBuffer more threadsafe 2019-02-25 21:10:39 +01:00
Paul Galbraith 2f133720cc Chocolatey install instructions (#209) 2019-02-19 07:30:43 +01:00
Paul Galbraith 250c90fa02 install option to force context menu integration without user interaction (#204) 2019-02-15 19:22:38 +01:00
Mike 7e2c5d2f45 Line ending translation (#172)
* On paste, the TerminalViewModel uses the ShellProfile to translate line endings.

Default line ending translation style is set to not modify the pasted contents, Next up, adding the radio button to shell profile.

* Added UI elements for choosing line ending style.

Using a converter to map between an enum and radio buttons, and another converter to map from enum to text based on the description of the enum value.

* Added "Paste without newlines" command.

* Adding missing converters to project file.

* A few fixes after cherry-picks.

* Adding missing Command property to PasteWithoutNewlines keybinding.

* Cleaned up some unnecessary property settings.

* fix #114 scrolling issue on presicion touchpads (#171)

* fix #114 scrolling issue on presicion touchpads, bounce scroll & pinch zoom

* rephrased comment

* removed css that enforced autohiding

* simplified js code handling scrollbar size

* Press enter to close an Input dialog (#177)

... As if the primary button is pressed.

* Renew certificate

* Update README.md

* Add option to use bold text in terminals (#183)

* Add option to use bold text in terminals

... as per #182

* Add BoldText to setting defaults

* Remove AppVeyor files

* Update README.md

* Add keybinding to change the (current) tab title (#193)

* Add keybinding for change the (current) tab title

Default binding is Ctrl+Shift+N

* Update change tab title default keybind to Ctrl+Shift+R

* Experimental ConPTY support (#199)

* First steps of ConPty support

* Added resize functionality

* fix cwd, args, hide console window

* Handle shell process exited

* Make ConPTY optional

* Fix automerge errors

* Accepted many XAML formatting changes, and changed command handling to switch.

* Fixed spaxcng on the settings page.

* adapted radio button binding to be in line with the rest of the application
2019-02-13 22:42:23 +01:00
FelixSe 9fed3fb192 Fix automerge errors 2019-02-09 23:06:45 +01:00
Felix 0d85f933c7 Experimental ConPTY support (#199)
* First steps of ConPty support

* Added resize functionality

* fix cwd, args, hide console window

* Handle shell process exited

* Make ConPTY optional
2019-02-08 22:12:29 +01:00
Eric Cornelissen f235ddf0f1 Add keybinding to change the (current) tab title (#193)
* Add keybinding for change the (current) tab title

Default binding is Ctrl+Shift+N

* Update change tab title default keybind to Ctrl+Shift+R
2019-02-08 21:55:23 +01:00
Felix 887cd45b4c Update README.md 2019-01-30 18:32:32 +01:00
FelixSe aac2f220d6 Remove AppVeyor files 2019-01-28 22:00:33 +01:00
Eric Cornelissen 90f15cc1e3 Add option to use bold text in terminals (#183)
* Add option to use bold text in terminals

... as per #182

* Add BoldText to setting defaults
2019-01-28 21:57:36 +01:00
Felix 292b66af4d Update README.md 2019-01-28 19:26:48 +01:00
FelixSe b6d835984c Renew certificate 2019-01-26 12:29:32 +01:00
Eric Cornelissen 369b5ddacf Press enter to close an Input dialog (#177)
... As if the primary button is pressed.
2019-01-18 21:20:56 +01:00
Mike 6be29f49d7 fix #114 scrolling issue on presicion touchpads (#171)
* fix #114 scrolling issue on presicion touchpads, bounce scroll & pinch zoom

* rephrased comment

* removed css that enforced autohiding

* simplified js code handling scrollbar size
2019-01-14 21:47:35 +01:00
91 changed files with 3283 additions and 944 deletions

No files matched your search

@@ -42,10 +42,11 @@ namespace FluentTerminal.App.Services.Test
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile);
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile, sessionType);
result.Should().BeEquivalentTo(response);
}
@@ -0,0 +1,7 @@
namespace FluentTerminal.App.Services.EventArgs
{
public class NewWindowRequestedEventArgs : System.EventArgs
{
public bool ShowProfileSelection { get; set; }
}
}
@@ -17,5 +17,6 @@ namespace FluentTerminal.App.Services
Task<bool> TryClose();
bool ToggleFullScreen();
bool IsApiContractPresent(string api, ushort version);
}
}
@@ -1,38 +0,0 @@
using FluentTerminal.Models;
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);
Task ConnectToSocket(string url);
void Close();
Task FocusTerminal();
Task<string> GetSelection();
Task FindNext(string searchText);
Task FindPrevious(string searchText);
void FocusSearchTextBox();
}
}
@@ -1,4 +1,5 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Threading.Tasks;
@@ -11,7 +12,7 @@ namespace FluentTerminal.App.Services
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile);
Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task ResizeTerminal(int id, TerminalSize size);
@@ -22,5 +23,7 @@ namespace FluentTerminal.App.Services
void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
Task CloseTerminal(int terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
int GetNextTerminalId();
}
}
@@ -21,6 +21,7 @@ namespace FluentTerminal.App.Services.Implementation
MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu,
AlwaysShowTabs = false,
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false,
EnableTrayIcon = true
};
@@ -116,6 +117,19 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ChangeTabTitle),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.R
}
};
case Command.CloseTab:
return new List<KeyBinding>
{
@@ -142,6 +156,19 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ConfigurableNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ConfigurableNewWindow),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ShowSettings:
return new List<KeyBinding>
{
@@ -181,6 +208,20 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.PasteWithoutNewlines:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.PasteWithoutNewlines),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.V
}
};
case Command.Search:
return new List<KeyBinding>
{
@@ -305,6 +346,7 @@ namespace FluentTerminal.App.Services.Implementation
ScrollBarStyle = ScrollBarStyle.Hidden,
FontFamily = "Consolas",
FontSize = 13,
BoldText = false,
BackgroundOpacity = 0.8,
Padding = 12,
ScrollBackLimit = 1000
@@ -328,6 +370,7 @@ namespace FluentTerminal.App.Services.Implementation
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
KeyBindings = new []
{
new KeyBinding
@@ -349,6 +392,7 @@ namespace FluentTerminal.App.Services.Implementation
Location = @"C:\Windows\System32\cmd.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
KeyBindings = new []
{
new KeyBinding
@@ -370,6 +414,7 @@ namespace FluentTerminal.App.Services.Implementation
Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
KeyBindings = new []
{
new KeyBinding
@@ -14,7 +14,8 @@ namespace FluentTerminal.App.Services.Implementation
{
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private Dictionary<int, Action<string>> _terminalOutputHandlers;
private readonly Dictionary<int, Action<string>> _terminalOutputHandlers;
private int _nextTerminalId = 0;
public event EventHandler<int> TerminalExited;
@@ -24,12 +25,28 @@ namespace FluentTerminal.App.Services.Implementation
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
}
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile)
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
{
Id = id,
Size = size,
Profile = shellProfile
Profile = shellProfile,
SessionType = sessionType
};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
+126
View File
@@ -0,0 +1,126 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public class Terminal
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
Id = _trayProcessCommunicationService.GetNextTerminalId();
}
/// <summary>
/// To be observed by both view and viewmodel
/// </summary>
public event EventHandler Closed;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> KeyboardCommandReceived;
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<string> OutputReceived;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<TerminalSize> SizeChanged;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public int Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
{
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
Closed?.Invoke(this, System.EventArgs.Empty);
}
/// <summary>
/// to be called by viewmodel
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback();
}
/// <summary>
/// to be called by view
/// </summary>
public void ProcessKeyboardCommand(string command)
{
KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be set by view
/// </summary>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
{
_selectedTextCallback = selectedTextCallback;
}
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
SizeChanged?.Invoke(this, size);
}
/// <summary>
/// to be called by view
/// </summary>
public void SetTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// </summary>
/// <param name="shellProfile"></param>
/// <param name="size"></param>
/// <param name="sessionType"></param>
public async Task StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
{
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
}
}
public Task Write(string text)
{
return _trayProcessCommunicationService.WriteText(Id, text);
}
}
}
+22 -7
View File
@@ -45,6 +45,7 @@ namespace FluentTerminal.App.ViewModels
_keyboardCommandService = keyboardCommandService;
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), () => AddTerminal(null, false, Guid.Empty));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewTab), () => AddTerminal(null, true, Guid.Empty));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), () => SelectedTerminal.EditTitle());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
// Add all of the commands for switching to a tab of a given ID, if there's one open there
@@ -59,7 +60,9 @@ namespace FluentTerminal.App.ViewModels
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NextTab), SelectNextTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.PreviousTab), SelectPreviousTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), NewWindow);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), () => NewWindow(false));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewWindow), () => NewWindow(true));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ShowSettings), ShowSettings);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ToggleFullScreen), ToggleFullScreen);
@@ -101,7 +104,7 @@ namespace FluentTerminal.App.ViewModels
public event EventHandler Closed;
public event EventHandler NewWindowRequested;
public event EventHandler<NewWindowRequestedEventArgs> NewWindowRequested;
public event EventHandler ShowSettingsRequested;
@@ -179,13 +182,20 @@ namespace FluentTerminal.App.ViewModels
if (profile == null)
{
if (Terminals.Count == 0)
{
Closed?.Invoke(this, EventArgs.Empty);
_applicationView.TryClose();
}
return;
}
}
else if (profileId == Guid.Empty)
{
profile = _settingsService.GetDefaultShellProfile();
} else
}
else
{
profile = _settingsService.GetShellProfile(profileId);
}
@@ -203,7 +213,7 @@ namespace FluentTerminal.App.ViewModels
{
foreach (var terminal in Terminals)
{
terminal.CloseView();
terminal.Close();
}
}
@@ -212,9 +222,14 @@ namespace FluentTerminal.App.ViewModels
SelectedTerminal?.CloseCommand.Execute(null);
}
private void NewWindow()
private void NewWindow(bool showProfileSelection)
{
NewWindowRequested?.Invoke(this, EventArgs.Empty);
var args = new NewWindowRequestedEventArgs
{
ShowProfileSelection = showProfileSelection
};
NewWindowRequested?.Invoke(this, args);
}
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
@@ -264,7 +279,7 @@ namespace FluentTerminal.App.ViewModels
{
if (sender is TerminalViewModel terminal)
{
terminal.CloseView();
terminal.Close();
if (SelectedTerminal == terminal)
{
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
@@ -54,6 +54,20 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool AlwaysUseWinPty
{
get => _applicationSettings.AlwaysUseWinPty;
set
{
if (_applicationSettings.AlwaysUseWinPty != value)
{
_applicationSettings.AlwaysUseWinPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool EnableTrayIcon
{
get => _applicationSettings.EnableTrayIcon;
@@ -264,6 +278,7 @@ namespace FluentTerminal.App.ViewModels.Settings
InactiveTabColorMode = defaults.InactiveTabColorMode;
NewTerminalLocation = defaults.NewTerminalLocation;
AlwaysShowTabs = defaults.AlwaysShowTabs;
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon;
}
@@ -32,7 +32,7 @@ namespace FluentTerminal.App.ViewModels.Settings
var defaultShellProfileId = _settingsService.GetDefaultShellProfileId();
foreach (var shellProfile in _settingsService.GetShellProfiles())
{
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider);
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider, false);
viewModel.Deleted += OnShellProfileDeleted;
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
@@ -112,7 +112,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_settingsService.SaveShellProfile(shellProfile, true);
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider);
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider, true);
viewModel.EditCommand.Execute(null);
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
viewModel.Deleted += OnShellProfileDeleted;
@@ -136,6 +136,20 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool BoldText
{
get => _terminalOptions.BoldText;
set
{
if (_terminalOptions.BoldText != value)
{
_terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
}
}
}
public IEnumerable<string> Fonts { get; }
public int FontSize
@@ -206,6 +220,7 @@ namespace FluentTerminal.App.ViewModels.Settings
ScrollBarStyle = defaults.ScrollBarStyle;
FontFamily = defaults.FontFamily;
FontSize = defaults.FontSize;
BoldText = defaults.BoldText;
BackgroundOpacity = defaults.BackgroundOpacity;
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
}
@@ -40,7 +40,7 @@ namespace FluentTerminal.App.ViewModels.Settings
var activeThemeId = _settingsService.GetCurrentThemeId();
foreach (var theme in _settingsService.GetThemes())
{
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService, false);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
@@ -139,7 +139,8 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_settingsService.SaveTheme(theme, true);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, true);
viewModel.EditCommand.Execute(null);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
Themes.Add(viewModel);
@@ -2,6 +2,7 @@
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
@@ -21,14 +22,17 @@ namespace FluentTerminal.App.ViewModels
private bool _inEditMode;
private bool _isDefault;
private string _location;
private LineEndingStyle _lineEndingStyle;
private string _name;
private bool _isNew;
private TabTheme _selectedTabTheme;
private TerminalTheme _selectedTerminalTheme;
private string _workingDirectory;
private readonly IApplicationView _applicationView;
private readonly IDefaultValueProvider _defaultValueProvider;
private bool _editingLineEndingStyle;
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider)
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider, Boolean isNew)
{
Model = shellProfile;
_settingsService = settingsService;
@@ -36,6 +40,7 @@ namespace FluentTerminal.App.ViewModels
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_defaultValueProvider = defaultValueProvider;
_isNew = isNew;
_settingsService.ThemeAdded += OnThemeAdded;
_settingsService.ThemeDeleted += OnThemeDeleted;
@@ -78,6 +83,7 @@ namespace FluentTerminal.App.ViewModels
WorkingDirectory = shellProfile.WorkingDirectory;
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == shellProfile.TabThemeId);
PreInstalled = shellProfile.PreInstalled;
LineEndingStyle = shellProfile.LineEndingTranslation;
KeyBindings.Clear();
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
@@ -191,6 +197,42 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _workingDirectory, value);
}
public LineEndingStyle LineEndingStyle
{
get => _lineEndingStyle;
set
{
if (_lineEndingStyle != value && !_editingLineEndingStyle)
{
_editingLineEndingStyle = true;
_lineEndingStyle = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(DoNotModifyIsSelected));
RaisePropertyChanged(nameof(ToCRLFIsSelected));
RaisePropertyChanged(nameof(ToLFIsSelected));
_editingLineEndingStyle = false;
}
}
}
public bool DoNotModifyIsSelected
{
get => LineEndingStyle == LineEndingStyle.DoNotModify;
set => LineEndingStyle = LineEndingStyle.DoNotModify;
}
public bool ToCRLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCRLF;
set => LineEndingStyle = LineEndingStyle.ToCRLF;
}
public bool ToLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToLF;
set => LineEndingStyle = LineEndingStyle.ToLF;
}
public void SaveChanges()
{
Model.Arguments = Arguments;
@@ -199,11 +241,13 @@ namespace FluentTerminal.App.ViewModels
Model.WorkingDirectory = WorkingDirectory;
Model.TabThemeId = SelectedTabTheme.Id;
Model.TerminalThemeId = SelectedTerminalTheme.Id;
Model.LineEndingTranslation = _lineEndingStyle;
Model.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
_settingsService.SaveShellProfile(Model);
KeyBindings.Editable = false;
InEditMode = false;
_isNew = false;
}
private async Task RestoreDefaults()
@@ -248,25 +292,53 @@ namespace FluentTerminal.App.ViewModels
private async Task CancelEdit()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
if (_isNew)
{
Arguments = _fallbackProfile.Arguments;
Location = _fallbackProfile.Location;
Name = _fallbackProfile.Name;
WorkingDirectory = _fallbackProfile.WorkingDirectory;
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TerminalThemeId);
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TabThemeId);
KeyBindings.KeyBindings.Clear();
foreach (var keyBinding in Model.KeyBindings.Select(x => new KeyBinding(x)).ToList())
await Delete();
}
else
{
ShellProfile changedProfile = new ShellProfile
{
KeyBindings.Add(keyBinding);
}
Arguments = Arguments,
Location = Location,
Name = Name,
WorkingDirectory = WorkingDirectory,
TabThemeId = SelectedTabTheme.Id,
TerminalThemeId = SelectedTerminalTheme.Id,
LineEndingTranslation = _lineEndingStyle,
KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList()
};
KeyBindings.Editable = false;
InEditMode = false;
if (!_fallbackProfile.Equals(changedProfile))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Arguments = _fallbackProfile.Arguments;
Location = _fallbackProfile.Location;
Name = _fallbackProfile.Name;
WorkingDirectory = _fallbackProfile.WorkingDirectory;
LineEndingStyle = _fallbackProfile.LineEndingTranslation;
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TerminalThemeId);
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TabThemeId);
KeyBindings.KeyBindings.Clear();
foreach (var keyBinding in Model.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
}
}
+155 -199
View File
@@ -1,31 +1,20 @@
using Fleck;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
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 IDispatcherTimer _resizeOverlayTimer;
private readonly ISettingsService _settingsService;
private readonly ShellProfile _shellProfile;
private readonly string _startupDirectory;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private bool _isSelected;
private bool _newOutput;
private string _resizeOverlayContent;
@@ -34,86 +23,93 @@ namespace FluentTerminal.App.ViewModels
private bool _showSearchPanel;
private TabTheme _tabTheme;
private TerminalTheme _terminalTheme;
private int _terminalId;
private ITerminalView _terminalView;
private string _title;
private IWebSocketConnection _webSocket;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
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;
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_settingsService.KeyBindingsChanged += OnKeyBindingsChanged;
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
_dialogService = dialogService;
DialogService = dialogService;
_keyboardCommandService = keyboardCommandService;
ApplicationSettings = applicationSettings;
_startupDirectory = startupDirectory;
_shellProfile = shellProfile;
_applicationView = applicationView;
_clipboardService = clipboardService;
StartupDirectory = startupDirectory;
ApplicationView = applicationView;
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());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == _shellProfile.TabThemeId);
TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
_resizeOverlayTimer = dispatcherTimer;
_resizeOverlayTimer.Interval = new TimeSpan(0, 0, 2);
_resizeOverlayTimer.Tick += OnResizeOverlayTimerFinished;
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
FindNextCommand = new RelayCommand(async () => await FindNext().ConfigureAwait(false));
FindPreviousCommand = new RelayCommand(async () => await FindPrevious().ConfigureAwait(false));
FindNextCommand = new RelayCommand(FindNext);
FindPreviousCommand = new RelayCommand(FindPrevious);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
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<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 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);
SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
}
public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}
public IClipboardService ClipboardService { get; }
public RelayCommand CloseCommand { get; }
public RelayCommand CloseSearchPanelCommand { get; }
public string DefaultTitle { get; private set; } = string.Empty;
public IDialogService DialogService { get; }
public IAsyncCommand EditTitleCommand { get; }
public RelayCommand FindNextCommand { get; }
public RelayCommand FindPreviousCommand { get; }
public IAsyncCommand EditTitleCommand { get; }
public bool Initialized { get; private set; }
public bool IsSelected
{
get => _isSelected;
@@ -123,8 +119,8 @@ namespace FluentTerminal.App.ViewModels
{
if (IsSelected)
{
_applicationView.Title = Title;
_applicationView.RunOnDispatcherThread(() => NewOutput = false);
ApplicationView.Title = Title ?? string.Empty;
NewOutput = false;
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
@@ -155,6 +151,10 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand<string> SelectTabThemeCommand { get; }
public ISettingsService SettingsService { get; }
public ShellProfile ShellProfile { get; }
public bool ShowResizeOverlay
{
get => _showResizeOverlay;
@@ -178,6 +178,8 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _showSearchPanel, value);
}
public string StartupDirectory { get; }
public TabTheme TabTheme
{
get => _tabTheme;
@@ -191,118 +193,72 @@ namespace FluentTerminal.App.ViewModels
public ObservableCollection<TabTheme> TabThemes { get; }
public Terminal Terminal { get; private set; }
public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}
public string Title
{
get => _title;
set
{
value = string.IsNullOrWhiteSpace(value) ? DefaultTitle : value;
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();
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
return Terminal.Close();
}
public void CopyText(string text)
{
_clipboardService.SetText(text);
ClipboardService.SetText(text);
}
public Task FocusTerminal()
public async Task EditTitle()
{
return _terminalView?.FocusTerminal();
var result = await DialogService.ShowInputDialogAsync("Edit Title");
if (result != null)
{
Title = result;
}
}
public async Task OnViewIsReady(ITerminalView terminalView)
public void FocusTerminal()
{
_terminalView = terminalView;
var options = _settingsService.GetTerminalOptions();
var keyBindings = _settingsService.GetCommandKeyBindings();
var profiles = _settingsService.GetShellProfiles();
var size = await _terminalView.CreateTerminal(options, TerminalTheme.Colors, FlattenKeyBindings(keyBindings, profiles)).ConfigureAwait(true);
if (!string.IsNullOrWhiteSpace(_startupDirectory))
{
_shellProfile.WorkingDirectory = _startupDirectory;
}
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile).ConfigureAwait(true);
if (response.Success)
{
_terminalId = response.Id;
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
{
_connectedEvent.Wait();
_webSocket.Send(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;
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);
Activated?.Invoke(this, EventArgs.Empty);
}
private void CloseSearchPanel()
{
SearchText = string.Empty;
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);
}
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<string, ICollection<KeyBinding>> commandKeyBindings, IEnumerable<ShellProfile> profiles)
{
return commandKeyBindings.Values.SelectMany(k => k).Concat(profiles.SelectMany(x => x.KeyBindings));
FindPreviousRequested?.Invoke(this, SearchText);
}
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
await _applicationView.RunOnDispatcherThread(() =>
await ApplicationView.RunOnDispatcherThread(() =>
{
ApplicationSettings = e;
RaisePropertyChanged(nameof(IsUnderlined));
@@ -312,55 +268,21 @@ namespace FluentTerminal.App.ViewModels
private async void OnCurrentThemeChanged(object sender, Guid e)
{
await _applicationView.RunOnDispatcherThread(async () =>
await ApplicationView.RunOnDispatcherThread(() =>
{
// 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;
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();
var profiles = _settingsService.GetShellProfiles();
await _applicationView.RunOnDispatcherThread(async () =>
{
await _terminalView.ChangeKeyBindings(FlattenKeyBindings(keyBindings, profiles)).ConfigureAwait(false);
});
}
private async void OnKeyboardCommandReceived(object sender, string command)
{
if (command == nameof(Command.Copy))
{
var selection = await _terminalView.GetSelection().ConfigureAwait(true);
_clipboardService.SetText(selection);
}
else if (command == nameof(Command.Paste))
{
var content = await _clipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
}
}
else if (command == nameof(Command.Search))
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
_terminalView.FocusSearchTextBox();
}
}
else
{
_keyboardCommandService.SendCommand(command);
}
KeyBindingsChanged?.Invoke(this, EventArgs.Empty);
}
private void OnResizeOverlayTimerFinished(object sender, object e)
@@ -369,36 +291,9 @@ namespace FluentTerminal.App.ViewModels
ShowResizeOverlay = false;
}
private void OnTerminalExited(object sender, int e)
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
if (e == _terminalId)
{
_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;
OptionsChanged?.Invoke(this, e);
}
private void SelectTabTheme(string name)
@@ -406,20 +301,82 @@ namespace FluentTerminal.App.ViewModels
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
}
private async Task EditTitle()
private void Terminal_Closed(object sender, EventArgs e)
{
var result = await _dialogService.ShowInputDialogAsync("Edit Title");
if (result != null)
Closed?.Invoke(this, EventArgs.Empty);
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
{
switch (e)
{
Title = result;
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()
{
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)
{
@@ -427,8 +384,7 @@ namespace FluentTerminal.App.ViewModels
}
}
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
Closed?.Invoke(this, EventArgs.Empty);
await Close().ConfigureAwait(true);
}
}
}
+79 -34
View File
@@ -29,15 +29,14 @@ namespace FluentTerminal.App.ViewModels
private string _cursor;
private string _cursorAccent;
private string _cyan;
private string _fallBackAuthor;
private TerminalColors _fallBackColors;
private string _fallBackName;
private TerminalTheme _fallbackTheme;
private string _foreground;
private string _green;
private bool _inEditMode;
private bool _isActive;
private string _magenta;
private string _name;
private bool _isNew;
private string _red;
private string _selection;
private string _white;
@@ -46,12 +45,13 @@ namespace FluentTerminal.App.ViewModels
public event EventHandler<string> BackgroundChanged;
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService)
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, bool isNew)
{
Model = theme;
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_isNew = isNew;
Name = Model.Name;
Author = Model.Author;
@@ -299,42 +299,89 @@ namespace FluentTerminal.App.ViewModels
_settingsService.SaveTheme(Model);
InEditMode = false;
_isNew = false;
}
private async Task CancelEdit()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
if (_isNew)
{
Black = _fallBackColors.Black;
Red = _fallBackColors.Red;
Green = _fallBackColors.Green;
Yellow = _fallBackColors.Yellow;
Blue = _fallBackColors.Blue;
Magenta = _fallBackColors.Magenta;
Cyan = _fallBackColors.Cyan;
White = _fallBackColors.White;
await Delete();
}
else
{
TerminalTheme changedTheme = new TerminalTheme()
{
Name = Name,
Author = Author,
Colors = new TerminalColors()
{
Black = Black,
Red = Red,
Green = Green,
Yellow = Yellow,
Blue = Blue,
Magenta = Magenta,
Cyan = Cyan,
White = White,
BrightBlack = _fallBackColors.BrightBlack;
BrightRed = _fallBackColors.BrightRed;
BrightGreen = _fallBackColors.BrightGreen;
BrightYellow = _fallBackColors.BrightYellow;
BrightBlue = _fallBackColors.BrightBlue;
BrightMagenta = _fallBackColors.BrightMagenta;
BrightCyan = _fallBackColors.BrightCyan;
BrightWhite = _fallBackColors.BrightWhite;
BrightBlack = BrightBlack,
BrightRed = BrightRed,
BrightGreen = BrightGreen,
BrightYellow = BrightYellow,
BrightBlue = BrightBlue,
BrightMagenta = BrightMagenta,
BrightCyan = BrightCyan,
BrightWhite = BrightWhite,
Background = _fallBackColors.Background;
Foreground = _fallBackColors.Foreground;
Cursor = _fallBackColors.Cursor;
CursorAccent = _fallBackColors.CursorAccent;
Selection = _fallBackColors.Selection;
Background = Background,
Foreground = Foreground,
Cursor = Cursor,
CursorAccent = CursorAccent,
Selection = Selection
}
};
Name = _fallBackName;
Author = _fallBackAuthor;
if (!_fallbackTheme.Equals(changedTheme))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
InEditMode = false;
if (result == DialogButton.OK)
{
Black = _fallbackTheme.Colors.Black;
Red = _fallbackTheme.Colors.Red;
Green = _fallbackTheme.Colors.Green;
Yellow = _fallbackTheme.Colors.Yellow;
Blue = _fallbackTheme.Colors.Blue;
Magenta = _fallbackTheme.Colors.Magenta;
Cyan = _fallbackTheme.Colors.Cyan;
White = _fallbackTheme.Colors.White;
BrightBlack = _fallbackTheme.Colors.BrightBlack;
BrightRed = _fallbackTheme.Colors.BrightRed;
BrightGreen = _fallbackTheme.Colors.BrightGreen;
BrightYellow = _fallbackTheme.Colors.BrightYellow;
BrightBlue = _fallbackTheme.Colors.BrightBlue;
BrightMagenta = _fallbackTheme.Colors.BrightMagenta;
BrightCyan = _fallbackTheme.Colors.BrightCyan;
BrightWhite = _fallbackTheme.Colors.BrightWhite;
Background = _fallbackTheme.Colors.Background;
Foreground = _fallbackTheme.Colors.Foreground;
Cursor = _fallbackTheme.Colors.Cursor;
CursorAccent = _fallbackTheme.Colors.CursorAccent;
Selection = _fallbackTheme.Colors.Selection;
Name = _fallbackTheme.Name;
Author = _fallbackTheme.Author;
InEditMode = false;
}
}
else
{
InEditMode = false;
}
}
}
@@ -355,9 +402,7 @@ namespace FluentTerminal.App.ViewModels
private void Edit()
{
_fallBackColors = new TerminalColors(Model.Colors);
_fallBackName = Name;
_fallBackAuthor = Author;
_fallbackTheme = new TerminalTheme(Model);
InEditMode = true;
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Timer = System.Timers.Timer;
namespace FluentTerminal.App.ViewModels.Utilities
{
public class InputBuffer
{
private readonly Timer _timer = new Timer();
private readonly Func<string, Task> _writeCallback;
private readonly List<string> _buffer = new List<string>();
private readonly object _lock = new object();
public InputBuffer(Func<string, Task> writeCallback)
{
_timer.AutoReset = true;
_timer.Elapsed += _timer_Elapsed;
_writeCallback = writeCallback;
_timer.Interval = 5;
_timer.Start();
}
private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
lock (_lock)
{
var data = string.Concat(_buffer);
_buffer.Clear();
_writeCallback.Invoke(data);
}
}
public void Write(string data)
{
lock (_lock)
{
_buffer.Add(data);
}
}
}
}
@@ -3,6 +3,7 @@ using FluentTerminal.App.Services.EventArgs;
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation.Metadata;
using Windows.UI.Core;
using Windows.UI.Core.Preview;
using Windows.UI.ViewManagement;
@@ -29,6 +30,11 @@ namespace FluentTerminal.App.Adapters
set => _applicationView.Title = value;
}
public bool IsApiContractPresent(string api, ushort version)
{
return ApiInformation.IsApiContractPresent(api, version);
}
public Task RunOnDispatcherThread(Action action)
{
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
+1
View File
@@ -37,6 +37,7 @@
<converters:EnumValueToVisibiltyConverter x:Key="EnumValueToVisibiltyConverter" />
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
<converters:EnumToDescriptionConverter x:Key="EnumToDescriptionConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
+17 -9
View File
@@ -4,6 +4,7 @@ using FluentTerminal.App.Dialogs;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Adapters;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.EventArgs;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.App.ViewModels;
using FluentTerminal.App.Views;
@@ -48,6 +49,8 @@ namespace FluentTerminal.App
InitializeComponent();
UnhandledException += OnUnhandledException;
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
@@ -92,6 +95,11 @@ namespace FluentTerminal.App
_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)
{
if (args is CommandLineActivatedEventArgs commandLineActivated)
@@ -119,7 +127,7 @@ namespace FluentTerminal.App
}
else
{
await CreateNewTerminalWindow(parameter).ConfigureAwait(true);
await CreateNewTerminalWindow(parameter, false).ConfigureAwait(true);
}
}
}
@@ -151,7 +159,7 @@ namespace FluentTerminal.App
}
else if (_mainViewModels.Count == 0)
{
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, string.Empty).ConfigureAwait(true);
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, false, string.Empty).ConfigureAwait(true);
}
}
@@ -212,13 +220,13 @@ namespace FluentTerminal.App
Window.Current.Activate();
}
private async Task CreateNewTerminalWindow(string startupDirectory)
private async Task CreateNewTerminalWindow(string startupDirectory, bool showProfileSelection)
{
var id = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, startupDirectory).ConfigureAwait(true);
var id = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, showProfileSelection, startupDirectory).ConfigureAwait(true);
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(id);
}
private async Task<int> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar, object parameter)
private async Task<int> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar, bool showProfileSelection, object parameter)
{
int windowId = 0;
TViewModel viewModel = default;
@@ -242,7 +250,7 @@ namespace FluentTerminal.App
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(mainViewModel);
await mainViewModel.AddTerminal(directory, false, Guid.Empty).ConfigureAwait(true);
await mainViewModel.AddTerminal(directory, showProfileSelection, Guid.Empty).ConfigureAwait(true);
}
if (viewModel is SettingsViewModel settingsViewModel)
@@ -271,9 +279,9 @@ namespace FluentTerminal.App
}
}
private async void OnNewWindowRequested(object sender, EventArgs e)
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
await CreateNewTerminalWindow(string.Empty).ConfigureAwait(true);
await CreateNewTerminalWindow(string.Empty, e.ShowProfileSelection).ConfigureAwait(true);
}
private void OnSettingsClosed(object sender, EventArgs e)
@@ -303,7 +311,7 @@ namespace FluentTerminal.App
{
if (_settingsViewModel == null)
{
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, null).ConfigureAwait(true);
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, false, null).ConfigureAwait(true);
}
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
}
@@ -0,0 +1,24 @@
using System;
using FluentTerminal.App.Services.Utilities;
using Windows.UI.Xaml.Data;
namespace FluentTerminal.App.Converters
{
public class EnumToDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
// value is the Enum, parameter is the string.
if (value is Enum enumValue)
{
return EnumHelper.GetEnumDescription(enumValue);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
}
@@ -7,6 +7,7 @@
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
CloseButtonText="Cancel"
PrimaryButtonText="OK"
KeyDown="Dialog_KeyUp"
mc:Ignorable="d">
<TextBox VerticalAlignment="Center" Text="{x:Bind Input, Mode=TwoWay}" />
</ContentDialog>
+14 -1
View File
@@ -2,11 +2,15 @@
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
using System;
using Windows.UI.Xaml.Input;
using Windows.System;
namespace FluentTerminal.App.Dialogs
{
public sealed partial class InputDialog : ContentDialog, IInputDialog
{
private bool EnterPressed = false;
public string Input { get; private set; }
public InputDialog()
@@ -17,7 +21,7 @@ namespace FluentTerminal.App.Dialogs
public async Task<string> GetInput()
{
var result = await ShowAsync();
if (result == ContentDialogResult.Primary)
if (result == ContentDialogResult.Primary || EnterPressed)
{
return Input;
}
@@ -28,5 +32,14 @@ namespace FluentTerminal.App.Dialogs
{
Title = title;
}
void Dialog_KeyUp(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
EnterPressed = true;
Hide();
}
}
}
}
+23 -2
View File
@@ -17,8 +17,8 @@
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WindowsXamlEnableOverview>true</WindowsXamlEnableOverview>
<PackageCertificateKeyFile>FluentTerminal.App_StoreKey.pfx</PackageCertificateKeyFile>
<PackageCertificateThumbprint>8FA8EF727E767FAE5409DDD1694C1CEE054A2962</PackageCertificateThumbprint>
<PackageCertificateKeyFile>FluentTerminal.App_TemporaryKey.pfx</PackageCertificateKeyFile>
<PackageCertificateThumbprint>F96F3E0E485F07E8E98DA6E5C06AA4B6F112B498</PackageCertificateThumbprint>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<AppxBundle>Always</AppxBundle>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
@@ -113,6 +113,7 @@
<Compile Include="Behaviors\MiddleClickBehavior.cs" />
<Compile Include="Converters\BackgroundToApplicationThemeConverter.cs" />
<Compile Include="Converters\ColorResourceKeyFallbackConverter.cs" />
<Compile Include="Converters\EnumToDescriptionConverter.cs" />
<Compile Include="Converters\TabColorFallbackConverter.cs" />
<Compile Include="Converters\EnumValueToVisibiltyConverter.cs" />
<Compile Include="Converters\FalseToVisibleConverter.cs" />
@@ -175,14 +176,23 @@
<DependentUpon>TabBar.xaml</DependentUpon>
</Compile>
<Compile Include="Views\TabBarBackgroundBindingHelper.cs" />
<Compile Include="Views\TerminalKeybindTemplateSelector.cs" />
<Compile Include="Views\TabThemeTemplateSelector.cs" />
<Compile Include="Views\TerminalColorPicker.xaml.cs">
<DependentUpon>TerminalColorPicker.xaml</DependentUpon>
</Compile>
<Compile Include="Views\NoValueTemplateSelector.cs" />
<Compile Include="Views\TerminalThemeTemplateSelector.cs" />
<Compile Include="Views\ITerminalView.cs" />
<Compile Include="Views\TerminalView.xaml.cs">
<DependentUpon>TerminalView.xaml</DependentUpon>
</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>
<AppxManifest Include="Package.appxmanifest">
@@ -347,6 +357,14 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</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>
<PackageReference Include="Autofac">
@@ -367,6 +385,9 @@
<PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version>
</PackageReference>
<PackageReference Include="VtNetCore">
<Version>1.0.15</Version>
</PackageReference>
<PackageReference Include="Win2D.uwp">
<Version>1.21.0</Version>
</PackageReference>
Binary file not shown.
+4
View File
@@ -1,5 +1,9 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Thickness x:Key="HeaderMargin">24,0,0,0</Thickness>
<Thickness x:Key="ContainerMargin">24,0,24,24</Thickness>
<Thickness x:Key="ItemMargin">0,24,0,0</Thickness>
<Style
x:Key="WindowTitleTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
+17
View File
@@ -0,0 +1,17 @@
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Views
{
public interface ITerminalView
{
Task ChangeTheme(TerminalTheme theme);
Task ChangeKeyBindings();
Task ChangeOptions(TerminalOptions options);
Task Initialize(TerminalViewModel viewModel);
Task FindNext(string searchText);
Task FindPrevious(string searchText);
Task FocusTerminal();
}
}
+2
View File
@@ -81,6 +81,8 @@
IsHitTestVisible="False"
Style="{StaticResource WindowTitleTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedTerminal.Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind ViewModel.ShowTabsOnTop, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
<views:TabBar
Grid.Column="1"
+2 -2
View File
@@ -62,11 +62,11 @@ namespace FluentTerminal.App.Views
}
}
private async void OnWindowActivated(object sender, WindowActivatedEventArgs e)
private void OnWindowActivated(object sender, WindowActivatedEventArgs e)
{
if (e.WindowActivationState != CoreWindowActivationState.Deactivated && TerminalContainer.Content is TerminalView terminal)
{
await terminal.FocusWebView().ConfigureAwait(true);
terminal.ViewModel.FocusTerminal();
}
}
@@ -0,0 +1,26 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public class NoValueTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultTemplate { get; set; }
public DataTemplate CustomTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
protected override DataTemplate SelectTemplateCore(object item)
{
if (item is String arguments)
{
return String.Empty.Equals(arguments) ? DefaultTemplate : CustomTemplate;
}
return null;
}
}
}
@@ -6,14 +6,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,0,0,24">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -36,52 +35,56 @@
Label="Check for updates" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
<Grid>
<ScrollViewer Grid.Row="2">
<Grid Margin="{StaticResource ContainerMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<!-- App Logo -->
<Image
MaxWidth="106"
MaxHeight="106"
Margin="24,0,24,0"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Source="/Assets/AppIcons/Square106x106Logo.black.png"
Stretch="None" />
<!-- Current version -->
<TextBlock Margin="0,24,0,8" Text="Current version" />
<TextBlock><Run Text="{x:Bind ViewModel.CurrentVersion}" />
- <Hyperlink NavigateUri="{x:Bind ViewModel.CurrentVersionReleaseNotesURL}">Release notes</Hyperlink></TextBlock>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Current version" />
<TextBlock>
<Run Text="{x:Bind ViewModel.CurrentVersion}" />
-<Hyperlink NavigateUri="{x:Bind ViewModel.CurrentVersionReleaseNotesURL}">Release notes</Hyperlink>
</TextBlock>
</StackPanel>
<!-- Latest version -->
<TextBlock Margin="0,24,0,8" Text="Latest version" />
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionLoading, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}"><Italic>loading...</Italic></TextBlock>
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionNotFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}"><Italic>Not found</Italic></TextBlock>
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<Run Text="{x:Bind ViewModel.LatestVersion, Mode=OneWay}" />
- <Hyperlink NavigateUri="{x:Bind ViewModel.LatestVersionReleaseNotesURL, Mode=OneWay}">
Release notes
</Hyperlink>
</TextBlock>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Latest version" />
<StackPanel Orientation="Horizontal" Visibility="{x:Bind ViewModel.LatestVersionLoading, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<ProgressRing Margin="0,0,8,0" IsActive="True" />
<TextBlock><Italic>Checking for updates...</Italic></TextBlock>
</StackPanel>
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionNotFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<Italic>Not found</Italic>
</TextBlock>
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<Run Text="{x:Bind ViewModel.LatestVersion, Mode=OneWay}" />
-<Hyperlink NavigateUri="{x:Bind ViewModel.LatestVersionReleaseNotesURL, Mode=OneWay}">Release notes</Hyperlink>
</TextBlock>
</StackPanel>
<!-- License -->
<TextBlock Margin="0,24,0,8" Text="License" />
<TextBlock><Hyperlink NavigateUri="https://github.com/felixse/FluentTerminal/blob/master/LICENSE">GNU General Public License v3.0</Hyperlink></TextBlock>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="License" />
<TextBlock><Hyperlink NavigateUri="https://github.com/felixse/FluentTerminal/blob/master/LICENSE">GNU General Public License v3.0</Hyperlink></TextBlock>
</StackPanel>
</StackPanel>
<StackPanel
Grid.Row="1"
Margin="0,72,0,24"
Margin="0,72,0,0"
VerticalAlignment="Bottom">
<!-- Created by... -->
<TextBlock>
Created by <Italic>felixse</Italic>
and <Hyperlink NavigateUri="https://github.com/felixse/FluentTerminal/graphs/contributors">contributors</Hyperlink></TextBlock>
<TextBlock x:Name="CreatedBy" />
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Page>
</Page>
@@ -1,5 +1,7 @@
using FluentTerminal.App.ViewModels.Settings;
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents;
using Windows.UI.Xaml.Navigation;
namespace FluentTerminal.App.Views.SettingsPages
@@ -11,6 +13,10 @@ namespace FluentTerminal.App.Views.SettingsPages
public About()
{
InitializeComponent();
CreatedBy.Inlines.Add(new Run { Text = "Created by " });
CreatedBy.Inlines.Add(new Italic { Inlines = { new Run { Text = "felixse " } } });
CreatedBy.Inlines.Add(new Run { Text = "and " });
CreatedBy.Inlines.Add(new Hyperlink { Inlines = { new Run { Text = "contributors" } }, NavigateUri = new Uri("https://github.com/felixse/FluentTerminal/graphs/contributors") });
}
protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -11,7 +11,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,00,0,24">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -34,75 +34,93 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
<StackPanel>
<ScrollViewer Grid.Row="2">
<StackPanel Margin="{StaticResource ContainerMargin}">
<StackPanel Margin="{StaticResource ItemMargin}">
<ToggleSwitch
Header="Run on startup"
IsEnabled="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay}"
IsOn="{x:Bind ViewModel.StartupTaskEnabled, Mode=TwoWay}" />
<TextBlock
Foreground="Red"
Text="{x:Bind ViewModel.StartupTaskErrorMessage, Mode=OneWay}"
Visibility="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<ToggleSwitch Header="Show in system tray" IsOn="{x:Bind ViewModel.EnableTrayIcon, Mode=TwoWay}" />
<TextBlock
Foreground="Red"
Text="Relaunch required to apply setting"
Visibility="{x:Bind ViewModel.ShouldRestartForTrayMessage, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Open new terminals as" />
<RadioButton
Content="Tabs"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.TabIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Windows"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.WindowIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Show tabs on" />
<RadioButton
Content="Top"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.TopIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Bottom"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.BottomIsSelected, Mode=TwoWay}" />
</StackPanel>
<ToggleSwitch
Header="Run on startup"
IsEnabled="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay}"
IsOn="{x:Bind ViewModel.StartupTaskEnabled, Mode=TwoWay}" />
<TextBlock
Foreground="Red"
Text="{x:Bind ViewModel.StartupTaskErrorMessage, Mode=OneWay}"
Visibility="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
<ToggleSwitch
Margin="0,24,0,0"
Header="Show in system tray"
IsOn="{x:Bind ViewModel.EnableTrayIcon, Mode=TwoWay}" />
<TextBlock
Foreground="Red"
Text="Relaunch required to apply setting"
Visibility="{x:Bind ViewModel.ShouldRestartForTrayMessage, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<TextBlock Margin="0,24,0,8" Text="Open new terminals as" />
<RadioButton
Content="Tabs"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.TabIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Windows"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.WindowIsSelected, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Show tabs on" />
<RadioButton
Content="Top"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.TopIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Bottom"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.BottomIsSelected, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Always show tabs"
IsOn="{x:Bind ViewModel.AlwaysShowTabs, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Underline selected tab"
IsOn="{x:Bind ViewModel.UnderlineSelectedTab, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Color on inactive tabs" />
<RadioButton
Content="Background"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.BackgroundIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Underline"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.UnderlinedIsSelected, Mode=TwoWay}" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Color on inactive tabs" />
<RadioButton
Content="Background"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.BackgroundIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Underline"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.UnderlinedIsSelected, Mode=TwoWay}" />
</StackPanel>
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Ask before closing windows"
IsOn="{x:Bind ViewModel.ConfirmClosingWindows, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Ask before closing tabs"
IsOn="{x:Bind ViewModel.ConfirmClosingTabs, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Show new output indicator on background tabs"
IsOn="{x:Bind ViewModel.ShowNewOutputIndicator, Mode=TwoWay}" />
<ToggleSwitch
Margin="{StaticResource ItemMargin}"
Header="Windows 10 1809+: Always use WinPty"
IsOn="{x:Bind ViewModel.AlwaysUseWinPty, Mode=TwoWay}" />
</StackPanel>
</ScrollViewer>
</Grid>
</Page>
</Page>
@@ -13,7 +13,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,0,0,0">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -42,11 +42,11 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,24,0,0">
<ItemsControl ItemsSource="{x:Bind ViewModel.KeyBindings, Mode=OneWay}">
<ScrollViewer Grid.Row="2">
<ItemsControl Margin="{StaticResource ContainerMargin}" ItemsSource="{x:Bind ViewModel.KeyBindings, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
<views:KeyBindingsView Margin="0,0,0,24" ViewModel="{x:Bind}" />
<views:KeyBindingsView Margin="{StaticResource ItemMargin}" ViewModel="{x:Bind}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -6,14 +6,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,00,0,24">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -36,40 +35,44 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
<StackPanel>
<ScrollViewer Grid.Row="2">
<StackPanel Margin="{StaticResource ContainerMargin}">
<ToggleSwitch
Margin="0,0,0,24"
Header="Copy on select"
IsOn="{x:Bind ViewModel.CopyOnSelect, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Middle button action" />
<RadioButton
Content="Nothing"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Paste"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickPasteIsSelected, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Right button action" />
<RadioButton
Content="Nothing"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Paste"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickPasteIsSelected, Mode=TwoWay}" />
Margin="{StaticResource ItemMargin}"
Header="Copy on select"
IsOn="{x:Bind ViewModel.CopyOnSelect, Mode=TwoWay}" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Middle button action" />
<RadioButton
Content="Nothing"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Paste"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickPasteIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Right button action" />
<RadioButton
Content="Nothing"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Paste"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickPasteIsSelected, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
@@ -48,6 +48,33 @@
</DataTemplate>
</views:TerminalThemeTemplateSelector.ThemeTemplate>
</views:TerminalThemeTemplateSelector>
<views:TerminalKeybindTemplateSelector x:Key="TerminalKeybindTemplateSelector">
<views:TerminalKeybindTemplateSelector.DefaultTemplate>
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
<TextBlock FontStyle="Italic" Text="None" />
</DataTemplate>
</views:TerminalKeybindTemplateSelector.DefaultTemplate>
<views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
<views:KeyBindingsView
Margin="0,6,0,0"
ShowCommandName="False"
ViewModel="{x:Bind}" />
</DataTemplate>
</views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
</views:TerminalKeybindTemplateSelector>
<views:NoValueTemplateSelector x:Key="NoValueTemplateSelector">
<views:NoValueTemplateSelector.DefaultTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock FontStyle="Italic" Text="None" />
</DataTemplate>
</views:NoValueTemplateSelector.DefaultTemplate>
<views:NoValueTemplateSelector.CustomTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{x:Bind}" />
</DataTemplate>
</views:NoValueTemplateSelector.CustomTemplate>
</views:NoValueTemplateSelector>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions>
@@ -63,16 +90,13 @@
Grid.RowSpan="3"
Grid.Column="0"
Background="{ThemeResource SystemControlAcrylicWindowBrush}" />
<Grid
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,0,0,24">
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="24,10,0,0"
Margin="0,10,0,0"
VerticalAlignment="Center"
FontSize="28"
Style="{StaticResource TitleTextBlockStyle}"
@@ -130,7 +154,7 @@
Grid.Row="2"
Grid.Column="0"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<ListView ItemsSource="{x:Bind ViewModel.ShellProfiles}" SelectedItem="{x:Bind ViewModel.SelectedShellProfile, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:ShellProfileViewModel">
@@ -181,23 +205,26 @@
<ScrollViewer
Grid.Row="2"
Grid.Column="1"
Margin="24,0,0,0"
HorizontalAlignment="Stretch"
VerticalScrollBarVisibility="Auto">
<ContentControl HorizontalContentAlignment="Stretch" Content="{x:Bind ViewModel.SelectedShellProfile, Mode=OneWay}">
<ContentControl
Margin="{StaticResource ContainerMargin}"
HorizontalContentAlignment="Stretch"
Content="{x:Bind ViewModel.SelectedShellProfile, Mode=OneWay}">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="viewmodels:ShellProfileViewModel">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<StackPanel Margin="0,0,24,0" Visibility="{x:Bind InEditMode, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
<StackPanel Visibility="{x:Bind InEditMode, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
<TextBox
Width="300"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Name"
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
IsSpellCheckEnabled="False"
Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Margin="0,24,0,0" Orientation="Horizontal">
<StackPanel Margin="{StaticResource ItemMargin}" Orientation="Horizontal">
<TextBox
Width="300"
Header="Shell executable location"
@@ -213,7 +240,7 @@
Style="{StaticResource ButtonRevealStyle}" />
</StackPanel>
<StackPanel Margin="0,24,0,0" Orientation="Horizontal">
<StackPanel Margin="{StaticResource ItemMargin}" Orientation="Horizontal">
<TextBox
Width="300"
Header="Working directory"
@@ -230,83 +257,122 @@
<TextBox
Width="300"
Margin="0,24,0,0"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Arguments"
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
IsSpellCheckEnabled="False"
Text="{x:Bind Arguments, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox
Width="150"
Margin="0,24,0,0"
Margin="{StaticResource ItemMargin}"
Header="Tab color"
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
ItemTemplateSelector="{StaticResource TabThemeTemplateSelector}"
ItemsSource="{x:Bind TabThemes, Mode=OneWay}"
SelectedItem="{Binding SelectedTabTheme, Mode=TwoWay}" />
<ComboBox
Width="150"
Margin="0,24,0,0"
Margin="{StaticResource ItemMargin}"
Header="Theme"
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
ItemTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
ItemsSource="{x:Bind TerminalThemes, Mode=OneWay}"
SelectedItem="{Binding SelectedTerminalTheme, Mode=TwoWay}" />
<StackPanel
Width="300"
Margin="0,24,0,0"
HorizontalAlignment="Left"
Orientation="Horizontal">
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Line ending translation mode on paste" />
<RadioButton
Content="Do not modify line endings"
GroupName="LineEndingStyle"
IsChecked="{x:Bind DoNotModifyIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Convert line endings to CR+LF"
GroupName="LineEndingStyle"
IsChecked="{x:Bind ToCRLFIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Convert line endings to LF"
GroupName="LineEndingStyle"
IsChecked="{x:Bind ToLFIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock
Margin="0,0,6,0"
VerticalAlignment="Center"
Text="Keyboard shortcut" />
<HyperlinkButton Command="{x:Bind AddKeyboardShortcutCommand}" Content="Add" />
<views:KeyBindingsView ShowCommandName="False" ViewModel="{x:Bind KeyBindings}" />
</StackPanel>
<views:KeyBindingsView ShowCommandName="False" ViewModel="{x:Bind KeyBindings}" />
</StackPanel>
<StackPanel Margin="0,0,24,0" Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind Name, Mode=OneWay}" />
<StackPanel Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
<TextBlock
Margin="{StaticResource ItemMargin}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Name, Mode=OneWay}" />
<controls:HeaderedTextBlock
Margin="0,58,0,0"
Margin="{StaticResource ItemMargin}"
Header="Shell executable location"
Text="{x:Bind Location, Mode=OneWay}" />
<controls:HeaderedTextBlock
Margin="0,36,0,0"
Header="Working directory"
Text="{x:Bind WorkingDirectory, Mode=OneWay}" />
<controls:HeaderedTextBlock
Margin="0,36,0,0"
Header="Arguments"
Text="{x:Bind Arguments, Mode=OneWay}" />
<controls:HeaderedContentControl
Margin="0,36,0,0"
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Theme">
Header="Working directory">
<ContentControl
Margin="0,6,0,0"
Content="{x:Bind SelectedTerminalTheme, Mode=OneWay}"
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}" />
Content="{x:Bind WorkingDirectory, Mode=OneWay}"
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="0,36,0,0"
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Arguments">
<ContentControl
Content="{x:Bind Arguments, Mode=OneWay}"
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Tab color">
<ContentControl
Margin="0,6,0,0"
Content="{x:Bind SelectedTabTheme, Mode=OneWay}"
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="0,36,0,0"
HorizontalContentAlignment="Stretch"
Header="Keyboard shortcut">
<views:KeyBindingsView
Margin="0,6,0,0"
ShowCommandName="False"
ViewModel="{x:Bind KeyBindings}" />
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Line ending translation mode">
<ContentControl
Content="{x:Bind LineEndingStyle, Mode=OneWay, Converter={StaticResource EnumToDescriptionConverter}}"
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Keyboard shortcut">
<ContentControl
Content="{x:Bind KeyBindings, Mode=OneWay}"
ContentTemplateSelector="{StaticResource TerminalKeybindTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
</StackPanel>
</Grid>
</DataTemplate>
@@ -314,4 +380,4 @@
</ContentControl>
</ScrollViewer>
</Grid>
</Page>
</Page>
@@ -15,7 +15,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,00,0,24">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -38,32 +38,40 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
<StackPanel>
<ScrollViewer Grid.Row="2">
<StackPanel Margin="{StaticResource ContainerMargin}">
<ComboBox
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Font family"
ItemsSource="{x:Bind ViewModel.Fonts}"
SelectedValue="{x:Bind ViewModel.FontFamily, Mode=TwoWay}" />
<ComboBox
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Font size"
ItemsSource="{x:Bind ViewModel.Sizes}"
SelectedValue="{x:Bind ViewModel.FontSize, Mode=TwoWay}" />
<ToggleSwitch
Margin="{StaticResource ItemMargin}"
Header="Bold text"
IsOn="{x:Bind ViewModel.BoldText, Mode=TwoWay}" />
<Slider
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Background opacity"
Maximum="1"
Minimum="0"
StepFrequency="0.01"
Value="{x:Bind ViewModel.BackgroundOpacity, Mode=TwoWay}" />
<Slider
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Padding"
Maximum="64"
@@ -71,45 +79,52 @@
StepFrequency="1"
ThumbToolTipValueConverter="{StaticResource ToolTipValueToPixelConverter}"
Value="{x:Bind ViewModel.Padding, Mode=TwoWay}" />
<TextBox
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Scrollback limit"
KeyDown="ScrollBackTextBoxOnKeyDown"
Text="{x:Bind ViewModel.ScrollBackLimit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Blinking cursor"
IsOn="{x:Bind ViewModel.CursorBlink, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Cursor style" />
<RadioButton
Content="Block"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BlockIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Underline"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.UnderlineIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Bar"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BarIsSelected, Mode=TwoWay}" />
<TextBlock Margin="0,24,0,8" Text="Scrollbar style" />
<RadioButton
Content="Hidden"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.HiddenIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Auto hiding"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.AutoHidingIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Visible"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.VisibleIsSelected, Mode=TwoWay}" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Cursor style" />
<RadioButton
Content="Block"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BlockIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Underline"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.UnderlineIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Bar"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BarIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Scrollbar style" />
<RadioButton
Content="Hidden"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.HiddenIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Auto hiding"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.AutoHidingIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Visible"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.VisibleIsSelected, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>
</Page>
@@ -28,16 +28,13 @@
Grid.RowSpan="3"
Grid.Column="0"
Background="{ThemeResource SystemControlAcrylicWindowBrush}" />
<Grid
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,0,0,24">
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="24,10,0,0"
Margin="0,10,0,0"
VerticalAlignment="Center"
FontSize="28"
Style="{StaticResource TitleTextBlockStyle}"
@@ -103,7 +100,7 @@
Grid.Row="2"
Grid.Column="0"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<ListView ItemsSource="{x:Bind ViewModel.Themes}" SelectedItem="{x:Bind ViewModel.SelectedTheme, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:ThemeViewModel">
@@ -175,15 +172,16 @@
<ContentControl
Grid.Row="2"
Grid.Column="1"
Margin="24,0,0,0"
HorizontalContentAlignment="Stretch"
Content="{x:Bind ViewModel.SelectedTheme, Mode=TwoWay}"
RequestedTheme="{x:Bind ViewModel.SelectedTheme.Background, Mode=OneWay, Converter={StaticResource BackgroundToApplicationThemeConverter}}">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="viewmodels:ThemeViewModel">
<ScrollViewer Padding="0,0,24,0" VerticalScrollBarVisibility="Auto">
<StackPanel>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="{StaticResource ContainerMargin}">
<!-- VIEW THEME -->
<TextBlock
Margin="{StaticResource ItemMargin}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Name, Mode=OneWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
@@ -191,22 +189,42 @@
<SolidColorBrush Color="{x:Bind Foreground, Mode=OneWay}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind Author, Mode=OneWay}" />
<TextBlock
Margin="{StaticResource ItemMargin}"
FontSize="14"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Author, Mode=OneWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
<TextBlock.Foreground>
<SolidColorBrush Color="{x:Bind Foreground, Mode=OneWay}" />
</TextBlock.Foreground>
</TextBlock>
<!-- EDIT THEME -->
<TextBox
Width="200"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Name"
Text="{x:Bind Name, Mode=TwoWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<TextBox
Margin="0,24,0,0"
Width="200"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Author"
Text="{x:Bind Author, Mode=TwoWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<Grid Margin="0,48,0,0">
<Grid Margin="{StaticResource ItemMargin}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="Border">
@@ -215,7 +233,16 @@
<Setter Property="Height" Value="24" />
</Style>
</Grid.Resources>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Grid.Row="0" Text="Color palette" />
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
FontSize="14"
FontStyle="Italic"
Foreground="LightGray"
Text="Click on a color to edit"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<StackPanel Grid.Row="2" Orientation="Horizontal">
<views:TerminalColorPicker
ColorName="Black"
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
@@ -249,7 +276,7 @@
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
SelectedColor="{x:Bind White, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=false}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<StackPanel Grid.Row="3" Orientation="Horizontal">
<views:TerminalColorPicker
ColorName="Bright Black"
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
@@ -283,7 +310,7 @@
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
SelectedColor="{x:Bind BrightWhite, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=false}" />
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<StackPanel Grid.Row="4" Orientation="Horizontal">
<views:TerminalColorPicker
ColorName="Foreground"
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
@@ -306,11 +333,6 @@
IsAlphaEnabled="True"
SelectedColor="{x:Bind Selection, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=true}" />
</StackPanel>
<TextBlock
Grid.Row="3"
Margin="0,6,0,0"
Text="Click on a color to edit"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
</Grid>
</StackPanel>
</ScrollViewer>
@@ -318,4 +340,4 @@
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Page>
</Page>
@@ -9,10 +9,10 @@
mc:Ignorable="d">
<Button
x:Name="Root"
Background="{x:Bind ButtonBackground, Mode=OneWay}"
IsEnabled="{x:Bind EnableEditing, Mode=OneWay}"
Style="{StaticResource ColorPreviewButtonStyle}"
ToolTipService.ToolTip="{x:Bind ColorName}"
Background="{x:Bind ButtonBackground, Mode=OneWay}">
ToolTipService.ToolTip="{x:Bind ColorName}">
<!--<Button.Background>
<SolidColorBrush Color="{x:Bind VisibleColor, Mode=OneWay}" />
</Button.Background>-->
@@ -7,7 +7,7 @@ namespace FluentTerminal.App.Views
{
public sealed partial class TerminalColorPicker : UserControl
{
bool colorIsNull = false;
private bool _colorIsNull;
public static readonly DependencyProperty ColorNameProperty =
DependencyProperty.Register(nameof(ColorName), typeof(string), typeof(TerminalColorPicker), new PropertyMetadata(null));
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Views
{
if (s is TerminalColorPicker terminalColorPicker)
{
terminalColorPicker.colorIsNull = ((Color?)e.NewValue) == null;
terminalColorPicker._colorIsNull = ((Color?)e.NewValue) == null;
// When leading the color, or changing the selected color (happens on page load)
// only set the picker (triggering the event action later) if the color is non-null
@@ -0,0 +1,26 @@
using FluentTerminal.App.ViewModels.Settings;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public class TerminalKeybindTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultTemplate { get; set; }
public DataTemplate KeyBindingsTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
protected override DataTemplate SelectTemplateCore(object item)
{
if (item is KeyBindingsViewModel keyBindings)
{
return keyBindings.KeyBindings.Count == 0 ? DefaultTemplate : KeyBindingsTemplate;
}
return null;
}
}
}
+2 -2
View File
@@ -12,7 +12,7 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid x:Name="WebViewContainer" Grid.Row="0" />
<Grid x:Name="TerminalContainer" Grid.Row="0" />
<Grid
Grid.Row="0"
Margin="12"
@@ -38,7 +38,7 @@
Name="SearchTextBox"
Width="300"
Margin="6,0,6,0"
KeyUp="SearchTextBox_KeyUp"
KeyUp="OnSearchTextBoxKeyUp"
PlaceholderText="Enter search term"
RelativePanel.RightOf="SearchLabel"
Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
+30 -258
View File
@@ -1,248 +1,68 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels;
using FluentTerminal.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 FluentTerminal.App.ViewModels;
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.UI;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public sealed partial class TerminalView : UserControl, ITerminalView, ITerminalEventListener
public sealed partial class TerminalView : UserControl
{
private MenuFlyoutItem _copyMenuItem;
private BlockingCollection<Action> _dispatcherJobs;
private readonly SemaphoreSlim _loaded;
private MenuFlyoutItem _pasteMenuItem;
private WebView _webView;
private readonly ITerminalView _terminalView;
public TerminalView(TerminalViewModel 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();
Loaded += OnLoaded;
_loaded = new SemaphoreSlim(0, 1);
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(true));
return settings;
};
StartMediatorTask();
//_terminalView = new XtermTerminalView();
_terminalView = new VtNetCoreTerminalView();
TerminalContainer.Children.Add((UIElement)_terminalView);
_terminalView.Initialize(ViewModel);
}
public event EventHandler<string> KeyboardCommandReceived;
public event EventHandler<TerminalSize> TerminalSizeChanged;
public event EventHandler<string> TerminalTitleChanged;
public TerminalViewModel ViewModel { get; }
public Task ChangeOptions(TerminalOptions options)
private async void OnActivated(object sender, EventArgs e)
{
var serialized = JsonConvert.SerializeObject(options);
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)
{
var serializedOptions = JsonConvert.SerializeObject(options);
var serializedTheme = JsonConvert.SerializeObject(theme);
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}')").ConfigureAwait(true);
return JsonConvert.DeserializeObject<TerminalSize>(size);
}
public Task<string> ExecuteScriptAsync(string script)
{
try
if (_terminalView != null)
{
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);
}
public async Task FocusTerminal()
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
await _terminalView.FocusTerminal().ConfigureAwait(true);
}
}
public async Task FocusWebView()
private async void OnFindNextRequested(object sender, string e)
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
}
await _terminalView.FindNext(e).ConfigureAwait(true);
}
public Task<string> GetSelection() => ExecuteScriptAsync("term.getSelection()");
public void OnKeyboardCommand(string command)
private async void OnFindPreviousRequested(object sender, string e)
{
if (Enum.TryParse(command, true, out Command commandValue))
{
_dispatcherJobs.Add(() => KeyboardCommandReceived?.Invoke(this, commandValue.ToString()));
}
else if (Guid.TryParse(command, out Guid shellProfileId))
{
_dispatcherJobs.Add(() => KeyboardCommandReceived?.Invoke(this, shellProfileId.ToString()));
}
await _terminalView.FindPrevious(e).ConfigureAwait(true);
}
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)
{
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)
private void OnSearchTextBoxKeyUp(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
{
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);
}
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));
await _terminalView.ChangeTheme(e).ConfigureAwait(true);
}
}
}
@@ -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;
}
}
}
+3 -3
View File
@@ -4305,9 +4305,9 @@
"dev": true
},
"xterm": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-3.10.1.tgz",
"integrity": "sha512-RHaUwJ8zwLiICu1QsXoxUHP+R2Pp8Rc8yVoNali/nKw3CVXwmXxT/4mgbk7U22psuNgOqLyI4Sg9nlQfYeTRQw=="
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-3.11.0.tgz",
"integrity": "sha512-VB9+s2Fhig31pNBkbxPnz+/wdCvxdQ6JQ0HZmWDwpULV8iAggzxoyUonB4FR+WW3lj0LNVb/ZaD64rxbw+HB4A=="
},
"y18n": {
"version": "4.0.0",
+1 -1
View File
@@ -14,6 +14,6 @@
"webpack-cli": "^3.1.0"
},
"dependencies": {
"xterm": "^3.10.1"
"xterm": "^3.11.0"
}
}
+1
View File
@@ -5,6 +5,7 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="xterm.css" />
<link rel="stylesheet" href="style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
</head>
<body>
+14 -9
View File
@@ -13,7 +13,7 @@ Terminal.applyAddon(search);
var term, socket;
var terminalContainer = document.getElementById('terminal-container');
function createTerminal(options, theme, keyBindings) {
function createTerminal(options, theme, keyBindings, sessionType) {
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
@@ -29,6 +29,8 @@ function createTerminal(options, theme, keyBindings) {
var terminalOptions = {
fontFamily: options.fontFamily,
fontSize: options.fontSize,
fontWeight: options.boldText ? 'bold' : 'normal',
fontWeightBold: options.boldText ? 'bolder' : 'bold',
cursorStyle: options.cursorStyle,
cursorBlink: options.cursorBlink,
bellStyle: options.bellStyle,
@@ -56,7 +58,11 @@ function createTerminal(options, theme, keyBindings) {
term.open(terminalContainer);
term.fit();
term.winptyCompatInit();
if (sessionType === 'WinPty') {
term.winptyCompatInit();
}
term.focus();
setPadding(options.padding);
@@ -139,20 +145,19 @@ function changeOptions(options) {
term.setOption('cursorStyle', options.cursorStyle);
term.setOption('fontFamily', options.fontFamily);
term.setOption('fontSize', options.fontSize);
term.setOption('fontWeight', options.boldText ? 'bold' : 'normal');
term.setOption('fontWeightBold', options.boldText ? 'bolder' : 'bold');
term.setOption('scrollback', options.scrollBackLimit);
setScrollBarStyle(options.scrollBarStyle);
setPadding(options.padding);
}
function setScrollBarStyle(scrollBarStyle) {
if (scrollBarStyle == 'hidden') {
document.getElementById('terminal-container').style['-ms-overflow-style'] = 'none';
} else if (scrollBarStyle == 'autoHiding') {
document.getElementById('terminal-container').style['-ms-overflow-style'] = '-ms-autohiding-scrollbar';
} else if (scrollBarStyle == 'visible') {
document.getElementById('terminal-container').style['-ms-overflow-style'] = 'scrollbar';
switch (scrollBarStyle) {
case 'hidden': return terminalContainer.style['-ms-overflow-style'] = 'none';
case 'autoHiding': return terminalContainer.style['-ms-overflow-style'] = '-ms-autohiding-scrollbar';
case 'visible': return terminalContainer.style['-ms-overflow-style'] = 'scrollbar';
}
}
function setPadding(padding) {
+9 -1
View File
@@ -5,11 +5,19 @@
html, body {
background: transparent;
height: 100%;
}
width: 100%;
overflow: hidden;
-ms-content-zooming: none;
}
#terminal-container {
width: 100%;
height: 100%;
/* re-enable scrolling only in the xterm container */
overflow-y: auto;
overflow-x: hidden;
touch-action: pan-y;
-ms-touch-action: pan-y;
}
.xterm .xterm-viewport {
+1
View File
@@ -1,6 +1,7 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
@@ -16,5 +16,6 @@ namespace FluentTerminal.Models
public bool AlwaysShowTabs { get; set; }
public bool ShowNewOutputIndicator { get; set; }
public bool EnableTrayIcon { get; set; }
public bool AlwaysUseWinPty { get; set; }
}
}
+9
View File
@@ -19,12 +19,18 @@ namespace FluentTerminal.Models.Enums
[Description("Configurable new tab")]
ConfigurableNewTab,
[Description("Change tab title")]
ChangeTabTitle,
[Description("Close tab")]
CloseTab,
[Description("New window")]
NewWindow,
[Description("Configurable new window")]
ConfigurableNewWindow,
[Description("Show settings")]
ShowSettings,
@@ -34,6 +40,9 @@ namespace FluentTerminal.Models.Enums
[Description("Paste")]
Paste,
[Description("Paste without newlines")]
PasteWithoutNewlines,
[Description("Search")]
Search,
@@ -0,0 +1,16 @@
using System.ComponentModel;
namespace FluentTerminal.Models.Enums
{
public enum LineEndingStyle
{
[Description("Do not modify")]
DoNotModify,
[Description("Convert to CRLF")]
ToCRLF,
[Description("Convert to LF")]
ToLF
}
}
@@ -0,0 +1,9 @@
namespace FluentTerminal.Models.Enums
{
public enum SessionType
{
Unknown,
WinPty,
ConPty
}
}
+14
View File
@@ -23,5 +23,19 @@
public bool Alt { get; set; }
public bool Shift { get; set; }
public bool Meta { get; set; }
public override bool Equals(object obj)
{
if (obj is KeyBinding other)
{
return other.Command.Equals(Command)
&& other.Key == Key
&& other.Ctrl == Ctrl
&& other.Alt == Alt
&& other.Shift == Shift
&& other.Meta == Meta;
}
return false;
}
}
}
+2
View File
@@ -9,8 +9,10 @@ namespace FluentTerminal.Models
public ICollection<KeyBinding> PreviousTab { get; set; }
public ICollection<KeyBinding> NewTab { get; set; }
public ICollection<KeyBinding> ConfigurableNewTab { get; set; }
public ICollection<KeyBinding> ChangeTabTitle { get; set; }
public ICollection<KeyBinding> CloseTab { get; set; }
public ICollection<KeyBinding> NewWindow { get; set; }
public ICollection<KeyBinding> ConfigurableNewWindow { get; set; }
public ICollection<KeyBinding> ShowSettings { get; set; }
public ICollection<KeyBinding> Copy { get; set; }
public ICollection<KeyBinding> Paste { get; set; }
@@ -1,8 +1,12 @@
namespace FluentTerminal.Models.Requests
using FluentTerminal.Models.Enums;
namespace FluentTerminal.Models.Requests
{
public class CreateTerminalRequest
{
public int Id { get; set; }
public TerminalSize Size { get; set; }
public ShellProfile Profile { 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 int Id { get; set; }
public bool Success { get; set; }
public string Error { get; set; }
public string ShellExecutableName { get; set; }
@@ -0,0 +1,7 @@
namespace FluentTerminal.Models.Responses
{
public class GetAvailablePortResponse
{
public int Port { get; set; }
}
}
+42
View File
@@ -1,11 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FluentTerminal.Models.Enums;
using System.Linq;
namespace FluentTerminal.Models
{
public class ShellProfile
{
/// <summary>
/// Replace all instances of anything resembling a newline, treating pairs of \r\n in either order as a single linebreak.
/// </summary>
public static Regex NewlinePattern = new Regex(@"\n\r|\r\n|\r|\n", RegexOptions.Compiled);
public ShellProfile()
{
@@ -21,6 +28,7 @@ namespace FluentTerminal.Models
WorkingDirectory = other.WorkingDirectory;
TabThemeId = other.TabThemeId;
TerminalThemeId = other.TerminalThemeId;
LineEndingTranslation = other.LineEndingTranslation;
KeyBindings = other.KeyBindings.Select(x => new KeyBinding(x)).ToList();
}
@@ -31,7 +39,41 @@ namespace FluentTerminal.Models
public string Location { get; set; }
public string WorkingDirectory { get; set; }
public int TabThemeId { get; set; }
public LineEndingStyle LineEndingTranslation { get; set; }
public string TranslateLineEndings(string content)
{
switch (LineEndingTranslation)
{
case LineEndingStyle.ToCRLF:
return NewlinePattern.Replace(content, "\r\n");
case LineEndingStyle.ToLF:
return NewlinePattern.Replace(content, "\n");
case LineEndingStyle.DoNotModify:
default:
return content;
}
}
public Guid TerminalThemeId { get; set; }
public ICollection<KeyBinding> KeyBindings { get; set; } = new List<KeyBinding>();
public override bool Equals(object obj)
{
if (obj is ShellProfile other)
{
return other.Id.Equals(Id)
&& other.PreInstalled.Equals(PreInstalled)
&& other.Name.Equals(Name)
&& other.Arguments.Equals(Arguments)
&& other.Location.Equals(Location)
&& other.WorkingDirectory.Equals(WorkingDirectory)
&& other.TabThemeId.Equals(TabThemeId)
&& other.TerminalThemeId.Equals(TerminalThemeId)
&& other.LineEndingTranslation == LineEndingTranslation
&& other.KeyBindings.SequenceEqual(KeyBindings);
}
return false;
}
}
}
+29
View File
@@ -56,5 +56,34 @@
BrightCyan = other.BrightCyan;
BrightWhite = other.BrightWhite;
}
public override bool Equals(object obj)
{
if (obj is TerminalColors other)
{
return other.Foreground.Equals(Foreground)
&& other.Background.Equals(Background)
&& other.Cursor.Equals(Cursor)
&& other.CursorAccent.Equals(CursorAccent)
&& other.Selection.Equals(Selection)
&& other.Black.Equals(Black)
&& other.Red.Equals(Red)
&& other.Green.Equals(Green)
&& other.Yellow.Equals(Yellow)
&& other.Blue.Equals(Blue)
&& other.Magenta.Equals(Magenta)
&& other.Cyan.Equals(Cyan)
&& other.White.Equals(White)
&& other.BrightBlack.Equals(BrightBlack)
&& other.BrightRed.Equals(BrightRed)
&& other.BrightGreen.Equals(BrightGreen)
&& other.BrightYellow.Equals(BrightYellow)
&& other.BrightBlue.Equals(BrightBlue)
&& other.BrightMagenta.Equals(BrightMagenta)
&& other.BrightCyan.Equals(BrightCyan)
&& other.BrightWhite.Equals(BrightWhite);
}
return false;
}
}
}
+2
View File
@@ -8,6 +8,8 @@ namespace FluentTerminal.Models
public int FontSize { get; set; }
public bool BoldText { get; set; }
public CursorStyle CursorStyle { get; set; }
public bool CursorBlink { get; set; }
+11
View File
@@ -22,5 +22,16 @@ namespace FluentTerminal.Models
public string Author { get; set; }
public bool PreInstalled { get; set; }
public TerminalColors Colors { get; set; }
public override bool Equals(object obj)
{
if (obj is TerminalTheme other)
{
return Equals(other.Name, Name)
&& Equals(other.Author, Author)
&& Equals(other.Colors, Colors);
}
return false;
}
}
}
@@ -108,7 +108,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Enums\MouseButton.cs" />
<Compile Include="Interfaces\ITerminalEventListener.cs" />
<Compile Include="Interfaces\IxtermEventListener.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="WebAllowedObjects\TerminalBridge.cs" />
</ItemGroup>
@@ -2,7 +2,7 @@
namespace FluentTerminal.RuntimeComponent.Interfaces
{
public interface ITerminalEventListener
public interface IxtermEventListener
{
void OnTerminalResized(int columns, int rows);
@@ -7,9 +7,9 @@ namespace FluentTerminal.RuntimeComponent.WebAllowedObjects
[AllowForWeb]
public sealed class TerminalBridge
{
private readonly ITerminalEventListener _terminalEventListener;
private readonly IxtermEventListener _terminalEventListener;
public TerminalBridge(ITerminalEventListener terminalEventListener)
public TerminalBridge(IxtermEventListener terminalEventListener)
{
_terminalEventListener = terminalEventListener;
}
@@ -112,12 +112,23 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Native\WindowApi.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\AppCommunicationService.cs" />
<Compile Include="Services\ConPty\ConPtySession.cs" />
<Compile Include="Services\ConPty\Native\ConsoleApi.cs" />
<Compile Include="Services\ConPty\Native\ProcessApi.cs" />
<Compile Include="Services\ConPty\Native\PseudoConsoleApi.cs" />
<Compile Include="Services\ConPty\Processes\Process.cs" />
<Compile Include="Services\ConPty\Processes\ProcessFactory.cs" />
<Compile Include="Services\ConPty\PseudoConsole.cs" />
<Compile Include="Services\ConPty\PseudoConsolePipe.cs" />
<Compile Include="Services\ConPty\Terminal.cs" />
<Compile Include="Services\ITerminalSession.cs" />
<Compile Include="Services\ToggleWindowService.cs" />
<Compile Include="Services\TerminalSession.cs" />
<Compile Include="Services\TerminalsManager.cs" />
<Compile Include="Services\WinPty\WinPtySession.cs" />
<Compile Include="SystemTrayApplicationContext.cs" />
<Compile Include="Utilities.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
@@ -0,0 +1,23 @@
using System;
using System.Runtime.InteropServices;
namespace FluentTerminal.SystemTray.Native
{
public static class WindowApi
{
public const int SW_HIDE = 0;
public const int SW_SHOWNORMAL = 1;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
public const int SW_SHOWNOACTIVATE = 4;
public const int SW_MINIMIZE = 6;
public const int SW_RESTORE = 9;
public const int SW_SHOWDEFAULT = 10;
[DllImport("user32.dll")]
public static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd, out uint ProcessId);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
}
}
@@ -7,6 +7,7 @@ using Windows.ApplicationModel;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using System;
using FluentTerminal.Models.Responses;
namespace FluentTerminal.SystemTray.Services
{
@@ -110,7 +111,17 @@ namespace FluentTerminal.SystemTray.Services
{
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
_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)
@@ -0,0 +1,115 @@
using System;
using System.IO;
using System.Threading.Tasks;
using FluentTerminal.Models;
using FluentTerminal.Models.Requests;
namespace FluentTerminal.SystemTray.Services.ConPty
{
public class ConPtySession : ITerminalSession
{
private TerminalsManager _terminalsManager;
private Terminal _terminal;
public int Id { get; private set; }
public string ShellExecutableName { get; private set; }
public event EventHandler ConnectionClosed;
public void Close()
{
ConnectionClosed?.Invoke(this, EventArgs.Empty);
}
public void Resize(TerminalSize size)
{
_terminal.Resize(size.Columns, size.Rows);
}
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
{
Id = request.Id;
_terminalsManager = terminalsManager;
ShellExecutableName = Path.GetFileNameWithoutExtension(request.Profile.Location);
var cwd = GetWorkingDirectory(request.Profile);
string args = $"\"{request.Profile.Location}\" {request.Profile.Arguments}";
_terminal = new Terminal();
_terminal.OutputReady += _terminal_OutputReady;
_terminal.Exited += _terminal_Exited;
Task.Run(() => _terminal.Start(args, cwd, request.Size.Columns, request.Size.Rows));
}
private void _terminal_Exited(object sender, EventArgs e)
{
Close();
}
private string GetWorkingDirectory(ShellProfile configuration)
{
if (string.IsNullOrWhiteSpace(configuration.WorkingDirectory) || !Directory.Exists(configuration.WorkingDirectory))
{
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
return configuration.WorkingDirectory;
}
private void _terminal_OutputReady(object sender, EventArgs e)
{
ListenToStdOut();
}
private void ListenToStdOut()
{
Task.Run(async () =>
{
var reader = new StreamReader(_terminal.ConsoleOutStream);
do
{
var offset = 0;
var buffer = new char[1024];
var readChars = await reader.ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
if (readChars > 0)
{
var output = new String(buffer, 0, readChars);
_terminalsManager.DisplayTerminalOutput(Id, output);
}
}
while (!reader.EndOfStream);
});
}
public void WriteText(string text)
{
_terminal.WriteToPseudoConsole(text);
}
#region IDisposable Support
private bool disposedValue = false;
private void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_terminal?.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
}
@@ -0,0 +1,56 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
namespace FluentTerminal.SystemTray.Services.ConPty.Native
{
/// <summary>
/// PInvoke signatures for Win32's Console API.
/// </summary>
internal static class ConsoleApi
{
internal const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
internal const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
internal const uint GENERIC_WRITE = 0x40000000;
internal const uint GENERIC_READ = 0x80000000;
internal const uint FILE_SHARE_WRITE = 0x00000002;
internal const uint OPEN_EXISTING = 0x00000003;
internal const uint FILE_ATTRIBUTE_NORMAL = 0x80;
internal const string ConsoleOutPseudoFilename = "CONOUT$";
[DllImport("kernel32.dll", EntryPoint = "AllocConsole", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
internal static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetConsoleMode(SafeFileHandle hConsoleHandle, uint mode);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool GetConsoleMode(SafeFileHandle handle, out uint mode);
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr CreateFileW(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetConsoleCtrlHandler(ConsoleEventDelegate callback, bool add);
internal delegate bool ConsoleEventDelegate(CtrlTypes ctrlType);
internal enum CtrlTypes : uint
{
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT
}
}
}
@@ -0,0 +1,86 @@
using System;
using System.Runtime.InteropServices;
namespace FluentTerminal.SystemTray.Services.ConPty.Native
{
/// <summary>
/// PInvoke signatures for Win32's Process API.
/// </summary>
internal static class ProcessApi
{
internal const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
internal struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool InitializeProcThreadAttributeList(
IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool UpdateProcThreadAttribute(
IntPtr lpAttributeList, uint dwFlags, IntPtr attribute, IntPtr lpValue,
IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CreateProcess(
string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CloseHandle(IntPtr hObject);
}
}
@@ -0,0 +1,33 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
namespace FluentTerminal.SystemTray.Services.ConPty.Native
{
/// <summary>
/// PInvoke signatures for Win32's PseudoConsole API.
/// </summary>
internal static class PseudoConsoleApi
{
internal const uint PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
[StructLayout(LayoutKind.Sequential)]
internal struct COORD
{
public short X;
public short Y;
}
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int CreatePseudoConsole(COORD size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out IntPtr phPC);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int ResizePseudoConsole(IntPtr hPC, COORD size);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int ClosePseudoConsole(IntPtr hPC);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, IntPtr lpPipeAttributes, int nSize);
}
}
@@ -0,0 +1,70 @@
using System;
using System.Runtime.InteropServices;
using static FluentTerminal.SystemTray.Services.ConPty.Native.ProcessApi;
namespace FluentTerminal.SystemTray.Services.ConPty.Processes
{
/// <summary>
/// Represents an instance of a process.
/// </summary>
internal sealed class Process : IDisposable
{
public Process(STARTUPINFOEX startupInfo, PROCESS_INFORMATION processInfo)
{
StartupInfo = startupInfo;
ProcessInfo = processInfo;
}
public STARTUPINFOEX StartupInfo { get; }
public PROCESS_INFORMATION ProcessInfo { get; }
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// dispose managed state (managed objects).
}
// dispose unmanaged state
// Free the attribute list
if (StartupInfo.lpAttributeList != IntPtr.Zero)
{
DeleteProcThreadAttributeList(StartupInfo.lpAttributeList);
Marshal.FreeHGlobal(StartupInfo.lpAttributeList);
}
// Close process and thread handles
if (ProcessInfo.hProcess != IntPtr.Zero)
{
CloseHandle(ProcessInfo.hProcess);
}
if (ProcessInfo.hThread != IntPtr.Zero)
{
CloseHandle(ProcessInfo.hThread);
}
disposedValue = true;
}
}
~Process()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
@@ -0,0 +1,99 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using static FluentTerminal.SystemTray.Services.ConPty.Native.ProcessApi;
namespace FluentTerminal.SystemTray.Services.ConPty.Processes
{
/// <summary>
/// Support for starting and configuring processes.
/// </summary>
/// <remarks>
/// Possible to replace with managed code? The key is being able to provide the PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE attribute
/// </remarks>
internal static class ProcessFactory
{
/// <summary>
/// Start and configure a process. The return value represents the process and should be disposed.
/// </summary>
internal static Process Start(string command, string directory, IntPtr attributes, IntPtr hPC)
{
var startupInfo = ConfigureProcessThread(hPC, attributes);
var processInfo = RunProcess(ref startupInfo, command, directory);
return new Process(startupInfo, processInfo);
}
private static STARTUPINFOEX ConfigureProcessThread(IntPtr hPC, IntPtr attributes)
{
// this method implements the behavior described in https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#preparing-for-creation-of-the-child-process
var lpSize = IntPtr.Zero;
var success = InitializeProcThreadAttributeList(
lpAttributeList: IntPtr.Zero,
dwAttributeCount: 1,
dwFlags: 0,
lpSize: ref lpSize
);
if (success || lpSize == IntPtr.Zero) // we're not expecting `success` here, we just want to get the calculated lpSize
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not calculate the number of bytes for the attribute list.");
}
var startupInfo = new STARTUPINFOEX();
startupInfo.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
startupInfo.lpAttributeList = Marshal.AllocHGlobal(lpSize);
success = InitializeProcThreadAttributeList(
lpAttributeList: startupInfo.lpAttributeList,
dwAttributeCount: 1,
dwFlags: 0,
lpSize: ref lpSize
);
if (!success)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not set up attribute list.");
}
success = UpdateProcThreadAttribute(
lpAttributeList: startupInfo.lpAttributeList,
dwFlags: 0,
attribute: attributes,
lpValue: hPC,
cbSize: (IntPtr)IntPtr.Size,
lpPreviousValue: IntPtr.Zero,
lpReturnSize: IntPtr.Zero
);
if (!success)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not set pseudoconsole thread attribute.");
}
return startupInfo;
}
private static PROCESS_INFORMATION RunProcess(ref STARTUPINFOEX sInfoEx, string commandLine, string directory)
{
int securityAttributeSize = Marshal.SizeOf<SECURITY_ATTRIBUTES>();
var pSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
var tSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
var success = CreateProcess(
lpApplicationName: null,
lpCommandLine: commandLine,
lpProcessAttributes: ref pSec,
lpThreadAttributes: ref tSec,
bInheritHandles: false,
dwCreationFlags: EXTENDED_STARTUPINFO_PRESENT,
lpEnvironment: IntPtr.Zero,
lpCurrentDirectory: directory,
lpStartupInfo: ref sInfoEx,
lpProcessInformation: out PROCESS_INFORMATION pInfo
);
if (!success)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not create process.");
}
return pInfo;
}
}
}
@@ -0,0 +1,45 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using static FluentTerminal.SystemTray.Services.ConPty.Native.PseudoConsoleApi;
namespace FluentTerminal.SystemTray.Services.ConPty
{
/// <summary>
/// Utility functions around the new Pseudo Console APIs.
/// </summary>
internal sealed class PseudoConsole : IDisposable
{
public static readonly IntPtr PseudoConsoleThreadAttribute = (IntPtr)PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE;
public IntPtr Handle { get; }
private PseudoConsole(IntPtr handle)
{
Handle = handle;
}
internal static PseudoConsole Create(SafeFileHandle inputReadSide, SafeFileHandle outputWriteSide, int width, int height)
{
var createResult = CreatePseudoConsole(
new COORD { X = (short)width, Y = (short)height },
inputReadSide, outputWriteSide,
0, out IntPtr hPC);
if (createResult != 0)
{
throw new Win32Exception(createResult, "Could not create psuedo console.");
}
return new PseudoConsole(hPC);
}
internal void Resize(int width, int height)
{
ResizePseudoConsole(Handle, new COORD { X = (short)width, Y = (short)height });
}
public void Dispose()
{
ClosePseudoConsole(Handle);
}
}
}
@@ -0,0 +1,48 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using static FluentTerminal.SystemTray.Services.ConPty.Native.PseudoConsoleApi;
namespace FluentTerminal.SystemTray.Services.ConPty
{
/// <summary>
/// A pipe used to talk to the pseudoconsole, as described in:
/// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
/// </summary>
/// <remarks>
/// We'll have two instances of this class, one for input and one for output.
/// </remarks>
internal sealed class PseudoConsolePipe : IDisposable
{
public readonly SafeFileHandle ReadSide;
public readonly SafeFileHandle WriteSide;
public PseudoConsolePipe()
{
if (!CreatePipe(out ReadSide, out WriteSide, IntPtr.Zero, 0))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "failed to create pipe");
}
}
#region IDisposable
void Dispose(bool disposing)
{
if (disposing)
{
ReadSide?.Dispose();
WriteSide?.Dispose();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
@@ -0,0 +1,205 @@
using FluentTerminal.SystemTray.Services.ConPty.Processes;
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using static FluentTerminal.SystemTray.Services.ConPty.Native.ConsoleApi;
using static FluentTerminal.SystemTray.Native.WindowApi;
namespace FluentTerminal.SystemTray.Services.ConPty
{
/// <summary>
/// Class for managing communication with the underlying console, and communicating with its pseudoconsole.
/// </summary>
public sealed class Terminal : IDisposable
{
private SafeFileHandle _consoleInputPipeWriteHandle;
private StreamWriter _consoleInputWriter;
private PseudoConsolePipe _inputPipe;
private PseudoConsolePipe _outputPipe;
private PseudoConsole _pseudoConsole;
/// <summary>
/// A stream of VT-100-enabled output from the console.
/// </summary>
public FileStream ConsoleOutStream { get; private set; }
/// <summary>
/// Fired once the console has been hooked up and is ready to receive input.
/// </summary>
public event EventHandler OutputReady;
public event EventHandler Exited;
public Terminal()
{
// By default, UI applications don't have a console associated with them.
// So first, we check to see if this process has a console.
if (GetConsoleWindow() == IntPtr.Zero)
{
// If it doesn't ask Windows to allocate one to it for us.
bool createConsoleSuccess = AllocConsole();
if (!createConsoleSuccess)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not allocate console for this process.");
}
}
var windowHandle = GetConsoleWindow();
ShowWindow(windowHandle, SW_HIDE);
// And enable VT processing for our process's console.
EnableVirtualTerminalSequenceProcessing();
}
private void EnableVirtualTerminalSequenceProcessing()
{
SafeFileHandle screenBuffer = GetConsoleScreenBuffer();
if (!GetConsoleMode(screenBuffer, out uint outConsoleMode))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not get console mode.");
}
outConsoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
if (!SetConsoleMode(screenBuffer, outConsoleMode))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not enable virtual terminal processing.");
}
}
/// <summary>
/// Start the psuedoconsole and run the process as shown in
/// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#creating-the-pseudoconsole
/// </summary>
/// <param name="command">the command to run, e.g. cmd.exe</param>
/// <param name="consoleHeight">The height (in characters) to start the pseudoconsole with. Defaults to 80.</param>
/// <param name="consoleWidth">The width (in characters) to start the pseudoconsole with. Defaults to 30.</param>
public void Start(string command, string directory, int consoleWidth = 80, int consoleHeight = 30)
{
_inputPipe = new PseudoConsolePipe();
_outputPipe = new PseudoConsolePipe();
_pseudoConsole = PseudoConsole.Create(_inputPipe.ReadSide, _outputPipe.WriteSide, consoleWidth, consoleHeight);
using (var process = ProcessFactory.Start(command, directory, PseudoConsole.PseudoConsoleThreadAttribute, _pseudoConsole.Handle))
{
// copy all pseudoconsole output to a FileStream and expose it to the rest of the app
ConsoleOutStream = new FileStream(_outputPipe.ReadSide, FileAccess.Read);
OutputReady.Invoke(this, EventArgs.Empty);
// Store input pipe handle, and a writer for later reuse
_consoleInputPipeWriteHandle = _inputPipe.WriteSide;
_consoleInputWriter = new StreamWriter(new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write))
{
AutoFlush = true
};
// free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar)
OnClose(() => DisposeResources(process, _pseudoConsole, _outputPipe, _inputPipe, _consoleInputWriter));
WaitForExit(process).WaitOne(Timeout.Infinite);
}
Exited?.Invoke(this, EventArgs.Empty);
}
public void Resize(int width, int height)
{
_pseudoConsole?.Resize(width, height);
}
/// <summary>
/// Sends the given string to the anonymous pipe that writes to the active pseudoconsole.
/// </summary>
/// <param name="input">A string of characters to write to the console. Supports VT-100 codes.</param>
public void WriteToPseudoConsole(string input)
{
if (_consoleInputWriter == null)
{
throw new InvalidOperationException("There is no writer attached to a pseudoconsole. Have you called Start on this instance yet?");
}
_consoleInputWriter.Write(input);
}
/// <summary>
/// Get an AutoResetEvent that signals when the process exits
/// </summary>
private static AutoResetEvent WaitForExit(Process process) =>
new AutoResetEvent(false)
{
SafeWaitHandle = new SafeWaitHandle(process.ProcessInfo.hProcess, ownsHandle: false)
};
/// <summary>
/// Set a callback for when the terminal is closed (e.g. via the "X" window decoration button).
/// Intended for resource cleanup logic.
/// </summary>
private static void OnClose(Action handler)
{
SetConsoleCtrlHandler(eventType =>
{
if (eventType == CtrlTypes.CTRL_CLOSE_EVENT)
{
handler();
}
return false;
}, true);
}
private void DisposeResources(params IDisposable[] disposables)
{
foreach (var disposable in disposables)
{
disposable.Dispose();
}
}
/// <summary>
/// A helper method that opens a handle on the console's screen buffer, which will allow us to get its output,
/// even if STDOUT has been redirected (which Visual Studio does by default).
/// </summary>
/// <returns>A file handle to the console's screen buffer.</returns>
/// <remarks>This is described in more detail here: https://docs.microsoft.com/en-us/windows/console/console-handles </remarks>
private SafeFileHandle GetConsoleScreenBuffer()
{
IntPtr file = CreateFileW(
ConsoleOutPseudoFilename,
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_WRITE,
IntPtr.Zero,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
IntPtr.Zero);
if (file == new IntPtr(-1))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not get console screen buffer.");
}
return new SafeFileHandle(file, true);
}
#region IDisposable Support
private bool disposedValue = false;
private void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_inputPipe?.Dispose();
_outputPipe?.Dispose();
_pseudoConsole?.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
}
@@ -0,0 +1,19 @@
using System;
using FluentTerminal.Models;
using FluentTerminal.Models.Requests;
namespace FluentTerminal.SystemTray.Services
{
public interface ITerminalSession : IDisposable
{
int Id { get; }
string ShellExecutableName { get; }
event EventHandler ConnectionClosed;
void Close();
void Resize(TerminalSize size);
void WriteText(string text);
void Start(CreateTerminalRequest request, TerminalsManager terminalsManager);
}
}
@@ -1,6 +1,10 @@
using FluentTerminal.Models;
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Requests;
using FluentTerminal.Models.Responses;
using FluentTerminal.SystemTray.Services.ConPty;
using FluentTerminal.SystemTray.Services.WinPty;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -9,14 +13,16 @@ namespace FluentTerminal.SystemTray.Services
{
public class TerminalsManager
{
private Dictionary<int, TerminalSession> _terminals = new Dictionary<int, TerminalSession>();
private readonly Dictionary<int, ITerminalSession> _terminals = new Dictionary<int, ITerminalSession>();
private readonly ISettingsService _settingsService;
public event EventHandler<DisplayTerminalOutputRequest> DisplayOutputRequested;
public event EventHandler<int> TerminalExited;
public TerminalsManager()
public TerminalsManager(ISettingsService settingsService)
{
_settingsService = settingsService;
}
public void DisplayTerminalOutput(int terminalId, string output)
@@ -30,10 +36,18 @@ namespace FluentTerminal.SystemTray.Services
public CreateTerminalResponse CreateTerminal(CreateTerminalRequest request)
{
TerminalSession terminal;
ITerminalSession terminal = null;
try
{
terminal = new TerminalSession(request, this);
if (request.SessionType == SessionType.WinPty)
{
terminal = new WinPtySession();
}
else if (request.SessionType == SessionType.ConPty)
{
terminal = new ConPtySession();
}
terminal.Start(request, this);
}
catch (Exception e)
{
@@ -42,18 +56,16 @@ namespace FluentTerminal.SystemTray.Services
terminal.ConnectionClosed += OnTerminalConnectionClosed;
_terminals.Add(terminal.Id, terminal);
return new CreateTerminalResponse
{
Success = true,
Id = terminal.Id,
ShellExecutableName = terminal.ShellExecutableName
};
}
public void WriteText(int id, string text)
{
if (_terminals.TryGetValue(id, out TerminalSession terminal))
if (_terminals.TryGetValue(id, out ITerminalSession terminal))
{
terminal.WriteText(text);
}
@@ -61,7 +73,7 @@ namespace FluentTerminal.SystemTray.Services
public void ResizeTerminal(int id, TerminalSize size)
{
if (_terminals.TryGetValue(id, out TerminalSession terminal))
if (_terminals.TryGetValue(id, out ITerminalSession terminal))
{
terminal.Resize(size);
}
@@ -73,7 +85,7 @@ namespace FluentTerminal.SystemTray.Services
public void CloseTerminal(int id)
{
if (_terminals.TryGetValue(id, out TerminalSession terminal))
if (_terminals.TryGetValue(id, out ITerminalSession terminal))
{
_terminals.Remove(terminal.Id);
terminal.Dispose();
@@ -82,7 +94,7 @@ namespace FluentTerminal.SystemTray.Services
private void OnTerminalConnectionClosed(object sender, System.EventArgs e)
{
if (sender is TerminalSession terminal)
if (sender is ITerminalSession terminal)
{
_terminals.Remove(terminal.Id);
terminal.Dispose();
@@ -11,12 +11,12 @@ using System.Windows.Input;
using System.Windows.Threading;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Core;
using static FluentTerminal.SystemTray.Native.WindowApi;
namespace FluentTerminal.SystemTray.Services
{
public class ToggleWindowService : IDisposable
{
private const int SW_MINIMIZE = 6;
private readonly HotKeyManager _hotKeyManager;
private readonly INotificationService _notificationService;
private readonly Dispatcher _dispatcher;
@@ -128,11 +128,7 @@ namespace FluentTerminal.SystemTray.Services
return string.Join(" + ", keys);
}
[DllImport("user32.dll")]
private static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd, out uint ProcessId);
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
private string GetActiveProcessFileName()
{
@@ -7,18 +7,19 @@ using System.Text;
using System.Threading.Tasks;
using static winpty.WinPty;
namespace FluentTerminal.SystemTray.Services
namespace FluentTerminal.SystemTray.Services.WinPty
{
public class TerminalSession : IDisposable
public class WinPtySession : IDisposable, ITerminalSession
{
private bool _disposedValue;
private readonly IntPtr _handle;
private readonly Stream _stdin;
private readonly Stream _stdout;
private readonly TerminalsManager _terminalsManager;
private IntPtr _handle;
private Stream _stdin;
private Stream _stdout;
private TerminalsManager _terminalsManager;
public TerminalSession(CreateTerminalRequest request, TerminalsManager terminalsManager)
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
{
Id = request.Id;
_terminalsManager = terminalsManager;
var configHandle = IntPtr.Zero;
@@ -62,19 +63,10 @@ namespace FluentTerminal.SystemTray.Services
winpty_error_free(errorHandle);
}
var port = Utilities.GetAvailablePort();
if (port == null)
{
throw new Exception("no port available");
}
Id = port.Value;
ListenToStdOut();
}
~TerminalSession()
~WinPtySession()
{
Dispose(false);
}
@@ -90,9 +82,9 @@ namespace FluentTerminal.SystemTray.Services
public event EventHandler ConnectionClosed;
public int Id { get; }
public int Id { get; private set; }
public string ShellExecutableName { get; }
public string ShellExecutableName { get; private set; }
public void Dispose()
{
+4
View File
@@ -10,6 +10,7 @@ namespace FluentTerminal.SystemTray
public static class Utilities
{
private const int FirstDynamicPort = 49151;
private static readonly List<int> _sentOutPorts = new List<int>();
public static int? GetAvailablePort()
{
@@ -26,12 +27,15 @@ namespace FluentTerminal.SystemTray
endPoints = properties.GetActiveUdpListeners();
usedPorts.AddRange(endPoints.Where(e => e.Port >= FirstDynamicPort).Select(e => e.Port));
usedPorts.AddRange(_sentOutPorts);
usedPorts.Sort();
for (var i = FirstDynamicPort; i < UInt16.MaxValue; i++)
{
if (!usedPorts.Contains(i))
{
_sentOutPorts.Add(i);
return i;
}
}
+2 -1
View File
@@ -28,6 +28,7 @@
param(
[switch]$Force = $false,
[switch]$ForceContextMenu = $false,
[switch]$GetDeveloperLicense = $false,
[string]$CertificatePath = $null
)
@@ -622,7 +623,7 @@ function DoStandardOperations
InstallPackageWithDependencies
if (ConfirmContextMenuInstall)
if ($ForceContextMenu -or (ConfirmContextMenuInstall))
{
InstallContextMenuIntegration
}
+8 -50
View File
@@ -1,6 +1,6 @@
# Fluent Terminal
[![Build status](https://ci.appveyor.com/api/projects/status/4r429bv594fxkygd/branch/master?svg=true)](https://ci.appveyor.com/project/felixse/fluentterminal/branch/master)
[![Build status](https://fs-apps.visualstudio.com/FluentTerminal/_apis/build/status/FluentTerminal-Universal%20Windows%20Platform-CI)](https://fs-apps.visualstudio.com/FluentTerminal/_build/latest?definitionId=3)
[![SonarCloud](https://sonarcloud.io/api/project_badges/measure?project=FluentTerminal&metric=alert_status)](https://sonarcloud.io/dashboard?id=FluentTerminal)
[![Gitter chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/FluentTerminal)
@@ -36,7 +36,12 @@ A Terminal Emulator based on UWP and web technologies.
### Prerequisite
- You need to update to Fall Creators Update or later.
### Automatic install
### Chocolatey package manager installation
- Install [Chocolatey](https://chocolatey.org/)
- From an elevated/admin shell, execute `choco install fluent-terminal`
### Bundled install script
- Download and extract the latest [release](https://github.com/felixse/FluentTerminal/releases).
- If not already present, download [`Install.ps1`](Install.ps1) to the extracted directory.
@@ -59,51 +64,4 @@ A Terminal Emulator based on UWP and web technologies.
- **Optional:** Install Context menu integration from [here](https://github.com/felixse/FluentTerminal/tree/master/Explorer%20Context%20Menu%20Integration)
## How to set up a development environment
This is not for the faint of heart, and won't be necessary unless you intend to contribute back to the project.
**When using unattended install helpers, like Chocolatey, you may not have access to all of the necessary installation configuration options. For this it is recommended that you use the installers directly. This applies to both NodeJS and Visual Studio.**
- Grab the [latest NodeJS 8.x installer](https://nodejs.org/en/download/), and install ensuring that both NPM and "install into system PATH" are checked during the install process.
- Grab the [latest Visual Studio 2017 Community installer](https://www.visualstudio.com/downloads/) and install (See next section)
- [Enable developer mode in Windows 10](https://docs.microsoft.com/en-US/windows/uwp/get-started/enable-your-device-for-development)
### Installing Visual Studio 2017
There's some specific parts of VS2017 that we need to make sure are available when you go through the install process.
- Under "Workloads" select:
- Universal Windows Platform development
- Node.js development (Probably not required, given we installed Node.js separately)
- Under individual components, you'll need to install the following:
- Git for Windows
- .NET Core runtime
- .NET 4.7.1 Targeting Pack
- Windows SDK 16299 for UWP: C#, VB, JS
An example screenshot from the main page of the VS2017 install process for a functioning development environment:
![Terminal window](Screenshots/vs2017_installer_example.png)
### The First Build
- Before opening the solution, open a Powershell terminal, go into the `FluentTerminal.Client` folder and run:
- `npm install` (Ignore the warnings related to `fluent-terminal-client`)
- `npm run build`
- When the solution is first opened, it will help to set the architecture to x64 for testing, as we didn't install the ARM cross-compiling stuff for VS2017 and we only include pre-compiled winpty binaries for x86 and x64.
- **For this reason, the default architecture of "Any CPU" will fail to build, and you will have to choose either x64 or x86**
- When first built, Visual studio will spend a significant amount of time resolving dependencies, and fetching additional components via nuGet. This is normal.
- If you have done the steps so far correctly, and the build is left as a debug build, then the solution should build and launch when you press F5.
### Subsequent Builds
- If you change the `FluentTerminal.Client` contents (**OR** if you merge or pull changes that resulted in changes to files in that folder),you'll need to re-run the `npm run build` (and `npm install` if you, or the pulled-in changes, change the dependencies).
- The rest of the build process is as usual through Visual Studio.
### Packaging
- To start, copy the `Package.StoreAssociation.xml.dev` to `Package.StoreAssociation.xml`, this will provide a basic StoreAssociation file to use when building the app.
- With the solution open in Visual Studio, you can build a package for your own use by right-clicking on the `FluentTerminal.App` project in the Solution Explorer and choosing `Store->Create App Packages`
- Follow the prompts to create a package for sideloading, and if this is only for your personal use, it is OK to only build the x64 package, and not include the debug (i.e PDB) symbols.
- Note that your commits with these settings (as well as a newly generated temporary store key) will involve changes to the `FluentTerminal.App.csproj` file, so be careful about that if you plan to submit them for PRs.
- This will produce, if successful, a folder in `FluentTerminal.App\AppPackages` that you can zip up and share. You'll need to follow the same install instructions as when installing a release from the GitHub repo, including installing the certificate and then installing the appxpackage.
Please refer to [this Wiki page](https://github.com/felixse/FluentTerminal/wiki/How-to-set-up-a-development-environment)
-25
View File
@@ -1,25 +0,0 @@
os: Visual Studio 2017
configuration: Debug
platform:
- x64
skip_commits:
files:
- README.md
install:
- ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1'))
- cmd: appveyor-tools\secure-file -decrypt FluentTerminal.App\FluentTerminal.App_StoreKey.pfx.enc -secret %CertificationFilesSecret%
- cmd: appveyor-tools\secure-file -decrypt FluentTerminal.App\Package.StoreAssociation.xml.enc -secret %CertificationFilesSecret%
before_build:
- cmd: pushd FluentTerminal.Client && npm install && npm run build && popd
- cmd: nuget restore
build:
project: FluentTerminal.sln
parallel: true
verbosity: minimal
before_test:
- cmd: dotnet build FluentTerminal.App.Services.Test\FluentTerminal.App.Services.Test.csproj
test:
assemblies:
only:
- '**\*.Test.dll'
deploy: off