Compare commits

...
40 Commits
Author SHA1 Message Date
FelixSe ef7f909d26 Increase version 2019-03-27 21:57:16 +01:00
FelixSe f2618fa940 Fix launch error when TERM was already defined 2019-03-27 21:13:23 +01:00
ivanjh 363f885aea Fix shell profile radio button line-ending binding (#257)
* Fix shell profile radio button line-ending binding
Only change when updated from a checked radio button

* Fix cancelling profile edit showing default line translation

* call NotifyPropertyChanged
2019-03-26 22:46:02 +01:00
Meir Blachman ed4a5adb16 added test for DialogService.ShowInputDialogAsync (#258) 2019-03-26 22:45:46 +01:00
Felix 82a3532e90 Add missing quotes (#253) 2019-03-25 22:48:26 +01:00
Felix 15d713191c Handle launch fail (#254) 2019-03-25 22:48:17 +01:00
ivanjh 575022139a Add convert line endings to CR option (#251)
* Add convert line endings to CR option

* Change order of line ending handling options (None,CRLF,CR,LF)
2019-03-25 22:48:06 +01:00
Harsh Chokshi b9686ac4ec Use idiomatic JS import style in web client (#245)
Add-ons imported as per Xterm documentation recommendations:
https://xtermjs.org/docs/guides/using-addons/
2019-03-25 20:49:13 +01:00
FelixSe 43e4f8290e Make logging configurable 2019-03-23 11:22:13 +01:00
FelixSe 93387ebc57 Increase version 2019-03-23 10:41:45 +01:00
David Refoua 54a9d7ce0d set TERM to xterm-256color (#239) 2019-03-22 23:11:54 +01:00
Felix 44b306ac20 Add option to always show original shell title in window title (#237) 2019-03-21 23:40:59 +01:00
Felix 14e1e0e3d3 [WIP] Set TERM_PROGRAM (#236)
* Implement for WinPty

* Implement for ConPty
2019-03-21 21:44:49 +01:00
Fabian 136ba3c54f Suggestion: Better CLI parsing, command (#194)
* Better CLI parsing, command

Added the option to run a command via the CLI.
Also added a better argument parsing solution.

You can now do `flute.exe new [PATH] [COMMAND]`

Before, you could only do `flute.exe new [PATH]`

You would also need to adjust your settings, say your default profile is using powershell with custom arguments, those arguments would have to end with ";", because the `[COMMAND]` would be appended to the arguments.

* parse arguments with commandlineparser nuget

* Run commands with no underlying shell process

* Create RunVerb, cleanup terminal creation

* Fix window close bug

* Use working directory of caller if none specified

* Restore profile arguments in WinPty
2019-03-20 21:12:14 +01:00
Mike MacCana 1a0c51c6d4 Fix missing Developer mode requirement for Chocolatey install (#235)
Fixes #230
2019-03-20 21:04:04 +01:00
FelixSe 363018a43f Fix terminal resizing 2019-03-17 13:32:00 +01:00
FelixSe 3dcfaa94e9 Fixed multi window close bug (hopefully) 2019-03-17 10:29:32 +01:00
FelixSe 05104e450e Improved window closed detection 2019-03-16 14:06:48 +01:00
FelixSe 37f20e70bb Fix tab close 2019-03-14 23:12:03 +01:00
FelixSe e5a39fbe0d Cleanup orphaned sessions 2019-03-14 22:33:23 +01:00
FelixSe 5c2f3e6f95 Improve initialization 2019-03-14 22:32:35 +01:00
FelixSe b3e831e3e6 Update UWP version 2019-03-14 22:32:15 +01:00
FelixSe 31ff544379 Remove debug code 2019-03-12 22:49:38 +01:00
FelixSe 7c4f62d16b Fix problems with closing secondary windows 2019-03-12 22:41:16 +01:00
FelixSe f4b4623dab Restore tab underline 2019-03-11 23:23:37 +01:00
FelixSe a32a86e15f Fix focus issues on Windows 10 1803 2019-03-11 22:53:02 +01:00
Eric Cornelissen 6b64d3985d Persist custom tab title (#225)
* Persist custom tab title

* Logically seperate Shell and Tab title, reset Tab title on empty string

* Fix applicationview title not synched with tabtitle
2019-03-10 21:55:41 +01:00
FelixSe ed9c9ad6a6 Only send read bytes 2019-03-10 21:24:11 +01:00
FelixSe bece957ea9 Fix App not closing on SystemTray exit 2019-03-10 20:07:12 +01:00
Felix 4e83e66a85 Fix close being called twice (#227) 2019-03-10 18:09:49 +01:00
Eric Cornelissen 57d86a9723 Add custom context menu for single tab layout (#224)
* Add custom context menu for single tab

* Apply color to single tab

* Make 'None' color transparent
2019-03-10 16:24:02 +01:00
FelixSe 8b6aae7bda Added logging, fixed a problem with websocket 2019-03-10 16:01:34 +01:00
FelixSe 2d204e48ab highlight monospaced fonts 2019-03-09 12:36:38 +01:00
FelixSe 38a7b71fa8 Update dependencies 2019-03-09 10:46:28 +01:00
FelixSe 95c94e3820 Removed unnecessary clearing of webview cache 2019-03-09 10:28:16 +01:00
FelixSe 1920fdf35c Removed reactiveui and implemented custom debouncing 2019-03-09 10:20:17 +01:00
FelixSe 9f3c46c366 FIx settings events on wrong thread 2019-03-09 09:38:48 +01:00
FelixSe 221a2c2c6f Update xterm.js and devDependencies 2019-03-08 22:38:17 +01:00
Wayne Arthurton bde65fa865 Update README.md (#223)
Keybindings for new terminals has been implemented
2019-03-08 18:40:42 +01:00
Felix d52860e90a Dev/renderer neutral (#220)
* Encapsulate xterm.js implementation in XtermTerminalView

* fix TerminalTheme Equals()

* Fix new output indicator

* Ut8 and some fixes

* fix test

* fixed infinite loop on shell exit
2019-03-06 20:06:19 +01:00
74 changed files with 3625 additions and 3009 deletions

No files matched your search

@@ -1,3 +1,3 @@
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%1" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%V" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%V" /f
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%1\"" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
@@ -92,5 +92,18 @@ namespace FluentTerminal.App.Services.Test
shellProfileSelectionDialog.Verify(x => x.SelectProfile(), Times.Once);
}
[Fact]
public void ShowInputDialogAsync_UsesIInputDialogSetTitle()
{
var title = "title";
var inputDialog = new Mock<IInputDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object);
dialogService.ShowInputDialogAsync(title);
inputDialog.Verify(x => x.SetTitle(title), Times.Once);
inputDialog.Verify(x => x.GetInput(), Times.Once);
}
}
}
}
@@ -11,13 +11,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.4.0" />
<PackageReference Include="FluentAssertions" Version="5.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Moq" Version="4.8.3" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<PackageReference Include="AutoFixture" Version="4.8.0" />
<PackageReference Include="FluentAssertions" Version="5.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="Moq" Version="4.10.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
@@ -43,10 +43,11 @@ namespace FluentTerminal.App.Services.Test
var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>();
var id = _fixture.Create<int>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile, sessionType);
var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
result.Should().BeEquivalentTo(response);
}
@@ -72,10 +73,10 @@ namespace FluentTerminal.App.Services.Test
}
[Fact]
public async Task WriteText_Default_SendsWriteTextRequest()
public async Task Write_Default_SendsWriteDataRequest()
{
var terminalId = _fixture.Create<int>();
var text = _fixture.Create<string>();
var data = _fixture.Create<byte[]>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
@@ -86,9 +87,9 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.WriteText(terminalId, text);
await trayProcessCommunicationService.Write(terminalId, data);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteTextRequest))), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteDataRequest))), Times.Once);
}
[Fact]
@@ -150,8 +151,8 @@ namespace FluentTerminal.App.Services.Test
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
{
var terminalId = _fixture.Create<int>();
var output = _fixture.Create<string>();
var receivedOutput = string.Empty;
var output = _fixture.Create<byte[]>();
var receivedOutput = default(byte[]);
var request = new DisplayTerminalOutputRequest
{
TerminalId = terminalId,
@@ -178,7 +179,7 @@ namespace FluentTerminal.App.Services.Test
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
receivedOutput.Should().Be(output);
receivedOutput.Should().BeEquivalentTo(output);
}
}
}
@@ -7,9 +7,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="PListNet" Version="2.0.3" />
<PackageReference Include="RestSharp" Version="106.5.4" />
<PackageReference Include="RestSharp" Version="106.6.9" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.3.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
</ItemGroup>
@@ -9,7 +9,9 @@ namespace FluentTerminal.App.Services
public interface IApplicationView
{
event CloseRequestedHandler CloseRequested;
event EventHandler Closed;
int Id { get; }
string Title { get; set; }
Task RunOnDispatcherThread(Action action);
@@ -1,9 +1,10 @@
using System.Collections.Generic;
using FluentTerminal.Models;
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface ISystemFontService
{
IEnumerable<string> GetSystemFontFamilies();
IEnumerable<FontInfo> GetSystemFontFamilies();
}
}
@@ -1,39 +0,0 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface ITerminalView
{
event EventHandler<TerminalSize> TerminalSizeChanged;
event EventHandler<string> TerminalTitleChanged;
event EventHandler<string> KeyboardCommandReceived;
Task ChangeTheme(TerminalColors theme);
Task ChangeOptions(TerminalOptions options);
Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings);
Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType);
Task ConnectToSocket(string url);
void Close();
Task FocusTerminal();
Task<string> GetSelection();
Task FindNext(string searchText);
Task FindPrevious(string searchText);
void FocusSearchTextBox();
}
}
@@ -12,16 +12,18 @@ namespace FluentTerminal.App.Services
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task ResizeTerminal(int id, TerminalSize size);
Task UpdateToggleWindowKeyBindings();
Task WriteText(int terminalId, string text);
Task Write(int terminalId, byte[] data);
void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback);
Task CloseTerminal(int terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
int GetNextTerminalId();
}
}
@@ -23,7 +23,8 @@ namespace FluentTerminal.App.Services.Implementation
AlwaysShowTabs = false,
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false,
EnableTrayIcon = true
EnableTrayIcon = true,
ShowCustomTitleInTitlebar = true
};
}
@@ -5,9 +5,7 @@ using FluentTerminal.Models.Responses;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Foundation.Metadata;
namespace FluentTerminal.App.Services.Implementation
{
@@ -15,30 +13,52 @@ namespace FluentTerminal.App.Services.Implementation
{
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private readonly Dictionary<int, Action<string>> _terminalOutputHandlers;
private readonly Dictionary<int, Action<byte[]>> _terminalOutputHandlers;
private int _nextTerminalId = 0;
public event EventHandler<int> TerminalExited;
public TrayProcessCommunicationService(ISettingsService settingsService)
{
_settingsService = settingsService;
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
_terminalOutputHandlers = new Dictionary<int, Action<byte[]>>();
}
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
public int GetNextTerminalId()
{
return _nextTerminalId++;
}
public async Task<GetAvailablePortResponse> GetAvailablePort()
{
var request = new GetAvailablePortRequest();
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
Logger.Instance.Debug("Received GetAvailablePortResponse: {@response}", response);
return response;
}
public async Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
{
var request = new CreateTerminalRequest
{
Id = id,
Size = size,
Profile = shellProfile,
SessionType = sessionType
};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
return JsonConvert.DeserializeObject<CreateTerminalResponse>(responseMessage[MessageKeys.Content]);
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>(responseMessage[MessageKeys.Content]);
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
return response;
}
public void Initialize(IAppServiceConnection appServiceConnection)
@@ -62,13 +82,15 @@ namespace FluentTerminal.App.Services.Implementation
}
else
{
Debug.WriteLine("output was not handled: " + request.Output);
Logger.Instance.Error("Received output for unknown terminal Id {id}", request.TerminalId);
}
}
else if (messageType == nameof(TerminalExitedRequest))
{
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
TerminalExited?.Invoke(this, request.TerminalId);
}
}
@@ -84,7 +106,7 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public void SubscribeForTerminalOutput(int terminalId, Action<string> callback)
public void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback)
{
_terminalOutputHandlers[terminalId] = callback;
}
@@ -101,12 +123,12 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public Task WriteText(int id, string text)
public Task Write(int id, byte[] data)
{
var request = new WriteTextRequest
var request = new WriteDataRequest
{
TerminalId = id,
Text = text
Data = data
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
@@ -119,6 +141,8 @@ namespace FluentTerminal.App.Services.Implementation
TerminalId = terminalId
};
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
+66
View File
@@ -0,0 +1,66 @@
using Serilog;
using Serilog.Events;
using System;
namespace FluentTerminal.App.Services
{
public sealed class Logger
{
public enum LogLevel
{
Verbose = 0,
Debug = 1,
Information = 2,
Warning = 3,
Error = 4,
Fatal = 5
}
public class Configuration
{
public LogLevel LogLevel { get; set; } = LogLevel.Error;
}
public static Logger Instance { get; } = new Logger();
private Logger()
{
}
public void Initialize(string filePath, Configuration configuration)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(filePath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7))
.MinimumLevel.Is((LogEventLevel)configuration.LogLevel)
.CreateLogger();
Log.Information("Initialized");
}
public void Debug(string text, params object[] propertyValues)
{
Log.Debug(text, propertyValues, propertyValues);
}
public void Information(string text, params object[] propertyValues)
{
Log.Information(text, propertyValues);
}
public void Warning(string text, params object[] propertyValues)
{
Log.Warning(text, propertyValues);
}
public void Error(string text, params object[] propertyValues)
{
Log.Error(text, propertyValues);
}
public void Error(Exception exception, string text, params object[] propertyValues)
{
Log.Error(exception, text, propertyValues);
}
}
}
+144
View File
@@ -0,0 +1,144 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
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;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
Id = _trayProcessCommunicationService.GetNextTerminalId();
}
private void OnTerminalExited(object sender, int e)
{
if (e == Id)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
/// <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<byte[]> 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);
}
/// <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<CreateTerminalResponse> 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);
}
return response;
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.Write(Id, data);
}
/// <summary>
/// Tells the ViewModel that this view failed to launch and can be closed.
/// </summary>
public void ReportLauchFailed()
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
}
@@ -6,8 +6,7 @@
<ItemGroup>
<PackageReference Include="Fleck" Version="1.0.3" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
<PackageReference Include="ReactiveUI" Version="9.4.1" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
</ItemGroup>
<ItemGroup>
+117 -50
View File
@@ -14,7 +14,6 @@ namespace FluentTerminal.App.ViewModels
{
public class MainViewModel : ViewModelBase
{
private readonly IApplicationView _applicationView;
private readonly IClipboardService _clipboardService;
private readonly IDialogService _dialogService;
private readonly IDispatcherTimer _dispatcherTimer;
@@ -26,6 +25,7 @@ namespace FluentTerminal.App.ViewModels
private double _backgroundOpacity;
private TerminalViewModel _selectedTerminal;
private TabsPosition _tabsPosition;
private string _windowTitle;
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
@@ -39,12 +39,12 @@ namespace FluentTerminal.App.ViewModels
_trayProcessCommunicationService = trayProcessCommunicationService;
_dialogService = dialogService;
_applicationView = applicationView;
ApplicationView = applicationView;
_dispatcherTimer = dispatcherTimer;
_clipboardService = clipboardService;
_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.NewTab), () => AddTerminal());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewTab), () => AddConfigurableTerminal());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), () => SelectedTerminal.EditTitle());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
@@ -68,7 +68,7 @@ namespace FluentTerminal.App.ViewModels
foreach (ShellProfile profile in _settingsService.GetShellProfiles())
{
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), () => AddTerminal(profile.WorkingDirectory, false, profile.Id));
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), () => AddTerminal(profile.Id));
}
var currentTheme = _settingsService.GetCurrentTheme();
@@ -78,14 +78,20 @@ namespace FluentTerminal.App.ViewModels
_applicationSettings = _settingsService.GetApplicationSettings();
TabsPosition = _applicationSettings.TabsPosition;
AddTerminalCommand = new RelayCommand(() => AddTerminal(null, false, Guid.Empty));
AddTerminalCommand = new RelayCommand(() => AddTerminal());
ShowAboutCommand = new RelayCommand(ShowAbout);
ShowSettingsCommand = new RelayCommand(ShowSettings);
_applicationView.CloseRequested += OnCloseRequest;
ApplicationView.CloseRequested += OnCloseRequest;
ApplicationView.Closed += OnClosed;
Terminals.CollectionChanged += OnTerminalsCollectionChanged;
}
private void OnClosed(object sender, EventArgs e)
{
Closed?.Invoke(this, e);
}
private void OnShellProfileDeleted(object sender, Guid e)
{
_keyboardCommandService.DeregisterCommandHandler(e.ToString());
@@ -93,7 +99,7 @@ namespace FluentTerminal.App.ViewModels
private void OnShellProfileAdded(object sender, ShellProfile e)
{
_keyboardCommandService.RegisterCommandHandler(e.Id.ToString(), () => AddTerminal(e.WorkingDirectory, false, e.Id));
_keyboardCommandService.RegisterCommandHandler(e.Id.ToString(), () => AddTerminal(e.Id));
}
private void OnTerminalsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
@@ -112,6 +118,18 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand AddTerminalCommand { get; }
public string WindowTitle
{
get => _windowTitle;
set
{
if (Set(ref _windowTitle, value))
{
ApplicationView.Title = value;
}
}
}
public bool ShowTabsOnTop
{
get => TabsPosition == TabsPosition.Top && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
@@ -149,6 +167,14 @@ namespace FluentTerminal.App.ViewModels
if (SelectedTerminal != null)
{
SelectedTerminal.IsSelected = true;
if (_applicationSettings.ShowCustomTitleInTitlebar)
{
WindowTitle = SelectedTerminal.TabTitle;
}
else
{
WindowTitle = SelectedTerminal.ShellTitle;
}
}
}
}
@@ -166,54 +192,89 @@ namespace FluentTerminal.App.ViewModels
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
/// <summary>
/// Add a new terminal window, either with a specified terminal profile, with the default, or by showing the profile selection dialog.
/// </summary>
/// <returns></returns>
public Task AddTerminal(string startupDirectory, bool showProfileSelection, Guid profileId)
public IApplicationView ApplicationView { get; }
public Task AddConfigurableTerminal()
{
return _applicationView.RunOnDispatcherThread(async () =>
return ApplicationView.RunOnDispatcherThread(async () =>
{
ShellProfile profile = null;
var profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (showProfileSelection)
if (profile == null)
{
profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (profile == null)
if (Terminals.Count == 0)
{
if (Terminals.Count == 0)
{
Closed?.Invoke(this, EventArgs.Empty);
_applicationView.TryClose();
}
return;
await ApplicationView.TryClose();
}
}
else if (profileId == Guid.Empty)
{
profile = _settingsService.GetDefaultShellProfile();
}
else
{
profile = _settingsService.GetShellProfile(profileId);
return;
}
AddTerminal(profile);
});
}
public void AddTerminal()
{
var profile = _settingsService.GetDefaultShellProfile();
AddTerminal(profile);
}
public void AddTerminal(Guid shellProfileId)
{
var profile = _settingsService.GetShellProfile(shellProfileId);
AddTerminal(profile);
}
public void AddTerminal(ShellProfile profile)
{
ApplicationView.RunOnDispatcherThread(() =>
{
var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService,
_applicationSettings, startupDirectory, profile, _applicationView, _dispatcherTimer, _clipboardService);
terminal.Closed += OnTerminalCloseRequested;
_applicationSettings, profile, ApplicationView, _dispatcherTimer, _clipboardService);
terminal.Closed += OnTerminalClosed;
terminal.ShellTitleChanged += Terminal_ShellTitleChanged;
terminal.CustomTitleChanged += Terminal_CustomTitleChanged;
Terminals.Add(terminal);
SelectedTerminal = terminal;
});
}
public void CloseAllTerminals()
private void Terminal_CustomTitleChanged(object sender, string e)
{
if (sender is TerminalViewModel terminal)
{
if (terminal.IsSelected && _applicationSettings.ShowCustomTitleInTitlebar)
{
if (string.IsNullOrWhiteSpace(e))
{
WindowTitle = terminal.ShellTitle;
}
else
{
WindowTitle = e;
}
}
}
}
private void Terminal_ShellTitleChanged(object sender, string e)
{
if (sender is TerminalViewModel terminal && terminal.IsSelected && !_applicationSettings.ShowCustomTitleInTitlebar)
{
WindowTitle = e;
}
}
public async Task CloseAllTerminals()
{
foreach (var terminal in Terminals)
{
terminal.CloseView();
terminal.Closed -= OnTerminalClosed;
terminal.ShellTitleChanged -= Terminal_ShellTitleChanged;
terminal.CustomTitleChanged -= Terminal_CustomTitleChanged;
await terminal.Close();
}
}
@@ -234,12 +295,21 @@ namespace FluentTerminal.App.ViewModels
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
await _applicationView.RunOnDispatcherThread(() =>
await ApplicationView.RunOnDispatcherThread(() =>
{
_applicationSettings = e;
TabsPosition = e.TabsPosition;
RaisePropertyChanged(nameof(ShowTabsOnTop));
RaisePropertyChanged(nameof(ShowTabsOnBottom));
if (e.ShowCustomTitleInTitlebar)
{
WindowTitle = SelectedTerminal?.TabTitle;
}
else
{
WindowTitle = SelectedTerminal?.ShellTitle;
}
});
}
@@ -251,8 +321,7 @@ namespace FluentTerminal.App.ViewModels
if (result == DialogButton.OK)
{
CloseAllTerminals();
Closed?.Invoke(this, EventArgs.Empty);
await CloseAllTerminals();
}
else
{
@@ -261,42 +330,40 @@ namespace FluentTerminal.App.ViewModels
}
else
{
CloseAllTerminals();
Closed?.Invoke(this, EventArgs.Empty);
await CloseAllTerminals();
}
}
private async void OnCurrentThemeChanged(object sender, Guid e)
{
await _applicationView.RunOnDispatcherThread(() =>
await ApplicationView.RunOnDispatcherThread(() =>
{
var currentTheme = _settingsService.GetTheme(e);
Background = currentTheme.Colors.Background;
});
}
private void OnTerminalCloseRequested(object sender, EventArgs e)
private async void OnTerminalClosed(object sender, EventArgs e)
{
if (sender is TerminalViewModel terminal)
{
terminal.CloseView();
if (SelectedTerminal == terminal)
{
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
}
Logger.Instance.Debug("Terminal with Id: {@id} closed.", terminal.Terminal.Id);
Terminals.Remove(terminal);
if (Terminals.Count == 0)
{
Closed?.Invoke(this, EventArgs.Empty);
_applicationView.TryClose();
await ApplicationView.TryClose();
}
}
}
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
await _applicationView.RunOnDispatcherThread(() =>
await ApplicationView.RunOnDispatcherThread(() =>
{
BackgroundOpacity = e.BackgroundOpacity;
});
@@ -336,7 +403,7 @@ namespace FluentTerminal.App.ViewModels
private void ToggleFullScreen()
{
_applicationView.ToggleFullScreen();
ApplicationView.ToggleFullScreen();
}
}
}
@@ -68,6 +68,20 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool ShowCustomTitleInTitlebar
{
get => _applicationSettings.ShowCustomTitleInTitlebar;
set
{
if (_applicationSettings.ShowCustomTitleInTitlebar != value)
{
_applicationSettings.ShowCustomTitleInTitlebar = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool EnableTrayIcon
{
get => _applicationSettings.EnableTrayIcon;
@@ -281,6 +295,7 @@ namespace FluentTerminal.App.ViewModels.Settings
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon;
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
}
}
@@ -1,17 +1,15 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
{
public class TerminalPageViewModel : ReactiveObject
public class TerminalPageViewModel : ViewModelBase
{
private readonly TerminalOptions _terminalOptions;
private readonly ISettingsService _settingsService;
@@ -65,7 +63,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.CursorBlink = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -79,7 +77,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontFamily = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -92,7 +90,8 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_terminalOptions.BackgroundOpacity != value)
{
_terminalOptions.BackgroundOpacity = value;
this.RaisePropertyChanged();
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
@@ -105,7 +104,8 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_terminalOptions.Padding != value)
{
_terminalOptions.Padding = value;
this.RaisePropertyChanged();
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
@@ -126,12 +126,12 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.ScrollBackLimit = intValue;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
else
{
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -145,12 +145,12 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
public IEnumerable<string> Fonts { get; }
public IEnumerable<FontInfo> Fonts { get; }
public int FontSize
{
@@ -161,7 +161,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontSize = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -180,10 +180,10 @@ namespace FluentTerminal.App.ViewModels.Settings
_isEditingCursorStyle = true;
_terminalOptions.CursorStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(BlockIsSelected));
this.RaisePropertyChanged(nameof(BarIsSelected));
this.RaisePropertyChanged(nameof(UnderlineIsSelected));
RaisePropertyChanged();
RaisePropertyChanged(nameof(BlockIsSelected));
RaisePropertyChanged(nameof(BarIsSelected));
RaisePropertyChanged(nameof(UnderlineIsSelected));
_isEditingCursorStyle = false;
}
}
@@ -199,10 +199,10 @@ namespace FluentTerminal.App.ViewModels.Settings
_isEditingScrollBarStyle = true;
_terminalOptions.ScrollBarStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(HiddenIsSelected));
this.RaisePropertyChanged(nameof(AutoHidingIsSelected));
this.RaisePropertyChanged(nameof(VisibleIsSelected));
RaisePropertyChanged();
RaisePropertyChanged(nameof(HiddenIsSelected));
RaisePropertyChanged(nameof(AutoHidingIsSelected));
RaisePropertyChanged(nameof(VisibleIsSelected));
_isEditingScrollBarStyle = false;
}
}
@@ -234,19 +234,10 @@ namespace FluentTerminal.App.ViewModels.Settings
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s);
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
Sizes = Enumerable.Range(1, 72);
_terminalOptions = _settingsService.GetTerminalOptions();
this.ObservableForProperty(x => x.BackgroundOpacity).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
{
_settingsService.SaveTerminalOptions(_terminalOptions);
});
this.ObservableForProperty(x => x.Padding).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
{
_settingsService.SaveTerminalOptions(_terminalOptions);
});
}
}
}
@@ -30,7 +30,6 @@ namespace FluentTerminal.App.ViewModels
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, Boolean isNew)
{
@@ -200,37 +199,31 @@ namespace FluentTerminal.App.ViewModels
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;
}
}
set => Set(ref _lineEndingStyle, value);
}
public bool DoNotModifyIsSelected
{
get => LineEndingStyle == LineEndingStyle.DoNotModify;
set => LineEndingStyle = LineEndingStyle.DoNotModify;
set { if (value) LineEndingStyle = LineEndingStyle.DoNotModify; }
}
public bool ToCRLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCRLF;
set => LineEndingStyle = LineEndingStyle.ToCRLF;
set { if (value) LineEndingStyle = LineEndingStyle.ToCRLF; }
}
public bool ToLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToLF;
set => LineEndingStyle = LineEndingStyle.ToLF;
set { if (value) LineEndingStyle = LineEndingStyle.ToLF; }
}
public bool ToCRIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCR;
set { if (value) LineEndingStyle = LineEndingStyle.ToCR; }
}
public void SaveChanges()
@@ -363,6 +356,7 @@ namespace FluentTerminal.App.ViewModels
_fallbackProfile = new ShellProfile
{
Arguments = Model.Arguments,
LineEndingTranslation = Model.LineEndingTranslation,
Location = Model.Location,
Name = Model.Name,
WorkingDirectory = Model.WorkingDirectory,
+178 -232
View File
@@ -1,106 +1,107 @@
using Fleck;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Utilities;
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.Text;
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 InputBuffer _buffer;
private bool _isSelected;
private bool _newOutput;
private bool _hasNewOutput;
private string _resizeOverlayContent;
private string _searchText;
private bool _showResizeOverlay;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private int _terminalId;
private TerminalTheme _terminalTheme;
private ITerminalView _terminalView;
private string _title;
private IWebSocketConnection _webSocket;
private string _tabTitle;
private string _shellTitle;
private bool _hasCustomTitle;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, 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;
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);
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 event EventHandler<string> ShellTitleChanged;
public event EventHandler<string> CustomTitleChanged;
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 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; }
@@ -108,8 +109,6 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand FindPreviousCommand { get; }
public bool Initialized { get; private set; }
public bool IsSelected
{
get => _isSelected;
@@ -119,8 +118,7 @@ namespace FluentTerminal.App.ViewModels
{
if (IsSelected)
{
_applicationView.Title = Title;
_applicationView.RunOnDispatcherThread(() => NewOutput = false);
HasNewOutput = false;
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
@@ -131,10 +129,10 @@ namespace FluentTerminal.App.ViewModels
public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) ||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null);
public bool NewOutput
public bool HasNewOutput
{
get => _newOutput;
set => Set(ref _newOutput, value);
get => _hasNewOutput;
set => Set(ref _hasNewOutput, value);
}
public string ResizeOverlayContent
@@ -151,6 +149,10 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand<string> SelectTabThemeCommand { get; }
public ISettingsService SettingsService { get; }
public ShellProfile ShellProfile { get; }
public bool ShowResizeOverlay
{
get => _showResizeOverlay;
@@ -187,145 +189,97 @@ 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
public string TabTitle
{
get => _title;
get => _tabTitle;
set
{
value = string.IsNullOrWhiteSpace(value) ? DefaultTitle : value;
if (Set(ref _title, value) && IsSelected)
if (Set(ref _tabTitle, value))
{
_applicationView.Title = Title;
CustomTitleChanged?.Invoke(this, value);
}
}
}
public async Task CloseView()
public string ShellTitle
{
_terminalView.Close();
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
get => _shellTitle;
set
{
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
{
TabTitle = value;
}
}
}
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task Close()
{
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
return Terminal.Close();
}
public void CopyText(string text)
{
_clipboardService.SetText(text);
}
public Task FocusTerminal()
{
return _terminalView?.FocusTerminal();
ClipboardService.SetText(text);
}
public async Task EditTitle()
{
var result = await _dialogService.ShowInputDialogAsync("Edit Title");
var result = await DialogService.ShowInputDialogAsync("Edit Title");
if (result != null)
{
Title = result;
if (string.IsNullOrWhiteSpace(result))
{
_hasCustomTitle = false;
TabTitle = ShellTitle;
}
else
{
_hasCustomTitle = true;
TabTitle = 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 settings = _settingsService.GetApplicationSettings();
var sessionType = SessionType.Unknown;
if (!_applicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7) || settings.AlwaysUseWinPty)
{
sessionType = SessionType.WinPty;
}
else
{
sessionType = SessionType.ConPty;
}
var size = await _terminalView.CreateTerminal(options, TerminalTheme.Colors, FlattenKeyBindings(keyBindings, profiles), sessionType).ConfigureAwait(true);
if (!string.IsNullOrWhiteSpace(_startupDirectory))
{
_shellProfile.WorkingDirectory = _startupDirectory;
}
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
_terminalId = response.Id;
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
{
_connectedEvent.Wait();
_buffer.Write(t);
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
_applicationView.RunOnDispatcherThread(() => NewOutput = true);
}
});
DefaultTitle = response.ShellExecutableName;
Title = DefaultTitle;
var webSocketUrl = "ws://127.0.0.1:" + response.Id;
var webSocketServer = new WebSocketServer(webSocketUrl);
webSocketServer.Start(socket =>
{
_webSocket = socket;
_buffer = new InputBuffer(_webSocket.Send);
socket.OnOpen = () => _connectedEvent.Set();
socket.OnMessage = message => _trayProcessCommunicationService.WriteText(_terminalId, message);
});
await _terminalView.ConnectToSocket(webSocketUrl).ConfigureAwait(true);
Initialized = true;
}
else
{
await _dialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
}
await FocusTerminal().ConfigureAwait(true);
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));
@@ -335,73 +289,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)
{
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)
{
switch (command)
{
case nameof(Command.Copy):
{
var selection = await _terminalView.GetSelection().ConfigureAwait(true);
_clipboardService.SetText(selection);
break;
}
case nameof(Command.Paste):
{
string content = await _clipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = _shellProfile.TranslateLineEndings(content);
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
}
break;
}
case nameof(Command.PasteWithoutNewlines):
{
string content = await _clipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
}
break;
}
case nameof(Command.Search):
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
_terminalView.FocusSearchTextBox();
}
break;
}
default:
{
_keyboardCommandService.SendCommand(command);
break;
}
}
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
}
private void OnResizeOverlayTimerFinished(object sender, object e)
@@ -410,36 +312,9 @@ namespace FluentTerminal.App.ViewModels
ShowResizeOverlay = false;
}
private void OnTerminalExited(object sender, int 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;
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
}
private void SelectTabTheme(string name)
@@ -447,11 +322,83 @@ namespace FluentTerminal.App.ViewModels
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
}
private void Terminal_Closed(object sender, EventArgs e)
{
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
{
switch (e)
{
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
ClipboardService.SetText(selection);
break;
}
case nameof(Command.Paste):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
}
case nameof(Command.PasteWithoutNewlines):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
}
case nameof(Command.Search):
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
SearchStarted?.Invoke(this, EventArgs.Empty);
}
break;
}
default:
{
_keyboardCommandService.SendCommand(e);
break;
}
}
}
private void Terminal_OutputReceived(object sender, byte[] e)
{
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = 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)
{
ShellTitle = e;
ShellTitleChanged?.Invoke(this, 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)
{
@@ -459,8 +406,7 @@ namespace FluentTerminal.App.ViewModels
}
}
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
Closed?.Invoke(this, EventArgs.Empty);
await Close().ConfigureAwait(true);
}
}
}
@@ -1,42 +0,0 @@
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);
}
}
}
}
@@ -14,20 +14,32 @@ namespace FluentTerminal.App.Adapters
{
private readonly ApplicationView _applicationView;
private readonly CoreDispatcher _dispatcher;
private bool _closed;
public event CloseRequestedHandler CloseRequested;
public event EventHandler Closed;
public ApplicationViewAdapter()
{
_applicationView = ApplicationView.GetForCurrentView();
_applicationView.Consolidated += _applicationView_Consolidated;
_dispatcher = CoreApplication.GetCurrentView().Dispatcher;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequest;
Logger.Instance.Debug("Created ApplicationViewAdapter for ApplicationView with Id: {Id}", _applicationView.Id);
}
private void _applicationView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
Closed?.Invoke(this, EventArgs.Empty);
}
public int Id => _applicationView.Id;
public string Title
{
get => _applicationView.Title;
set => _applicationView.Title = value;
set => _applicationView.Title = value ?? string.Empty;
}
public bool IsApiContractPresent(string api, ushort version)
@@ -40,9 +52,16 @@ namespace FluentTerminal.App.Adapters
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
}
public Task<bool> TryClose()
public async Task<bool> TryClose()
{
return _applicationView.TryConsolidateAsync().AsTask();
if (_closed)
{
Logger.Instance.Debug("ApplicationViewAdapter.TryClose was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
return true;
}
Logger.Instance.Debug("TryClose ApplicationView with Id: {Id}", _applicationView.Id);
_closed = await _applicationView.TryConsolidateAsync().AsTask();
return _closed;
}
public bool ToggleFullScreen()
@@ -63,6 +82,7 @@ namespace FluentTerminal.App.Adapters
var deferral = e.GetDeferral();
var args = new CancelableEventArgs();
if (CloseRequested != null)
{
await CloseRequested.Invoke(this, args);
@@ -70,6 +90,11 @@ namespace FluentTerminal.App.Adapters
e.Handled = args.Cancelled;
if (!e.Handled)
{
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
}
deferral.Complete();
}
}
+1
View File
@@ -38,6 +38,7 @@
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
<converters:EnumToDescriptionConverter x:Key="EnumToDescriptionConverter" />
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
+180 -48
View File
@@ -1,5 +1,7 @@
using Autofac;
using CommandLine;
using FluentTerminal.App.Adapters;
using FluentTerminal.App.CommandLineArguments;
using FluentTerminal.App.Dialogs;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Adapters;
@@ -10,9 +12,13 @@ using FluentTerminal.App.ViewModels;
using FluentTerminal.App.Views;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
@@ -42,6 +48,7 @@ namespace FluentTerminal.App
private int? _settingsWindowId;
private IAppServiceConnection _appServiceConnection;
private BackgroundTaskDeferral _appServiceDeferral;
private Parser _commandLineParser;
public App()
{
@@ -49,6 +56,8 @@ namespace FluentTerminal.App
InitializeComponent();
UnhandledException += OnUnhandledException;
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
@@ -91,9 +100,55 @@ namespace FluentTerminal.App
_trayProcessCommunicationService = _container.Resolve<ITrayProcessCommunicationService>();
_applicationSettings = _settingsService.GetApplicationSettings();
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(typeof(CamelCaseNamingStrategy)));
return settings;
};
_commandLineParser = new Parser(settings =>
{
settings.CaseSensitive = false;
settings.CaseInsensitiveEnumValues = true;
});
}
protected override async void OnActivated(IActivatedEventArgs args)
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.Instance.Error(e.Exception, "Unhandled Exception");
}
private static IEnumerable<string> SplitArguments(string arguments)
{
var chars = arguments.ToCharArray();
var inQuote = false;
for (var i = 0; i < chars.Length; i++)
{
if (chars[i] == '"')
{
inQuote = !inQuote;
}
if (!inQuote && chars[i] == ' ')
{
chars[i] = '\n';
}
}
foreach (var value in new string(chars).Split('\n'))
{
yield return value.Trim('"');
}
}
protected override void OnActivated(IActivatedEventArgs args)
{
if (args is CommandLineActivatedEventArgs commandLineActivated)
{
@@ -102,42 +157,67 @@ namespace FluentTerminal.App
return;
}
var arguments = commandLineActivated.Operation.Arguments.Split(' ', 2);
var command = arguments[0];
var parameter = arguments.Length > 1 ? arguments[1] : string.Empty;
if (_alreadyLaunched)
_commandLineParser.ParseArguments(SplitArguments(commandLineActivated.Operation.Arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
{
if (command == "settings")
if (verb is SettingsVerb)
{
await ShowSettings().ConfigureAwait(true);
}
else if (command == "new")
else if (verb is NewVerb newVerb)
{
if (_applicationSettings.NewTerminalLocation == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
var profile = default(ShellProfile);
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
{
await _mainViewModels.Last().AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
}
else
if (profile == null)
{
await CreateNewTerminalWindow(parameter, false).ConfigureAwait(true);
profile = _settingsService.GetDefaultShellProfile();
}
if (!string.IsNullOrWhiteSpace(newVerb.Directory))
{
profile.WorkingDirectory = newVerb.Directory;
}
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
}
else
{
if (command == "settings")
else if (verb is RunVerb runVerb)
{
var viewModel = _container.Resolve<SettingsViewModel>();
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
var profile = new ShellProfile
{
Id = Guid.Empty,
Location = null,
Arguments = runVerb.Command,
WorkingDirectory = runVerb.Directory
};
if (!string.IsNullOrWhiteSpace(runVerb.Theme))
{
var theme = _settingsService.GetThemes().FirstOrDefault(x => x.Name.Equals(runVerb.Theme, StringComparison.CurrentCultureIgnoreCase));
if (theme != null)
{
profile.TerminalThemeId = theme.Id;
}
}
if (string.IsNullOrWhiteSpace(profile.WorkingDirectory))
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = runVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: runVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
else if (command == "new")
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
}
}
});
}
}
@@ -145,14 +225,29 @@ namespace FluentTerminal.App
{
if (!_alreadyLaunched)
{
var logDirectory = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("Logs", CreationCollisionOption.OpenIfExists);
var logFile = Path.Combine(logDirectory.Path, "fluentterminal.app.log");
var configFile = await logDirectory.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var configContent = await FileIO.ReadTextAsync(configFile);
if (string.IsNullOrWhiteSpace(configContent))
{
configContent = JsonConvert.SerializeObject(new Logger.Configuration());
await FileIO.WriteTextAsync(configFile, configContent);
}
var config = JsonConvert.DeserializeObject<Logger.Configuration>(configContent) ?? new Logger.Configuration();
Logger.Instance.Initialize(logFile, config);
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminal(null, false, Guid.Empty).ConfigureAwait(true);
viewModel.AddTerminal();
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
Window.Current.Activate();
}
else if (_mainViewModels.Count == 0)
{
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, false, string.Empty).ConfigureAwait(true);
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
}
}
@@ -213,13 +308,19 @@ namespace FluentTerminal.App
Window.Current.Activate();
}
private async Task CreateNewTerminalWindow(string startupDirectory, bool showProfileSelection)
private async Task<MainViewModel> CreateNewTerminalWindow()
{
var id = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, showProfileSelection, startupDirectory).ConfigureAwait(true);
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(id);
var viewModel = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
viewModel.Closed += OnMainViewModelClosed;
viewModel.NewWindowRequested += OnNewWindowRequested;
viewModel.ShowSettingsRequested += OnShowSettingsRequested;
viewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(viewModel);
return viewModel;
}
private async Task<int> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar, bool showProfileSelection, object parameter)
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar)
{
int windowId = 0;
TViewModel viewModel = default;
@@ -236,23 +337,16 @@ namespace FluentTerminal.App
windowId = ApplicationView.GetForCurrentView().Id;
});
if (viewModel is MainViewModel mainViewModel && parameter is string directory)
{
mainViewModel.Closed += OnMainViewModelClosed;
mainViewModel.NewWindowRequested += OnNewWindowRequested;
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(mainViewModel);
await mainViewModel.AddTerminal(directory, showProfileSelection, Guid.Empty).ConfigureAwait(true);
}
if (viewModel is SettingsViewModel settingsViewModel)
{
_settingsViewModel = settingsViewModel;
_settingsViewModel.Closed += OnSettingsClosed;
_settingsWindowId = windowId;
}
return windowId;
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(windowId);
return viewModel;
}
private void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
@@ -264,9 +358,12 @@ namespace FluentTerminal.App
{
if (sender is MainViewModel viewModel)
{
Logger.Instance.Debug("MainViewModel with ApplicationView Id: {@id} closed.", viewModel.ApplicationView.Id);
viewModel.Closed -= OnMainViewModelClosed;
viewModel.NewWindowRequested -= OnNewWindowRequested;
viewModel.ShowSettingsRequested -= OnShowSettingsRequested;
viewModel.ShowAboutRequested -= OnShowAboutRequested;
_mainViewModels.Remove(viewModel);
}
@@ -274,7 +371,16 @@ namespace FluentTerminal.App
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
await CreateNewTerminalWindow(string.Empty, e.ShowProfileSelection).ConfigureAwait(true);
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
if (e.ShowProfileSelection)
{
await viewModel.AddConfigurableTerminal().ConfigureAwait(true);
}
else
{
viewModel.AddTerminal();
}
}
private void OnSettingsClosed(object sender, EventArgs e)
@@ -300,20 +406,46 @@ namespace FluentTerminal.App
_settingsViewModel.NavigateToAboutPage();
}
private async Task CreateTerminal(ShellProfile profile, NewTerminalLocation location)
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<MainViewModel>();
viewModel.AddTerminal(profile);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
}
else if (location == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
{
_mainViewModels.Last().AddTerminal(profile);
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
viewModel.AddTerminal(profile);
}
}
private async Task ShowSettings()
{
if (_settingsViewModel == null)
if (!_alreadyLaunched)
{
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, false, null).ConfigureAwait(true);
var viewModel = _container.Resolve<SettingsViewModel>();
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
}
else if (_settingsViewModel == null)
{
await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true).ConfigureAwait(true);
}
else
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
}
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
}
private async Task StartSystemTray()
{
var launch = FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("AppLaunchedParameterGroup").AsTask();
var clearCache = WebView.ClearTemporaryWebDataAsync().AsTask();
await Task.WhenAll(launch, clearCache, _trayReady.Task).ConfigureAwait(true);
await Task.WhenAll(launch, _trayReady.Task).ConfigureAwait(true);
_trayProcessCommunicationService.Initialize(_appServiceConnection);
}
}
@@ -0,0 +1,17 @@
using CommandLine;
namespace FluentTerminal.App.CommandLineArguments
{
[Verb("new")]
public class NewVerb
{
[Value(0)]
public string Directory { get; set; }
[Option("profile")]
public string Profile { get; set; }
[Option("target")]
public Target Target { get; set; }
}
}
@@ -0,0 +1,20 @@
using CommandLine;
namespace FluentTerminal.App.CommandLineArguments
{
[Verb("run")]
public class RunVerb
{
[Value(0)]
public string Command { get; set; }
[Option("directory")]
public string Directory { get; set; }
[Option("theme")]
public string Theme { get; set; }
[Option("target")]
public Target Target { get; set; }
}
}
@@ -0,0 +1,9 @@
using CommandLine;
namespace FluentTerminal.App.CommandLineArguments
{
[Verb("settings")]
public class SettingsVerb
{
}
}
@@ -0,0 +1,9 @@
namespace FluentTerminal.App.CommandLineArguments
{
public enum Target
{
Default,
Tab,
Window
}
}
@@ -1,5 +1,6 @@
using Microsoft.Toolkit.Uwp.Helpers;
using System;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;
@@ -0,0 +1,23 @@
using System;
using Windows.UI.Text;
using Windows.UI.Xaml.Data;
namespace FluentTerminal.App.Converters
{
public class TrueToBoldConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool bold)
{
return bold ? FontWeights.Bold : FontWeights.Normal;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}
+26 -5
View File
@@ -111,6 +111,10 @@
<DependentUpon>App.xaml</DependentUpon>
</Compile>
<Compile Include="Behaviors\MiddleClickBehavior.cs" />
<Compile Include="CommandLineArguments\NewVerb.cs" />
<Compile Include="CommandLineArguments\RunVerb.cs" />
<Compile Include="CommandLineArguments\SettingsVerb.cs" />
<Compile Include="CommandLineArguments\Target.cs" />
<Compile Include="Converters\BackgroundToApplicationThemeConverter.cs" />
<Compile Include="Converters\ColorResourceKeyFallbackConverter.cs" />
<Compile Include="Converters\EnumToDescriptionConverter.cs" />
@@ -124,6 +128,7 @@
<Compile Include="Converters\TabThemeSelectedConverter.cs" />
<Compile Include="Converters\TerminalViewModelToViewConverter.cs" />
<Compile Include="Converters\ToolTipValueToPixelConverter.cs" />
<Compile Include="Converters\TrueToBoldConverter.cs" />
<Compile Include="Converters\TrueToVisibleConverter.cs" />
<Compile Include="Adapters\MessageDialogAdapter.cs" />
<Compile Include="Dialogs\InputDialog.xaml.cs">
@@ -138,6 +143,7 @@
<Compile Include="Services\SystemFontService.cs" />
<Compile Include="Utilities\ColorExtensions.cs" />
<Compile Include="Utilities\ContrastHelper.cs" />
<Compile Include="Utilities\DebouncedAction.cs" />
<Compile Include="Views\KeyBindingsView.xaml.cs">
<DependentUpon>KeyBindingsView.xaml</DependentUpon>
</Compile>
@@ -183,9 +189,13 @@
</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\XtermTerminalView.xaml.cs">
<DependentUpon>XtermTerminalView.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
@@ -202,7 +212,11 @@
<Content Include="Win32\FluentTerminal.SystemTray.pdb" />
<Content Include="Win32\GlobalHotKey.dll" />
<Content Include="Win32\Newtonsoft.Json.dll" />
<Content Include="Win32\PListNet.dll" />
<Content Include="Win32\RestSharp.dll" />
<Content Include="Win32\Serilog.dll" />
<Content Include="Win32\Serilog.Sinks.Async.dll" />
<Content Include="Win32\Serilog.Sinks.File.dll" />
<Content Include="Win32\System.Data.Common.dll" />
<Content Include="Win32\System.Diagnostics.StackTrace.dll" />
<Content Include="Win32\System.Diagnostics.Tracing.dll" />
@@ -350,25 +364,32 @@
<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">
<Version>4.8.1</Version>
<Version>4.9.1</Version>
</PackageReference>
<PackageReference Include="CommandLineParser">
<Version>2.4.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.1.5</Version>
<Version>6.2.8</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.UI">
<Version>3.0.0</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.UI.Controls">
<Version>3.0.0</Version>
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Xaml.Behaviors.Uwp.Managed">
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version>
<Version>12.0.1</Version>
</PackageReference>
<PackageReference Include="Win2D.uwp">
<Version>1.21.0</Version>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" IgnorableNamespaces="uap mp desktop rescap">
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.3.1.0" />
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.4.1.0" />
<mp:PhoneIdentity PhoneProductId="f7225014-f405-4342-ad2e-ebf6ac60ea2c" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
<Properties>
<DisplayName>Fluent Terminal</DisplayName>
@@ -1,13 +1,28 @@
using Microsoft.Graphics.Canvas.Text;
using FluentTerminal.Models;
using Microsoft.Graphics.Canvas.Text;
using System.Collections.Generic;
using System.Linq;
namespace FluentTerminal.App.Services
{
public class SystemFontService : ISystemFontService
{
public IEnumerable<string> GetSystemFontFamilies()
public IEnumerable<FontInfo> GetSystemFontFamilies()
{
return CanvasTextFormat.GetSystemFontFamilies();
return CanvasFontSet.GetSystemFontSet().Fonts.Select(x => new FontInfo { Name = x.FamilyNames.Values.FirstOrDefault(), IsMonospaced = x.IsMonospaced }).Distinct(new FontInfoComparer());
}
private class FontInfoComparer : IEqualityComparer<FontInfo>
{
public bool Equals(FontInfo x, FontInfo y)
{
return x.Name == y.Name;
}
public int GetHashCode(FontInfo obj)
{
return obj.Name.GetHashCode();
}
}
}
}
@@ -0,0 +1,49 @@
using System;
using Windows.UI.Core;
using Windows.UI.Xaml;
namespace FluentTerminal.App.Utilities
{
public class DebouncedAction<T>
{
private DispatcherTimer timer;
private readonly CoreDispatcher _dispatcher;
private readonly TimeSpan _interval;
private readonly Action<T> _action;
private T _parameter;
public DebouncedAction(CoreDispatcher dispatcher, TimeSpan interval, Action<T> action)
{
_dispatcher = dispatcher;
_interval = interval;
_action = action;
}
public void Invoke(T parameter)
{
_parameter = parameter;
timer?.Stop();
timer = null;
timer = new DispatcherTimer
{
Interval = _interval
};
timer.Tick += (s, e) =>
{
if (timer == null)
{
return;
}
timer?.Stop();
timer = null;
_dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => _action.Invoke(_parameter));
};
timer.Start();
}
}
}
+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();
}
}
+101 -7
View File
@@ -76,14 +76,108 @@
</MenuFlyout>
</Button.Flyout>
</Button>
<TextBlock
<RelativePanel
Grid.Column="1"
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}}" />
Height="32"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Background="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabColorFallbackConverter}, ConverterParameter=Background}">
<RelativePanel.ContextFlyout>
<MenuFlyout>
<MenuFlyoutSubItem Text="Color">
<ToggleMenuFlyoutItem
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
CommandParameter="None"
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=None}"
Text="None">
<ToggleMenuFlyoutItem.Icon>
<FontIcon FontFamily="Segoe MDL2 Assets" Glyph="&#xF126;" />
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
<ToggleMenuFlyoutItem
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
CommandParameter="Red"
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Red}"
Text="Red">
<ToggleMenuFlyoutItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Foreground="#E81123"
Glyph="&#xF127;" />
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
<ToggleMenuFlyoutItem
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
CommandParameter="Green"
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Green}"
Text="Green">
<ToggleMenuFlyoutItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Foreground="#10893E"
Glyph="&#xF127;" />
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
<ToggleMenuFlyoutItem
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
CommandParameter="Blue"
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Blue}"
Text="Blue">
<ToggleMenuFlyoutItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Foreground="#0078D7"
Glyph="&#xF127;" />
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
<ToggleMenuFlyoutItem
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
CommandParameter="Purple"
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Purple}"
Text="Purple">
<ToggleMenuFlyoutItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Foreground="#881798"
Glyph="&#xF127;" />
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
<ToggleMenuFlyoutItem
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
CommandParameter="Orange"
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Orange}"
Text="Orange">
<ToggleMenuFlyoutItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Foreground="#FF8C00"
Glyph="&#xF127;" />
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
<ToggleMenuFlyoutItem
Command="{x:Bind ViewModel.SelectedTerminal.SelectTabThemeCommand}"
CommandParameter="Teal"
IsChecked="{x:Bind ViewModel.SelectedTerminal.TabTheme, Mode=OneWay, Converter={StaticResource TabThemeSelectedConverter}, ConverterParameter=Teal}"
Text="Teal">
<ToggleMenuFlyoutItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Foreground="#00B7C3"
Glyph="&#xF127;" />
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
</MenuFlyoutSubItem>
<MenuFlyoutItem Command="{x:Bind ViewModel.SelectedTerminal.EditTitleCommand}" Text="Edit Title" />
</MenuFlyout>
</RelativePanel.ContextFlyout>
<TextBlock
Margin="12,8,12,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.WindowTitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind ViewModel.ShowTabsOnTop, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
</RelativePanel>
<views:TabBar
Grid.Column="1"
HorizontalAlignment="Left"
+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();
}
}
@@ -84,6 +84,11 @@
Header="Always show tabs"
IsOn="{x:Bind ViewModel.AlwaysShowTabs, Mode=TwoWay}" />
<ToggleSwitch
Margin="{StaticResource ItemMargin}"
Header="Show custom tab title also in titlebar"
IsOn="{x:Bind ViewModel.ShowCustomTitleInTitlebar, Mode=TwoWay}" />
<ToggleSwitch
Margin="{StaticResource ItemMargin}"
Header="Underline selected tab"
@@ -292,6 +292,10 @@
Content="Convert line endings to CR+LF"
GroupName="LineEndingStyle"
IsChecked="{x:Bind ToCRLFIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Convert line endings to CR"
GroupName="LineEndingStyle"
IsChecked="{x:Bind ToCRIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Convert line endings to LF"
GroupName="LineEndingStyle"
@@ -5,6 +5,7 @@
xmlns:converters="using:FluentTerminal.App.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:FluentTerminal.Models"
mc:Ignorable="d">
<Page.Resources>
<converters:ToolTipValueToPixelConverter x:Key="ToolTipValueToPixelConverter" />
@@ -45,7 +46,14 @@
Margin="{StaticResource ItemMargin}"
Header="Font family"
ItemsSource="{x:Bind ViewModel.Fonts}"
SelectedValue="{x:Bind ViewModel.FontFamily, Mode=TwoWay}" />
SelectedValue="{x:Bind ViewModel.FontFamily, Mode=TwoWay}"
SelectedValuePath="Name">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:FontInfo">
<TextBlock FontWeight="{x:Bind IsMonospaced, Converter={StaticResource TrueToBoldConverter}}" Text="{x:Bind Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox
Width="200"
+3 -3
View File
@@ -58,7 +58,7 @@
Margin="0"
VerticalAlignment="Stretch"
Background="Transparent"
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
ToolTipService.ToolTip="{x:Bind TabTitle, Mode=OneWay}">
<Interactivity:Interaction.Behaviors>
<behaviors:MiddleClickBehavior>
<behaviors:MiddleClickBehavior.Actions>
@@ -171,7 +171,7 @@
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.LeftOf="CloseButton"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Title, Mode=OneWay}"
Text="{x:Bind TabTitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<Grid
@@ -189,7 +189,7 @@
<SymbolIcon Symbol="Cancel" />
</Viewbox>
</Button>
<Grid Height="32" Visibility="{x:Bind NewOutput, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<Grid Height="32" Visibility="{x:Bind HasNewOutput, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<Viewbox
Width="12"
Height="12"
+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}" />
+29 -258
View File
@@ -1,248 +1,67 @@
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();
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, SessionType sessionType)
{
var serializedOptions = JsonConvert.SerializeObject(options);
var serializedTheme = JsonConvert.SerializeObject(theme);
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}', '{sessionType}')").ConfigureAwait(true);
return JsonConvert.DeserializeObject<TerminalSize>(size);
}
public Task<string> ExecuteScriptAsync(string script)
{
try
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 +73,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,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,361 @@
using Fleck;
using FluentTerminal.App.Services;
using FluentTerminal.App.Utilities;
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.RuntimeComponent.Enums;
using FluentTerminal.RuntimeComponent.Interfaces;
using FluentTerminal.RuntimeComponent.WebAllowedObjects;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
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 _navigationCompleted;
private readonly MenuFlyoutItem _copyMenuItem;
private BlockingCollection<Action> _dispatcherJobs;
private readonly MenuFlyoutItem _pasteMenuItem;
private WebView _webView;
private readonly DebouncedAction<TerminalOptions> _optionsChanged;
private IWebSocketConnection _socket;
public XtermTerminalView()
{
InitializeComponent();
StartMediatorTask();
_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 }
};
_optionsChanged = new DebouncedAction<TerminalOptions>(Dispatcher, TimeSpan.FromMilliseconds(800), async options =>
{
var serialized = JsonConvert.SerializeObject(options);
await ExecuteScriptAsync($"changeOptions('{serialized}')");
});
_navigationCompleted = new SemaphoreSlim(0, 1);
_connectedEvent = new ManualResetEventSlim(false);
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
}
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)
{
_optionsChanged.Invoke(options);
return Task.CompletedTask;
}
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 _navigationCompleted.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();
var response = await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType).ConfigureAwait(true);
if (!response.Success)
{
await ViewModel.DialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
ViewModel.Terminal.ReportLauchFailed();
return;
}
_webView.Focus(FocusState.Programmatic);
}
void IxtermEventListener.OnKeyboardCommand(string command)
{
Logger.Instance.Debug("Received keyboard command: '{command}'", 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)
{
Logger.Instance.Debug("WebView navigation completed. Target: {uri}", args.Uri);
_navigationCompleted.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)
{
FleckLog.LogAction = (level, message, exception) =>
{
// todo: send debug to verbose
switch (level)
{
case LogLevel.Info:
Logger.Instance.Information(message);
break;
case LogLevel.Warn:
Logger.Instance.Warning(message);
break;
case LogLevel.Error:
Logger.Instance.Error(message);
break;
}
if (exception != null)
{
Logger.Instance.Error(exception, "Fleck Exception");
}
};
var webSocketUrl = "ws://127.0.0.1:" + port;
var webSocketServer = new WebSocketServer(webSocketUrl);
webSocketServer.Start(socket =>
{
_socket = socket;
socket.OnOpen = () =>
{
Logger.Instance.Debug("WebSocket open");
_connectedEvent.Set();
};
socket.OnMessage = message => ViewModel.Terminal.Write(Encoding.UTF8.GetBytes(message));
});
Logger.Instance.Debug("WebSocketServer started. Calling connectToWebSocket() now.");
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.Factory.StartNew(async () =>
{
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
{
try
{
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => job.Invoke());
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
}, TaskCreationOptions.LongRunning);
}
private async void Terminal_Closed(object sender, EventArgs e)
{
await ViewModel.ApplicationView.RunOnDispatcherThread(() =>
{
ViewModel.Terminal.OutputReceived -= Terminal_OutputReceived;
ViewModel.Terminal.Closed -= Terminal_Closed;
_webView?.Navigate(new Uri("about:blank"));
_webView = null;
});
}
private void Terminal_OutputReceived(object sender, byte[] e)
{
_socket.Send(Encoding.UTF8.GetString(e));
}
void IxtermEventListener.OnError(string error)
{
Logger.Instance.Error(error);
}
}
}
+1759 -2095
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -10,10 +10,10 @@
"author": "FS Apps",
"license": "GPL-3.0",
"devDependencies": {
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0"
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3"
},
"dependencies": {
"xterm": "^3.11.0"
"xterm": "^3.12.0"
}
}
+11 -9
View File
@@ -1,9 +1,8 @@
import * as Terminal from '../node_modules/xterm/dist/xterm';
import * as attach from '../node_modules/xterm/dist/addons/attach/attach';
import * as fit from '../node_modules/xterm/dist/addons/fit/fit';
import * as winptyCompat from '../node_modules/xterm/dist/addons/winptyCompat/winptyCompat';
import * as search from '../node_modules/xterm/dist/addons/search/search';
import { Terminal } from "xterm";
import * as attach from "xterm/lib/addons/attach/attach";
import * as fit from "xterm/lib/addons/fit/fit";
import * as winptyCompat from "xterm/lib/addons/winptyCompat/winptyCompat";
import * as search from "xterm/lib/addons/search/search";
Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
@@ -126,10 +125,13 @@ function attachTerminal() {
function connectToWebSocket(url) {
socket = new WebSocket(url);
socket.onopen = attachTerminal;
socket.onclose = function() {
terminalBridge.invokeCommand('CloseTab');
socket.onerror = function(event) {
terminalBridge.reportError(`Socket error: ${JSON.stringify(event)}`);
}
socket.onclose = function(event) {
terminalBridge.reportError(`Socket closed: ${JSON.stringify(event)}`);
};
socket.onopen = attachTerminal;
}
function changeTheme(theme) {
@@ -17,5 +17,6 @@ namespace FluentTerminal.Models
public bool ShowNewOutputIndicator { get; set; }
public bool EnableTrayIcon { get; set; }
public bool AlwaysUseWinPty { get; set; }
public bool ShowCustomTitleInTitlebar { get; set; }
}
}
@@ -7,6 +7,9 @@ namespace FluentTerminal.Models.Enums
[Description("Do not modify")]
DoNotModify,
[Description("Convert to CR")]
ToCR,
[Description("Convert to CRLF")]
ToCRLF,
+12
View File
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluentTerminal.Models
{
public class FontInfo
{
public string Name { get; set; }
public bool IsMonospaced { get; set; }
}
}
@@ -4,6 +4,7 @@ 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; }
@@ -3,6 +3,6 @@
public class DisplayTerminalOutputRequest
{
public int TerminalId { get; set; }
public string Output { get; set; }
public byte[] Output { get; set; }
}
}
@@ -0,0 +1,6 @@
namespace FluentTerminal.Models.Requests
{
public class GetAvailablePortRequest
{
}
}
@@ -1,9 +1,9 @@
namespace FluentTerminal.Models.Requests
{
public class WriteTextRequest
public class WriteDataRequest
{
public int TerminalId { get; set; }
public string Text { get; set; }
public byte[] Data { get; set; }
}
}
@@ -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; }
}
}
+2
View File
@@ -45,6 +45,8 @@ namespace FluentTerminal.Models
{
switch (LineEndingTranslation)
{
case LineEndingStyle.ToCR:
return NewlinePattern.Replace(content, "\r");
case LineEndingStyle.ToCRLF:
return NewlinePattern.Replace(content, "\r\n");
case LineEndingStyle.ToLF:
+3 -3
View File
@@ -27,9 +27,9 @@ namespace FluentTerminal.Models
{
if (obj is TerminalTheme other)
{
return other.Name.Equals(Name)
&& other.Author == null ? Author == null : other.Author.Equals(Author)
&& other.Colors.Equals(Colors);
return Equals(other.Name, Name)
&& Equals(other.Author, Author)
&& Equals(other.Colors, Colors);
}
return false;
}
@@ -108,13 +108,13 @@
</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>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.1.5</Version>
<Version>6.2.8</Version>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
@@ -2,7 +2,7 @@
namespace FluentTerminal.RuntimeComponent.Interfaces
{
public interface ITerminalEventListener
public interface IxtermEventListener
{
void OnTerminalResized(int columns, int rows);
@@ -13,5 +13,7 @@ namespace FluentTerminal.RuntimeComponent.Interfaces
void OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection);
void OnSelectionChanged(string selection);
void OnError(string error);
}
}
@@ -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;
}
@@ -43,5 +43,10 @@ namespace FluentTerminal.RuntimeComponent.WebAllowedObjects
{
_terminalEventListener.OnSelectionChanged(selection);
}
public void ReportError(string error)
{
_terminalEventListener.OnError(error);
}
}
}
@@ -12,6 +12,7 @@
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -112,6 +113,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Native\ProcessApi.cs" />
<Compile Include="Native\WindowApi.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
@@ -160,7 +162,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac">
<Version>4.8.1</Version>
<Version>4.9.1</Version>
</PackageReference>
<PackageReference Include="GlobalHotKey">
<Version>1.1.0</Version>
@@ -0,0 +1,11 @@
using System;
using System.Runtime.InteropServices;
namespace FluentTerminal.SystemTray.Native
{
public static class ProcessApi
{
[DllImport("kernel32.dll")]
public static extern int GetProcessId(IntPtr handle);
}
}
+34 -1
View File
@@ -4,7 +4,11 @@ using FluentTerminal.App.Services.Adapters;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.SystemTray.Services;
using GlobalHotKey;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -27,6 +31,17 @@ namespace FluentTerminal.SystemTray
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(typeof(CamelCaseNamingStrategy)));
return settings;
};
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
@@ -42,7 +57,7 @@ namespace FluentTerminal.SystemTray
containerBuilder.RegisterType<NotificationService>().As<INotificationService>().InstancePerDependency();
containerBuilder.RegisterType<TerminalsManager>().SingleInstance();
containerBuilder.RegisterType<ToggleWindowService>().SingleInstance();
containerBuilder.RegisterType<HotKeyManager>().SingleInstance();
containerBuilder.RegisterInstance(new HotKeyManager()).SingleInstance();
containerBuilder.RegisterType<SystemTrayApplicationContext>().SingleInstance();
containerBuilder.RegisterType<AppCommunicationService>().SingleInstance();
containerBuilder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>();
@@ -52,6 +67,19 @@ namespace FluentTerminal.SystemTray
var container = containerBuilder.Build();
Task.Run(async () =>
{
var logDirectory = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("Logs", CreationCollisionOption.OpenIfExists).AsTask().ConfigureAwait(true);
var logFile = Path.Combine(logDirectory.Path, "fluentterminal.systemtray.log");
var configFile = await logDirectory.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists).AsTask().ConfigureAwait(true);
var configContent = await FileIO.ReadTextAsync(configFile).AsTask().ConfigureAwait(true);
var config = JsonConvert.DeserializeObject<Logger.Configuration>(configContent) ?? new Logger.Configuration();
Logger.Instance.Initialize(logFile, config);
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
});
var appCommunicationService = container.Resolve<AppCommunicationService>();
if (args.Length > 0 && args[2] == "appLaunched")
@@ -79,5 +107,10 @@ namespace FluentTerminal.SystemTray
eventWaitHandle.Set();
}
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Logger.Instance.Error((Exception)e.ExceptionObject, "Unhandled Exception");
}
}
}
@@ -7,6 +7,8 @@ using Windows.ApplicationModel;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using System;
using FluentTerminal.Models.Responses;
using FluentTerminal.App.Services;
namespace FluentTerminal.SystemTray.Services
{
@@ -44,7 +46,7 @@ namespace FluentTerminal.SystemTray.Services
TerminalId = e
};
_appServiceConnection.SendMessageAsync(CreateMessage(request));
_appServiceConnection?.SendMessageAsync(CreateMessage(request));
}
private void _terminalsManager_DisplayOutputRequested(object sender, DisplayTerminalOutputRequest e)
@@ -83,8 +85,13 @@ namespace FluentTerminal.SystemTray.Services
var deferral = args.GetDeferral();
var request = JsonConvert.DeserializeObject<CreateTerminalRequest>(messageContent);
Logger.Instance.Debug("Received CreateTerminalRequest: {@request}", request);
var response = _terminalsManager.CreateTerminal(request);
Logger.Instance.Debug("Sending CreateTerminalResponse: {@response}", response);
await args.Request.SendResponseAsync(CreateMessage(response));
deferral.Complete();
@@ -101,16 +108,26 @@ namespace FluentTerminal.SystemTray.Services
_toggleWindowService.SetHotKeys(request.KeyBindings);
}
else if (messageType == nameof(WriteTextRequest))
else if (messageType == nameof(WriteDataRequest))
{
var request = JsonConvert.DeserializeObject<WriteTextRequest>(messageContent);
_terminalsManager.WriteText(request.TerminalId, request.Text);
var request = JsonConvert.DeserializeObject<WriteDataRequest>(messageContent);
_terminalsManager.Write(request.TerminalId, request.Data);
}
else if (messageType == nameof(TerminalExitedRequest))
{
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)
@@ -1,8 +1,8 @@
using System;
using FluentTerminal.Models;
using FluentTerminal.Models.Requests;
using System;
using System.IO;
using System.Threading.Tasks;
using FluentTerminal.Models;
using FluentTerminal.Models.Requests;
namespace FluentTerminal.SystemTray.Services.ConPty
{
@@ -10,8 +10,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty
{
private TerminalsManager _terminalsManager;
private Terminal _terminal;
private bool _exited;
public int Id { get; private set; }
public string ShellExecutableName { get; private set; }
public event EventHandler ConnectionClosed;
@@ -28,28 +30,31 @@ namespace FluentTerminal.SystemTray.Services.ConPty
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
{
Id = request.Id;
_terminalsManager = terminalsManager;
var port = Utilities.GetAvailablePort();
if (port == null)
{
throw new Exception("no port available");
}
Id = port.Value;
ShellExecutableName = Path.GetFileNameWithoutExtension(request.Profile.Location);
var cwd = GetWorkingDirectory(request.Profile);
string args = $"\"{request.Profile.Location}\" {request.Profile.Arguments}";
var args = string.Empty;
if (!string.IsNullOrWhiteSpace(request.Profile.Location))
{
args = $"\"{request.Profile.Location}\" {request.Profile.Arguments}";
}
else
{
args = 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));
Task.Run(() => _terminal.Start(args, cwd, terminalsManager.GetDefaultEnvironmentVariableString(), request.Size.Columns, request.Size.Rows));
}
private void _terminal_Exited(object sender, EventArgs e)
{
_exited = true;
Close();
}
@@ -69,34 +74,34 @@ namespace FluentTerminal.SystemTray.Services.ConPty
private void ListenToStdOut()
{
Task.Run(async () =>
Task.Factory.StartNew(async () =>
{
var reader = new StreamReader(_terminal.ConsoleOutStream);
do
using (var reader = new StreamReader(_terminal.ConsoleOutStream))
{
var offset = 0;
var buffer = new char[1024];
var readChars = await reader.ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
if (readChars > 0)
do
{
var output = new String(buffer, 0, readChars);
_terminalsManager.DisplayTerminalOutput(Id, output);
var buffer = new byte[1024];
var readBytes = await _terminal.ConsoleOutStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
var read = new byte[readBytes];
Buffer.BlockCopy(buffer, 0, read, 0, readBytes);
if (readBytes > 0)
{
_terminalsManager.DisplayTerminalOutput(Id, read);
}
}
while (!_exited);
}
while (!reader.EndOfStream);
});
}, TaskCreationOptions.LongRunning);
}
public void WriteText(string text)
public void Write(byte[] data)
{
_terminal.WriteToPseudoConsole(text);
_terminal.WriteToPseudoConsole(data);
}
#region IDisposable Support
private bool disposedValue = false;
private void Dispose(bool disposing)
@@ -114,8 +119,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty
public void Dispose()
{
_terminal.Exited -= _terminal_Exited;
Dispose(true);
}
#endregion
#endregion IDisposable Support
}
}
}
@@ -1,4 +1,5 @@
using System;
using FluentTerminal.App.Services;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using static FluentTerminal.SystemTray.Services.ConPty.Native.ProcessApi;
@@ -16,10 +17,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty.Processes
/// <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)
internal static Process Start(string command, string directory, string environment, IntPtr attributes, IntPtr hPC)
{
var startupInfo = ConfigureProcessThread(hPC, attributes);
var processInfo = RunProcess(ref startupInfo, command, directory);
var processInfo = RunProcess(ref startupInfo, command, directory, environment);
return new Process(startupInfo, processInfo);
}
@@ -71,8 +72,9 @@ namespace FluentTerminal.SystemTray.Services.ConPty.Processes
return startupInfo;
}
private static PROCESS_INFORMATION RunProcess(ref STARTUPINFOEX sInfoEx, string commandLine, string directory)
private static PROCESS_INFORMATION RunProcess(ref STARTUPINFOEX sInfoEx, string commandLine, string directory, string environment)
{
var lpEnvironment = Marshal.StringToHGlobalAnsi(environment);
int securityAttributeSize = Marshal.SizeOf<SECURITY_ATTRIBUTES>();
var pSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
var tSec = new SECURITY_ATTRIBUTES { nLength = securityAttributeSize };
@@ -83,16 +85,20 @@ namespace FluentTerminal.SystemTray.Services.ConPty.Processes
lpThreadAttributes: ref tSec,
bInheritHandles: false,
dwCreationFlags: EXTENDED_STARTUPINFO_PRESENT,
lpEnvironment: IntPtr.Zero,
lpEnvironment: lpEnvironment,
lpCurrentDirectory: directory,
lpStartupInfo: ref sInfoEx,
lpProcessInformation: out PROCESS_INFORMATION pInfo
);
if (!success)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not create process.");
var errorCode = Marshal.GetLastWin32Error();
Logger.Instance.Error("RunProcess: Could not create Process. Win32 Error: {@errorCode}", errorCode);
Marshal.FreeHGlobal(lpEnvironment);
throw new Win32Exception(errorCode, "Could not create process.");
}
Marshal.FreeHGlobal(lpEnvironment);
return pInfo;
}
}
@@ -16,7 +16,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty
public sealed class Terminal : IDisposable
{
private SafeFileHandle _consoleInputPipeWriteHandle;
private StreamWriter _consoleInputWriter;
private FileStream _consoleInputWriter;
private PseudoConsolePipe _inputPipe;
private PseudoConsolePipe _outputPipe;
private PseudoConsole _pseudoConsole;
@@ -75,13 +75,13 @@ namespace FluentTerminal.SystemTray.Services.ConPty
/// <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)
public void Start(string command, string directory, string environment, 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))
using (var process = ProcessFactory.Start(command, directory, environment, 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);
@@ -89,10 +89,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty
// Store input pipe handle, and a writer for later reuse
_consoleInputPipeWriteHandle = _inputPipe.WriteSide;
_consoleInputWriter = new StreamWriter(new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write))
{
AutoFlush = true
};
_consoleInputWriter = new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write);
// 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));
@@ -110,14 +107,10 @@ namespace FluentTerminal.SystemTray.Services.ConPty
/// <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)
public void WriteToPseudoConsole(byte[] data)
{
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);
_consoleInputWriter.Write(data, 0, data.Length);
_consoleInputWriter.Flush();
}
/// <summary>
@@ -13,7 +13,7 @@ namespace FluentTerminal.SystemTray.Services
void Close();
void Resize(TerminalSize size);
void WriteText(string text);
void Write(byte[] data);
void Start(CreateTerminalRequest request, TerminalsManager terminalsManager);
}
}
@@ -6,8 +6,11 @@ using FluentTerminal.Models.Responses;
using FluentTerminal.SystemTray.Services.ConPty;
using FluentTerminal.SystemTray.Services.WinPty;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Windows.ApplicationModel;
namespace FluentTerminal.SystemTray.Services
{
@@ -25,7 +28,7 @@ namespace FluentTerminal.SystemTray.Services
_settingsService = settingsService;
}
public void DisplayTerminalOutput(int terminalId, string output)
public void DisplayTerminalOutput(int terminalId, byte[] output)
{
DisplayOutputRequested?.Invoke(this, new DisplayTerminalOutputRequest
{
@@ -36,6 +39,16 @@ namespace FluentTerminal.SystemTray.Services
public CreateTerminalResponse CreateTerminal(CreateTerminalRequest request)
{
if (_terminals.ContainsKey(request.Id))
{
// App terminated without cleaning up, removing orphaned sessions
foreach (var item in _terminals.Values)
{
item.Dispose();
}
_terminals.Clear();
}
ITerminalSession terminal = null;
try
{
@@ -59,16 +72,15 @@ namespace FluentTerminal.SystemTray.Services
return new CreateTerminalResponse
{
Success = true,
Id = terminal.Id,
ShellExecutableName = terminal.ShellExecutableName
};
}
public void WriteText(int id, string text)
public void Write(int id, byte[] data)
{
if (_terminals.TryGetValue(id, out ITerminalSession terminal))
{
terminal.WriteText(text);
terminal.Write(data);
}
}
@@ -89,10 +101,28 @@ namespace FluentTerminal.SystemTray.Services
if (_terminals.TryGetValue(id, out ITerminalSession terminal))
{
_terminals.Remove(terminal.Id);
terminal.Dispose();
terminal.Close();
}
}
public string GetDefaultEnvironmentVariableString()
{
var environmentVariables = Environment.GetEnvironmentVariables();
environmentVariables["TERM"] = "xterm-256color";
environmentVariables["TERM_PROGRAM"] = "FluentTerminal";
environmentVariables["TERM_PROGRAM_VERSION"] = $"{Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}.{Package.Current.Id.Version.Build}.{Package.Current.Id.Version.Revision}";
var builder = new StringBuilder();
foreach (DictionaryEntry item in environmentVariables)
{
builder.Append(item.Key).Append("=").Append(item.Value).Append("\0");
}
builder.Append('\0');
return builder.ToString();
}
private void OnTerminalConnectionClosed(object sender, System.EventArgs e)
{
if (sender is ITerminalSession terminal)
@@ -103,4 +133,4 @@ namespace FluentTerminal.SystemTray.Services
}
}
}
}
}
@@ -1,10 +1,15 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Requests;
using FluentTerminal.SystemTray.Native;
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using static winpty.WinPty;
namespace FluentTerminal.SystemTray.Services.WinPty
@@ -16,9 +21,12 @@ namespace FluentTerminal.SystemTray.Services.WinPty
private Stream _stdin;
private Stream _stdout;
private TerminalsManager _terminalsManager;
private Process _shellProcess;
private bool _exited;
public void Start(CreateTerminalRequest request, TerminalsManager terminalsManager)
{
Id = request.Id;
_terminalsManager = terminalsManager;
var configHandle = IntPtr.Zero;
@@ -36,13 +44,18 @@ namespace FluentTerminal.SystemTray.Services.WinPty
throw new Exception(winpty_error_msg(errorHandle));
}
string exe = request.Profile.Location;
string args = $"\"{exe}\" {request.Profile.Arguments}";
string cwd = GetWorkingDirectory(request.Profile);
spawnConfigHandle = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, exe, args, cwd, null, out errorHandle);
var cwd = GetWorkingDirectory(request.Profile);
var args = request.Profile.Arguments;
if (!string.IsNullOrWhiteSpace(request.Profile.Location))
{
args = $"\"{request.Profile.Location}\" {args}";
}
spawnConfigHandle = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, request.Profile.Location, args, cwd, terminalsManager.GetDefaultEnvironmentVariableString(), out errorHandle);
if (errorHandle != IntPtr.Zero)
{
throw new Exception(winpty_error_msg(errorHandle).ToString());
throw new Exception(winpty_error_msg(errorHandle));
}
_stdin = CreatePipe(winpty_conin_name(_handle), PipeDirection.Out);
@@ -50,10 +63,22 @@ namespace FluentTerminal.SystemTray.Services.WinPty
if (!winpty_spawn(_handle, spawnConfigHandle, out IntPtr process, out IntPtr thread, out int procError, out errorHandle))
{
throw new Exception($"Failed to start the shell process. Please check your shell settings.\nTried to start: {args}");
throw new Exception($@"Failed to start the shell process. Please check your shell settings.\nTried to start: {request.Profile.Location} ""{request.Profile.Arguments}""");
}
ShellExecutableName = Path.GetFileNameWithoutExtension(exe);
var shellProcessId = ProcessApi.GetProcessId(process);
_shellProcess = Process.GetProcessById(shellProcessId);
_shellProcess.EnableRaisingEvents = true;
_shellProcess.Exited += _shellProcess_Exited;
if (!string.IsNullOrWhiteSpace(request.Profile.Location))
{
ShellExecutableName = Path.GetFileNameWithoutExtension(request.Profile.Location);
}
else
{
ShellExecutableName = request.Profile.Arguments.Split(' ')[0];
}
}
finally
{
@@ -62,23 +87,22 @@ namespace FluentTerminal.SystemTray.Services.WinPty
winpty_error_free(errorHandle);
}
var port = Utilities.GetAvailablePort();
if (port == null)
{
throw new Exception("no port available");
}
Id = port.Value;
ListenToStdOut();
}
private void _shellProcess_Exited(object sender, EventArgs e)
{
Close();
_exited = true;
}
~WinPtySession()
{
Dispose(false);
}
private string GetWorkingDirectory(ShellProfile configuration)
{
if (string.IsNullOrWhiteSpace(configuration.WorkingDirectory) || !Directory.Exists(configuration.WorkingDirectory))
@@ -96,6 +120,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty
public void Dispose()
{
_shellProcess.Exited -= _shellProcess_Exited;
Dispose(true);
GC.SuppressFinalize(this);
}
@@ -105,10 +130,9 @@ namespace FluentTerminal.SystemTray.Services.WinPty
ConnectionClosed?.Invoke(this, EventArgs.Empty);
}
public void WriteText(string text)
public void Write(byte[] data)
{
var bytes = Encoding.UTF8.GetBytes(text);
_stdin.Write(bytes, 0, bytes.Length);
_stdin.Write(data, 0, data.Length);
}
public void Resize(TerminalSize size)
@@ -119,7 +143,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty
winpty_set_size(_handle, size.Columns, size.Rows, out errorHandle);
if (errorHandle != IntPtr.Zero)
{
throw new Exception(winpty_error_msg(errorHandle).ToString());
throw new Exception(winpty_error_msg(errorHandle));
}
}
finally
@@ -168,25 +192,25 @@ namespace FluentTerminal.SystemTray.Services.WinPty
private void ListenToStdOut()
{
Task.Run(async () =>
Task.Factory.StartNew(async () =>
{
var reader = new StreamReader(_stdout);
do
using (var reader = new StreamReader(_stdout))
{
var offset = 0;
var buffer = new char[1024];
var readChars = await reader.ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
if (readChars > 0)
do
{
var output = new System.String(buffer, 0, readChars);
_terminalsManager.DisplayTerminalOutput(Id, output);
var buffer = new byte[1024];
var readBytes = await _stdout.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
var read = new byte[readBytes];
Buffer.BlockCopy(buffer, 0, read, 0, readBytes);
if (readBytes > 0)
{
_terminalsManager.DisplayTerminalOutput(Id, read);
}
}
while (!_exited);
}
while (!reader.EndOfStream);
Close();
});
}, TaskCreationOptions.LongRunning);
}
}
}
+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;
}
}
+3 -3
View File
@@ -497,19 +497,19 @@ function InstallContextMenuIntegration
-RegistryPath "hkcu:\Software\Classes\Directory\shell\Open Fluent Terminal here\command"`
-Name "(Default)"`
-Type "ExpandString"`
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new %1"
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new `"%1`""
ForceAddRegKeyValue `
-RegistryPath "hkcu:\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command"`
-Name "(Default)"`
-Type "ExpandString"`
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new %V"
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new `"%V`""
ForceAddRegKeyValue `
-RegistryPath "hkcu:\Software\Classes\Directory\LibraryFolder\shell\Open Fluent Terminal here\command"`
-Name "(Default)"`
-Type "ExpandString"`
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new %V"
-Value "`"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe`" new `"%V`""
}
#
+2 -1
View File
@@ -26,7 +26,7 @@ A Terminal Emulator based on UWP and web technologies.
## Up Next
- Launch shell profile with a custom defined keybinding
- ~~Launch shell profile with a custom defined keybinding~~
- Copy&Paste options
- Improved tabs
- Split screen support
@@ -38,6 +38,7 @@ A Terminal Emulator based on UWP and web technologies.
### Chocolatey package manager installation
- In **Settings**, click **For Developers**, and select **Developer mode**.
- Install [Chocolatey](https://chocolatey.org/)
- From an elevated/admin shell, execute `choco install fluent-terminal`